Browse Source

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
Christopher Haster 3 years ago
parent
commit
c7f7094a06
4 changed files with 271 additions and 266 deletions
  1. 5 5
      Makefile
  2. 106 84
      runners/test_runner.c
  3. 113 118
      scripts/test.py
  4. 47 59
      scripts/tracebd.py

+ 5 - 5
Makefile

@@ -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
-test-runner: override CFLAGS+=--coverage
-test-runner: $(BUILDDIR)runners/test_runner
+.PHONY: test-runner build-test
+test-runner build-test: override CFLAGS+=--coverage
+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)

+ 106 - 84
runners/test_runner.c

@@ -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_stop = -1;
-size_t test_step = 1;
+size_t test_step_start = 0;
+size_t test_step_stop = -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
-                        && step < test_stop
-                        && (step-test_start) % test_step == 0)) {
-                    step += 1;
+                if (!(test_step >= test_step_start
+                        && test_step < test_step_stop
+                        && (test_step-test_step_start) % test_step_step == 0)) {
+                    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             = 8,
-    OPT_STOP             = 9,
+    OPT_STEP             = 's',
     OPT_DISK             = 'd',
     OPT_DISK             = 'd',
     OPT_TRACE            = 't',
     OPT_TRACE            = 't',
-    OPT_READ_SLEEP       = 10,
-    OPT_PROG_SLEEP       = 11,
-    OPT_ERASE_SLEEP      = 12,
+    OPT_READ_SLEEP       = 7,
+    OPT_PROG_SLEEP       = 8,
+    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.",
-    "Stop before the nth test.",
-    "Only run every n tests, calculated after --start and --stop.",
+    "Comma-separated range of test permutations to run (start,stop,step).",
     "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: {
+            case OPT_STEP: {
                 char *parsed = NULL;
                 char *parsed = NULL;
-                test_start = strtoumax(optarg, &parsed, 0);
-                if (parsed == optarg) {
-                    fprintf(stderr, "error: invalid skip: %s\n", optarg);
-                    exit(-1);
+                size_t start = strtoumax(optarg, &parsed, 0);
+                // allow empty string for start=0
+                if (parsed != optarg) {
+                    test_step_start = start;
                 }
                 }
-                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);
+                optarg = parsed + strspn(parsed, " ");
+
+                if (*optarg != ',' && *optarg != '\0') {
+                    goto step_unknown;
                 }
                 }
-                break;
-            }
-            case OPT_STEP: {
-                char *parsed = NULL;
-                test_step = strtoumax(optarg, &parsed, 0);
-                if (parsed == optarg) {
-                    fprintf(stderr, "error: invalid every: %s\n", optarg);
-                    exit(-1);
+
+                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;
-                perm = strtoumax(perm_, &parsed, 10);
-                if (parsed == perm_) {
-                    fprintf(stderr, "error: "
-                            "could not parse test permutation: %s\n", perm_);
-                    exit(-1);
+                    if (cycles_) {
+                        // parse power cycles
+                        size_t cycle_capacity = 0;
+                        while (*cycles_ != '\0') {
+                            *(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;
+                        }
+                    }
                 }
                 }
             }
             }
         }
         }
 
 
-        // 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,

+ 113 - 118
scripts/test.py

@@ -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[m' if color(**args) else '',
+                '\x1b[01;33m' if args['color'] 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 = testpath(path)
+        self.path = 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[m' if color(**args) else '',
+                '\x1b[01;33m' if args['color'] 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)'
-                % args['test_ids'])
+            print('more than one test suite for compilation? (%r)' % test_paths)
             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):
-    cmd = args['runner'].copy()
-    cmd.extend(args.get('test_ids'))
+def find_runner(runner, test_ids, **args):
+    cmd = runner.copy()
+    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):
-    cmd = runner(**args)
+def list_(runner, test_ids, **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)
-                if args.get('output'):
+                last_stdout.append(line)
+                if args.get('stdout'):
                     try:
                     try:
-                        if not output:
-                            output = openio(args['output'], 'a', 1, nb=True)
-                        output.write(line)
+                        if not stdout:
+                            stdout = openio(args['stdout'], 'a', 1, nb=True)
+                        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
-    if args.get('output'):
-        output = openio(args['output'], 'w', 1)
+    stdout = None
+    if args.get('stdout'):
+        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 | {
-            'test_ids': [by] if by is not None else args.get('test_ids', [])})
+        stage_runner = find_runner(runner,
+            [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:
-        output.close()
+    if stdout:
+        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 '',
-        '\x1b[m' if color(**args) else '',
+            if args['color'] 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[m' if color(**args) else '',
+            '\x1b[01;31m' if args['color'] 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:
-            output = failure.output
+        if failure.stdout:
+            stdout = failure.stdout
             if failure.assert_ is not None:
             if failure.assert_ is not None:
-                output = output[:-1]
-            for line in output[-5:]:
+                stdout = stdout[:-1]
+            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[m' if color(**args) else '',
+                '\x1b[01;31m' if args['color'] 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',
-        help="Direct stdout and stderr to this file.")
+    test_parser.add_argument('-O', '--stdout',
+        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}))

+ 47 - 59
scripts/tracebd.py

@@ -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,
-        start=None,
-        stop=None,
-        start_off=None,
-        stop_off=None,
+        block=(None,None),
+        off=(None,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
-        else block if block is not None
-        else 0)
-    stop = (stop if stop is not None
-        else block+1 if block is not None
-        else block_count if block_count is not None
-        else None)
-    start_off = (start_off if start_off is not None
-        else 0)
-    stop_off = (stop_off if stop_off is not None
-        else block_size if block_size is not None
-        else None)
+    block_start = block[0]
+    block_stop = block[1] if len(block) > 1 else block[0]+1
+    off_start = off[0]
+    off_stop = off[1] if len(off) > 1 else off[0]+1
+
+    if block_start is None:
+        block_start = 0
+    if block_stop is None and block_count is not None:
+        block_stop = block_count
+    if off_start is None:
+        off_start = 0
+    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:
-                size = stop_off-start_off
-            if stop is not None:
-                count = stop-start
+            if off_stop is not None:
+                size = off_stop-off_start
+            if block_stop is not None:
+                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
-                        else stop_off-start_off),
-                    count=(count if stop is None
-                        else stop-start))
+                    size=(size if off_stop is None
+                        else off_stop-off_start),
+                    count=(count if block_stop is None
+                        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
-            if stop_off is not None:
-                if off >= stop_off:
+            block -= block_start
+            if off_stop is not None:
+                if off >= off_stop:
                     return False
                     return False
-                size = min(size, stop_off-off)
-            off -= start_off
+                size = min(size, off_stop-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
-            if stop_off is not None:
-                if off >= stop_off:
+            block -= block_start
+            if off_stop is not None:
+                if off >= off_stop:
                     return False
                     return False
-                size = min(size, stop_off-off)
-            off -= start_off
+                size = min(size, off_stop-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),
-        help="Show a specific block.")
+        type=lambda x: tuple(int(x,0) if x else None for x in x.split(',',1)),
+        help="Show a specific block or range of blocks.")
     parser.add_argument(
     parser.add_argument(
-        '--start',
-        type=lambda x: int(x, 0),
-        help="Start at this block.")
-    parser.add_argument(
-        '--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.")
+        '-i',
+        '--off',
+        type=lambda x: tuple(int(x,0) if x else None for x in x.split(',',1)),
+        help="Show a specific offset or range of offsets.")
     parser.add_argument(
     parser.add_argument(
         '-B',
         '-B',
         '--block-size',
         '--block-size',