add version decorator

Also adds the TableModConfig test to the main basic test module, using the new
decorator.
diff --git a/oft b/oft
index 11e181b..00e31bd 100755
--- a/oft
+++ b/oft
@@ -284,10 +284,11 @@
 
     return result
 
-def prune_tests(test_specs, test_modules):
+def prune_tests(test_specs, test_modules, version):
     """
-    Return tests matching the given test-specs
+    Return tests matching the given test-specs and OpenFlow version
     @param test_specs A list of group names or test names.
+    @param version An OpenFlow version (e.g. "1.0")
     @param test_modules Same format as the output of load_test_modules.
     @returns Same format as the output of load_test_modules.
     """
@@ -306,7 +307,8 @@
                 if e in test._groups or e == "%s.%s" % (modname, testname):
                     result.setdefault(modname, (mod, {}))
                     if not negated:
-                        result[modname][1][testname] = test
+                        if not hasattr(test, "_versions") or version in test._versions:
+                            result[modname][1][testname] = test
                     else:
                         if modname in result and testname in result[modname][1]:
                             del result[modname][1][testname]
@@ -419,6 +421,8 @@
             all_groups.update(test._groups)
             if groups:
                 desc = "(%s) %s" % (",".join(groups), desc)
+            if hasattr(test, "_versions"):
+                desc = "(%s) %s" % (",".join(sorted(test._versions)), desc)
             start_str = " %s%s %s:" % (test._nonstandard and "*" or " ",
                                        test._disabled and "!" or " ",
                                        testname)
@@ -434,7 +438,7 @@
 
     sys.exit(0)
 
-test_modules = prune_tests(test_specs, test_modules)
+test_modules = prune_tests(test_specs, test_modules, config["openflow_version"])
 
 # Check if test list is requested; display and exit if so
 if config["list_test_names"]:
diff --git a/src/python/oftest/testutils.py b/src/python/oftest/testutils.py
index ef2e517..6738d42 100644
--- a/src/python/oftest/testutils.py
+++ b/src/python/oftest/testutils.py
@@ -3,6 +3,7 @@
 import logging
 import types
 import time
+import re
 
 try:
     import scapy.all as scapy
@@ -1050,3 +1051,44 @@
         cls._groups.append(name)
         return cls
     return fn
+
+def version(ver):
+    """
+    Testcase decorator that specifies which versions of OpenFlow the test
+    supports. The default is 1.0+. This decorator may only be used once.
+
+    Supported syntax:
+    1.0 -> 1.0
+    1.0,1.2,1.3 -> 1.0, 1.2, 1.3
+    1.0+ -> 1.0, 1.1, 1.2, 1.3
+    """
+    versions = parse_version(ver)
+    def fn(cls):
+        cls._versions = versions
+        return cls
+    return fn
+
+def parse_version(ver):
+    allowed_versions = ["1.0", "1.1", "1.2", "1.3"]
+    if re.match("^1\.\d+$", ver):
+        versions = set([ver])
+    elif re.match("^(1\.\d+)\+$", ver):
+        if not ver[:-1] in allowed_versions:
+            raise ValueError("invalid OpenFlow version %s" % ver[:-1])
+        versions = set()
+        if ver != "1.1+": versions.add("1.0")
+        if ver != "1.2+": versions.add("1.1")
+        if ver != "1.3+": versions.add("1.2")
+        versions.add("1.3")
+    else:
+        versions = set(ver.split(','))
+
+    for version in versions:
+        if not version in allowed_versions:
+            raise ValueError("invalid OpenFlow version %s" % version)
+
+    return versions
+
+assert(parse_version("1.0") == set(["1.0"]))
+assert(parse_version("1.0,1.2,1.3") == set(["1.0", "1.2", "1.3"]))
+assert(parse_version("1.0+") == set(["1.0", "1.1", "1.2", "1.3"]))
diff --git a/tests/basic.py b/tests/basic.py
index 550ac66..54aea3a 100644
--- a/tests/basic.py
+++ b/tests/basic.py
@@ -399,5 +399,47 @@
         self.assertTrue(reply.code == ofp.OFPBRC_BAD_TYPE,
                         "reply error code is not bad type")
 
+@group('smoke')
+@version('1.1+')
+class TableModConfig(base_tests.SimpleProtocol):
+    """
+    Simple table modification
+
+    Mostly to make sure the switch correctly responds to these messages.
+    More complicated tests in the multi-tables.py tests
+    """
+    def runTest(self):
+        # First table should always exist
+        table_id = 0
+
+        def get_table_config():
+            request = ofp.message.table_stats_request()
+            response, _ = self.controller.transact(request)
+            try:
+                table_stats = [x for x in response.stats if x.table_id == table_id][0]
+            except IndexError:
+                raise AssertionError("table id %d not found" % table_id)
+            return table_stats.config
+
+        # Get current configuration
+        orig_table_config = get_table_config()
+
+        # Change the configuration
+        if orig_table_config == ofp.OFPTC_TABLE_MISS_CONTROLLER:
+            new_table_config = ofp.OFPTC_TABLE_MISS_DROP
+        else:
+            new_table_config = ofp.OFPTC_TABLE_MISS_CONTROLLER
+        request = ofp.message.table_mod(table_id=table_id, config=new_table_config)
+        self.controller.message_send(request)
+        self.controller.transact(ofp.message.barrier_request())
+
+        # Check the configuration took
+        self.assertEqual(get_table_config(), new_table_config)
+
+        # Change the configuration back
+        request = ofp.message.table_mod(table_id=table_id, config=orig_table_config)
+        self.controller.message_send(request)
+        self.controller.transact(ofp.message.barrier_request())
+
 if __name__ == "__main__":
     print "Please run through oft script:  ./oft --test_spec=basic"