Skip to content

cargo-test: perry-runtime's suite fails a different number of tests on every run #7365

Closed

Description

@proggeramlug

Summary

cargo test -p perry-runtime --lib fails a different number of tests on every run, on clean main, with no changes applied. It is a required check (cargo-test), so this is not cosmetic: a suite whose failure count is nondeterministic can hide a real regression indefinitely — a genuine new failure looks exactly like the noise.

Measurement

Ten consecutive runs on 8f05d65f9, unmodified:

3 failed   1 failed   1 failed   3 failed
4 failed   2 failed   2 failed   (earlier baseline, same tree)

Seven distinct tests are involved, by frequency across six sampled runs:

count test
5 gc::tests::teardown::map_set_owner_records_follow_growth
3 gc::tests::teardown::map_set_side_allocations_release_exactly_once
2 promise::keyed_table::tests::settling_many_keys_is_not_quadratic
1 url::node_compat::tests::path_to_file_url_posix_preserves_relative_trailing_slash
1 url::node_compat::tests::path_to_file_url_posix_does_not_add_slash_without_input_slash
1 object::prop_plan::tests::read_plan_roundtrip_and_epoch_flush
1 gc::tests::teardown::map_set_side_allocations_release_on_thread_exit

Every one of them passes in isolation. Verified individually:

gc::tests::teardown::map_set_owner_records_follow_growth              ok
gc::tests::teardown::map_set_side_allocations_release_exactly_once    ok
gc::tests::teardown::map_set_side_allocations_release_on_thread_exit  ok
object::prop_plan::tests::read_plan_roundtrip_and_epoch_flush         ok

So it is interference under parallel execution, not a defect in any individual test.

Two distinct causes, probably

1. Shared process-global state. The gc::tests::teardown::map_set_* family is 9 of the 14 observed failures. Those tests assert on side-table ownership records and release counts — process-global structures that other tests mutate concurrently. Related prior art: PERRY_GC_FORCE_EVACUATE's test-only mode override is deliberately thread-local "so one test enabling the instrument cannot change the collector's behaviour for any other test" (arena/quarantine.rs), and #7251 found a test made vacuous by libtest ordering, where array::tests built the intrinsic towers first. Same family of problem.

2. A timing assertion. settling_many_keys_is_not_quadratic asserts on complexity, which under a loaded 10-core box running 1600+ tests in parallel is a coin flip.

Why it matters now

cargo-test is a required status check. Today its signal is: "somewhere between 0 and 4 tests failed, and which ones varies." Nobody can distinguish a regression from the baseline noise, which is the same failure mode as a gate that cannot fail — it just fails uselessly instead of passing uselessly.

