1

In pytest-django there is a builtin fixture live_server though it seems like this server (that is actually based on LiveServerTestCase) can't handle web-sockets or at least won't interact with my asgi.py module.

How can one mimic that fixture in order to use ChannelsLiveServerTestCase instead? Or anything else that will run a test-database and will be able to serve an ASGI application?

My goal eventually is to have as close to production environment as possible, for testing and being able to test interaction between different Consumers.

P.S: I know I can run manage.py testserver <Fixture> on another thread / process by overriding django_db_setup though I seek for a better solution.

ניר
  • 1,204
  • 1
  • 8
  • 28

2 Answers2

2

You can implement a channels_live_server fixture based on the implementations of:

@medihack implemented it at https://github.com/pytest-dev/pytest-django/issues/1027:

from functools import partial
from channels.routing import get_default_application
from daphne.testing import DaphneProcess
from django.contrib.staticfiles.handlers import ASGIStaticFilesHandler
from django.core.exceptions import ImproperlyConfigured
from django.db import connections
from django.test.utils import modify_settings


def make_application(*, static_wrapper):
    # Module-level function for pickle-ability
    application = get_default_application()
    if static_wrapper is not None:
        application = static_wrapper(application)
    return application


class ChannelsLiveServer:
    host = "localhost"
    ProtocolServerProcess = DaphneProcess
    static_wrapper = ASGIStaticFilesHandler
    serve_static = True

    def __init__(self) -> None:
        for connection in connections.all():
            if connection.vendor == "sqlite" and connection.is_in_memory_db():
                raise ImproperlyConfigured(
                    "ChannelsLiveServer can not be used with in memory databases"
                )

        self._live_server_modified_settings = modify_settings(ALLOWED_HOSTS={"append": self.host})
        self._live_server_modified_settings.enable()

        get_application = partial(
            make_application,
            static_wrapper=self.static_wrapper if self.serve_static else None,
        )

        self._server_process = self.ProtocolServerProcess(self.host, get_application)
        self._server_process.start()
        self._server_process.ready.wait()
        self._port = self._server_process.port.value

    def stop(self) -> None:
        self._server_process.terminate()
        self._server_process.join()
        self._live_server_modified_settings.disable()

    @property
    def url(self) -> str:
        return f"http://{self.host}:{self._port}"


@pytest.fixture
def channels_live_server(request):
    server = ChannelsLiveServer()
    request.addfinalizer(server.stop)
    return server
aaron
  • 39,695
  • 6
  • 46
  • 102
-1

@aaron's solution can't work, due to pytest-django conservative approach for database access.

another proccess wouldn't be aware that your test has database access permissions therefore you won't have database access. (here is a POC)

Using a scoped fixture of daphne Server suffice for now.

import threading
import time

from functools import partial

from django.contrib.staticfiles.handlers import ASGIStaticFilesHandler
from django.core.exceptions import ImproperlyConfigured
from django.db import connections
from django.test.utils import modify_settings

from daphne.server import Server as DaphneServer
from daphne.endpoints import build_endpoint_description_strings


def get_open_port() -> int:
    import socket
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(("", 0))
    s.listen(1)
    port = s.getsockname()[1]
    s.close()
    return port


def make_application(*, static_wrapper):
    # Module-level function for pickle-ability
    if static_wrapper is not None:
        application = static_wrapper(your_asgi_app)
    return application


class ChannelsLiveServer:
    port = get_open_port()
    host = "localhost"
    static_wrapper = ASGIStaticFilesHandler
    serve_static = True

    def __init__(self) -> None:
        for connection in connections.all():
            if connection.vendor == "sqlite" and connection.is_in_memory_db():
                raise ImproperlyConfigured(
                    "ChannelsLiveServer can not be used with in memory databases"
                )

        self._live_server_modified_settings = modify_settings(ALLOWED_HOSTS={"append": self.host})
        self._live_server_modified_settings.enable()

        get_application = partial(
            make_application,
            static_wrapper=self.static_wrapper if self.serve_static else None,
        )
        endpoints = build_endpoint_description_strings(
            host=self.host, port=self.port
        )

        self._server = DaphneServer(
            application=get_application(),
            endpoints=endpoints
        )
        t = threading.Thread(target=self._server.run)
        t.start()
        for i in range(10):
            time.sleep(0.10)
            if self._server.listening_addresses:
                break
        assert self._server.listening_addresses[0]

    def stop(self) -> None:
        self._server.stop()
        self._live_server_modified_settings.disable()

    @property
    def url(self) -> str:
        return f"ws://{self.host}:{self.port}"

    @property
    def http_url(self):
        return f"http://{self.host}:{self.port}"

@pytest.fixture(scope='session')
def channels_live_server(request, live_server):
    server = ChannelsLiveServer()
    request.addfinalizer(server.stop)
    return server

ניר
  • 1,204
  • 1
  • 8
  • 28
  • Can you share a [minimal, reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) that works with this but not my answer? – aaron Jan 10 '23 at 14:44
  • @aaron "minimal" is, unfortunately, not quite possible with django. – ניר Jan 11 '23 at 07:45
  • @aaron [Here is a POC](https://github.com/nrbnlulu/channels_pytest_liveserver) – ניר Jan 11 '23 at 08:03