Skip to content

Commit cd44771

Browse files
dee077carltongibson
andcommitted
Made ChannelsLiveServerTestCase compatible with Django 5.2.
Closes #2148. - Updates ChannelsLiveServerTestCase for Django 5.2+ - Adds selenium test project. Co-authored-by: Carlton Gibson <[email protected]> Co-authored-by: @devkral
1 parent b72da7f commit cd44771

File tree

27 files changed

+816
-48
lines changed

27 files changed

+816
-48
lines changed

.github/workflows/tests.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ jobs:
2828
with:
2929
python-version: ${{ matrix.python-version }}
3030

31+
- name: Set up Chrome
32+
uses: browser-actions/setup-chrome@v1
33+
34+
- name: Set up ChromeDriver
35+
uses: nanasess/setup-chromedriver@v2
36+
3137
- name: Install dependencies
3238
run: |
3339
python -m pip install --upgrade pip wheel setuptools

channels/testing/live.py

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,36 +39,44 @@ def live_server_url(self):
3939
def live_server_ws_url(self):
4040
return "ws://%s:%s" % (self.host, self._port)
4141

42-
def _pre_setup(self):
42+
@classmethod
43+
def setUpClass(cls):
4344
for connection in connections.all():
44-
if self._is_in_memory_db(connection):
45+
if cls._is_in_memory_db(connection):
4546
raise ImproperlyConfigured(
4647
"ChannelLiveServerTestCase can not be used with in memory databases"
4748
)
4849

49-
super(ChannelsLiveServerTestCase, self)._pre_setup()
50+
super().setUpClass()
5051

