Skip to content

Commit a565574

Browse files
authored
Skip absent test dependencies, and stop unit runs deleting the integration cache (#27)
Running the full test suite against a Babel release that actually publishes the DuckDB Parquet files produced **4 failures and 4 errors**, and not one of them was a defect in the code under test. Chasing those turned up a fifth problem that had been corrupting the runs themselves: any `pytest` invocation deleted the integration cache, including the `-m "not integration"` loop you run constantly while editing. This PR makes the suite report absent dependencies honestly and stop sabotaging itself, so that a red run means a real problem again. It is **tests and documentation only** — no change to `src/`. Closes #19. ## What's here **A file being published is settled per file, not per session.** A release can publish `Concord.parquet` without `Identifiers.parquet` — the current one does exactly that, serving a 4.6 GB Concord with no Identifiers beside it — so "does this release have the Parquet files?" is not one question. Every DuckDB file now goes through `_download_or_skip()`, which skips on the `MissingBabelFileError` the downloader already raises for a 404 on a `duckdb/` path. `shared_downloader` is left answering only what is genuinely session-wide — is the server reachable — and probes the release root rather than a named file, so it cannot be misread as a publication check that forgot to check anything. **A unit-test run no longer deletes the integration cache.** `pytest_sessionfinish` removed `data/test` unconditionally, and that path is fixed rather than per-run, so *any* session did it. A `pytest -m "not integration"` in one terminal silently destroyed a multi-gigabyte download in progress in another, and the integration run simply started over — which reads as a downloader defect rather than an unrelated `pytest` two windows away. The cleanup now runs only when the session actually selected integration tests. The unit suite never creates or reads that directory, so it has no business removing it. **An unreachable NodeNorm skips instead of failing.** The `nodenorm` fixture only constructed a client, so a connection error surfaced inside each test body and failed it. CI runs `integration-test` on every push to `main` and on a weekly cron, so a RENCI outage turned the build red for reasons unrelated to any code change — the exact failure the Babel-side skip was written to avoid. The fixture now probes `status` once and skips on a `RequestException`. **`NCIT:C55060` is removed from the test CURIEs.** It has no rows at all in `Concord.parquet` as of Babel `2026jul22`, so the four tests parametrized over it asserted `len(results) > 0` against an empty list. NodeNorm still resolves it — as *"Hypertension, CTCAE"*, a `biolink:PhenotypicFeature` with a clique of 2, from Babel `2025sep1` — so this is not a typo or a retired identifier, and something changed between those two releases. That question is #29, not this PR's: whether Babel was right to drop the cross-references is a question about Babel's data, and if a good one went for a bad reason it belongs upstream. `valid_curies.txt` keeps a two-line pointer so the short list explains itself. **`Identifiers.parquet` is checked for being a Parquet file, not for being large.** The assertion was `size > 2 GB`, a figure picked against a release long since superseded. It would fail a legitimately smaller future release while passing a 2.5 GB *truncated* download. It now checks the `PAR1` marker at both ends, which tests what actually goes wrong: a download cut short, or an error page saved under a `.parquet` name. ## What it produces Against a release that publishes the Parquet files, the full suite is **299 passed, 3 skipped, 0 failures, 0 errors** (16m31s, dominated by the 4.6 GB download). It was 296 passed / 4 failed / 4 errors / 28 skipped before. The 25 tests that turned from skips into passes are the Parquet-dependent ones that had never actually run — they now exercise real data, including the DuckDB query paths and the recursive expansion. Against a release without the files, the suite still skips cleanly, as documented. Nine new unit tests cover the three guards, because this whole class of bug is otherwise invisible: these code paths only execute during an integration run, and CI's integration job skips, so nothing would notice a guard being dropped. It is the same blind spot that hid ten stale `get_curie_xref.cache_clear()` calls until a review found them by reading rather than running. Each guard was confirmed non-vacuous by removing it and watching the test fail. Four things that cost real time are now in `CLAUDE.md`: that a release can publish one DuckDB file without the other; that `pytest_sessionfinish` deletes `data/test` so a full run re-pays the download; that two concurrent *integration* runs still clash, with how to detect, avoid and recover; and that `addopts` carries `-n auto`, so a serial run needs `-n0` rather than `-p no:xdist`. ## What it deliberately does not do - **Diagnose `NCIT:C55060`.** Tracked in #29, which covers both outcomes: picking a replacement CURIE if Babel was right to drop it, or taking it upstream if a genuinely good cross-reference went for a bad reason. - **Make two concurrent integration runs safe.** Unavoidable while `data/test` is a shared fixed path. Documented instead, with the symptom spelled out, because it presents as a downloader bug. - **Cover an outage that begins mid-run.** Both probes are single up-front checks. A service that dies between the probe and the call still fails the test, which is correct — the probes exist for the case that recurs. - **Give CI a release that publishes the Parquet files.** Filed as #28, to be decided alongside #18's sizing question, since `Concord.parquet` alone is a 4.6 GB download per run. - **Clean up DuckDB spill files.** Filed as #26. ## Nothing is blocking this merge Everything outstanding is tracked in #18, #26, #28 and #29, and none of it makes what ships here wrong. #29 is the one worth reading before merging: it records that the test CURIE list is now two entries, both `MONDO:`, so the integration tests exercise no other namespace until a third is restored. That is reduced coverage, not a defect in this change. <details> <summary><b>How this landed</b> — two things reached their final shape by correction rather than design. Kept for anyone tracing why a line looks the way it does; the durable conclusions are in the code comments and CLAUDE.md above.</summary> **The per-file skip arrived in two steps.** The `Identifiers.parquet` skip was first added inline in the `downloaded_identifiers` fixture, duplicating the `MissingBabelFileError` handling that `shared_downloader` was doing with a HEAD probe. That left two mechanisms for one rule, which prompted collapsing both onto `_download_or_skip()` and reducing `shared_downloader` to a reachability check — and then re-pointing that check at the release root, since a probe naming `Concord.parquet` still looked like the publication check it had stopped being. **The cache deletion was misdiagnosed twice before the cause turned up.** The symptom was a full run whose `data/test` shrank instead of growing and that never finished. It was blamed first on the new "a resume requires a validator" rule in the downloader — wrong, the server sends both an ETag and a Last-Modified, so that rule never fired — and then on two concurrent full suites, which was real but not the cause. The actual culprit was the unit suite: every `pytest -m "not integration"` run during the investigation deleted the download it was investigating. That is why `pytest_sessionfinish` now carries an explicit "do not remove this guard" comment naming the misdiagnosis, and why CLAUDE.md describes the symptom rather than just the rule — the failure mode is much easier to recognise than to reason out. </details>
2 parents a07d647 + 3f8c530 commit a565574

5 files changed

Lines changed: 282 additions & 23 deletions

File tree

CLAUDE.md

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,15 @@ uv run pytest -v -m "integration and not slow"
184184

185185
# Run a single test file
186186
uv run pytest -v tests/test_nodenorm.py
187+
188+
# Run serially, e.g. to read one test's output
189+
uv run pytest -v -n0 tests/test_nodenorm.py
187190
```
188191

192+
`[tool.pytest.ini_options]` puts `-n auto` in `addopts`, so every run is parallel by default.
193+
Disable it with `-n0`, **not** `-p no:xdist` — unloading the plugin leaves the already-parsed
194+
`-n` behind and pytest exits with `unrecognized arguments: -n`.
195+
189196
### Linting
190197

191198
**Run both of these before committing or pushing.** CI checks them on every PR, and a push
@@ -309,9 +316,44 @@ uv run pytest --collect-only -q # full count
309316
a couple of dozen skips is the expected result without a Translator `BABEL_RELEASES_URL` in `.env`, not a
310317
broken test environment.
311318

319+
**A release can publish one DuckDB file without the other.** `2026jul22` serves a 4.6 GB
320+
`Concord.parquet` with no `Identifiers.parquet` beside it, so "does this release have the Parquet
321+
files?" is not one question. Every DuckDB file goes through `_download_or_skip()`, which skips on
322+
the `MissingBabelFileError` the downloader already raises for a 404 on a `duckdb/` path.
323+
`shared_downloader` answers only the genuinely session-wide question — is the server reachable —
324+
and deliberately probes the release root rather than a specific file, so it cannot be mistaken for
325+
a publication check. Do not collapse these back into one up-front probe: the non-slow integration
326+
tests run perfectly well against a release that lacks `Identifiers.parquet`.
327+
328+
**A full run re-downloads everything.** `pytest_sessionfinish` deletes `data/test`, so each
329+
`uv run pytest` with a Babel release configured pays the multi-gigabyte download again (~9 minutes
330+
for `2026jul22`). Use `-m "not integration"` while iterating, and budget for the full run.
331+
332+
**Only one pytest session may touch `data/test` at a time.** It is a fixed path shared by every
333+
run, not a per-run temporary directory, and `pytest_sessionfinish` deletes it at the end of the
334+
session. The `FileLock` around each download does not help: it guards one file, not a session.
335+
336+
`pytest_sessionfinish` skips the cleanup when the session selected no `integration` tests, so the
337+
fast `-m "not integration"` loop you run while editing is safe alongside a long integration run.
338+
**Do not remove that guard.** Without it, every unit run — the most frequent command in this
339+
repository — silently deletes a multi-gigabyte download in progress in another terminal, and the
340+
integration run just starts over, looking like a downloader bug rather than an unrelated `pytest`
341+
invocation two windows away. This was diagnosed twice as a resume defect before the real cause
342+
turned up.
343+
344+
Two concurrent *integration* runs still clash, and nothing here prevents that:
345+
346+
- **Detect it**: `du -sh data/test` going *down* instead of up, and a run sailing well past the ~9
347+
minutes it should take. `ps -eo pid,etime,command | grep "[p]ytest"` showing two sets of workers
348+
with different `etime` values confirms it.
349+
- **Avoid it**: check for a run already in flight before starting one, and never `rm -rf data/test`
350+
to "start clean" without checking first — that is the fastest way to corrupt a run in progress.
351+
- **Recover**: kill every pytest process, `rm -rf data/test`, and start exactly one run. Nothing
352+
outside `data/test` is affected, so no repository or cache state needs repairing.
353+
312354
### Test Infrastructure
313355

314-
- **`tests/conftest.py`** — Session-scoped fixtures that download Parquet files once and share them across all integration tests. The `shared_downloader` fixture HEADs `duckdb/Concord.parquet` first and skips the session on 404. Teardown removes the `data/test/` directory so the next run starts fresh.
356+
- **`tests/conftest.py`** — Session-scoped fixtures that download Parquet files once and share them across all integration tests. `shared_downloader` HEADs the release root and skips the session if the server is unreachable; `_download_or_skip()` then skips per file if the release does not publish it. `nodenorm` probes `status` the same way, so a NodeNorm outage skips rather than reddening CI. Teardown removes the `data/test/` directory so the next run starts fresh — see the concurrency warning above before running two suites at once.
315357
- **`tests/constants.py`** — Shared constants (URLs, file paths) and `load_curies()` helper.
316358
- **`tests/data/valid_curies.txt`** — One CURIE per line (`#` comments allowed). Integration tests are parametrized over this list — adding a new line automatically expands test coverage.
317359

tests/conftest.py

Lines changed: 71 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from filelock import FileLock
1414

1515
from babel_explorer.core.babel_xrefs import BabelXRefs
16-
from babel_explorer.core.downloader import BabelDownloader
16+
from babel_explorer.core.downloader import BabelDownloader, MissingBabelFileError
1717
from babel_explorer.core.nodenorm import NodeNorm
1818
from tests.constants import (
1919
BABEL_URL,
@@ -38,6 +38,11 @@ def valid_curies() -> list[str]:
3838
return curies
3939

4040

41+
def _selected_any_integration_test(session) -> bool:
42+
"""Did this session actually select a test that uses ``data/test``?"""
43+
return any(item.get_closest_marker("integration") for item in session.items)
44+
45+
4146
def pytest_sessionfinish(session, exitstatus):
4247
"""Remove the shared test data directory once every worker has finished.
4348
@@ -50,9 +55,19 @@ def pytest_sessionfinish(session, exitstatus):
5055
down at an unpredictable time and gw0 could delete Concord.parquet while gw5
5156
is still reading it. Guarding that teardown on the worker id, as this used to,
5257
meant the directory was simply never removed.
58+
59+
**A session that selected no integration tests cleans up nothing.** ``data/test``
60+
is a fixed path, not a per-run temporary directory, so an unconditional delete here
61+
means any ``pytest -m "not integration"`` — the fast loop you run constantly while
62+
editing — silently destroys the multi-gigabyte download of an integration run going
63+
on in another terminal. The unit suite never creates or reads that directory, so it
64+
has no business removing it. Two concurrent *integration* runs still clash; that one
65+
is unavoidable while the path is shared, and is documented in CLAUDE.md.
5366
"""
5467
if hasattr(session.config, "workerinput"):
5568
return
69+
if not _selected_any_integration_test(session):
70+
return
5671
if os.path.exists(TEST_DATA_DIR):
5772
shutil.rmtree(TEST_DATA_DIR, ignore_errors=True)
5873

@@ -68,20 +83,43 @@ def test_data_dir():
6883
return TEST_DATA_DIR
6984

7085

86+
def _download_or_skip(downloader, remote_path, test_data_dir, lock_name) -> str:
87+
"""Download one Babel file, skipping the tests that need it if the release omits it.
88+
89+
One rule, one implementation, for every DuckDB file. A release can publish
90+
``Concord.parquet`` without ``Identifiers.parquet`` — ``2026jul22`` does exactly that
91+
— so "does this release have the Parquet files?" is a question each file has to answer
92+
for itself, not once for the session. ``MissingBabelFileError`` is what the downloader
93+
already raises on a 404 for a ``duckdb/`` path, so there is no second HEAD request here
94+
to drift out of step with the real one.
95+
"""
96+
lock_path = os.path.join(test_data_dir, lock_name)
97+
with FileLock(lock_path):
98+
try:
99+
return downloader.get_downloaded_file(remote_path)
100+
except MissingBabelFileError as e:
101+
pytest.skip(str(e))
102+
103+
71104
@pytest.fixture(scope="session")
72105
def shared_downloader(test_data_dir) -> BabelDownloader:
73106
"""A BabelDownloader pointed at the test data directory.
74107
75-
Skips the whole session when the composed Babel URL points at a release that does
76-
not publish the DuckDB Parquet files (as the public releases currently do not).
108+
Skips the session when the Babel server cannot be reached at all. Whether a given
109+
file is *published* is settled per file by ``_download_or_skip``, not here — see
110+
that function for why the two cannot be collapsed into one probe.
111+
112+
The probe deliberately targets the release root rather than ``Concord.parquet``.
113+
Naming a file made this look like a publication check, which is what it used to be;
114+
only the response status told the two apart, and that distinction is gone now.
115+
Reachability is all this answers, so it asks about the release, not a file in it —
116+
and the status is not examined, because a 404 from a reachable server is still a
117+
reachable server.
77118
"""
78-
probe_url = BABEL_URL + CONCORD_FILE
79119
try:
80-
response = requests.head(probe_url, timeout=30)
120+
requests.head(BABEL_URL, timeout=30)
81121
except requests.RequestException as e:
82-
pytest.skip(f"Babel server unreachable at {probe_url}: {e}")
83-
if response.status_code == 404:
84-
pytest.skip(f"{BABEL_URL} does not publish {CONCORD_FILE}")
122+
pytest.skip(f"Babel server unreachable at {BABEL_URL}: {e}")
85123
return BabelDownloader(url_base=BABEL_URL, local_path=test_data_dir)
86124

87125

@@ -92,17 +130,17 @@ def downloaded_concord(shared_downloader, test_data_dir) -> str:
92130
Multi-gigabyte in current releases and growing; do not record a figure here,
93131
it drifts silently and then misleads.
94132
"""
95-
lock_path = os.path.join(test_data_dir, "concord.lock")
96-
with FileLock(lock_path):
97-
return shared_downloader.get_downloaded_file(CONCORD_FILE)
133+
return _download_or_skip(
134+
shared_downloader, CONCORD_FILE, test_data_dir, "concord.lock"
135+
)
98136

99137

100138
@pytest.fixture(scope="session")
101139
def downloaded_metadata(shared_downloader, test_data_dir) -> str:
102140
"""Download duckdb/Metadata.parquet (small). Returns the local path."""
103-
lock_path = os.path.join(test_data_dir, "metadata.lock")
104-
with FileLock(lock_path):
105-
return shared_downloader.get_downloaded_file(METADATA_FILE)
141+
return _download_or_skip(
142+
shared_downloader, METADATA_FILE, test_data_dir, "metadata.lock"
143+
)
106144

107145

108146
@pytest.fixture(scope="session")
@@ -119,15 +157,30 @@ def downloaded_identifiers(shared_downloader, test_data_dir) -> str:
119157
"""Download duckdb/Identifiers.parquet, the largest file Babel publishes.
120158
121159
Every test that reaches this is marked ``slow``.
160+
161+
Skips when the release does not publish it, which is not the same question as
162+
whether it publishes ``Concord.parquet`` — see ``_download_or_skip``.
122163
"""
123-
lock_path = os.path.join(test_data_dir, "identifiers.lock")
124-
with FileLock(lock_path):
125-
return shared_downloader.get_downloaded_file(IDENTIFIERS_FILE)
164+
return _download_or_skip(
165+
shared_downloader, IDENTIFIERS_FILE, test_data_dir, "identifiers.lock"
166+
)
126167

127168

128169
@pytest.fixture(scope="session")
129170
def nodenorm() -> NodeNorm:
130-
"""A NodeNorm client pointed at the public API."""
171+
"""A NodeNorm client pointed at the public API.
172+
173+
Probes ``status`` once and skips the NodeNorm integration tests if it cannot be
174+
reached, matching what ``shared_downloader`` does for Babel. Without this a RENCI
175+
outage turned every one of these tests red on CI's push and weekly-cron runs — a
176+
failure that says nothing about the code under test. The probe cannot cover an
177+
outage that begins mid-run; it covers the case that actually recurs.
178+
"""
179+
probe_url = NODENORM_URL.rstrip("/") + "/status"
180+
try:
181+
requests.get(probe_url, timeout=30).raise_for_status()
182+
except requests.RequestException as e:
183+
pytest.skip(f"NodeNorm unreachable at {probe_url}: {e}")
131184
return NodeNorm(nodenorm_url=NODENORM_URL)
132185

133186

tests/data/valid_curies.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Valid CURIEs for integration tests.
22
# Add new CURIEs here to expand test coverage — tests are parametrized over this list.
3+
#
4+
# NCIT:C55060 was removed in #27: it has no rows in Concord.parquet as of Babel
5+
# 2026jul22. Tracked in #29, which also covers restoring a third CURIE — with only
6+
# these two, both MONDO, the suite exercises no other namespace.
37
MONDO:0004979
48
MONDO:0005044
5-
NCIT:C55060

tests/test_downloader.py

Lines changed: 122 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
import pytest
1515
import requests
16+
from _pytest.outcomes import Skipped
1617

1718
from babel_explorer.core.downloader import (
1819
VERSION_MARKER,
@@ -22,6 +23,7 @@
2223
compose_babel_url,
2324
resolve_babel_version,
2425
)
26+
from tests import conftest
2527
from tests.constants import BABEL_URL, CONCORD_FILE
2628

2729

@@ -1279,7 +1281,124 @@ def test_download_caching_real_files(shared_downloader, downloaded_concord):
12791281
@pytest.mark.integration
12801282
@pytest.mark.slow
12811283
def test_download_identifiers_parquet(downloaded_identifiers):
1282-
"""Verify Identifiers.parquet downloads and is > 2 GB."""
1284+
"""Verify Identifiers.parquet downloads as a complete Parquet file.
1285+
1286+
Checks the format rather than a byte count. A hard size figure is exactly the kind
1287+
of number CLAUDE.md says drifts silently and then misleads — it was ``> 2 GB``,
1288+
chosen against a release long superseded — and the ``PAR1`` marker at both ends is
1289+
a better test of the thing that actually goes wrong: a truncated download, or an
1290+
error page saved under a .parquet name.
1291+
"""
12831292
assert os.path.isfile(downloaded_identifiers)
1284-
size = os.path.getsize(downloaded_identifiers)
1285-
assert size > 2 * 1024 * 1024 * 1024, f"Identifiers.parquet too small: {size} bytes"
1293+
assert os.path.getsize(downloaded_identifiers) > 8, "too short to be a Parquet file"
1294+
with open(downloaded_identifiers, "rb") as f:
1295+
assert f.read(4) == b"PAR1", "missing Parquet header"
1296+
f.seek(-4, os.SEEK_END)
1297+
assert f.read(4) == b"PAR1", "missing Parquet footer — download was truncated"
1298+
1299+
1300+
class TestIdentifiersFixtureSkips:
1301+
"""A release that omits Identifiers.parquet must skip, not error.
1302+
1303+
The bug this guards was invisible for the same reason the stale
1304+
`get_curie_xref.cache_clear()` calls were: it only shows up in a run against a
1305+
real Babel release, and those skip entirely for anyone without one configured.
1306+
A unit test is the only place it gets exercised routinely.
1307+
"""
1308+
1309+
@staticmethod
1310+
def _call_fixture(downloader, tmp_path):
1311+
"""Invoke the fixture's underlying function directly, past the decorator."""
1312+
return conftest.downloaded_identifiers.__wrapped__(downloader, str(tmp_path))
1313+
1314+
def test_missing_file_skips(self, tmp_path):
1315+
downloader = Mock()
1316+
downloader.get_downloaded_file.side_effect = MissingBabelFileError(
1317+
"This Babel release (2026jul22) does not publish duckdb/Identifiers.parquet."
1318+
)
1319+
1320+
with pytest.raises(Skipped) as excinfo:
1321+
self._call_fixture(downloader, tmp_path)
1322+
1323+
# The downloader's own message explains how to point at a release that has it.
1324+
assert "does not publish duckdb/Identifiers.parquet" in str(excinfo.value)
1325+
1326+
def test_present_file_is_returned(self, tmp_path):
1327+
"""The skip must not swallow the normal path."""
1328+
downloader = Mock()
1329+
downloader.get_downloaded_file.return_value = (
1330+
"/cache/duckdb/Identifiers.parquet"
1331+
)
1332+
1333+
assert (
1334+
self._call_fixture(downloader, tmp_path)
1335+
== "/cache/duckdb/Identifiers.parquet"
1336+
)
1337+
1338+
def test_other_errors_still_propagate(self, tmp_path):
1339+
"""Only a missing file is a skip; a real failure must still fail the run."""
1340+
downloader = Mock()
1341+
downloader.get_downloaded_file.side_effect = RuntimeError("connection reset")
1342+
1343+
with pytest.raises(RuntimeError, match="connection reset"):
1344+
self._call_fixture(downloader, tmp_path)
1345+
1346+
1347+
class TestSessionFinishCleanup:
1348+
"""A unit run must not delete an integration run's multi-gigabyte download.
1349+
1350+
`data/test` is a fixed path, not a per-run temporary directory, so the cleanup hook
1351+
is the one piece of test infrastructure that can destroy another process's work.
1352+
"""
1353+
1354+
@staticmethod
1355+
def _session(markers):
1356+
"""A stub session whose items carry the given marker names."""
1357+
1358+
def item(name):
1359+
it = Mock()
1360+
it.get_closest_marker.side_effect = lambda m, n=name: (
1361+
Mock() if m == n else None
1362+
)
1363+
return it
1364+
1365+
session = Mock()
1366+
session.items = [item(m) for m in markers]
1367+
del session.config.workerinput # a controller, not an xdist worker
1368+
return session
1369+
1370+
def test_unit_only_session_leaves_the_directory_alone(self, tmp_path, monkeypatch):
1371+
data_dir = tmp_path / "test"
1372+
data_dir.mkdir()
1373+
(data_dir / "Concord.parquet").write_bytes(b"someone else is using this")
1374+
monkeypatch.setattr(conftest, "TEST_DATA_DIR", str(data_dir))
1375+
1376+
conftest.pytest_sessionfinish(self._session([None, None]), 0)
1377+
1378+
assert data_dir.exists(), (
1379+
"a `-m 'not integration'` run must not delete the integration cache"
1380+
)
1381+
1382+
def test_integration_session_cleans_up(self, tmp_path, monkeypatch):
1383+
data_dir = tmp_path / "test"
1384+
data_dir.mkdir()
1385+
(data_dir / "Concord.parquet").write_bytes(b"this run's own download")
1386+
monkeypatch.setattr(conftest, "TEST_DATA_DIR", str(data_dir))
1387+
1388+
conftest.pytest_sessionfinish(self._session([None, "integration"]), 0)
1389+
1390+
assert not data_dir.exists(), "the next run must start fresh"
1391+
1392+
def test_xdist_worker_never_cleans_up(self, tmp_path, monkeypatch):
1393+
"""Only the controller cleans up; a worker finishing early must not."""
1394+
data_dir = tmp_path / "test"
1395+
data_dir.mkdir()
1396+
monkeypatch.setattr(conftest, "TEST_DATA_DIR", str(data_dir))
1397+
1398+
worker = Mock()
1399+
worker.items = [Mock()]
1400+
worker.config.workerinput = {"workerid": "gw0"}
1401+
1402+
conftest.pytest_sessionfinish(worker, 0)
1403+
1404+
assert data_dir.exists()

0 commit comments

Comments
 (0)