centralize test-spec parsing and matching

This cleans up the code and fixes inconsistencies between different users of
the test-spec.
diff --git a/oft b/oft
index 8bd9340..d7aeb48 100755
--- a/oft
+++ b/oft
@@ -90,7 +90,7 @@
 Examine oft.log if things don't work.
 
 @todo Support per-component debug levels (esp controller vs dataplane)
-@todo Consider moving oft up a level
+@todo Allow specification of priority to override prio check
 
 Current test case setup:
     Files in the tests direcoty that contain a function test_set_init are
@@ -356,15 +356,66 @@
 
     return result
 
+def prune_tests(test_spec, test_modules):
+    """
+    Return tests matching a given test-spec.
+    @param test_spec A test-spec string.
+    @param test_modules Same format as the output of load_test_modules.
+    @returns Same format as the output of load_test_modules.
+    """
+    result = {}
+    for (spec_modname, spec_testname) in parse_test_spec(test_spec):
+        matched = False
+        for (modname, (mod, tests)) in test_modules.items():
+            if (spec_modname == None or spec_modname == modname):
+                for (testname, test) in tests.items():
+                    if (spec_testname == None or spec_testname == testname):
+                            result.setdefault(modname, (mod, {}))
+                            result[modname][1][testname] = test
+                            matched = True
+        if not matched:
+            if spec_modname and spec_testname:
+                el = "%s.%s" % (spec_modname, spec_testname)
+            else:
+                el = spec_modname or spec_testname or "all"
+            die("test-spec element %s did not match any tests" % el)
+    return result
+
+def parse_test_spec(test_spec):
+    """
+    The input string is split on commas and each element is parsed
+    individually into a module name and test name. Either may be None
+    for a wildcard. The case of the first letter resolves ambiguity
+    of whether a word is a test or module name. The special string
+    "all" results in both fields wildcarded.
+
+    Examples:
+      basic.Echo -> ("basic", "Echo")
+      basic -> ("basic", None)
+      Echo -> (None, "Echo")
+      all -> (None, None)
+    """
+    results = []
+    for ts_entry in test_spec.split(","):
+        parts = ts_entry.split(".")
+        if len(parts) == 1:
+            if ts_entry == "all":
+                results.append((None, None))
+            elif ts_entry[0].isupper():
+                results.append((None, ts_entry))
+            else:
+                results.append((ts_entry, None))
+        elif len(parts) == 2:
+            results.append((parts[0], parts[1]))
+        else:
+            die("Bad test spec: " + ts_entry)
+    return results
+
 def die(msg, exit_val=1):
     print msg
     logging.critical(msg)
     sys.exit(exit_val)
 
-def add_test(suite, mod, test):
-    logging.info("Adding test " + mod.__name__ + "." + test.__name__)
-    suite.addTest(test())
-
 def _space_to(n, str):
     """
     Generate a string of spaces to achieve width n given string str
@@ -405,23 +456,18 @@
 # Allow tests to import each other
 sys.path.append(config["test_dir"])
 
-test_modules = load_test_modules(config)
+test_modules = prune_tests(config["test_spec"], load_test_modules(config))
 oft_config = config
 
 load_profile(config)
 
 # Check if test list is requested; display and exit if so
 if config["list"]:
-    did_print = False
     mod_count = 0
     test_count = 0
     print "\nTest List:"
     for (modname, (mod, tests)) in test_modules.items():
-        if config["test_spec"] != "all" and \
-                config["test_spec"] != modname:
-            continue
         mod_count += 1
-        did_print = True
         desc = mod.__doc__.strip()
         desc = desc.split('\n')[0]
         start_str = "  Module " + mod.__name__ + ": "
@@ -441,13 +487,10 @@
             print start_str + _space_to(22, start_str) + desc
             test_count += 1
         print
-    if not did_print:
-        print "No tests found for " + config["test_spec"]
-    else:
-        print "%d modules shown with a total of %d tests" % \
-            (mod_count, test_count)
-        print
-        print "Tests preceded by * are not run by default"
+    print "%d modules shown with a total of %d tests" % \
+        (mod_count, test_count)
+    print
+    print "Tests preceded by * are not run by default"
     print "Tests marked (TP1) after name take --test-params including:"
     print "    'vid=N;strip_vlan=bool;add_vlan=bool'"
     print "Note that --profile may override which tests are run"
@@ -456,9 +499,6 @@
 # Check if test list is requested; display and exit if so
 if config["list_test_names"]:
     for (modname, (mod, tests)) in test_modules.items():
-        if config["test_spec"] != "all" and \
-                config["test_spec"] != modname:
-            continue
         for (testname, test) in tests.items():
             if test_prio_get(mod, testname) >= 0:
                 print "%s.%s" % (modname, testname)
@@ -468,46 +508,11 @@
 #@todo Decide if multiple suites are ever needed
 suite = unittest.TestSuite()
 
-#@todo Allow specification of priority to override prio check
-if config["test_spec"] == "all":
-    for (modname, (mod, tests)) in test_modules.items():
-        for (testname, test) in tests:
-            # For now, a way to avoid tests
-            if test_prio_get(mod, testname) >= 0:
-                add_test(suite, mod, test)
-
-else:
-    for ts_entry in config["test_spec"].split(","):
-        parts = ts_entry.split(".")
-
-        if len(parts) == 1: # Either a module or test name
-            if ts_entry in test_modules:
-                (mod, tests) = test_modules[ts_entry]
-                for testname, test in tests.items():
-                    if test_prio_get(mod, testname) >= 0:
-                        add_test(suite, mod, test)
-            else: # Search for matching tests
-                test_found = False
-                for (modname, (mod, tests)) in test_modules.items():
-                    if ts_entry in tests:
-                        add_test(suite, mod, tests[ts_entry])
-                        test_found = True
-                if not test_found:
-                    die("Could not find module or test: " + ts_entry)
-
-        elif len(parts) == 2: # module.test
-            if parts[0] not in test_modules:
-                die("Unknown module in test spec: " + ts_entry)
-            modname = parts[0]
-            testname = parts[1]
-            (mod, tests) = test_modules[modname]
-            if testname in tests:
-                add_test(suite, mod, tests[testname])
-            else:
-                die("No known test matches: " + ts_entry)
-
-        else:
-            die("Bad test spec: " + ts_entry)
+for (modname, (mod, tests)) in test_modules.items():
+    for (testname, test) in tests.items():
+        if test_prio_get(mod, testname) >= 0:
+            logging.info("Adding test " + modname + "." + testname)
+            suite.addTest(test())
 
 # Load the platform module
 platform_name = config["platform"]