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..f8bb080 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,23 @@ +branches: + only: + - master + +language: python + +python: + - '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/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 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/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(): 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/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', diff --git a/tests/unit_test.py b/tests/unit_test.py new file mode 100644 index 0000000..31aa291 --- /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 = b'{"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()