From e94b74eea4b2f33913a9a1c5350563791f286094 Mon Sep 17 00:00:00 2001 From: Don Omondi <282319807+mockydevs@users.noreply.github.com> Date: Thu, 29 Oct 2026 04:16:02 +0300 Subject: [PATCH] test(app): keep a child's start from holding the GPU start file's lock The GPU start tests release the start file's lock and take it again. portal_save's mock_portal starts dbus-daemon meanwhile, and until a child runs its program it holds a copy of every open descriptor, so the lock outlived the test's own handle: the next start found the file locked, took it as another copy of the app running and left nothing out. Parallel runs of the app's tests failed 12 times in 20. A test-only RwLock: mock_portal holds it exclusively while it spawns, and each GPU start test holds it shared for the whole test. 0 failures in 70 parallel runs. --- apps/vectorcraft/src/gpu/tests.rs | 19 +++++++++++-------- apps/vectorcraft/src/main.rs | 23 +++++++++++++++++++++++ apps/vectorcraft/src/portal_save.rs | 8 +++++--- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/apps/vectorcraft/src/gpu/tests.rs b/apps/vectorcraft/src/gpu/tests.rs index 906e35fb6..d13a1c417 100644 --- a/apps/vectorcraft/src/gpu/tests.rs +++ b/apps/vectorcraft/src/gpu/tests.rs @@ -548,11 +548,14 @@ fn a_normal_exit_or_another_failure_does_not_restart() { assert_eq!(finish(Err("a bug".into()), &startup), Err("a bug".into())); } -/// A log folder of its own for each test. -fn start_dir(tag: &str) -> std::path::PathBuf { +/// A log folder of its own for each test, and the guard that keeps other tests from starting a +/// program while it runs: the test releases the start file's lock and takes it again, and a child +/// starting meanwhile would keep it held (`crate::no_spawns`). +fn start_dir(tag: &str) -> (std::path::PathBuf, std::sync::RwLockReadGuard<'static, ()>) { + let spawns = crate::no_spawns(); let d = std::env::temp_dir().join(format!("vectorcraft-gpu-start-{tag}-{}", std::process::id())); let _ = std::fs::remove_dir_all(&d); - d + (d, spawns) } /// What a start in `dir` leaves out; it then ends without crashing. @@ -578,7 +581,7 @@ fn issue_2243() -> Vec { /// it, without a status-bar note. #[test] fn a_start_that_never_showed_a_frame_leaves_its_adapter_out_of_the_next() { - let dir = start_dir("crash"); + let (dir, _spawns) = start_dir("crash"); let c = issue_2243(); let first = Startup::default(); assert_eq!(first.track(&dir, None, false), LeftOut::default()); @@ -609,7 +612,7 @@ fn a_start_that_never_showed_a_frame_leaves_its_adapter_out_of_the_next() { /// an error, a restart on another adapter), leaves nothing out of the next. #[test] fn a_start_that_showed_its_first_frame_or_ended_is_cleared() { - let dir = start_dir("shown"); + let (dir, _spawns) = start_dir("shown"); let c = issue_2243(); for end in [Startup::presented as fn(&Startup), Startup::ended] { let first = Startup::default(); @@ -633,7 +636,7 @@ fn a_start_that_showed_its_first_frame_or_ended_is_cleared() { /// macOS, where wgpu has none. #[test] fn a_crash_while_listing_lists_opengl_alone() { - let dir = start_dir("listing"); + let (dir, _spawns) = start_dir("listing"); let first = Startup::default(); first.track(&dir, None, false); drop(first); @@ -660,7 +663,7 @@ fn a_crash_while_listing_lists_opengl_alone() { /// start shows its first frame on the adapter that crashed, the adapter counts again. #[test] fn wgpu_backend_or_adapter_name_ignores_the_crashes() { - let dir = start_dir("explicit"); + let (dir, _spawns) = start_dir("explicit"); let c = issue_2243(); let first = Startup::default(); first.track(&dir, None, false); @@ -681,7 +684,7 @@ fn wgpu_backend_or_adapter_name_ignores_the_crashes() { /// are ignored: the start goes on with nothing left out. #[test] fn a_damaged_or_unreadable_start_file_is_ignored() { - let dir = start_dir("damaged"); + let (dir, _spawns) = start_dir("damaged"); std::fs::create_dir_all(&dir).unwrap(); let damaged: [&[u8]; 9] = [ b"", diff --git a/apps/vectorcraft/src/main.rs b/apps/vectorcraft/src/main.rs index c37aebe96..926f30b25 100644 --- a/apps/vectorcraft/src/main.rs +++ b/apps/vectorcraft/src/main.rs @@ -483,5 +483,28 @@ fn main() -> std::process::ExitCode { } } +/// Tests only: while a test starts another program, the child briefly holds a copy of every file +/// this process has open, until it runs that program. A file lock held through such a copy outlives +/// its own handle, so a GPU start test that released the start file and opened it again found it +/// still locked, as if another copy of the app were running, and failed. A test that starts a +/// program holds [`spawning`]; a test that depends on the start file's lock holds [`no_spawns`]. +#[cfg(all( + test, + any(feature = "wgpu", any(target_os = "linux", target_os = "freebsd", target_os = "dragonfly", target_os = "netbsd", target_os = "openbsd")) +))] +static CHILD_SPAWN: std::sync::RwLock<()> = std::sync::RwLock::new(()); + +/// Held while a test starts another program (see [`CHILD_SPAWN`]). +#[cfg(all(test, any(target_os = "linux", target_os = "freebsd", target_os = "dragonfly", target_os = "netbsd", target_os = "openbsd")))] +fn spawning() -> std::sync::RwLockWriteGuard<'static, ()> { + CHILD_SPAWN.write().unwrap_or_else(std::sync::PoisonError::into_inner) +} + +/// Held by a test that releases a locked file and opens it again (see [`CHILD_SPAWN`]). +#[cfg(all(test, feature = "wgpu"))] +fn no_spawns() -> std::sync::RwLockReadGuard<'static, ()> { + CHILD_SPAWN.read().unwrap_or_else(std::sync::PoisonError::into_inner) +} + #[cfg(all(test, feature = "wgpu"))] mod tests; diff --git a/apps/vectorcraft/src/portal_save.rs b/apps/vectorcraft/src/portal_save.rs index 10bc2d8a8..b6dd3cc8e 100644 --- a/apps/vectorcraft/src/portal_save.rs +++ b/apps/vectorcraft/src/portal_save.rs @@ -450,11 +450,13 @@ mod tests { use std::io::BufRead as _; use zbus::zvariant::OwnedObjectPath; - let mut daemon = std::process::Command::new("dbus-daemon") + let spawning = crate::spawning(); + let daemon = std::process::Command::new("dbus-daemon") .args(["--session", "--nofork", "--print-address=1"]) .stdout(std::process::Stdio::piped()) - .spawn() - .ok()?; + .spawn(); + drop(spawning); + let mut daemon = daemon.ok()?; let mut address = String::new(); std::io::BufReader::new(daemon.stdout.take()?).read_line(&mut address).ok()?; let address = address.trim().to_string();