Suggested fix

  • The map_set_* teardown tests need serialisation against each other and anything else touching the side tables (a shared test lock, or #[serial]), or the global they assert on needs to be per-thread the way the quarantine's mode override already is.
  • The quadratic assertion should measure operation counts rather than wall time, or be marked #[ignore] under parallel runs.

Found while establishing a baseline for #7341 — I nearly filed a false regression against my own change before checking clean main gave the same varying counts.

Activity

  1. proggeramlug commented on Aug 10, 2026

    @proggeramlug
    ContributorAuthor

    A datum from running the suite repeatedly while doing unrelated work on this machine.

    Four back-to-back runs of cargo test -p perry-runtime --lib --no-fail-fast, on an otherwise-busy host (a full test-files/ parity sweep was saturating the CPU):

    run failures
    1 none
    2 promise::keyed_table::tests::draining_a_heavily_populated_key_ahead_of_another_is_not_quadratic
    3 gc::tests::inline_generation_gate_contract::sabotaged_parent_gate_strands_a_young_child_the_shipped_gate_keeps, promise::keyed_table::tests::settling_many_keys_is_not_quadratic
    4 promise::keyed_table::tests::settling_many_keys_is_not_quadratic

    That is the issue's symptom exactly — "a different number of tests on every run" — and it points at a mechanism: the two *_is_not_quadratic tests assert a wall-clock ratio, which is a property of how much CPU the test got, not of the code. Under load the ratio moves. On an idle host the same four runs are green, which is why this reads as random rather than as a load correlation.

    If that holds up, the fix shape for those two is to assert on an operation COUNT (or an algorithmic invariant) rather than elapsed time — the same "assert the subject, not a proxy for it" move CLAUDE.md's gate-hazard section describes. I have not looked at whether the inline_generation_gate_contract one has the same cause; it appeared once in four runs.

    Noting rather than fixing — it came up while validating #7752 and none of it is that PR's subject.

  2. proggeramlug commented on Aug 10, 2026

    @proggeramlug
    ContributorAuthor

    Named it. On main @ db44b31b7, perry-dev, macOS arm64, the intermittent failure in cargo test -p perry-runtime --lib is a single test:

    test gc::tests::root_words::bare_address_in_shadow_slot_survives_a_real_collection ... FAILED
    

    Rate: 2 of 10 consecutive runs on a clean tree (no local changes, archives rebuilt in between). Every failing run reports exactly 2051 passed; 1 failed, and every passing run 2052 passed; 0 failed — so on this host the "different number of tests every run" is one flaky test toggling, not a varying set.

    Found while validating an unrelated Intl change: it failed once in my working tree, and the only way to tell whether I had caused it was to revert to clean main, rebuild, and run ten times. That is the cost this issue describes — every unrelated PR pays it — so the name above is worth having even before the fix.

    Two things that make it easy to miss when you go looking:

    • The failing-test line does not survive the usual greps. cargo test's summary block is buried in a lot of output and my first three attempts at grep -E "^failures|FAILED" on the captured log came back empty; capturing each run to its own file and breaking on the first ^test result: FAILED is what surfaced it.
    • Re-running immediately is not evidence. At 2/10 a single green re-run "clears" it 80% of the time, which is exactly how it gets waved through as environmental.

    The test's own name states the property it is asserting — a bare address in a shadow slot surviving a real collection — so a 20% failure rate is a real intermittent liveness result, not a harness artifact. It should probably be triaged as a GC-correctness intermittency in its own right rather than only as test-suite noise.

  3. proggeramlug commented on Aug 10, 2026

    @proggeramlug
    ContributorAuthor

    Second flaky test, and this one is order-dependent, which changes how it should be hunted:

    test object::class_registry::dispatch::obj_dispatch_ic_tests::a_hit_requires_matching_name_bytes_not_a_matching_address ... FAILED
    

    Measured on clean main @ db44b31b7, perry-dev, macOS arm64:

    how it is run failure rate on clean main
    cargo test -p perry-runtime --lib obj_dispatch_ic_tests (isolated) 10 / 12
    full cargo test -p perry-runtime --lib ~1 / 6

    So it mostly passes in the suite and mostly fails alone — the opposite of the usual flake, and evidence it depends on state some other test leaves behind (an inline-cache table primed by an earlier test, given the name). That matters for this issue in two ways:

    • --exact / filtered reruns are misleading here. The standard triage move — rerun just the failing test to confirm — makes this one look reliably broken rather than flaky, and the same move on the other known offender (gc::tests::root_words::bare_address_in_shadow_slot_survives_a_real_collection, 2/10 in-suite) makes it look fine. Neither reproduces its in-suite rate in isolation.
    • It is why an unrelated PR cannot self-clear. I hit this while validating a class-registry change, so "did I break it?" was a live question: with the change 7/12 isolated, on clean main 10/12. The change made it less likely to fail, and only measuring both arms showed that. Every PR touching this area pays that same cost.

    Both tests together mean the suite has at least two independent intermittencies with different mechanisms — one timing, one ordering — so a single "retry until green" policy will not characterise either.

  4. proggeramlug commented on Aug 14, 2026

    @proggeramlug
    ContributorAuthor

    Closing this as superseded/resolved after reconciling every named failure against freshly fetched origin/main at 08173f8b403be848f2218746b22a462fd883042f.

    Exact failure → fix map

    Original issue-body set

    Failures named in later #7365 comments

    Later runtime-suite members were split out and closed explicitly

    No #7365 test was quarantined. The only four current perry-runtime ignores are two explicit dyn-eval benchmarks, one manual Promise performance probe, and an abort-on-purpose GC sabotage child; none belongs to this issue.

    Separate from #7365: #8060/#8061

    The six Map/Set indexed-read failures seen immediately before #8061 were a deterministic regression introduced by #8041, not a resurfacing of the flaky Map/Set teardown family. Strict array cleaning rejected live collections before their registry dispatch and returned NaN every time. #8061 (118f49d41192c248f36436b7e020acc291aebaff) restored tag-gated, authoritative-registry-confirmed collection routing before array-only cleaning. Its reported full-suite keyed-table failure came from a pre-#8059 branch/base and is covered by #8059 above.

    Scope of this closure

    This closes the finite evidence in #7365: every failure named in the issue and its comments has a merged causal fix on current main, and the later independently catalogued suite flakes were also resolved rather than retried or ignored. It is not an open-ended claim that every present or future perry-runtime test is forever safe under arbitrary parallelism.

    CLAUDE.md still carries a blanket single-thread warning whose concrete proxy/element-shape examples predate and were subsequently fixed by #7954. That wording should be reconciled separately; it is not evidence that one of the named #7365 failures remains live, and I did not edit it here.

    This was a source/history/status audit only: no CI result was used as proof, no timing-performance conclusion was drawn, and no broad local build/soak was run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions