Merge branch 'master' of github.com:floodlight/oftest
diff --git a/src/python/oftest/dataplane.py b/src/python/oftest/dataplane.py
index 7ce2738..252726a 100644
--- a/src/python/oftest/dataplane.py
+++ b/src/python/oftest/dataplane.py
@@ -31,6 +31,19 @@
 ETH_P_ALL = 0x03
 RCV_TIMEOUT = 10000
 
+def match_exp_pkt(exp_pkt, pkt):
+    """
+    Compare the string value of pkt with the string value of exp_pkt,
+    and return True iff they are identical.  If the length of exp_pkt is
+    less than the minimum Ethernet frame size (60 bytes), then padding
+    bytes in pkt are ignored.
+    """
+    e = str(exp_pkt)
+    p = str(pkt)
+    if len(e) < 60:
+        p = p[:len(e)]
+    return e == p
+
 class DataPlanePort(Thread):
     """
     Class defining a port monitoring object.
@@ -140,7 +153,7 @@
                 if (not self.parent.want_pkt_port or
                         self.parent.want_pkt_port == self.port_number):
                     if self.parent.exp_pkt:
-                        if str(self.parent.exp_pkt) != str(rcvmsg):
+                        if not match_exp_pkt(self.parent.exp_pkt, rcvmsg):
                             drop_pkt = True
                     if not drop_pkt:
                         self.parent.got_pkt_port = self.port_number
@@ -331,7 +344,7 @@
                 pkt, time = self.port_list[port_number].dequeue(use_lock=False)
                 if not exp_pkt:
                     break
-                if str(pkt) == str(exp_pkt):
+                if match_exp_pkt(exp_pkt, pkt):
                     break
                 pkt = None # Discard silently
             if pkt:
diff --git a/tests/.gitignore b/tests/.gitignore
new file mode 100644
index 0000000..48da841
--- /dev/null
+++ b/tests/.gitignore
@@ -0,0 +1,3 @@
+*.pyc
+profiles/*.pyc
+*~
diff --git a/tests/basic.py b/tests/basic.py
index 9dec947..bf5798e 100644
--- a/tests/basic.py
+++ b/tests/basic.py
@@ -21,6 +21,7 @@
 import logging
 
 import unittest
+import random
 
 import oftest.controller as controller
 import oftest.cstruct as ofp
@@ -252,7 +253,7 @@
                    (response, raw) = self.controller.poll(ofp.OFPT_PACKET_IN, 2)
                    if not response:  # Timeout
                        break
-                   if str(pkt) == response.data[:len(str(pkt))]:  # Got match
+                   if dataplane.match_exp_pkt(pkt, response.data): # Got match
                        break
                    if not basic_config["relax"]:  # Only one attempt to match
                        break
@@ -263,12 +264,9 @@
                self.assertTrue(response is not None, 
                                'Packet in message not received on port ' + 
                                str(of_port))
-               if str(pkt) != response.data[:len(str(pkt))]:
-                   basic_logger.debug("pkt  len " + str(len(str(pkt))) +
-                                      ": " + str(pkt))
-                   basic_logger.debug("resp len " + 
-                                      str(len(str(response.data))) + 
-                                      ": " + str(response.data))
+               if not dataplane.match_exp_pkt(pkt, response.data):
+                   basic_logger.debug("Sent %s" % format_packet(pkt))
+                   basic_logger.debug("Resp %s" % format_packet(response.data))
                    self.assertTrue(False,
                                    'Response packet does not match send packet' +
                                    ' for port ' + str(of_port))
@@ -321,9 +319,56 @@
                basic_logger.info("PacketOut: got pkt from " + str(of_port))
                if of_port is not None:
                    self.assertEqual(of_port, dp_port, "Unexpected receive port")
+               if not dataplane.match_exp_pkt(outpkt, pkt):
+                   basic_logger.debug("Sent %s" % format_packet(outpkt))
+                   basic_logger.debug("Resp %s" % format_packet(
+                           str(pkt)[:len(str(outpkt))]))
                self.assertEqual(str(outpkt), str(pkt)[:len(str(outpkt))],
                                 'Response packet does not match send packet')
 
+class PacketOutMC(SimpleDataPlane):
+    """
+    Test packet out to multiple output ports
+
+    Send packet out message to controller for 1 to N dataplane ports and
+    verify the packet appears on the appropriate ports
+    """
+    def runTest(self):
+        # Construct packet to send to dataplane
+        # Send packet to dataplane
+        # Poll controller with expect message type packet in
+
+        rc = delete_all_flows(self.controller, basic_logger)
+        self.assertEqual(rc, 0, "Failed to delete all flows")
+
+        # These will get put into function
+        of_ports = basic_port_map.keys()
+        random.shuffle(of_ports)
+        for num_ports in range(1,len(of_ports)+1):
+            for outpkt, opt in [
+               (simple_tcp_packet(), "simple TCP packet"),
+               (simple_eth_packet(), "simple Ethernet packet"),
+               (simple_eth_packet(pktlen=40), "tiny Ethernet packet")]:
+
+               dp_ports = of_ports[0:num_ports]
+               basic_logger.info("PKT OUT test with " + opt +
+                                 ", ports " + str(dp_ports))
+               msg = message.packet_out()
+               msg.data = str(outpkt)
+               act = action.action_output()
+               for i in range(0,num_ports):
+                  act.port = dp_ports[i]
+                  self.assertTrue(msg.actions.add(act),
+                                  'Could not add action to msg')
+
+               basic_logger.info("PacketOut to: " + str(dp_ports))
+               rv = self.controller.message_send(msg)
+               self.assertTrue(rv == 0, "Error sending out message")
+
+               receive_pkt_check(self.dataplane, outpkt, dp_ports,
+                                 set(of_ports).difference(dp_ports),
+                                 self, basic_logger, basic_config)
+
 class FlowStatsGet(SimpleProtocol):
     """
     Get stats 
