diff --git a/CLAUDE.md b/CLAUDE.md index 3b901dc27d..bd8719cfcd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1376 +**Current Version:** 0.5.1377 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 086f1fc4f3..b292fd183c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1376" +version = "0.5.1377" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1376" +version = "0.5.1377" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1376" +version = "0.5.1377" [[package]] name = "perry-ui-tvos" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1376" +version = "0.5.1377" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 6984708317..ecef104bbb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1376" +version = "0.5.1377" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/changelog.d/7665-test-isolation-global-sinks.md b/changelog.d/7665-test-isolation-global-sinks.md new file mode 100644 index 0000000000..e5ec904454 --- /dev/null +++ b/changelog.d/7665-test-isolation-global-sinks.md @@ -0,0 +1,14 @@ +**`fix(test-isolation)`: two process-global sinks recorded across test boundaries.** + +Both follow one pattern: a **process-global collector** paired with a lock that only serialises the tests which *take* it. Any concurrently-running test that exercises the recording path — without knowing the collector exists — wrote into the lock-holder's snapshot. + +- **`opt_report`** — `FORCED` and the `Mutex>` sink are global; `Session` drains on entry and drop but cannot stop a neighbour emitting mid-test. `only_the_return_position_is_marked_served` asserts `rows.len() == 2` and failed at **3**. +- **`ext_registry`** — `USED_PROVIDERS` is a process-wide `Mutex`; `record_ffi_call` fires from any lowering of an ext symbol. `ext_prefix_net_does_not_over_match` asserts the set is empty after an unlisted symbol and failed with `ioredis` still in it. + +Both are **pre-existing** and both were surfaced by #7662 (Layer 1 slice 7), which added `child_process` and Proxy/Reflect lowering tests and so changed the parallel schedule: `only_the_return_position_is_marked_served` was 0/14 on `main` and 2/18 on that branch; `ext_prefix_net_does_not_over_match` was 1/20. + +**Diagnosed from the extra row, not from the timing.** A 1-in-6 flake invites a retry; the value 3-instead-of-2 says a neighbour's entry is in the snapshot, which names the mechanism directly. + +Recording is now narrowed to the thread holding the guard — `test_support::recording_thread_is_current()` for `opt_report`, `ProviderTestGuard` + `provider_recording_permitted()` for `ext_registry`. **Production is untouched in both**: the `opt_report` check sits inside the `#[cfg(test)]` forced branch and the env-var path is unchanged, and `PROVIDER_TEST_THREAD` is `#[cfg(test)]` with the predicate returning `true` whenever no provider test is running. + +Verified 0 failures in 25 consecutive `cargo test -p perry-codegen --lib --no-fail-fast` runs on top of #7662, where the pair reproduced at 3/38 before. diff --git a/crates/perry-codegen/src/ext_registry.rs b/crates/perry-codegen/src/ext_registry.rs index b65d1ffa12..f4df38f3bd 100644 --- a/crates/perry-codegen/src/ext_registry.rs +++ b/crates/perry-codegen/src/ext_registry.rs @@ -635,6 +635,34 @@ const EXT_PREFIX_REGISTRY: &[(&str, &str)] = &[ /// optimization horizon worth measuring. static USED_PROVIDERS: Mutex>> = Mutex::new(None); +/// Test-only: the thread that holds `PROVIDER_TEST_LOCK`. +/// +/// `USED_PROVIDERS` is process-wide but `PROVIDER_TEST_LOCK` only serialises +/// tests that TAKE it, so a concurrently-running test that lowers any code +/// touching an ext symbol used to add providers to the holder's set. Observed: +/// `ext_prefix_net_does_not_over_match` asserts the set is empty after an +/// unlisted symbol and failed once in twenty once #7662 added `child_process` +/// lowering tests, which call `record_ffi_call`. Recording is narrowed to the +/// holder's thread while a provider test is running; production is untouched +/// (`PROVIDER_TEST_THREAD` is `None` outside tests, which permits everything). +#[cfg(test)] +static PROVIDER_TEST_THREAD: Mutex> = Mutex::new(None); + +/// May the calling thread record into `USED_PROVIDERS`? Always yes in +/// production and whenever no provider test holds the lock. +#[inline] +fn provider_recording_permitted() -> bool { + #[cfg(test)] + { + if let Ok(holder) = PROVIDER_TEST_THREAD.lock() { + if let Some(id) = *holder { + return id == std::thread::current().id(); + } + } + } + true +} + // Per-module capture buffer, active only between [`begin_module_capture`] // and [`take_module_capture`] on the same thread. // @@ -663,7 +691,7 @@ thread_local! { pub(crate) fn record_ffi_call(symbol: &str) { for (name, owner) in FFI_REGISTRY { if *name == symbol { - { + if provider_recording_permitted() { let mut guard = USED_PROVIDERS.lock().expect("USED_PROVIDERS poisoned"); guard.get_or_insert_with(HashSet::new).insert(*owner); } @@ -797,6 +825,30 @@ mod tests { static PROVIDER_TEST_LOCK: Mutex<()> = Mutex::new(()); + /// Takes `PROVIDER_TEST_LOCK` **and** claims `PROVIDER_TEST_THREAD`, so + /// `record_ffi_call` on any other thread stops writing into this test's + /// `USED_PROVIDERS` snapshot. Taking the lock alone is not enough: it only + /// serialises tests that take it, and a neighbouring lowering test does not. + struct ProviderTestGuard(std::sync::MutexGuard<'static, ()>); + + impl ProviderTestGuard { + fn new() -> Self { + let g = PROVIDER_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + if let Ok(mut t) = super::PROVIDER_TEST_THREAD.lock() { + *t = Some(std::thread::current().id()); + } + ProviderTestGuard(g) + } + } + + impl Drop for ProviderTestGuard { + fn drop(&mut self) { + if let Ok(mut t) = super::PROVIDER_TEST_THREAD.lock() { + *t = None; + } + } + } + /// #6439: per-module capture must see exactly what this thread emitted, /// sorted and deduped, and must end cleanly. This is what the object /// cache persists beside each `.o`. @@ -837,7 +889,7 @@ mod tests { /// the same providers, hence the same link line. #[test] fn replayed_symbols_route_like_live_codegen() { - let _guard = PROVIDER_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + let _guard = ProviderTestGuard::new(); let _ = take_used_providers(); replay_ffi_symbols(["js_ws_connect_start", "js_not_a_real_symbol"]); let got = take_used_providers(); @@ -868,9 +920,7 @@ mod tests { // one test cannot steal another test's providers. #[test] fn registry_dispatch_routes_to_correct_owner() { - let _guard = PROVIDER_TEST_LOCK - .lock() - .expect("provider test lock poisoned"); + let _guard = ProviderTestGuard::new(); // Drain anything left over from prior tests. let _ = take_used_providers(); @@ -927,9 +977,7 @@ mod tests { /// `Undefined symbols: _js_node_http_create_server_with_options`. #[test] fn emitted_create_server_symbol_routes_to_http() { - let _guard = PROVIDER_TEST_LOCK - .lock() - .expect("provider test lock poisoned"); + let _guard = ProviderTestGuard::new(); assert_symbol_routes_to( "js_node_http_create_server_with_options", OwnerKind::WellKnown("http"), @@ -943,9 +991,7 @@ mod tests { /// owner so the wrapper joins the link line. #[test] fn emitted_http_suite_external_symbols_route_to_owners() { - let _guard = PROVIDER_TEST_LOCK - .lock() - .expect("provider test lock poisoned"); + let _guard = ProviderTestGuard::new(); for symbol in [ "js_http_request_overload", "js_http_set_header", @@ -983,9 +1029,7 @@ mod tests { /// perry-runtime and must NOT flip the feature. #[test] fn emitted_crypto_symbols_route_to_stdlib_crypto_feature() { - let _guard = PROVIDER_TEST_LOCK - .lock() - .expect("provider test lock poisoned"); + let _guard = ProviderTestGuard::new(); let crypto_owner = OwnerKind::Stdlib { feature: Some("crypto"), }; @@ -1015,9 +1059,7 @@ mod tests { /// (#6439 shape). #[test] fn crypto_prefix_net_marker_survives_module_capture_replay() { - let _guard = PROVIDER_TEST_LOCK - .lock() - .expect("provider test lock poisoned"); + let _guard = ProviderTestGuard::new(); let _ = take_used_providers(); begin_module_capture(); @@ -1046,9 +1088,7 @@ mod tests { /// failed with `Undefined symbols: _js_event_emitter_new_with_options`. #[test] fn emitted_event_emitter_symbols_route_to_events() { - let _guard = PROVIDER_TEST_LOCK - .lock() - .expect("provider test lock poisoned"); + let _guard = ProviderTestGuard::new(); for symbol in [ "js_event_emitter_new", "js_event_emitter_new_with_options", @@ -1078,9 +1118,7 @@ mod tests { /// compiles the fixtures instead of linking unresolved `js_net_*` calls. #[test] fn emitted_net_validation_external_symbols_route_to_net() { - let _guard = PROVIDER_TEST_LOCK - .lock() - .expect("provider test lock poisoned"); + let _guard = ProviderTestGuard::new(); for symbol in [ "js_net_create_server", "js_net_server_listen", @@ -1104,9 +1142,7 @@ mod tests { /// codegen provenance alone — no exact-table row per symbol required. #[test] fn emitted_ext_prefix_symbols_route_to_well_known_binding() { - let _guard = PROVIDER_TEST_LOCK - .lock() - .expect("provider test lock poisoned"); + let _guard = ProviderTestGuard::new(); for (symbol, binding) in [ ("js_ioredis_new", "ioredis"), ("js_ioredis_set", "ioredis"), @@ -1125,9 +1161,7 @@ mod tests { /// prefix-family symbol must never leak into a DIFFERENT binding. #[test] fn ext_prefix_net_does_not_over_match() { - let _guard = PROVIDER_TEST_LOCK - .lock() - .expect("provider test lock poisoned"); + let _guard = ProviderTestGuard::new(); let _ = take_used_providers(); record_ffi_call("js_ioredis_new"); let got = take_used_providers(); @@ -1150,9 +1184,7 @@ mod tests { /// flip, so a warm `.o` cache links identically to a cold one (#6439 shape). #[test] fn ext_prefix_marker_survives_module_capture_replay() { - let _guard = PROVIDER_TEST_LOCK - .lock() - .expect("provider test lock poisoned"); + let _guard = ProviderTestGuard::new(); let _ = take_used_providers(); begin_module_capture(); diff --git a/crates/perry-codegen/src/opt_report/mod.rs b/crates/perry-codegen/src/opt_report/mod.rs index 0fa3e0652e..552a9c5957 100644 --- a/crates/perry-codegen/src/opt_report/mod.rs +++ b/crates/perry-codegen/src/opt_report/mod.rs @@ -58,7 +58,16 @@ use std::sync::{Mutex, OnceLock}; pub fn enabled() -> bool { #[cfg(test)] if test_support::forced() { - return true; + // Only the thread that opened the `Session` records. The gate and the + // sink are both process-global while `Session`'s lock only serialises + // tests that TAKE it, so any concurrently-running test that lowers code + // without one used to emit into the holder's sink — its snapshot then + // saw a neighbour's rows. That is a real, observed flake, not a + // theoretical one: `only_the_return_position_is_marked_served` reads + // `rows.len() == 2` and failed at 3 roughly one run in six once slice 7 + // (#7662) added lowering tests, which changed the parallel schedule. + // Diagnosed from the extra row, not from the timing. + return test_support::recording_thread_is_current(); } static CACHED: OnceLock = OnceLock::new(); *CACHED.get_or_init(|| { @@ -81,11 +90,37 @@ pub(crate) mod test_support { static FORCED: AtomicBool = AtomicBool::new(false); static LOCK: OnceLock> = OnceLock::new(); + /// Thread that currently holds a `Session`. The sink is process-global but + /// the lock only serialises tests that take a `Session`, so recording is + /// additionally narrowed to the holder's thread — otherwise a neighbouring + /// test that lowers code without a `Session` writes into this one's + /// snapshot. + static RECORDING_THREAD: Mutex> = Mutex::new(None); pub(crate) fn forced() -> bool { FORCED.load(Ordering::Relaxed) } + /// Is the calling thread the one that opened the active `Session`? + pub(crate) fn recording_thread_is_current() -> bool { + RECORDING_THREAD + .lock() + .map(|t| *t == Some(std::thread::current().id())) + .unwrap_or(false) + } + + fn claim_recording_thread() { + if let Ok(mut t) = RECORDING_THREAD.lock() { + *t = Some(std::thread::current().id()); + } + } + + fn release_recording_thread() { + if let Ok(mut t) = RECORDING_THREAD.lock() { + *t = None; + } + } + /// Enable the report and drain any leftover entries. Restores the gate on /// drop, so a panicking test cannot leave it on for its neighbours. pub(crate) struct Session { @@ -99,6 +134,7 @@ pub(crate) mod test_support { .lock() .unwrap_or_else(|e| e.into_inner()); FORCED.store(true, Ordering::Relaxed); + claim_recording_thread(); let _ = super::take_entries(); // The drain above may itself have collapsed leftovers; zero the // counter so a hand-built render test never reads a neighbour's. @@ -114,6 +150,7 @@ pub(crate) mod test_support { .lock() .unwrap_or_else(|e| e.into_inner()); FORCED.store(false, Ordering::Relaxed); + claim_recording_thread(); let _ = super::take_entries(); super::MASKED_BY_DEDUP.store(0, Ordering::Relaxed); Session { _guard: guard } @@ -128,6 +165,7 @@ pub(crate) mod test_support { fn drop(&mut self) { let _ = super::take_entries(); FORCED.store(false, Ordering::Relaxed); + release_recording_thread(); } } }