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.
This commit is contained in:
Don Omondi
2026-10-29 13:00:41 +07:00
committed by bflatastic
parent 6923939460
commit e94b74eea4
3 changed files with 39 additions and 11 deletions
+11 -8
View File
@@ -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<Candidate> {
/// 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"",
+23
View File
@@ -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;
+5 -3
View File
@@ -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();