From b448a19fe6bdb7902a38502b9350c8af09cc2276 Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Tue, 29 Sep 2026 10:16:42 -0700 Subject: [PATCH] chore: Factor out more repeated test code --- packages/django-cf/tests/conftest.py | 51 ++++--------------- .../tests/in_worker/test_in_worker.py | 20 ++++---- .../tests/in_worker/worker/src/conftest.py | 6 --- .../tests/in_worker/worker/src/worker.py | 10 ---- .../tests/bindings-test/src/conftest.py | 9 ---- packages/runtime-sdk/tests/conftest.py | 50 ++---------------- packages/runtime-sdk/tests/test_bindings.py | 2 +- packages/runtime-sdk/tests/test_django.py | 17 +++---- packages/runtime-sdk/tests/test_fastapi.py | 19 +++---- packages/runtime-sdk/tests/test_flask.py | 11 +--- packages/runtime-sdk/tests/test_in_workerd.py | 4 +- .../django-async/src/conftest.py | 10 ---- .../fastapi-tests/src/conftest.py | 10 ---- .../fastapi-tests/src/worker.py | 9 ---- packages/testlib/AGENTS.md | 19 ++++++- packages/testlib/testlib/host.py | 51 +++++++++++++++++-- 16 files changed, 108 insertions(+), 190 deletions(-) delete mode 100644 packages/runtime-sdk/tests/web-frameworks-test/django-async/src/conftest.py delete mode 100644 packages/runtime-sdk/tests/web-frameworks-test/fastapi-tests/src/conftest.py diff --git a/packages/django-cf/tests/conftest.py b/packages/django-cf/tests/conftest.py index d9846d80..5473941b 100644 --- a/packages/django-cf/tests/conftest.py +++ b/packages/django-cf/tests/conftest.py @@ -11,18 +11,17 @@ import pytest import requests from testlib.host import ( - COMPAT_CONFIGS, GENERATED_FILE_PATTERN, - CompatConfig, + compat_config, dev_server, + fail_with_log, pywrangler_sync, run_dev_server, -) -from testlib.host import ( - register_in_worker_suites as register_testlib_suites, + worker_project_dir, ) -__all__ = ["dev_server"] +# Re-export fixtures so pytest discovers them +__all__ = ["compat_config", "dev_server", "worker_project_dir"] TEST_DIR: Path = Path(__file__).parent PACKAGE_DIR: Path = TEST_DIR.parent @@ -41,27 +40,23 @@ class DevServer: base_url: str -def _fail(log_path: Path, message: str) -> None: - pytest.fail( - f"{message}\n\n--- pywrangler dev log ---\n{log_path.read_text(errors='replace')}" - ) - - def _seed(base_url: str, log_path: Path) -> None: for endpoint in ("__run_migrations__", "__create_admin__"): try: response = requests.get(f"{base_url}/{endpoint}/", timeout=SEED_TIMEOUT) except requests.RequestException as error: - _fail(log_path, f"GET /{endpoint}/ failed: {error}") + fail_with_log(log_path, f"GET /{endpoint}/ failed: {error}") else: if response.status_code != 200: - _fail( + fail_with_log( log_path, f"GET /{endpoint}/ returned {response.status_code}: {response.text[:2000]}", ) payload = response.json() if payload.get("status") == "error": - _fail(log_path, f"GET /{endpoint}/ reported: {payload.get('message')}") + fail_with_log( + log_path, f"GET /{endpoint}/ reported: {payload.get('message')}" + ) def _serve(project_dir: Path, tmp_path: Path) -> Generator[DevServer]: @@ -108,30 +103,6 @@ def r2_web_server(tmp_path_factory: pytest.TempPathFactory) -> Generator[DevServ yield from _serve(R2_PROJECT, tmp_path_factory.mktemp("r2")) -@pytest.fixture( - scope="module", - params=COMPAT_CONFIGS, - ids=[c.python_version for c in COMPAT_CONFIGS], -) -def compat_config(request: pytest.FixtureRequest) -> CompatConfig: - return request.param - - -@pytest.fixture(scope="module") -def worker_project_dir() -> Path: - """Worker project the `dev_server` fixture should serve. - - Test modules using `dev_server` must override this fixture. - """ - raise NotImplementedError( - "override the `worker_project_dir` fixture in your test module" - ) - - @pytest.fixture(scope="module") -def dev_startup_timeout(): +def dev_startup_timeout() -> int: return DEV_STARTUP_TIMEOUT - - -def register_in_worker_suites(namespace: dict, src_dir: Path) -> None: - register_testlib_suites(namespace, src_dir, source_roots=[PACKAGE_DIR]) diff --git a/packages/django-cf/tests/in_worker/test_in_worker.py b/packages/django-cf/tests/in_worker/test_in_worker.py index d73de77f..4e1364a2 100644 --- a/packages/django-cf/tests/in_worker/test_in_worker.py +++ b/packages/django-cf/tests/in_worker/test_in_worker.py @@ -12,22 +12,20 @@ import pytest import requests +from testlib.host import ( + COMPAT_CONFIGS, + compat_config_fixture, + register_in_worker_suites, +) -from tests.conftest import COMPAT_CONFIGS, CompatConfig, register_in_worker_suites +from tests.conftest import PACKAGE_DIR IN_WORKER_PROJECT: Path = Path(__file__).parent / "worker" IN_WORKER_SRC_DIR: Path = IN_WORKER_PROJECT / "src" -MATRIX: list[CompatConfig] = [c for c in COMPAT_CONFIGS if c.python_version != "3.12"] - - -@pytest.fixture( - scope="module", - params=MATRIX, - ids=[c.python_version for c in MATRIX], +compat_config = compat_config_fixture( + [c for c in COMPAT_CONFIGS if c.python_version != "3.12"] ) -def compat_config(request: pytest.FixtureRequest) -> CompatConfig: - return request.param @pytest.fixture(scope="module") @@ -35,7 +33,7 @@ def worker_project_dir() -> Path: return IN_WORKER_PROJECT -register_in_worker_suites(globals(), IN_WORKER_SRC_DIR) +register_in_worker_suites(globals(), IN_WORKER_SRC_DIR, source_roots=[PACKAGE_DIR]) def test_django_wsgi_header_transformation(dev_server: str) -> None: diff --git a/packages/django-cf/tests/in_worker/worker/src/conftest.py b/packages/django-cf/tests/in_worker/worker/src/conftest.py index 8f18c4eb..e8c78368 100644 --- a/packages/django-cf/tests/in_worker/worker/src/conftest.py +++ b/packages/django-cf/tests/in_worker/worker/src/conftest.py @@ -2,14 +2,8 @@ import pytest from django.core.cache import cache -from workers import env as _env @pytest.fixture(autouse=True) def clear_cache(): cache.clear() - - -@pytest.fixture -def env(): - return _env diff --git a/packages/django-cf/tests/in_worker/worker/src/worker.py b/packages/django-cf/tests/in_worker/worker/src/worker.py index 2b163358..9610b3d7 100644 --- a/packages/django-cf/tests/in_worker/worker/src/worker.py +++ b/packages/django-cf/tests/in_worker/worker/src/worker.py @@ -6,7 +6,6 @@ import django import django.conf -import pytest from _django_app import R2_LOCATION, django_wsgi_app from django.http import HttpResponse, JsonResponse, StreamingHttpResponse from django.urls import path @@ -136,15 +135,6 @@ def _django_headers_view(request): ] -class EnvPlugin: - def __init__(self, env): - self._env = env - - @pytest.fixture - def env(self): - return self._env - - class Default(DjangoCF, TestRunnerEntrypoint): def get_app(self): return django_wsgi_app() diff --git a/packages/runtime-sdk/tests/bindings-test/src/conftest.py b/packages/runtime-sdk/tests/bindings-test/src/conftest.py index 0f7c05ab..4848d685 100644 --- a/packages/runtime-sdk/tests/bindings-test/src/conftest.py +++ b/packages/runtime-sdk/tests/bindings-test/src/conftest.py @@ -1,15 +1,6 @@ # pyright: reportMissingImports=false import uuid -import pytest - -from workers import env as _env - - -@pytest.fixture -def env(): - return _env - def unique_table_name() -> str: """Unique per call""" diff --git a/packages/runtime-sdk/tests/conftest.py b/packages/runtime-sdk/tests/conftest.py index 315c254f..675349d4 100644 --- a/packages/runtime-sdk/tests/conftest.py +++ b/packages/runtime-sdk/tests/conftest.py @@ -1,22 +1,15 @@ """Shared fixtures and helpers for the host-side test suite.""" -from pathlib import Path -from typing import Any - import pytest from testlib.host import ( - COMPAT_CONFIGS, - CompatConfig, + compat_config, dev_server, + dev_startup_timeout, + worker_project_dir, ) -from testlib.host import ( - register_in_worker_suites as register_testlib_suites, -) - -__all__ = ["dev_server"] - -TEST_DIR: Path = Path(__file__).parent +# Re-export fixtures so pytest discovers them +__all__ = ["compat_config", "dev_server", "dev_startup_timeout", "worker_project_dir"] OPT_IN_MARKERS: tuple[str, ...] = ("hyperdrive",) @@ -33,36 +26,3 @@ def pytest_collection_modifyitems( for item in items: if marker in item.keywords: item.add_marker(skip) - - -@pytest.fixture( - scope="module", - params=COMPAT_CONFIGS, - ids=[c.python_version for c in COMPAT_CONFIGS], -) -def compat_config(request: pytest.FixtureRequest) -> CompatConfig: - return request.param - - -@pytest.fixture(scope="module") -def worker_project_dir() -> Path: - """Worker project the `dev_server` fixture should serve. - - Test modules using `dev_server` must override this fixture. - """ - raise NotImplementedError( - "override the `worker_project_dir` fixture in your test module" - ) - - -@pytest.fixture(scope="module") -def dev_startup_timeout(): - return 120 - - -def register_in_worker_suites( - namespace: dict[str, Any], - src_dir: Path, - marks: dict[str, pytest.MarkDecorator] | None = None, -) -> None: - register_testlib_suites(namespace, src_dir, marks=marks) diff --git a/packages/runtime-sdk/tests/test_bindings.py b/packages/runtime-sdk/tests/test_bindings.py index 4ac9d296..6deb9ce5 100644 --- a/packages/runtime-sdk/tests/test_bindings.py +++ b/packages/runtime-sdk/tests/test_bindings.py @@ -13,7 +13,7 @@ from pathlib import Path import pytest -from conftest import register_in_worker_suites +from testlib.host import register_in_worker_suites BINDINGS_TEST_DIR: Path = Path(__file__).parent / "bindings-test" BINDINGS_SRC_DIR: Path = BINDINGS_TEST_DIR / "src" diff --git a/packages/runtime-sdk/tests/test_django.py b/packages/runtime-sdk/tests/test_django.py index fb35071c..19144283 100644 --- a/packages/runtime-sdk/tests/test_django.py +++ b/packages/runtime-sdk/tests/test_django.py @@ -12,7 +12,11 @@ from pathlib import Path import pytest -from conftest import COMPAT_CONFIGS, CompatConfig, register_in_worker_suites +from testlib.host import ( + COMPAT_CONFIGS, + compat_config_fixture, + register_in_worker_suites, +) WEB_FRAMEWORKS_DIR: Path = ( Path(__file__).parent / "web-frameworks-test" / "django-async" @@ -26,16 +30,9 @@ def worker_project_dir() -> Path: # Exclude Python 3.12 (Pyodide 0.26.0a2) for async tests -ASYNC_COMPAT_CONFIGS = [c for c in COMPAT_CONFIGS if c.python_version != "3.12"] - - -@pytest.fixture( - scope="module", - params=ASYNC_COMPAT_CONFIGS, - ids=[c.python_version for c in ASYNC_COMPAT_CONFIGS], +compat_config = compat_config_fixture( + [c for c in COMPAT_CONFIGS if c.python_version != "3.12"] ) -def compat_config(request: pytest.FixtureRequest) -> CompatConfig: - return request.param register_in_worker_suites(globals(), WEB_FRAMEWORKS_SRC_DIR) diff --git a/packages/runtime-sdk/tests/test_fastapi.py b/packages/runtime-sdk/tests/test_fastapi.py index d907673c..87997c08 100644 --- a/packages/runtime-sdk/tests/test_fastapi.py +++ b/packages/runtime-sdk/tests/test_fastapi.py @@ -9,7 +9,11 @@ from pathlib import Path import pytest -from conftest import COMPAT_CONFIGS, CompatConfig, register_in_worker_suites +from testlib.host import ( + COMPAT_CONFIGS, + compat_config_fixture, + register_in_worker_suites, +) WEB_FRAMEWORKS_DIR: Path = ( Path(__file__).parent / "web-frameworks-test" / "fastapi-tests" @@ -23,18 +27,9 @@ def worker_project_dir() -> Path: # Exclude Python 3.12 (Pyodide 0.26.0a2) for async tests. -FASTAPI_COMPAT_CONFIGS = [ - c for c in COMPAT_CONFIGS if c.python_version not in ("3.12",) -] - - -@pytest.fixture( - scope="module", - params=FASTAPI_COMPAT_CONFIGS, - ids=[c.python_version for c in FASTAPI_COMPAT_CONFIGS], +compat_config = compat_config_fixture( + [c for c in COMPAT_CONFIGS if c.python_version != "3.12"] ) -def compat_config(request: pytest.FixtureRequest) -> CompatConfig: - return request.param register_in_worker_suites(globals(), WEB_FRAMEWORKS_SRC_DIR) diff --git a/packages/runtime-sdk/tests/test_flask.py b/packages/runtime-sdk/tests/test_flask.py index ce8cb3ef..11481316 100644 --- a/packages/runtime-sdk/tests/test_flask.py +++ b/packages/runtime-sdk/tests/test_flask.py @@ -3,7 +3,7 @@ from pathlib import Path import pytest -from conftest import COMPAT_CONFIGS, CompatConfig, register_in_worker_suites +from testlib.host import register_in_worker_suites WEB_FRAMEWORKS_DIR = Path(__file__).parent / "web-frameworks-test" / "flask-tests" WEB_FRAMEWORKS_SRC_DIR = WEB_FRAMEWORKS_DIR / "src" @@ -14,13 +14,4 @@ def worker_project_dir() -> Path: return WEB_FRAMEWORKS_DIR -@pytest.fixture( - scope="module", - params=COMPAT_CONFIGS, - ids=[config.python_version for config in COMPAT_CONFIGS], -) -def compat_config(request: pytest.FixtureRequest) -> CompatConfig: - return request.param - - register_in_worker_suites(globals(), WEB_FRAMEWORKS_SRC_DIR) diff --git a/packages/runtime-sdk/tests/test_in_workerd.py b/packages/runtime-sdk/tests/test_in_workerd.py index 04cafaf8..d971d376 100644 --- a/packages/runtime-sdk/tests/test_in_workerd.py +++ b/packages/runtime-sdk/tests/test_in_workerd.py @@ -4,11 +4,9 @@ from pathlib import Path import pytest -from conftest import ( +from testlib.host import ( COMPAT_CONFIGS, CompatConfig, -) -from testlib.host import ( configure_compatibility, link_packages, pywrangler_sync, diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/conftest.py b/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/conftest.py deleted file mode 100644 index 127ebf72..00000000 --- a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/conftest.py +++ /dev/null @@ -1,10 +0,0 @@ -# pyright: reportMissingImports=false - -import pytest - -from workers import env as _env - - -@pytest.fixture -def env(): - return _env diff --git a/packages/runtime-sdk/tests/web-frameworks-test/fastapi-tests/src/conftest.py b/packages/runtime-sdk/tests/web-frameworks-test/fastapi-tests/src/conftest.py deleted file mode 100644 index 127ebf72..00000000 --- a/packages/runtime-sdk/tests/web-frameworks-test/fastapi-tests/src/conftest.py +++ /dev/null @@ -1,10 +0,0 @@ -# pyright: reportMissingImports=false - -import pytest - -from workers import env as _env - - -@pytest.fixture -def env(): - return _env diff --git a/packages/runtime-sdk/tests/web-frameworks-test/fastapi-tests/src/worker.py b/packages/runtime-sdk/tests/web-frameworks-test/fastapi-tests/src/worker.py index 07985850..f5fecdd1 100644 --- a/packages/runtime-sdk/tests/web-frameworks-test/fastapi-tests/src/worker.py +++ b/packages/runtime-sdk/tests/web-frameworks-test/fastapi-tests/src/worker.py @@ -576,15 +576,6 @@ async def native_file(): app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") -class EnvPlugin: - def __init__(self, env): - self._env = env - - @pytest.fixture - def env(self): - return self._env - - class FastAPIAppPlugin: @pytest.fixture def fastapi_app(self): diff --git a/packages/testlib/AGENTS.md b/packages/testlib/AGENTS.md index 235d48b5..ffc012ff 100644 --- a/packages/testlib/AGENTS.md +++ b/packages/testlib/AGENTS.md @@ -10,10 +10,27 @@ via `../packages/testlib` in `[tool.uv.sources]`. | Module | Runs on | Purpose | |---|---|---| -| `testlib/host.py` | host | `dev_server`, `pywrangler_sync`, `register_in_worker_suites`, arg/keyword forwarding | +| `testlib/host.py` | host | `dev_server` and its backing fixtures, `pywrangler_sync`, `register_in_worker_suites`, arg/keyword forwarding | | `testlib/entrypoint.py` | worker | `TestRunner`, `TestRunnerEntrypoint` (`/run-tests/`, `/health`), `ResultCollector` | | `testlib/tracebacks.py` | both | Pickle worker exceptions and remap their frames onto host source roots | +## Fixtures shared with the package conftests + +- `host.py` defines `dev_server` plus the fixtures it depends on: + `compat_config` (parametrised over `COMPAT_CONFIGS`), `worker_project_dir` + (raises; every test module using `dev_server` must override it) and + `dev_startup_timeout` (120s). Each package's `tests/conftest.py` imports them + by name and lists them in `__all__` so pytest discovers them; it should not + redefine them. Override per package (django-cf bumps `dev_startup_timeout`) + or per module as needed. +- To run a module against a subset of runtimes, assign + `compat_config = compat_config_fixture([...])` at module level instead of + hand-writing a parametrised fixture. +- `fail_with_log(log_path, message)` fails the test with the `pywrangler dev` + log appended; use it rather than reimplementing it. +- `TestRunner` always registers `EnvPlugin`, so every in-worker suite already + has an `env` fixture. Do not add one to a worker's `src/conftest.py`. + ## How in-worker suites are exposed on the host - A worker project has `src/test_.py` modules. The worker serves diff --git a/packages/testlib/testlib/host.py b/packages/testlib/testlib/host.py index cadb8fa7..d0d1ba37 100644 --- a/packages/testlib/testlib/host.py +++ b/packages/testlib/testlib/host.py @@ -119,6 +119,50 @@ def configure_compatibility(file: Path, config: CompatConfig) -> None: file.write_text(content) +def compat_config_fixture(configs: Sequence[CompatConfig]) -> Callable[..., Any]: + """Build a module-scoped ``compat_config`` fixture parametrised over *configs*. + + Assign the result to ``compat_config`` in a test module to run its + ``dev_server`` against a subset of ``COMPAT_CONFIGS``:: + + compat_config = compat_config_fixture( + [c for c in COMPAT_CONFIGS if c.python_version != "3.12"] + ) + """ + + @pytest.fixture( + scope="module", + params=list(configs), + ids=[c.python_version for c in configs], + ) + def compat_config(request: pytest.FixtureRequest) -> CompatConfig: + return request.param + + return compat_config + + +# Default fixtures backing ``dev_server``. Conftests re-export them alongside +# ``dev_server``; test modules override ``worker_project_dir`` (mandatory) and +# ``compat_config`` / ``dev_startup_timeout`` as needed. +compat_config = compat_config_fixture(COMPAT_CONFIGS) + + +@pytest.fixture(scope="module") +def worker_project_dir() -> Path: + """Worker project the `dev_server` fixture should serve. + + Test modules using `dev_server` must override this fixture. + """ + raise NotImplementedError( + "override the `worker_project_dir` fixture in your test module" + ) + + +@pytest.fixture(scope="module") +def dev_startup_timeout() -> int: + return 120 + + class InWorkerTestResult(TypedDict): status: Literal["passed", "failed", "error", "skipped"] error: str @@ -136,7 +180,8 @@ def get_free_port() -> int: return sock.getsockname()[1] -def _fail(log_path: Path, message: str) -> None: +def fail_with_log(log_path: Path, message: str) -> None: + """Fail the current test with *message* followed by the ``pywrangler dev`` log.""" pytest.fail( f"{message}\n\n--- pywrangler dev log ---\n" f"{log_path.read_text(errors='replace')}" @@ -154,7 +199,7 @@ def wait_for_ready( # noqa: PLR0913 deadline = time.monotonic() + timeout while time.monotonic() < deadline: if process.poll() is not None: - _fail( + fail_with_log( log_path, f"pywrangler dev exited early with code {process.returncode}" ) try: @@ -165,7 +210,7 @@ def wait_for_ready( # noqa: PLR0913 pass time.sleep(0.5) - _fail(log_path, f"pywrangler dev was not ready within {timeout}s") + fail_with_log(log_path, f"pywrangler dev was not ready within {timeout}s") def _terminate(process: subprocess.Popen[bytes], timeout: int) -> None: