Reworked test_runner/bench_runner to evaluate define permutations lazily

I wondered if walking in Python 2's footsteps was going to run into the
same issues and sure enough, memory backed iterators became unweildy.

The motivation for this change is that large ranges in tests, such as
iterators over seeds or permutations, became prohibitively expensive to
compile. This meant more iteration moving into tests with more steps to
reproduce failures. This sort of defeats the purpuse of the test
framework.

The solution here is to move test permutation generation out of test.py
and into the test runner itself. The allows defines to generate their
values programmatically.

This does conflict with the test frameworks support of sets of explicit
permutations, but this is fixed by also moving these "permutation sets"
down into the test runner.

I guess it turns out the closer your representation matches your
implementation the better everythign works.

Additionally the define caching layer got a bit of tweaking. We can't
precalculate the defines because of mutual recursion, but we can
precalculate which define/permutation each define id maps to. This is
necessary as otherwise figuring out each define's define-specific
permutation would be prohibitively expensive.
This commit is contained in:
Christopher Haster
2023-03-15 00:04:53 -05:00
parent 15e27f92af
commit 59a57cb767
7 changed files with 635 additions and 493 deletions

View File

@ -93,13 +93,13 @@ class TestCase:
def parse_define(v):
# a define entry can be a list
if isinstance(v, list):
for v_ in v:
yield from parse_define(v_)
return sum((parse_define(v_) for v_ in v), [])
# or a string
elif isinstance(v, str):
# which can be comma-separated values, with optional
# range statements. This matches the runtime define parser in
# the runner itself.
vs = []
for v_ in csplit(v):
m = re.search(r'\brange\b\s*\('
'(?P<start>[^,\s]*)'
@ -115,25 +115,24 @@ class TestCase:
if m.group('step') else 1)
if m.lastindex <= 1:
start, stop = 0, start
for x in range(start, stop, step):
yield from parse_define('%s(%d)%s' % (
v_[:m.start()], x, v_[m.end():]))
vs.append(range(start, stop, step))
else:
yield v_
vs.append(v_)
return vs
# or a literal value
elif isinstance(v, bool):
yield 'true' if v else 'false'
return ['true' if v else 'false']
else:
yield v
return [v]
# build possible permutations
for suite_defines_ in suite_defines:
self.defines |= suite_defines_.keys()
for defines_ in defines:
self.defines |= defines_.keys()
self.permutations.extend(dict(perm) for perm in it.product(*(
[(k, v) for v in parse_define(vs)]
for k, vs in sorted((suite_defines_ | defines_).items()))))
self.permutations.append({
k: parse_define(v)
for k, v in (suite_defines_ | defines_).items()})
for k in config.keys():
print('%swarning:%s in %s, found unused key %r' % (
@ -316,34 +315,32 @@ def compile(test_paths, **args):
# the test defines
def write_case_functions(f, suite, case):
# create case define functions
if case.defines:
# deduplicate defines by value to try to reduce the
# number of functions we generate
define_cbs = {}
for i, defines in enumerate(case.permutations):
for k, v in sorted(defines.items()):
if v not in define_cbs:
name = ('__test__%s__%s__%d'
% (case.name, k, i))
define_cbs[v] = name
f.writeln('intmax_t %s('
'__attribute__((unused)) '
'void *data) {' % name)
f.writeln(4*' '+'return %s;' % v)
f.writeln('}')
f.writeln()
f.writeln('const test_define_t '
'__test__%s__defines[]['
'TEST_IMPLICIT_DEFINE_COUNT+%d] = {'
% (case.name, len(suite.defines)))
for defines in case.permutations:
f.writeln(4*' '+'{')
for k, v in sorted(defines.items()):
f.writeln(8*' '+'[%-24s] = {%s, NULL},' % (
k+'_i', define_cbs[v]))
f.writeln(4*' '+'},')
f.writeln('};')
f.writeln()
for i, permutation in enumerate(case.permutations):
for k, vs in sorted(permutation.items()):
f.writeln('intmax_t __test__%s__%s__%d('
'__attribute__((unused)) void *data, '
'size_t i) {'
% (case.name, k, i))
j = 0
for v in vs:
# generate range
if isinstance(v, range):
f.writeln(
4*' '+'if (i < %d) '
'return (i-%d)*%d + %d;'
% (j+len(v), j, v.step, v.start))
j += len(v)
# translate index to define
else:
f.writeln(
4*' '+'if (i == %d) '
'return %s;'
% (j, v))
j += 1;
f.writeln(4*' '+'__builtin_unreachable();')
f.writeln('}')
f.writeln()
# create case filter function
if suite.if_ is not None or case.if_ is not None:
@ -397,11 +394,11 @@ def compile(test_paths, **args):
if case.in_ is None:
write_case_functions(f, suite, case)
else:
if case.defines:
f.writeln('extern const test_define_t '
'__test__%s__defines[]['
'TEST_IMPLICIT_DEFINE_COUNT+%d];'
% (case.name, len(suite.defines)))
for i, permutation in enumerate(case.permutations):
for k, vs in sorted(permutation.items()):
f.writeln('extern intmax_t __test__%s__%s__%d('
'void *data, size_t i);'
% (case.name, k, i))
if suite.if_ is not None or case.if_ is not None:
f.writeln('extern bool __test__%s__filter('
'void);'
@ -429,13 +426,14 @@ def compile(test_paths, **args):
if suite.defines:
# create suite define names
f.writeln(4*' '+'.define_names = (const char *const['
'TEST_IMPLICIT_DEFINE_COUNT+%d]){' % (
len(suite.defines)))
'TEST_IMPLICIT_DEFINE_COUNT+%d]){'
% (len(suite.defines)))
for k in sorted(suite.defines):
f.writeln(8*' '+'[%-24s] = "%s",' % (k+'_i', k))
f.writeln(4*' '+'},')
f.writeln(4*' '+'.define_count = '
'TEST_IMPLICIT_DEFINE_COUNT+%d,' % len(suite.defines))
'TEST_IMPLICIT_DEFINE_COUNT+%d,'
% len(suite.defines))
if suite.cases:
f.writeln(4*' '+'.cases = (const struct test_case[]){')
for case in suite.cases:
@ -447,12 +445,26 @@ def compile(test_paths, **args):
% (' | '.join(filter(None, [
'TEST_REENTRANT' if case.reentrant else None]))
or 0))
f.writeln(12*' '+'.permutations = %d,'
% len(case.permutations))
if case.defines:
f.writeln(12*' '+'.defines '
'= (const test_define_t*)__test__%s__defines,'
% (case.name))
f.writeln(12*' '+'.defines = '
'(const test_define_t*)(const test_define_t[]['
'TEST_IMPLICIT_DEFINE_COUNT+%d]){'
% (len(suite.defines)))
for i, permutation in enumerate(case.permutations):
f.writeln(16*' '+'{')
for k, vs in sorted(permutation.items()):
f.writeln(20*' '
+'[%-24s] = {__test__%s__%s__%d, NULL, '
'%d},'
% (k+'_i', case.name, k, i,
sum(len(v)
if isinstance(v, range)
else 1
for v in vs)))
f.writeln(16*' '+'},')
f.writeln(12*' '+'},')
f.writeln(12*' '+'.permutations = %d,'
% len(case.permutations))
if suite.if_ is not None or case.if_ is not None:
f.writeln(12*' '+'.filter = __test__%s__filter,'
% (case.name))