Merge pull request #20 from InCNTRE/master

Add conformance tests "actions"
diff --git a/STYLE b/STYLE
new file mode 100644
index 0000000..358805a
--- /dev/null
+++ b/STYLE
@@ -0,0 +1,10 @@
+OFTest Coding Style
+-------------------
+
+See the Python standard style guide, PEP-8: http://www.python.org/dev/peps/pep-0008/.
+
+Some important style points:
+ - Test file names should be lowercase with underscores and short. Test case
+   class names are CamelCased.
+ - Any additional documentation added to the repository should be in plain text
+   or Markdown.
diff --git a/platforms/local.py b/platforms/local.py
new file mode 100644
index 0000000..77e15d9
--- /dev/null
+++ b/platforms/local.py
@@ -0,0 +1,22 @@
+"""
+Local platform
+
+This platform uses veth pairs to send packets to and from a userspace
+switch. The switch should be connected to veth0, veth2, veth4, and veth6.
+"""
+
+def platform_config_update(config):
+    """
+    Update configuration for the local platform
+
+    @param config The configuration dictionary to use/update
+    """
+    base_of_port = 1
+    base_if_index = 1
+    port_count = 4
+
+    port_map = {}
+    # Use every other veth interface (veth1, veth3, ...)
+    for idx in range(port_count):
+        port_map[base_of_port + idx] = "veth%d" % (base_if_index + 2 * idx)
+    config['port_map'] = port_map
diff --git a/tests/remote.py b/platforms/remote.py
similarity index 75%
rename from tests/remote.py
rename to platforms/remote.py
index 5931153..3666bb4 100644
--- a/tests/remote.py
+++ b/platforms/remote.py
@@ -1,23 +1,23 @@
 """
-Platform configuration file
-platform == remote
+Remote platform
+
+This platform uses physical ethernet interfaces.
 """
 
+# Update this dictionary to suit your environment.
 remote_port_map = {
     23 : "eth2",
     24 : "eth3",
     25 : "eth4",
     26 : "eth5"
-    }
+}
 
 def platform_config_update(config):
     """
     Update configuration for the remote platform
 
     @param config The configuration dictionary to use/update
-    This routine defines the port map used for this configuration
     """
-
     global remote_port_map
     config["port_map"] = remote_port_map.copy()
     config["caps_table_idx"] = 0
diff --git a/profiles/default.py b/profiles/default.py
new file mode 100644
index 0000000..8313eb7
--- /dev/null
+++ b/profiles/default.py
@@ -0,0 +1,9 @@
+"""
+Default profile
+
+No tests skipped.
+"""
+
+#@var skip_test_list The list of tests to skip for this run
+skip_test_list = [
+]
diff --git a/tests/profiles/example.py b/profiles/example.py
similarity index 89%
rename from tests/profiles/example.py
rename to profiles/example.py
index 938a6f2..6e23b92 100644
--- a/tests/profiles/example.py
+++ b/profiles/example.py
@@ -1,7 +1,7 @@
 """
 Sample profile
 
-A profile determines run specific behavior.  It is meant to capture 
+A profile determines run specific behavior.  It is meant to capture
 variations between different switch targets.
 
 A profile defines two target specific variables.
@@ -13,9 +13,6 @@
 2. A set of tests to run (overriding the default "skip" priority)
 optionally specifying a test parameters specific to the test run
 
-This file should be imported "as profile" so references to the 
-module will properly map.
-
 @todo Allow a test to be run multiple times with different params
 """
 
diff --git a/tests/profiles/noing.py b/profiles/noing.py
similarity index 100%
rename from tests/profiles/noing.py
rename to profiles/noing.py
diff --git a/src/python/oftest/controller.py b/src/python/oftest/controller.py
index cf4cf69..cc8b97c 100644
--- a/src/python/oftest/controller.py
+++ b/src/python/oftest/controller.py
@@ -286,6 +286,8 @@
         if s and s == self.listen_socket:
             if self.switch_socket:
                 self.logger.warning("Ignoring incoming connection; already connected to switch")
+                (sock, addr) = self.listen_socket.accept()
+                sock.close()
                 return 0
 
             (sock, addr) = self.listen_socket.accept()
