Skip to content
Open
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
34 changes: 24 additions & 10 deletions packages/runtime-sdk/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,10 +192,15 @@ def dev_server(


@functools.cache
def get_suite_results(dev_server: str, suite: str) -> SuiteResults | str:
def get_suite_results(
dev_server: str, suite: str, mode: str | None = None
) -> SuiteResults | str:
url = f"{dev_server}/run-tests/{suite}"
if mode is not None:
url = f"{url}?mode={mode}"
try:
resp = requests.get(
f"{dev_server}/run-tests/{suite}",
url,
timeout=(SUITE_CONNECT_TIMEOUT, SUITE_READ_TIMEOUT),
)
except requests.RequestException as error:
Expand All @@ -205,9 +210,9 @@ def get_suite_results(dev_server: str, suite: str) -> SuiteResults | str:
return resp.json()


def _make_test(suite: str, test_name: str) -> Callable:
def _make_test(suite: str, test_name: str, mode: str | None = None) -> Callable:
def test_fn(self: Any, dev_server: str) -> None:
results = get_suite_results(dev_server, suite)
results = get_suite_results(dev_server, suite, mode)
if isinstance(results, str):
pytest.fail(results)
return
Expand All @@ -224,12 +229,17 @@ def test_fn(self: Any, dev_server: str) -> None:
return test_fn


def make_suite_class(suite: str, tests: list[str]) -> type:
def make_suite_class(suite: str, tests: list[str], mode: str | None = None) -> type:
"""Build a test class with one method per in-worker test of `suite`."""
name = (
f"Test{suite.upper()}"
if mode is None
else f"Test{suite.upper()}_{mode.upper()}"
)
return type(
f"Test{suite.upper()}",
name,
(),
{f"test_{name}": _make_test(suite, name) for name in tests},
{f"test_{test}": _make_test(suite, test, mode) for test in tests},
)


Expand All @@ -256,12 +266,16 @@ def discover_suites(src_dir: Path) -> dict[str, list[str]]:
}


def register_in_worker_suites(namespace: dict[str, Any], src_dir: Path) -> None:
def register_in_worker_suites(
namespace: dict[str, Any], src_dir: Path, mode: str | None = None
) -> None:
"""Define a ``TestXxx`` class in `namespace` for every suite found in `src_dir`.

Call with ``globals()`` from a test module so each in-worker test surfaces as
its own pytest case without manual registration.
its own pytest case without manual registration. Pass `mode` to run the same
suites again against a different in-worker app; tests that do not apply to a
mode report themselves as skipped.
"""
for suite, test_names in discover_suites(src_dir).items():
suite_cls = make_suite_class(suite, test_names)
suite_cls = make_suite_class(suite, test_names, mode)
namespace[suite_cls.__name__] = suite_cls
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
To add a new framework: create a subdirectory under web-frameworks-test/ with
worker.py, wrangler.jsonc, pyproject.toml, and test_*.py files.

Each Django suite runs twice against the same worker: once through the ASGI
handler (async views) and once through the WSGI handler (the sync mirror in
``django_app.urls_sync``), so one set of test files covers both adaptors.

Python 3.12 (Pyodide 0.26.0a2) is excluded. The in-worker pytest suite drives
async tests via ``loop.run_until_complete``, which is a no-op on Pyodide 0.26.0a2
(no ``run_sync``/JSPI): async tests return unawaited futures and report false
Expand All @@ -14,9 +18,7 @@
import pytest
from conftest import COMPAT_CONFIGS, CompatConfig, register_in_worker_suites

WEB_FRAMEWORKS_DIR: Path = (
Path(__file__).parent / "web-frameworks-test" / "django-async"
)
WEB_FRAMEWORKS_DIR: Path = Path(__file__).parent / "web-frameworks-test" / "django"
WEB_FRAMEWORKS_SRC_DIR: Path = WEB_FRAMEWORKS_DIR / "src"


Expand All @@ -38,4 +40,5 @@ def compat_config(request: pytest.FixtureRequest) -> CompatConfig:
return request.param


register_in_worker_suites(globals(), WEB_FRAMEWORKS_SRC_DIR)
register_in_worker_suites(globals(), WEB_FRAMEWORKS_SRC_DIR, mode="asgi")
register_in_worker_suites(globals(), WEB_FRAMEWORKS_SRC_DIR, mode="wsgi")

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[project]
name = "django-async-test"
name = "django-test"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import json as _json

from django.core.handlers.wsgi import WSGIHandler

import asgi
from workers import Request
from workers import Request, wsgi

BASE_URL = "http://testserver"

Expand All @@ -18,13 +20,19 @@ def _with_content_length(headers, body):
return hdrs


def is_wsgi(app):
return isinstance(app, WSGIHandler)


