Skip to content

Commit

Permalink
Add async tests (#1835)
Browse files Browse the repository at this point in the history
* Add asynchronous examples (#1819)

* Add tests for async usage (#1819)

* Include daphne is requirements_dev
  • Loading branch information
salomvary authored Jul 16, 2024
1 parent 25656ee commit a9a66a9
Show file tree
Hide file tree
Showing 11 changed files with 181 additions and 2 deletions.
10 changes: 10 additions & 0 deletions example/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,13 @@ environment variable::

$ DB_BACKEND=postgresql python example/manage.py migrate
$ DB_BACKEND=postgresql python example/manage.py runserver

Using an asynchronous (ASGI) server:

Install [Daphne](https://pypi.org/project/daphne/) first:

$ python -m pip install daphne

Then run the Django development server:

$ ASYNC_SERVER=true python example/manage.py runserver
9 changes: 9 additions & 0 deletions example/asgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""ASGI config for example project."""

import os

from django.core.asgi import get_asgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings")

application = get_asgi_application()
3 changes: 2 additions & 1 deletion example/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
# Application definition

INSTALLED_APPS = [
*(["daphne"] if os.getenv("ASYNC_SERVER", False) else []), # noqa: FBT003
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
Expand Down Expand Up @@ -66,6 +67,7 @@
USE_TZ = True

WSGI_APPLICATION = "example.wsgi.application"
ASGI_APPLICATION = "example.asgi.application"


# Cache and database
Expand Down Expand Up @@ -103,7 +105,6 @@

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


# Only enable the toolbar when we're in debug mode and we're
# not running tests. Django will change DEBUG to be False for
# tests, so we can't rely on DEBUG alone.
Expand Down
14 changes: 14 additions & 0 deletions example/templates/async_db.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Async DB</title>
</head>
<body>
<h1>Async DB</h1>
<p>
<span>Value </span>
<span>{{ user_count }}</span>
</p>
</body>
</html>
11 changes: 10 additions & 1 deletion example/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
from django.views.generic import TemplateView

from debug_toolbar.toolbar import debug_toolbar_urls
from example.views import increment, jinja2_view
from example.views import (
async_db,
async_db_concurrent,
async_home,
increment,
jinja2_view,
)

urlpatterns = [
path("", TemplateView.as_view(template_name="index.html"), name="home"),
Expand All @@ -13,6 +19,9 @@
name="bad_form",
),
path("jinja/", jinja2_view, name="jinja"),
path("async/", async_home, name="async_home"),
path("async/db/", async_db, name="async_db"),
path("async/db-concurrent/", async_db_concurrent, name="async_db_concurrent"),
path("jquery/", TemplateView.as_view(template_name="jquery/index.html")),
path("mootools/", TemplateView.as_view(template_name="mootools/index.html")),
path("prototype/", TemplateView.as_view(template_name="prototype/index.html")),
Expand Down
27 changes: 27 additions & 0 deletions example/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import asyncio

from asgiref.sync import sync_to_async
from django.contrib.auth.models import User
from django.http import JsonResponse
from django.shortcuts import render

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

def jinja2_view(request):
return render(request, "index.jinja", {"foo": "bar"}, using="jinja2")


async def async_home(request):
return await sync_to_async(render)(request, "index.html")


async def async_db(request):
user_count = await User.objects.acount()

return await sync_to_async(render)(
request, "async_db.html", {"user_count": user_count}
)


async def async_db_concurrent(request):
# Do database queries concurrently
(user_count, _) = await asyncio.gather(
User.objects.acount(), User.objects.filter(username="test").acount()
)

return await sync_to_async(render)(
request, "async_db.html", {"user_count": user_count}
)
4 changes: 4 additions & 0 deletions requirements_dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ selenium
tox
black

# Integration support

daphne # async in Example app

# Documentation

Sphinx
Expand Down
46 changes: 46 additions & 0 deletions tests/panels/test_sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ def sql_call(*, use_iterator=False):
return list(qs)


async def async_sql_call(*, use_iterator=False):
qs = User.objects.all()
if use_iterator:
qs = qs.iterator()
return await sync_to_async(list)(qs)


async def concurrent_async_sql_call(*, use_iterator=False):
qs = User.objects.all()
if use_iterator:
qs = qs.iterator()
return await asyncio.gather(sync_to_async(list)(qs), User.objects.acount())


class SQLPanelTestCase(BaseTestCase):
panel_id = "SQLPanel"

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

async def test_recording_async(self):
self.assertEqual(len(self.panel._queries), 0)

await async_sql_call()

# ensure query was logged
self.assertEqual(len(self.panel._queries), 1)
query = self.panel._queries[0]
self.assertEqual(query["alias"], "default")
self.assertTrue("sql" in query)
self.assertTrue("duration" in query)
self.assertTrue("stacktrace" in query)

# ensure the stacktrace is populated
self.assertTrue(len(query["stacktrace"]) > 0)

async def test_recording_concurrent_async(self):
self.assertEqual(len(self.panel._queries), 0)

await concurrent_async_sql_call()

# ensure query was logged
self.assertEqual(len(self.panel._queries), 2)
query = self.panel._queries[0]
self.assertEqual(query["alias"], "default")
self.assertTrue("sql" in query)
self.assertTrue("duration" in query)
self.assertTrue("stacktrace" in query)

# ensure the stacktrace is populated
self.assertTrue(len(query["stacktrace"]) > 0)

@unittest.skipUnless(
connection.vendor == "postgresql", "Test valid only on PostgreSQL"
)
Expand Down
44 changes: 44 additions & 0 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,24 @@ def test_data_gone(self):
)
self.assertIn("Please reload the page and retry.", response.json()["content"])

def test_sql_page(self):
response = self.client.get("/execute_sql/")
self.assertEqual(
len(response.toolbar.get_panel_by_id("SQLPanel").get_stats()["queries"]), 1
)

def test_async_sql_page(self):
response = self.client.get("/async_execute_sql/")
self.assertEqual(
len(response.toolbar.get_panel_by_id("SQLPanel").get_stats()["queries"]), 1
)

def test_concurrent_async_sql_page(self):
response = self.client.get("/async_execute_sql_concurrently/")
self.assertEqual(
len(response.toolbar.get_panel_by_id("SQLPanel").get_stats()["queries"]), 2
)


@override_settings(DEBUG=True)
class DebugToolbarIntegrationTestCase(IntegrationTestCase):
Expand Down Expand Up @@ -843,3 +861,29 @@ def test_theme_toggle(self):
self.get("/regular/basic/")
toolbar = self.selenium.find_element(By.ID, "djDebug")
self.assertEqual(toolbar.get_attribute("data-theme"), "light")

def test_async_sql_action(self):
self.get("/async_execute_sql/")
self.selenium.find_element(By.ID, "SQLPanel")
self.selenium.find_element(By.ID, "djDebugWindow")

# Click to show the SQL panel
self.selenium.find_element(By.CLASS_NAME, "SQLPanel").click()

# SQL panel loads
self.wait.until(
EC.visibility_of_element_located((By.CSS_SELECTOR, ".remoteCall"))
)

def test_concurrent_async_sql_action(self):
self.get("/async_execute_sql_concurrently/")
self.selenium.find_element(By.ID, "SQLPanel")
self.selenium.find_element(By.ID, "djDebugWindow")

# Click to show the SQL panel
self.selenium.find_element(By.CLASS_NAME, "SQLPanel").click()

# SQL panel loads
self.wait.until(
EC.visibility_of_element_located((By.CSS_SELECTOR, ".remoteCall"))
)
2 changes: 2 additions & 0 deletions tests/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
path("non_ascii_request/", views.regular_view, {"title": NonAsciiRepr()}),
path("new_user/", views.new_user),
path("execute_sql/", views.execute_sql),
path("async_execute_sql/", views.async_execute_sql),
path("async_execute_sql_concurrently/", views.async_execute_sql_concurrently),
path("cached_view/", views.cached_view),
path("cached_low_level_view/", views.cached_low_level_view),
path("json_view/", views.json_view),
Expand Down
13 changes: 13 additions & 0 deletions tests/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import asyncio

from asgiref.sync import sync_to_async
from django.contrib.auth.models import User
from django.core.cache import cache
from django.http import HttpResponseRedirect, JsonResponse
Expand All @@ -11,6 +14,16 @@ def execute_sql(request):
return render(request, "base.html")


async def async_execute_sql(request):
await sync_to_async(list)(User.objects.all())
return render(request, "base.html")


async def async_execute_sql_concurrently(request):
await asyncio.gather(sync_to_async(list)(User.objects.all()), User.objects.acount())
return render(request, "base.html")


def regular_view(request, title):
return render(request, "basic.html", {"title": title})

Expand Down

0 comments on commit a9a66a9

Please sign in to comment.