|
@@ -0,0 +1,269 @@
|
|
|
|
|
+#!/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 <stdio.h>
|
|
|
|
|
+"""
|
|
|
|
|
+# 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<case>\[\s*cases\s*\.\s*(?P<name>\w+)\s*\])' +
|
|
|
|
|
+ '|(?P<code>code\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}))
|