@@ -367,6 +412,21 @@
         self.assertTrue(response is not None, "Did not get response")
         basic_logger.debug(response.show())
 
+class DescStatsGet(SimpleProtocol):
+    """
+    Get stats 
+
+    Simply verify stats get transaction
+    """
+    def runTest(self):
+        basic_logger.info("Running DescStatsGet")
+        
+        basic_logger.info("Sending stats request")
+        request = message.desc_stats_request()
+        response, pkt = self.controller.transact(request, timeout=2)
+        self.assertTrue(response is not None, "Did not get response")
+        basic_logger.debug(response.show())
+
 class FlowMod(SimpleProtocol):
     """
     Insert a flow
diff --git a/tests/caps.py b/tests/caps.py
index 9c2c697..bb85818 100644
--- a/tests/caps.py
+++ b/tests/caps.py
@@ -99,7 +99,7 @@
     while True:
         request.match.nw_src += 1
         rv = obj.controller.message_send(request)
-#        do_barrier(obj.controller)
+        do_barrier(obj.controller)
         flow_count += 1
         if flow_count % count_check == 0:
             response, pkt = obj.controller.transact(tstats, timeout=2)
diff --git a/tests/flow_expire.py b/tests/flow_expire.py
index 55f8ebc..53d7181 100644
--- a/tests/flow_expire.py
+++ b/tests/flow_expire.py
@@ -22,11 +22,11 @@
 
 #@var port_map Local copy of the configuration map from OF port
 # numbers to OS interfaces
-pa_port_map = None
-#@var pa_logger Local logger object
-pa_logger = None
-#@var pa_config Local copy of global configuration data
-pa_config = None
+fe_port_map = None
+#@var fe_logger Local logger object
+fe_logger = None
+#@var fe_config Local copy of global configuration data
+fe_config = None
 
 def test_set_init(config):
     """
@@ -37,14 +37,14 @@
 
     basic.test_set_init(config)
 
-    global pa_port_map
-    global pa_logger
-    global pa_config
+    global fe_port_map
+    global fe_logger
+    global fe_config
 
-    pa_logger = logging.getLogger("pkt_act")
-    pa_logger.info("Initializing test set")
-    pa_port_map = config["port_map"]
-    pa_config = config
+    fe_logger = logging.getLogger("flow_expire")
+    fe_logger.info("Initializing test set")
+    fe_port_map = config["port_map"]
+    fe_config = config
 
 class FlowExpire(basic.SimpleDataPlane):
     """
@@ -55,13 +55,16 @@
     Verify the flow expiration message is received
     """
     def runTest(self):
