From b302bf6d254cc9bbbd12bb493f5621cfc3a294d4 Mon Sep 17 00:00:00 2001 From: Francis Secada Date: Wed, 19 Aug 2026 17:14:07 -0400 Subject: [PATCH 1/2] fix(tests): register django_cotton in the integration tier, isolate test-all by process (#79) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tests/integration/cotton_app/settings.py never listed django_cotton in INSTALLED_APPS, so its AppConfig.ready() never ran and DTL treated tags as inert literal text — test_cotton_integration.py's assertions were passing against uncompiled markup. Registering it exposed a bigger problem: django-cotton's AppConfig.ready() mutates settings.TEMPLATES in place and resets Django's global template-engine cache, a permanent process-wide side effect. justfile's test-all recipe ran tests/unit and tests/integration in one pytest process, so once cotton was registered its loader/builtins leaked into every unit test's render_to_string call and broke ~400 of them. CI was never affected — ci.yml already runs each tier as its own step — but test-all needed the same separation. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Ur2c6d3peuerUGgRdJ6Fvs --- justfile | 11 +++++++- tests/integration/conftest.py | 27 +++++++++----------- tests/integration/cotton_app/settings.py | 5 ++++ tests/integration/test_cotton_integration.py | 5 ++++ 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/justfile b/justfile index 0095021..96ab875 100644 --- a/justfile +++ b/justfile @@ -55,8 +55,17 @@ test-integration: test-e2e: pytest tests/e2e -q --tb=short +# Separate pytest invocations, not `pytest tests/` — django-cotton's +# AppConfig.ready() mutates settings.TEMPLATES in place and resets Django's +# global template-engine cache the moment it's in INSTALLED_APPS (see +# tests/integration/cotton_app/settings.py), so any single process that +# combines the integration tier with the unit tier leaks real cotton +# compilation into the unit tier's render_to_string calls. Matches how +# .github/workflows/ci.yml already runs each tier as its own step. test-all: - pytest tests/ -q --tb=short + pytest tests/unit -q --tb=short + pytest tests/integration -q --tb=short + pytest tests/e2e -q --tb=short check: lint lint-templates test diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index ff259a7..edacc8b 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,21 +1,18 @@ """ Integration test configuration. -Django can only be set up once per process. When running the full test suite, -tests/unit/conftest.py configures Django first with minimal settings. This -conftest handles the case where we run integration tests in isolation (so -Django is NOT yet configured) by setting DJANGO_SETTINGS_MODULE before -collection begins. - -When the full suite runs together, the unit conftest already configured Django. -The cotton integration tests use Django's test Client which works regardless of -which settings module was used to configure Django, as long as the required -apps (cf_ui.django.CfUiConfig) are in INSTALLED_APPS — which they are in both -settings modules. - -The ROOT_URLCONF setting differs: unit tests don't set it, integration tests -need it. The session-scoped fixture below ensures ROOT_URLCONF is set for all -integration tests regardless of which conftest configured Django first. +Django can only be set up once per process, so this tier must never share a +pytest process with tests/unit — see the justfile's `test-all` recipe and +.github/workflows/ci.yml, which both run tests/unit and tests/integration as +separate invocations for exactly this reason. django-cotton's +AppConfig.ready() (see tests/integration/cotton_app/settings.py) mutates +settings.TEMPLATES in place and resets Django's global template-engine cache +the moment it's in INSTALLED_APPS, so a combined process would leak real +cotton compilation into the unit tier's render_to_string calls. + +This conftest sets DJANGO_SETTINGS_MODULE before collection begins so Django +configures from tests/integration/cotton_app/settings — the settings module +this tier actually needs cotton registered under. """ import os diff --git a/tests/integration/cotton_app/settings.py b/tests/integration/cotton_app/settings.py index 5c72a6b..5438ac2 100644 --- a/tests/integration/cotton_app/settings.py +++ b/tests/integration/cotton_app/settings.py @@ -8,6 +8,7 @@ "django.contrib.contenttypes", "django.contrib.auth", "django.contrib.staticfiles", + "django_cotton", "cf_ui.django.CfUiConfig", ] DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}} @@ -23,5 +24,9 @@ } ] CF_UI_THEME = "bulma" +# django-cotton default COTTON_DIR="cotton" resolves -> +# cotton/cf/card.html, picked up via APP_DIRS from cf_ui's package templates. +# Allow hyphenated filenames (form-field.html, checkbox-group.html) — see #79. +COTTON_SNAKE_CASED_NAMES = False ROOT_URLCONF = "tests.integration.cotton_app.urls" STATIC_URL = "/static/" diff --git a/tests/integration/test_cotton_integration.py b/tests/integration/test_cotton_integration.py index 008a7bc..629b8fd 100644 --- a/tests/integration/test_cotton_integration.py +++ b/tests/integration/test_cotton_integration.py @@ -13,6 +13,9 @@ def test_form_field_cotton_renders(client): assert r.status_code == 200 assert b'name="email"' in r.content assert b"Email" in r.content + # The gallery template's raw `` tag must be + # compiled away, not passed through as literal text — see #79. + assert b" Date: Wed, 19 Aug 2026 17:30:44 -0400 Subject: [PATCH 2/2] fix(tests): enforce the tests/unit + tests/integration process-isolation invariant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /review on PR #80 found the invariant was only convention (justfile/CI recipe discipline), not enforced — a bare `pytest` (pyproject.toml's testpaths = ["tests"]) or `pytest tests/` still collects both tiers into one process and reintroduces the settings.TEMPLATES leak, producing ~400 confusing unit-test failures instead of an obvious error. Verified: `pytest -q` now fails fast with a clear pytest.UsageError instead of the silent breakage; both tiers still pass individually. Also corrected CLAUDE.md's now-stale claim that only E2E exercises real compilation — the integration tier does too as of this PR. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Ur2c6d3peuerUGgRdJ6Fvs --- CLAUDE.md | 2 +- tests/integration/conftest.py | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2851c8e..b56c1e4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -118,7 +118,7 @@ Three tiers: 2. **Integration** (`tests/integration/`) — FastAPI `TestClient` + Django test `Client`. Real HTTP, no browser. 3. **E2E** (`tests/e2e/`) — Playwright against live servers. Django E2E server runs as a subprocess with isolated settings to avoid Django singleton conflicts. Parameterized over `["js_on", "js_off"]`. -The Cotton unit tests pass even when cotton is not in INSTALLED_APPS (variables injected as raw context). Only E2E actually exercises `` compilation. Keep this in mind when debugging Cotton rendering issues. +The Cotton unit tests pass even when cotton is not in INSTALLED_APPS (variables injected as raw context) — that tier still doesn't exercise the django-cotton compiler. Integration and E2E both do: `tests/integration/cotton_app/settings.py` and `tests/e2e/_e2e_django_settings.py` both register `django_cotton`, so both exercise real `` compilation (each in its own process — see `tests/integration/conftest.py` for why that isolation matters). Keep this in mind when debugging Cotton rendering issues. ## Component Naming Convention diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index edacc8b..aef2dc1 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -31,6 +31,29 @@ def pytest_configure(config): django.setup() +def pytest_collection_modifyitems(items): + """Fail fast if tests/unit and tests/integration get collected together. + + A bare `pytest` (pyproject.toml's `testpaths = ["tests"]` default), + `pytest tests/`, or an IDE "run all tests" still collects both tiers + into one process — silently reintroducing the settings.TEMPLATES leak + this conftest's docstring describes, as hundreds of unrelated unit + failures rather than an obvious error. Catch it here instead. + """ + has_unit = any(item.nodeid.startswith("tests/unit/") for item in items) + has_integration = any(item.nodeid.startswith("tests/integration/") for item in items) + if has_unit and has_integration: + raise pytest.UsageError( + "tests/unit and tests/integration were collected into the same " + "pytest process. django-cotton's AppConfig.ready() mutates " + "Django's global template settings once tests/integration " + "registers it, which silently breaks tests/unit's " + "render_to_string calls. Run the tiers separately — " + "`just test-all`, or `pytest tests/unit` then " + "`pytest tests/integration`." + ) + + @pytest.fixture(scope="session", autouse=True) def _set_root_urlconf(): """Ensure ROOT_URLCONF is set for integration URL routing."""