From 79f695760109d548791558833b7bb1afc4f79308 Mon Sep 17 00:00:00 2001 From: FredApps <153173854+FredApps@users.noreply.github.com> Date: Sun, 1 Nov 2026 01:38:57 +0100 Subject: [PATCH] ci(xtask): run the GPU tests and i18n report from the test build (#2767) `cargo test -p effectcraft-gpu` and `cargo test -p effectcraft-ui-egui` resolve features for one package, so after the workspace nextest run they recompiled for about 2 minutes and 1 minute 15 seconds. Both now run the lib test binaries the workspace build produced, found with `cargo test --no-run` and the same selection. The GPU tests still run in one libtest process. Co-authored-by: FredApps Co-authored-by: Claude Opus 5.5 --- xtask/src/main.rs | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 401ed861e..266f5acc3 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -479,18 +479,52 @@ fn tests() -> Result<(), String> { let mut runs = Vec::new(); if nextest { runs.push(run(Command::new(cargo).args(["nextest", "run"]).args(targets).args(["-E", "not package(effectcraft-gpu)"]))); - runs.push(run(Command::new(cargo).args(["test", "-p", "effectcraft-gpu", "--lib", "--no-fail-fast"]))); + // effectcraft-gpu's tests share one device, so they run in one libtest process: the + // binary nextest just built (`cargo test -p effectcraft-gpu` would rebuild ~2 minutes). + runs.push(lib_test_binary("effectcraft-gpu").and_then(|mut c| run(&mut c))); } else { println!("cargo-nextest not found, running cargo test (install: cargo install cargo-nextest --locked)"); runs.push(run(Command::new(cargo).arg("test").args(targets))); } - // The test runs above hide a passing test's output, so the always-passing report runs again with --nocapture. - runs.push(run(Command::new(cargo).args(["test", "-p", "effectcraft-ui-egui", "--lib", "missing_translations_report", "--", "--nocapture"]))); + // The test runs above hide a passing test's output, so the always-passing report runs again + // with --nocapture, from the binary already built. + runs.push(lib_test_binary("effectcraft-ui-egui").and_then(|mut c| run(c.args(["missing_translations_report", "--nocapture"])))); runs.push(run(Command::new(cargo).args(["test", "--workspace", "--doc", "--no-fail-fast"]))); let failed: Vec = runs.into_iter().filter_map(Result::err).collect(); if failed.is_empty() { Ok(()) } else { Err(failed.join("; ")) } } +/// A command running `package`'s lib test binary from the test build above, the way `cargo test` +/// runs it (from the package directory), found with `--no-run` and the same workspace selection +/// so nothing rebuilds: `cargo test -p ` resolves features for that package alone and +/// recompiles much of the workspace. +fn lib_test_binary(package: &str) -> Result { + let out = Command::new(env!("CARGO")) + .args(["test", "--workspace", "--lib", "--bins", "--tests", "--no-run", "--message-format=json-render-diagnostics"]) + .stderr(std::process::Stdio::inherit()) + .output() + .map_err(|e| format!("cargo test --no-run: {e}"))?; + if !out.status.success() { + return Err(format!("cargo test --no-run: exited with {}", out.status)); + } + let lib = package.replace('-', "_"); + String::from_utf8_lossy(&out.stdout) + .lines() + .filter_map(|line| serde_json::from_str::(line).ok()) + .find(|m| { + m["reason"] == "compiler-artifact" + && m["profile"]["test"] == true + && m["target"]["name"] == lib.as_str() + && m["target"]["kind"].as_array().is_some_and(|k| k.iter().any(|k| k == "lib")) + }) + .and_then(|m| { + let mut c = Command::new(m["executable"].as_str()?); + c.current_dir(std::path::Path::new(m["manifest_path"].as_str()?).parent()?); + Some(c) + }) + .ok_or_else(|| format!("no {package} lib test binary in the test build")) +} + type Stage = fn() -> Result<(), String>; /// Every stage runs even when an earlier one fails, so one run reports all the problems.