51-
self._live_server_modified_settings = modify_settings(
52-
ALLOWED_HOSTS={"append": self.host}
52+
cls._live_server_modified_settings = modify_settings(
53+
ALLOWED_HOSTS={"append": cls.host}
5354
)
54-
self._live_server_modified_settings.enable()
55+
cls._live_server_modified_settings.enable()
5556

5657
get_application = partial(
5758
make_application,
58-
static_wrapper=self.static_wrapper if self.serve_static else None,
59+
static_wrapper=cls.static_wrapper if cls.serve_static else None,
5960
)
60-
self._server_process = self.ProtocolServerProcess(self.host, get_application)
61-
self._server_process.start()
62-
self._server_process.ready.wait()
63-
self._port = self._server_process.port.value
61+
cls._server_process = cls.ProtocolServerProcess(cls.host, get_application)
62+
cls._server_process.start()
63+
while True:
64+
if not cls._server_process.ready.wait(timeout=1):
65+
if cls._server_process.is_alive():
66+
continue
67+
raise RuntimeError("Server stopped") from None
68+
break
69+
cls._port = cls._server_process.port.value
6470

65-
def _post_teardown(self):
66-
self._server_process.terminate()
67-
self._server_process.join()
68-
self._live_server_modified_settings.disable()
69-
super(ChannelsLiveServerTestCase, self)._post_teardown()
71+
@classmethod
72+
def tearDownClass(cls):
73+
cls._server_process.terminate()
74+
cls._server_process.join()
75+
cls._live_server_modified_settings.disable()
76+
super().tearDownClass()
7077

71-
def _is_in_memory_db(self, connection):
78+
@classmethod
79+
def _is_in_memory_db(cls, connection):
7280
"""
7381
Check if DatabaseWrapper holds in memory database.
7482
"""

setup.cfg

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ tests =
4242
pytest
4343
pytest-django
4444
pytest-asyncio
45+
selenium
4546
daphne =
4647
daphne>=4.0.0
4748

@@ -53,6 +54,8 @@ exclude =
5354
exclude = venv/*,tox/*,docs/*,testproject/*,build/*
5455
max-line-length = 88
5556
extend-ignore = E203, W503
57+
per-file-ignores =
58+
tests/sample_project/config/asgi.py:E402
5659

5760
[isort]
5861
profile = black

tests/conftest.py

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,12 @@
1+
import os
2+
13
import pytest
24
from django.conf import settings
35

46

57
def pytest_configure():
6-
settings.configure(
7-
DATABASES={
8-
"default": {
9-
"ENGINE": "django.db.backends.sqlite3",
10-
# Override Django’s default behaviour of using an in-memory database
11-
# in tests for SQLite, since that avoids connection.close() working.
12-
"TEST": {"NAME": "test_db.sqlite3"},
13-
}
14-
},
15-
INSTALLED_APPS=[
16-
"django.contrib.auth",
17-
"django.contrib.contenttypes",
18-
"django.contrib.sessions",
19-
"django.contrib.admin",
20-
"channels",
21-
],
22-
SECRET_KEY="Not_a_secret_key",
23-
)
8+
os.environ["DJANGO_SETTINGS_MODULE"] = "tests.sample_project.config.settings"
9+
settings._setup()
2410

2511

2612
def pytest_generate_tests(metafunc):

tests/sample_project/__init__.py

Whitespace-only changes.

tests/sample_project/config/__init__.py

Whitespace-only changes.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""
2+
ASGI config for sample_project project.
3+
4+
It exposes the ASGI callable as a module-level variable named ``application``.
5+
6+
For more information on this file, see
7+
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
8+
"""
9+
10+
from django.core.asgi import get_asgi_application
11+
from django.urls import path
12+
13+
application = get_asgi_application()
14+
15+
from channels.auth import AuthMiddlewareStack
16+
from channels.routing import ProtocolTypeRouter, URLRouter
17+
from channels.security.websocket import AllowedHostsOriginValidator
18+
from tests.sample_project.sampleapp.consumers import LiveMessageConsumer
19+
20+
application = ProtocolTypeRouter(
21+
{
22+
"websocket": AllowedHostsOriginValidator(
23+
AuthMiddlewareStack(
24+
URLRouter(
25+
[
26+
path(
27+
"ws/message/",
28+
LiveMessageConsumer.as_asgi(),
29+
name="live_message_counter",
30+
),
31+
]
32+
)
33+
)
34+
),
35+
"http": application,
36+
}
37+
)
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
from pathlib import Path
2+
3+
BASE_DIR = Path(__file__).resolve().parent.parent
4+
5+
SECRET_KEY = "Not_a_secret_key"
6+
7+
DEBUG = True
8+
9+
ALLOWED_HOSTS = []
10+
11+
INSTALLED_APPS = [
12+
"daphne",
13+
"django.contrib.admin",
14+
"django.contrib.auth",
15+
"django.contrib.contenttypes",
16+
"django.contrib.sessions",
17+
"django.contrib.messages",
18+
"django.contrib.staticfiles",
19+
"tests.sample_project.sampleapp",
20+
"channels",
21+
]
22+
23+
MIDDLEWARE = [
24+
"django.middleware.security.SecurityMiddleware",
25+
"django.contrib.sessions.middleware.SessionMiddleware",
26+
"django.middleware.common.CommonMiddleware",
27+
"django.middleware.csrf.CsrfViewMiddleware",
28+
"django.contrib.auth.middleware.AuthenticationMiddleware",
29+
"django.contrib.messages.middleware.MessageMiddleware",
30+
"django.middleware.clickjacking.XFrameOptionsMiddleware",
31+
]
32+
33+
ROOT_URLCONF = "tests.sample_project.config.urls"
34+
35+
TEMPLATES = [
36+
{
37+
"BACKEND": "django.template.backends.django.DjangoTemplates",
38+
"DIRS": [],
39+
"APP_DIRS": True,
40+
"OPTIONS": {
41+
"context_processors": [
42+
"django.template.context_processors.csrf",
43+
"django.template.context_processors.request",
44+
"django.contrib.auth.context_processors.auth",
45+
"django.contrib.messages.context_processors.messages",
46+
],
47+
},
48+
},
49+
]
50+
51+
WSGI_APPLICATION = "tests.sample_project.config.wsgi.application"
52+
ASGI_APPLICATION = "tests.sample_project.config.asgi.application"
53+
54+
CHANNEL_LAYERS = {
55+
"default": {
56+
"BACKEND": "channels.layers.InMemoryChannelLayer",
57+
},
58+
}
59+
60+
DATABASES = {
61+
"default": {
62+
"ENGINE": "django.db.backends.sqlite3",
63+
"NAME": BASE_DIR / "sampleapp/sampleapp.sqlite3",
64+
# Override Django’s default behaviour of using an in-memory database
65+
# in tests for SQLite, since that avoids connection.close() working.
66+
"TEST": {"NAME": "test_db.sqlite3"},
67+
}
68+
}
69+
70+
71+
AUTH_PASSWORD_VALIDATORS = [
72+
{
73+
"NAME": (
74+
"django.contrib.auth.password_validation."
75+
"UserAttributeSimilarityValidator"
76+
),
77+
},
78+
{
79+
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
80+
},
81+
{
82+
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
83+
},
84+
{
85+
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
86+
},
87+
]
88+
89+
LANGUAGE_CODE = "en-us"
90+
91+
TIME_ZONE = "UTC"
92+
93+
USE_I18N = True
94+
95+
USE_TZ = True
96+
97+
STATIC_URL = "static/"
98+
99+
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""
2+
URL configuration for sample_project project.
3+
4+
The `urlpatterns` list routes URLs to views. For more information please see:
5+
https://docs.djangoproject.com/en/5.2/topics/http/urls/
6+
Examples:
7+
Function views
8+
1. Add an import: from my_app import views
9+
2. Add a URL to urlpatterns: path('', views.home, name='home')
10+
Class-based views
11+
1. Add an import: from other_app.views import Home
12+
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
13+
Including another URLconf
14+
1. Import the include() function: from django.urls import include, path
15+
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
16+
"""
17+
18+
from django.conf import settings
19+
from django.contrib import admin
20+
from django.urls import path
21+
from django.views.generic import RedirectView
22+
23+
urlpatterns = [
24+
path("admin/", admin.site.urls),
25+
path(
26+
"favicon.ico",
27+
RedirectView.as_view(
28+
url=settings.STATIC_URL + "sampleapp/images/django.svg", permanent=True
29+
),
30+
),
31+
]
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""
2+
WSGI config for sample_project project.
3+
4+
It exposes the WSGI callable as a module-level variable named ``application``.
5+
6+
For more information on this file, see
7+
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
8+
"""
9+
10+
import os
11+
12+
from django.core.wsgi import get_wsgi_application
13+
14+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
15+
16+
application = get_wsgi_application()

0 commit comments

Comments
 (0)