Esempio n. 1
0
class MsgQTest(unittest.TestCase):
    """
    Tests for the behaviour of MsgQ. This is for the core of MsgQ, other
    subsystems are in separate test fixtures.
    """
    def setUp(self):
        self.__msgq = MsgQ()

    def parse_msg(self, msg):
        """
        Parse a binary representation of message to the routing header and the
        data payload. It assumes the message is correctly encoded and the
        payload is not omitted. It'd probably throw in other cases, but we
        don't use it in such situations in this test.
        """
        (length, header_len) = struct.unpack('>IH', msg[:6])
        header = json.loads(msg[6:6 + header_len].decode('utf-8'))
        data = json.loads(msg[6 + header_len:].decode('utf-8'))
        return (header, data)

    def test_unknown_command(self):
        """
        Test the command handler returns error when the command is unknown.
        """
        # Fake we are running, to disable test workarounds
        self.__msgq.running = True
        self.assertEqual({'result': [1, "unknown command: unknown"]},
                         self.__msgq.command_handler('unknown', {}))

    def test_get_members(self):
        """
        Test getting members of a group or of all connected clients.
        """
        # Push two dummy "clients" into msgq (the ugly way, by directly
        # tweaking relevant data structures).
        class Sock:
            def __init__(self, fileno):
                self.fileno = lambda: fileno
        self.__msgq.lnames['first'] = Sock(1)
        self.__msgq.lnames['second'] = Sock(2)
        self.__msgq.fd_to_lname[1] = 'first'
        self.__msgq.fd_to_lname[2] = 'second'
        # Subscribe them to some groups
        self.__msgq.process_command_subscribe(self.__msgq.lnames['first'],
                                              {'group': 'G1', 'instance': '*'},
                                              None)
        self.__msgq.process_command_subscribe(self.__msgq.lnames['second'],
                                              {'group': 'G1', 'instance': '*'},
                                              None)
        self.__msgq.process_command_subscribe(self.__msgq.lnames['second'],
                                              {'group': 'G2', 'instance': '*'},
                                              None)
        # Now query content of some groups through the command handler.
        self.__msgq.running = True # Enable the command handler
        def check_both(result):
            """
            Check the result is successful one and it contains both lnames (in
            any order).
            """
            array = result['result'][1]
            self.assertEqual(set(['first', 'second']), set(array))
            self.assertEqual({'result': [0, array]}, result)
            # Make sure the result can be encoded as JSON
            # (there seems to be types that look like a list but JSON choks
            # on them)
            json.dumps(result)
        # Members of the G1 and G2
        self.assertEqual({'result': [0, ['second']]},
                         self.__msgq.command_handler('members',
                                                     {'group': 'G2'}))
        check_both(self.__msgq.command_handler('members', {'group': 'G1'}))
        # We pretend that all the possible groups exist, just that most
        # of them are empty. So requesting for Empty is request for an empty
        # group and should not fail.
        self.assertEqual({'result': [0, []]},
                         self.__msgq.command_handler('members',
                                                     {'group': 'Empty'}))
        # Without the name of the group, we just get all the clients.
        check_both(self.__msgq.command_handler('members', {}))
        # Omitting the parameters completely in such case is OK
        check_both(self.__msgq.command_handler('members', None))

    def notifications_setup(self):
        """
        Common setup of some notifications tests. Mock several things.
        """
        # Mock the method to send notifications (we don't really want
        # to send them now, just see they'd be sent).
        # Mock the poller, as we don't need it at all (and we don't have
        # real socket to give it now).
        notifications = []
        def send_notification(event, params):
            notifications.append((event, params))
        class FakePoller:
            def register(self, socket, mode):
                pass
            def unregister(self, sock):
                pass
        self.__msgq.members_notify = send_notification
        self.__msgq.poller = FakePoller()

        # Create a socket
        class Sock:
            def __init__(self, fileno):
                self.fileno = lambda: fileno
            def close(self):
                pass
        sock = Sock(1)
        return notifications, sock

    def test_notifies(self):
        """
        Test the message queue sends notifications about connecting,
        disconnecting and subscription changes.
        """
        notifications, sock = self.notifications_setup()

        # We should notify about new cliend when we register it
        self.__msgq.register_socket(sock)
        lname = self.__msgq.fd_to_lname[1] # Steal the lname
        self.assertEqual([('connected', {'client': lname})], notifications)
        del notifications[:]

        # A notification should happen for a subscription to a group
        self.__msgq.process_command_subscribe(sock, {'group': 'G',
                                                     'instance': '*'},
                                              None)
        self.assertEqual([('subscribed', {'client': lname, 'group': 'G'})],
                         notifications)
        del notifications[:]

        # As well for unsubscription
        self.__msgq.process_command_unsubscribe(sock, {'group': 'G',
                                                       'instance': '*'},
                                                None)
        self.assertEqual([('unsubscribed', {'client': lname, 'group': 'G'})],
                         notifications)
        del notifications[:]

        # Unsubscription from a group it isn't subscribed to
        self.__msgq.process_command_unsubscribe(sock, {'group': 'H',
                                                       'instance': '*'},
                                                None)
        self.assertEqual([], notifications)

        # And, finally, for removal of client
        self.__msgq.kill_socket(sock.fileno(), sock)
        self.assertEqual([('disconnected', {'client': lname})], notifications)

    def test_notifies_implicit_kill(self):
        """
        Test that the unsubscription notifications are sent before the socket
        is dropped, even in case it does not unsubscribe explicitly.
        """
        notifications, sock = self.notifications_setup()

        # Register and subscribe. Notifications for these are in above test.
        self.__msgq.register_socket(sock)
        lname = self.__msgq.fd_to_lname[1] # Steal the lname
        self.__msgq.process_command_subscribe(sock, {'group': 'G',
                                                     'instance': '*'},
                                              None)
        del notifications[:]

        self.__msgq.kill_socket(sock.fileno(), sock)
        # Now, the notification for unsubscribe should be first, second for
        # the disconnection.
        self.assertEqual([('unsubscribed', {'client': lname, 'group': 'G'}),
                          ('disconnected', {'client': lname})
                         ], notifications)

    def test_undeliverable_errors(self):
        """
        Send several packets through the MsgQ and check it generates
        undeliverable notifications under the correct circumstances.

        The test is not exhaustive as it doesn't test all combination
        of existence of the recipient, addressing schemes, want_answer
        header and the reply header. It is not needed, these should
        be mostly independent. That means, for example, if the message
        is a reply and there's no recipient to send it to, the error
        would not be generated no matter if we addressed the recipient
        by lname or group. If we included everything, the test would
        have too many scenarios with little benefit.
        """
        self.__sent_messages = []
        def fake_send_prepared_msg(socket, msg):
            self.__sent_messages.append((socket, msg))
            return True
        self.__msgq.send_prepared_msg = fake_send_prepared_msg
        # These would be real sockets in the MsgQ, but we pass them as
        # parameters only, so we don't need them to be. We use simple
        # integers to tell one from another.
        sender = 1
        recipient = 2
        another_recipiet = 3
        # The routing headers and data to test with.
        routing = {
            'to': '*',
            'from': 'sender',
            'group': 'group',
            'instance': '*',
            'seq': 42
        }
        data = {
            "data": "Just some data"
        }

        # Some common checking patterns
        def check_error():
            self.assertEqual(1, len(self.__sent_messages))
            self.assertEqual(1, self.__sent_messages[0][0])
            self.assertEqual(({
                                  'group': 'group',
                                  'instance': '*',
                                  'reply': 42,
                                  'seq': 42,
                                  'from': 'msgq',
                                  'to': 'sender',
                                  'want_answer': True
                              }, {'result': [-1, "No such recipient"]}),
                              self.parse_msg(self.__sent_messages[0][1]))
            self.__sent_messages = []

        def check_no_message():
            self.assertEqual([], self.__sent_messages)

        def check_delivered(rcpt_socket=recipient):
            self.assertEqual(1, len(self.__sent_messages))
            self.assertEqual(rcpt_socket, self.__sent_messages[0][0])
            self.assertEqual((routing, data),
                             self.parse_msg(self.__sent_messages[0][1]))
            self.__sent_messages = []

        # Send the message. No recipient, but errors are not requested,
        # so none is generated.
        self.__msgq.process_command_send(sender, routing, data)
        check_no_message()

        # It should act the same if we explicitly say we do not want replies.
        routing["want_answer"] = False
        self.__msgq.process_command_send(sender, routing, data)
        check_no_message()

        # Ask for errors if it can't be delivered.
        routing["want_answer"] = True
        self.__msgq.process_command_send(sender, routing, data)
        check_error()

        # If the message is a reply itself, we never generate the errors
        routing["reply"] = 3
        self.__msgq.process_command_send(sender, routing, data)
        check_no_message()

        # If there are recipients (but no "reply" header), the error should not
        # be sent and the message should get delivered.
        del routing["reply"]
        self.__msgq.subs.find = lambda group, instance: [recipient]
        self.__msgq.process_command_send(sender, routing, data)
        check_delivered()

        # When we send a direct message and the recipient is not there, we get
        # the error too
        routing["to"] = "lname"
        self.__msgq.process_command_send(sender, routing, data)
        check_error()

        # But when the recipient is there, it is delivered and no error is
        # generated.
        self.__msgq.lnames["lname"] = recipient
        self.__msgq.process_command_send(sender, routing, data)
        check_delivered()

        # If an attempt to send fails, consider it no recipient.
        def fail_send_prepared_msg(socket, msg):
            '''
            Pretend sending a message failed. After one call, return to the
            usual mock, so the errors or other messages can be sent.
            '''
            self.__msgq.send_prepared_msg = fake_send_prepared_msg
            return False

        self.__msgq.send_prepared_msg = fail_send_prepared_msg
        self.__msgq.process_command_send(sender, routing, data)
        check_error()

        # But if there are more recipients and only one fails, it should
        # be delivered to the other and not considered an error
        self.__msgq.send_prepared_msg = fail_send_prepared_msg
        routing["to"] = '*'
        self.__msgq.subs.find = lambda group, instance: [recipient,
                                                         another_recipiet]
        self.__msgq.process_command_send(sender, routing, data)
        check_delivered(rcpt_socket=another_recipiet)
