Several tweaks to test.py and test runner

These are just some minor quality of life improvements

- Added a "make build-test" alias
- Made test runner a positional arg for test.py since it is almost
  always required. This shortens the command line invocation most of the
  time.
- Added --context to test.py
- Renamed --output in test.py to --stdout, note this still merges
  stderr. Maybe at some point these should be split, but it's not really
  worth it for now.
- Reworked the test_id parsing code a bit.
- Changed the test runner --step to take a range such as -s0,12,2
- Changed tracebd.py --block and --off to take ranges
This commit is contained in:
Christopher Haster
2022-09-08 19:54:07 -05:00
parent a208d848e5
commit c7f7094a06
4 changed files with 272 additions and 267 deletions

View File

@@ -107,18 +107,18 @@ size: $(OBJ)
tags: tags:
$(CTAGS) --totals --c-types=+p $(shell find -H -name '*.h') $(SRC) $(CTAGS) --totals --c-types=+p $(shell find -H -name '*.h') $(SRC)
.PHONY: test-runner .PHONY: test-runner build-test
test-runner: override CFLAGS+=--coverage test-runner build-test: override CFLAGS+=--coverage
test-runner: $(BUILDDIR)runners/test_runner test-runner build-test: $(BUILDDIR)runners/test_runner
rm -f $(TEST_GCDA) rm -f $(TEST_GCDA)
.PHONY: test .PHONY: test
test: test-runner test: test-runner
./scripts/test.py --runner=$(BUILDDIR)runners/test_runner $(TESTFLAGS) ./scripts/test.py $(BUILDDIR)runners/test_runner $(TESTFLAGS)
.PHONY: test-list .PHONY: test-list
test-list: test-runner test-list: test-runner
./scripts/test.py --runner=$(BUILDDIR)runners/test_runner $(TESTFLAGS) -l ./scripts/test.py $(BUILDDIR)runners/test_runner $(TESTFLAGS) -l
.PHONY: code .PHONY: code
code: $(OBJ) code: $(OBJ)

View File