-        global pa_port_map
+        global fe_port_map
 
-        of_ports = pa_port_map.keys()
+        # TODO: set from command-line parameter
+        test_timeout = 60
+
+        of_ports = fe_port_map.keys()
         of_ports.sort()
         self.assertTrue(len(of_ports) > 1, "Not enough ports for test")
 
-        rc = delete_all_flows(self.controller, pa_logger)
+        rc = delete_all_flows(self.controller, fe_logger)
         self.assertEqual(rc, 0, "Failed to delete all flows")
 
         pkt = simple_tcp_packet()
@@ -71,9 +74,13 @@
                         "Could not generate flow match from pkt")
         act = action.action_output()
 
-        ingress_port = pa_config["base_of_port"]
-        egress_port  = (pa_config["base_of_port"] + 1) % len(of_ports)
-        pa_logger.info("Ingress " + str(ingress_port) + 
+        of_ports = fe_port_map.keys()
+        of_ports.sort()
+        self.assertTrue(len(of_ports) > 1, "Not enough ports for test")
+
+        ingress_port = of_ports[0]
+        egress_port  = of_ports[1]
+        fe_logger.info("Ingress " + str(ingress_port) + 
                        " to egress " + str(egress_port))
         
         match.in_port = ingress_port
@@ -87,13 +94,14 @@
         act.port = egress_port
         self.assertTrue(request.actions.add(act), "Could not add action")
         
-        pa_logger.info("Inserting flow")
+        fe_logger.info("Inserting flow")
         rv = self.controller.message_send(request)
         self.assertTrue(rv != -1, "Error installing flow mod")
         do_barrier(self.controller)
 
-        (response, raw) = self.controller.poll(ofp.OFPT_FLOW_REMOVED, 2)
-        
+        (response, pkt) = self.controller.poll(exp_msg=ofp.OFPT_FLOW_REMOVED,
+                                               timeout=test_timeout)
+
         self.assertTrue(response is not None, 
                         'Did not receive flow removed message ')
 
diff --git a/tests/flow_stats.py b/tests/flow_stats.py
index 91c67a6..38560bc 100644
--- a/tests/flow_stats.py
+++ b/tests/flow_stats.py
@@ -49,6 +49,8 @@
     @param config The configuration dictionary; see oft
     """
 
+    basic.test_set_init(config)
+
     global fs_port_map
     global fs_logger
     global fs_config
@@ -58,6 +60,31 @@
     fs_port_map = config["port_map"]
     fs_config = config
 
+def sendPacket(obj, pkt, ingress_port, egress_port, test_timeout):
+
+    fs_logger.info("Sending packet to dp port " + str(ingress_port) +
+                   ", expecting output on " + str(egress_port))
+    obj.dataplane.send(ingress_port, str(pkt))
+
+    exp_pkt_arg = None
+    exp_port = None
+    if fs_config["relax"]:
+        exp_pkt_arg = pkt
+        exp_port = egress_port
+
+    (rcv_port, rcv_pkt, pkt_time) = obj.dataplane.poll(timeout=1, 
+                                                       port_number=exp_port,
+                                                       exp_pkt=exp_pkt_arg)
+    obj.assertTrue(rcv_pkt is not None,
+                   "Packet not received on port " + str(egress_port))
+    fs_logger.debug("Packet len " + str(len(rcv_pkt)) + " in on " + 
+                    str(rcv_port))
+    obj.assertEqual(rcv_port, egress_port,
+                    "Packet received on port " + str(rcv_port) +
+                    ", expected port " + str(egress_port))
+    obj.assertEqual(str(pkt), str(rcv_pkt),
+                    'Response packet does not match send packet')
+
 class SingleFlowStats(basic.SimpleDataPlane):
     """
     Verify flow stats are properly retrieved.
@@ -151,21 +178,8 @@
         num_sends = random.randint(10,20)
         fs_logger.info("Sending " + str(num_sends) + " test packets")
         for i in range(0,num_sends):
-            fs_logger.info("Sending packet to dp port " + 
-                           str(ingress_port))
-            self.dataplane.send(ingress_port, str(pkt))
-            (rcv_port, rcv_pkt, pkt_time) = self.dataplane.poll(timeout=
-                                                                test_timeout)
-            self.assertTrue(rcv_pkt is not None, "Did not receive packet")
-            fs_logger.debug("Packet len " + str(len(pkt)) + " in on " + 
-                            str(rcv_port))
-            self.assertEqual(rcv_port, egress_port, "Unexpected receive port")
-            for j in range(0,test_timeout):
-                if str(pkt) == str(rcv_pkt):
-                    break
-                sleep(1)
-            self.assertTrue(j < test_timeout,
-                            'Timeout sending packets for flow stats test')
+            sendPacket(self, pkt, ingress_port, egress_port,
+                       test_timeout)
 
         self.verifyStats(match, ofp.OFPP_NONE, test_timeout, num_sends)
         self.verifyStats(match, egress_port, test_timeout, num_sends)
@@ -209,22 +223,19 @@
 
         return flow_mod_msg
 
-    def sendPacket(self, pkt, ingress_port, egress_port, test_timeout):
-        fs_logger.info("Sending packet to dp port " + 
-                       str(ingress_port))
-        self.dataplane.send(ingress_port, str(pkt))
-        (rcv_port, rcv_pkt, pkt_time) = self.dataplane.poll(timeout=
-                                                            test_timeout)
-        self.assertTrue(rcv_pkt is not None, "Did not receive packet")
-        fs_logger.debug("Packet len " + str(len(pkt)) + " in on " + 
-                        str(rcv_port))
-        self.assertEqual(rcv_port, egress_port, "Unexpected receive port")
-        for j in range(0,test_timeout):
-            if str(pkt) == str(rcv_pkt):
-                break
-            sleep(1)
-        self.assertTrue(j < test_timeout,
-                        'Timeout sending packets for flow stats test')
+    def sumStatsReplyCounts(self, response):
+        total_packets = 0
+        for obj in response.stats:
+            # TODO: pad1 and pad2 fields may be nonzero, is this a bug?
+            # for now, just clear them so the assert is simpler
+            #obj.match.pad1 = 0
+            #obj.match.pad2 = [0, 0]
+            #self.assertEqual(match, obj.match,
+            #                 "Matches do not match")
+           fs_logger.info("Received " + str(obj.packet_count)
+                          + " packets")
+           total_packets += obj.packet_count
+        return total_packets
 
     def verifyStats(self, match, out_port, test_timeout, packet_count):
         stat_req = message.flow_stats_request()
@@ -235,29 +246,27 @@
         all_packets_received = 0
         for i in range(0,test_timeout):
             fs_logger.info("Sending stats request")
+            # TODO: move REPLY_MORE handling to controller.transact?
             response, pkt = self.controller.transact(stat_req,
                                                      timeout=test_timeout)
             self.assertTrue(response is not None,
                             "No response to stats request")
-            self.assertTrue(len(response.stats) >= 1,
-                            "Did not receive flow stats reply")
-            total_packets = 0
-            for obj in response.stats:
-                # TODO: pad1 and pad2 fields may be nonzero, is this a bug?
-                # for now, just clear them so the assert is simpler
-                #obj.match.pad1 = 0
-                #obj.match.pad2 = [0, 0]
-                #self.assertEqual(match, obj.match,
-                #                 "Matches do not match")
-                fs_logger.info("Received " + str(obj.packet_count) + " packets")
-                total_packets += obj.packet_count
+            total_packets = self.sumStatsReplyCounts(response)
+
+            while response.flags == ofp.OFPSF_REPLY_MORE:
+               response, pkt = self.controller.poll(exp_msg=
+                                                    ofp.OFPT_STATS_REPLY,
+                                                    timeout=test_timeout)
+               total_packets += self.sumStatsReplyCounts(response)
+
             if total_packets == packet_count:
                 all_packets_received = 1
                 break
             sleep(1)
 
         self.assertTrue(all_packets_received,
-                        "Packet count does not match number sent")
+                        "Total stats packet count " + str(total_packets) +
+                        " does not match number sent " + str(packet_count))
 
     def runTest(self):
         global fs_port_map
