Skip to content

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)