#!/usr/bin/env python3 # # Script to compile and runs tests. # import glob import itertools as it import os import re import shutil import toml TEST_PATHS = ['tests_'] SUITE_PROLOGUE = """ //////// AUTOGENERATED //////// #include "runners/test_runner.h" #include """ # TODO handle indention implicity? # TODO change cfg to be not by value? maybe not? CASE_PROLOGUE = """ lfs_t lfs; struct lfs_config cfg = *cfg_; """ CASE_EPILOGUE = """ """ # TODO # def testpath(path): # def testcase(path): # def testperm(path): def testsuite(path): name = os.path.basename(path) if name.endswith('.toml'): name = name[:-len('.toml')] return name # TODO move this out in other files def openio(path, mode='r'): if path == '-': if 'r' in mode: return os.fdopen(os.dup(sys.stdin.fileno()), 'r') else: return os.fdopen(os.dup(sys.stdout.fileno()), 'w') else: return open(path, mode) class TestCase: # create a TestCase object from a config def __init__(self, config, args={}): self.name = config.pop('name') self.path = config.pop('path') self.suite = config.pop('suite') self.lineno = config.pop('lineno', None) self.code = config.pop('code') self.code_lineno = config.pop('code_lineno', None) self.permutations = 1 for k in config.keys(): print('warning: in %s, found unused key %r' % (self.id(), k), file=sys.stderr) def id(self): return '%s#%s' % (self.suite, self.name) class TestSuite: # create a TestSuite object from a toml file def __init__(self, path, args={}): self.name = testsuite(path) self.path = path # load toml file and parse test cases with open(self.path) as f: # load tests config = toml.load(f) # find line numbers f.seek(0) case_linenos = [] code_linenos = [] for i, line in enumerate(f): match = re.match( '(?P\[\s*cases\s*\.\s*(?P\w+)\s*\])' + '|(?Pcode\s*=\s*(?:\'\'\'|"""))', line) if match and match.group('case'): case_linenos.append((i+1, match.group('name'))) elif match and match.group('code'): code_linenos.append(i+2) # sort in case toml parsing did not retain order case_linenos.sort() cases = config.pop('cases', []) for (lineno, name), (nlineno, _) in it.zip_longest( case_linenos, case_linenos[1:], fillvalue=(float('inf'), None)): code_lineno = min( (l for l in code_linenos if l >= lineno and l < nlineno), default=None) cases[name]['lineno'] = lineno cases[name]['code_lineno'] = code_lineno self.code = config.pop('code', None) self.code_lineno = min( (l for l in code_linenos if not case_linenos or l < case_linenos[0][0]), default=None) self.cases = [] for name, case in sorted(cases.items(), key=lambda c: c[1].get('lineno')): self.cases.append(TestCase(config={ 'name': name, 'path': path + (':%d' % case['lineno'] if 'lineno' in case else ''), 'suite': self.name, **case})) for k in config.keys(): print('warning: in %s, found unused key %r' % (self.id(), k), file=sys.stderr) def id(self): return self.name def compile(**args): # find .toml files paths = [] for path in args['test_paths']: if os.path.isdir(path): path = path + '/*.toml' for path in glob.glob(path): paths.append(path) if not paths: print('no test suites found in %r?' % args['test_paths']) sys.exit(-1) if not args.get('source'): if len(paths) > 1: print('more than one test suite for compilation? (%r)' % args['test_paths']) sys.exit(-1) # write out a test suite suite = TestSuite(paths[0]) if 'output' in args: with openio(args['output'], 'w') as f: f.write(SUITE_PROLOGUE) f.write('\n') if suite.code is not None: if suite.code_lineno is not None: f.write('#line %d "%s"\n' % (suite.code_lineno, suite.path)) f.write(suite.code) f.write('\n') # create test functions and case structs for case in suite.cases: f.write('void __test__%s__%s(' '__attribute__((unused)) struct lfs_config *cfg_, ' '__attribute__((unused)) uint32_t perm) {\n' % (suite.name, case.name)) f.write(CASE_PROLOGUE) f.write('\n') f.write(4*' '+'// test case %s\n' % case.id()) if case.code_lineno is not None: f.write(4*' '+'#line %d "%s"\n' % (case.code_lineno, suite.path)) f.write(case.code) f.write('\n') f.write(CASE_EPILOGUE) f.write('}\n') f.write('\n') f.write('const struct test_case __test__%s__%s__case = {\n' % (suite.name, case.name)) f.write(4*' '+'.id = "%s",\n' % case.id()) f.write(4*' '+'.name = "%s",\n' % case.name) f.write(4*' '+'.path = "%s",\n' % case.path) f.write(4*' '+'.permutations = %d,\n' % case.permutations) f.write(4*' '+'.run = __test__%s__%s,\n' % (suite.name, case.name)) f.write('};\n') f.write('\n') # create suite struct f.write('const struct test_suite __test__%s__suite = {\n' % (suite.name)) f.write(4*' '+'.id = "%s",\n' % suite.id()) f.write(4*' '+'.name = "%s",\n' % suite.name) f.write(4*' '+'.path = "%s",\n' % suite.path) f.write(4*' '+'.cases = (const struct test_case *const []){\n') for case in suite.cases: f.write(8*' '+'&__test__%s__%s__case,\n' % (suite.name, case.name)) f.write(4*' '+'},\n') f.write(4*' '+'.case_count = %d,\n' % len(suite.cases)) f.write('};\n') f.write('\n') else: # load all suites suites = [TestSuite(path) for path in paths] suites.sort(key=lambda s: s.name) # write out a test source if 'output' in args: with openio(args['output'], 'w') as f: f.write(SUITE_PROLOGUE) f.write('\n') f.write('#line 1 "%s"\n' % args['source']) with open(args['source']) as sf: shutil.copyfileobj(sf, f) # add suite info to test_runner.c if args['source'] == 'runners/test_runner.c': f.write('\n') for suite in suites: f.write('extern const struct test_suite ' '__test__%s__suite;\n' % suite.name) f.write('const struct test_suite *test_suites[] = {\n') for suite in suites: f.write(4*' '+'&__test__%s__suite,\n' % suite.name) f.write('};\n') f.write('const size_t test_suite_count = %d;\n' % len(suites)) def run(**args): pass def main(**args): if args.get('compile'): compile(**args) else: run(**args) if __name__ == "__main__": import argparse import sys parser = argparse.ArgumentParser( description="Build and run tests.") # TODO document test case/perm specifier parser.add_argument('test_paths', nargs='*', default=TEST_PATHS, help="Description of test(s) to run. May be a directory, a path, or \ test identifier. Defaults to all tests in %r." % TEST_PATHS) # test flags test_parser = parser.add_argument_group('test options') # compilation flags comp_parser = parser.add_argument_group('compilation options') comp_parser.add_argument('-c', '--compile', action='store_true', help="Compile a test suite or source file.") comp_parser.add_argument('-s', '--source', help="Source file to compile, possibly injecting internal tests.") comp_parser.add_argument('-o', '--output', help="Output file.") # TODO apply this to other scripts? sys.exit(main(**{k: v for k, v in vars(parser.parse_args()).items() if v is not None}))