def test_connect_when_setupRabbitMQ_does_not_raise(self, m_setupRabbitMQ):
        """
        Handle the return values of setupRabbitMQ correctly when it does not
        raise any exceptions.
        """
        # Constructor must set 'rmq' to None and not connect to RabbitMQ.
        es = eventstore.EventStore(topics=['#'])
        assert es.rmq is None
        assert m_setupRabbitMQ.call_count == 0

        # If the 'rmq' is not None then setupRabbitMQ must not be called.
        es.rmq = {'x': 'y'}
        assert m_setupRabbitMQ.call_count == 0
        assert es.connect() == (True, None, None)
        assert m_setupRabbitMQ.call_count == 0
        assert es.rmq == {'x': 'y'}

        # If the 'rmq' is None then setupRabbitMQ must be called and its return
        # value stored in the 'rmq' instance variable.
        es.rmq = None
        m_setupRabbitMQ.reset_mock()
        m_setupRabbitMQ.return_value = RetVal(True, None, {'foo': 'bar'})
        assert m_setupRabbitMQ.call_count == 0
        assert es.connect() == (True, None, None)
        assert m_setupRabbitMQ.call_count == 1
        assert es.rmq == {'foo': 'bar'}
    def test_disconnect(self):
        # Get an EventStore instance.
        es = eventstore.EventStore(topics=['#'])

        # Must do nothing when es.rmq is None.
        es.rmq = None
        assert es.disconnect() == (True, None, None)

        # Specify RabbitMQ handles.
        m_chan = MagicMock()
        m_conn = MagicMock()
        es.rmq = {'chan': m_chan, 'conn': m_conn}

        # This time, 'disconnect' must call the close methods on the RabbitMQ
        # channel and connection, respectively.
        assert es.disconnect() == (True, None, None)
        m_chan.close.call_count == 1
        m_conn.close.call_count == 1
        assert es.rmq is None
    def test_connect_when_setupRabbitMQ_raises_exception(
            self, m_setupRabbitMQ):
        """
        SetupRabbitMQ raises an error. Our 'setup' method must intercept them
        and return an error.
        """
        # Define possible exceptions.
        possible_exceptions = [
            pika.exceptions.ChannelClosed,
            pika.exceptions.ChannelError,
            pika.exceptions.ConnectionClosed,
        ]

        # Verify that each error is intercepted.
        for err in possible_exceptions:
            es = eventstore.EventStore(topics=['#'])
            m_setupRabbitMQ.mock_reset()
            m_setupRabbitMQ.side_effect = err
            assert not es.connect().ok
    def createEventStoreClients(self, num_clients: int, topics: list):
        """
        Return a of connected EventStore clients.

        The list will contain `num_client` thread handles.

        This method does not return until all threads have successfully
        established a connection to RabbitMQ.
        """
        # Spawn the threads.
        es = [eventstore.EventStore(topics=topics) for _ in range(3)]
        [_.start() for _ in es]

        # Wait until each thread updated its 'rmq' attribute.
        while True:
            if len([_.rmq for _ in es if _.rmq is None]) == 0:
                return es
            time.sleep(0.5)
            print('waiting')
    def test_invalid_key(self):
        """
        Similar test as before, but this time we subscribe to a particular
        topic instead of all topics. That is, publish three message, two to
        the topic we subscribed to, and one to another topic. We should only
        receive two messages.
        """
        # Create an EventStore instance and subscribe it to the 'foo' topic.
        es = self.createEventStoreClients(num_clients=1, topics=['foo'])[0]

        # No messages must have arrived yet.
        assert es.getMessages() == (True, None, [])

        # Create a dedicated publisher instance because the class does not play
        # nice when called from different threads.
        pub = eventstore.EventStore(topics=['foo'])

        # Publish our test messages.
        pub.publish(topic='foo', msg=b'bar0')
        pub.publish(topic='blah', msg=b'bar1')
        pub.publish(topic='foo', msg=b'bar2')

        # Wait until the client received at least two messages (RabbitMQ incurs
        # some latency).
        for ii in range(10):
            time.sleep(0.1)
            if len(es.messages) >= 2:
                break
            assert ii < 9

        # Verify that we got the two messages for our topic.
        ret = es.getMessages()
        assert ret.ok
        assert ret.data == [
            ('foo', b'bar0'),
            ('foo', b'bar2'),
        ]

        # Stop the thread.
        es.stop()
        es.join()
    def test_basic_publishing(self):
        """
        Create an EventStore instance that listens for all messages. Then
        publish some messages and verify they arrive as expected.
        """
        # Create an EventStore instance and subscribe it to all topics.
        es = self.createEventStoreClients(num_clients=1, topics=['#'])[0]

        # Create a dedicated publisher instance because the class does not play
        # nice when called from different threads.
        pub = eventstore.EventStore(topics=['foo'])

        # No messages must have arrived yet.
        assert es.getMessages() == (True, None, [])

        # Publish our test messages.
        assert pub.publish(topic='foo', msg=b'bar0').ok
        assert pub.publish(topic='foo', msg=b'bar1').ok

        # Wait until the client received at least two messages (RabbitMQ incurs
        # some latency).
        for ii in range(10):
            time.sleep(0.1)
            if len(es.messages) >= 2:
                break
            assert ii < 9

        # Verify we got both messages.
        ret = es.getMessages()
        assert ret.ok
        assert ret.data == [
            ('foo', b'bar0'),
            ('foo', b'bar1'),
        ]

        # There must be no new messages.
        assert es.getMessages() == (True, None, [])

        # Stop the thread.
        es.stop()
        es.join()
    def test_publish_raises_no_error(self, m_connect):
        # Get an EventStore instance.
        es = eventstore.EventStore(topics=['#'])

        # Side effect function for the mock (see below).
        m_chan = MagicMock()
        rmq_mock_handles = {
            'chan': m_chan,
            'conn': MagicMock(),
            'name_queue': 'foo',
            'name_exchange': 'foo'
        }

        # Install mocked RabbitMQ as a side effect because the rest of the
        # connect function will assume they exist.
        def side_effect_fun():
            es.rmq = rmq_mock_handles

        # We expect the mocked 'connect' function to return success and install
        # the 'rmq' instance variable.
        m_connect.side_effect = side_effect_fun
        m_connect.return_value = RetVal(True, None, None)

        # Publish one message when no connection has been established yet.
        assert es.rmq is None
        assert m_chan.basic_publish.call_count == 0
        assert m_connect.call_count == 0
        assert es.publish(topic='foo', msg=b'bar') == (True, None, None)
        assert m_connect.call_count == 1
        assert m_chan.basic_publish.call_count == 1

        # Publish one message when the connection has already been established.
        es.rmq = rmq_mock_handles
        m_chan.reset_mock()
        m_connect.reset_mock()
        m_connect.return_value = RetVal(True, None, None)
        assert m_chan.basic_publish.call_count == 0
        assert m_connect.call_count == 0
        assert es.publish(topic='foo', msg=b'bar') == (True, None, None)
        assert m_connect.call_count == 0
        assert m_chan.basic_publish.call_count == 1
    def test_blockingConsume_when_pika_raises_error(self):
        """
        Creat mocked Pika handles and let 'start_consume' raise an error. Our
        own 'blockingConsume' must safely intercept the error.
        """
        # Create one EventStore instance and subscribed it to all topics.
        possible_exceptions = [
            pika.exceptions.ChannelClosed,
            pika.exceptions.ChannelError,
            pika.exceptions.ConnectionClosed,
        ]

        # Create an EventStore instance, mock out the RabbitMQ channel, and let
        # the 'start_consuming' method raise one of the various RabbitMQ errors
        # we want to intercept.
        for err in possible_exceptions:
            es = eventstore.EventStore(topics=['#'])
            m_chan = MagicMock()
            m_chan.start_consuming.side_effect = err
            es.rmq = {'chan': m_chan, 'conn': MagicMock(), 'name_queue': 'foo'}
            assert not es.blockingConsume().ok
    def test_blockingConsume_not_yet_connected(self, m__blockingConsumePika):
        """
        Our 'blockingConsume' function must automatically connect to RabbitMQ
        unless the connection already exists.
        """
        # Get an EventStore instance.
        es = eventstore.EventStore(topics=['#'])

        # 'blockingConsume' must return with an error if no RabbitMQ handles
        # are available.
        assert es.rmq is None
        assert m__blockingConsumePika.call_count == 0
        assert es.blockingConsume() == (False, 'Not yet connected', None)
        assert m__blockingConsumePika.call_count == 0

        # 'blockingConsume' must call the Pika handler if handles are available
        es.rmq = {'foo': 'bar'}
        m__blockingConsumePika.return_value = RetVal(True, None, None)
        assert m__blockingConsumePika.call_count == 0
        assert es.blockingConsume().ok
        assert m__blockingConsumePika.call_count == 1
    def test_run_auto_connect(self, m_blockingConsume, m_disconnect,
                              m_connect):
        # Get an EventStore instance.
        es = eventstore.EventStore(topics=['#'])

        # 'blockingConsume' will return an error the first two times, and no
        # error the last time.
        m_blockingConsume.side_effect = [
            RetVal(False, None, None),
            RetVal(False, None, None),
            RetVal(True, None, None)
        ]

        # 'run' must call the 'connect' method whenever an error has occurred,
        # and exit once 'blockingConsume' returns without error (this
        # constitutes terminating the thread). Note: run always calls
        # 'disconnect' and 'connnect' before it makes any attempts to consume,
        # hence the '+1' in the test below.
        es.run()
        assert m_connect.call_count == 2 + 1
        assert m_disconnect.call_count == 2 + 1
        assert m_blockingConsume.call_count == 3
    def test_publish_raises_errors(self):
        """
        Our 'publish' method must safely intercept all errors raised by Pika's
        'basic_publish' method.
        """
        # Define possible exceptions.
        possible_exceptions = [
            pika.exceptions.ChannelClosed,
            pika.exceptions.ChannelError,
            pika.exceptions.ConnectionClosed,
        ]

        # Verify that each error is intercepted.
        for err in possible_exceptions:
            es = eventstore.EventStore(topics=['#'])
            m_chan = MagicMock()
            m_chan.basic_publish.side_effect = err
            es.rmq = {
                'chan': m_chan,
                'conn': MagicMock(),
                'name_exchange': 'foo'
            }
            assert not es.publish(topic='foo', msg=b'bar').ok
    def test_listen_for_multiple_topics(self):
        """
        Subscribe two multiple topics at once.
        """
        # Create an EventStore instance and subscribe it to all messages.
        es = self.createEventStoreClients(num_clients=1, topics=['foo',
                                                                 'bar'])[0]

        # Create a dedicated publisher instance because the class does not play
        # nice when called from different threads.
        pub = eventstore.EventStore(topics=['foo'])

        # Publish our test messages.
        pub.publish(topic='foo', msg=b'0')
        pub.publish(topic='blah', msg=b'1')
        pub.publish(topic='bar', msg=b'2')

        # Wait until the client received at least two messages (RabbitMQ incurs
        # some latency).
        for ii in range(10):
            time.sleep(0.1)
            if len(es.messages) >= 2:
                break
            assert ii < 9

        # Verify we got the message published for 'foo' and 'bar'.
        ret = es.getMessages()
        assert ret.ok
        assert ret.data == [
            ('foo', b'0'),
            ('bar', b'2'),
        ]

        # Stop the thread.
        es.stop()
        es.join()
    def test_multiple_receivers(self):
        """
        Create several listeners and verify they all receive the message.
        """
        # Start several event store threads and subscribe them to 'foo'.
        es = self.createEventStoreClients(num_clients=3, topics=['foo'])

        # Create a dedicated publisher instance because the class does not play
        # nice when called from different threads.
        pub = eventstore.EventStore(topics=['foo'])

        # Publish our test messages.
        pub.publish(topic='foo', msg=b'bar0')
        pub.publish(topic='blah', msg=b'bar1')
        pub.publish(topic='foo', msg=b'bar2')

        # Wait until each client received at least two messages (RabbitMQ incurs
        # some latency).
        for ii in range(10):
            time.sleep(0.1)
            if min([len(_.messages) for _ in es]) >= 2:
                break
            assert ii < 9

        # Verify each client got both messages.
        for thread in es:
            ret = thread.getMessages()
            assert ret.ok
            assert ret.data == [
                ('foo', b'bar0'),
                ('foo', b'bar2'),
            ]

        # Stop the threads.
        [_.stop() for _ in es]
        [_.join() for _ in es]
    def test_shutdown(self):
        """
        Verify that the EventStore threads shut down properly. Threads must be
        cooperative in this regard because Python lacks the mechanism to
        forcefully terminate them.
        """
        # Create an EventStore instance and subscribe it to all topics.
        es = self.createEventStoreClients(num_clients=1, topics=['#'])[0]

        # Tell the thread to stop. Wait at most one Second, then verify it has
        # really stopped.
        es.stop()
        es.join(1.0)
        assert not es.is_alive()

        # Repeat the above test with many threads.
        threads = [eventstore.EventStore(topics=['#']) for _ in range(100)]
        [_.start() for _ in threads]
        time.sleep(0.2)
        [_.stop() for _ in threads]
        [_.join(1.0) for _ in threads]
        for thread in threads:
            assert not thread.is_alive()
        del threads