@@ -294,15 +303,18 @@
         num_pkt2s = random.randint(10,30)
         fs_logger.info("Sending " + str(num_pkt2s) + " pkt2s")
         for i in range(0,num_pkt1s):
-            self.sendPacket(pkt1, ingress_port, egress_port1, test_timeout)
+            sendPacket(self, pkt1, ingress_port, egress_port1, test_timeout)
         for i in range(0,num_pkt2s):
-            self.sendPacket(pkt2, ingress_port, egress_port2, test_timeout)
+            sendPacket(self, pkt2, ingress_port, egress_port2, test_timeout)
             
         match1 = parse.packet_to_flow_match(pkt1)
+        fs_logger.info("Verifying flow1's " + str(num_pkt1s) + " packets")
         self.verifyStats(match1, ofp.OFPP_NONE, test_timeout, num_pkt1s)
         match2 = parse.packet_to_flow_match(pkt2)
+        fs_logger.info("Verifying flow2's " + str(num_pkt2s) + " packets")
         self.verifyStats(match2, ofp.OFPP_NONE, test_timeout, num_pkt2s)
         match1.wildcards |= ofp.OFPFW_DL_SRC
+        fs_logger.info("Verifying combined " + str(num_pkt1s+num_pkt2s) + " packets")
         self.verifyStats(match1, ofp.OFPP_NONE, test_timeout, 
                          num_pkt1s+num_pkt2s)
         # TODO: sweep through the wildcards to verify matching?