diff --git a/src/python/oftest/illegal_message.py b/src/python/oftest/illegal_message.py
new file mode 100644
index 0000000..0c6a878
--- /dev/null
+++ b/src/python/oftest/illegal_message.py
@@ -0,0 +1,108 @@
+"""
+Support an illegal message
+"""
+
+from cstruct import *
+
+ILLEGAL_MESSAGE_TYPE=217
+
+class illegal_message_type:
+    """
+    Wrapper class for illegal message
+
+    OpenFlow message header: length, version, xid, type
+    @arg length: The total length of the message
+    @arg version: The OpenFlow version (1)
+    @arg xid: The transaction ID
+    @arg type: The message type (OFPT_ECHO_REQUEST=2)
+
+    @arg data: Binary string following message members
+
+    The message type is set to "illegal" and the pack assert
+    check for the OF header is disabled
+    """
+
+    def __init__(self):
+        self.header = ofp_header()
+        self.header.type = ILLEGAL_MESSAGE_TYPE
+        self.data = ""
+
+    def pack(self):
+        """
+        Pack object into string
+
+        @return The packed string which can go on the wire
+
+        """
+        self.header.length = len(self)
+        packed = self.header.pack(assertstruct=False)
+
+        packed += self.data
+        return packed
+
+    def unpack(self, binary_string):
+        """
+        Unpack object from a binary string
+
+        @param binary_string The wire protocol byte string holding the object
+        represented as an array of bytes.
+        @return The remainder of binary_string that was not parsed.
+
+        """
+        binary_string = self.header.unpack(binary_string)
+
+        self.data = binary_string
+        binary_string = ''
+        return binary_string
+
+    def __len__(self):
+        """
+        Return the length of this object once packed into a string
+
+        @return An integer representing the number bytes in the packed
+        string.
+
+        """
+        length = OFP_HEADER_BYTES
+
+        length += len(self.data)
+        return length
+
+    def show(self, prefix=''):
+        """
+        Generate a string (with multiple lines) describing the contents
+        of the object in a readable manner
+
+        @param prefix Pre-pended at the beginning of each line.
+
+        """
+
+        outstr = prefix + 'illegal_message (' + \
+            str(ILLEGAL_MESSAGE_TYPE) + ')\n'
+        prefix += '  '
+        outstr += prefix + 'ofp header\n'
+        outstr += self.header.show(prefix + '  ')
+        outstr += prefix + 'data is of length ' + str(len(self.data)) + '\n'
+        return outstr
+
+    def __eq__(self, other):
+        """
+        Return True if self and other hold the same data
+
+        @param other Other object in comparison
+
+        """
+        if type(self) != type(other): return False
+        if not self.header.__eq__(other.header): return False
+
+        if self.data != other.data: return False
+        return True
+
+    def __ne__(self, other):
+        """
+        Return True if self and other do not hold the same data
+
+        @param other Other object in comparison
+
+        """
+        return not self.__eq__(other)
diff --git a/tests/basic.py b/tests/basic.py
index feda04f..721f953 100644
--- a/tests/basic.py
+++ b/tests/basic.py
@@ -29,6 +29,8 @@
 import oftest.dataplane as dataplane
 import oftest.action as action
 
+import oftest.illegal_message as illegal_message
+
 from testutils import *
 
 #@var basic_port_map Local copy of the configuration map from OF port
@@ -90,13 +92,19 @@
         #@todo Add an option to wait for a pkt transaction to ensure version
         # compatibilty?
         self.controller.connect(timeout=20)
+
+        # By default, respond to echo requests
+        self.controller.keep_alive = True
+        
         if not self.controller.active:
             raise Exception("Controller startup failed")
         if self.controller.switch_addr is None: 
             raise Exception("Controller startup failed (no switch addr)")
         basic_logger.info("Connected " + str(self.controller.switch_addr))
         request = message.features_request()
-        reply, pkt = self.controller.transact(request, timeout=10)
+        reply, pkt = self.controller.transact(request)
+        self.assertTrue(reply is not None,
+                        "Did not complete features_request for handshake")
         self.supported_actions = reply.actions
         basic_logger.info("Supported actions: " + hex(self.supported_actions))
 
@@ -211,6 +219,8 @@
         # self.dataplane.show()
         # Would like an assert that checks the data plane
 