async def fetch(app, path, method="GET", headers=None, body=None):
request = Request(
f"{BASE_URL}{path}",
method=method,
headers=_with_content_length(headers, body),
body=body,
)
if is_wsgi(app):
return await wsgi.fetch(app, request, {})
return await asgi.fetch(app, request, {})


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
app_name = "api"


async def info(request):
def info(request):
return JsonResponse({"namespace": "api-v1"})


Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
from asgiref.sync import iscoroutinefunction, markcoroutinefunction


class CustomAsyncMiddleware:
class CustomMiddleware:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making this middleware work on both sync and async mode.

async_capable = True
sync_capable = False
sync_capable = True

def __init__(self, get_response):
self.get_response = get_response
if iscoroutinefunction(self.get_response):
markcoroutinefunction(self)

async def __call__(self, request):
def __call__(self, request):
if iscoroutinefunction(self):
return self.__acall__(request)
request.custom_middleware_applied = True
response = self.get_response(request)
response["X-Custom-Middleware"] = "applied"
return response

async def __acall__(self, request):
request.custom_middleware_applied = True
response = await self.get_response(request)
response["X-Custom-Middleware"] = "applied"
Expand Down

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

django's async and sync views need to be registered differently. The only difference between urls.py and urls_sync.py is that whether the views are synchronous or asynchronous (async def vs def)

Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from django.urls import include, path, re_path
from rest_framework.routers import DefaultRouter

from django_app import views_sync as views

router = DefaultRouter()


urlpatterns = [
path("hello/", views.hello, name="hello"),
path("status/<int:code>/", views.status_code, name="status-code"),
path("echo-method/", views.echo_method, name="echo-method"),
path("echo-headers/", views.echo_headers, name="echo-headers"),
path("echo-query/", views.echo_query, name="echo-query"),
path("echo-body/", views.echo_body, name="echo-body"),
path("echo-form/", views.echo_form, name="echo-form"),
path("items/<int:id>/", views.item_detail, name="item-detail"),
path("users/<str:name>/", views.user_detail, name="user-detail"),
path("posts/<slug:slug>/", views.post_detail, name="post-detail"),
path("uuids/<uuid:uid>/", views.uuid_detail, name="uuid-detail"),
re_path(r"^archive/(?P<year>[0-9]{4})/$", views.archive_year, name="archive-year"),
path("api/v1/", include(("django_app.api_urls", "api"), namespace="v1")),
path("reverse-test/", views.reverse_test, name="reverse-test"),
path("template/hello/", views.template_hello, name="template-hello"),
path("template/context/", views.template_context, name="template-context"),
path(
"template/inheritance/", views.template_inheritance, name="template-inheritance"
),
path("form/validate/", views.form_validate, name="form-validate"),
path("form/process/", views.form_process, name="form-process"),
path("trigger-404/", views.trigger_404, name="trigger-404"),
path("trigger-403/", views.trigger_403, name="trigger-403"),
path("trigger-400/", views.trigger_400, name="trigger-400"),
path("trigger-500/", views.trigger_500, name="trigger-500"),
path("session/set/", views.session_set, name="session-set"),
path("session/get/", views.session_get, name="session-get"),
path("session/flush/", views.session_flush, name="session-flush"),
path("stream/sync-gen/", views.stream_sync_gen, name="stream-sync-gen"),
path("csrf/form/", views.csrf_form, name="csrf-form"),
path("csrf/exempt/", views.csrf_exempt_view, name="csrf-exempt"),
path("cache/set/", views.cache_set, name="cache-set"),
path("cache/get/", views.cache_get, name="cache-get"),
path("cache/delete/", views.cache_delete, name="cache-delete"),
path("cache/clear/", views.cache_clear, name="cache-clear"),
path("signals/send/", views.signal_send, name="signal-send"),
path("signals/robust/", views.signal_robust, name="signal-robust"),
path("auth/login/", views.auth_login, name="auth-login"),
path("auth/logout/", views.auth_logout, name="auth-logout"),
path("auth/user/", views.auth_user, name="auth-user"),
path("auth/protected/", views.auth_protected, name="auth-protected"),
path("auth/permission/", views.auth_permission, name="auth-permission"),
path("upload/single/", views.upload_single, name="upload-single"),
path("upload/multiple/", views.upload_multiple, name="upload-multiple"),
path("paginate/", views.paginate_view, name="paginate"),
path("cbv/", views.SyncCBV.as_view(), name="async-cbv"),
path("drf/", include(router.urls)),
path("drf/", include("django_app.drf_views")),
]


handler400 = "django_app.views_sync.custom_handler400"
handler403 = "django_app.views_sync.custom_handler403"
handler404 = "django_app.views_sync.custom_handler404"
handler500 = "django_app.views_sync.custom_handler500"
Loading
Loading