Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog.d/9767-isolated-runtime-fixtures.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Fixed

- Isolate runtime test fixtures that inspect loaded libraries, process-wide box counters, and the composed symbol cache. Prevent neighboring tests from corrupting their assertions, and tighten the box reuse bound (#9197).
22 changes: 11 additions & 11 deletions crates/perry-runtime/src/box/release_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,10 @@ fn foreign_pointer_release_is_a_total_noop() {
/// asyncpipe_big).
#[test]
fn completed_activation_residue_is_bounded_not_linear() {
crate::test_support::isolated_test(completed_activation_residue_body);
}

fn completed_activation_residue_body() {
super::test_clear_box_registry();
const TURNS: usize = 100;
const ACTIVATIONS_PER_TURN: usize = 20;
Expand Down Expand Up @@ -544,22 +548,18 @@ fn completed_activation_residue_is_bounded_not_linear() {
flush_released_boxes();
}
let (a1, r1, _) = box_release_stats();
// The counters are process-global; sibling tests on other threads
// also allocate boxes, so assert lower bounds and give the residue
// bound slack instead of demanding exact equality.
// These process-global counters now measure only this fixture, so a
// sibling's allocations cannot hide reuse or push residue over the bound.
let total_allocs = (a1 - a0) as usize;
let residue = total_allocs.saturating_sub((r1 - r0) as usize);
let own_allocs = TURNS * ACTIVATIONS_PER_TURN * CELLS_PER_ACTIVATION;
assert!(
total_allocs >= own_allocs,
"every lifecycle allocates its frame ({total_allocs} < {own_allocs})"
assert_eq!(
total_allocs, own_allocs,
"every lifecycle allocates exactly its frame"
);
// One turn's working set (the first turn mints real cells; every
// later turn reuses them), plus generous slack for whatever the
// parallel sibling tests allocate (they use a handful of cells
// each). The pre-fix residue is TURNS * the per-turn bound, two
// orders of magnitude past this.
let bound = 4 * ACTIVATIONS_PER_TURN * CELLS_PER_ACTIVATION;
// later turn reuses them). The pre-fix residue is TURNS * this bound.
let bound = ACTIVATIONS_PER_TURN * CELLS_PER_ACTIVATION;
assert!(
residue <= bound,
"malloc residue must be bounded by one turn's working set: \
Expand Down
12 changes: 12 additions & 0 deletions crates/perry-runtime/src/gc/roots/stack_maps_decode_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,11 @@ mod tests {
#[cfg(target_os = "linux")]
#[test]
fn discovers_a_map_from_a_later_loaded_shared_object() {
crate::test_support::isolated_test(discovers_later_loaded_map_body);
}

#[cfg(target_os = "linux")]
fn discovers_later_loaded_map_body() {
use std::ffi::CString;
use std::fmt::Write as _;
use std::os::unix::ffi::OsStrExt;
Expand Down Expand Up @@ -327,6 +332,13 @@ mod tests {
#[cfg(target_os = "linux")]
#[test]
fn rejects_an_unreadable_loaded_shared_object() {
// This fixture deliberately poisons the process's loaded-image set.
// Isolate the writer too, so no sibling stack-map scan can observe it.
crate::test_support::isolated_test(rejects_unreadable_loaded_object_body);
}

#[cfg(target_os = "linux")]
fn rejects_unreadable_loaded_object_body() {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
use std::process::Command;
Expand Down
4 changes: 4 additions & 0 deletions crates/perry-runtime/src/symbol/get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1427,6 +1427,10 @@ mod own_data_ic_tests {

#[test]
fn composed_symbol_field_cache_reloads_mutated_final_slot() {
crate::test_support::isolated_test(composed_symbol_field_cache_body);
}

fn composed_symbol_field_cache_body() {
let _global = crate::gc::global_side_table_test_lock();
unsafe {
crate::gc::gc_suppress();
Expand Down
35 changes: 33 additions & 2 deletions crates/perry-runtime/src/test_support.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
//! Test-only serialization for PROCESS-global state that is neither a runtime
//! side table (those use `gc::global_side_table_test_lock`) nor thread-local.
//! Test-only isolation and serialization for process-global fixtures.
//!
//! First resident: the process working directory. `std::env::set_current_dir`
//! is process-wide, so a test that changes it (`typed_feedback`'s
Expand All @@ -21,3 +20,35 @@ pub(crate) fn process_cwd_test_lock() -> std::sync::MutexGuard<'static, ()> {
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}

/// Run a libtest case in its own process when its fixture needs exclusive
/// ownership of process-global state (#9197). A lock shared by only a few
/// tests cannot exclude the runtime's other side-table readers or counters.
///
/// Use the harness's current test name so renaming/moving a test cannot leave
/// a stale filter. Require a marker emitted AFTER the body as well as a clean
/// exit: selecting zero tests or exiting early must not produce a false pass.
pub(crate) fn isolated_test(body: impl FnOnce()) {
const CHILD_ENV: &str = "PERRY_RUNTIME_ISOLATED_TEST_NAME";
let thread = std::thread::current();
let name = thread.name().expect("libtest must name the test thread");
let completed = format!("perry isolated test completed: {name}");
if std::env::var(CHILD_ENV).ok().as_deref() == Some(name) {
body();
println!("{completed}");
return;
}

let output = std::process::Command::new(std::env::current_exe().expect("current test binary"))
.args(["--exact", name, "--nocapture", "--test-threads=1"])
.env(CHILD_ENV, name)
.output()
.expect("launch isolated runtime test");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success() && stdout.lines().any(|line| line.ends_with(&completed)),
"isolated test {name} did not complete: {}\nstdout:\n{stdout}\nstderr:\n{stderr}",
output.status
);
}
Loading