ADR-0085 — Fixture scope discipline (function default, session opt-in for containers)¶
Status: accepted (committee plan_review session 94aa2881 PASSED 2026-05-06 ; ratified by the same gate as the S03 architecture dossier ; operator decision B on wp#118)
Date: 2026-05-06
Introduced by: CVN-N015-EA-S03 / GH issue #838 / OP wp#118
Companion document: documentation/reviews/2026-05-06-cvn-n015-ea-s03-architecture-plan.md §1 row B + §3
Context¶
Pytest fixtures have 4 scopes (function, class, module, session). Each represents a tradeoff :
- function (default in pytest) : safest — fresh setup per test, no cross-test state leak ; slowest — every test pays the setup/teardown cost
- session : fastest — setup runs once per pytest invocation, shared across all tests ; risky — any test that mutates the fixture's state pollutes every subsequent test
For CVNTrade's foundation Epic, the wall-clock budgets (S02 NF1-NF3 : p95 ≤ 2 min fast / ≤ 10 min integration / ≤ 30 min nightly) put pressure to use session scope for expensive resources (Testcontainers, MLflow tracking server, pre-trained model pickles). But the cost of cross-test isolation bugs is high : they manifest as flakes that appear only under specific test orderings, are hard to reproduce locally, and erode confidence in the suite over time.
Decision¶
Default fixture scope = function. Every fixture is function-scoped unless it appears on the explicit session-scope opt-in list below.
Session-scope opt-in list (canonical, frozen on this Story's merge) :
| Fixture | Why session-scope |
|---|---|
pg_container (Testcontainers Postgres) |
Postgres cold-start ~2 s × every test = unacceptable overhead. Tests use transaction rollback (per ADR-04 cache discipline analogue) for isolation. |
redis_container |
Cold-start ~1 s ; tests use unique key prefixes for isolation. |
minio_container |
Cold-start ~2 s ; tests use unique bucket names for isolation. |
mlflow_server |
Cold-start ~5 s (gunicorn boot dominated) ; tests use unique experiment names + tracking URIs for isolation. |
airflow_scheduler |
Cold-start ~8 s (DAG bag parse dominated) ; tests reset the metadata DB between tests (helper fixture, not session-scoped itself). |
Adding to this list requires an amendment Story. Local session-scoped fixtures (anywhere outside this list) are forbidden ; CI lint rule blocks them. This lint rule is a S04 deliverable and ships alongside the fast-tier workflow file (.github/workflows/ci-fast.yml) ; until S04 lands, the invariant is documented but not yet machine-enforced — operator + committee pr_review enforce it manually in the interim.
The wall-clock budgets are met via pytest-xdist -n auto parallelism (S02 NF4), NOT shared session state. The session-scope opt-in is a performance optimisation for expensive container cold-starts only — not a general-purpose escape hatch.
Discipline rules :
1. A function-scoped fixture MUST NOT modify shared state outside its own return value's lifecycle.
2. A session-scoped fixture (from the opt-in list) MUST provide a per-test isolation primitive (transaction rollback, unique prefix, unique bucket, unique experiment).
3. Tests that depend on a session-scoped fixture MUST use the isolation primitive ; CI runs the whole suite under pytest -n auto once daily to catch isolation bugs.
4. A test that fails under -n auto but passes under -n 1 is a fixture isolation bug, NOT a flake — it gets reported via the F5 flaky-test detector AND blocks the merge until fixed.
Consequences¶
Positive :
- Function-scope default eliminates an entire class of cross-test isolation bugs by construction
- The 5-fixture session-scope opt-in list is small + auditable + frozen — every reviewer can verify the cost/benefit ratio
- xdist parallelism remains the primary speed-up mechanism — predictable, well-tested in pytest ecosystem
- The "fail under -n auto, pass under -n 1" rule catches isolation bugs early, before they flake the suite
Negative / risks :
- Function-scope default measurably hurts the fast-tier 2-min budget if a popular fixture has non-trivial setup ; mitigation = expand the session-opt-in list via amendment Story (1-line change), not flip the default
- Per-test transaction rollback / unique-prefix isolation requires per-test discipline ; mitigation = pre-built isolation helpers in tests/fixtures/ (S04 deliverable)
Cross-references :
- S03 architecture dossier §1 row B (decision rationale) + §3 service-virtualisation conventions
- S02 NF4 parallelism contract (the speedup mechanism this ADR delegates to)
- ADR-0084 (foundation test stack pick — pytest + pytest-xdist)
- ADR-0086 (CI tier promotion gate — strict any-failure-blocks makes isolation bugs unmissable)
- ADR-77 (MkDocs SSoT)