generate.py 8.16 KB
Newer Older
Vicent Marti committed
1
#!/usr/bin/env python
Russell Belfer committed
2 3 4 5 6 7
#
# Copyright (c) Vicent Marti. All rights reserved.
#
# This file is part of clar, distributed under the ISC license.
# For full terms see the included COPYING file.
#
Vicent Marti committed
8 9 10

from __future__ import with_statement
from string import Template
11
import re, fnmatch, os, sys, codecs, pickle
Vicent Marti committed
12 13 14 15 16 17 18 19

class Module(object):
    class Template(object):
        def __init__(self, module):
            self.module = module

        def _render_callback(self, cb):
            if not cb:
Russell Belfer committed
20 21
                return '    { NULL, NULL }'
            return '    { "%s", &%s }' % (cb['short_name'], cb['symbol'])
Vicent Marti committed
22 23 24

    class DeclarationTemplate(Template):
        def render(self):
Russell Belfer committed
25
            out = "\n".join("extern %s;" % cb['declaration'] for cb in self.module.callbacks) + "\n"
Vicent Marti committed
26

27 28
            for initializer in self.module.initializers:
                out += "extern %s;\n" % initializer['declaration']
Vicent Marti committed
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43

            if self.module.cleanup:
                out += "extern %s;\n" % self.module.cleanup['declaration']

            return out

    class CallbacksTemplate(Template):
        def render(self):
            out = "static const struct clar_func _clar_cb_%s[] = {\n" % self.module.name
            out += ",\n".join(self._render_callback(cb) for cb in self.module.callbacks)
            out += "\n};\n"
            return out

    class InfoTemplate(Template):
        def render(self):
44 45 46 47 48 49 50 51 52 53 54 55 56
            templates = []

            initializers = self.module.initializers
            if len(initializers) == 0:
                initializers = [ None ]

            for initializer in initializers:
                name = self.module.clean_name()
                if initializer and initializer['short_name'].startswith('initialize_'):
                    variant = initializer['short_name'][len('initialize_'):]
                    name += " (%s)" % variant.replace('_', ' ')

                template = Template(
Russell Belfer committed
57 58 59 60 61 62 63
            r"""
    {
        "${clean_name}",
    ${initialize},
    ${cleanup},
        ${cb_ptr}, ${cb_count}, ${enabled}
    }"""
64 65 66 67 68 69 70 71 72 73 74
                ).substitute(
                    clean_name = name,
                    initialize = self._render_callback(initializer),
                    cleanup = self._render_callback(self.module.cleanup),
                    cb_ptr = "_clar_cb_%s" % self.module.name,
                    cb_count = len(self.module.callbacks),
                    enabled = int(self.module.enabled)
                )
                templates.append(template)

            return ','.join(templates)
Vicent Marti committed
75 76 77

    def __init__(self, name):
        self.name = name
Russell Belfer committed
78 79

        self.mtime = 0
Vicent Marti committed
80
        self.enabled = True
Russell Belfer committed
81
        self.modified = False
Vicent Marti committed
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97

    def clean_name(self):
        return self.name.replace("_", "::")

    def _skip_comments(self, text):
        SKIP_COMMENTS_REGEX = re.compile(
            r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"',
            re.DOTALL | re.MULTILINE)

        def _replacer(match):
            s = match.group(0)
            return "" if s.startswith('/') else s

        return re.sub(SKIP_COMMENTS_REGEX, _replacer, text)

    def parse(self, contents):
Linquize committed
98
        TEST_FUNC_REGEX = r"^(void\s+(test_%s__(\w+))\s*\(\s*void\s*\))\s*\{"
Vicent Marti committed
99 100 101 102 103

        contents = self._skip_comments(contents)
        regex = re.compile(TEST_FUNC_REGEX % self.name, re.MULTILINE)

        self.callbacks = []
104
        self.initializers = []
Vicent Marti committed
105 106 107 108 109 110 111 112 113
        self.cleanup = None

        for (declaration, symbol, short_name) in regex.findall(contents):
            data = {
                "short_name" : short_name,
                "declaration" : declaration,
                "symbol" : symbol
            }

114 115
            if short_name.startswith('initialize'):
                self.initializers.append(data)
Vicent Marti committed
116 117 118 119 120 121 122
            elif short_name == 'cleanup':
                self.cleanup = data
            else:
                self.callbacks.append(data)

        return self.callbacks != []

Russell Belfer committed
123 124 125
    def refresh(self, path):
        self.modified = False

Vicent Marti committed
126
        try:
Russell Belfer committed
127 128 129 130 131 132 133 134 135
            st = os.stat(path)

            # Not modified
            if st.st_mtime == self.mtime:
                return True

            self.modified = True
            self.mtime = st.st_mtime

Linquize committed
136
            with codecs.open(path, encoding='utf-8') as fp:
Russell Belfer committed
137 138
                raw_content = fp.read()

