Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add async tests #1835

Merged
merged 5 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
tim-schilling marked this conversation as resolved.
Show resolved Hide resolved

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