Allow running tests under valgrind
diff --git a/tools/run_tests/run_tests.py b/tools/run_tests/run_tests.py
index c9aedc1..2bd2e6d 100755
--- a/tools/run_tests/run_tests.py
+++ b/tools/run_tests/run_tests.py
@@ -11,14 +11,42 @@
 import jobset
 import watch_dirs
 
-# flags required for make for each configuration
-_CONFIGS = ['dbg', 'opt', 'tsan', 'msan', 'asan']
+
+# SimpleConfig: just compile with CONFIG=config, and run the binary to test
+class SimpleConfig(object):
+  def __init__(self, config):
+    self.build_config = config
+
+  def run_command(self, binary):
+    return [binary]
+
+
+# ValgrindConfig: compile with some CONFIG=config, but use valgrind to run
+class ValgrindConfig(object):
+  def __init__(self, config):
+    self.build_config = config
+
+  def run_command(self, binary):
+    return ['valgrind', binary]
+
+
+# different configurations we can run under
+_CONFIGS = {
+  'dbg': SimpleConfig('dbg'),
+  'opt': SimpleConfig('opt'),
+  'tsan': SimpleConfig('tsan'),
+  'msan': SimpleConfig('msan'),
+  'asan': SimpleConfig('asan'),
+  'valgrind': ValgrindConfig('dbg'),
+  }
+
+
 _DEFAULT = ['dbg', 'opt']
 
 # parse command line
 argp = argparse.ArgumentParser(description='Run grpc tests.')
 argp.add_argument('-c', '--config',
-                  choices=['all'] + _CONFIGS,
+                  choices=['all'] + sorted(_CONFIGS.keys()),
                   nargs='+',
                   default=_DEFAULT)
 argp.add_argument('-t', '--test-filter', nargs='*', default=['*'])
@@ -30,10 +58,11 @@
 args = argp.parse_args()
 
 # grab config
-configs = [cfg
-           for cfg in itertools.chain.from_iterable(
-               _CONFIGS if x == 'all' else [x]
-               for x in args.config)]
+run_configs = set(_CONFIGS[cfg]
+                  for cfg in itertools.chain.from_iterable(
+                      _CONFIGS.iterkeys() if x == 'all' else [x]
+                      for x in args.config))
+build_configs = set(cfg.build_config for cfg in run_configs)
 filters = args.test_filter
 runs_per_test = args.runs_per_test
 forever = args.forever
@@ -44,20 +73,21 @@
   # build latest, sharing cpu between the various makes
   if not jobset.run(
       (['make',
-        '-j', '%d' % max(multiprocessing.cpu_count() / len(configs), 1),
+        '-j', '%d' % (multiprocessing.cpu_count() + 1),
         'buildtests_c',
         'CONFIG=%s' % cfg]
-       for cfg in configs), check_cancelled):
+       for cfg in build_configs), check_cancelled):
     sys.exit(1)
 
   # run all the tests
-  jobset.run(([x]
-              for x in itertools.chain.from_iterable(
-                  itertools.chain.from_iterable(itertools.repeat(
-                      glob.glob('bins/%s/%s_test' % (config, filt)),
-                      runs_per_test))
-                  for config in configs
-                  for filt in filters)), check_cancelled)
+  jobset.run((
+      config.run_command(x)
+      for config in run_configs
+      for filt in filters
+      for x in itertools.chain.from_iterable(itertools.repeat(
+          glob.glob('bins/%s/%s_test' % (
+              config.build_config, filt)),
+          runs_per_test))), check_cancelled)
 
 
 if forever: