Browse Source

In ./scripts/test.py, readded external commands, tweaked subprocesses

- Added --exec for wrapping the test-runner with external commands, such as
  Qemu or Valgrind.

- Added --valgrind, which just aliases --exec=valgrind with a few extra
  flags useful during testing.

- Dropped the "valgrind" type for tests. These aren't separate tests
  that run in the test-runner, and I don't see a need for disabling
  Valgrind for any tests. This can be added back later if needed.

- Readded support for dropping directly into gdb after a test failure,
  either at the assert failure, entry point of test case, or entry point
  of the test runner with --gdb, --gdb-case, or --gdb-main.

- Added --isolate for running each test permutation in its own process,
  this is required for associating Valgrind errors with the right test
  case.

- Fixed an issue where explicit test identifier conflicted with
  per-stage test identifiers generated as a part of --by-suite and
  --by-case.
Christopher Haster 3 years ago
parent
commit
d679fbb389
3 changed files with 103 additions and 53 deletions
  1. 6 15
      runners/test_runner.c
  2. 0 1
      runners/test_runner.h
  3. 97 37
      scripts/test_.py

+ 6 - 15
runners/test_runner.c

@@ -235,10 +235,9 @@ static void summary(void) {
     char perm_buf[64];
     char perm_buf[64];
     sprintf(perm_buf, "%zu/%zu", filtered, perms);
     sprintf(perm_buf, "%zu/%zu", filtered, perms);
     char type_buf[64];
     char type_buf[64];
-    sprintf(type_buf, "%s%s%s",
+    sprintf(type_buf, "%s%s",
             (types & TEST_NORMAL)    ? "n" : "",
             (types & TEST_NORMAL)    ? "n" : "",
-            (types & TEST_REENTRANT) ? "r" : "",
-            (types & TEST_VALGRIND)  ? "V" : "");
+            (types & TEST_REENTRANT) ? "r" : "");
     printf("%-36s %7s %7zu %7zu %11s\n",
     printf("%-36s %7s %7zu %7zu %11s\n",
             "TOTAL",
             "TOTAL",
             type_buf,
             type_buf,
@@ -270,10 +269,9 @@ static void list_suites(void) {
         char perm_buf[64];
         char perm_buf[64];
         sprintf(perm_buf, "%zu/%zu", filtered, perms);
         sprintf(perm_buf, "%zu/%zu", filtered, perms);
         char type_buf[64];
         char type_buf[64];
-        sprintf(type_buf, "%s%s%s",
+        sprintf(type_buf, "%s%s",
                 (test_suites[i]->types & TEST_NORMAL)    ? "n" : "",
                 (test_suites[i]->types & TEST_NORMAL)    ? "n" : "",
-                (test_suites[i]->types & TEST_REENTRANT) ? "r" : "",
-                (test_suites[i]->types & TEST_VALGRIND)  ? "V" : "");
+                (test_suites[i]->types & TEST_REENTRANT) ? "r" : "");
         printf("%-36s %7s %7zu %11s\n",
         printf("%-36s %7s %7zu %11s\n",
                 test_suites[i]->id,
                 test_suites[i]->id,
                 type_buf,
                 type_buf,
@@ -305,10 +303,9 @@ static void list_cases(void) {
             char perm_buf[64];
             char perm_buf[64];
             sprintf(perm_buf, "%zu/%zu", filtered, perms);
             sprintf(perm_buf, "%zu/%zu", filtered, perms);
             char type_buf[64];
             char type_buf[64];
-            sprintf(type_buf, "%s%s%s",
+            sprintf(type_buf, "%s%s",
                     (types & TEST_NORMAL)    ? "n" : "",
                     (types & TEST_NORMAL)    ? "n" : "",
-                    (types & TEST_REENTRANT) ? "r" : "",
-                    (types & TEST_VALGRIND)  ? "V" : "");
+                    (types & TEST_REENTRANT) ? "r" : "");
             printf("%-36s %7s %11s\n",
             printf("%-36s %7s %11s\n",
                     test_suites[i]->cases[j]->id,
                     test_suites[i]->cases[j]->id,
                     type_buf,
                     type_buf,
@@ -532,7 +529,6 @@ enum opt_flags {
     OPT_GEOMETRY        = 'G',
     OPT_GEOMETRY        = 'G',
     OPT_NORMAL          = 'n',
     OPT_NORMAL          = 'n',
     OPT_REENTRANT       = 'r',
     OPT_REENTRANT       = 'r',
-    OPT_VALGRIND        = 'V',
     OPT_START           = 5,
     OPT_START           = 5,
     OPT_STEP            = 6,
     OPT_STEP            = 6,
     OPT_STOP            = 7,
     OPT_STOP            = 7,
@@ -555,7 +551,6 @@ const struct option long_opts[] = {
     {"geometry",        required_argument, NULL, OPT_GEOMETRY},
     {"geometry",        required_argument, NULL, OPT_GEOMETRY},
     {"normal",          no_argument,       NULL, OPT_NORMAL},
     {"normal",          no_argument,       NULL, OPT_NORMAL},
     {"reentrant",       no_argument,       NULL, OPT_REENTRANT},
     {"reentrant",       no_argument,       NULL, OPT_REENTRANT},
-    {"valgrind",        no_argument,       NULL, OPT_VALGRIND},
     {"start",           required_argument, NULL, OPT_START},
     {"start",           required_argument, NULL, OPT_START},
     {"stop",            required_argument, NULL, OPT_STOP},
     {"stop",            required_argument, NULL, OPT_STOP},
     {"step",            required_argument, NULL, OPT_STEP},
     {"step",            required_argument, NULL, OPT_STEP},
@@ -577,7 +572,6 @@ const char *const help_text[] = {
     "Filter by geometry.",
     "Filter by geometry.",
     "Filter for normal tests. Can be combined.",
     "Filter for normal tests. Can be combined.",
     "Filter for reentrant tests. Can be combined.",
     "Filter for reentrant tests. Can be combined.",
-    "Filter for Valgrind tests. Can be combined.",
     "Start at the nth test.",
     "Start at the nth test.",
     "Stop before the nth test.",
     "Stop before the nth test.",
     "Only run every n tests, calculated after --start and --stop.",
     "Only run every n tests, calculated after --start and --stop.",
@@ -724,9 +718,6 @@ invalid_define:
             case OPT_REENTRANT:
             case OPT_REENTRANT:
                 test_types |= TEST_REENTRANT;
                 test_types |= TEST_REENTRANT;
                 break;
                 break;
-            case OPT_VALGRIND:
-                test_types |= TEST_VALGRIND;
-                break;
             case OPT_START: {
             case OPT_START: {
                 char *parsed = NULL;
                 char *parsed = NULL;
                 test_start = strtoumax(optarg, &parsed, 0);
                 test_start = strtoumax(optarg, &parsed, 0);

+ 0 - 1
runners/test_runner.h

@@ -8,7 +8,6 @@
 enum test_types {
 enum test_types {
     TEST_NORMAL    = 0x1,
     TEST_NORMAL    = 0x1,
     TEST_REENTRANT = 0x2,
     TEST_REENTRANT = 0x2,
-    TEST_VALGRIND  = 0x4,
 };
 };
 
 
 typedef uint8_t test_types_t;
 typedef uint8_t test_types_t;

+ 97 - 37
scripts/test_.py

@@ -13,6 +13,7 @@ import pty
 import re
 import re
 import shlex
 import shlex
 import shutil
 import shutil
+import signal
 import subprocess as sp
 import subprocess as sp
 import threading as th
 import threading as th
 import time
 import time
@@ -77,8 +78,6 @@ class TestCase:
             config.pop('suite_normal', True))
             config.pop('suite_normal', True))
         self.reentrant = config.pop('reentrant',
         self.reentrant = config.pop('reentrant',
             config.pop('suite_reentrant', False))
             config.pop('suite_reentrant', False))
-        self.valgrind = config.pop('valgrind',
-            config.pop('suite_valgrind', True))
 
 
         # figure out defines and build possible permutations
         # figure out defines and build possible permutations
         self.defines = set()
         self.defines = set()
@@ -163,7 +162,6 @@ class TestSuite:
             in_ = config.pop('in', None)
             in_ = config.pop('in', None)
             normal = config.pop('normal', True)
             normal = config.pop('normal', True)
             reentrant = config.pop('reentrant', False)
             reentrant = config.pop('reentrant', False)
-            valgrind = config.pop('valgrind', True)
 
 
             self.cases = []
             self.cases = []
             for name, case in sorted(cases.items(),
             for name, case in sorted(cases.items(),
@@ -177,7 +175,6 @@ class TestSuite:
                     'suite_in': in_,
                     'suite_in': in_,
                     'suite_normal': normal,
                     'suite_normal': normal,
                     'suite_reentrant': reentrant,
                     'suite_reentrant': reentrant,
-                    'suite_valgrind': valgrind,
                     **case}))
                     **case}))
 
 
             # combine per-case defines
             # combine per-case defines
@@ -187,7 +184,6 @@ class TestSuite:
             # combine other per-case things
             # combine other per-case things
             self.normal = any(case.normal for case in self.cases)
             self.normal = any(case.normal for case in self.cases)
             self.reentrant = any(case.reentrant for case in self.cases)
             self.reentrant = any(case.reentrant for case in self.cases)
-            self.valgrind = any(case.valgrind for case in self.cases)
 
 
         for k in config.keys():
         for k in config.keys():
             print('\x1b[01;33mwarning:\x1b[m in %s, found unused key %r'
             print('\x1b[01;33mwarning:\x1b[m in %s, found unused key %r'
@@ -202,7 +198,7 @@ class TestSuite:
 def compile(**args):
 def compile(**args):
     # find .toml files
     # find .toml files
     paths = []
     paths = []
-    for path in args.get('test_paths', TEST_PATHS):
+    for path in args.get('test_ids', TEST_PATHS):
         if os.path.isdir(path):
         if os.path.isdir(path):
             path = path + '/*.toml'
             path = path + '/*.toml'
 
 
@@ -210,13 +206,13 @@ def compile(**args):
             paths.append(path)
             paths.append(path)
 
 
     if not paths:
     if not paths:
-        print('no test suites found in %r?' % args['test_paths'])
+        print('no test suites found in %r?' % args['test_ids'])
         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)'
-                % args['test_paths'])
+                % args['test_ids'])
             sys.exit(-1)
             sys.exit(-1)
 
 
         # load our suite
         # load our suite
@@ -373,8 +369,7 @@ def compile(**args):
                     f.writeln(4*' '+'.types = %s,'
                     f.writeln(4*' '+'.types = %s,'
                         % ' | '.join(filter(None, [
                         % ' | '.join(filter(None, [
                             'TEST_NORMAL' if case.normal else None,
                             'TEST_NORMAL' if case.normal else None,
-                            'TEST_REENTRANT' if case.reentrant else None,
-                            'TEST_VALGRIND' if case.valgrind else None])))
+                            'TEST_REENTRANT' if case.reentrant else None])))
                     f.writeln(4*' '+'.permutations = %d,'
                     f.writeln(4*' '+'.permutations = %d,'
                         % len(case.permutations))
                         % len(case.permutations))
                     if case.defines:
                     if case.defines:
@@ -406,8 +401,7 @@ def compile(**args):
                 f.writeln(4*' '+'.types = %s,'
                 f.writeln(4*' '+'.types = %s,'
                     % ' | '.join(filter(None, [
                     % ' | '.join(filter(None, [
                         'TEST_NORMAL' if suite.normal else None,
                         'TEST_NORMAL' if suite.normal else None,
-                        'TEST_REENTRANT' if suite.reentrant else None,
-                        'TEST_VALGRIND' if suite.valgrind else None])))
+                        'TEST_REENTRANT' if suite.reentrant else None])))
                 if suite.defines:
                 if suite.defines:
                     f.writeln(4*' '+'.define_names = __test__%s__define_names,'
                     f.writeln(4*' '+'.define_names = __test__%s__define_names,'
                         % suite.name)
                         % suite.name)
@@ -434,7 +428,9 @@ def compile(**args):
                 # write any internal tests
                 # write any internal tests
                 for suite in suites:
                 for suite in suites:
                     for case in suite.cases:
                     for case in suite.cases:
-                        if case.in_ == args.get('source'):
+                        if (case.in_ is not None
+                                and os.path.normpath(case.in_)
+                                    == os.path.normpath(args['source'])):
                             # write defines, but note we need to undef any
                             # write defines, but note we need to undef any
                             # new defines since we're in someone else's file
                             # new defines since we're in someone else's file
                             if suite.defines:
                             if suite.defines:
@@ -475,15 +471,27 @@ def compile(**args):
 
 
 def runner(**args):
 def runner(**args):
     cmd = args['runner'].copy()
     cmd = args['runner'].copy()
-    # TODO multiple paths?
-    if 'test_paths' in args:
-        cmd.extend(args.get('test_paths'))
+    cmd.extend(args.get('test_ids'))
 
 
+    # run under some external command?
+    cmd[:0] = args.get('exec', [])
+
+    # run under valgrind?
+    if args.get('valgrind'):
+        cmd[:0] = filter(None, [
+            'valgrind',
+            '--leak-check=full',
+            '--track-origins=yes',
+            '--error-exitcode=4',
+            '-q'])
+
+    # filter tests?
     if args.get('normal'):    cmd.append('-n')
     if args.get('normal'):    cmd.append('-n')
     if args.get('reentrant'): cmd.append('-r')
     if args.get('reentrant'): cmd.append('-r')
-    if args.get('valgrind'):  cmd.append('-V')
     if args.get('geometry'):
     if args.get('geometry'):
         cmd.append('-G%s' % args.get('geometry'))
         cmd.append('-G%s' % args.get('geometry'))
+
+    # defines?
     if args.get('define'):
     if args.get('define'):
         for define in args.get('define'):
         for define in args.get('define'):
             cmd.append('-D%s' % define)
             cmd.append('-D%s' % define)
@@ -522,8 +530,7 @@ def find_cases(runner_, **args):
         '^(?P<id>(?P<case>(?P<suite>[^#]+)#[^\s#]+)[^\s]*)\s+'
         '^(?P<id>(?P<case>(?P<suite>[^#]+)#[^\s#]+)[^\s]*)\s+'
             '[^\s]+\s+(?P<filtered>\d+)/(?P<perms>\d+)')
             '[^\s]+\s+(?P<filtered>\d+)/(?P<perms>\d+)')
     # skip the first line
     # skip the first line
-    next(proc.stdout)
-    for line in proc.stdout:
+    for line in it.islice(proc.stdout, 1, None):
         m = pattern.match(line)
         m = pattern.match(line)
         if m:
         if m:
             filtered = int(m.group('filtered'))
             filtered = int(m.group('filtered'))
@@ -558,7 +565,6 @@ def find_paths(runner_, **args):
     pattern = re.compile(
     pattern = re.compile(
         '^(?P<id>(?P<case>(?P<suite>[^#]+)#[^\s#]+)[^\s]*)\s+'
         '^(?P<id>(?P<case>(?P<suite>[^#]+)#[^\s#]+)[^\s]*)\s+'
             '(?P<path>[^:]+):(?P<lineno>\d+)')
             '(?P<path>[^:]+):(?P<lineno>\d+)')
-    # skip the first line
     for line in proc.stdout:
     for line in proc.stdout:
         m = pattern.match(line)
         m = pattern.match(line)
         if m:
         if m:
@@ -586,7 +592,6 @@ def find_defines(runner_, **args):
     pattern = re.compile(
     pattern = re.compile(
         '^(?P<id>(?P<case>(?P<suite>[^#]+)#[^\s#]+)[^\s]*)\s+'
         '^(?P<id>(?P<case>(?P<suite>[^#]+)#[^\s#]+)[^\s]*)\s+'
             '(?P<defines>(?:\w+=\w+\s*)+)')
             '(?P<defines>(?:\w+=\w+\s*)+)')
-    # skip the first line
     for line in proc.stdout:
     for line in proc.stdout:
         m = pattern.match(line)
         m = pattern.match(line)
         if m:
         if m:
@@ -614,7 +619,6 @@ def run_stage(name, runner_, **args):
     expected_suite_perms, expected_case_perms, expected_perms, total_perms = (
     expected_suite_perms, expected_case_perms, expected_perms, total_perms = (
         find_cases(runner_, **args))
         find_cases(runner_, **args))
 
 
-    # TODO valgrind/gdb/exec
     passed_suite_perms = co.defaultdict(lambda: 0)
     passed_suite_perms = co.defaultdict(lambda: 0)
     passed_case_perms = co.defaultdict(lambda: 0)
     passed_case_perms = co.defaultdict(lambda: 0)
     passed_perms = 0
     passed_perms = 0
@@ -627,7 +631,6 @@ def run_stage(name, runner_, **args):
             '|' '(?P<path>[^:]+):(?P<lineno>\d+):(?P<op_>assert):'
             '|' '(?P<path>[^:]+):(?P<lineno>\d+):(?P<op_>assert):'
                 ' *(?P<message>.*)' ')$')
                 ' *(?P<message>.*)' ')$')
     locals = th.local()
     locals = th.local()
-    # TODO use process group instead of this set?
     children = set()
     children = set()
 
 
     def run_runner(runner_):
     def run_runner(runner_):
@@ -714,17 +717,23 @@ def run_stage(name, runner_, **args):
         nonlocal failures
         nonlocal failures
         nonlocal locals
         nonlocal locals
 
 
-        while (start or 0) < total_perms:
+        start = start or 0
+        step = step or 1
+        while start < total_perms:
             runner_ = runner.copy()
             runner_ = runner.copy()
             if start is not None:
             if start is not None:
                 runner_.append('--start=%d' % start)
                 runner_.append('--start=%d' % start)
             if step is not None:
             if step is not None:
                 runner_.append('--step=%d' % step)
                 runner_.append('--step=%d' % step)
+            if args.get('isolate') or args.get('valgrind'):
+                runner_.append('--stop=%d' % (start+step))
 
 
             try:
             try:
                 # run the tests
                 # run the tests
                 locals.seen_perms = 0
                 locals.seen_perms = 0
                 run_runner(runner_)
                 run_runner(runner_)
+                assert locals.seen_perms > 0
+                start += locals.seen_perms*step
 
 
             except TestFailure as failure:
             except TestFailure as failure:
                 # race condition for multiple failures?
                 # race condition for multiple failures?
@@ -735,14 +744,13 @@ def run_stage(name, runner_, **args):
 
 
                 if args.get('keep_going') and not killed:
                 if args.get('keep_going') and not killed:
                     # resume after failed test
                     # resume after failed test
-                    start = (start or 0) + locals.seen_perms*(step or 1)
+                    assert locals.seen_perms > 0
+                    start += locals.seen_perms*step
                     continue
                     continue
                 else:
                 else:
                     # stop other tests
                     # stop other tests
                     for child in children.copy():
                     for child in children.copy():
                         child.kill()
                         child.kill()
-
-            break
     
     
 
 
     # parallel jobs?
     # parallel jobs?
@@ -808,7 +816,7 @@ def run(**args):
     start = time.time()
     start = time.time()
 
 
     runner_ = runner(**args)
     runner_ = runner(**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 = (
         find_cases(runner_, **args))
         find_cases(runner_, **args))
@@ -823,15 +831,19 @@ def run(**args):
     passed = 0
     passed = 0
     failures = []
     failures = []
     for type, by in it.product(
     for type, by in it.product(
-            ['normal', 'reentrant', 'valgrind'],
+            ['normal', 'reentrant'],
             expected_case_perms.keys() if args.get('by_cases')
             expected_case_perms.keys() if args.get('by_cases')
                 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
+        stage_runner = runner(**args | {
+            'test_ids': [by] if by is not None else args.get('test_ids', []),
+            'normal': type == 'normal',
+            'reentrant': type == 'reentrant'})
 
 
+        # spawn jobs for stage
         expected_, passed_, failures_, killed = run_stage(
         expected_, passed_, failures_, killed = run_stage(
-            '%s %s' % (type, by or 'tests'),
-            runner_ + ['--%s' % type] + ([by] if by is not None else []),
-            **args)
+            '%s %s' % (type, by or 'tests'), stage_runner, **args)
         expected += expected_
         expected += expected_
         passed += passed_
         passed += passed_
         failures.extend(failures_)
         failures.extend(failures_)
@@ -879,6 +891,40 @@ def run(**args):
                 print(line)
                 print(line)
         print()
         print()
 
 
+    # drop into gdb?
+    if failures and (args.get('gdb')
+            or args.get('gdb_case')
+            or args.get('gdb_main')):
+        failure = failures[0]
+        runner_ = runner(**args | {'test_ids': [failure.id]})
+
+        if args.get('gdb_main'):
+            cmd = ['gdb',
+                '-ex', 'break main',
+                '-ex', 'run',
+                '--args'] + runner_
+        elif args.get('gdb_case'):
+            path, lineno = runner_paths[testcase(failure.id)]
+            cmd = ['gdb',
+                '-ex', 'break %s:%d' % (path, lineno),
+                '-ex', 'run',
+                '--args'] + runner_
+        elif failure.assert_ is not None:
+            cmd = ['gdb',
+                '-ex', 'run',
+                '-ex', 'frame function raise',
+                '-ex', 'up 2',
+                '--args'] + runner_
+        else:
+            cmd = ['gdb',
+                '-ex', 'run',
+                '--args'] + runner_
+
+        # exec gdb interactively
+        if args.get('verbose'):
+            print(' '.join(shlex.quote(c) for c in cmd))
+        os.execvp(cmd[0], cmd)
+
     return 1 if failures else 0
     return 1 if failures else 0
 
 
 
 
@@ -903,10 +949,11 @@ if __name__ == "__main__":
     parser = argparse.ArgumentParser(
     parser = argparse.ArgumentParser(
         description="Build and run tests.",
         description="Build and run tests.",
         conflict_handler='resolve')
         conflict_handler='resolve')
-    # TODO document test case/perm specifier
-    parser.add_argument('test_paths', nargs='*',
+    parser.add_argument('test_ids', nargs='*',
         help="Description of testis to run. May be a directory, path, or \
         help="Description of testis to run. May be a directory, path, or \
-            test identifier. Defaults to %r." % TEST_PATHS)
+            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 %r." % 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.")
     # test flags
     # test flags
@@ -933,8 +980,6 @@ if __name__ == "__main__":
         help="Filter for normal tests. Can be combined.")
         help="Filter for normal tests. Can be combined.")
     test_parser.add_argument('-r', '--reentrant', action='store_true',
     test_parser.add_argument('-r', '--reentrant', action='store_true',
         help="Filter for reentrant tests. Can be combined.")
         help="Filter for reentrant tests. Can be combined.")
-    test_parser.add_argument('-V', '--valgrind', action='store_true',
-        help="Filter for Valgrind tests. Can be combined.")
     test_parser.add_argument('-d', '--disk',
     test_parser.add_argument('-d', '--disk',
         help="Use this file as the disk.")
         help="Use this file as the disk.")
     test_parser.add_argument('-t', '--trace',
     test_parser.add_argument('-t', '--trace',
@@ -949,10 +994,25 @@ if __name__ == "__main__":
         help="Number of parallel runners to run.")
         help="Number of parallel runners to run.")
     test_parser.add_argument('-k', '--keep-going', action='store_true',
     test_parser.add_argument('-k', '--keep-going', action='store_true',
         help="Don't stop on first error.")
         help="Don't stop on first error.")
+    test_parser.add_argument('-i', '--isolate', action='store_true',
+        help="Run each test permutation in a separate process.")
     test_parser.add_argument('-b', '--by-suites', action='store_true',
     test_parser.add_argument('-b', '--by-suites', action='store_true',
         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('--gdb', action='store_true',
+        help="Drop into gdb on test failure.")
+    test_parser.add_argument('--gdb-case', action='store_true',
+        help="Drop into gdb on test failure but stop at the beginning \
+            of the failing test case.")
+    test_parser.add_argument('--gdb-main', action='store_true',
+        help="Drop into gdb on test failure but stop at the beginning \
+            of main.")
+    test_parser.add_argument('--valgrind', action='store_true',
+        help="Run under Valgrind to find memory errors. Implicitly sets \
+            --isolate.")
+    test_parser.add_argument('--exec', default=[], type=lambda e: e.split(),
+        help="Run under another executable.")
     # compilation flags
     # compilation flags
     comp_parser = parser.add_argument_group('compilation options')
     comp_parser = parser.add_argument_group('compilation options')
     comp_parser.add_argument('-c', '--compile', action='store_true',
     comp_parser.add_argument('-c', '--compile', action='store_true',