| #!/usr/bin/env python |
| # |
| # 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. |
| # |
| |
| from __future__ import with_statement |
| from string import Template |
| import re, fnmatch, os, codecs, pickle |
| |
| class Module(object): |
| class Template(object): |
| def __init__(self, module): |
| self.module = module |
| |
| def _render_callback(self, cb): |
| if not cb: |
| return ' { NULL, NULL }' |
| return ' { "%s", &%s }' % (cb['short_name'], cb['symbol']) |
| |
| class DeclarationTemplate(Template): |
| def render(self): |
| out = "\n".join("extern %s;" % cb['declaration'] for cb in self.module.callbacks) + "\n" |
| |
| if self.module.initialize: |
| out += "extern %s;\n" % self.module.initialize['declaration'] |
| |
| 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): |
| return Template( |
| r""" |
| { |
| "${clean_name}", |
| ${initialize}, |
| ${cleanup}, |
| ${cb_ptr}, ${cb_count}, ${enabled} |
| }""" |
| ).substitute( |
| clean_name = self.module.clean_name(), |
| initialize = self._render_callback(self.module.initialize), |
| 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) |
| ) |
| |
| def __init__(self, name): |
| self.name = name |
| |
| self.mtime = 0 |
| self.enabled = True |
| self.modified = False |
| |
| 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): |
| TEST_FUNC_REGEX = r"^(void\s+(test_%s__(\w+))\s*\(\s*void\s*\))\s*\{" |
| |
| contents = self._skip_comments(contents) |
| regex = re.compile(TEST_FUNC_REGEX % self.name, re.MULTILINE) |
| |
| self.callbacks = [] |
| self.initialize = None |
| self.cleanup = None |
| |
| for (declaration, symbol, short_name) in regex.findall(contents): |
| data = { |
| "short_name" : short_name, |
| "declaration" : declaration, |
| "symbol" : symbol |
| } |
| |
| if short_name == 'initialize': |
| self.initialize = data |
| elif short_name == 'cleanup': |
| self.cleanup = data |
| else: |
| self.callbacks.append(data) |
| |
| return self.callbacks != [] |
| |
| def refresh(self, path): |
| self.modified = False |
| |
| try: |
| st = os.stat(path) |
| |
| # Not modified |
| if st.st_mtime == self.mtime: |
| return True |
| |
| self.modified = True |
| self.mtime = st.st_mtime |
| |
| with codecs.open(path, encoding='utf-8') as fp: |
| raw_content = fp.read() |
| |
| except IOError: |
| return False |
| |
| return self.parse(raw_content) |
| |
| class TestSuite(object): |
| |
| def __init__(self, path): |
| self.path = path |
| |
| 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 |
| |
| 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) |
| module_name = "_".join(module_root + [test_file[:-2]]).replace("-", "_") |
| |
| modules.append((full_path, module_name)) |
| |
| return modules |
| |
| def load_cache(self): |
| path = os.path.join(self.path, '.clarcache') |
| cache = {} |
| |
| try: |
| fp = open(path, 'rb') |
| cache = pickle.load(fp) |
| fp.close() |
| except (IOError, ValueError): |
| pass |
| |
| return cache |
| |
| def save_cache(self): |
| path = os.path.join(self.path, '.clarcache') |
| with open(path, 'wb') as cache: |
| pickle.dump(self.modules, cache) |
| |
| def load(self, force = False): |
| module_data = self.find_modules() |
| self.modules = {} if force else self.load_cache() |
| |
| for path, name in module_data: |
| if name not in self.modules: |
| self.modules[name] = Module(name) |
| |
| if not self.modules[name].refresh(path): |
| 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): |
| return len(self.modules) |
| |
| def callback_count(self): |
| return sum(len(module.callbacks) for module in self.modules.values()) |
| |
| def write(self): |
| output = os.path.join(self.path, 'clar.suite') |
| |
| if not self.should_generate(output): |
| return False |
| |
| with open(output, 'w') as data: |
| for module in self.modules.values(): |
| t = Module.DeclarationTemplate(module) |
| data.write(t.render()) |
| |
| for module in self.modules.values(): |
| t = Module.CallbacksTemplate(module) |
| data.write(t.render()) |
| |
| suites = "static struct clar_suite _clar_suites[] = {" + ','.join( |
| Module.InfoTemplate(module).render() for module in sorted(self.modules.values(), key=lambda module: module.name) |
| ) + "\n};\n" |
| |
| data.write(suites) |
| |
| 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()) |
| |
| self.save_cache() |
| return True |
| |
| if __name__ == '__main__': |
| from optparse import OptionParser |
| |
| parser = OptionParser() |
| parser.add_option('-f', '--force', action="store_true", dest='force', default=False) |
| parser.add_option('-x', '--exclude', dest='excluded', action='append', default=[]) |
| |
| options, args = parser.parse_args() |
| |
| for path in args or ['.']: |
| suite = TestSuite(path) |
| 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())) |
| |