Uh oh!
There was an error while loading. Please reload this page.
A test file with no tests was switching the units system on at collection (#567) — and CI can now run in parallel - #574
Merged
Conversation
… module too pytest builds higher-scoped fixtures before lower-scoped ones. The suite's isolation fixture is function-scoped, so a module-scoped fixture -- the natural home for a mesh several tests share -- is built BEFORE the first reset of the global model ever runs. Whatever the process did earlier is still in force, including module-level code that ran during collection. That is #567. A units test set reference quantities at import time, which switches the units system on globally. The point-locator suite builds its mesh and P1 variable in a module-scoped fixture, and with units active var.coords returns dimensional coordinates -- 0..2.9e6 metres instead of the mesh's 0..1 -- so the fixture filled nodal values sampled 2.9 million times too far apart and every later evaluation missed the closed form by O(1). It read as a broken locator and was nothing of the sort: the locator, the DMInterpolation cache and the kd-tree reach are all exonerated. It only bit when that file ran FIRST in its process, because otherwise an earlier file's per-test reset had already cleaned up. Under --dist loadfile the worker count decides which file goes first, which is why the suite was green serially and at 4 and 8 workers and red at 16. The new test runs a real pytest in a subprocess against a copy of the live conftest, so deleting the module-scoped reset makes it fail (measured: the fixture reads 2900000.0 where it must read 1.0). Underworld development team with AI support from Claude Code
… running at import test_0741_expression_arithmetic_units.py was a converted script: it collected zero tests, printed its findings, and created a Model with reference quantities at module level. That ran during pytest COLLECTION and switched the units system on for the whole process before a single test executed. It is the only file in the suite that did this (measured by importing every test module in turn), and it is the leak behind #567. The four products it was checking -- cm/year times Myr, spelled as quantities or as expressions, in either order -- are now four assertions, plus one that the units survive the trip to .sym, which is what the JIT compiler reads. The reference quantities live in a fixture that tears them down again. Underworld development team with AI support from Claude Code
The comment blocks in scripts/test.sh and the workflow both say CI stays in-process until #567 lands, and that setting WORKERS in the workflow is the whole remaining change. It has landed, so this is that change. Measured end to end on 16 cores with ./scripts/test.sh --p 2, both runs green with zero failures: serial batches 25.1 min -> 9.6 min at 8 workers MPI batches 300s -> 294s (untouched; they run their own mpirun) end to end 28:04 -> 12:21 The workflow gets WORKERS: 4, the vCPU count of the runner. Deliberately a literal: an earlier attempt to detect it inside the job got 1 and ran xdist with a single worker, paying a spawn and a fresh underworld3 import per batch to parallelise nothing. Underworld development team with AI support from Claude Code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#567.
It was never the locator
The three failing tests are point-locator tests, the assertion message says
"answered in a cell that does not contain it", and the failure moves with the
xdist worker count. Every one of those signals points at the locator. None of
them is right. The locator, the
DMInterpolationCache,mesh._index, the #560rejection reach and the per-cell face control points are all exonerated — the
cache key is per-mesh and no key collides.
evaluatereturned exactly theright value for the data it was given. The data was wrong, and it was
written by the test's own fixture.
The minimal reproducing pair
and it does not need xdist at all:
pytest --config-file=tests/pytest.ini \ tests/test_0741_expression_arithmetic_units.py \ tests/test_0761_point_locator.py \ -q -k test_p1_field_on_a_shared_edge # 3 failed, 3 passed, 19 deselectedEvery test in
test_0741is deselected there. In facttest_0741contained notests at all — it was a converted script whose whole body ran at module level.
Merely collecting it is enough to break the locator suite.
The leaking state
test_0741did this at import:That runs during pytest collection, and it switches the units system on for the
whole process. Importing every test module in turn says it is the only file
in the suite that mutates the global default model at import time.
conftest.isolate_test_stateresets the default model before every test, sothis should be harmless. It is not, and the reason is fixture ordering: pytest
builds higher-scoped fixtures first, so
test_0761's module-scopedlocated_boxfixture — which builds the mesh and the P1 variable the whole fileshares — is set up before the first test's function-scoped reset ever runs. A
function-scoped reset cannot protect a module-scoped fixture. Over thirty test
files have module- or session-scoped fixtures; the locator suite is simply the
one whose fixture reads coordinates.
With the units system active,
var.coordsreturns dimensional coordinates.Measured, on the unit box with a 2900 km length scale:
So
field.data[:, 0] = _nodal_signal(field.coords)sampled asin(97x)cos(89y)signal at points 2.9 million times too far apart and wrotethe result as nodal values. The oracle then computed the closed form from the
DM's vertex coordinates. Two different fields, compared: O(1) disagreement at
every point, which is exactly the reported signature.
Isolating the step with a standalone probe:
1.6747e+00is the number CI reported for[2d-0.5], to every digit.Why the worker count decided it
The pollution only reaches a module-scoped fixture if no earlier test in that
process has already run — one function-scoped reset cleans it up for good. So
the failure needs
test_0761to be the first file in its worker. Serially itnever is (
test_0500runs first). Under--dist loadfilethe worker countdecides the file grouping, and at 16 workers — and at 4, in CI — it lands first.
Confirmed directly: in the failing run
test_0761was the first file ongw0and its first three tests failed.
That also disposes of the ruled-out list in the issue. It was never about a
preceding file, which is why running the obvious neighbours before it changed
nothing; the polluter acts at collection, before any file runs.
User-facing, or test-only?
Test-only.
var.coordsreturning metres when the units system is on is thedocumented contract, not a defect. Two consecutive
evaluatecalls on differentmeshes cannot return each other's answers — no cache key collides. What was
wrong was a test fixture reading coordinates in one unit system and an oracle
reading them in another, and a test file that switched unit systems globally at
import.
The fix
Two commits.
tests/conftest.py— reset at module scope as well as function scope. Thisis the durable half. A
scope="module"autouse fixture is ordered before amodule's own fixtures, so every module-scoped fixture in the suite is now built
with a pristine default model regardless of what the process did earlier,
including at collection. The per-test reset stays as it was.
tests/test_0741_expression_arithmetic_units.py— stop running at import.The four products it was checking (
cm/year * Myr, spelled as quantities or asexpressions, in either order) are now five real assertions, and the reference
quantities live in a fixture that tears them down again. A test file with no
tests that mutates global state at import time is a hazard whatever else is
true.
Regression test
tests/test_0742_module_fixture_units_isolation.pyruns a real pytest in asubprocess against a copy of the live
conftest.py. The generated modulepollutes at import exactly as
test_0741did, and its module-scoped fixtureasserts that a unit-box variable's coordinates reach 1.0.
Negative control, with the module-scoped reset deleted from
conftest.py:It fails for the right reason and passes with the guard in place. Deleting the
guard is the only way to make it fail, which is what pins the fix rather than
the symptom.
Verification
The original reproduction, CI's own batch:
tests/test_05*py tests/test_07*py -n 4 --dist loadfileLevel 1, the whole selection from the issue:
The three named tests are green serially and at 4, 8 and 16 workers; the full
test_0761file is green serially (4.4s) and at 4 (6.0s), 8 (6.1s) and 16(7.2s) workers.
scripts/test_levels.sh 1(the developer loop #566 added): 1491 passed, 31skipped, 2 xfailed in 2:22, exit 0.
scripts/test.sh --p 2end to end, both configurations green with zerofailures:
WORKERS=82.6x on the serial batches, 2.3x end to end. The MPI batches are untouched —
they run their own
mpirun.CI parallelism is now safe to enable, and the third commit enables it
scripts/test.shand the workflow both carried a comment saying CI staysin-process until #567 lands and that setting
WORKERSin the workflow is thewhole remaining change. It has landed, so the third commit is that change:
WORKERS: 4inbuild_uw3_and_test.yaml(the runner's vCPU count, a literal —an earlier attempt to detect it inside the job got 1 and ran xdist with a single
worker, paying a spawn and a fresh underworld3 import per batch to parallelise
nothing), plus the stale comment blocks rewritten.
It is a separate commit deliberately. Drop it if you would rather watch a green
CI run on the fix alone first; the other two commits stand on their own.
Related: #572 raised the job cap to 120 min as a stop-gap, #573 tracks the
durable fix, #551 is the locator umbrella and #556 added the tests that caught
this.
Underworld development team with AI support from Claude Code