Skip to content

Commit a9a66a9

Browse files
authored
Add async tests (#1835)
* Add asynchronous examples (#1819) * Add tests for async usage (#1819) * Include daphne is requirements_dev
1 parent 25656ee commit a9a66a9

File tree

11 files changed

+181
-2
lines changed

11 files changed

+181
-2
lines changed

example/README.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,13 @@ environment variable::
4646

4747
$ DB_BACKEND=postgresql python example/manage.py migrate
4848
$ DB_BACKEND=postgresql python example/manage.py runserver
49+
50+
Using an asynchronous (ASGI) server:
51+
52+
Install [Daphne](https://pypi.org/project/daphne/) first:
53+
54+
$ python -m pip install daphne
55+
56+
Then run the Django development server:
57+
58+
$ ASYNC_SERVER=true python example/manage.py runserver

example/asgi.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""ASGI config for example project."""
2+
3+
import os
4+
5+
from django.core.asgi import get_asgi_application
6+
7+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings")
8+
9+
application = get_asgi_application()

example/settings.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
# Application definition
1919

2020
INSTALLED_APPS = [
21+
*(["daphne"] if os.getenv("ASYNC_SERVER", False) else []), # noqa: FBT003
2122
"django.contrib.admin",
2223
"django.contrib.auth",
2324
"django.contrib.contenttypes",
@@ -66,6 +67,7 @@
6667
USE_TZ = True
6768

6869
WSGI_APPLICATION = "example.wsgi.application"
70+
ASGI_APPLICATION = "example.asgi.application"
6971

7072

7173
# Cache and database
@@ -103,7 +105,6 @@
103105

104106
STATICFILES_DIRS = [os.path.join(BASE_DIR, "example", "static")]
105107

106-
107108
# Only enable the toolbar when we're in debug mode and we're
108109
# not running tests. Django will change DEBUG to be False for
109110
# tests, so we can't rely on DEBUG alone.

example/templates/async_db.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta http-equiv="content-type" content="text/html; charset=utf-8">
5+
<title>Async DB</title>
6+
</head>
7+
<body>
8+
<h1>Async DB</h1>
9+
<p>
10+
<span>Value </span>
11+
<span>{{ user_count }}</span>
12+
</p>
13+
</body>
14+
</html>

example/urls.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@
33
from django.views.generic import TemplateView
44

55
from debug_toolbar.toolbar import debug_toolbar_urls
6-
from example.views import increment, jinja2_view
6+
from example.views import (
7+
async_db,
8+
async_db_concurrent,
9+
async_home,
10+
increment,
11+
jinja2_view,
12+
)
713

814
urlpatterns = [
915
path("", TemplateView.as_view(template_name="index.html"), name="home"),
@@ -13,6 +19,9 @@
1319
name="bad_form",
1420
),
1521
path("jinja/", jinja2_view, name="jinja"),
22+
path("async/", async_home, name="async_home"),
23+
path("async/db/", async_db, name="async_db"),
24+
path("async/db-concurrent/", async_db_concurrent, name="async_db_concurrent"),
1625
path("jquery/", TemplateView.as_view(template_name="jquery/index.html")),
1726
path("mootools/", TemplateView.as_view(template_name="mootools/index.html")),
1827
path("prototype/", TemplateView.as_view(template_name="prototype/index.html")),

example/views.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import asyncio
2+
3+
from asgiref.sync import sync_to_async
4+
from django.contrib.auth.models import User
15
from django.http import JsonResponse
26
from django.shortcuts import render
37

@@ -13,3 +17,26 @@ def increment(request):
1317

1418
def jinja2_view(request):
1519
return render(request, "index.jinja", {"foo": "bar"}, using="jinja2")
20+
21+
22+
async def async_home(request):
23+
return await sync_to_async(render)(request, "index.html")
24+
25+
26+
async def async_db(request):
27+
user_count = await User.objects.acount()
28+
29+
return await sync_to_async(render)(
30+
request, "async_db.html", {"user_count": user_count}
31+
)
32+
33+
34+
async def async_db_concurrent(request):
35+
# Do database queries concurrently
36+
(user_count, _) = await asyncio.gather(
37+
User.objects.acount(), User.objects.filter(username="test").acount()
38+
)
39+
40+
return await sync_to_async(render)(
41+
request, "async_db.html", {"user_count": user_count}
42+
)

requirements_dev.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ selenium
1212
tox
1313
black
1414

15+
# Integration support
16+
17+
daphne # async in Example app
18+
1519
# Documentation
1620

1721
Sphinx

tests/panels/test_sql.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,20 @@ def sql_call(*, use_iterator=False):
3232
return list(qs)
3333

3434

35+
async def async_sql_call(*, use_iterator=False):
36+
qs = User.objects.all()
37+
if use_iterator:
38+
qs = qs.iterator()
39+
return await sync_to_async(list)(qs)
40+
41+
42+
async def concurrent_async_sql_call(*, use_iterator=False):
43+
qs = User.objects.all()
44+
if use_iterator:
45+
qs = qs.iterator()
46+
return await asyncio.gather(sync_to_async(list)(qs), User.objects.acount())
47+
48+
3549
class SQLPanelTestCase(BaseTestCase):
3650
panel_id = "SQLPanel"
3751

@@ -57,6 +71,38 @@ def test_recording(self):
5771
# ensure the stacktrace is populated
5872
self.assertTrue(len(query["stacktrace"]) > 0)
5973

74+
async def test_recording_async(self):
75+
self.assertEqual(len(self.panel._queries), 0)
76+
77+
await async_sql_call()
78+
79+
# ensure query was logged
80+
self.assertEqual(len(self.panel._queries), 1)
81+
query = self.panel._queries[0]
82+
self.assertEqual(query["alias"], "default")
83+
self.assertTrue("sql" in query)
84+
self.assertTrue("duration" in query)
85+
self.assertTrue("stacktrace" in query)
86+
87+
# ensure the stacktrace is populated
88+
self.assertTrue(len(query["stacktrace"]) > 0)
89+
90+
async def test_recording_concurrent_async(self):
91+
self.assertEqual(len(self.panel._queries), 0)
92+
93+
await concurrent_async_sql_call()
94+
95+
# ensure query was logged
96+
self.assertEqual(len(self.panel._queries), 2)
97+
query = self.panel._queries[0]
98+
self.assertEqual(query["alias"], "default")
99+
self.assertTrue("sql" in query)
100+
self.assertTrue("duration" in query)
101+
self.assertTrue("stacktrace" in query)
102+
103+
# ensure the stacktrace is populated
104+
self.assertTrue(len(query["stacktrace"]) > 0)
105+
60106
@unittest.skipUnless(
61107
connection.vendor == "postgresql", "Test valid only on PostgreSQL"
62108
)

tests/test_integration.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,24 @@ def test_data_gone(self):
250250
)
251251
self.assertIn("Please reload the page and retry.", response.json()["content"])
252252

253+
def test_sql_page(self):
254+
response = self.client.get("/execute_sql/")
255+
self.assertEqual(
256+
len(response.toolbar.get_panel_by_id("SQLPanel").get_stats()["queries"]), 1
257+
)
258+
259+
def test_async_sql_page(self):
260+
response = self.client.get("/async_execute_sql/")
261+
self.assertEqual(
262+
len(response.toolbar.get_panel_by_id("SQLPanel").get_stats()["queries"]), 1
263+
)
264+
265+
def test_concurrent_async_sql_page(self):
266+
response = self.client.get("/async_execute_sql_concurrently/")
267+
self.assertEqual(
268+
len(response.toolbar.get_panel_by_id("SQLPanel").get_stats()["queries"]), 2
269+
)
270+
253271

254272
@override_settings(DEBUG=True)
255273
class DebugToolbarIntegrationTestCase(IntegrationTestCase):
@@ -843,3 +861,29 @@ def test_theme_toggle(self):
843861
self.get("/regular/basic/")
844862
toolbar = self.selenium.find_element(By.ID, "djDebug")
845863
self.assertEqual(toolbar.get_attribute("data-theme"), "light")
864+
865+
def test_async_sql_action(self):
866+
self.get("/async_execute_sql/")
867+
self.selenium.find_element(By.ID, "SQLPanel")
868+
self.selenium.find_element(By.ID, "djDebugWindow")
869+
870+
# Click to show the SQL panel
871+
self.selenium.find_element(By.CLASS_NAME, "SQLPanel").click()
872+
873+
# SQL panel loads
874+
self.wait.until(
875+
EC.visibility_of_element_located((By.CSS_SELECTOR, ".remoteCall"))
876+
)
877+
878+
def test_concurrent_async_sql_action(self):
879+
self.get("/async_execute_sql_concurrently/")
880+
self.selenium.find_element(By.ID, "SQLPanel")
881+
self.selenium.find_element(By.ID, "djDebugWindow")
882+
883+
# Click to show the SQL panel
884+
self.selenium.find_element(By.CLASS_NAME, "SQLPanel").click()
885+
886+
# SQL panel loads
887+
self.wait.until(
888+
EC.visibility_of_element_located((By.CSS_SELECTOR, ".remoteCall"))
889+
)

tests/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
path("non_ascii_request/", views.regular_view, {"title": NonAsciiRepr()}),
1818
path("new_user/", views.new_user),
1919
path("execute_sql/", views.execute_sql),
20+
path("async_execute_sql/", views.async_execute_sql),
21+
path("async_execute_sql_concurrently/", views.async_execute_sql_concurrently),
2022
path("cached_view/", views.cached_view),
2123
path("cached_low_level_view/", views.cached_low_level_view),
2224
path("json_view/", views.json_view),

tests/views.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import asyncio
2+
3+
from asgiref.sync import sync_to_async
14
from django.contrib.auth.models import User
25
from django.core.cache import cache
36
from django.http import HttpResponseRedirect, JsonResponse
@@ -11,6 +14,16 @@ def execute_sql(request):
1114
return render(request, "base.html")
1215

1316

17+
async def async_execute_sql(request):
18+
await sync_to_async(list)(User.objects.all())
19+
return render(request, "base.html")
20+
21+
22+
async def async_execute_sql_concurrently(request):
23+
await asyncio.gather(sync_to_async(list)(User.objects.all()), User.objects.acount())
24+
return render(request, "base.html")
25+
26+
1427
def regular_view(request, title):
1528
return render(request, "basic.html", {"title": title})
1629

0 commit comments

Comments
 (0)