Merge pull request #56 from rlane/of13

OpenFlow 1.3 basic test suite
diff --git a/src/python/loxi/of10/common.py b/src/python/loxi/of10/common.py
index 616cf75..28b079a 100644
--- a/src/python/loxi/of10/common.py
+++ b/src/python/loxi/of10/common.py
@@ -385,7 +385,7 @@
 
 class match_v1(object):
 
-    def __init__(self, wildcards=None, in_port=None, eth_src=None, eth_dst=None, vlan_vid=None, vlan_pcp=None, eth_type=None, ip_dscp=None, ip_proto=None, src_meta_id=None, dst_meta_id=None, ipv4_src=None, ipv4_dst=None, tcp_src=None, tcp_dst=None):
+    def __init__(self, wildcards=None, in_port=None, eth_src=None, eth_dst=None, vlan_vid=None, vlan_pcp=None, eth_type=None, ip_dscp=None, ip_proto=None, ipv4_src=None, ipv4_dst=None, tcp_src=None, tcp_dst=None):
         if wildcards != None:
             self.wildcards = wildcards
         else:
@@ -422,14 +422,6 @@
             self.ip_proto = ip_proto
         else:
             self.ip_proto = 0
-        if src_meta_id != None:
-            self.src_meta_id = src_meta_id
-        else:
-            self.src_meta_id = 0
-        if dst_meta_id != None:
-            self.dst_meta_id = dst_meta_id
-        else:
-            self.dst_meta_id = 0
         if ipv4_src != None:
             self.ipv4_src = ipv4_src
         else:
@@ -460,8 +452,7 @@
         packed.append(struct.pack("!H", self.eth_type))
         packed.append(struct.pack("!B", self.ip_dscp))
         packed.append(struct.pack("!B", self.ip_proto))
-        packed.append(struct.pack("!B", self.src_meta_id))
-        packed.append(struct.pack("!B", self.dst_meta_id))
+        packed.append('\x00' * 2)
         packed.append(struct.pack("!L", self.ipv4_src))
         packed.append(struct.pack("!L", self.ipv4_dst))
         packed.append(struct.pack("!H", self.tcp_src))
@@ -485,8 +476,7 @@
         obj.eth_type = reader.read("!H")[0]
         obj.ip_dscp = reader.read("!B")[0]
         obj.ip_proto = reader.read("!B")[0]
-        obj.src_meta_id = reader.read("!B")[0]
-        obj.dst_meta_id = reader.read("!B")[0]
+        reader.skip(2)
         obj.ipv4_src = reader.read("!L")[0]
         obj.ipv4_dst = reader.read("!L")[0]
         obj.tcp_src = reader.read("!H")[0]
@@ -504,8 +494,6 @@
         if self.eth_type != other.eth_type: return False
         if self.ip_dscp != other.ip_dscp: return False
         if self.ip_proto != other.ip_proto: return False
-        if self.src_meta_id != other.src_meta_id: return False
-        if self.dst_meta_id != other.dst_meta_id: return False
         if self.ipv4_src != other.ipv4_src: return False
         if self.ipv4_dst != other.ipv4_dst: return False
         if self.tcp_src != other.tcp_src: return False
@@ -551,12 +539,6 @@
                 q.text("ip_proto = ");
                 q.text("%#x" % self.ip_proto)
                 q.text(","); q.breakable()
-                q.text("src_meta_id = ");
-                q.text("%#x" % self.src_meta_id)
-                q.text(","); q.breakable()
-                q.text("dst_meta_id = ");
-                q.text("%#x" % self.dst_meta_id)
-                q.text(","); q.breakable()
                 q.text("ipv4_src = ");
                 q.text(util.pretty_ipv4(self.ipv4_src))
                 q.text(","); q.breakable()
diff --git a/src/python/loxi/of10/const.py b/src/python/loxi/of10/const.py
index 0273c7a..1d29975 100644
--- a/src/python/loxi/of10/const.py
+++ b/src/python/loxi/of10/const.py
@@ -235,8 +235,6 @@
 OFPFW_NW_DST_MASK = 1032192
 OFPFW_DL_VLAN_PCP = 1048576
 OFPFW_NW_TOS = 2097152
-OFPFW_SRC_META_ID = 4194304
-OFPFW_DST_META_ID = 8388608
 OFPFW_ALL = 4194303
 
 ofp_flow_wildcards_map = {
@@ -250,8 +248,6 @@
     128: 'OFPFW_TP_DST',
     1048576: 'OFPFW_DL_VLAN_PCP',
     2097152: 'OFPFW_NW_TOS',
-    4194304: 'OFPFW_SRC_META_ID',
-    8388608: 'OFPFW_DST_META_ID',
 }
 
 # Identifiers from group ofp_hello_failed_code
diff --git a/src/python/loxi/of10/message.py b/src/python/loxi/of10/message.py
index e01c342..6e6649e 100644
--- a/src/python/loxi/of10/message.py
+++ b/src/python/loxi/of10/message.py
@@ -6281,8 +6281,8 @@
 
 def parse_message(buf):
     msg_ver, msg_type, msg_len, msg_xid = parse_header(buf)
-    if msg_ver != const.OFP_VERSION and msg_type != ofp.OFPT_HELLO:
-        raise loxi.ProtocolError("wrong OpenFlow version")
+    if msg_ver != const.OFP_VERSION and msg_type != const.OFPT_HELLO:
+        raise loxi.ProtocolError("wrong OpenFlow version (expected %d, got %d)" % (const.OFP_VERSION, msg_ver))
     if len(buf) != msg_len:
         raise loxi.ProtocolError("incorrect message size")
     if msg_type in parsers:
@@ -6291,9 +6291,10 @@
         raise loxi.ProtocolError("unexpected message type")
 
 def parse_flow_mod(buf):
-    if len(buf) < 56 + 2:
+    if len(buf) < 57 + 1:
         raise loxi.ProtocolError("message too short")
-    cmd, = struct.unpack_from("!H", buf, 56)
+    # Technically uint16_t for OF 1.0
+    cmd, = struct.unpack_from("!B", buf, 57)
     if cmd in flow_mod_parsers:
         return flow_mod_parsers[cmd](buf)
     else:
diff --git a/src/python/loxi/of11/message.py b/src/python/loxi/of11/message.py
index 12aa4a1..091e70c 100644
--- a/src/python/loxi/of11/message.py
+++ b/src/python/loxi/of11/message.py
@@ -5973,8 +5973,8 @@
 
 def parse_message(buf):
     msg_ver, msg_type, msg_len, msg_xid = parse_header(buf)
-    if msg_ver != const.OFP_VERSION and msg_type != ofp.OFPT_HELLO:
-        raise loxi.ProtocolError("wrong OpenFlow version")
+    if msg_ver != const.OFP_VERSION and msg_type != const.OFPT_HELLO:
+        raise loxi.ProtocolError("wrong OpenFlow version (expected %d, got %d)" % (const.OFP_VERSION, msg_ver))
     if len(buf) != msg_len:
         raise loxi.ProtocolError("incorrect message size")
     if msg_type in parsers:
@@ -5983,9 +5983,10 @@
         raise loxi.ProtocolError("unexpected message type")
 
 def parse_flow_mod(buf):
-    if len(buf) < 56 + 2:
+    if len(buf) < 25 + 1:
         raise loxi.ProtocolError("message too short")
-    cmd, = struct.unpack_from("!H", buf, 56)
+    # Technically uint16_t for OF 1.0
+    cmd, = struct.unpack_from("!B", buf, 25)
     if cmd in flow_mod_parsers:
         return flow_mod_parsers[cmd](buf)
     else:
diff --git a/src/python/loxi/of12/message.py b/src/python/loxi/of12/message.py
index 76424fe..ed1f04f 100644
--- a/src/python/loxi/of12/message.py
+++ b/src/python/loxi/of12/message.py
@@ -6405,8 +6405,8 @@
 
 def parse_message(buf):
     msg_ver, msg_type, msg_len, msg_xid = parse_header(buf)
-    if msg_ver != const.OFP_VERSION and msg_type != ofp.OFPT_HELLO:
-        raise loxi.ProtocolError("wrong OpenFlow version")
+    if msg_ver != const.OFP_VERSION and msg_type != const.OFPT_HELLO:
+        raise loxi.ProtocolError("wrong OpenFlow version (expected %d, got %d)" % (const.OFP_VERSION, msg_ver))
     if len(buf) != msg_len:
         raise loxi.ProtocolError("incorrect message size")
     if msg_type in parsers:
@@ -6415,9 +6415,10 @@
         raise loxi.ProtocolError("unexpected message type")
 
 def parse_flow_mod(buf):
-    if len(buf) < 56 + 2:
+    if len(buf) < 25 + 1:
         raise loxi.ProtocolError("message too short")
-    cmd, = struct.unpack_from("!H", buf, 56)
+    # Technically uint16_t for OF 1.0
+    cmd, = struct.unpack_from("!B", buf, 25)
     if cmd in flow_mod_parsers:
         return flow_mod_parsers[cmd](buf)
     else:
diff --git a/src/python/loxi/of12/oxm.py b/src/python/loxi/of12/oxm.py
index fb1291d..f17151e 100644
--- a/src/python/loxi/of12/oxm.py
+++ b/src/python/loxi/of12/oxm.py
@@ -566,114 +566,6 @@
             q.breakable()
         q.text('}')
 
-class dst_meta_id(OXM):
-    type_len = 258561
-
-    def __init__(self, value=None):
-        if value != None:
-            self.value = value
-        else:
-            self.value = 0
-
-    def pack(self):
-        packed = []
-        packed.append(struct.pack("!L", self.type_len))
-        packed.append(struct.pack("!B", self.value))
-        return ''.join(packed)
-
-    @staticmethod
-    def unpack(buf):
-        obj = dst_meta_id()
-        if type(buf) == loxi.generic_util.OFReader:
-            reader = buf
-        else:
-            reader = loxi.generic_util.OFReader(buf)
-        _type_len = reader.read("!L")[0]
-        assert(_type_len == 258561)
-        obj.value = reader.read("!B")[0]
-        return obj
-
-    def __eq__(self, other):
-        if type(self) != type(other): return False
-        if self.value != other.value: return False
-        return True
-
-    def __ne__(self, other):
-        return not self.__eq__(other)
-
-    def show(self):
-        import loxi.pp
-        return loxi.pp.pp(self)
-
-    def pretty_print(self, q):
-        q.text("dst_meta_id {")
-        with q.group():
-            with q.indent(2):
-                q.breakable()
-                q.text("value = ");
-                q.text("%#x" % self.value)
-            q.breakable()
-        q.text('}')
-
-class dst_meta_id_masked(OXM):
-    type_len = 258818
-
-    def __init__(self, value=None, value_mask=None):
-        if value != None:
-            self.value = value
-        else:
-            self.value = 0
-        if value_mask != None:
-            self.value_mask = value_mask
-        else:
-            self.value_mask = 0
-
-    def pack(self):
-        packed = []
-        packed.append(struct.pack("!L", self.type_len))
-        packed.append(struct.pack("!B", self.value))
-        packed.append(struct.pack("!B", self.value_mask))
-        return ''.join(packed)
-
-    @staticmethod
-    def unpack(buf):
-        obj = dst_meta_id_masked()
-        if type(buf) == loxi.generic_util.OFReader:
-            reader = buf
-        else:
-            reader = loxi.generic_util.OFReader(buf)
-        _type_len = reader.read("!L")[0]
-        assert(_type_len == 258818)
-        obj.value = reader.read("!B")[0]
-        obj.value_mask = reader.read("!B")[0]
-        return obj
-
-    def __eq__(self, other):
-        if type(self) != type(other): return False
-        if self.value != other.value: return False
-        if self.value_mask != other.value_mask: return False
-        return True
-
-    def __ne__(self, other):
-        return not self.__eq__(other)
-
-    def show(self):
-        import loxi.pp
-        return loxi.pp.pp(self)
-
-    def pretty_print(self, q):
-        q.text("dst_meta_id_masked {")
-        with q.group():
-            with q.indent(2):
-                q.breakable()
-                q.text("value = ");
-                q.text("%#x" % self.value)
-                q.text(","); q.breakable()
-                q.text("value_mask = ");
-                q.text("%#x" % self.value_mask)
-            q.breakable()
-        q.text('}')
-
 class eth_dst(OXM):
     type_len = 2147485190
 
@@ -3374,114 +3266,6 @@
             q.breakable()
         q.text('}')
 
-class src_meta_id(OXM):
-    type_len = 258049
-
-    def __init__(self, value=None):
-        if value != None:
-            self.value = value
-        else:
-            self.value = 0
-
-    def pack(self):
-        packed = []
-        packed.append(struct.pack("!L", self.type_len))
-        packed.append(struct.pack("!B", self.value))
-        return ''.join(packed)
-
-    @staticmethod
-    def unpack(buf):
-        obj = src_meta_id()
-        if type(buf) == loxi.generic_util.OFReader:
-            reader = buf
-        else:
-            reader = loxi.generic_util.OFReader(buf)
-        _type_len = reader.read("!L")[0]
-        assert(_type_len == 258049)
-        obj.value = reader.read("!B")[0]
-        return obj
-
-    def __eq__(self, other):
-        if type(self) != type(other): return False
-        if self.value != other.value: return False
-        return True
-
-    def __ne__(self, other):
-        return not self.__eq__(other)
-
-    def show(self):
-        import loxi.pp
-        return loxi.pp.pp(self)
-
-    def pretty_print(self, q):
-        q.text("src_meta_id {")
-        with q.group():
-            with q.indent(2):
-                q.breakable()
-                q.text("value = ");
-                q.text("%#x" % self.value)
-            q.breakable()
-        q.text('}')
-
-class src_meta_id_masked(OXM):
-    type_len = 258306
-
-    def __init__(self, value=None, value_mask=None):
-        if value != None:
-            self.value = value
-        else:
-            self.value = 0
-        if value_mask != None:
-            self.value_mask = value_mask
-        else:
-            self.value_mask = 0
-
-    def pack(self):
-        packed = []
-        packed.append(struct.pack("!L", self.type_len))
-        packed.append(struct.pack("!B", self.value))
-        packed.append(struct.pack("!B", self.value_mask))
-        return ''.join(packed)
-
-    @staticmethod
-    def unpack(buf):
-        obj = src_meta_id_masked()
-        if type(buf) == loxi.generic_util.OFReader:
-            reader = buf
-        else:
-            reader = loxi.generic_util.OFReader(buf)
-        _type_len = reader.read("!L")[0]
-        assert(_type_len == 258306)
-        obj.value = reader.read("!B")[0]
-        obj.value_mask = reader.read("!B")[0]
-        return obj
-
-    def __eq__(self, other):
-        if type(self) != type(other): return False
-        if self.value != other.value: return False
-        if self.value_mask != other.value_mask: return False
-        return True
-
-    def __ne__(self, other):
-        return not self.__eq__(other)
-
-    def show(self):
-        import loxi.pp
-        return loxi.pp.pp(self)
-
-    def pretty_print(self, q):
-        q.text("src_meta_id_masked {")
-        with q.group():
-            with q.indent(2):
-                q.breakable()
-                q.text("value = ");
-                q.text("%#x" % self.value)
-                q.text(","); q.breakable()
-                q.text("value_mask = ");
-                q.text("%#x" % self.value_mask)
-            q.breakable()
-        q.text('}')
-
 class tcp_dst(OXM):
     type_len = 2147490818
 
@@ -4132,10 +3916,6 @@
 
 
 parsers = {
-    258049 : src_meta_id.unpack,
-    258306 : src_meta_id_masked.unpack,
-    258561 : dst_meta_id.unpack,
-    258818 : dst_meta_id_masked.unpack,
     2147483652 : in_port.unpack,
     2147483912 : in_port_masked.unpack,
     2147484164 : in_phy_port.unpack,
diff --git a/src/python/loxi/of13/message.py b/src/python/loxi/of13/message.py
index 543442b..4c22e45 100644
--- a/src/python/loxi/of13/message.py
+++ b/src/python/loxi/of13/message.py
@@ -7554,8 +7554,8 @@
 
 def parse_message(buf):
     msg_ver, msg_type, msg_len, msg_xid = parse_header(buf)
-    if msg_ver != const.OFP_VERSION and msg_type != ofp.OFPT_HELLO:
-        raise loxi.ProtocolError("wrong OpenFlow version")
+    if msg_ver != const.OFP_VERSION and msg_type != const.OFPT_HELLO:
+        raise loxi.ProtocolError("wrong OpenFlow version (expected %d, got %d)" % (const.OFP_VERSION, msg_ver))
     if len(buf) != msg_len:
         raise loxi.ProtocolError("incorrect message size")
     if msg_type in parsers:
@@ -7564,9 +7564,10 @@
         raise loxi.ProtocolError("unexpected message type")
 
 def parse_flow_mod(buf):
-    if len(buf) < 56 + 2:
+    if len(buf) < 25 + 1:
         raise loxi.ProtocolError("message too short")
-    cmd, = struct.unpack_from("!H", buf, 56)
+    # Technically uint16_t for OF 1.0
+    cmd, = struct.unpack_from("!B", buf, 25)
     if cmd in flow_mod_parsers:
         return flow_mod_parsers[cmd](buf)
     else:
@@ -7648,7 +7649,39 @@
     const.OFPFC_DELETE_STRICT : flow_delete_strict.unpack,
 }
 
-# TODO OF 1.3 multipart messages
+multipart_reply_parsers = {
+    const.OFPMP_DESC : desc_stats_reply.unpack,
+    const.OFPMP_FLOW : flow_stats_reply.unpack,
+    const.OFPMP_AGGREGATE : aggregate_stats_reply.unpack,
+    const.OFPMP_TABLE : table_stats_reply.unpack,
+    const.OFPMP_PORT_STATS : port_stats_reply.unpack,
+    const.OFPMP_QUEUE : queue_stats_reply.unpack,
+    const.OFPMP_GROUP : group_stats_reply.unpack,
+    const.OFPMP_GROUP_DESC : group_desc_stats_reply.unpack,
+    const.OFPMP_GROUP_FEATURES : group_features_stats_reply.unpack,
+    const.OFPMP_METER : meter_stats_reply.unpack,
+    const.OFPMP_METER_CONFIG : meter_config_stats_reply.unpack,
+    const.OFPMP_METER_FEATURES : meter_features_stats_reply.unpack,
+    const.OFPMP_TABLE_FEATURES : table_features_stats_reply.unpack,
+    const.OFPMP_PORT_DESC : port_desc_stats_reply.unpack,
+}
+
+multipart_request_parsers = {
+    const.OFPMP_DESC : desc_stats_request.unpack,
+    const.OFPMP_FLOW : flow_stats_request.unpack,
+    const.OFPMP_AGGREGATE : aggregate_stats_request.unpack,
+    const.OFPMP_TABLE : table_stats_request.unpack,
+    const.OFPMP_PORT_STATS : port_stats_request.unpack,
+    const.OFPMP_QUEUE : queue_stats_request.unpack,
+    const.OFPMP_GROUP : group_stats_request.unpack,
+    const.OFPMP_GROUP_DESC : group_desc_stats_request.unpack,
+    const.OFPMP_GROUP_FEATURES : group_features_stats_request.unpack,
+    const.OFPMP_METER : meter_stats_request.unpack,
+    const.OFPMP_METER_CONFIG : meter_config_stats_request.unpack,
+    const.OFPMP_METER_FEATURES : meter_features_stats_request.unpack,
+    const.OFPMP_TABLE_FEATURES : table_features_stats_request.unpack,
+    const.OFPMP_PORT_DESC : port_desc_stats_request.unpack,
+}
 
 experimenter_parsers = {
     6035143 : {
diff --git a/src/python/loxi/of13/oxm.py b/src/python/loxi/of13/oxm.py
index fb1291d..f17151e 100644
--- a/src/python/loxi/of13/oxm.py
+++ b/src/python/loxi/of13/oxm.py
@@ -566,114 +566,6 @@
             q.breakable()
         q.text('}')
 
-class dst_meta_id(OXM):
-    type_len = 258561
-
-    def __init__(self, value=None):
-        if value != None:
-            self.value = value
-        else:
-            self.value = 0
-
-    def pack(self):
-        packed = []
-        packed.append(struct.pack("!L", self.type_len))
-        packed.append(struct.pack("!B", self.value))
-        return ''.join(packed)
-
-    @staticmethod
-    def unpack(buf):
-        obj = dst_meta_id()
-        if type(buf) == loxi.generic_util.OFReader:
-            reader = buf
-        else:
-            reader = loxi.generic_util.OFReader(buf)
-        _type_len = reader.read("!L")[0]
-        assert(_type_len == 258561)
-        obj.value = reader.read("!B")[0]
-        return obj
-
-    def __eq__(self, other):
-        if type(self) != type(other): return False
-        if self.value != other.value: return False
-        return True
-
-    def __ne__(self, other):
-        return not self.__eq__(other)
-
-    def show(self):
-        import loxi.pp
-        return loxi.pp.pp(self)
-
-    def pretty_print(self, q):
-        q.text("dst_meta_id {")
-        with q.group():
-            with q.indent(2):
-                q.breakable()
-                q.text("value = ");
-                q.text("%#x" % self.value)
-            q.breakable()
-        q.text('}')
-
-class dst_meta_id_masked(OXM):
-    type_len = 258818
-
-    def __init__(self, value=None, value_mask=None):
-        if value != None:
-            self.value = value
-        else:
-            self.value = 0
-        if value_mask != None:
-            self.value_mask = value_mask
-        else:
-            self.value_mask = 0
-
-    def pack(self):
-        packed = []
-        packed.append(struct.pack("!L", self.type_len))
-        packed.append(struct.pack("!B", self.value))
-        packed.append(struct.pack("!B", self.value_mask))
-        return ''.join(packed)
-
-    @staticmethod
-    def unpack(buf):
-        obj = dst_meta_id_masked()
-        if type(buf) == loxi.generic_util.OFReader:
-            reader = buf
-        else:
-            reader = loxi.generic_util.OFReader(buf)
-        _type_len = reader.read("!L")[0]
-        assert(_type_len == 258818)
-        obj.value = reader.read("!B")[0]
-        obj.value_mask = reader.read("!B")[0]
-        return obj
-
-    def __eq__(self, other):
-        if type(self) != type(other): return False
-        if self.value != other.value: return False
-        if self.value_mask != other.value_mask: return False
-        return True
-
-    def __ne__(self, other):
-        return not self.__eq__(other)
-
-    def show(self):
-        import loxi.pp
-        return loxi.pp.pp(self)
-
-    def pretty_print(self, q):
-        q.text("dst_meta_id_masked {")
-        with q.group():
-            with q.indent(2):
-                q.breakable()
-                q.text("value = ");
-                q.text("%#x" % self.value)
-                q.text(","); q.breakable()
-                q.text("value_mask = ");
-                q.text("%#x" % self.value_mask)
-            q.breakable()
-        q.text('}')
-
 class eth_dst(OXM):
     type_len = 2147485190
 
@@ -3374,114 +3266,6 @@
             q.breakable()
         q.text('}')
 
-class src_meta_id(OXM):
-    type_len = 258049
-
-    def __init__(self, value=None):
-        if value != None:
-            self.value = value
-        else:
-            self.value = 0
-
-    def pack(self):
-        packed = []
-        packed.append(struct.pack("!L", self.type_len))
-        packed.append(struct.pack("!B", self.value))
-        return ''.join(packed)
-
-    @staticmethod
-    def unpack(buf):
-        obj = src_meta_id()
-        if type(buf) == loxi.generic_util.OFReader:
-            reader = buf
-        else:
-            reader = loxi.generic_util.OFReader(buf)
-        _type_len = reader.read("!L")[0]
-        assert(_type_len == 258049)
-        obj.value = reader.read("!B")[0]
-        return obj
-
-    def __eq__(self, other):
-        if type(self) != type(other): return False
-        if self.value != other.value: return False
-        return True
-
-    def __ne__(self, other):
-        return not self.__eq__(other)
-
-    def show(self):
-        import loxi.pp
-        return loxi.pp.pp(self)
-
-    def pretty_print(self, q):
-        q.text("src_meta_id {")
-        with q.group():
-            with q.indent(2):
-                q.breakable()
-                q.text("value = ");
-                q.text("%#x" % self.value)
-            q.breakable()
-        q.text('}')
-
-class src_meta_id_masked(OXM):
-    type_len = 258306
-
-    def __init__(self, value=None, value_mask=None):
-        if value != None:
-            self.value = value
-        else:
-            self.value = 0
-        if value_mask != None:
-            self.value_mask = value_mask
-        else:
-            self.value_mask = 0
-
-    def pack(self):
-        packed = []
-        packed.append(struct.pack("!L", self.type_len))
-        packed.append(struct.pack("!B", self.value))
-        packed.append(struct.pack("!B", self.value_mask))
-        return ''.join(packed)
-
-    @staticmethod
-    def unpack(buf):
-        obj = src_meta_id_masked()
-        if type(buf) == loxi.generic_util.OFReader:
-            reader = buf
-        else:
-            reader = loxi.generic_util.OFReader(buf)
-        _type_len = reader.read("!L")[0]
-        assert(_type_len == 258306)
-        obj.value = reader.read("!B")[0]
-        obj.value_mask = reader.read("!B")[0]
-        return obj
-
-    def __eq__(self, other):
-        if type(self) != type(other): return False
-        if self.value != other.value: return False
-        if self.value_mask != other.value_mask: return False
-        return True
-
-    def __ne__(self, other):
-        return not self.__eq__(other)
-
-    def show(self):
-        import loxi.pp
-        return loxi.pp.pp(self)
-
-    def pretty_print(self, q):
-        q.text("src_meta_id_masked {")
-        with q.group():
-            with q.indent(2):
-                q.breakable()
-                q.text("value = ");
-                q.text("%#x" % self.value)
-                q.text(","); q.breakable()
-                q.text("value_mask = ");
-                q.text("%#x" % self.value_mask)
-            q.breakable()
-        q.text('}')
-
 class tcp_dst(OXM):
     type_len = 2147490818
 
@@ -4132,10 +3916,6 @@
 
 
 parsers = {
-    258049 : src_meta_id.unpack,
-    258306 : src_meta_id_masked.unpack,
-    258561 : dst_meta_id.unpack,
-    258818 : dst_meta_id_masked.unpack,
     2147483652 : in_port.unpack,
     2147483912 : in_port_masked.unpack,
     2147484164 : in_phy_port.unpack,
diff --git a/src/python/oftest/testutils.py b/src/python/oftest/testutils.py
index 821f962..7e1b2c8 100644
--- a/src/python/oftest/testutils.py
+++ b/src/python/oftest/testutils.py
@@ -40,9 +40,15 @@
 
     logging.info("Deleting all flows")
     msg = ofp.message.flow_delete()
-    msg.match.wildcards = ofp.OFPFW_ALL
-    msg.out_port = ofp.OFPP_NONE
-    msg.buffer_id = 0xffffffff
+    if ofp.OFP_VERSION in [1, 2]:
+        msg.match.wildcards = ofp.OFPFW_ALL
+        msg.out_port = ofp.OFPP_NONE
+        msg.buffer_id = 0xffffffff
+    elif ofp.OFP_VERSION >= 3:
+        msg.table_id = ofp.OFPTT_ALL
+        msg.buffer_id = ofp.OFP_NO_BUFFER
+        msg.out_port = ofp.OFPP_ANY
+        msg.out_group = ofp.OFPG_ANY
     ctrl.message_send(msg)
     do_barrier(ctrl)
     return 0 # for backwards compatibility
@@ -357,16 +363,28 @@
     @returns (hwaddr, config, advert) The hwaddress, configuration and
     advertised values
     """
-    request = ofp.message.features_request()
-    reply, pkt = controller.transact(request)
-    logging.debug(reply.show())
-    if reply is None:
-        logging.warn("Get feature request failed")
-        return None, None, None
-    for idx in range(len(reply.ports)):
-        if reply.ports[idx].port_no == port_no:
-            return (reply.ports[idx].hw_addr, reply.ports[idx].config,
-                    reply.ports[idx].advertised)
+
+    if ofp.OFP_VERSION <= 3:
+        request = ofp.message.features_request()
+        reply, _ = controller.transact(request)
+        if reply is None:
+            logging.warn("Get feature request failed")
+            return None, None, None
+        logging.debug(reply.show())
+        ports = reply.ports
+    else:
+        request = ofp.message.port_desc_stats_request()
+        # TODO do multipart correctly
+        reply, _ = controller.transact(request)
+        if reply is None:
+            logging.warn("Port desc stats request failed")
+            return None, None, None
+        logging.debug(reply.show())
+        ports = reply.entries
+
+    for port in ports:
+        if port.port_no == port_no:
+            return (port.hw_addr, port.config, port.advertised)
     
     logging.warn("Did not find port number for port config")
     return None, None, None
@@ -379,24 +397,16 @@
     configuration value according to config and mask
     """
     logging.info("Setting port " + str(port_no) + " to config " + str(config))
-    request = ofp.message.features_request()
-    reply, pkt = controller.transact(request)
-    if reply is None:
-        return -1
-    logging.debug(reply.show())
-    p = None
-    for idx in range(len(reply.ports)):
-        if reply.ports[idx].port_no == port_no:
-            p = reply.ports[idx]
-            break
+
+    hw_addr, _, _ = port_config_get(controller, port_no)
+
     mod = ofp.message.port_mod()
     mod.port_no = port_no
-    if p:
-        mod.hw_addr = p.hw_addr
+    if hw_addr != None:
+        mod.hw_addr = hw_addr
     mod.config = config
     mod.mask = mask
-    if p:
-        mod.advertise = p.advertised
+    mod.advertise = 0 # No change
     controller.message_send(mod)
     return 0
 
@@ -545,7 +555,11 @@
 
 def packet_to_flow_match(parent, packet):
     match = oftest.parse.packet_to_flow_match(packet)
-    match.wildcards |= required_wildcards(parent)
+    if ofp.OFP_VERSION in [1, 2]:
+        match.wildcards |= required_wildcards(parent)
+    else:
+        # TODO remove incompatible OXM entries
+        pass
     return match
 
 def flow_msg_create(parent, pkt, ing_port=None, action_list=None, wildcards=None,
@@ -1144,25 +1158,51 @@
     """
     Retrieve a list of stats entries. Handles OFPSF_REPLY_MORE.
     """
+    if ofp.OFP_VERSION <= 3:
+        more_flag = ofp.OFPSF_REPLY_MORE
+    else:
+        more_flag = ofp.OFPMPF_REPLY_MORE
     stats = []
     reply, _ = test.controller.transact(req)
     test.assertTrue(reply is not None, "No response to stats request")
     stats.extend(reply.entries)
-    while reply.flags & ofp.OFPSF_REPLY_MORE != 0:
+    while reply.flags & more_flag != 0:
         reply, pkt = self.controller.poll(exp_msg=ofp.OFPT_STATS_REPLY)
         test.assertTrue(reply is not None, "No response to stats request")
         stats.extend(reply.entries)
     return stats
 
-def get_flow_stats(test, match, table_id=0xff, out_port=None):
+def get_flow_stats(test, match, table_id=None,
+                   out_port=None, out_group=None,
+                   cookie=0, cookie_mask=0):
     """
     Retrieve a list of flow stats entries.
     """
+
+    if table_id == None:
+        if ofp.OFP_VERSION <= 2:
+            table_id = 0xff
+        else:
+            table_id = ofp.OFPTT_ALL
+
     if out_port == None:
-        out_port = ofp.OFPP_NONE
+        if ofp.OFP_VERSION == 1:
+            out_port = ofp.OFPP_NONE
+        else:
+            out_port = ofp.OFPP_ANY
+
+    if out_group == None:
+        if ofp.OFP_VERSION > 1:
+            out_group = ofp.OFPP_ANY
+
     req = ofp.message.flow_stats_request(match=match,
-                                          table_id=table_id,
-                                          out_port=out_port)
+                                         table_id=table_id,
+                                         out_port=out_port)
+    if ofp.OFP_VERSION > 1:
+        req.out_group = out_group
+        req.cookie = cookie
+        req.cookie_mask = cookie_mask
+
     return get_stats(test, req)
 
 def get_port_stats(test, port_no):
@@ -1323,11 +1363,21 @@
     @param reason Expected packet_in reason, or None
     """
 
-    if in_port and in_port != msg.in_port:
+    if ofp.OFP_VERSION <= 2:
+        pkt_in_port = msg.in_port
+    else:
+        oxms = { type(oxm): oxm for oxm in msg.match.oxm_list }
+        if ofp.oxm.in_port in oxms:
+            pkt_in_port = oxms[ofp.oxm.in_port].value
+        else:
+            logging.warn("Missing in_port in packet-in message")
+            pkt_in_port = None
+
+    if in_port != None and in_port != pkt_in_port:
         logging.debug("Incorrect packet_in in_port (expected %d, received %d)", in_port, msg.in_port)
         return False
 
-    if reason and reason != msg.reason:
+    if reason != None and reason != msg.reason:
         logging.debug("Incorrect packet_in reason (expected %d, received %d)", reason, msg.reason)
         return False
 
diff --git a/tests-1.3/basic.py b/tests-1.3/basic.py
index f096940..c8e6758 100644
--- a/tests-1.3/basic.py
+++ b/tests-1.3/basic.py
@@ -1,9 +1,16 @@
+# Distributed under the OpenFlow Software License (see LICENSE)
+# Copyright (c) 2010 The Board of Trustees of The Leland Stanford Junior University
+# Copyright (c) 2012, 2013 Big Switch Networks, Inc.
+# Copyright (c) 2012, 2013 CPqD
+# Copyright (c) 2012, 2013 Ericsson
 """
 Basic test cases
 
 Test cases in other modules depend on this functionality.
 """
 
+import logging
+
 from oftest import config
 import oftest.base_tests as base_tests
 import ofp
@@ -25,3 +32,446 @@
         self.assertEqual(request.xid, response.xid,
                          'response xid != request xid')
         self.assertEqual(len(response.data), 0, 'response data non-empty')
+
+class EchoWithData(base_tests.SimpleProtocol):
+    """
+    Test echo response with short string data
+    """
+    def runTest(self):
+        data = 'OpenFlow Will Rule The World'
+        request = ofp.message.echo_request(data=data)
+        response, _ = self.controller.transact(request)
+        self.assertTrue(response is not None,
+                        "Did not get echo reply")
+        self.assertEqual(response.type, ofp.OFPT_ECHO_REPLY,
+                         'response is not echo_reply')
+        self.assertEqual(request.xid, response.xid,
+                         'response xid != request xid')
+        self.assertEqual(request.data, response.data,
+                         'response data != request data')
+
+class FeaturesRequest(base_tests.SimpleProtocol):
+    """
+    Test features_request to make sure we get a response
+
+    Does NOT test the contents; just that we get a response
+    """
+    def runTest(self):
+        request = ofp.message.features_request()
+        response,_ = self.controller.transact(request)
+        self.assertTrue(response is not None,
+                        'Did not get features reply')
+
+class OutputExact(base_tests.SimpleDataPlane):
+    """
+    Test output function for an exact-match flow
+
+    For each port A, adds a flow directing matching packets to that port.
+    Then, for all other ports B != A, verifies that sending a matching packet
+    to B results in an output to A.
+    """
+    def runTest(self):
+        ports = sorted(config["port_map"].keys())
+
+        delete_all_flows(self.controller)
+
+        parsed_pkt = simple_tcp_packet()
+        pkt = str(parsed_pkt)
+        match = packet_to_flow_match(self, parsed_pkt)
+
+        for out_port in ports:
+            request = ofp.message.flow_add(
+                    table_id=0,
+                    cookie=42,
+                    match=match,
+                    instructions=[
+                        ofp.instruction.apply_actions(
+                            actions=[
+                                ofp.action.output(
+                                    port=out_port,
+                                    max_len=ofp.OFPCML_NO_BUFFER)])],
+                    buffer_id=ofp.OFP_NO_BUFFER,
+                    priority=1000)
+
+            logging.info("Inserting flow sending matching packets to port %d", out_port)
+            self.controller.message_send(request)
+            do_barrier(self.controller)
+
+            for in_port in ports:
+                if in_port == out_port:
+                    continue
+                logging.info("OutputExact test, ports %d to %d", in_port, out_port)
+                self.dataplane.send(in_port, pkt)
+                receive_pkt_verify(self, [out_port], pkt, in_port)
+
+class PacketInExact(base_tests.SimpleDataPlane):
+    """
+    Test packet in function for an exact-match flow
+
+    Send a packet to each dataplane port and verify that a packet
+    in message is received from the controller for each
+    """
+    def runTest(self):
+        delete_all_flows(self.controller)
+
+        parsed_pkt = simple_tcp_packet()
+        pkt = str(parsed_pkt)
+        match = packet_to_flow_match(self, parsed_pkt)
+
+        request = ofp.message.flow_add(
+            table_id=0,
+            cookie=42,
+            match=match,
+            instructions=[
+                ofp.instruction.apply_actions(
+                    actions=[
+                        ofp.action.output(
+                            port=ofp.OFPP_CONTROLLER,
+                            max_len=ofp.OFPCML_NO_BUFFER)])],
+            buffer_id=ofp.OFP_NO_BUFFER,
+            priority=1000)
+
+        logging.info("Inserting flow sending matching packets to controller")
+        self.controller.message_send(request)
+        do_barrier(self.controller)
+
+        for of_port in config["port_map"].keys():
+            logging.info("PacketInExact test, port %d", of_port)
+            self.dataplane.send(of_port, pkt)
+            verify_packet_in(self, pkt, of_port, ofp.OFPR_ACTION)
+
+class PacketInMiss(base_tests.SimpleDataPlane):
+    """
+    Test packet in function for a table-miss flow
+
+    Send a packet to each dataplane port and verify that a packet
+    in message is received from the controller for each
+    """
+    def runTest(self):
+        delete_all_flows(self.controller)
+
+        parsed_pkt = simple_tcp_packet()
+        pkt = str(parsed_pkt)
+
+        request = ofp.message.flow_add(
+            table_id=0,
+            cookie=42,
+            instructions=[
+                ofp.instruction.apply_actions(
+                    actions=[
+                        ofp.action.output(
+                            port=ofp.OFPP_CONTROLLER,
+                            max_len=ofp.OFPCML_NO_BUFFER)])],
+            buffer_id=ofp.OFP_NO_BUFFER,
+            priority=0)
+
+        logging.info("Inserting table-miss flow sending all packets to controller")
+        self.controller.message_send(request)
+        do_barrier(self.controller)
+
+        for of_port in config["port_map"].keys():
+            logging.info("PacketInMiss test, port %d", of_port)
+            self.dataplane.send(of_port, pkt)
+            verify_packet_in(self, pkt, of_port, ofp.OFPR_NO_MATCH)
+
+class PacketOut(base_tests.SimpleDataPlane):
+    """
+    Test packet out function
+
+    Send packet out message to controller for each dataplane port and
+    verify the packet appears on the appropriate dataplane port
+    """
+    def runTest(self):
+        pkt = str(simple_tcp_packet())
+
+        for of_port in config["port_map"].keys():
+            msg = ofp.message.packet_out(
+                in_port=ofp.OFPP_CONTROLLER,
+                actions=[ofp.action.output(port=of_port)],
+                buffer_id=ofp.OFP_NO_BUFFER,
+                data=pkt)
+
+            logging.info("PacketOut test, port %d", of_port)
+            self.controller.message_send(msg)
+            receive_pkt_verify(self, [of_port], pkt, ofp.OFPP_CONTROLLER)
+
+class FlowRemoveAll(base_tests.SimpleProtocol):
+    """
+    Remove all flows; required for almost all tests
+
+    Add a bunch of flows, remove them, and then make sure there are no flows left
+    This is an intentionally naive test to see if the baseline functionality works
+    and should be a precondition to any more complicated deletion test (e.g.,
+    delete_strict vs. delete)
+    """
+    def runTest(self):
+        for i in range(1,5):
+            logging.debug("Adding flow %d", i)
+            request = ofp.message.flow_add(
+                buffer_id=ofp.OFP_NO_BUFFER,
+                priority=i*1000)
+            self.controller.message_send(request)
+        do_barrier(self.controller)
+
+        delete_all_flows(self.controller)
+
+        logging.info("Sending flow stats request")
+        stats = get_flow_stats(self, ofp.match())
+        self.assertEqual(len(stats), 0, "Expected empty flow stats reply")
+
+
+## Multipart messages
+
+class DescStats(base_tests.SimpleProtocol):
+    """
+    Switch description multipart transaction
+
+    Only verifies we get a single reply.
+    """
+    def runTest(self):
+        request = ofp.message.desc_stats_request()
+        logging.info("Sending desc stats request")
+        response, _ = self.controller.transact(request)
+        self.assertTrue(response != None, "No response to desc stats request")
+        logging.info(response.show())
+        self.assertEquals(response.flags, 0, "Unexpected bit set in desc stats reply flags")
+
+class FlowStats(base_tests.SimpleProtocol):
+    """
+    Flow stats multipart transaction
+
+    Only verifies we get a reply.
+    """
+    def runTest(self):
+        logging.info("Sending flow stats request")
+        stats = get_flow_stats(self, ofp.match())
+        logging.info("Received %d flow stats entries", len(stats))
+        for entry in stats:
+            logging.info(entry.show())
+
+class AggregateStats(base_tests.SimpleProtocol):
+    """
+    Aggregate flow stats multipart transaction
+
+    Only verifies we get a single reply.
+    """
+    def runTest(self):
+        request = ofp.message.aggregate_stats_request(
+            table_id=ofp.OFPTT_ALL,
+            out_port=ofp.OFPP_ANY,
+            out_group=ofp.OFPG_ANY,
+            cookie=0,
+            cookie_mask=0)
+        logging.info("Sending aggregate flow stats request")
+        response, _ = self.controller.transact(request)
+        self.assertTrue(response != None, "No response to aggregate stats request")
+        logging.info(response.show())
+        self.assertEquals(response.flags, 0, "Unexpected bit set in aggregate stats reply flags")
+
+class TableStats(base_tests.SimpleProtocol):
+    """
+    Table stats multipart transaction
+
+    Only verifies we get a reply.
+    """
+    def runTest(self):
+        logging.info("Sending table stats request")
+        stats = get_stats(self, ofp.message.table_stats_request())
+        logging.info("Received %d table stats entries", len(stats))
+        for entry in stats:
+            logging.info(entry.show())
+
+class PortStats(base_tests.SimpleProtocol):
+    """
+    Port stats multipart transaction
+
+    Only verifies we get a reply.
+    """
+    def runTest(self):
+        request = ofp.message.port_stats_request(port_no=ofp.OFPP_ANY)
+        logging.info("Sending port stats request")
+        stats = get_stats(self, request)
+        logging.info("Received %d port stats entries", len(stats))
+        for entry in stats:
+            logging.info(entry.show())
+
+class QueueStats(base_tests.SimpleProtocol):
+    """
+    Queue stats multipart transaction
+
+    Only verifies we get a reply.
+    """
+    def runTest(self):
+        request = ofp.message.queue_stats_request(port_no=ofp.OFPP_ANY,
+                                                  queue_id=ofp.OFPQ_ALL)
+        logging.info("Sending queue stats request")
+        stats = get_stats(self, request)
+        logging.info("Received %d queue stats entries", len(stats))
+        for entry in stats:
+            logging.info(entry.show())
+
+class GroupStats(base_tests.SimpleProtocol):
+    """
+    Group stats multipart transaction
+
+    Only verifies we get a reply.
+    """
+    def runTest(self):
+        request = ofp.message.group_stats_request(group_id=ofp.OFPG_ALL)
+        logging.info("Sending group stats request")
+        stats = get_stats(self, request)
+        logging.info("Received %d group stats entries", len(stats))
+        for entry in stats:
+            logging.info(entry.show())
+
+class GroupDescStats(base_tests.SimpleProtocol):
+    """
+    Group description multipart transaction
+
+    Only verifies we get a reply.
+    """
+    def runTest(self):
+        request = ofp.message.group_desc_stats_request()
+        logging.info("Sending group desc stats request")
+        stats = get_stats(self, request)
+        logging.info("Received %d group desc stats entries", len(stats))
+        for entry in stats:
+            logging.info(entry.show())
+
+class GroupFeaturesStats(base_tests.SimpleProtocol):
+    """
+    Group features multipart transaction
+
+    Only verifies we get a single reply.
+    """
+    def runTest(self):
+        request = ofp.message.group_features_stats_request()
+        logging.info("Sending group features stats request")
+        response, _ = self.controller.transact(request)
+        self.assertTrue(response != None, "No response to group features stats request")
+        logging.info(response.show())
+        self.assertEquals(response.flags, 0, "Unexpected bit set in group features stats reply flags")
+
+class MeterStats(base_tests.SimpleProtocol):
+    """
+    Meter stats multipart transaction
+
+    Only verifies we get a reply.
+    """
+    def runTest(self):
+        request = ofp.message.meter_stats_request(meter_id=ofp.OFPM_ALL)
+        logging.info("Sending meter stats request")
+        stats = get_stats(self, request)
+        logging.info("Received %d meter stats entries", len(stats))
+        for entry in stats:
+            logging.info(entry.show())
+
+class MeterConfigStats(base_tests.SimpleProtocol):
+    """
+    Meter config multipart transaction
+
+    Only verifies we get a reply.
+    """
+    def runTest(self):
+        request = ofp.message.meter_config_stats_request(meter_id=ofp.OFPM_ALL)
+        logging.info("Sending meter config stats request")
+        stats = get_stats(self, request)
+        logging.info("Received %d meter config stats entries", len(stats))
+        for entry in stats:
+            logging.info(entry.show())
+
+class MeterFeaturesStats(base_tests.SimpleProtocol):
+    """
+    Meter features multipart transaction
+
+    Only verifies we get a single reply.
+    """
+    def runTest(self):
+        request = ofp.message.meter_features_stats_request()
+        logging.info("Sending meter features stats request")
+        response, _ = self.controller.transact(request)
+        self.assertTrue(response != None, "No response to meter features stats request")
+        logging.info(response.show())
+        self.assertEquals(response.flags, 0, "Unexpected bit set in meter features stats reply flags")
+
+@disabled # pyloxi does not yet support table features
+class TableFeaturesStats(base_tests.SimpleProtocol):
+    """
+    Table features multipart transaction
+
+    Only verifies we get a reply.
+    """
+    def runTest(self):
+        logging.info("Sending table features stats request")
+        stats = get_stats(self, ofp.message.table_features_stats_request())
+        logging.info("Received %d table features stats entries", len(stats))
+        for entry in stats:
+            logging.info(entry.show())
+
+class PortDescStats(base_tests.SimpleProtocol):
+    """
+    Port description multipart transaction
+
+    Only verifies we get a reply.
+    """
+    def runTest(self):
+        logging.info("Sending port desc stats request")
+        stats = get_stats(self, ofp.message.port_desc_stats_request())
+        logging.info("Received %d port desc stats entries", len(stats))
+        for entry in stats:
+            logging.info(entry.show())
+
+class PortConfigMod(base_tests.SimpleProtocol):
+    """
+    Modify a bit in port config and verify changed
+
+    Get the switch configuration, modify the port configuration
+    and write it back; get the config again and verify changed.
+    Then set it back to the way it was.
+    """
+
+    def runTest(self):
+        logging.info("Running " + str(self))
+        for of_port, _ in config["port_map"].items(): # Grab first port
+            break
+
+        (_, config1, _) = \
+            port_config_get(self.controller, of_port)
+        self.assertTrue(config is not None, "Did not get port config")
+
+        logging.debug("OFPPC_NO_PACKET_IN bit port " + str(of_port) + " is now " +
+                      str(config1 & ofp.OFPPC_NO_PACKET_IN))
+
+        rv = port_config_set(self.controller, of_port,
+                             config1 ^ ofp.OFPPC_NO_PACKET_IN,
+                             ofp.OFPPC_NO_PACKET_IN)
+        self.assertTrue(rv != -1, "Error sending port mod")
+
+        # Verify change took place with same feature request
+        (_, config2, _) = port_config_get(self.controller, of_port)
+        self.assertTrue(config2 is not None, "Did not get port config2")
+        logging.debug("OFPPC_NO_PACKET_IN bit port " + str(of_port) + " is now " +
+                      str(config2 & ofp.OFPPC_NO_PACKET_IN))
+        self.assertTrue(config2 & ofp.OFPPC_NO_PACKET_IN !=
+                        config1 & ofp.OFPPC_NO_PACKET_IN,
+                        "Bit change did not take")
+        # Set it back
+        rv = port_config_set(self.controller, of_port, config1,
+                             ofp.OFPPC_NO_PACKET_IN)
+        self.assertTrue(rv != -1, "Error sending port mod")
+
+class AsyncConfigGet(base_tests.SimpleProtocol):
+    """
+    Verify initial async config
+
+    Other tests rely on connections starting with these values.
+    """
+
+    def runTest(self):
+        logging.info("Sending get async config request")
+        response, _ = self.controller.transact(ofp.message.async_get_request())
+        self.assertTrue(response != None, "No response to get async config request")
+        logging.info(response.show())
+        self.assertEquals(response.packet_in_mask_equal_master & 0x07, 0x07)
+        self.assertEquals(response.port_status_mask_equal_master & 0x07, 0x07)
+        self.assertEquals(response.flow_removed_mask_equal_master & 0x0f, 0x0f)