From 4a09cec2d436f2b38efc6bd8338c33d60a9be08b Mon Sep 17 00:00:00 2001 From: Tim Watts Date: Wed, 29 Jun 2016 20:26:21 +0200 Subject: [PATCH] Test runserver (#214) * Add tests for runserver and runworker management commands * Fix flake8 and isort errors * Refactor mocking, add comments to tests * rm unneeded vargs --- .coveragerc | 2 +- channels/log.py | 9 +- channels/management/commands/runserver.py | 2 +- channels/management/commands/runworker.py | 2 +- channels/tests/settings.py | 12 ++ channels/tests/test_management.py | 158 ++++++++++++++++++++++ tox.ini | 1 + 7 files changed, 180 insertions(+), 6 deletions(-) create mode 100644 channels/tests/test_management.py diff --git a/.coveragerc b/.coveragerc index 142b3c3..ef6d66c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,6 +1,6 @@ [run] branch = True -source = channels, django.http.response +source = channels omit = channels/tests/* [report] diff --git a/channels/log.py b/channels/log.py index 9078919..581f5b8 100644 --- a/channels/log.py +++ b/channels/log.py @@ -1,14 +1,16 @@ import logging +handler = logging.StreamHandler() + def setup_logger(name, verbosity=1): """ Basic logger for runserver etc. """ - formatter = logging.Formatter(fmt='%(asctime)s - %(levelname)s - %(module)s - %(message)s') + formatter = logging.Formatter( + fmt='%(asctime)s - %(levelname)s - %(module)s - %(message)s') - handler = logging.StreamHandler() handler.setFormatter(formatter) # Set up main logger @@ -22,7 +24,8 @@ def setup_logger(name, verbosity=1): for module in ["daphne.ws_protocol", "daphne.http_protocol"]: daphne_logger = logging.getLogger(module) daphne_logger.addHandler(handler) - daphne_logger.setLevel(logging.DEBUG if verbosity > 1 else logging.INFO) + daphne_logger.setLevel( + logging.DEBUG if verbosity > 1 else logging.INFO) logger.propagate = False return logger diff --git a/channels/management/commands/runserver.py b/channels/management/commands/runserver.py index 26d99fe..49fa7ae 100644 --- a/channels/management/commands/runserver.py +++ b/channels/management/commands/runserver.py @@ -2,6 +2,7 @@ import datetime import sys import threading +from daphne.server import Server from django.conf import settings from django.core.management.commands.runserver import \ Command as RunserverCommand @@ -72,7 +73,6 @@ class Command(RunserverCommand): # actually a subthread under the autoreloader. self.logger.debug("Daphne running, listening on %s:%s", self.addr, self.port) try: - from daphne.server import Server Server( channel_layer=self.channel_layer, host=self.addr, diff --git a/channels/management/commands/runworker.py b/channels/management/commands/runworker.py index 81f80b9..2b77020 100644 --- a/channels/management/commands/runworker.py +++ b/channels/management/commands/runworker.py @@ -5,8 +5,8 @@ from django.core.management import BaseCommand, CommandError from channels import DEFAULT_CHANNEL_LAYER, channel_layers from channels.log import setup_logger -from channels.worker import Worker from channels.staticfiles import StaticFilesConsumer +from channels.worker import Worker class Command(BaseCommand): diff --git a/channels/tests/settings.py b/channels/tests/settings.py index c06b6b4..d720f33 100644 --- a/channels/tests/settings.py +++ b/channels/tests/settings.py @@ -1,5 +1,13 @@ SECRET_KEY = 'cat' +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.admin', + 'channels', +) + DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', @@ -11,6 +19,10 @@ CHANNEL_LAYERS = { 'BACKEND': 'asgiref.inmemory.ChannelLayer', 'ROUTING': [], }, + 'fake_channel': { + 'BACKEND': 'channels.tests.test_management.FakeChannelLayer', + 'ROUTING': [], + } } MIDDLEWARE_CLASSES = [] diff --git a/channels/tests/test_management.py b/channels/tests/test_management.py new file mode 100644 index 0000000..39d8f53 --- /dev/null +++ b/channels/tests/test_management.py @@ -0,0 +1,158 @@ +from __future__ import unicode_literals + +import logging + +from asgiref.inmemory import ChannelLayer +from django.core.management import CommandError, call_command +from channels.staticfiles import StaticFilesConsumer +from django.test import TestCase, mock +from six import StringIO + +from channels.management.commands import runserver + + +class FakeChannelLayer(ChannelLayer): + ''' + Dummy class to bypass the 'inmemory' string check. + ''' + pass + + +@mock.patch('channels.management.commands.runworker.Worker') +class RunWorkerTests(TestCase): + + def setUp(self): + import channels.log + self.stream = StringIO() + channels.log.handler = logging.StreamHandler(self.stream) + + def test_runworker_no_local_only(self, mock_worker): + """ + Runworker should fail with the default "inmemory" worker. + """ + with self.assertRaises(CommandError): + call_command('runworker') + + def test_debug(self, mock_worker): + """ + Test that the StaticFilesConsumer is used in debug mode. + """ + with self.settings(DEBUG=True, STATIC_URL='/static/'): + # Use 'fake_channel' that bypasses the 'inmemory' check + call_command('runworker', '--layer', 'fake_channel') + mock_worker.assert_called_with( + only_channels=None, exclude_channels=None, callback=None, channel_layer=mock.ANY) + + channel_layer = mock_worker.call_args[1]['channel_layer'] + static_consumer = channel_layer.router.root.routing[0].consumer + self.assertIsInstance(static_consumer, StaticFilesConsumer) + + def test_runworker(self, mock_worker): + # Use 'fake_channel' that bypasses the 'inmemory' check + call_command('runworker', '--layer', 'fake_channel') + mock_worker.assert_called_with(callback=None, + only_channels=None, + channel_layer=mock.ANY, + exclude_channels=None) + + def test_runworker_verbose(self, mocked_worker): + # Use 'fake_channel' that bypasses the 'inmemory' check + call_command('runworker', '--layer', + 'fake_channel', '--verbosity', '2') + + # Verify the callback is set + mocked_worker.assert_called_with(callback=mock.ANY, + only_channels=None, + channel_layer=mock.ANY, + exclude_channels=None) + + +class RunServerTests(TestCase): + + def setUp(self): + import channels.log + self.stream = StringIO() + # Capture the logging of the channels moduel to match against the + # output. + channels.log.handler = logging.StreamHandler(self.stream) + + @mock.patch('channels.management.commands.runserver.sys.stdout', new_callable=StringIO) + @mock.patch('channels.management.commands.runserver.Server') + @mock.patch('channels.management.commands.runworker.Worker') + def test_runserver_basic(self, mocked_worker, mocked_server, mock_stdout): + # Django's autoreload util uses threads and this is not needed + # in the test envirionment. + # See: + # https://github.com/django/django/blob/master/django/core/management/commands/runserver.py#L105 + call_command('runserver', '--noreload') + mocked_server.assert_called_with(port=8000, signal_handlers=True, http_timeout=60, + host='127.0.0.1', action_logger=mock.ANY, channel_layer=mock.ANY) + + @mock.patch('channels.management.commands.runserver.sys.stdout', new_callable=StringIO) + @mock.patch('channels.management.commands.runserver.Server') + @mock.patch('channels.management.commands.runworker.Worker') + def test_runserver_debug(self, mocked_worker, mocked_server, mock_stdout): + """ + Test that the server runs with `DEBUG=True`. + """ + # Debug requires the static url is set. + with self.settings(DEBUG=True, STATIC_URL='/static/'): + call_command('runserver', '--noreload') + mocked_server.assert_called_with(port=8000, signal_handlers=True, http_timeout=60, + host='127.0.0.1', action_logger=mock.ANY, channel_layer=mock.ANY) + + call_command('runserver', '--noreload', 'localhost:8001') + mocked_server.assert_called_with(port=8001, signal_handlers=True, http_timeout=60, + host='localhost', action_logger=mock.ANY, channel_layer=mock.ANY) + + self.assertFalse(mocked_worker.called, + "The worker should not be called with '--noworker'") + + @mock.patch('channels.management.commands.runserver.sys.stdout', new_callable=StringIO) + @mock.patch('channels.management.commands.runserver.Server') + @mock.patch('channels.management.commands.runworker.Worker') + def test_runserver_noworker(self, mocked_worker, mocked_server, mock_stdout): + ''' + Test that the Worker is not called when using the `--noworker` parameter. + ''' + call_command('runserver', '--noreload', '--noworker') + mocked_server.assert_called_with(port=8000, signal_handlers=True, http_timeout=60, + host='127.0.0.1', action_logger=mock.ANY, channel_layer=mock.ANY) + self.assertFalse(mocked_worker.called, + "The worker should not be called with '--noworker'") + + @mock.patch('channels.management.commands.runserver.sys.stderr', new_callable=StringIO) + def test_log_action(self, mocked_stderr): + cmd = runserver.Command() + test_actions = [ + (100, 'http', 'complete', + 'HTTP GET /a-path/ 100 [0.12, a-client]'), + (200, 'http', 'complete', + 'HTTP GET /a-path/ 200 [0.12, a-client]'), + (300, 'http', 'complete', + 'HTTP GET /a-path/ 300 [0.12, a-client]'), + (304, 'http', 'complete', + 'HTTP GET /a-path/ 304 [0.12, a-client]'), + (400, 'http', 'complete', + 'HTTP GET /a-path/ 400 [0.12, a-client]'), + (404, 'http', 'complete', + 'HTTP GET /a-path/ 404 [0.12, a-client]'), + (500, 'http', 'complete', + 'HTTP GET /a-path/ 500 [0.12, a-client]'), + (None, 'websocket', 'connected', + 'WebSocket CONNECT /a-path/ [a-client]'), + (None, 'websocket', 'disconnected', + 'WebSocket DISCONNECT /a-path/ [a-client]'), + (None, 'websocket', 'something', ''), # This shouldn't happen + ] + + for status_code, protocol, action, output in test_actions: + details = {'status': status_code, + 'method': 'GET', + 'path': '/a-path/', + 'time_taken': 0.12345, + 'client': 'a-client'} + cmd.log_action(protocol, action, details) + self.assertIn(output, mocked_stderr.getvalue()) + # Clear previous output + mocked_stderr.truncate(0) diff --git a/tox.ini b/tox.ini index 51b3fc2..69a082a 100644 --- a/tox.ini +++ b/tox.ini @@ -13,6 +13,7 @@ setenv = deps = autobahn coverage + daphne asgiref>=0.9 six redis==2.10.5