Observed on
PR #719 CI run 32931567792 — Lint + test (macos-latest) lane:
test scan_fs::walk_registry::walker::tests::walker_skips_default_noise_dirs ... FAILED
test scan_fs::walk_registry::walker::tests::walker_survives_symlink_loop ... FAILED
thread 'scan_fs::walk_registry::walker::tests::walker_skips_default_noise_dirs' panicked at
waybill-cli/src/scan_fs/walk_registry/walker.rs:609:9:
top-level file should be visited; log=[]
The subsequent re-run (SHA 1b6d87a) passed the same tests — classic parallel-test flake symptom, not a real regression.
Root cause
Three tests in waybill-cli/src/scan_fs/walk_registry/walker.rs share one static log for their assertions:
static SEMANTICS_LOG: Mutex<Vec<String>> = Mutex::new(Vec::new());
Each test calls SEMANTICS_LOG.lock().unwrap().clear() at its start, populates it via a record_visit callback registered on the walker, then asserts against the collected entries.
Sites (as of dc8018d):
walker_survives_symlink_loop (line 488) — #[cfg(unix)]
walker_respects_exclusion_set (line 525)
walker_skips_default_noise_dirs (line 568)
Cargo runs test fns from the same target in parallel by default (--test-threads = nproc). When two of these three tests interleave, one calls .clear() after another has populated the log but before that other reads its assertions, leaving log=[] and tripping the "should be visited" assertion.
Local runs on macOS pass because scheduling happens to serialize the three tests; CI macOS-arm64 has different core counts and interleaves them differently. This is why the m664 merge lane passed and this PR's first run failed even though walk_registry/ had zero changes on this branch.
Fix options (any one)
- Per-test-local log: replace the static Mutex with a
Vec<String> owned by each test, threaded into record_visit via the SharedWalkerContext.state payload. Cleanest — removes the shared mutable state entirely.
- File-scoped serial Mutex: add a
SERIALIZE: Mutex<()> static and have each test acquire it BEFORE SEMANTICS_LOG.clear(). Cheap but only prevents interleaving inside this one file; other tests in the same binary that touch the log would still race.
#[serial] from the serial_test crate: standard-idiomatic. Adds a dev-dep the workspace doesn't currently use. Skip unless the pattern shows up in more places.
Recommend option 1 — it's the right shape for a walker that already threads a state: Option<Box<dyn Any + Send + Sync>> through SharedWalkerContext per m664 contract C4.
Not blocking anything
Neither test is a functional gate; both are unit-tests over walker semantics. The flake surfaces intermittently on macOS lanes and is worked around by re-running the failed lane. Filing so someone can fix at leisure and so the next occurrence doesn't get root-caused a second time.
Observed on
PR #719 CI run 32931567792 —
Lint + test (macos-latest)lane:The subsequent re-run (SHA
1b6d87a) passed the same tests — classic parallel-test flake symptom, not a real regression.Root cause
Three tests in
waybill-cli/src/scan_fs/walk_registry/walker.rsshare one static log for their assertions:Each test calls
SEMANTICS_LOG.lock().unwrap().clear()at its start, populates it via arecord_visitcallback registered on the walker, then asserts against the collected entries.Sites (as of
dc8018d):walker_survives_symlink_loop(line 488) —#[cfg(unix)]walker_respects_exclusion_set(line 525)walker_skips_default_noise_dirs(line 568)Cargo runs test fns from the same target in parallel by default (
--test-threads = nproc). When two of these three tests interleave, one calls.clear()after another has populated the log but before that other reads its assertions, leavinglog=[]and tripping the "should be visited" assertion.Local runs on macOS pass because scheduling happens to serialize the three tests; CI macOS-arm64 has different core counts and interleaves them differently. This is why the m664 merge lane passed and this PR's first run failed even though
walk_registry/had zero changes on this branch.Fix options (any one)
Vec<String>owned by each test, threaded intorecord_visitvia theSharedWalkerContext.statepayload. Cleanest — removes the shared mutable state entirely.SERIALIZE: Mutex<()>static and have each test acquire it BEFORESEMANTICS_LOG.clear(). Cheap but only prevents interleaving inside this one file; other tests in the same binary that touch the log would still race.#[serial]from theserial_testcrate: standard-idiomatic. Adds a dev-dep the workspace doesn't currently use. Skip unless the pattern shows up in more places.Recommend option 1 — it's the right shape for a walker that already threads a
state: Option<Box<dyn Any + Send + Sync>>throughSharedWalkerContextper m664 contract C4.Not blocking anything
Neither test is a functional gate; both are unit-tests over walker semantics. The flake surfaces intermittently on macOS lanes and is worked around by re-running the failed lane. Filing so someone can fix at leisure and so the next occurrence doesn't get root-caused a second time.