+test_prio["DataPlaneOnly"] = -1
+
 class Echo(SimpleProtocol):
     """
     Test echo response with no data
@@ -218,6 +228,8 @@
     def runTest(self):
         request = message.echo_request()
         response, pkt = self.controller.transact(request)
+        self.assertTrue(response is not None,
+                        "Did not get echo reply")
         self.assertEqual(response.header.type, ofp.OFPT_ECHO_REPLY,
                          'response is not echo_reply')
         self.assertEqual(request.header.xid, response.header.xid,
@@ -232,6 +244,8 @@
         request = message.echo_request()
         request.data = 'OpenFlow Will Rule The World'
         response, pkt = self.controller.transact(request)
+        self.assertTrue(response is not None,
+                        "Did not get echo reply (with data)")
         self.assertEqual(response.header.type, ofp.OFPT_ECHO_REPLY,
                          'response is not echo_reply')
         self.assertEqual(request.header.xid, response.header.xid,
@@ -270,7 +284,7 @@
                #@todo Check for unexpected messages?
                count = 0
                while True:
-                   (response, raw) = self.controller.poll(ofp.OFPT_PACKET_IN, 2)
+                   (response, raw) = self.controller.poll(ofp.OFPT_PACKET_IN)
                    if not response:  # Timeout
                        break
                    if dataplane.match_exp_pkt(pkt, response.data): # Got match
@@ -308,7 +322,7 @@
             self.dataplane.send(of_port, str(pkt))
             count = 0
             while True:
-                (response, raw) = self.controller.poll(ofp.OFPT_PACKET_IN, 2)
+                (response, raw) = self.controller.poll(ofp.OFPT_PACKET_IN)
                 if not response:  # Timeout
                     break
                 if dataplane.match_exp_pkt(pkt, response.data): # Got match
@@ -471,7 +485,8 @@
         request.table_id = 0xff
         request.match.wildcards = 0 # ofp.OFPFW_ALL
         response, pkt = self.controller.transact(request)
-        self.assertTrue(response is not None, "Did not get response")
+        self.assertTrue(response is not None,
+                        "Did not get response for flow stats")
         basic_logger.debug(response.show())
 
 test_prio["FlowStatsGet"] = -1
@@ -492,7 +507,8 @@
         basic_logger.info("Sending table stats request")
         request = message.table_stats_request()
         response, pkt = self.controller.transact(request)
-        self.assertTrue(response is not None, "Did not get response")
+        self.assertTrue(response is not None,
+                        "Did not get reply for table stats")
         basic_logger.debug(response.show())
 
 class DescStatsGet(SimpleProtocol):
@@ -507,7 +523,8 @@
         basic_logger.info("Sending stats request")
         request = message.desc_stats_request()
         response, pkt = self.controller.transact(request)
-        self.assertTrue(response is not None, "Did not get response")
+        self.assertTrue(response is not None,
+                        "Did not get reply for desc stats")
         basic_logger.debug(response.show())
 
 class FlowMod(SimpleProtocol):
@@ -588,7 +605,7 @@
 
         # poll for error message
         while True:
-            (response, raw) = self.controller.poll(ofp.OFPT_ERROR, 2)
+            (response, raw) = self.controller.poll(ofp.OFPT_ERROR)
             if not response:  # Timeout
                 break
             if response.code == ofp.OFPPMFC_BAD_PORT:
@@ -602,5 +619,23 @@
 
         self.assertTrue(response is not None, 'Did not receive error message')
 
+class BadMessage(SimpleProtocol):
+    """
+    Send a message with a bad type and verify an error is returned
+    """
+
+    def runTest(self):
+        basic_logger.info("Running " + str(self))
+        request = illegal_message.illegal_message_type()
+
+        reply, pkt = self.controller.transact(request)
+        self.assertTrue(reply is not None, "Did not get response to bad req")
+        self.assertTrue(reply.header.type == ofp.OFPT_ERROR,
+                        "reply not an error message")
+        self.assertTrue(reply.type == ofp.OFPET_BAD_REQUEST,
+                        "reply error type is not bad request")
+        self.assertTrue(reply.code == ofp.OFPBRC_BAD_TYPE,
+                        "reply error code is not bad type")
+
 if __name__ == "__main__":
     print "Please run through oft script:  ./oft --test_spec=basic"
diff --git a/tests/bsn_ipmask.py b/tests/bsn_ipmask.py
index 0609582..cb64cb5 100644
--- a/tests/bsn_ipmask.py
+++ b/tests/bsn_ipmask.py
@@ -85,7 +85,7 @@
         m.data = struct.pack( "!LBBBBL", 1, index, 0, 0, 0, 0 )
         rc = self.controller.message_send(m)
         self.assertNotEqual(rc, -1, "Error sending get IP mask command")
-        m, r = self.controller.poll(ofp.OFPT_VENDOR, 2)
+        m, r = self.controller.poll(ofp.OFPT_VENDOR)
         self.assertEqual(m.vendor, 0x005c16c7, "Wrong vendor ID")
         x = struct.unpack("!LBBBBL", m.data)
         self.assertEqual(x[0], 2, "Wrong subtype")
diff --git a/tests/cxn.py b/tests/cxn.py
new file mode 100644
index 0000000..3f044fc
--- /dev/null
+++ b/tests/cxn.py
@@ -0,0 +1,165 @@
+"""
+Connection test cases
+
+"""
+
+import time
+import signal
+import sys
+import logging
+
+import unittest
+import random
+
+import oftest.controller as controller
+import oftest.cstruct as ofp
+import oftest.message as message
+import oftest.dataplane as dataplane
+import oftest.action as action
+
+from testutils import *
+
+#@var cxn_port_map Local copy of the configuration map from OF port
+# numbers to OS interfaces
+cxn_port_map = None
+#@var cxn_logger Local logger object
+cxn_logger = None
+#@var cxn_config Local copy of global configuration data
+cxn_config = None
+
+test_prio = {}
+
+def test_set_init(config):
+    """
+    Set up function for connection test classes
+
+    @param config The configuration dictionary; see oft
+    """
+
+    global cxn_port_map
+    global cxn_logger
+    global cxn_config
+
+    cxn_logger = logging.getLogger("cxn")
+    cxn_logger.info("Initializing test set")
+    cxn_port_map = config["port_map"]
+    cxn_config = config
+
+class BaseHandshake(unittest.TestCase):
+    """
+    Base handshake case to set up controller, but do not send hello.
+    """
+
+    def sig_handler(self, v1, v2):
+        cxn_logger.critical("Received interrupt signal; exiting")
+        print "Received interrupt signal; exiting"
+        self.clean_shutdown = False
+        self.tearDown()
+        sys.exit(1)
+
+    def controllerSetup(self, host, port):
+        self.controller = controller.Controller(host=host,port=port)
+
+        # clean_shutdown should be set to False to force quit app
+        self.clean_shutdown = True
+        # disable initial hello so hello is under control of test
+        self.controller.initial_hello = False
+
+        self.controller.start()
+        #@todo Add an option to wait for a pkt transaction to ensure version
+        # compatibilty?
+        self.controller.connect(timeout=10)
+        self.assertTrue(self.controller.active,
+                        "Controller startup failed, not active")
+        self.assertTrue(self.controller.switch_addr is not None,
+                        "Controller startup failed, no switch addr")
+
+    def setUp(self):
+        self.logger = cxn_logger
+        self.config = cxn_config
+        #@todo Test cases shouldn't monkey with signals; move SIGINT handler
+        # to top-level oft
+        try:
+           signal.signal(signal.SIGINT, self.sig_handler)
+        except ValueError, e:
+           cxn_logger.info("Could not set SIGINT handler: %s" % e)
+        cxn_logger.info("** START TEST CASE " + str(self))
+
+        self.test_timeout = test_param_get(cxn_config,
+                                           'handshake_timeout') or 60
+
+    def inheritSetup(self, parent):
+        """
+        Inherit the setup of a parent
+
+        This allows running at test from within another test.  Do the
+        following:
+
+        sub_test = SomeTestClass()  # Create an instance of the test class
+        sub_test.inheritSetup(self) # Inherit setup of parent
+        sub_test.runTest()          # Run the test
+
+        Normally, only the parent's setUp and tearDown are called and
+        the state after the sub_test is run must be taken into account
+        by subsequent operations.
+        """
+        self.logger = parent.logger
+        self.config = parent.config
+        cxn_logger.info("** Setup " + str(self) + 
+                                    " inheriting from " + str(parent))
+        self.controller = parent.controller
+        
+    def tearDown(self):
+        cxn_logger.info("** END TEST CASE " + str(self))
+        self.controller.shutdown()
+        if self.clean_shutdown:
+            self.controller.join()
+
+    def runTest(self):
+        # do nothing in the base case
+        pass
+
+    def assertTrue(self, cond, msg):
+        if not cond:
+            cxn_logger.error("** FAILED ASSERTION: " + msg)
+        unittest.TestCase.assertTrue(self, cond, msg)
+
+test_prio["BaseHandshake"] = -1
+
+class HandshakeNoHello(BaseHandshake):
+    def runTest(self):
+        self.controllerSetup(cxn_config["controller_host"],
+                             cxn_config["controller_port"])
+
+        cxn_logger.info("TCP Connected " + 
+                        str(self.controller.switch_addr))
+        cxn_logger.info("Hello not sent, waiting for timeout")
+
+        # wait for controller to die
+        count = 0
+        while self.controller.active and count < self.test_timeout:
+            time.sleep(1)
+            count = count + 1
+        self.assertTrue(not self.controller.active, 
+                        "Expected controller disconnect, but still active")
+
+class HandshakeNoFeaturesRequest(BaseHandshake):
+    def runTest(self):
+        self.controllerSetup(cxn_config["controller_host"],
+                             cxn_config["controller_port"])
+
+        cxn_logger.info("TCP Connected " + 
+                                    str(self.controller.switch_addr))
+        cxn_logger.info("Sending hello")
+        self.controller.message_send(message.hello())
+
+        cxn_logger.info("Features request not sent, waiting for timeout")
+
+        # wait for controller to die
+        count = 0
+        while self.controller.active and count < self.test_timeout:
+            time.sleep(1)
+            count = count + 1
+        self.assertTrue(not self.controller.active, 
+                        "Expected controller disconnect, but still active")
+
diff --git a/tests/flow_query.py b/tests/flow_query.py
index bad4749..63e6cb5 100644
--- a/tests/flow_query.py
+++ b/tests/flow_query.py
@@ -1371,7 +1371,7 @@
         # </TBD>
         n = 0
         while True:
-            (resp, pkt) = self.controller.poll(ofp.OFPT_STATS_REPLY, 4)
+            (resp, pkt) = self.controller.poll(ofp.OFPT_STATS_REPLY)
             if resp is None:
                 return False            # Did not get expected response
             if n == 0:
@@ -1429,7 +1429,7 @@
 
     def barrier(self):
         barrier = message.barrier_request()
-        (resp, pkt) = self.controller.transact(barrier, 20)
+        (resp, pkt) = self.controller.transact(barrier, 30)
         return (resp is not None)
 
     def errors_verify(self, num_exp, type = 0, code = 0):
diff --git a/tests/local.py b/tests/local.py
deleted file mode 100644
index 0a3bc04..0000000
--- a/tests/local.py
+++ /dev/null
@@ -1,15 +0,0 @@
-"""
-Platform configuration file
-platform == local
-
-Update this file to override defaults
-"""
-
-def platform_config_update(config):
-    """
-    Update configuration for the local platform
-
-    @param config The configuration dictionary to use/update
-
-    Update this routine if values other the defaults are required
-    """
diff --git a/tests/oft b/tests/oft
index 81d378e..f59e99d 100755
--- a/tests/oft
+++ b/tests/oft
@@ -15,9 +15,6 @@
     platform          : String identifying the target platform
     controller_host   : Host on which test controller is running (for sockets)
     controller_port   : Port on which test controller listens for switch cxn
-    port_count        : (Optional) Number of ports in dataplane
-    base_of_port      : (Optional) Base OpenFlow port number in dataplane
-    base_if_index     : (Optional) Base OS network interface for dataplane
     test_dir          : (TBD) Directory to search for test files (default .)
     test_spec         : (TBD) Specification of test(s) to run
     log_file          : Filename for test logging
@@ -70,15 +67,10 @@
 current run.  In particular, it should set up config["port_map"] with
 the proper map from OF port numbers to OF interface names.
 
-You can add your own platform, say gp104, by adding a file gp104.py
-that defines the function platform_config_update and then use the
-parameter --platform=gp104 on the command line.
-
-If platform_config_update does not set config["port_map"], an attempt
-is made to generate a default map via the function default_port_map_setup.
-This will use "local" and "remote" for platform names if available
-and generate a sequential map based on the values of base_of_port and
-base_if_index in the configuration structure.
+You can add your own platform, say gp104, by adding a file gp104.py to the
+platforms directory that defines the function platform_config_update and then
+use the parameter --platform=gp104 on the command line. You can also use the
+--platform-dir option to change which directory is searched.
 
 The current model for test sets is basic.py.  The current convention is
 that the test set should implement a function test_set_init which takes
@@ -105,9 +97,8 @@
 @todo Consider moving oft up a level
 
 Current test case setup:
-    Files in this or sub directories (or later, directory specified on 
-command line) that contain a function test_set_init are considered test
-files.
+    Files in the tests direcoty that contain a function test_set_init are
+considered test files.
     The function test_set_init examines the test_spec config variable
 and generates a suite of tests.
     Support a command line option --test_mod so that all tests in that
@@ -160,6 +151,8 @@
 _debug_default = "warning"
 _debug_level_default = DEBUG_LEVELS[_debug_default]
 
+root_dir = os.path.join(os.path.dirname(__file__), "..")
+
 ##@var config_default
 # The default configuration dictionary for OFT
 config_default = {
@@ -168,9 +161,6 @@
     "platform_args"      : None,
     "controller_host"    : "0.0.0.0",
     "controller_port"    : 6633,
-    "port_count"         : 4,
-    "base_of_port"       : 1,
-    "base_if_index"      : 1,
     "relax"              : False,
     "test_spec"          : "all",
     "test_dir"           : os.path.dirname(__file__),
@@ -181,12 +171,14 @@
     "dbg_level"          : _debug_level_default,
     "port_map"           : {},
     "test_params"        : "None",
-    "profile"            : None,
+    "profile"            : "default",
     "allow_user"         : False,
     "fail_skipped"       : False,
     "default_timeout"    : 2,
     "minsize"            : 0,
     "random_seed"        : None,
+    "platform_dir"       : os.path.join(root_dir, "platforms"),
+    "profile_dir"        : os.path.join(root_dir, "profiles"),
 }
 
 # Default test priority
@@ -286,6 +278,12 @@
     parser.add_option("--random-seed", type="int",
                       help="Random number generator seed",
                       default=None)
+    parser.add_option("--test-dir", type="string",
+                      help="Directory containing tests")
+    parser.add_option("--platform-dir", type="string",
+                      help="Directory containing platform modules")
+    parser.add_option("--profile-dir", type="string",
+                      help="Directory containing profile modules")
 
     # Might need this if other parsers want command line
     # parser.allow_interspersed_args = False
@@ -295,28 +293,21 @@
 
     return (config, args)
 
-def check_profile(config):
+def load_profile(config):
     """
     Import a profile from the profiles library
     """
 
     global profile_mod
-    if "profile" in config and config["profile"]:
-        logging.info("Importing profile: %s" % config["profile"])
-        profile_name = "profiles." + config["profile"]
-        try:
-            top_mod =  __import__(profile_name)
-            profile_mod = eval("top_mod." + config["profile"])
-            logging.info("Imported profile %s.  Dir: %s" % 
-                         (config["profile"], str(dir(profile_mod))))
-        except:
-            logging.info("Could not import profile: %s.py" % 
-                         config["profile"])
-            print "Failed to import profile: %s" % config["profile"]
-            raise
-    else:
-        logging.info("No profile specified")
-        
+    logging.info("Importing profile: %s" % config["profile"])
+    try:
+        profile_mod = imp.load_module(config["profile"], *imp.find_module(config["profile"], [config["profile_dir"]]))
+        if not "skip_test_list" in dir(profile_mod):
+            die("Profile did not define skip_test_list")
+    except:
+        logging.info("Could not import profile: %s.py" % config["profile"])
+        print "Failed to import profile: %s" % config["profile"]
+        raise
 
 def logging_setup(config):
     """
@@ -328,31 +319,6 @@
                         level=config["dbg_level"],
                         format=_format, datefmt=_datefmt)
 
-def default_port_map_setup(config):
-    """
-    Setup the OF port mapping based on config
-    @param config The OFT configuration structure
-    @return Port map dictionary
-    """
-    if (config["base_of_port"] is None) or not config["port_count"]:
-        return None
-    port_map = {}
-    if config["platform"] == "local":
-        # For local, use every other veth port
-        for idx in range(config["port_count"]):
-            port_map[config["base_of_port"] + idx] = "veth" + \
-                str(config["base_if_index"] + (2 * idx))
-    elif config["platform"] == "remote":
-        # For remote, use eth ports
-        for idx in range(config["port_count"]):
-            port_map[config["base_of_port"] + idx] = "eth" + \
-                str(config["base_if_index"] + idx)
-    else:
-        return None
-
-    logging.info("Built default port map")
-    return port_map
-
 def test_list_generate(config):
     """Generate the list of all known tests indexed by module name
 
@@ -427,10 +393,9 @@
     If set in the test_prio variable for the module, return
     that value.  Otherwise return 100 (default)
     """
-    if profile_mod:
-        if profile_mod.skip_test_list and test in profile_mod.skip_test_list:
-            logging.info("Skipping test %s due to profile" % test)
-            return TEST_PRIO_SKIP
+    if test in profile_mod.skip_test_list:
+        logging.info("Skipping test %s due to profile" % test)
+        return TEST_PRIO_SKIP
     if 'test_prio' in dir(mod):
         if test in mod.test_prio.keys():
             return mod.test_prio[test]