@@ -341,23 +353,6 @@
 
         return flow_mod_msg
 
-    def sendPacket(self, pkt, ingress_port, egress_port, test_timeout):
-        fs_logger.info("Sending packet to dp port " + 
-                       str(ingress_port))
-        self.dataplane.send(ingress_port, str(pkt))
-        (rcv_port, rcv_pkt, pkt_time) = self.dataplane.poll(timeout=
-                                                            test_timeout)
-        self.assertTrue(rcv_pkt is not None, "Did not receive packet")
-        fs_logger.debug("Packet len " + str(len(pkt)) + " in on " + 
-                        str(rcv_port))
-        self.assertEqual(rcv_port, egress_port, "Unexpected receive port")
-        for j in range(0,test_timeout):
-            if str(pkt) == str(rcv_pkt):
-                break
-            sleep(1)
-        self.assertTrue(j < test_timeout,
-                        'Timeout sending packets for flow stats test')
-
     def verifyAggFlowStats(self, match, out_port, test_timeout, 
                            flow_count, packet_count):
         stat_req = message.aggregate_stats_request()
@@ -424,9 +419,9 @@
         num_pkt2s = random.randint(10,30)
         fs_logger.info("Sending " + str(num_pkt2s) + " pkt2s")
         for i in range(0,num_pkt1s):
-            self.sendPacket(pkt1, ingress_port, egress_port1, test_timeout)
+            sendPacket(self, pkt1, ingress_port, egress_port1, test_timeout)
         for i in range(0,num_pkt2s):
-            self.sendPacket(pkt2, ingress_port, egress_port2, test_timeout)
+            sendPacket(self, pkt2, ingress_port, egress_port2, test_timeout)
             
         # loop on flow stats request until timeout
         match = parse.packet_to_flow_match(pkt1)
diff --git a/tests/oft b/tests/oft
index c397372..828eefa 100755
--- a/tests/oft
+++ b/tests/oft
@@ -134,6 +134,9 @@
     except:
         sys.exit("Need to install scapy for packet parsing")
 
+##@var Profile module
+profile_mod = None
+
 ##@var DEBUG_LEVELS
 # Map from strings to debugging levels
 DEBUG_LEVELS = {
@@ -167,11 +170,13 @@
     "debug"              : _debug_default,
     "dbg_level"          : _debug_level_default,
     "port_map"           : {},
-    "test_params"        : "None"
+    "test_params"        : "None",
+    "profile"            : None
 }
 
 # Default test priority
 TEST_PRIO_DEFAULT=100
+TEST_PRIO_SKIP=-1
 
 #@todo Set up a dict of config params so easier to manage:
 # <param> <cmdline flags> <default value> <help> <optional parser>
@@ -245,6 +250,8 @@
                       help="Relax packet match checks allowing other packets")
     parser.add_option("--param", type="int",
                       help="Parameter sent to test (for debugging)")
+    parser.add_option("--profile", 
+                      help="File listing tests to skip/run")
     parser.add_option("-t", "--test-params",
                       help="""Set test parameters: key=val;...
         NOTE:  key MUST be a valid Python identifier, egr_count not egr-count
@@ -257,6 +264,29 @@
 
     return (config, args)
 
+def check_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"]
+            profile_mod = None
+    else:
+        logging.info("No profile specified")
+        
+
 def logging_setup(config):
     """
     Set up logging based on config
@@ -359,9 +389,16 @@
 def test_prio_get(mod, test):
     """
     Return the priority of a test
+
+    If test is in "skip list" from profile, return the skip value
+
     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_prio' in dir(mod):
         if test in mod.test_prio.keys():
             return mod.test_prio[test]
@@ -410,11 +447,14 @@
         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"
     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()
diff --git a/tests/pktact.py b/tests/pktact.py
index 129f96d..693293b 100644
--- a/tests/pktact.py
+++ b/tests/pktact.py
@@ -80,6 +80,8 @@
     @param config The configuration dictionary; see oft
     """
 
+    basic.test_set_init(config)
+
     global pa_port_map
     global pa_logger
     global pa_config
@@ -800,9 +802,11 @@
     Verify flow_expiration message is correct when command option is set
     """
     def runTest(self):
+        vid = test_param_get(self.config, 'vid', default=TEST_VID_DEFAULT)
         for wc in WILDCARD_VALUES:
             if wc & ofp.OFPFW_DL_VLAN:
-                dl_vlan = 0
+                # Set nonzero VLAN id to avoid sending priority-tagged packet
+                dl_vlan = vid
             else:
                 dl_vlan = -1
             flow_match_test(self, pa_port_map, wildcards=wc, 
@@ -830,10 +834,12 @@
     Verify flow_expiration message is correct when command option is set
     """
     def runTest(self):
+        vid = test_param_get(self.config, 'vid', default=TEST_VID_DEFAULT)
         for wc in WILDCARD_VALUES:
             all_exp_one_wildcard = ofp.OFPFW_ALL ^ wc
             if all_exp_one_wildcard & ofp.OFPFW_DL_VLAN:
-                dl_vlan = 0
+                # Set nonzero VLAN id to avoid sending priority-tagged packet
+                dl_vlan = vid
             else:
                 dl_vlan = -1
             flow_match_test(self, pa_port_map, wildcards=all_exp_one_wildcard,
@@ -1111,7 +1117,7 @@
     def runTest(self):
         sup_acts = supported_actions_get(self)
         if not (sup_acts & 1 << ofp.OFPAT_SET_DL_DST):
-            skip_message_emit(self, "ModifyL2dstMC test")
+            skip_message_emit(self, "ModifyL2dstIngress test")
             return
 
         (pkt, exp_pkt, acts) = pkt_action_setup(self, mod_fields=['dl_dst'],
@@ -1186,13 +1192,92 @@
         flow_match_test(self, pa_port_map, pkt=pkt, exp_pkt=exp_pkt, 
                         action_list=acts, max_test=2, egr_count=-1)
 
+class FlowToggle(BaseMatchCase):
+    """
+    Add flows to the table and modify them repeatedly
+    """
+    def runTest(self):
+        flow_count = test_param_get(self.config, 'ft_flow_count', default=20)
+        iter_count = test_param_get(self.config, 'ft_iter_count', default=10)
+
+        pa_logger.info("Running flow toggle with %d flows, %d iterations" %
+                       (flow_count, iter_count))
+        acts = []
+        acts.append(action.action_output())
+        acts.append(action.action_output())
+    
+        of_ports = pa_port_map.keys()
+        if len(of_ports) < 3:
+            self.assertTrue(False, "Too few ports for test")
+    
+        for idx in range(2):
+            acts[idx].port = of_ports[idx]
+    
+        flows = []
+        flows.append([])
+        flows.append([])
+    
+        wildcards = ofp.OFPFW_DL_SRC | ofp.OFPFW_DL_DST
+        # Create up the flows in an array
+        for toggle in range(2):
+            for f_idx in range(flow_count):
+                pkt = simple_tcp_packet(tcp_sport=f_idx)
+                msg = message.flow_mod()
+                match = parse.packet_to_flow_match(pkt)
+                match.in_port = of_ports[3]
+                match.wildcards = wildcards
+                msg.match = match
+                msg.buffer_id = 0xffffffff
+                msg.actions.add(acts[toggle])
+                flows[toggle].append(msg)
+
+        # Show two sample flows
+        pa_logger.debug(flows[0][0].show())
+        pa_logger.debug(flows[1][0].show())
+
+        # Install the first set of flows
+        for f_idx in range(flow_count):
+            rv = self.controller.message_send(flows[0][f_idx])
+            self.assertTrue(rv != -1, "Error installing flow %d" % f_idx)
+        do_barrier(self.controller)
+    
+        pa_logger.info("Installed %d flows" % flow_count)
+    
+        # Repeatedly modify all the flows back and forth
+        updates = 0
+        # Report status about 5 times
+        mod_val = (iter_count / 4) + 1
+        start = time.time()
+        for iter_idx in range(iter_count):
+            if not iter_idx % mod_val:
+                pa_logger.info("Flow Toggle: iter %d of %d. " %
+                               (iter_idx, iter_count) + 
+                               "%d updates in %d secs" %
+                               (updates, time.time() - start))
+            for toggle in range(2):
+                t_idx = 1 - toggle
+                for f_idx in range(flow_count):
+                    rv = self.controller.message_send(flows[t_idx][f_idx])
+                    updates += 1
+                    self.assertTrue(rv != -1, "Error modifying flow %d" % 
+                                    f_idx)
+                do_barrier(self.controller)
+
+        end = time.time()
+        divisor = end - start or (end - start + 1)
+        pa_logger.info("Flow toggle: %d iterations" % iter_count)
+        pa_logger.info("   %d flow mods in %d secs, %d mods/sec" %
+                       (updates, end - start, updates/divisor))
+            
+
 # You can pick and choose these by commenting tests in or out
 iter_classes = [
     basic.PacketIn,
     basic.PacketOut,
     DirectPacket,
+    FlowToggle,
     DirectTwoPorts,
-    DirectMC,
+    DirectMCNonIngress,
     AllWildcardMatch,
     AllWildcardMatchTagged,
     SingleWildcardMatch,
@@ -1208,6 +1293,12 @@
     ]
 
 class IterCases(BaseMatchCase):
+    """
+    Iterate over a bunch of test cases
+
+    The cases come from the list above
+    """
+
     def runTest(self):
         count = test_param_get(self.config, 'iter_count', default=10)
         tests_done = 0
@@ -1221,11 +1312,13 @@
                 test.inheritSetup(self)
                 test.runTest()
                 tests_done += 1
+                # Report update about every minute, between tests
                 if time.time() - last > 60:
                     last = time.time()
-                    print("IterCases: Ran %d tests in %d " %
-                          (tests_done, last - start) + 
-                          "seconds so far")
+                    pa_logger.info(
+                        "IterCases: Iter %d of %d; Ran %d tests in %d " %
+                        (idx, count, tests_done, last - start) + 
+                        "seconds so far")
         stats = all_stats_get(self)
         last = time.time()
         pa_logger.info("\nIterCases ran %d tests in %d seconds." %
diff --git a/tools/pylibopenflow/include/Put C header files here... b/tests/profiles/__init__.py
similarity index 100%
copy from tools/pylibopenflow/include/Put C header files here...
copy to tests/profiles/__init__.py
diff --git a/tests/profiles/example.py b/tests/profiles/example.py
new file mode 100644
index 0000000..938a6f2
--- /dev/null
+++ b/tests/profiles/example.py
@@ -0,0 +1,36 @@
+"""
+Sample profile
+
+A profile determines run specific behavior.  It is meant to capture 
+variations between different switch targets.
+
+A profile defines two target specific variables.
+
+1. A set of tests to skip
+
+TO BE IMPLEMENTED:
+
+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
+"""
+
+#@var skip_test_list The list of tests to skip for this run
+skip_test_list = []
+
+# TO BE IMPLEMENTED
+# A list of test cases with parameters(?)
+# TBD
+#@var run_test_list List of tests to run which would normally be skipped
+run_test_list = dict(
+    # Example
+    # SomeTestCase = [dict(<params1>), dict(<params2>),...],
+)
+
+# for test_dict in profile.run_test_list:
+#     for test_name, test_params in test_dict.items():
+#          ...
diff --git a/tests/profiles/noing.py b/tests/profiles/noing.py
new file mode 100644
index 0000000..c9abc63
--- /dev/null
+++ b/tests/profiles/noing.py
@@ -0,0 +1,19 @@
+"""
+No-ingress action profile
+
+Profile for switch that does not support the IN_PORT port
+in the output action.
+
+We also don't run the port config modify test for this profile.
+"""
+
+#@var skip_test_list The list of tests to skip for this run
+skip_test_list = [
+    "PortConfigMod",
+    "FloodMinusPort",
+    "ModifyL2DstIngressMC",
+    "ModifyL2DstIngress",
+    "DirectMC",
+    "AllPlusIngress",
+    "FloodPlusIngress"
+]
diff --git a/tests/testutils.py b/tests/testutils.py
index 0a6c4bd..f1459bf 100644
--- a/tests/testutils.py
+++ b/tests/testutils.py
@@ -224,11 +224,11 @@
     rv = controller.message_send(mod)
     return rv
 
-def receive_pkt_check(dataplane, pkt, yes_ports, no_ports, assert_if, logger,
+def receive_pkt_check(dp, pkt, yes_ports, no_ports, assert_if, logger,
                       config):
     """
     Check for proper receive packets across all ports
-    @param dataplane The dataplane object
+    @param dp The dataplane object
     @param pkt Expected packet; may be None if yes_ports is empty
     @param yes_ports Set or list of ports that should recieve packet
     @param no_ports Set or list of ports that should not receive packet
@@ -240,17 +240,20 @@
 
     for ofport in yes_ports:
         logger.debug("Checking for pkt on port " + str(ofport))
-        (rcv_port, rcv_pkt, pkt_time) = dataplane.poll(
+        (rcv_port, rcv_pkt, pkt_time) = dp.poll(
             port_number=ofport, timeout=1, exp_pkt=exp_pkt_arg)
         assert_if.assertTrue(rcv_pkt is not None, 
                              "Did not receive pkt on " + str(ofport))
-        assert_if.assertEqual(str(pkt), str(rcv_pkt),
-                              "Response packet does not match send packet " +
-                              "on port " + str(ofport))
+        if not dataplane.match_exp_pkt(pkt, rcv_pkt):
+            logger.debug("Sent %s" % format_packet(pkt))
+            logger.debug("Resp %s" % format_packet(rcv_pkt))
+        assert_if.assertTrue(dataplane.match_exp_pkt(pkt, rcv_pkt),
+                             "Response packet does not match send packet " +
+                             "on port " + str(ofport))
 
     for ofport in no_ports:
         logger.debug("Negative check for pkt on port " + str(ofport))
-        (rcv_port, rcv_pkt, pkt_time) = dataplane.poll(
+        (rcv_port, rcv_pkt, pkt_time) = dp.poll(
             port_number=ofport, timeout=1, exp_pkt=exp_pkt_arg)
         assert_if.assertTrue(rcv_pkt is None, 
                              "Unexpected pkt on port " + str(ofport))
@@ -817,3 +820,26 @@
         rv["matched"] += obj.matched_count
 
     return rv
+
+FILTER=''.join([(len(repr(chr(x)))==3) and chr(x) or '.' 
+                for x in range(256)])
+
+def hex_dump_buffer(src, length=16):
+    """
+    Convert src to a hex dump string and return the string
+    @param src The source buffer
+    @param length The number of bytes shown in each line
+    @returns A string showing the hex dump
+    """
+    result = []
+    for i in xrange(0, len(src), length):
+       chars = src[i:i+length]
+       hex = ' '.join(["%02x" % ord(x) for x in chars])
+       printable = ''.join(["%s" % ((ord(x) <= 127 and
+                                     FILTER[ord(x)]) or '.') for x in chars])
+       result.append("%04x  %-*s  %s\n" % (i, length*3, hex, printable))
+    return ''.join(result)
+
+def format_packet(pkt):
+    return "Packet length %d \n%s" % (len(str(pkt)), 
+                                      hex_dump_buffer(str(pkt)))
diff --git a/tools/pylibopenflow/include/Put C header files here... b/tools/pylibopenflow/include/Put_C_header_files_here
similarity index 100%
rename from tools/pylibopenflow/include/Put C header files here...
rename to tools/pylibopenflow/include/Put_C_header_files_here