123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361 |
- #!/usr/bin/env python
- # Copyright 2024 Google LLC
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- from __future__ import with_statement
- from string import Template
- import re, fnmatch, os
- VERSION = "0.10.0"
- TEST_FUNC_REGEX = r"^(void\s+(%s__(\w+))\(\s*void\s*\))\s*\{"
- EVENT_CB_REGEX = re.compile(
- r"^(void\s+clar_on_(\w+)\(\s*void\s*\))\s*\{",
- re.MULTILINE)
- SKIP_COMMENTS_REGEX = re.compile(
- r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"',
- re.DOTALL | re.MULTILINE)
- CATEGORY_REGEX = re.compile(r"CL_IN_CATEGORY\(\s*\"([^\"]+)\"\s*\)")
- CLAR_HEADER = """
- /*
- * Clar v%s
- *
- * This is an autogenerated file. Do not modify.
- * To add new unit tests or suites, regenerate the whole
- * file with `./clar`
- */
- """ % VERSION
- CLAR_EVENTS = [
- 'init',
- 'shutdown',
- 'test',
- 'suite'
- ]
- def main():
- from optparse import OptionParser
- parser = OptionParser()
- parser.add_option('-c', '--clar-path', dest='clar_path')
- parser.add_option('-v', '--report-to', dest='print_mode', default='default')
- parser.add_option('-f', '--file', dest='file')
- options, args = parser.parse_args()
- folder = args[0] or '.'
- print('folder: %s' % folder)
- builder = ClarTestBuilder(folder,
- clar_path = options.clar_path,
- print_mode = options.print_mode)
- if options.file is not None:
- builder.load_file(options.file)
- else:
- builder.load_dir(folder)
- builder.render()
- class ClarTestBuilder:
- def __init__(self, path, clar_path = None, print_mode = 'default'):
- self.declarations = []
- self.suite_names = []
- self.callback_data = {}
- self.suite_data = {}
- self.category_data = {}
- self.event_callbacks = []
- self.clar_path = os.path.abspath(clar_path) if clar_path else None
- self.path = os.path.abspath(path)
- self.modules = [
- "clar_sandbox.c",
- "clar_fixtures.c",
- "clar_fs.c",
- "clar_mock.c",
- "clar_categorize.c",
- ]
- self.modules.append("clar_print_%s.c" % print_mode)
- def load_dir(self, folder):
- print("Loading test suites...")
- for root, dirs, 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)
- test_name = "_".join(module_root + [test_file[:-2]])
- with open(full_path) as f:
- self._process_test_file(test_name, f.read())
- def load_file(self, filename):
- with open(filename) as f:
- test_name = os.path.basename(filename)[:-2]
- self._process_test_file(test_name, f.read())
- def render(self):
- if not self.suite_data:
- raise RuntimeError('No tests found under "%s"' % self.path)
- if not os.path.isdir(self.path):
- os.makedirs(self.path)
- main_file = os.path.join(self.path, 'clar_main.c')
- with open(main_file, "w") as out:
- out.write(self._render_main())
- header_file = os.path.join(self.path, 'clar.h')
- with open(header_file, "w") as out:
- out.write(self._render_header())
- print ('Written Clar suite to "%s"' % self.path)
- #####################################################
- # Internal methods
- #####################################################
- def _render_cb(self, cb):
- return '{"%s", &%s}' % (cb['short_name'], cb['symbol'])
- def _render_suite(self, suite, index):
- template = Template(
- r"""
- {
- ${suite_index},
- "${clean_name}",
- ${initialize},
- ${cleanup},
- ${categories},
- ${cb_ptr}, ${cb_count}
- }
- """)
- callbacks = {}
- for cb in ['initialize', 'cleanup']:
- callbacks[cb] = (self._render_cb(suite[cb])
- if suite[cb] else "{NULL, NULL}")
- if len(self.category_data[suite['name']]) > 0:
- cats = "_clar_cat_%s" % suite['name']
- else:
- cats = "NULL"
- return template.substitute(
- suite_index = index,
- clean_name = suite['name'].replace("_", "::"),
- initialize = callbacks['initialize'],
- cleanup = callbacks['cleanup'],
- categories = cats,
- cb_ptr = "_clar_cb_%s" % suite['name'],
- cb_count = suite['cb_count']
- ).strip()
- def _render_callbacks(self, suite_name, callbacks):
- template = Template(
- r"""
- static const struct clar_func _clar_cb_${suite_name}[] = {
- ${callbacks}
- };
- """)
- callbacks = [
- self._render_cb(cb)
- for cb in callbacks
- if cb['short_name'] not in ('initialize', 'cleanup')
- ]
- return template.substitute(
- suite_name = suite_name,
- callbacks = ",\n\t".join(callbacks)
- ).strip()
- def _render_categories(self, suite_name, categories):
- template = Template(
- r"""
- static const char *_clar_cat_${suite_name}[] = { "${categories}", NULL };
- """)
- if len(categories) > 0:
- return template.substitute(
- suite_name = suite_name,
- categories = '","'.join(categories)
- ).strip()
- else:
- return ""
- def _render_event_overrides(self):
- overrides = []
- for event in CLAR_EVENTS:
- if event in self.event_callbacks:
- continue
- overrides.append(
- "#define clar_on_%s() /* nop */" % event
- )
- return '\n'.join(overrides)
- def _render_header(self):
- template = Template(self._load_file('clar.h'))
- declarations = "\n".join(
- "extern %s;" % decl
- for decl in sorted(self.declarations)
- )
- return template.substitute(
- extern_declarations = declarations,
- )
- def _render_main(self):
- template = Template(self._load_file('clar.c'))
- suite_names = sorted(self.suite_names)
- suite_data = [
- self._render_suite(self.suite_data[s], i)
- for i, s in enumerate(suite_names)
- ]
- callbacks = [
- self._render_callbacks(s, self.callback_data[s])
- for s in suite_names
- ]
- callback_count = sum(
- len(cbs) for cbs in self.callback_data.values()
- )
- categories = [
- self._render_categories(s, self.category_data[s])
- for s in suite_names
- ]
- return template.substitute(
- clar_modules = self._get_modules(),
- clar_callbacks = "\n".join(callbacks),
- clar_categories = "".join(categories),
- clar_suites = ",\n\t".join(suite_data),
- clar_suite_count = len(suite_data),
- clar_callback_count = callback_count,
- clar_event_overrides = self._render_event_overrides(),
- )
- def _load_file(self, filename):
- if self.clar_path:
- filename = os.path.join(self.clar_path, filename)
- with open(filename) as cfile:
- return cfile.read()
- else:
- import zlib, base64, sys
- content = CLAR_FILES[filename]
- if sys.version_info >= (3, 0):
- content = bytearray(content, 'utf_8')
- content = base64.b64decode(content)
- content = zlib.decompress(content)
- return str(content, 'utf-8')
- else:
- content = base64.b64decode(content)
- return zlib.decompress(content)
- def _get_modules(self):
- return "\n".join(self._load_file(f) for f in self.modules)
- def _skip_comments(self, text):
- def _replacer(match):
- s = match.group(0)
- return "" if s.startswith('/') else s
- return re.sub(SKIP_COMMENTS_REGEX, _replacer, text)
- def _process_test_file(self, suite_name, contents):
- contents = self._skip_comments(contents)
- self._process_events(contents)
- self._process_declarations(suite_name, contents)
- self._process_categories(suite_name, contents)
- def _process_events(self, contents):
- for (decl, event) in EVENT_CB_REGEX.findall(contents):
- if event not in CLAR_EVENTS:
- continue
- self.declarations.append(decl)
- self.event_callbacks.append(event)
- def _process_declarations(self, suite_name, contents):
- callbacks = []
- initialize = cleanup = None
- regex_string = TEST_FUNC_REGEX % suite_name
- regex = re.compile(regex_string, re.MULTILINE)
- for (declaration, symbol, short_name) in regex.findall(contents):
- data = {
- "short_name" : short_name,
- "declaration" : declaration,
- "symbol" : symbol
- }
- if short_name == 'initialize':
- initialize = data
- elif short_name == 'cleanup':
- cleanup = data
- else:
- callbacks.append(data)
- if not callbacks:
- return
- tests_in_suite = len(callbacks)
- suite = {
- "name" : suite_name,
- "initialize" : initialize,
- "cleanup" : cleanup,
- "cb_count" : tests_in_suite
- }
- if initialize:
- self.declarations.append(initialize['declaration'])
- if cleanup:
- self.declarations.append(cleanup['declaration'])
- self.declarations += [
- callback['declaration']
- for callback in callbacks
- ]
- callbacks.sort(key=lambda x: x['short_name'])
- self.callback_data[suite_name] = callbacks
- self.suite_data[suite_name] = suite
- self.suite_names.append(suite_name)
- print(" %s (%d tests)" % (suite_name, tests_in_suite))
- def _process_categories(self, suite_name, contents):
- self.category_data[suite_name] = [
- cat for cat in CATEGORY_REGEX.findall(contents) ]
|