Vicent Marti committed
139 140 141
        except IOError:
            return False

Russell Belfer committed
142 143
        return self.parse(raw_content)

Vicent Marti committed
144
class TestSuite(object):
Russell Belfer committed
145

146
    def __init__(self, path, output):
Vicent Marti committed
147
        self.path = path
148
        self.output = output
Vicent Marti committed
149

Russell Belfer committed
150 151 152 153 154 155 156 157 158
    def should_generate(self, path):
        if not os.path.isfile(path):
            return True

        if any(module.modified for module in self.modules.values()):
            return True

        return False

Vicent Marti committed
159 160 161 162 163 164 165 166 167 168
    def find_modules(self):
        modules = []
        for root, _, files in os.walk(self.path):
            module_root = root[len(self.path):]
            module_root = [c for c in module_root.split(os.sep) if c]

            tests_in_module = fnmatch.filter(files, "*.c")

            for test_file in tests_in_module:
                full_path = os.path.join(root, test_file)
Linquize committed
169
                module_name = "_".join(module_root + [test_file[:-2]]).replace("-", "_")
Vicent Marti committed
170 171 172 173 174

                modules.append((full_path, module_name))

        return modules

Russell Belfer committed
175
    def load_cache(self):
176
        path = os.path.join(self.output, '.clarcache')
Russell Belfer committed
177 178 179 180 181 182 183 184 185 186 187 188
        cache = {}

        try:
            fp = open(path, 'rb')
            cache = pickle.load(fp)
            fp.close()
        except (IOError, ValueError):
            pass

        return cache

    def save_cache(self):
189
        path = os.path.join(self.output, '.clarcache')
Russell Belfer committed
190 191 192
        with open(path, 'wb') as cache:
            pickle.dump(self.modules, cache)

Vicent Marti committed
193 194
    def load(self, force = False):
        module_data = self.find_modules()
Russell Belfer committed
195
        self.modules = {} if force else self.load_cache()
Vicent Marti committed
196 197 198 199 200

        for path, name in module_data:
            if name not in self.modules:
                self.modules[name] = Module(name)

Russell Belfer committed
201
            if not self.modules[name].refresh(path):
Vicent Marti committed
202 203 204 205 206 207 208 209 210 211 212
                del self.modules[name]

    def disable(self, excluded):
        for exclude in excluded:
            for module in self.modules.values():
                name = module.clean_name()
                if name.startswith(exclude):
                    module.enabled = False
                    module.modified = True

    def suite_count(self):
213
        return sum(max(1, len(m.initializers)) for m in self.modules.values())
Vicent Marti committed
214 215 216 217 218

    def callback_count(self):
        return sum(len(module.callbacks) for module in self.modules.values())

    def write(self):
219
        output = os.path.join(self.output, 'clar.suite')
Vicent Marti committed
220

Russell Belfer committed
221 222 223
        if not self.should_generate(output):
            return False

Vicent Marti committed
224
        with open(output, 'w') as data:
225 226 227
            modules = sorted(self.modules.values(), key=lambda module: module.name)

            for module in modules:
Vicent Marti committed
228 229 230
                t = Module.DeclarationTemplate(module)
                data.write(t.render())

231
            for module in modules:
Vicent Marti committed
232 233 234 235
                t = Module.CallbacksTemplate(module)
                data.write(t.render())

            suites = "static struct clar_suite _clar_suites[] = {" + ','.join(
236
                Module.InfoTemplate(module).render() for module in modules
Russell Belfer committed
237
            ) + "\n};\n"
Vicent Marti committed
238 239 240

            data.write(suites)

Russell Belfer committed
241 242
            data.write("static const size_t _clar_suite_count = %d;\n" % self.suite_count())
            data.write("static const size_t _clar_callback_count = %d;\n" % self.callback_count())
Vicent Marti committed
243

Linquize committed
244
        self.save_cache()
Russell Belfer committed
245 246
        return True

Vicent Marti committed
247 248 249 250
if __name__ == '__main__':
    from optparse import OptionParser

    parser = OptionParser()
Linquize committed
251
    parser.add_option('-f', '--force', action="store_true", dest='force', default=False)
Vicent Marti committed
252
    parser.add_option('-x', '--exclude', dest='excluded', action='append', default=[])
253
    parser.add_option('-o', '--output', dest='output')
Vicent Marti committed
254 255

    options, args = parser.parse_args()
256 257 258 259 260
    if len(args) > 1:
        print("More than one path given")
        sys.exit(1)

    path = args.pop() if args else '.'
261 262
    output = options.output or path
    suite = TestSuite(path, output)
263 264 265 266
    suite.load(options.force)
    suite.disable(options.excluded)
    if suite.write():
        print("Written `clar.suite` (%d tests in %d suites)" % (suite.callback_count(), suite.suite_count()))
Vicent Marti committed
267