mirror of
https://github.com/django/daphne.git
synced 2025-07-30 00:49:44 +03:00
Add ChannelLiveServerTestCase (#497)
Adds a new test case base class that fires up a live Daphne server and workers for the test case to use, like Django's LiveServerTestCase.
This commit is contained in:
parent
7383280776
commit
db1b3ba951
|
@ -1,2 +1,3 @@
|
||||||
from .base import TransactionChannelTestCase, ChannelTestCase, Client, apply_routes # NOQA isort:skip
|
from .base import TransactionChannelTestCase, ChannelTestCase, Client, apply_routes # NOQA isort:skip
|
||||||
from .http import HttpClient # NOQA isort:skip
|
from .http import HttpClient # NOQA isort:skip
|
||||||
|
from .liveserver import ChannelLiveServerTestCase # NOQA isort:skip
|
||||||
|
|
228
channels/test/liveserver.py
Normal file
228
channels/test/liveserver.py
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
import multiprocessing
|
||||||
|
|
||||||
|
import django
|
||||||
|
from daphne.server import Server
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
from django.db import connections
|
||||||
|
from django.db.utils import load_backend
|
||||||
|
from django.test.testcases import TransactionTestCase
|
||||||
|
from django.test.utils import modify_settings, override_settings
|
||||||
|
from twisted.internet import reactor
|
||||||
|
|
||||||
|
from .. import DEFAULT_CHANNEL_LAYER
|
||||||
|
from ..asgi import ChannelLayerManager
|
||||||
|
from ..staticfiles import StaticFilesConsumer
|
||||||
|
from ..worker import Worker, WorkerGroup
|
||||||
|
|
||||||
|
# NOTE: We use ChannelLayerManager to prevent layer instance sharing
|
||||||
|
# between forked process. Some layers implementations create
|
||||||
|
# connections inside the __init__ method. After forking child
|
||||||
|
# processes can lose the ability to use this connection and typically
|
||||||
|
# stuck on some network operation. To prevent this we use new
|
||||||
|
# ChannelLayerManager each time we want to initiate default layer.
|
||||||
|
# This gives us guaranty that new layer instance will be created and
|
||||||
|
# new connection will be established.
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessSetup(multiprocessing.Process):
|
||||||
|
"""Common initialization steps for test subprocess."""
|
||||||
|
|
||||||
|
def common_setup(self):
|
||||||
|
|
||||||
|
self.setup_django()
|
||||||
|
self.setup_databases()
|
||||||
|
self.override_settings()
|
||||||
|
|
||||||
|
def setup_django(self):
|
||||||
|
|
||||||
|
if django.VERSION >= (1, 10):
|
||||||
|
django.setup(set_prefix=False)
|
||||||
|
else:
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
def setup_databases(self):
|
||||||
|
|
||||||
|
for alias, db in self.databases.items():
|
||||||
|
backend = load_backend(db['ENGINE'])
|
||||||
|
conn = backend.DatabaseWrapper(db, alias)
|
||||||
|
if django.VERSION >= (1, 9):
|
||||||
|
connections[alias].creation.set_as_test_mirror(
|
||||||
|
conn.settings_dict,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
test_db_name = conn.settings_dict['NAME']
|
||||||
|
connections[alias].settings_dict['NAME'] = test_db_name
|
||||||
|
|
||||||
|
def override_settings(self):
|
||||||
|
|
||||||
|
if self.overridden_settings:
|
||||||
|
overridden = override_settings(**self.overridden_settings)
|
||||||
|
overridden.enable()
|
||||||
|
|
||||||
|
if self.modified_settings:
|
||||||
|
modified = modify_settings(self.modified_settings)
|
||||||
|
modified.enable()
|
||||||
|
|
||||||
|
|
||||||
|
class WorkerProcess(ProcessSetup):
|
||||||
|
|
||||||
|
def __init__(self, is_ready, n_threads, overridden_settings,
|
||||||
|
modified_settings, databases):
|
||||||
|
|
||||||
|
self.is_ready = is_ready
|
||||||
|
self.n_threads = n_threads
|
||||||
|
self.overridden_settings = overridden_settings
|
||||||
|
self.modified_settings = modified_settings
|
||||||
|
self.databases = databases
|
||||||
|
super(WorkerProcess, self).__init__()
|
||||||
|
self.daemon = True
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.common_setup()
|
||||||
|
channel_layers = ChannelLayerManager()
|
||||||
|
channel_layers[DEFAULT_CHANNEL_LAYER].router.check_default(
|
||||||
|
http_consumer=StaticFilesConsumer(),
|
||||||
|
)
|
||||||
|
if self.n_threads == 1:
|
||||||
|
self.worker = Worker(
|
||||||
|
channel_layer=channel_layers[DEFAULT_CHANNEL_LAYER],
|
||||||
|
signal_handlers=False,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.worker = WorkerGroup(
|
||||||
|
channel_layer=channel_layers[DEFAULT_CHANNEL_LAYER],
|
||||||
|
signal_handlers=False,
|
||||||
|
n_threads=self.n_threads,
|
||||||
|
)
|
||||||
|
self.worker.ready()
|
||||||
|
self.is_ready.set()
|
||||||
|
self.worker.run()
|
||||||
|
except Exception:
|
||||||
|
self.is_ready.set()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
class DaphneProcess(ProcessSetup):
|
||||||
|
|
||||||
|
def __init__(self, host, port_storage, is_ready, overridden_settings,
|
||||||
|
modified_settings, databases):
|
||||||
|
|
||||||
|
self.host = host
|
||||||
|
self.port_storage = port_storage
|
||||||
|
self.is_ready = is_ready
|
||||||
|
self.overridden_settings = overridden_settings
|
||||||
|
self.modified_settings = modified_settings
|
||||||
|
self.databases = databases
|
||||||
|
super(DaphneProcess, self).__init__()
|
||||||
|
self.daemon = True
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.common_setup()
|
||||||
|
channel_layers = ChannelLayerManager()
|
||||||
|
self.server = Server(
|
||||||
|
channel_layer=channel_layers[DEFAULT_CHANNEL_LAYER],
|
||||||
|
endpoints=['tcp:interface=%s:port=0' % (self.host)],
|
||||||
|
signal_handlers=False,
|
||||||
|
)
|
||||||
|
reactor.callLater(0.5, self.resolve_port)
|
||||||
|
self.server.run()
|
||||||
|
except Exception:
|
||||||
|
self.is_ready.set()
|
||||||
|
raise
|
||||||
|
|
||||||
|
def resolve_port(self):
|
||||||
|
|
||||||
|
port = self.server.listeners[0].result.getHost().port
|
||||||
|
self.port_storage.value = port
|
||||||
|
self.is_ready.set()
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelLiveServerTestCase(TransactionTestCase):
|
||||||
|
"""
|
||||||
|
Does basically the same as TransactionTestCase but also launches a
|
||||||
|
live Daphne server and Channels worker in a separate process, so
|
||||||
|
that the tests may use another test framework, such as Selenium,
|
||||||
|
instead of the built-in dummy client.
|
||||||
|
"""
|
||||||
|
|
||||||
|
host = 'localhost'
|
||||||
|
ProtocolServerProcess = DaphneProcess
|
||||||
|
WorkerProcess = WorkerProcess
|
||||||
|
worker_threads = 1
|
||||||
|
|
||||||
|
@property
|
||||||
|
def live_server_url(self):
|
||||||
|
|
||||||
|
return 'http://%s:%s' % (self.host, self._port_storage.value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def live_server_ws_url(self):
|
||||||
|
|
||||||
|
return 'ws://%s:%s' % (self.host, self._port_storage.value)
|
||||||
|
|
||||||
|
def _pre_setup(self):
|
||||||
|
|
||||||
|
for connection in connections.all():
|
||||||
|
if self._is_in_memory_db(connection):
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
'ChannelLiveServerTestCase can not be used with in memory databases'
|
||||||
|
)
|
||||||
|
|
||||||
|
channel_layers = ChannelLayerManager()
|
||||||
|
if len(channel_layers.configs) > 1:
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
'ChannelLiveServerTestCase does not support multiple CHANNEL_LAYERS at this time'
|
||||||
|
)
|
||||||
|
|
||||||
|
channel_layer = channel_layers[DEFAULT_CHANNEL_LAYER]
|
||||||
|
if 'flush' in channel_layer.extensions:
|
||||||
|
channel_layer.flush()
|
||||||
|
|
||||||
|
super(ChannelLiveServerTestCase, self)._pre_setup()
|
||||||
|
|
||||||
|
server_ready = multiprocessing.Event()
|
||||||
|
self._port_storage = multiprocessing.Value('i')
|
||||||
|
self._server_process = self.ProtocolServerProcess(
|
||||||
|
self.host,
|
||||||
|
self._port_storage,
|
||||||
|
server_ready,
|
||||||
|
self._overridden_settings,
|
||||||
|
self._modified_settings,
|
||||||
|
connections.databases,
|
||||||
|
)
|
||||||
|
self._server_process.start()
|
||||||
|
server_ready.wait()
|
||||||
|
|
||||||
|
worker_ready = multiprocessing.Event()
|
||||||
|
self._worker_process = self.WorkerProcess(
|
||||||
|
worker_ready,
|
||||||
|
self.worker_threads,
|
||||||
|
self._overridden_settings,
|
||||||
|
self._modified_settings,
|
||||||
|
connections.databases,
|
||||||
|
)
|
||||||
|
self._worker_process.start()
|
||||||
|
worker_ready.wait()
|
||||||
|
|
||||||
|
def _post_teardown(self):
|
||||||
|
|
||||||
|
self._server_process.terminate()
|
||||||
|
self._server_process.join()
|
||||||
|
self._worker_process.terminate()
|
||||||
|
self._worker_process.join()
|
||||||
|
super(ChannelLiveServerTestCase, self)._post_teardown()
|
||||||
|
|
||||||
|
def _is_in_memory_db(self, connection):
|
||||||
|
"""Check if DatabaseWrapper holds in memory database."""
|
||||||
|
|
||||||
|
if connection.vendor == 'sqlite':
|
||||||
|
if django.VERSION >= (1, 11):
|
||||||
|
return connection.is_in_memory_db()
|
||||||
|
else:
|
||||||
|
return connection.is_in_memory_db(
|
||||||
|
connection.settings_dict['NAME'],
|
||||||
|
)
|
|
@ -5,5 +5,4 @@ warnings.warn(
|
||||||
DeprecationWarning,
|
DeprecationWarning,
|
||||||
)
|
)
|
||||||
|
|
||||||
from channels.test.base import TransactionChannelTestCase, ChannelTestCase, Client, apply_routes # NOQA isort:skip
|
from channels.test import * # NOQA isort:skip
|
||||||
from channels.test.http import HttpClient # NOQA isort:skip
|
|
||||||
|
|
|
@ -18,7 +18,6 @@ Major Changes
|
||||||
work but will trigger a deprecation warning, and ``channels.tests`` will be
|
work but will trigger a deprecation warning, and ``channels.tests`` will be
|
||||||
removed completely in version 1.3.
|
removed completely in version 1.3.
|
||||||
|
|
||||||
|
|
||||||
Minor Changes & Bugfixes
|
Minor Changes & Bugfixes
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
|
|
|
@ -294,3 +294,34 @@ mocked.
|
||||||
|
|
||||||
You can pass an ``alias`` argument to ``get_next_message``, ``Client`` and ``Channel``
|
You can pass an ``alias`` argument to ``get_next_message``, ``Client`` and ``Channel``
|
||||||
to use a different layer too.
|
to use a different layer too.
|
||||||
|
|
||||||
|
Live Server Test Case
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
You can use browser automation libraries like Selenium or Splinter to
|
||||||
|
check your application against real layer installation. Use
|
||||||
|
``ChannelLiveServerTestCase`` for your acceptance tests.
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from channels.test import ChannelLiveServerTestCase
|
||||||
|
from splinter import Browser
|
||||||
|
|
||||||
|
class IntegrationTest(ChannelLiveServerTestCase):
|
||||||
|
|
||||||
|
def test_browse_site_index(self):
|
||||||
|
|
||||||
|
with Browser() as browser:
|
||||||
|
|
||||||
|
browser.visit(self.live_server_url)
|
||||||
|
# the rest of your integration test...
|
||||||
|
|
||||||
|
In the test above Daphne and Channels worker processes were fired up.
|
||||||
|
These processes run your project against the test database and the
|
||||||
|
default channel layer you spacify in the settings. If channel layer
|
||||||
|
support ``flush`` extension, initial cleanup will be done. So do not
|
||||||
|
run this code against your production environment. When channels
|
||||||
|
infrastructure is ready default web browser will be also started. You
|
||||||
|
can open your website in the real browser which can execute JavaScript
|
||||||
|
and operate on WebSockets. ``live_server_ws_url`` property is also
|
||||||
|
provided if you decide to run messaging directly from Python.
|
||||||
|
|
|
@ -10,6 +10,7 @@ if __name__ == "__main__":
|
||||||
os.environ['DJANGO_SETTINGS_MODULE'] = "tests.settings"
|
os.environ['DJANGO_SETTINGS_MODULE'] = "tests.settings"
|
||||||
django.setup()
|
django.setup()
|
||||||
TestRunner = get_runner(settings)
|
TestRunner = get_runner(settings)
|
||||||
|
tests = sys.argv[1:] or ["tests"]
|
||||||
test_runner = TestRunner()
|
test_runner = TestRunner()
|
||||||
failures = test_runner.run_tests(["tests"])
|
failures = test_runner.run_tests(tests)
|
||||||
sys.exit(bool(failures))
|
sys.exit(bool(failures))
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -22,7 +22,7 @@ setup(
|
||||||
'mock ; python_version < "3.0"',
|
'mock ; python_version < "3.0"',
|
||||||
'flake8>=2.0,<3.0',
|
'flake8>=2.0,<3.0',
|
||||||
'isort',
|
'isort',
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 5 - Production/Stable',
|
'Development Status :: 5 - Production/Stable',
|
||||||
|
|
|
@ -2,5 +2,6 @@ from channels.sessions import enforce_ordering
|
||||||
|
|
||||||
|
|
||||||
def ws_message(message):
|
def ws_message(message):
|
||||||
"Echoes messages back to the client"
|
"Echoes messages back to the client."
|
||||||
|
|
||||||
message.reply_channel.send({'text': message['text']})
|
message.reply_channel.send({'text': message['text']})
|
||||||
|
|
|
@ -2,4 +2,5 @@ from django.http import HttpResponse
|
||||||
|
|
||||||
|
|
||||||
def index(request):
|
def index(request):
|
||||||
|
|
||||||
return HttpResponse("OK")
|
return HttpResponse("OK")
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
from setuptools import setup
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='channels-benchmark',
|
name='channels-benchmark',
|
||||||
|
packages=find_packages(),
|
||||||
py_modules=['benchmark'],
|
py_modules=['benchmark'],
|
||||||
install_requires=['autobahn', 'Twisted'],
|
install_requires=[
|
||||||
|
'autobahn',
|
||||||
|
'Twisted',
|
||||||
|
'statistics ; python_version < "3.0"',
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
#Blank on purpose
|
# Blank on purpose
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
@ -23,5 +24,10 @@ DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||||
}
|
'TEST': {
|
||||||
|
'NAME': os.path.join(BASE_DIR, 'test_db.sqlite3'),
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = ['*']
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
# Settings for channels specifically
|
# Settings for channels specifically
|
||||||
from testproject.settings.base import *
|
from testproject.settings.base import *
|
||||||
|
|
||||||
|
|
||||||
INSTALLED_APPS += (
|
INSTALLED_APPS += (
|
||||||
'channels',
|
'channels',
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
# Settings for channels specifically
|
# Settings for channels specifically
|
||||||
from testproject.settings.base import *
|
from testproject.settings.base import *
|
||||||
|
|
||||||
INSTALLED_APPS += ('channels',)
|
INSTALLED_APPS += (
|
||||||
|
'channels',
|
||||||
|
)
|
||||||
|
|
||||||
CHANNEL_LAYERS = {
|
CHANNEL_LAYERS = {
|
||||||
'default': {
|
'default': {
|
||||||
'BACKEND': 'asgi_rabbitmq.RabbitmqChannelLayer',
|
'BACKEND': 'asgi_rabbitmq.RabbitmqChannelLayer',
|
||||||
'ROUTING': 'testproject.urls.channel_routing',
|
'ROUTING': 'testproject.urls.channel_routing',
|
||||||
'CONFIG': {
|
'CONFIG': {
|
||||||
'url': os.environ['RABBITMQ_URL'],
|
'url': os.environ.get(
|
||||||
|
'RABBITMQ_URL',
|
||||||
|
'amqp://guest:guest@localhost:5672/%2F',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ CHANNEL_LAYERS = {
|
||||||
'fake_channel': {
|
'fake_channel': {
|
||||||
'BACKEND': 'tests.test_management.FakeChannelLayer',
|
'BACKEND': 'tests.test_management.FakeChannelLayer',
|
||||||
'ROUTING': [],
|
'ROUTING': [],
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
MIDDLEWARE_CLASSES = []
|
MIDDLEWARE_CLASSES = []
|
||||||
|
|
2
tox.ini
2
tox.ini
|
@ -14,4 +14,4 @@ deps =
|
||||||
commands =
|
commands =
|
||||||
flake8: flake8
|
flake8: flake8
|
||||||
isort: isort --check-only --recursive channels
|
isort: isort --check-only --recursive channels
|
||||||
django: coverage run --parallel-mode {toxinidir}/runtests.py
|
django: coverage run --parallel-mode {toxinidir}/runtests.py {posargs}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user