Isolate release-tooling tests from each other's progress files - #7278
Isolate release-tooling tests from each other's progress files#7278bjlittle wants to merge 2 commits into
Conversation
`IrisRelease.merge_back` writes a progress file for the *next* patch,
named after that patch rather than date-stamped like every other
`nothing` progress file:
next_patch_stem = self._get_file_stem().with_stem(next_patch_str)
`nothing.Progress._get_file_stem` resolves `.nothing/` against
`Path().cwd()`, which every `pytest-xdist` worker shares. Two tests
reach that code with the same version -- `TestMergeBack::test_branches`
in its `more_patches` parametrisation, and `test_next_patch_file` --
so both resolve to the one `.nothing/v1_1_1.json`.
That is a race, not a conflict. `NextPatch(...)` writes the JSON
non-atomically and then verifies by reloading it, and its logger opens
the sibling `.log` with `mode="w"`. `test_next_patch_file` loads the
same path back and asserts on its contents. With no `--dist` setting
in `pyproject.toml`, xdist schedules dynamically, so whether the two
land on the same worker varies run to run: the observed `py3.14` failure
on SciTools#7276 reproduced on no other job, and the identical commit passed on
re-run.
Give each test its own working directory. An autouse fixture is the
right scope because `_get_file_stem` runs during `__post_init__`, so
every construction of `IrisRelease` is exposed, not just the two tests
that collide today. All three `subprocess` calls in
`release_do_nothing.py` are the git helpers already mocked by autouse
fixtures, and every other path it builds is anchored to `Path(__file__)`,
so moving the working directory reaches nothing else.
This also stops the suite writing `.nothing/` into the repository as a
side effect -- the reason `**/.nothing` sits in `.gitignore`.
The race cannot be reproduced on demand, so the accompanying test
asserts the isolation that prevents it instead of the failure itself.
Co-Authored-By: Claude Opus 5 <[email protected]>
Co-Authored-By: Claude Opus 5 <[email protected]>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #7278 +/- ##
=======================================
Coverage 90.35% 90.35%
=======================================
Files 93 93
Lines 25816 25816
Branches 4796 4796
=======================================
Hits 23327 23327
Misses 1709 1709
Partials 780 780 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Codex review (model: GPT-5): I reviewed the fixture change, its interaction with pytest fixture ordering and Local verification completed successfully both serially and in parallel: Residual risk is low and limited to a future test in this module intentionally depending on the repository working directory. |
🤖 Agentic pull request
This was written by Claude (Opus 5), driven by @bjlittle. The commit carries a
Co-Authored-Bytrailer. Please review it as you would any other contribution.Found while investigating the
tests (py3.14)failure on #7276, which wasunrelated to that pull request.
The failure
tools/test_release_do_nothing.py::TestMergeBack::test_next_patch_filefailedon one job and no other. Re-running the identical commit passed. That pattern is
the diagnosis: it is a race between tests, not a bug in the code under test.
What races
IrisRelease.merge_backwrites a progress file for the next patch. Unlikeevery other
nothingprogress file, it is named after that patch instead ofbeing date-stamped —
tools/release_do_nothing.py:1292:nothing.Progress._get_file_stemresolves.nothing/againstPath().cwd(),which every
pytest-xdistworker shares. So the directory is the only thingdistinguishing one test's progress file from another's, and it does not vary.
Two tests reach that line with the same version, and so with the same path,
.nothing/v1_1_1.json:TestMergeBack::test_branches, in itsmore_patchesparametrisationTestMergeBack::test_next_patch_fileBoth set
git_tag = "v1.0.1"andpatch_min_max_tag = ("v1.0.1", "v1.2.1"),which makes the next patch
v1.1.1in each case.It is a race rather than a plain clash because the writes are not atomic and the
reads are not guarded:
NextPatch(...)writes the JSON withwrite_textand then verifies it byloading it straight back.
.logwithmode="w", truncating it.test_next_patch_fileloads the same path and asserts on its contents.pyproject.tomlsets no--dist, so xdist uses dynamic scheduling. Whetherthose two tests land on the same worker varies from run to run, which is exactly
the intermittency observed.
The fix
A module-level autouse fixture giving each test its own working directory.
Autouse is the right scope rather than patching the two colliding tests:
_get_file_stemruns during__post_init__, so every construction ofIrisReleaseis exposed to the shared directory, not just today's twooffenders.
Moving the working directory is safe here. All three
subprocesscalls inrelease_do_nothing.pyare the git helpers (_git_remote_v,_git_remote_get_url,_git_ls_remote_tags), each already mocked by an autousefixture; every other path the module builds is anchored to
Path(__file__).It also stops the suite writing
.nothing/into the working copy as a sideeffect — which is why
**/.nothingis in.gitignore(.gitignore:85). Onmain,pytest toolsleaves.nothing/v1_1_1.jsonandv1_1_1.login therepository root. With this change it leaves nothing behind.
On testing a race
The failure is scheduling-dependent and cannot be reproduced on demand, so
test_progress_files_are_isolatedasserts the isolation that prevents it ratherthan the failure itself.
It is a real regression test: with the fixture body removed it fails, and names
the repository root in the message.
Testing
pytest toolsgives122 passed, 1 skipped, against121 passed, 1 skippedonmain— the one addition is the new test. Passes both serially and under-n auto, and leaves no.nothing/in the repository.Note that
pytest toolsneeds thenothingpackage, which is installed only bynoxfile.pyand appears in no requirements or lock file. That is #7277, raisedseparately for the core developers to rule on; it is an observation, not a
blocker for this.
Why this targets
mainThis is a repository-wide CI fix, not part of the merge/concatenate programme
that the
greenfieldfeature branch carries.greenfieldneeds it too — it inherits the same test module and the samefailure. It should pick it up by mergeback from
main, not a cherry-pick,so the branch keeps a single ancestry and does not carry a duplicate commit that
would have to be resolved later.
greenfielddescends frommain, so the mergeis clean. @bjlittle and I will raise that mergeback once this lands.