@@ -377,9 +377,9 @@ const test_id_t *test_ids = (const test_id_t[]) {
}; };
size_t test_id_count = 1; size_t test_id_count = 1;
size_t test_start = 0; size_t test_step_start = 0;
size_t test_stop = -1; size_t test_step_stop = -1;
size_t test_step = 1; size_t test_step_step = 1;
const char *test_disk_path = NULL; const char *test_disk_path = NULL;
const char *test_trace_path = NULL; const char *test_trace_path = NULL;
@@ -1430,7 +1430,7 @@ static void list_powerlosses(void) {
// global test step count // global test step count
size_t step = 0; size_t test_step = 0;
// run the tests // run the tests
static void run_perms( static void run_perms(
@@ -1461,13 +1461,13 @@ static void run_perms(
continue; continue;
} }
if (!(step >= test_start if (!(test_step >= test_step_start
&& step < test_stop && test_step < test_step_stop
&& (step-test_start) % test_step == 0)) { && (test_step-test_step_start) % test_step_step == 0)) {
step += 1; test_step += 1;
continue; continue;
} }
step += 1; test_step += 1;
// filter? // filter?
if (case_->filter && !case_->filter()) { if (case_->filter && !case_->filter()) {
@@ -1537,17 +1537,15 @@ enum opt_flags {
OPT_DEFINE = 'D', OPT_DEFINE = 'D',
OPT_GEOMETRY = 'g', OPT_GEOMETRY = 'g',
OPT_POWERLOSS = 'p', OPT_POWERLOSS = 'p',
OPT_START = 7, OPT_STEP = 's',
OPT_STEP = 8,
OPT_STOP = 9,
OPT_DISK = 'd', OPT_DISK = 'd',
OPT_TRACE = 't', OPT_TRACE = 't',
OPT_READ_SLEEP = 10, OPT_READ_SLEEP = 7,
OPT_PROG_SLEEP = 11, OPT_PROG_SLEEP = 8,
OPT_ERASE_SLEEP = 12, OPT_ERASE_SLEEP = 9,
}; };
const char *short_opts = "hYlLD:g:p:d:t:"; const char *short_opts = "hYlLD:g:p:s:d:t:";
const struct option long_opts[] = { const struct option long_opts[] = {
{"help", no_argument, NULL, OPT_HELP}, {"help", no_argument, NULL, OPT_HELP},
@@ -1563,8 +1561,6 @@ const struct option long_opts[] = {
{"define", required_argument, NULL, OPT_DEFINE}, {"define", required_argument, NULL, OPT_DEFINE},
{"geometry", required_argument, NULL, OPT_GEOMETRY}, {"geometry", required_argument, NULL, OPT_GEOMETRY},
{"powerloss", required_argument, NULL, OPT_POWERLOSS}, {"powerloss", required_argument, NULL, OPT_POWERLOSS},
{"start", required_argument, NULL, OPT_START},
{"stop", required_argument, NULL, OPT_STOP},
{"step", required_argument, NULL, OPT_STEP}, {"step", required_argument, NULL, OPT_STEP},
{"disk", required_argument, NULL, OPT_DISK}, {"disk", required_argument, NULL, OPT_DISK},
{"trace", required_argument, NULL, OPT_TRACE}, {"trace", required_argument, NULL, OPT_TRACE},
@@ -1588,9 +1584,7 @@ const char *const help_text[] = {
"Override a test define.", "Override a test define.",
"Comma-separated list of disk geometries to test. Defaults to d,e,E,n,N.", "Comma-separated list of disk geometries to test. Defaults to d,e,E,n,N.",
"Comma-separated list of power-loss scenarios to test. Defaults to 0,l.", "Comma-separated list of power-loss scenarios to test. Defaults to 0,l.",
"Start at the nth test.", "Comma-separated range of test permutations to run (start,stop,step).",
"Stop before the nth test.",
"Only run every n tests, calculated after --start and --stop.",
"Redirect block device operations to this file.", "Redirect block device operations to this file.",
"Redirect trace output to this file.", "Redirect trace output to this file.",
"Artificial read delay in seconds.", "Artificial read delay in seconds.",
@@ -1995,32 +1989,51 @@ powerloss_next:
} }
break; break;
} }
case OPT_START: {
char *parsed = NULL;
test_start = strtoumax(optarg, &parsed, 0);
if (parsed == optarg) {
fprintf(stderr, "error: invalid skip: %s\n", optarg);
exit(-1);
}
break;
}
case OPT_STOP: {
char *parsed = NULL;
test_stop = strtoumax(optarg, &parsed, 0);
if (parsed == optarg) {
fprintf(stderr, "error: invalid count: %s\n", optarg);
exit(-1);
}
break;
}
case OPT_STEP: { case OPT_STEP: {
char *parsed = NULL; char *parsed = NULL;
test_step = strtoumax(optarg, &parsed, 0); size_t start = strtoumax(optarg, &parsed, 0);
if (parsed == optarg) { // allow empty string for start=0
fprintf(stderr, "error: invalid every: %s\n", optarg); if (parsed != optarg) {
exit(-1); test_step_start = start;
} }
optarg = parsed + strspn(parsed, " ");
if (*optarg != ',' && *optarg != '\0') {
goto step_unknown;
}
if (*optarg == ',') {
optarg += 1;
size_t stop = strtoumax(optarg, &parsed, 0);
// allow empty string for stop=end
if (parsed != optarg) {
test_step_stop = stop;
}
optarg = parsed + strspn(parsed, " ");
if (*optarg != ',' && *optarg != '\0') {
goto step_unknown;
}
if (*optarg == ',') {
optarg += 1;
size_t step = strtoumax(optarg, &parsed, 0);
// allow empty string for stop=1
if (parsed != optarg) {
test_step_step = step;
}
optarg = parsed + strspn(parsed, " ");
if (*optarg != '\0') {
goto step_unknown;
}
}
}
break; break;
step_unknown:
fprintf(stderr, "error: invalid step: %s\n", optarg);
exit(-1);
} }
case OPT_DISK: case OPT_DISK:
test_disk_path = optarg; test_disk_path = optarg;
@@ -2077,53 +2090,62 @@ getopt_done: ;
// parse test identifier, if any, cannibalizing the arg in the process // parse test identifier, if any, cannibalizing the arg in the process
for (; argc > optind; optind++) { for (; argc > optind; optind++) {
// parse suite
char *suite = argv[optind];
char *case_ = strchr(suite, '#');
size_t perm = -1; size_t perm = -1;
test_geometry_t *geometry = NULL; test_geometry_t *geometry = NULL;
lfs_testbd_powercycles_t *cycles = NULL; lfs_testbd_powercycles_t *cycles = NULL;
size_t cycle_count = 0; size_t cycle_count = 0;
// parse suite
char *suite = argv[optind];
char *case_ = strchr(suite, '#');
if (case_) { if (case_) {
*case_ = '\0'; *case_ = '\0';
case_ += 1; case_ += 1;
}
// remove optional path and .toml suffix
char *slash = strrchr(suite, '/');
if (slash) {
suite = slash+1;
}
size_t suite_len = strlen(suite);
if (suite_len > 5 && strcmp(&suite[suite_len-5], ".toml") == 0) {
suite[suite_len-5] = '\0';
}
if (case_) {
// parse case // parse case
char *perm_ = strchr(case_, '#'); char *perm_ = strchr(case_, '#');
if (perm_) { if (perm_) {
*perm_ = '\0'; *perm_ = '\0';
perm_ += 1; perm_ += 1;
}
// parse geometry // nothing really to do for case
if (perm_) {
// parse permutation
char *geometry_ = strchr(perm_, '#'); char *geometry_ = strchr(perm_, '#');
if (geometry_) { if (geometry_) {
*geometry_ = '\0'; *geometry_ = '\0';
geometry_ += 1; geometry_ += 1;
}
// parse power cycles char *parsed = NULL;
perm = strtoumax(perm_, &parsed, 10);
if (parsed == perm_) {
fprintf(stderr, "error: "
"could not parse test permutation: %s\n", perm_);
exit(-1);
}
if (geometry_) {
// parse geometry
char *cycles_ = strchr(geometry_, '#'); char *cycles_ = strchr(geometry_, '#');
if (cycles_) { if (cycles_) {
*cycles_ = '\0'; *cycles_ = '\0';
cycles_ += 1; cycles_ += 1;
size_t cycle_capacity = 0;
while (*cycles_ != '\0') {
char *parsed = NULL;
*(lfs_testbd_powercycles_t*)mappend(
(void**)&cycles,
sizeof(lfs_testbd_powercycles_t),
&cycle_count,
&cycle_capacity)
= leb16_parse(cycles_, &parsed);
if (parsed == cycles_) {
fprintf(stderr, "error: "
"could not parse test cycles: %s\n",
cycles_);
exit(-1);
}
cycles_ = parsed;
}
} }
geometry = malloc(sizeof(test_geometry_t)); geometry = malloc(sizeof(test_geometry_t));
@@ -2131,7 +2153,6 @@ getopt_done: ;
size_t count = 0; size_t count = 0;
while (*geometry_ != '\0') { while (*geometry_ != '\0') {
char *parsed = NULL;
uintmax_t x = leb16_parse(geometry_, &parsed); uintmax_t x = leb16_parse(geometry_, &parsed);
if (parsed == geometry_ || count >= 4) { if (parsed == geometry_ || count >= 4) {
fprintf(stderr, "error: " fprintf(stderr, "error: "
@@ -2158,29 +2179,30 @@ getopt_done: ;
geometry->block_count geometry->block_count
= count >= 4 ? sizes[3] = count >= 4 ? sizes[3]
: (1024*1024) / geometry->block_size; : (1024*1024) / geometry->block_size;
}
char *parsed = NULL; if (cycles_) {
perm = strtoumax(perm_, &parsed, 10); // parse power cycles
if (parsed == perm_) { size_t cycle_capacity = 0;
fprintf(stderr, "error: " while (*cycles_ != '\0') {
"could not parse test permutation: %s\n", perm_); *(lfs_testbd_powercycles_t*)mappend(
exit(-1); (void**)&cycles,
sizeof(lfs_testbd_powercycles_t),
&cycle_count,
&cycle_capacity)
= leb16_parse(cycles_, &parsed);
if (parsed == cycles_) {
fprintf(stderr, "error: "
"could not parse test cycles: %s\n",
cycles_);
exit(-1);
}
cycles_ = parsed;
}
}
} }
} }
} }
// remove optional path and .toml suffix
char *slash = strrchr(suite, '/');
if (slash) {
suite = slash+1;
}
size_t suite_len = strlen(suite);
if (suite_len > 5 && strcmp(&suite[suite_len-5], ".toml") == 0) {
suite[suite_len-5] = '\0';
}
// append to identifier list // append to identifier list
*(test_id_t*)mappend( *(test_id_t*)mappend(
(void**)&test_ids, (void**)&test_ids,

View File

@@ -20,26 +20,10 @@ import time
import toml import toml
TEST_PATHS = ['tests'] RUNNER_PATH = 'runners/test_runner'
RUNNER_PATH = './runners/test_runner'
HEADER_PATH = 'runners/test_runner.h' HEADER_PATH = 'runners/test_runner.h'
def testpath(path):
path, *_ = path.split('#', 1)
return path
def testsuite(path):
suite = testpath(path)
suite = os.path.basename(suite)
if suite.endswith('.toml'):
suite = suite[:-len('.toml')]
return suite
def testcase(path):
_, case, *_ = path.split('#', 2)
return '%s#%s' % (testsuite(path), case)
def openio(path, mode='r', buffering=-1, nb=False): def openio(path, mode='r', buffering=-1, nb=False):
if path == '-': if path == '-':
if 'r' in mode: if 'r' in mode:
@@ -56,14 +40,6 @@ def openio(path, mode='r', buffering=-1, nb=False):
else: else:
return open(path, mode, buffering) return open(path, mode, buffering)
def color(**args):
if args.get('color') == 'auto':
return sys.stdout.isatty()
elif args.get('color') == 'always':
return True
else:
return False
class TestCase: class TestCase:
# create a TestCase object from a config # create a TestCase object from a config
def __init__(self, config, args={}): def __init__(self, config, args={}):
@@ -105,8 +81,8 @@ class TestCase:
for k in config.keys(): for k in config.keys():
print('%swarning:%s in %s, found unused key %r' % ( print('%swarning:%s in %s, found unused key %r' % (
'\x1b[01;33m' if color(**args) else '', '\x1b[01;33m' if args['color'] else '',
'\x1b[m' if color(**args) else '', '\x1b[m' if args['color'] else '',
self.id(), self.id(),
k), k),
file=sys.stderr) file=sys.stderr)
@@ -118,8 +94,10 @@ class TestCase:
class TestSuite: class TestSuite:
# create a TestSuite object from a toml file # create a TestSuite object from a toml file
def __init__(self, path, args={}): def __init__(self, path, args={}):
self.name = testsuite(path) self.path = path
self.path = testpath(path) self.name = os.path.basename(path)
if self.name.endswith('.toml'):
self.name = self.name[:-len('.toml')]
# load toml file and parse test cases # load toml file and parse test cases
with open(self.path) as f: with open(self.path) as f:
@@ -191,8 +169,8 @@ class TestSuite:
for k in config.keys(): for k in config.keys():
print('%swarning:%s in %s, found unused key %r' % ( print('%swarning:%s in %s, found unused key %r' % (
'\x1b[01;33m' if color(**args) else '', '\x1b[01;33m' if args['color'] else '',
'\x1b[m' if color(**args) else '', '\x1b[m' if args['color'] else '',
self.id(), self.id(),
k), k),
file=sys.stderr) file=sys.stderr)
@@ -202,10 +180,10 @@ class TestSuite:
def compile(**args): def compile(test_paths, **args):
# find .toml files # find .toml files
paths = [] paths = []
for path in args.get('test_ids', TEST_PATHS): for path in test_paths:
if os.path.isdir(path): if os.path.isdir(path):
path = path + '/*.toml' path = path + '/*.toml'
@@ -213,13 +191,12 @@ def compile(**args):
paths.append(path) paths.append(path)
if not paths: if not paths:
print('no test suites found in %r?' % args['test_ids']) print('no test suites found in %r?' % test_paths)
sys.exit(-1) sys.exit(-1)
if not args.get('source'): if not args.get('source'):
if len(paths) > 1: if len(paths) > 1:
print('more than one test suite for compilation? (%r)' print('more than one test suite for compilation? (%r)' % test_paths)
% args['test_ids'])
sys.exit(-1) sys.exit(-1)
# load our suite # load our suite
@@ -251,7 +228,7 @@ def compile(**args):
f.writeln() f.writeln()
# include test_runner.h in every generated file # include test_runner.h in every generated file
f.writeln("#include \"%s\"" % HEADER_PATH) f.writeln("#include \"%s\"" % args['include'])
f.writeln() f.writeln()
# write out generated functions, this can end up in different # write out generated functions, this can end up in different
@@ -448,9 +425,9 @@ def compile(**args):
f.writeln('#endif') f.writeln('#endif')
f.writeln() f.writeln()
def runner(**args): def find_runner(runner, test_ids, **args):
cmd = args['runner'].copy() cmd = runner.copy()
cmd.extend(args.get('test_ids')) cmd.extend(test_ids)
# run under some external command? # run under some external command?
cmd[:0] = args.get('exec', []) cmd[:0] = args.get('exec', [])
@@ -466,10 +443,19 @@ def runner(**args):
# other context # other context
if args.get('geometry'): if args.get('geometry'):
cmd.append('-G%s' % args.get('geometry')) cmd.append('-g%s' % args['geometry'])
if args.get('powerloss'): if args.get('powerloss'):
cmd.append('-p%s' % args.get('powerloss')) cmd.append('-p%s' % args['powerloss'])
if args.get('disk'):
cmd.append('-d%s' % args['disk'])
if args.get('trace'):
cmd.append('-t%s' % args['trace'])
if args.get('read_sleep'):
cmd.append('--read-sleep=%s' % args['read_sleep'])
if args.get('prog_sleep'):
cmd.append('--prog-sleep=%s' % args['prog_sleep'])
if args.get('erase_sleep'):
cmd.append('--erase-sleep=%s' % args['erase_sleep'])
# defines? # defines?
if args.get('define'): if args.get('define'):
@@ -478,8 +464,8 @@ def runner(**args):
return cmd return cmd
def list_(**args): def list_(runner, test_ids, **args):
cmd = runner(**args) cmd = find_runner(runner, test_ids, **args)
if args.get('summary'): cmd.append('--summary') if args.get('summary'): cmd.append('--summary')
if args.get('list_suites'): cmd.append('--list-suites') if args.get('list_suites'): cmd.append('--list-suites')
if args.get('list_cases'): cmd.append('--list-cases') if args.get('list_cases'): cmd.append('--list-cases')
@@ -492,7 +478,7 @@ def list_(**args):
if args.get('verbose'): if args.get('verbose'):
print(' '.join(shlex.quote(c) for c in cmd)) print(' '.join(shlex.quote(c) for c in cmd))
sys.exit(sp.call(cmd)) return sp.call(cmd)
def find_cases(runner_, **args): def find_cases(runner_, **args):
@@ -624,10 +610,10 @@ def find_defines(runner_, id, **args):
class TestFailure(Exception): class TestFailure(Exception):
def __init__(self, id, returncode, output, assert_=None): def __init__(self, id, returncode, stdout, assert_=None):
self.id = id self.id = id
self.returncode = returncode self.returncode = returncode
self.output = output self.stdout = stdout
self.assert_ = assert_ self.assert_ = assert_
def run_stage(name, runner_, **args): def run_stage(name, runner_, **args):
@@ -659,17 +645,6 @@ def run_stage(name, runner_, **args):
# run the tests! # run the tests!
cmd = runner_.copy() cmd = runner_.copy()
# TODO move all these to runner?
if args.get('disk'):
cmd.append('--disk=%s' % args['disk'])
if args.get('trace'):
cmd.append('--trace=%s' % args['trace'])
if args.get('read_sleep'):
cmd.append('--read-sleep=%s' % args['read_sleep'])
if args.get('prog_sleep'):
cmd.append('--prog-sleep=%s' % args['prog_sleep'])
if args.get('erase_sleep'):
cmd.append('--erase-sleep=%s' % args['erase_sleep'])
if args.get('verbose'): if args.get('verbose'):
print(' '.join(shlex.quote(c) for c in cmd)) print(' '.join(shlex.quote(c) for c in cmd))
@@ -678,10 +653,10 @@ def run_stage(name, runner_, **args):
os.close(spty) os.close(spty)
children.add(proc) children.add(proc)
mpty = os.fdopen(mpty, 'r', 1) mpty = os.fdopen(mpty, 'r', 1)
output = None stdout = None
last_id = None last_id = None
last_output = [] last_stdout = []
last_assert = None last_assert = None
try: try:
while True: while True:
@@ -694,19 +669,19 @@ def run_stage(name, runner_, **args):
raise raise
if not line: if not line:
break break
last_output.append(line) last_stdout.append(line)
if args.get('output'): if args.get('stdout'):
try: try:
if not output: if not stdout:
output = openio(args['output'], 'a', 1, nb=True) stdout = openio(args['stdout'], 'a', 1, nb=True)
output.write(line) stdout.write(line)
except OSError as e: except OSError as e:
if e.errno not in [ if e.errno not in [
errno.ENXIO, errno.ENXIO,
errno.EPIPE, errno.EPIPE,
errno.EAGAIN]: errno.EAGAIN]:
raise raise
output = None stdout = None
if args.get('verbose'): if args.get('verbose'):
sys.stdout.write(line) sys.stdout.write(line)
@@ -716,7 +691,7 @@ def run_stage(name, runner_, **args):
if op == 'running': if op == 'running':
locals.seen_perms += 1 locals.seen_perms += 1
last_id = m.group('id') last_id = m.group('id')
last_output = [] last_stdout = []
last_assert = None last_assert = None
elif op == 'powerloss': elif op == 'powerloss':
last_id = m.group('id') last_id = m.group('id')
@@ -736,7 +711,7 @@ def run_stage(name, runner_, **args):
if args.get('keep_going'): if args.get('keep_going'):
proc.kill() proc.kill()
except KeyboardInterrupt: except KeyboardInterrupt:
raise TestFailure(last_id, 1, last_output) raise TestFailure(last_id, 1, last_stdout)
finally: finally:
children.remove(proc) children.remove(proc)
mpty.close() mpty.close()
@@ -746,7 +721,7 @@ def run_stage(name, runner_, **args):
raise TestFailure( raise TestFailure(
last_id, last_id,
proc.returncode, proc.returncode,
last_output, last_stdout,
last_assert) last_assert)
def run_job(runner, start=None, step=None): def run_job(runner, start=None, step=None):
@@ -758,12 +733,10 @@ def run_stage(name, runner_, **args):
step = step or 1 step = step or 1
while start < total_perms: while start < total_perms:
runner_ = runner.copy() runner_ = runner.copy()
if start is not None:
runner_.append('--start=%d' % start)
if step is not None:
runner_.append('--step=%d' % step)
if args.get('isolate') or args.get('valgrind'): if args.get('isolate') or args.get('valgrind'):
runner_.append('--stop=%d' % (start+step)) runner_.append('-s%s,%s,%s' % (start, start+step, step))
else:
runner_.append('-s%s,,%s' % (start, step))
try: try:
# run the tests # run the tests
@@ -805,14 +778,14 @@ def run_stage(name, runner_, **args):
daemon=True)) daemon=True))
def print_update(done): def print_update(done):
if not args.get('verbose') and (color(**args) or done): if not args.get('verbose') and (args['color'] or done):
sys.stdout.write('%s%srunning %s%s:%s %s%s' % ( sys.stdout.write('%s%srunning %s%s:%s %s%s' % (
'\r\x1b[K' if color(**args) else '', '\r\x1b[K' if args['color'] else '',
'\x1b[?7l' if not done else '', '\x1b[?7l' if not done else '',
('\x1b[32m' if not failures else '\x1b[31m') ('\x1b[32m' if not failures else '\x1b[31m')
if color(**args) else '', if args['color'] else '',
name, name,
'\x1b[m' if color(**args) else '', '\x1b[m' if args['color'] else '',
', '.join(filter(None, [ ', '.join(filter(None, [
'%d/%d suites' % ( '%d/%d suites' % (
sum(passed_suite_perms[k] == v sum(passed_suite_perms[k] == v
@@ -829,10 +802,10 @@ def run_stage(name, runner_, **args):
'%dpls!' % powerlosses '%dpls!' % powerlosses
if powerlosses else None, if powerlosses else None,
'%s%d/%d failures%s' % ( '%s%d/%d failures%s' % (
'\x1b[31m' if color(**args) else '', '\x1b[31m' if args['color'] else '',
len(failures), len(failures),
expected_perms, expected_perms,
'\x1b[m' if color(**args) else '') '\x1b[m' if args['color'] else '')
if failures else None])), if failures else None])),
'\x1b[?7h' if not done else '\n')) '\x1b[?7h' if not done else '\n'))
sys.stdout.flush() sys.stdout.flush()
@@ -862,9 +835,9 @@ def run_stage(name, runner_, **args):
killed) killed)
def run(**args): def run(runner, test_ids, **args):
# query runner for tests # query runner for tests
runner_ = runner(**args) runner_ = find_runner(runner, test_ids, **args)
print('using runner: %s' print('using runner: %s'
% ' '.join(shlex.quote(c) for c in runner_)) % ' '.join(shlex.quote(c) for c in runner_))
expected_suite_perms, expected_case_perms, expected_perms, total_perms = ( expected_suite_perms, expected_case_perms, expected_perms, total_perms = (
@@ -877,9 +850,9 @@ def run(**args):
print() print()
# truncate and open logs here so they aren't disconnected between tests # truncate and open logs here so they aren't disconnected between tests
output = None stdout = None
if args.get('output'): if args.get('stdout'):
output = openio(args['output'], 'w', 1) stdout = openio(args['stdout'], 'w', 1)
trace = None trace = None
if args.get('trace'): if args.get('trace'):
trace = openio(args['trace'], 'w', 1) trace = openio(args['trace'], 'w', 1)
@@ -896,8 +869,8 @@ def run(**args):
else expected_suite_perms.keys() if args.get('by_suites') else expected_suite_perms.keys() if args.get('by_suites')
else [None]): else [None]):
# rebuild runner for each stage to override test identifier if needed # rebuild runner for each stage to override test identifier if needed
stage_runner = runner(**args | { stage_runner = find_runner(runner,
'test_ids': [by] if by is not None else args.get('test_ids', [])}) [by] if by is not None else test_ids, **args)
# spawn jobs for stage # spawn jobs for stage
expected_, passed_, powerlosses_, failures_, killed = run_stage( expected_, passed_, powerlosses_, failures_, killed = run_stage(
@@ -913,8 +886,8 @@ def run(**args):
stop = time.time() stop = time.time()
if output: if stdout:
output.close() stdout.close()
if trace: if trace:
trace.close() trace.close()
@@ -922,8 +895,8 @@ def run(**args):
print() print()
print('%sdone:%s %s' % ( print('%sdone:%s %s' % (
('\x1b[32m' if not failures else '\x1b[31m') ('\x1b[32m' if not failures else '\x1b[31m')
if color(**args) else '', if args['color'] else '',
'\x1b[m' if color(**args) else '', '\x1b[m' if args['color'] else '',
', '.join(filter(None, [ ', '.join(filter(None, [
'%d/%d passed' % (passed, expected), '%d/%d passed' % (passed, expected),
'%d/%d failed' % (len(failures), expected), '%d/%d failed' % (len(failures), expected),
@@ -943,28 +916,28 @@ def run(**args):
# show summary of failure # show summary of failure
print('%s%s:%d:%sfailure:%s %s%s failed' % ( print('%s%s:%d:%sfailure:%s %s%s failed' % (
'\x1b[01m' if color(**args) else '', '\x1b[01m' if args['color'] else '',
path, lineno, path, lineno,
'\x1b[01;31m' if color(**args) else '', '\x1b[01;31m' if args['color'] else '',
'\x1b[m' if color(**args) else '', '\x1b[m' if args['color'] else '',
failure.id, failure.id,
' (%s)' % ', '.join('%s=%s' % (k,v) for k,v in defines.items()) ' (%s)' % ', '.join('%s=%s' % (k,v) for k,v in defines.items())
if defines else '')) if defines else ''))
if failure.output: if failure.stdout:
output = failure.output stdout = failure.stdout
if failure.assert_ is not None: if failure.assert_ is not None:
output = output[:-1] stdout = stdout[:-1]
for line in output[-5:]: for line in stdout[-args.get('context', 5):]:
sys.stdout.write(line) sys.stdout.write(line)
if failure.assert_ is not None: if failure.assert_ is not None:
path, lineno, message = failure.assert_ path, lineno, message = failure.assert_
print('%s%s:%d:%sassert:%s %s' % ( print('%s%s:%d:%sassert:%s %s' % (
'\x1b[01m' if color(**args) else '', '\x1b[01m' if args['color'] else '',
path, lineno, path, lineno,
'\x1b[01;31m' if color(**args) else '', '\x1b[01;31m' if args['color'] else '',
'\x1b[m' if color(**args) else '', '\x1b[m' if args['color'] else '',
message)) message))
with open(path) as f: with open(path) as f:
line = next(it.islice(f, lineno-1, None)).strip('\n') line = next(it.islice(f, lineno-1, None)).strip('\n')
@@ -976,7 +949,7 @@ def run(**args):
or args.get('gdb_case') or args.get('gdb_case')
or args.get('gdb_main')): or args.get('gdb_main')):
failure = failures[0] failure = failures[0]
runner_ = runner(**args | {'test_ids': [failure.id]}) runner_ = find_runner(runner, [failure.id], **args)
if args.get('gdb_main'): if args.get('gdb_main'):
cmd = ['gdb', cmd = ['gdb',
@@ -984,7 +957,7 @@ def run(**args):
'-ex', 'run', '-ex', 'run',
'--args'] + runner_ '--args'] + runner_
elif args.get('gdb_case'): elif args.get('gdb_case'):
path, lineno = runner_paths[testcase(failure.id)] path, lineno = find_path(runner_, failure.id, **args)
cmd = ['gdb', cmd = ['gdb',
'-ex', 'break %s:%d' % (path, lineno), '-ex', 'break %s:%d' % (path, lineno),
'-ex', 'run', '-ex', 'run',
@@ -1009,8 +982,16 @@ def run(**args):
def main(**args): def main(**args):
# figure out what color should be
if args.get('color') == 'auto':
args['color'] = sys.stdout.isatty()
elif args.get('color') == 'always':
args['color'] = True
else:
args['color'] = False
if args.get('compile'): if args.get('compile'):
compile(**args) return compile(**args)
elif (args.get('summary') elif (args.get('summary')
or args.get('list_suites') or args.get('list_suites')
or args.get('list_cases') or args.get('list_cases')
@@ -1020,9 +1001,9 @@ def main(**args):
or args.get('list_implicit') or args.get('list_implicit')
or args.get('list_geometries') or args.get('list_geometries')
or args.get('list_powerlosses')): or args.get('list_powerlosses')):
list_(**args) return list_(**args)
else: else:
run(**args) return run(**args)
if __name__ == "__main__": if __name__ == "__main__":
@@ -1033,18 +1014,19 @@ if __name__ == "__main__":
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Build and run tests.", description="Build and run tests.",
conflict_handler='ignore') conflict_handler='ignore')
parser.add_argument('test_ids', nargs='*',
help="Description of testis to run. May be a directory, path, or \
test identifier. Test identifiers are of the form \
<suite_name>#<case_name>#<permutation>, but suffixes can be \
dropped to run any matching tests. Defaults to %s." % TEST_PATHS)
parser.add_argument('-v', '--verbose', action='store_true', parser.add_argument('-v', '--verbose', action='store_true',
help="Output commands that run behind the scenes.") help="Output commands that run behind the scenes.")
parser.add_argument('--color', parser.add_argument('--color',
choices=['never', 'always', 'auto'], default='auto', choices=['never', 'always', 'auto'], default='auto',
help="When to use terminal colors.") help="When to use terminal colors.")
# test flags # test flags
test_parser = parser.add_argument_group('test options') test_parser = parser.add_argument_group('test options')
test_parser.add_argument('runner', nargs='?',
type=lambda x: x.split(),
help="Test runner to use for testing. Defaults to %r." % RUNNER_PATH)
test_parser.add_argument('test_ids', nargs='*',
help="Description of tests to run.")
test_parser.add_argument('-Y', '--summary', action='store_true', test_parser.add_argument('-Y', '--summary', action='store_true',
help="Show quick summary.") help="Show quick summary.")
test_parser.add_argument('-l', '--list-suites', action='store_true', test_parser.add_argument('-l', '--list-suites', action='store_true',
@@ -1075,17 +1057,14 @@ if __name__ == "__main__":
help="Direct block device operations to this file.") help="Direct block device operations to this file.")
test_parser.add_argument('-t', '--trace', test_parser.add_argument('-t', '--trace',
help="Direct trace output to this file.") help="Direct trace output to this file.")
test_parser.add_argument('-o', '--output', test_parser.add_argument('-O', '--stdout',
help="Direct stdout and stderr to this file.") help="Direct stdout to this file. Note stderr is already merged here.")
test_parser.add_argument('--read-sleep', test_parser.add_argument('--read-sleep',
help="Artificial read delay in seconds.") help="Artificial read delay in seconds.")
test_parser.add_argument('--prog-sleep', test_parser.add_argument('--prog-sleep',
help="Artificial prog delay in seconds.") help="Artificial prog delay in seconds.")
test_parser.add_argument('--erase-sleep', test_parser.add_argument('--erase-sleep',
help="Artificial erase delay in seconds.") help="Artificial erase delay in seconds.")
test_parser.add_argument('--runner', default=[RUNNER_PATH],
type=lambda x: x.split(),
help="Path to runner, defaults to %r" % RUNNER_PATH)
test_parser.add_argument('-j', '--jobs', nargs='?', type=int, test_parser.add_argument('-j', '--jobs', nargs='?', type=int,
const=len(os.sched_getaffinity(0)), const=len(os.sched_getaffinity(0)),
help="Number of parallel runners to run.") help="Number of parallel runners to run.")
@@ -1097,6 +1076,9 @@ if __name__ == "__main__":
help="Step through tests by suite.") help="Step through tests by suite.")
test_parser.add_argument('-B', '--by-cases', action='store_true', test_parser.add_argument('-B', '--by-cases', action='store_true',
help="Step through tests by case.") help="Step through tests by case.")
test_parser.add_argument('--context', type=lambda x: int(x, 0),
help="Show this many lines of stdout on test failure. \
Defaults to 5.")
test_parser.add_argument('--gdb', action='store_true', test_parser.add_argument('--gdb', action='store_true',
help="Drop into gdb on test failure.") help="Drop into gdb on test failure.")
test_parser.add_argument('--gdb-case', action='store_true', test_parser.add_argument('--gdb-case', action='store_true',
@@ -1110,14 +1092,27 @@ if __name__ == "__main__":
test_parser.add_argument('--valgrind', action='store_true', test_parser.add_argument('--valgrind', action='store_true',
help="Run under Valgrind to find memory errors. Implicitly sets \ help="Run under Valgrind to find memory errors. Implicitly sets \
--isolate.") --isolate.")
# compilation flags # compilation flags
comp_parser = parser.add_argument_group('compilation options') comp_parser = parser.add_argument_group('compilation options')
comp_parser.add_argument('test_paths', nargs='*',
help="Description of *.toml files to compile. May be a directory \
or a list of paths.")
comp_parser.add_argument('-c', '--compile', action='store_true', comp_parser.add_argument('-c', '--compile', action='store_true',
help="Compile a test suite or source file.") help="Compile a test suite or source file.")
comp_parser.add_argument('-s', '--source', comp_parser.add_argument('-s', '--source',
help="Source file to compile, possibly injecting internal tests.") help="Source file to compile, possibly injecting internal tests.")
comp_parser.add_argument('--include', default=HEADER_PATH,
help="Inject this header file into every compiled test file. \
Defaults to %r." % HEADER_PATH)
comp_parser.add_argument('-o', '--output', comp_parser.add_argument('-o', '--output',
help="Output file.") help="Output file.")
# runner + test_ids overlaps test_paths, so we need to do some munging here
args = parser.parse_args()
args.test_paths = [' '.join(args.runner or [])] + args.test_ids
args.runner = args.runner or [RUNNER_PATH]
sys.exit(main(**{k: v sys.exit(main(**{k: v
for k, v in vars(parser.parse_args()).items() for k, v in vars(args).items()
if v is not None})) if v is not None}))

View File

@@ -6,6 +6,7 @@
import collections as co import collections as co
import itertools as it import itertools as it
import math as m import math as m
import os
import re import re
import shutil import shutil
import threading as th import threading as th
@@ -322,11 +323,8 @@ def main(path='-', *,
chars=None, chars=None,
wear_chars=None, wear_chars=None,
color='auto', color='auto',
block=None, block=(None,None),
start=None, off=(None,None),
stop=None,
start_off=None,
stop_off=None,
block_size=None, block_size=None,
block_count=None, block_count=None,
block_cycles=None, block_cycles=None,
@@ -346,25 +344,26 @@ def main(path='-', *,
if color == 'auto': if color == 'auto':
color = 'always' if sys.stdout.isatty() else 'never' color = 'always' if sys.stdout.isatty() else 'never'
start = (start if start is not None block_start = block[0]
else block if block is not None block_stop = block[1] if len(block) > 1 else block[0]+1
else 0) off_start = off[0]
stop = (stop if stop is not None off_stop = off[1] if len(off) > 1 else off[0]+1
else block+1 if block is not None
else block_count if block_count is not None if block_start is None:
else None) block_start = 0
start_off = (start_off if start_off is not None if block_stop is None and block_count is not None:
else 0) block_stop = block_count
stop_off = (stop_off if stop_off is not None if off_start is None:
else block_size if block_size is not None off_start = 0
else None) if off_stop is None and block_size is not None:
off_stop = block_size
bd = Bd( bd = Bd(
size=(block_size if block_size is not None size=(block_size if block_size is not None
else stop_off-start_off if stop_off is not None else off_stop-off_start if off_stop is not None
else 1), else 1),
count=(block_count if block_count is not None count=(block_count if block_count is not None
else stop-start if stop is not None else block_stop-block_start if block_stop is not None
else 1), else 1),
width=(width or 80)*height) width=(width or 80)*height)
lock = th.Lock() lock = th.Lock()
@@ -431,21 +430,21 @@ def main(path='-', *,
size = int(m.group('block_size'), 0) size = int(m.group('block_size'), 0)
count = int(m.group('block_count'), 0) count = int(m.group('block_count'), 0)
if stop_off is not None: if off_stop is not None:
size = stop_off-start_off size = off_stop-off_start
if stop is not None: if block_stop is not None:
count = stop-start count = block_stop-block_start
with lock: with lock:
if reset: if reset:
bd.reset() bd.reset()
# ignore the new values is stop/stop_off is explicit # ignore the new values if block_stop/off_stop is explicit
bd.smoosh( bd.smoosh(
size=(size if stop_off is None size=(size if off_stop is None
else stop_off-start_off), else off_stop-off_start),
count=(count if stop is None count=(count if block_stop is None
else stop-start)) else block_stop-block_start))
return True return True
elif m.group('read') and read: elif m.group('read') and read:
@@ -453,14 +452,14 @@ def main(path='-', *,
off = int(m.group('read_off'), 0) off = int(m.group('read_off'), 0)
size = int(m.group('read_size'), 0) size = int(m.group('read_size'), 0)
if stop is not None and block >= stop: if block_stop is not None and block >= block_stop:
return False return False
block -= start block -= block_start
if stop_off is not None: if off_stop is not None:
if off >= stop_off: if off >= off_stop:
return False return False
size = min(size, stop_off-off) size = min(size, off_stop-off)
off -= start_off off -= off_start
with lock: with lock:
bd.read(block, slice(off,off+size)) bd.read(block, slice(off,off+size))
@@ -471,14 +470,14 @@ def main(path='-', *,
off = int(m.group('prog_off'), 0) off = int(m.group('prog_off'), 0)
size = int(m.group('prog_size'), 0) size = int(m.group('prog_size'), 0)
if stop is not None and block >= stop: if block_stop is not None and block >= block_stop:
return False return False
block -= start block -= block_start
if stop_off is not None: if off_stop is not None:
if off >= stop_off: if off >= off_stop:
return False return False
size = min(size, stop_off-off) size = min(size, off_stop-off)
off -= start_off off -= off_start
with lock: with lock:
bd.prog(block, slice(off,off+size)) bd.prog(block, slice(off,off+size))
@@ -487,9 +486,9 @@ def main(path='-', *,
elif m.group('erase') and (erase or wear): elif m.group('erase') and (erase or wear):
block = int(m.group('erase_block'), 0) block = int(m.group('erase_block'), 0)
if stop is not None and block >= stop: if block_stop is not None and block >= block_stop:
return False return False
block -= start block -= block_start
with lock: with lock:
bd.erase(block) bd.erase(block)
@@ -691,24 +690,13 @@ if __name__ == "__main__":
parser.add_argument( parser.add_argument(
'-b', '-b',
'--block', '--block',
type=lambda x: int(x, 0), type=lambda x: tuple(int(x,0) if x else None for x in x.split(',',1)),
help="Show a specific block.") help="Show a specific block or range of blocks.")
parser.add_argument( parser.add_argument(
'--start', '-i',
type=lambda x: int(x, 0), '--off',
help="Start at this block.") type=lambda x: tuple(int(x,0) if x else None for x in x.split(',',1)),
parser.add_argument( help="Show a specific offset or range of offsets.")
'--stop',
type=lambda x: int(x, 0),
help="Stop before this block.")
parser.add_argument(
'--start-off',
type=lambda x: int(x, 0),
help="Start at this offset.")
parser.add_argument(
'--stop-off',
type=lambda x: int(x, 0),
help="Stop before this offset.")
parser.add_argument( parser.add_argument(
'-B', '-B',
'--block-size', '--block-size',