From a5fde1a0e4e383e132d87c905d58b0056e44c229 Mon Sep 17 00:00:00 2001 From: Andreas Lutro Date: Fri, 6 Apr 2018 09:22:22 +0200 Subject: [PATCH 1/6] write tests after the fact, add travis config --- .gitignore | 2 + .travis.yml | 24 ++++++ amqpconsumer/events.py | 4 +- requirements-dev.txt | 4 + tests/unit_test.py | 189 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 .travis.yml create mode 100644 requirements-dev.txt create mode 100644 tests/unit_test.py diff --git a/.gitignore b/.gitignore index b288303..49c4ca2 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ nosetests.xml .project .pydevproject .idea +.pytest_cache +/htmlcov diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ec07af8 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,24 @@ +branches: + only: + - master + +language: python + +python: + - '2.6' + - '2.7' + - '3.4' + - '3.5' + - '3.6' + +install: + # install setup.py's dependencies and dev requirements + - pip install -e . -r requirements-dev.txt + +script: + - pytest + - prospector + +sudo: false + +cache: pip diff --git a/amqpconsumer/events.py b/amqpconsumer/events.py index 67ff555..991c7be 100644 --- a/amqpconsumer/events.py +++ b/amqpconsumer/events.py @@ -40,7 +40,7 @@ class EventConsumer(object): # on_bindok (skipped if no exchange is provided) -> # start_consuming - def __init__(self, amqp_url, queue, handler, exchange=None, exchange_type=None, routing_key=None): + def __init__(self, amqp_url, queue, handler, exchange=None, exchange_type=None, routing_key=None): # pylint: disable=too-many-arguments """Create a new instance of the consumer class, passing in the URL of RabbitMQ, the queue to listen to, and a callable handler that handles the events. @@ -187,7 +187,7 @@ def setup_queue(self): When completed, the on_queue_declareok method will be invoked by pika. """ - logger.debug("Declaring queue %s" % self._queue) + logger.debug('Declaring queue %s', self._queue) self._channel.queue_declare(self.on_queue_declareok, self._queue, durable=True) def on_queue_declareok(self, _): diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..0fd7d2f --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,4 @@ +pytest +pytest-cov +prospector +mock diff --git a/tests/unit_test.py b/tests/unit_test.py new file mode 100644 index 0000000..c8399e3 --- /dev/null +++ b/tests/unit_test.py @@ -0,0 +1,189 @@ +import mock +from amqpconsumer.events import EventConsumer + + +def get_consumer( + amqp_url='amqps://fake-url', + queue='fake-queue', + handler=None, + exchange=None, + exchange_type=None, + routing_key=None, +): + consumer = EventConsumer( + amqp_url=amqp_url, + queue=queue, + handler=handler, + exchange=exchange, + exchange_type=exchange_type, + routing_key=routing_key, + ) + consumer._connection = mock.MagicMock() + consumer._channel = mock.Mock() + return consumer + + +def get_consumer_with_exchange(): + mock_exchange = mock.Mock() + consumer = get_consumer(exchange=mock_exchange, exchange_type='fake-type', + routing_key='fake-key') + return consumer + + +def test_connect_calls_pika_selectconnection(): + consumer = get_consumer() + with mock.patch('pika.SelectConnection') as mock_select: + consumer.connect() + + mock_select.assert_called_once() + + +def test_on_connection_open(): + consumer = get_consumer() + consumer.on_connection_open('??') + + consumer._connection.add_on_close_callback.assert_called_once_with(consumer.on_connection_closed) + consumer._connection.channel.assert_called_once_with(on_open_callback=consumer.on_channel_open) + + +def test_on_connection_closed_reconnects(): + consumer = get_consumer() + consumer.on_connection_closed('??', 'fake-code', 'fake-text') + + consumer._connection.add_timeout.assert_called_once_with(5, consumer.reconnect) + + +def test_on_connection_closed_stops_when_closing(): + consumer = get_consumer() + consumer._closing = True + consumer.on_connection_closed('??', 'fake-code', 'fake-text') + + consumer._connection.ioloop.stop.assert_called_once_with() + + +def test_open_channel_calls_connection_channel(): + consumer = get_consumer() + consumer.open_channel() + + consumer._connection.channel.assert_called_once_with(on_open_callback=consumer.on_channel_open) + + +def test_on_channel_open(): + channel = mock.Mock() + consumer = get_consumer() + consumer.on_channel_open(channel) + + channel.add_on_close_callback.assert_called_once_with(consumer.on_channel_closed) + channel.basic_qos.assert_called_once_with(prefetch_count=1, callback=consumer.on_qos_set) + + +def test_on_qos_set_when_exchange_is_none(): + consumer = get_consumer(exchange=None) + consumer.on_qos_set('??') + + consumer._channel.queue_declare.assert_called_once_with( + consumer.on_queue_declareok, consumer._queue, durable=True + ) + + +def test_on_exchange_declareok(): + consumer = get_consumer_with_exchange() + consumer.on_exchange_declareok('??') + + consumer._channel.queue_declare.assert_called_once_with( + consumer.on_queue_declareok, consumer._queue, durable=True + ) + + +def test_on_qos_set_when_exchange_is_something(): + consumer = get_consumer_with_exchange() + consumer.on_qos_set('??') + + consumer._channel.exchange_declare.assert_called_once_with( + consumer.on_exchange_declareok, + consumer._exchange, + consumer._exchange_type, + durable=True, + ) + + +def test_on_queue_declareok_when_exchange_is_something(): + consumer = get_consumer_with_exchange() + consumer.on_queue_declareok('??') + + consumer._channel.queue_bind( + consumer.on_bindok, consumer._queue, consumer._exchange, consumer._routing_key + ) + + +def test_on_queue_declareok_when_exchange_is_none(): + consumer = get_consumer() + consumer._channel.basic_consume.return_value = 'fake-tag' + consumer.on_queue_declareok('??') + + consumer._channel.add_on_cancel_callback.assert_called_once_with(consumer.on_consumer_cancelled) + consumer._channel.basic_consume.assert_called_once_with(consumer.on_message, consumer._queue) + assert consumer._consumer_tag == 'fake-tag' + + +def test_on_bindok(): + consumer = get_consumer() + consumer._channel.basic_consume.return_value = 'fake-tag' + consumer.on_bindok('??') + + consumer._channel.add_on_cancel_callback.assert_called_once_with(consumer.on_consumer_cancelled) + consumer._channel.basic_consume.assert_called_once_with(consumer.on_message, consumer._queue) + assert consumer._consumer_tag == 'fake-tag' + + +def test_on_channel_closed(): + consumer = get_consumer() + consumer.on_channel_closed('fake-channel', 'fake-code', 'fake-text') + + consumer._connection.close.assert_called_once() + + +def test_on_consumer_cancelled(): + consumer = get_consumer() + consumer.on_consumer_cancelled('fake-frame') + + consumer._channel.close.assert_called_once() + + +def test_on_message(): + mock_handler = mock.Mock() + consumer = get_consumer(handler=mock_handler) + body = '{"foo":"bar"}' + mock_deliver = mock.Mock() + mock_properties = mock.Mock() + consumer.on_message('??', mock_deliver, mock_properties, body) + + mock_handler.assert_called_once_with({'foo': 'bar'}) + consumer._channel.basic_ack.assert_called_once_with(mock_deliver.delivery_tag) + + +def test_run(): + consumer = get_consumer() + with mock.patch('pika.SelectConnection') as mock_select: + consumer.run() + + mock_select.assert_called_once() + consumer._connection.ioloop.start.assert_called_once() + + +def test_stop(): + consumer = get_consumer() + consumer.stop() + + assert consumer._closing + consumer._channel.basic_cancel.assert_called_once_with( + consumer.on_cancelok, consumer._consumer_tag + ) + consumer._connection.ioloop.start.assert_called_once() + + +def test_on_cancelok(): + consumer = get_consumer() + consumer.on_cancelok('??') + + consumer._channel.close.assert_called_once_with() From 93d0c883b9e59e00323fcdd11efce94959fce5e9 Mon Sep 17 00:00:00 2001 From: Andreas Lutro Date: Fri, 6 Apr 2018 09:37:29 +0200 Subject: [PATCH 2/6] body needs to be a bytestring --- tests/unit_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit_test.py b/tests/unit_test.py index c8399e3..31aa291 100644 --- a/tests/unit_test.py +++ b/tests/unit_test.py @@ -153,7 +153,7 @@ def test_on_consumer_cancelled(): def test_on_message(): mock_handler = mock.Mock() consumer = get_consumer(handler=mock_handler) - body = '{"foo":"bar"}' + body = b'{"foo":"bar"}' mock_deliver = mock.Mock() mock_properties = mock.Mock() consumer.on_message('??', mock_deliver, mock_properties, body) From a193c9ef97d789789b622d97d2276ad0b6727532 Mon Sep 17 00:00:00 2001 From: Andreas Lutro Date: Fri, 6 Apr 2018 09:44:49 +0200 Subject: [PATCH 3/6] don't support python 2.6 --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ec07af8..f8bb080 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,6 @@ branches: language: python python: - - '2.6' - '2.7' - '3.4' - '3.5' From d2025c0d65be78ce376ec96fc334681c4417ccaa Mon Sep 17 00:00:00 2001 From: Andreas Lutro Date: Fri, 6 Apr 2018 09:48:48 +0200 Subject: [PATCH 4/6] make example listener python2/3 compatible --- example_listener.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/example_listener.py b/example_listener.py index 21ebf97..75189bd 100755 --- a/example_listener.py +++ b/example_listener.py @@ -1,11 +1,12 @@ #!/usr/bin/env python +from __future__ import print_function import logging from amqpconsumer.events import EventConsumer def handler(event): - print "Got event:", event + print("Got event:", event) def main(): From 2b7154bae87aeb94947529e567e449b1f26c9e9d Mon Sep 17 00:00:00 2001 From: Andreas Lutro Date: Fri, 6 Apr 2018 11:33:49 +0200 Subject: [PATCH 5/6] include readme.rst as long_description --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 45303cb..a8e6b5b 100644 --- a/setup.py +++ b/setup.py @@ -5,6 +5,7 @@ name='amqpconsumer', version='1.7', description='AMQP event listener', + long_description=open('README.rst').read(), url='https://github.com/ByteInternet/amqpconsumer', author='Byte B.V.', author_email='tech@byte.nl', From 00ab2f6d444e4b88ee11f308f9dbcc369d972dfb Mon Sep 17 00:00:00 2001 From: Andreas Lutro Date: Fri, 6 Apr 2018 11:42:09 +0200 Subject: [PATCH 6/6] update readme --- README.rst | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/README.rst b/README.rst index 48e86c0..0598730 100644 --- a/README.rst +++ b/README.rst @@ -7,33 +7,38 @@ A Python module to facilitate listening for and acting on AMQP events. Usage ----- -Example: +See `example_listener.py `_ for a code snippet. -.. code-block:: python +Development +----------- - import logging +We recommend setting up a virtual environment for the project. All the following commands assume you've activated the virtual environment. - from amqpconsumer.events import EventConsumer +Make sure both module and development dependencies are installed: +.. code-block:: bash - def handler(event): - print "Got event:", event + pip install -e . -r requirements-dev.txt +Run tests: - def main(): - logging.basicConfig(level=logging.INFO) - consumer = EventConsumer('amqp://guest:guest@localhost:5672/%2f', - 'testqueue', - handler) - try: - consumer.run() - except KeyboardInterrupt: - consumer.stop() +.. code-block:: bash + pytest - if __name__ == '__main__': - main() +If you want to generate code coverage statistics: +.. code-block:: bash + + pytest --cov amqpconsumer --cov-report html + +Then open `htmlcov/index.html` in your favorite web browser. + +Run linting and static analysis: + +.. code-block:: bash + + prospector ===== About