Files
littlefs/scripts/test_.py
Christopher Haster 56a990336b Created new test_runner.c and test_.py
This is to try a different design for testing, the goals are to make the
test infrastructure a bit simpler, with clear stages for building and
running, and faster, by avoiding rebuilding lfs.c n-times.
2022-04-16 13:50:34 -05:00

270 lines
9.1 KiB
Python
Executable File

#!/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}))