Browse Source

[2398] Add test setup for sending messages in msgq

Test setup raising arbitrary exceptions from the write socket.

We can extend this to do the same for reading I think.

Also added tests; some for things that worked already (EAGAIN, EWOULDBLOCK), but also for the SIGPIPE that is the problem in #2398
Jelte Jansen 12 years ago
parent
commit
5378e305cb
1 changed files with 197 additions and 3 deletions
  1. 197 3
      src/bin/msgq/tests/msgq_test.py

+ 197 - 3
src/bin/msgq/tests/msgq_test.py

@@ -6,6 +6,8 @@ import socket
 import signal
 import sys
 import time
+import errno
+import threading
 import isc.cc
 
 #
@@ -112,6 +114,83 @@ class TestSubscriptionManager(unittest.TestCase):
         msgq = MsgQ("/does/not/exist")
         self.assertRaises(socket.error, msgq.setup)
 
+class DummySocket:
+    """
+    Dummy socket class.
+    This one does nothing at all, but some calls are used.
+    It is mainly intended to override the listen socket for msgq, which
+    we do not need in these tests.
+    """
+    def fileno():
+        return -1
+
+    def close():
+        pass
+
+class BadSocket:
+    """
+    Special socket wrapper class. Once given a socket in its constructor,
+    it completely behaves like that socket, except that its send() call
+    will only actually send one byte per call, and optionally raise a given
+    exception at a given time.
+    """
+    def __init__(self, real_socket, raise_on_send=0, send_exception=None):
+        """
+        Parameters:
+        real_socket: The actual socket to wrap
+        raise_on_send: integer. If send_exception is not None, it will be
+                                raised on this byte (i.e. 1 = on the first
+                                call to send(), 1 = on the 4th call to send)
+                                Note: if 0, send_exception will not be raised.
+        send_exception: if not None, this exception will be raised
+                        (if raise_on_send is not 0)
+        """
+        self.socket = real_socket
+        self.send_count = 0
+        self.raise_on_send = raise_on_send
+        self.send_exception = send_exception
+
+    # completely wrap all calls and member access
+    # (except explicitely overridden ones)
+    def __getattr__(self, name, *args):
+        attr = getattr(self.socket, name)
+        if callable(attr):
+            def callable_attr(*args):
+                return attr.__call__(*args)
+            return callable_attr
+        else:
+            return attr
+
+    def send(self, data):
+        self.send_count += 1
+        if self.send_exception is not None and\
+           self.send_count == self.raise_on_send:
+            raise self.send_exception
+
+        if len(data) > 0:
+            return self.socket.send(data[:1])
+        else:
+            return 0
+
+class MsgQThread(threading.Thread):
+    """
+    Very simple thread class that runs msgq.run() when started,
+    and stores the exception that msgq.run() raises, if any.
+    """
+    def __init__(self, msgq):
+        threading.Thread.__init__(self)
+        self.msgq_ = msgq
+        self.stop = False
+        self.caught_exception = None
+
+    def run(self):
+        try:
+            while not self.stop:
+                self.msgq_.run()
+        except Exception as exc:
+            self.caught_exception = exc
+
+
 class SendNonblock(unittest.TestCase):
     """
     Tests that the whole thing will not get blocked if someone does not read.
@@ -191,9 +270,6 @@ class SendNonblock(unittest.TestCase):
         msgq = MsgQ()
         # msgq.run needs to compare with the listen_socket, so we provide
         # a replacement
-        class DummySocket:
-            def fileno():
-                return -1
         msgq.listen_socket = DummySocket
         (queue, out) = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
         def run():
@@ -245,5 +321,123 @@ class SendNonblock(unittest.TestCase):
             data = data + data
         self.send_many(data)
 
+    def do_send(self, write, read, expect_arrive=True,
+                expect_send_exception=None):
+        """
+        Makes a msgq object that is talking to itself,
+        run it in a separate thread so we can use and
+        test run().
+        Parameters:
+        write: a socket that is used to send the data to
+        read: a socket that is used to read the data from
+        expect_arrive: if True, the read socket is read from, and the data
+                       that is read is expected to be the same as the data
+                       that has been sent to the write socket.
+        expect_send_exception: if not None, this is the exception that is
+                               expected to be raised by msgq
+        """
+
+        # Some message and envelope data to send and check
+        env = b'{"env": "foo"}'
+        msg = b'{"msg": "bar"}'
+
+        msgq = MsgQ()
+        # Don't need a listen_socket
+        msgq.listen_socket = DummySocket
+        msgq.setup_poller()
+        msgq.register_socket(write)
+        # Queue the message for sending
+        msgq.sendmsg(write, env, msg)
+
+        # Run it in a thread
+        msgq_thread = MsgQThread(msgq)
+        # If we're done, just kill it
+        msgq_thread.daemon = True
+        msgq_thread.start()
+
+        if expect_arrive:
+            (recv_env, recv_msg) = msgq.read_packet(read.fileno(),
+                read)
+            self.assertEqual(env, recv_env)
+            self.assertEqual(msg, recv_msg)
+
+        # Give it a chance to stop, if it doesn't, no problem, it'll
+        # die when the program does
+        msgq_thread.join(0.2)
+
+        # Check the exception from the thread, if any
+        self.assertEqual(expect_send_exception, msgq_thread.caught_exception)
+
+    def do_send_with_send_error(self, raise_on_send, send_exception,
+                                expect_answer=True,
+                                expect_send_exception=None):
+        """
+        Sets up two connected sockets, wraps the sender socket into a BadSocket
+        class, then performs a do_send() test.
+        Parameters:
+        raise_on_send: the byte at which send_exception should be raised
+                       (see BadSocket)
+        send_exception: the exception to raise (see BadSocket)
+        expect_answer: whether the send is expected to complete (and hence
+                       the read socket should get the message)
+        expect_send_exception: the exception msgq is expected to raise when
+                               send_exception is raised by BadSocket.
+        """
+        (write, read) = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
+        # prevent the test from hanging if something goes wrong
+        read.settimeout(0.2)
+        write.settimeout(0.2)
+        badwrite = BadSocket(write, raise_on_send, send_exception)
+        self.do_send(badwrite, read, expect_answer, expect_send_exception)
+        write.close()
+        read.close()
+
+    def test_send_raise_eagain(self):
+        """
+        Test whether msgq survices an EAGAIN socket error when sending.
+        Two tests are done: one where EAGAIN is raised on the 3rd octet,
+                            and one on the 23rd.
+        """
+        sockerr = socket.error
+        sockerr.errno = errno.EAGAIN
+        self.do_send_with_send_error(3, sockerr)
+        self.do_send_with_send_error(23, sockerr)
+
+    def test_send_raise_ewouldblock(self):
+        """
+        Test whether msgq survices an EWOULDBLOCK socket error when sending.
+        Two tests are done: one where EWOULDBLOCK is raised on the 3rd octet,
+                            and one on the 23rd.
+        """
+        sockerr = socket.error
+        sockerr.errno = errno.EWOULDBLOCK
+        self.do_send_with_send_error(3, sockerr)
+        self.do_send_with_send_error(23, sockerr)
+
+    def test_send_raise_pipe(self):
+        """
+        Test whether msgq survices an EPIPE socket error when sending.
+        Two tests are done: one where EPIPE is raised on the 3rd octet,
+                            and one on the 23rd.
+        """
+        sockerr = socket.error
+        sockerr.errno = errno.EPIPE
+        self.do_send_with_send_error(3, sockerr, False)
+        self.do_send_with_send_error(23, sockerr, False)
+
+    def test_send_raise_exception(self):
+        """
+        Test whether msgq does NOT survive on a general exception.
+        Note, perhaps it should; but we'd have to first discuss and decide
+        how it should recover (i.e. drop the socket and consider the client
+        dead?
+        It may be a coding problem in msgq itself, and we certainly don't
+        want to ignore those.
+        """
+        sockerr = Exception("just some general exception")
+        self.do_send_with_send_error(3, sockerr, False, sockerr)
+        self.do_send_with_send_error(23, sockerr, False, sockerr)
+
+
 if __name__ == '__main__':
     unittest.main()