Esempio n. 2
0
class MsgQTest(unittest.TestCase):
    """
    Tests for the behaviour of MsgQ. This is for the core of MsgQ, other
    subsystems are in separate test fixtures.
    """
    def setUp(self):
        self.__msgq = MsgQ()

    def parse_msg(self, msg):
        """
        Parse a binary representation of message to the routing header and the
        data payload. It assumes the message is correctly encoded and the
        payload is not omitted. It'd probably throw in other cases, but we
        don't use it in such situations in this test.
        """
        (length, header_len) = struct.unpack('>IH', msg[:6])
        header = json.loads(msg[6:6 + header_len].decode('utf-8'))
        data = json.loads(msg[6 + header_len:].decode('utf-8'))
        return (header, data)

    def test_unknown_command(self):
        """
        Test the command handler returns error when the command is unknown.
        """
        # Fake we are running, to disable test workarounds
        self.__msgq.running = True
        self.assertEqual({'result': [1, "unknown command: unknown"]},
                         self.__msgq.command_handler('unknown', {}))

    def test_get_members(self):
        """
        Test getting members of a group or of all connected clients.
        """

        # Push two dummy "clients" into msgq (the ugly way, by directly
        # tweaking relevant data structures).
        class Sock:
            def __init__(self, fileno):
                self.fileno = lambda: fileno

        self.__msgq.lnames['first'] = Sock(1)
        self.__msgq.lnames['second'] = Sock(2)
        self.__msgq.fd_to_lname[1] = 'first'
        self.__msgq.fd_to_lname[2] = 'second'
        # Subscribe them to some groups
        self.__msgq.process_command_subscribe(self.__msgq.lnames['first'], {
            'group': 'G1',
            'instance': '*'
        }, None)
        self.__msgq.process_command_subscribe(self.__msgq.lnames['second'], {
            'group': 'G1',
            'instance': '*'
        }, None)
        self.__msgq.process_command_subscribe(self.__msgq.lnames['second'], {
            'group': 'G2',
            'instance': '*'
        }, None)
        # Now query content of some groups through the command handler.
        self.__msgq.running = True  # Enable the command handler

        def check_both(result):
            """
            Check the result is successful one and it contains both lnames (in
            any order).
            """
            array = result['result'][1]
            self.assertEqual(set(['first', 'second']), set(array))
            self.assertEqual({'result': [0, array]}, result)
            # Make sure the result can be encoded as JSON
            # (there seems to be types that look like a list but JSON choks
            # on them)
            json.dumps(result)

        # Members of the G1 and G2
        self.assertEqual({'result': [0, ['second']]},
                         self.__msgq.command_handler('members',
                                                     {'group': 'G2'}))
        check_both(self.__msgq.command_handler('members', {'group': 'G1'}))
        # We pretend that all the possible groups exist, just that most
        # of them are empty. So requesting for Empty is request for an empty
        # group and should not fail.
        self.assertEqual({'result': [0, []]},
                         self.__msgq.command_handler('members',
                                                     {'group': 'Empty'}))
        # Without the name of the group, we just get all the clients.
        check_both(self.__msgq.command_handler('members', {}))
        # Omitting the parameters completely in such case is OK
        check_both(self.__msgq.command_handler('members', None))

    def notifications_setup(self):
        """
        Common setup of some notifications tests. Mock several things.
        """
        # Mock the method to send notifications (we don't really want
        # to send them now, just see they'd be sent).
        # Mock the poller, as we don't need it at all (and we don't have
        # real socket to give it now).
        notifications = []

        def send_notification(event, params):
            notifications.append((event, params))

        class FakePoller:
            def register(self, socket, mode):
                pass

            def unregister(self, sock):
                pass

        self.__msgq.members_notify = send_notification
        self.__msgq.poller = FakePoller()

        # Create a socket
        class Sock:
            def __init__(self, fileno):
                self.fileno = lambda: fileno

            def close(self):
                pass

        sock = Sock(1)
        return notifications, sock

    def test_notifies(self):
        """
        Test the message queue sends notifications about connecting,
        disconnecting and subscription changes.
        """
        notifications, sock = self.notifications_setup()

        # We should notify about new cliend when we register it
        self.__msgq.register_socket(sock)
        lname = self.__msgq.fd_to_lname[1]  # Steal the lname
        self.assertEqual([('connected', {'client': lname})], notifications)
        del notifications[:]

        # A notification should happen for a subscription to a group
        self.__msgq.process_command_subscribe(sock, {
            'group': 'G',
            'instance': '*'
        }, None)
        self.assertEqual([('subscribed', {
            'client': lname,
            'group': 'G'
        })], notifications)
        del notifications[:]

        # As well for unsubscription
        self.__msgq.process_command_unsubscribe(sock, {
            'group': 'G',
            'instance': '*'
        }, None)
        self.assertEqual([('unsubscribed', {
            'client': lname,
            'group': 'G'
        })], notifications)
        del notifications[:]

        # Unsubscription from a group it isn't subscribed to
        self.__msgq.process_command_unsubscribe(sock, {
            'group': 'H',
            'instance': '*'
        }, None)
        self.assertEqual([], notifications)

        # And, finally, for removal of client
        self.__msgq.kill_socket(sock.fileno(), sock)
        self.assertEqual([('disconnected', {'client': lname})], notifications)

    def test_notifies_implicit_kill(self):
        """
        Test that the unsubscription notifications are sent before the socket
        is dropped, even in case it does not unsubscribe explicitly.
        """
        notifications, sock = self.notifications_setup()

        # Register and subscribe. Notifications for these are in above test.
        self.__msgq.register_socket(sock)
        lname = self.__msgq.fd_to_lname[1]  # Steal the lname
        self.__msgq.process_command_subscribe(sock, {
            'group': 'G',
            'instance': '*'
        }, None)
        del notifications[:]

        self.__msgq.kill_socket(sock.fileno(), sock)
        # Now, the notification for unsubscribe should be first, second for
        # the disconnection.
        self.assertEqual([('unsubscribed', {
            'client': lname,
            'group': 'G'
        }), ('disconnected', {
            'client': lname
        })], notifications)

    def test_undeliverable_errors(self):
        """
        Send several packets through the MsgQ and check it generates
        undeliverable notifications under the correct circumstances.

        The test is not exhaustive as it doesn't test all combination
        of existence of the recipient, addressing schemes, want_answer
        header and the reply header. It is not needed, these should
        be mostly independent. That means, for example, if the message
        is a reply and there's no recipient to send it to, the error
        would not be generated no matter if we addressed the recipient
        by lname or group. If we included everything, the test would
        have too many scenarios with little benefit.
        """
        self.__sent_messages = []

        def fake_send_prepared_msg(socket, msg):
            self.__sent_messages.append((socket, msg))
            return True

        self.__msgq.send_prepared_msg = fake_send_prepared_msg
        # These would be real sockets in the MsgQ, but we pass them as
        # parameters only, so we don't need them to be. We use simple
        # integers to tell one from another.
        sender = 1
        recipient = 2
        another_recipiet = 3
        # The routing headers and data to test with.
        routing = {
            'to': '*',
            'from': 'sender',
            'group': 'group',
            'instance': '*',
            'seq': 42
        }
        data = {"data": "Just some data"}

        # Some common checking patterns
        def check_error():
            self.assertEqual(1, len(self.__sent_messages))
            self.assertEqual(1, self.__sent_messages[0][0])
            self.assertEqual(({
                'group': 'group',
                'instance': '*',
                'reply': 42,
                'seq': 42,
                'from': 'msgq',
                'to': 'sender',
                'want_answer': True
            }, {
                'result': [-1, "No such recipient"]
            }), self.parse_msg(self.__sent_messages[0][1]))
            self.__sent_messages = []

        def check_no_message():
            self.assertEqual([], self.__sent_messages)

        def check_delivered(rcpt_socket=recipient):
            self.assertEqual(1, len(self.__sent_messages))
            self.assertEqual(rcpt_socket, self.__sent_messages[0][0])
            self.assertEqual((routing, data),
                             self.parse_msg(self.__sent_messages[0][1]))
            self.__sent_messages = []

        # Send the message. No recipient, but errors are not requested,
        # so none is generated.
        self.__msgq.process_command_send(sender, routing, data)
        check_no_message()

        # It should act the same if we explicitly say we do not want replies.
        routing["want_answer"] = False
        self.__msgq.process_command_send(sender, routing, data)
        check_no_message()

        # Ask for errors if it can't be delivered.
        routing["want_answer"] = True
        self.__msgq.process_command_send(sender, routing, data)
        check_error()

        # If the message is a reply itself, we never generate the errors
        routing["reply"] = 3
        self.__msgq.process_command_send(sender, routing, data)
        check_no_message()

        # If there are recipients (but no "reply" header), the error should not
        # be sent and the message should get delivered.
        del routing["reply"]
        self.__msgq.subs.find = lambda group, instance: [recipient]
        self.__msgq.process_command_send(sender, routing, data)
        check_delivered()

        # When we send a direct message and the recipient is not there, we get
        # the error too
        routing["to"] = "lname"
        self.__msgq.process_command_send(sender, routing, data)
        check_error()

        # But when the recipient is there, it is delivered and no error is
        # generated.
        self.__msgq.lnames["lname"] = recipient
        self.__msgq.process_command_send(sender, routing, data)
        check_delivered()

        # If an attempt to send fails, consider it no recipient.
        def fail_send_prepared_msg(socket, msg):
            '''
            Pretend sending a message failed. After one call, return to the
            usual mock, so the errors or other messages can be sent.
            '''
            self.__msgq.send_prepared_msg = fake_send_prepared_msg
            return False

        self.__msgq.send_prepared_msg = fail_send_prepared_msg
        self.__msgq.process_command_send(sender, routing, data)
        check_error()

        # But if there are more recipients and only one fails, it should
        # be delivered to the other and not considered an error
        self.__msgq.send_prepared_msg = fail_send_prepared_msg
        routing["to"] = '*'
        self.__msgq.subs.find = lambda group, instance: [
            recipient, another_recipiet
        ]
        self.__msgq.process_command_send(sender, routing, data)
        check_delivered(rcpt_socket=another_recipiet)