@@ -443,9 +408,14 @@
 # Get configuration, set up logging, import platform from file
 (config, args) = config_setup(config_default)
 
+logging_setup(config)
+logging.info("++++++++ " + time.asctime() + " ++++++++")
+
 test_list_generate(config)
 oft_config = config
 
+load_profile(config)
+
 # Check if test list is requested; display and exit if so
 if config["list"]:
     did_print = False
@@ -501,11 +471,6 @@
             print "%s.%s" % (mod.__name__, test)
     sys.exit(0)
 
-logging_setup(config)
-logging.info("++++++++ " + time.asctime() + " ++++++++")
-
-check_profile(config)
-
 # Generate the test suite
 #@todo Decide if multiple suites are ever needed
 suite = unittest.TestSuite()
@@ -549,28 +514,24 @@
         else:
             die("Bad test spec: " + ts_entry)
 
-# Check if platform specified
-if config["platform"]:
-    _imp_string = "from " + config["platform"] + " import *"
-    logging.info("Importing platform: " + _imp_string)
-    try:
-        exec(_imp_string)
-    except:
-        logging.warn("Failed to import " + config["platform"] + " file")
-        raise
+# Load the platform module
+platform_name = config["platform"]
+logging.info("Importing platform: " + platform_name)
+platform_mod = None
+try:
+    platform_mod = imp.load_module(platform_name, *imp.find_module(platform_name, [config["platform_dir"], config["test_dir"]]))
+except:
+    logging.warn("Failed to import " + platform_name + " platform module")
+    raise
 
 try:
-    platform_config_update(config)
+    platform_mod.platform_config_update(config)
 except:
     logging.warn("Could not run platform host configuration")
     raise
 
 if not config["port_map"]:
-    # Try to set up default port mapping if not done by platform
-    config["port_map"] = default_port_map_setup(config)
-
-if not config["port_map"]:
-    die("Interface port map is not defined.  Exiting")
+    die("Interface port map was not defined by the platform. Exiting.")
 
 logging.debug("Configuration: " + str(config))
 logging.info("OF port map: " + str(config["port_map"]))
diff --git a/tests/pktact.py b/tests/pktact.py
index 14f9e8f..660aa07 100644
--- a/tests/pktact.py
+++ b/tests/pktact.py
@@ -458,7 +458,7 @@
                 exp_port = None
 
                 while True:
-                    (response, raw) = self.controller.poll(ofp.OFPT_PACKET_IN, 2)
+                    (response, raw) = self.controller.poll(ofp.OFPT_PACKET_IN)
                     if not response:  # Timeout
                         break
                     if dataplane.match_exp_pkt(pkt, response.data): # Got match
@@ -1888,6 +1888,88 @@
     """   
 
 test_prio["MixedVLAN"] = -1
- 
+
+class MatchEach(basic.SimpleDataPlane):
+    """
+    Check that each match field is actually matched on.
+    Installs two flows that differ in one field. The flow that should not
+    match has a higher priority, so if that field is ignored during matching
+    the packet will be sent out the wrong port.
+
+    TODO test UDP, ARP, ICMP, etc.
+    """
+    def runTest(self):
+        of_ports = pa_port_map.keys()
+        of_ports.sort()
+        self.assertTrue(len(of_ports) > 1, "Not enough ports for test")
+
+        delete_all_flows(self.controller, pa_logger)
+
+        pkt = simple_tcp_packet()
+        ingress_port = of_ports[0]
+        egress_port = of_ports[1]
+
+        def testField(field, mask):
+            pa_logger.info("Testing field %s" % field)
+
+            def addFlow(matching, priority, output_port):
+                match = packet_to_flow_match(self, pkt)
+                self.assertTrue(match is not None, "Could not generate flow match from pkt")
+                match.wildcards &= ~ofp.OFPFW_IN_PORT
+                match.in_port = ingress_port
+                if not matching:
+                    # Make sure flow doesn't match
+                    orig = getattr(match, field)
+                    if isinstance(orig, list):
+                        new = map(lambda a: ~a[0] & a[1], zip(orig, mask))
+                    else:
+                        new = ~orig & mask
+                    setattr(match, field, new)
+                request = message.flow_mod()
+                request.match = match
+                request.buffer_id = 0xffffffff
+                request.priority = priority
+                act = action.action_output()
+                act.port = output_port
+                self.assertTrue(request.actions.add(act), "Could not add action")
+                pa_logger.info("Inserting flow")
+                self.controller.message_send(request)
+
+            # This flow should match.
+            addFlow(matching=True, priority=0, output_port=egress_port)
+            # This flow should not match, but it has a higher priority.
+            addFlow(matching=False, priority=1, output_port=ofp.OFPP_IN_PORT)
+
+            self.assertEqual(do_barrier(self.controller), 0, "Barrier failed")
+
+            pa_logger.info("Sending packet to dp port " + str(ingress_port))
+            self.dataplane.send(ingress_port, str(pkt))
+
+            exp_pkt_arg = None
+            exp_port = None
+            if pa_config["relax"]:
+                exp_pkt_arg = pkt
+                exp_port = egress_port
+
+            (rcv_port, rcv_pkt, pkt_time) = self.dataplane.poll(port_number=exp_port,
+                                                                exp_pkt=exp_pkt_arg)
+            self.assertTrue(rcv_pkt is not None, "Did not receive packet")
+            pa_logger.debug("Packet len " + str(len(rcv_pkt)) + " in on " + str(rcv_port))
+            self.assertEqual(rcv_port, egress_port, "Unexpected receive port")
+            self.assertEqual(str(pkt), str(rcv_pkt), 'Response packet does not match send packet')
+
+        # TODO in_port
+        testField("dl_src", [0xff]*6)
+        testField("dl_dst", [0xff]*6)
+        testField("dl_type", 0xffff)
+        testField("dl_vlan", 0xfff)
+        # TODO dl_vlan_pcp
+        testField("nw_src", 0xffffffff)
+        testField("nw_dst", 0xffffffff)
+        testField("nw_tos", 0x3f)
+        testField("nw_proto", 0xff)
+        testField("tp_src", 0xffff)
+        testField("tp_dst", 0xffff)
+
 if __name__ == "__main__":
     print "Please run through oft script:  ./oft --test_spec=basic"
diff --git a/tests/profiles/__init__.py b/tests/profiles/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/tests/profiles/__init__.py
+++ /dev/null
diff --git a/tests/serial_failover.py b/tests/serial_failover.py
index 90a4236..d12040e 100644
--- a/tests/serial_failover.py
+++ b/tests/serial_failover.py
@@ -80,11 +80,15 @@
         self.controller.start()
         #@todo Add an option to wait for a pkt transaction to ensure version
         # compatibilty?
-        self.controller.connect()
+        self.controller.connect(timeout=10)
         self.assertTrue(self.controller.active,
                         "Controller startup failed, not active")
         self.assertTrue(self.controller.switch_addr is not None,
                         "Controller startup failed, no switch addr")
+        request = message.features_request()
+        reply, pkt = self.controller.transact(request, timeout=20)
+        self.assertTrue(reply is not None,
+                        "Did not complete features_request for handshake")
         serial_failover_logger.info("Connected " + 
                                     str(self.controller.switch_addr))
 
@@ -117,7 +121,7 @@
         # controller_list is list of ip/port tuples
         partial_list = test_param_get(serial_failover_config,
                                       'controller_list')
-        serial_failover_logger.debug(str(partial_list))
+        serial_failover_logger.debug("ctrl list: " + str(partial_list))
         self.controller_list = [(serial_failover_config["controller_host"],
                                  serial_failover_config["controller_port"])]
         if partial_list is not None: