Merge into master from pull request #75:
group table management tests (https://github.com/floodlight/oftest/pull/75)
diff --git a/src/python/oftest/testutils.py b/src/python/oftest/testutils.py
index 8b27f25..8a5a563 100644
--- a/src/python/oftest/testutils.py
+++ b/src/python/oftest/testutils.py
@@ -46,6 +46,18 @@
     do_barrier(ctrl)
     return 0 # for backwards compatibility
 
+def delete_all_groups(ctrl):
+    """
+    Delete all groups on the switch
+    @param ctrl The controller object for the test
+    """
+
+    logging.info("Deleting all groups")
+    msg = ofp.message.group_mod(
+        command=ofp.OFPGC_DELETE, group_id=ofp.OFPG_ALL)
+    ctrl.message_send(msg)
+    do_barrier(ctrl)
+
 def required_wildcards(parent):
     w = test_param_get('required_wildcards', default='default')
     if w == 'l3-l4':
@@ -732,10 +744,14 @@
         request.flags |= ofp.OFPFF_SEND_FLOW_REM
         request.hard_timeout = 1
 
+    if ofp.OFP_VERSION == 1:
+        actions = request.actions
+    else:
+        actions = []
+        request.instructions.append(ofp.instruction.apply_actions(actions))
+
     if action_list is not None:
-        for act in action_list:
-            logging.debug("Adding action " + act.show())
-            request.actions.append(act)
+        actions.extend(action_list)
 
     # Set up output/enqueue action if directed
     if egr_queue is not None:
@@ -744,12 +760,12 @@
         for egr_port in egr_port_list:
             act.port = egr_port
             act.queue_id = egr_queue
-            request.actions.append(act)
+            actions.append(act)
     elif egr_ports is not None:
         for egr_port in egr_port_list:
             act = ofp.action.output()
             act.port = egr_port
-            request.actions.append(act)
+            actions.append(act)
 
     logging.debug(request.show())
 
@@ -1298,15 +1314,18 @@
     Retrieve a list of stats entries. Handles OFPSF_REPLY_MORE.
     """
     if ofp.OFP_VERSION <= 3:
+        msgtype = ofp.OFPT_STATS_REPLY
         more_flag = ofp.OFPSF_REPLY_MORE
     else:
+        msgtype = ofp.OFPT_MULTIPART_REPLY
         more_flag = ofp.OFPMPF_REPLY_MORE
     stats = []
     reply, _ = test.controller.transact(req)
     test.assertTrue(reply is not None, "No response to stats request")
+    test.assertEquals(reply.type, msgtype, "Response had unexpected message type")
     stats.extend(reply.entries)
     while reply.flags & more_flag != 0:
-        reply, pkt = test.controller.poll(exp_msg=ofp.OFPT_STATS_REPLY)
+        reply, pkt = test.controller.poll(exp_msg=msgtype)
         test.assertTrue(reply is not None, "No response to stats request")
         stats.extend(reply.entries)
     return stats
@@ -1662,5 +1681,9 @@
             verify_no_packet(test, pkt, ofport)
     verify_no_other_packets(test)
 
+def verify_no_errors(ctrl):
+    error, _ = ctrl.poll(ofp.OFPT_ERROR, 0)
+    if error:
+        raise AssertionError("unexpected error type=%d code=%d" % (error.err_type, error.code))
 
 __all__ = list(set(locals()) - _import_blacklist)
diff --git a/tests-1.3/groups.py b/tests-1.3/groups.py
new file mode 100644
index 0000000..64bd656
--- /dev/null
+++ b/tests-1.3/groups.py
@@ -0,0 +1,782 @@
+# 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
+"""
+Group table test cases.
+"""
+
+import logging
+
+from oftest import config
+import oftest
+import oftest.base_tests as base_tests
+import ofp
+
+from oftest.testutils import *
+
+class GroupTest(base_tests.SimpleDataPlane):
+    def setUp(self):
+        base_tests.SimpleDataPlane.setUp(self)
+        delete_all_flows(self.controller)
+        delete_all_groups(self.controller)
+
+
+class GroupAdd(GroupTest):
+    """
+    A regular group should be added successfully
+    """
+
+    def runTest(self):
+        port1, = openflow_ports(1)
+
+        msg = ofp.message.group_mod(
+            command=ofp.OFPGC_ADD,
+            group_type=ofp.OFPGT_ALL,
+            group_id=0,
+            buckets=[
+                ofp.bucket(actions=[ofp.action.output(port1)])])
+
+        self.controller.message_send(msg)
+        do_barrier(self.controller)
+
+        stats = get_stats(self, ofp.message.group_desc_stats_request())
+        self.assertEquals(stats, [
+            ofp.group_desc_stats_entry(
+                type=msg.group_type,
+                group_id=msg.group_id,
+                buckets=msg.buckets)])
+
+
+class GroupAddMaxID(GroupTest):
+    """
+    A group with ID OFPG_MAX should be added successfully
+    """
+
+    def runTest(self):
+        port1, = openflow_ports(1)
+
+        msg = ofp.message.group_mod(
+            command=ofp.OFPGC_ADD,
+            group_type=ofp.OFPGT_ALL,
+            group_id=ofp.OFPG_MAX,
+            buckets=[
+                ofp.bucket(actions=[ofp.action.output(port1)])])
+
+        self.controller.message_send(msg)
+        do_barrier(self.controller)
+
+        stats = get_stats(self, ofp.message.group_desc_stats_request())
+        self.assertEquals(stats, [
+            ofp.group_desc_stats_entry(
+                type=msg.group_type,
+                group_id=msg.group_id,
+                buckets=msg.buckets)])
+
+
+class GroupAddInvalidAction(GroupTest):
+    """
+    If any action in the buckets is invalid, OFPET_BAD_ACTION/<code> should be returned
+    """
+
+    def runTest(self):
+        msg = ofp.message.group_mod(
+            command=ofp.OFPGC_ADD,
+            group_type=ofp.OFPGT_ALL,
+            group_id=0,
+            buckets=[
+                ofp.bucket(actions=[ofp.action.output(ofp.OFPP_ANY)])])
+
+        response, _ = self.controller.transact(msg)
+        self.assertIsInstance(response, ofp.message.error_msg)
+        self.assertEquals(response.err_type, ofp.OFPET_BAD_ACTION)
+        self.assertEquals(response.code, ofp.OFPBAC_BAD_OUT_PORT)
+
+
+class GroupAddExisting(GroupTest):
+    """
+    An addition with existing group id should result in OFPET_GROUP_MOD_FAILED/OFPGMFC_GROUP_EXISTS
+    """
+
+    def runTest(self):
+        port1, port2, = openflow_ports(2)
+
+        msg = ofp.message.group_mod(
+            command=ofp.OFPGC_ADD,
+            group_type=ofp.OFPGT_ALL,
+            group_id=0,
+            buckets=[
+                ofp.bucket(actions=[ofp.action.output(port1)])])
+
+        self.controller.message_send(msg)
+        do_barrier(self.controller)
+
+        msg = ofp.message.group_mod(
+            command=ofp.OFPGC_ADD,
+            group_type=ofp.OFPGT_ALL,
+            group_id=0,
+            buckets=[
+                ofp.bucket(actions=[ofp.action.output(port2)])])
+
+        response, _ = self.controller.transact(msg)
+        self.assertIsInstance(response, ofp.message.error_msg)
+        self.assertEquals(response.err_type, ofp.OFPET_GROUP_MOD_FAILED)
+        self.assertEquals(response.code, ofp.OFPGMFC_GROUP_EXISTS)
+
+
+class GroupAddInvalidID(GroupTest):
+    """
+    An addition with invalid group id (reserved) should result in OFPET_GROUP_MOD_FAILED/OFPGMFC_INVALID_GROUP
+    """
+
+    def runTest(self):
+        port1, = openflow_ports(1)
+
+        msg = ofp.message.group_mod(
+            command=ofp.OFPGC_ADD,
+            group_type=ofp.OFPGT_ALL,
+            group_id=ofp.OFPG_ALL,
+            buckets=[
+                ofp.bucket(actions=[ofp.action.output(port1)])])
+
+        response, _ = self.controller.transact(msg)
+        self.assertIsInstance(response, ofp.message.error_msg)
+        self.assertEquals(response.err_type, ofp.OFPET_GROUP_MOD_FAILED)
+        self.assertEquals(response.code, ofp.OFPGMFC_INVALID_GROUP)
+
+
+class GroupAddMinimumInvalidID(GroupTest):
+    """
+    An addition with invalid group id (reserved) should result in OFPET_GROUP_MOD_FAILED/OFPGMFC_INVALID_GROUP
+    """
+
+    def runTest(self):
+        port1, = openflow_ports(1)
+
+        msg = ofp.message.group_mod(
+            command=ofp.OFPGC_ADD,
+            group_type=ofp.OFPGT_ALL,
+            group_id=ofp.OFPG_MAX+1,
+            buckets=[
+                ofp.bucket(actions=[ofp.action.output(port1)])])
+
+        response, _ = self.controller.transact(msg)
+        self.assertIsInstance(response, ofp.message.error_msg)
+        self.assertEquals(response.err_type, ofp.OFPET_GROUP_MOD_FAILED)
+        self.assertEquals(response.code, ofp.OFPGMFC_INVALID_GROUP)
+
+
+class GroupModify(GroupTest):
+    """
+    A regular group modification should be successful
+    """
+
+    def runTest(self):
+        port1, port2, = openflow_ports(2)
+
+        msg = ofp.message.group_mod(
+            command=ofp.OFPGC_ADD,
+            group_type=ofp.OFPGT_ALL,
+            group_id=0,
+            buckets=[
+                ofp.bucket(actions=[ofp.action.output(port1)])])
+
+        self.controller.message_send(msg)
+        do_barrier(self.controller)
+
+        msg = ofp.message.group_mod(
+            command=ofp.OFPGC_MODIFY,
+            group_type=ofp.OFPGT_ALL,
+            group_id=0,
+            buckets=[
+                ofp.bucket(actions=[ofp.action.output(port2)])])
+
+        self.controller.message_send(msg)
+        do_barrier(self.controller)
+
+        stats = get_stats(self, ofp.message.group_desc_stats_request())
+        self.assertEquals(stats, [
+            ofp.group_desc_stats_entry(
+                type=msg.group_type,
+                group_id=msg.group_id,
+                buckets=msg.buckets)])
+
+
+class GroupModifyNonexisting(GroupTest):
+    """
+    A modification for a non-existing group should result in OFPET_GROUP_MOD_FAILED/OFPGMFC_UNKNOWN_GROUP
+    """
+
+    def runTest(self):
+        port1, = openflow_ports(1)
+
+        msg = ofp.message.group_mod(
+            command=ofp.OFPGC_MODIFY,
+            group_type=ofp.OFPGT_ALL,
+            group_id=0,
+            buckets=[
+                ofp.bucket(actions=[ofp.action.output(port1)])])
+
+        response, _ = self.controller.transact(msg)
+        self.assertIsInstance(response, ofp.message.error_msg)
+        self.assertEquals(response.err_type, ofp.OFPET_GROUP_MOD_FAILED)
+        self.assertEquals(response.code, ofp.OFPGMFC_UNKNOWN_GROUP)
+
+
+class GroupModifyLoop(GroupTest):
+    """
+    A modification causing loop should result in OFPET_GROUP_MOD_FAILED/OFPGMFC_LOOP
+    """
+
+    def runTest(self):
+        port1, = openflow_ports(1)
+
+        msg = ofp.message.group_mod(
+            command=ofp.OFPGC_ADD,
+            group_type=ofp.OFPGT_ALL,
+            group_id=0,
+            buckets=[
+                ofp.bucket(actions=[ofp.action.output(port1)])])
+
+        self.controller.message_send(msg)
+        do_barrier(self.controller)
+
+        msg = ofp.message.group_mod(
+            command=ofp.OFPGC_ADD,
+            group_type=ofp.OFPGT_ALL,
+            group_id=1,
+            buckets=[
+                ofp.bucket(actions=[ofp.action.group(0)])])
+
+        self.controller.message_send(msg)
+        do_barrier(self.controller)
+
+        msg = ofp.message.group_mod(
+            command=ofp.OFPGC_ADD,
+            group_type=ofp.OFPGT_ALL,
+            group_id=2,
+            buckets=[
+                ofp.bucket(actions=[ofp.action.group(1)])])
+
+        self.controller.message_send(msg)
+        do_barrier(self.controller)
+
+        msg = ofp.message.group_mod(
+            command=ofp.OFPGC_MODIFY,
+            group_type=ofp.OFPGT_ALL,
+            group_id=0,
+            buckets=[
+                ofp.bucket(actions=[ofp.action.group(2)])])
+
+        response, _ = self.controller.transact(msg)
+        self.assertIsInstance(response, ofp.message.error_msg)
+        self.assertEquals(response.err_type, ofp.OFPET_GROUP_MOD_FAILED)
+        self.assertEquals(response.code, ofp.OFPGMFC_LOOP)
+
+
+class GroupModifyInvalidID(GroupTest):
+    """
+    A modification for a reserved group should result in OFPET_GROUP_MOD_FAILED/OFPGMFC_INVALID_GROUP
+    """
+
+    def runTest(self):
+        port1, = openflow_ports(1)
+
+        msg = ofp.message.group_mod(
+            command=ofp.OFPGC_ADD,
+            group_type=ofp.OFPGT_ALL,
+            group_id=ofp.OFPG_ALL,
+            buckets=[
+                ofp.bucket(actions=[ofp.action.output(port1)])])
+
+        response, _ = self.controller.transact(msg)
+        self.assertIsInstance(response, ofp.message.error_msg)
+        self.assertEquals(response.err_type, ofp.OFPET_GROUP_MOD_FAILED)
+        self.assertEquals(response.code, ofp.OFPGMFC_INVALID_GROUP)
+
+
+class GroupModifyEmpty(GroupTest):
+    """
+    A modification for an existing group with no buckets should be accepted
+    """
+
+    def runTest(self):
+        port1, = openflow_ports(1)
+
+        msg = ofp.message.group_mod(
+            command=ofp.OFPGC_ADD,
+            group_type=ofp.OFPGT_ALL,
+            group_id=0,
+            buckets=[
+                ofp.bucket(actions=[ofp.action.output(port1)])])
+
+        self.controller.message_send(msg)
+        do_barrier(self.controller)
+
+        msg = ofp.message.group_mod(
+            command=ofp.OFPGC_MODIFY,
+            group_type=ofp.OFPGT_ALL,
+            group_id=0,
+            buckets=[])
+
+        self.controller.message_send(msg)
+        do_barrier(self.controller)
+
+        stats = get_stats(self, ofp.message.group_desc_stats_request())
+        self.assertEquals(stats, [
+            ofp.group_desc_stats_entry(
+                type=msg.group_type,
+                group_id=msg.group_id,
+                buckets=msg.buckets)])
+
+
+class GroupDeleteExisting(GroupTest):
+    """
+    A deletion for an existing group should remove the group
+    """
+
+    def runTest(self):
+        port1, = openflow_ports(1)
+
+        msg = ofp.message.group_mod(
+            command=ofp.OFPGC_ADD,
+            group_type=ofp.OFPGT_ALL,
+            group_id=0,
+            buckets=[
+                ofp.bucket(actions=[ofp.action.output(port1)])])
+
+        self.controller.message_send(msg)
+        do_barrier(self.controller)
+
+        msg = ofp.message.group_mod(
+            command=ofp.OFPGC_DELETE,
+            group_id=0)
+
+        self.controller.message_send(msg)
+        do_barrier(self.controller)
+
+        stats = get_stats(self, ofp.message.group_desc_stats_request())
+        self.assertEquals(stats, [])
+
+
+class GroupDeleteNonexisting(GroupTest):
+    """
+    A deletion for nonexisting group should result in no error
+    """
+
+    def runTest(self):
+        msg = ofp.message.group_mod(
+            command=ofp.OFPGC_DELETE,
+            group_id=0)
+
+        self.controller.message_send(msg)
+        do_barrier(self.controller)
+        verify_no_errors(self.controller)
+
+
+class GroupDeleteAll(GroupTest):
+    """
+    A deletion for OFPG_ALL should remove all groups
+    """
+
+    def runTest(self):
+        port1, = openflow_ports(1)
+
+        msg = ofp.message.group_mod(
+            command=ofp.OFPGC_ADD,
+            group_type=ofp.OFPGT_ALL,
+            group_id=0,
+            buckets=[
+                ofp.bucket(actions=[ofp.action.output(port1)])])
+
+        self.controller.message_send(msg)
+        do_barrier(self.controller)
+
+        msg = ofp.message.group_mod(
+            command=ofp.OFPGC_ADD,
+            group_type=ofp.OFPGT_ALL,
+            group_id=1,
+            buckets=[
+                ofp.bucket(actions=[ofp.action.output(port1)])])
+
+        self.controller.message_send(msg)
+        do_barrier(self.controller)
+
+        msg = ofp.message.group_mod(
+            command=ofp.OFPGC_DELETE,
+            group_id=ofp.OFPG_ALL)
+
+        self.controller.message_send(msg)
+        do_barrier(self.controller)
+
+        stats = get_stats(self, ofp.message.group_desc_stats_request())
+        self.assertEquals(stats, [])
+
+
+class GroupAddAllWeight(GroupTest):
+    """
+    An ALL group with weights for buckets should result in OFPET_GROUP_MOD_FAILED, OFPGMFC_INVALID_GROUP
+    """
+
+    def runTest(self):
+        port1, port2, = openflow_ports(2)
+
+        msg = ofp.message.group_mod(
+            command=ofp.OFPGC_ADD,
+            group_type=ofp.OFPGT_ALL,
+            group_id=0,
+            buckets=[
+                ofp.bucket(weight=1, actions=[ofp.action.output(port1)]),
+                ofp.bucket(weight=2, actions=[ofp.action.output(port2)])])
+
+        response, _ = self.controller.transact(msg)
+        self.assertIsInstance(response, ofp.message.error_msg)
+        self.assertEquals(response.err_type, ofp.OFPET_GROUP_MOD_FAILED)
+        self.assertEquals(response.code, ofp.OFPGMFC_INVALID_GROUP)
+
+
+class GroupAddIndirectWeight(GroupTest):
+    """
+    An INDIRECT group with weights for buckets should result in OFPET_GROUP_MOD_FAILED, OFPGMFC_INVALID_GROUP
+    """
+
+    def runTest(self):
+        port1, = openflow_ports(1)
+
+        msg = ofp.message.group_mod(
+            command=ofp.OFPGC_ADD,
+            group_type=ofp.OFPGT_INDIRECT,
+            group_id=0,
+            buckets=[
+                ofp.bucket(weight=1, actions=[ofp.action.output(port1)])])
+
+        response, _ = self.controller.transact(msg)
+        self.assertIsInstance(response, ofp.message.error_msg)
+        self.assertEquals(response.err_type, ofp.OFPET_GROUP_MOD_FAILED)
+        self.assertEquals(response.code, ofp.OFPGMFC_INVALID_GROUP)
+
+
+class GroupAddIndirectBuckets(GroupTest):
+    """
+    An INDIRECT group with <>1 bucket should result in OFPET_GROUP_MOD_FAILED, OFPGMFC_INVALID_GROUP
+    """
+
+    def runTest(self):
+        port1, port2, = openflow_ports(2)
+
+        msg = ofp.message.group_mod(
+            command=ofp.OFPGC_ADD,
+            group_type=ofp.OFPGT_INDIRECT,
+            group_id=0,
+            buckets=[
+                ofp.bucket(actions=[ofp.action.output(port1)]),
+                ofp.bucket(actions=[ofp.action.output(port2)])])
+
+        response, _ = self.controller.transact(msg)
+        self.assertIsInstance(response, ofp.message.error_msg)
+        self.assertEquals(response.err_type, ofp.OFPET_GROUP_MOD_FAILED)
+        self.assertEquals(response.code, ofp.OFPGMFC_INVALID_GROUP)
+
+
+class GroupAddSelectNoWeight(GroupTest):
+    """
+    A SELECT group with ==0 weights should result in OFPET_GROUP_MOD_FAILED, OFPGMFC_INVALID_GROUP
+    """
+
+    def runTest(self):
+        port1, port2, = openflow_ports(2)
+
+        msg = ofp.message.group_mod(
+            command=ofp.OFPGC_ADD,
+            group_type=ofp.OFPGT_SELECT,
+            group_id=0,
+            buckets=[
+                ofp.bucket(actions=[ofp.action.output(port1)]),
+                ofp.bucket(actions=[ofp.action.output(port2)])])
+
+        response, _ = self.controller.transact(msg)
+        self.assertIsInstance(response, ofp.message.error_msg)
+        self.assertEquals(response.err_type, ofp.OFPET_GROUP_MOD_FAILED)
+        self.assertEquals(response.code, ofp.OFPGMFC_INVALID_GROUP)
+
+
+class GroupStats(GroupTest):
+    """
+    A group stats request should return an entry for the specified group
+    """
+
+    def runTest(self):
+        port1, port2, = openflow_ports(2)
+
+        msg = ofp.message.group_mod(
+            command=ofp.OFPGC_ADD,
+            group_type=ofp.OFPGT_ALL,
+            group_id=10,
+            buckets=[
+                ofp.bucket(actions=[
+                    ofp.action.set_field(ofp.oxm.tcp_src(2000)),
+                    ofp.action.output(port1)]),
+                ofp.bucket(actions=[
+                    ofp.action.set_field(ofp.oxm.tcp_src(3000)),
+                    ofp.action.output(port2)])])
+
+        self.controller.message_send(msg)
+        do_barrier(self.controller)
+
+        request = ofp.message.group_stats_request(group_id=10)
+        stats = get_stats(self, request)
+
+        self.assertEquals(len(stats), 1)
+        self.assertEquals(stats[0].group_id, 10)
+        self.assertEquals(stats[0].ref_count, 0)
+        self.assertEquals(stats[0].packet_count, 0)
+        self.assertEquals(stats[0].byte_count, 0)
+        self.assertEquals(len(stats[0].bucket_stats), 2)
+
+
+class GroupStatsNonexistent(GroupTest):
+    """
+    A group stats request for a nonexistent group should return an empty list
+    """
+
+    def runTest(self):
+        request = ofp.message.group_stats_request(group_id=10)
+        stats = get_stats(self, request)
+        self.assertEquals(len(stats), 0)
+
+
+class GroupStatsAll(GroupTest):
+    """
+    A group stats request with OFPG_ALL should return an entry for each group
+    """
+
+    def runTest(self):
+        port1, port2, = openflow_ports(2)
+
+        msg0 = ofp.message.group_mod(
+            command=ofp.OFPGC_ADD,
+            group_type=ofp.OFPGT_ALL,
+            group_id=0,
+            buckets=[
+                ofp.bucket(actions=[
+                    ofp.action.set_field(ofp.oxm.tcp_src(2000)),
+                    ofp.action.output(port1)]),
+                ofp.bucket(actions=[
+                    ofp.action.set_field(ofp.oxm.tcp_src(3000)),
+                    ofp.action.output(port2)])])
+
+        self.controller.message_send(msg0)
+        do_barrier(self.controller)
+
+        msg1 = ofp.message.group_mod(
+            command=ofp.OFPGC_ADD,
+            group_type=ofp.OFPGT_ALL,
+            group_id=1,
+            buckets=[
+                ofp.bucket(actions=[
+                    ofp.action.set_field(ofp.oxm.tcp_src(2001)),
+                    ofp.action.output(port1)]),
+                ofp.bucket(actions=[
+                    ofp.action.set_field(ofp.oxm.tcp_src(3001)),
+                    ofp.action.output(port2)])])
+
+        self.controller.message_send(msg1)
+        do_barrier(self.controller)
+
+        request = ofp.message.group_stats_request(group_id=ofp.OFPG_ALL)
+        stats = sorted(get_stats(self, request), key=lambda x: x.group_id)
+
+        self.assertEquals(len(stats), 2)
+
+        self.assertEquals(stats[0].group_id, 0)
+        self.assertEquals(stats[0].ref_count, 0)
+        self.assertEquals(stats[0].packet_count, 0)
+        self.assertEquals(stats[0].byte_count, 0)
+        self.assertEquals(len(stats[0].bucket_stats), 2)
+
+        self.assertEquals(stats[1].group_id, 1)
+        self.assertEquals(stats[1].ref_count, 0)
+        self.assertEquals(stats[1].packet_count, 0)
+        self.assertEquals(stats[1].byte_count, 0)
+        self.assertEquals(len(stats[1].bucket_stats), 2)
+
+
+class GroupDescStats(GroupTest):
+    """
+    A group desc stats request should return the type, id, and buckets for each group
+    """
+
+    def runTest(self):
+        port1, port2, port3, = openflow_ports(3)
+
+        msg0 = ofp.message.group_mod(
+            command=ofp.OFPGC_ADD,
+            group_type=ofp.OFPGT_ALL,
+            group_id=0,
+            buckets=[
+                ofp.bucket(actions=[
+                    ofp.action.set_field(ofp.oxm.tcp_src(2000)),
+                    ofp.action.output(port1)]),
+                ofp.bucket(actions=[
+                    ofp.action.set_field(ofp.oxm.tcp_src(3000)),
+                    ofp.action.output(port2)]),
+                ofp.bucket(actions=[
+                    ofp.action.set_field(ofp.oxm.tcp_src(4000)),
+                    ofp.action.output(port3)])])
+
+        self.controller.message_send(msg0)
+        do_barrier(self.controller)
+
+        msg1 = ofp.message.group_mod(
+            command=ofp.OFPGC_ADD,
+            group_type=ofp.OFPGT_SELECT,
+            group_id=1,
+            buckets=[
+                ofp.bucket(
+                    weight=1,
+                    actions=[
+                        ofp.action.set_field(ofp.oxm.tcp_src(2001)),
+                        ofp.action.output(port1)]),
+                ofp.bucket(
+                    weight=2,
+                    actions=[
+                        ofp.action.set_field(ofp.oxm.tcp_src(3001)),
+                        ofp.action.output(port2)]),
+                ofp.bucket(
+                    weight=3,
+                    actions=[
+                        ofp.action.set_field(ofp.oxm.tcp_src(4001)),
+                        ofp.action.output(port3)])])
+
+        self.controller.message_send(msg1)
+        do_barrier(self.controller)
+
+        msg2 = ofp.message.group_mod(
+            command=ofp.OFPGC_ADD,
+            group_type=ofp.OFPGT_FF,
+            group_id=2,
+            buckets=[
+                ofp.bucket(
+                    watch_port=port1,
+                    actions=[
+                        ofp.action.set_field(ofp.oxm.tcp_src(2002)),
+                        ofp.action.output(port1)]),
+                ofp.bucket(
+                    watch_port=port2,
+                    actions=[
+                        ofp.action.set_field(ofp.oxm.tcp_src(3002)),
+                        ofp.action.output(port2,)]),
+                ofp.bucket(
+                    watch_port=port3,
+                    actions=[
+                        ofp.action.set_field(ofp.oxm.tcp_src(4002)),
+                        ofp.action.output(port3,)])])
+
+        self.controller.message_send(msg2)
+        do_barrier(self.controller)
+
+        request = ofp.message.group_desc_stats_request()
+        stats = sorted(get_stats(self, request), key=lambda x: x.group_id)
+
+        self.assertEquals(len(stats), 3)
+
+        self.assertEquals(stats[0].group_id, msg0.group_id)
+        self.assertEquals(stats[0].type, msg0.group_type)
+        self.assertEquals(stats[0].buckets, msg0.buckets)
+
+        self.assertEquals(stats[1].group_id, msg1.group_id)
+        self.assertEquals(stats[1].type, msg1.group_type)
+        self.assertEquals(stats[1].buckets, msg1.buckets)
+
+        self.assertEquals(stats[2].group_id, msg2.group_id)
+        self.assertEquals(stats[2].type, msg2.group_type)
+        self.assertEquals(stats[2].buckets, msg2.buckets)
+
+
+class GroupFlowSelect(GroupTest):
+    """
+    A flow stats request qualified on group id should select the correct flows
+    """
+
+    def runTest(self):
+        port1, = openflow_ports(1)
+
+        msg = ofp.message.group_mod(
+            command=ofp.OFPGC_ADD,
+            group_type=ofp.OFPGT_ALL,
+            group_id=1,
+            buckets=[])
+
+        self.controller.message_send(msg)
+        do_barrier(self.controller)
+
+        msg = ofp.message.group_mod(
+            command=ofp.OFPGC_ADD,
+            group_type=ofp.OFPGT_ALL,
+            group_id=2,
+            buckets=[])
+
+        self.controller.message_send(msg)
+        do_barrier(self.controller)
+
+        packet_in1 = simple_tcp_packet(tcp_sport=1000)
+
+        flow_add_msg1 = flow_msg_create(
+            self,
+            packet_in1,
+            ing_port=1,
+            action_list=[
+                ofp.action.group(1),
+                ofp.action.output(port1)])
+
+        self.controller.message_send(flow_add_msg1)
+        do_barrier(self.controller)
+
+        packet_in2 = simple_tcp_packet(tcp_sport=2000)
+
+        flow_add_msg2 = flow_msg_create(
+            self,
+            packet_in2,
+            ing_port=1,
+            action_list=[
+                ofp.action.group(2),
+                ofp.action.output(port1)])
+
+        self.controller.message_send(flow_add_msg2)
+        do_barrier(self.controller)
+
+        packet_in3 = simple_tcp_packet(tcp_sport=3000)
+
+        flow_add_msg3 = flow_msg_create(
+            self,
+            packet_in3,
+            ing_port=1,
+            action_list=[
+                ofp.action.group(2),
+                ofp.action.output(port1)])
+
+        self.controller.message_send(flow_add_msg3)
+        do_barrier(self.controller)
+
+        packet_in4 = simple_tcp_packet(tcp_sport=4000)
+
+        flow_add_msg4 = flow_msg_create(
+            self,
+            packet_in4,
+            ing_port=1,
+            action_list=[
+                ofp.action.output(port1)])
+
+        self.controller.message_send(flow_add_msg4)
+        do_barrier(self.controller)
+
+        request = ofp.message.aggregate_stats_request(
+            table_id=ofp.OFPTT_ALL,
+            out_port=ofp.OFPP_ANY,
+            out_group=2)
+
+        response, _ = self.controller.transact(request)
+
+        self.assertEqual(response.flow_count, 2,
+                         'Did not match expected flow count')