From c3c4cf16aba424f47230ca348433d061d792c887 Mon Sep 17 00:00:00 2001 From: Matthew James Sivak Date: Mon, 19 Oct 2026 11:51:28 +0200 Subject: [PATCH] ci --quick: build the workspace's test targets, run only the changed packages' binaries `cargo test -p ` resolves features for that selection only, so its artifacts differ from the full `ci`'s workspace build (and from a worktree warmed from one): it recompiled ~130 crates, dependencies included. Now `--quick` runs `cargo test --workspace --no-run` (fresh when the workspace was built before) and executes the test binaries of the changed packages, with the package directory as cwd like cargo does. Doc tests stay with the full `ci`. A one-file change in lightcraft-raw in a warmed worktree: 170 s for the whole `ci --quick`. Co-Authored-By: Claude Opus 5.5 --- xtask/src/main.rs | 65 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 0a5e64532..8d4126330 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -190,6 +190,60 @@ fn total_ram_gb() -> Option { Some(bytes.saturating_add(1 << 29) >> 30) } +/// `ci --quick`'s tests: build every workspace test target, then run only the test binaries of +/// `packages`. Building with `-p` instead would resolve features per selection: different +/// artifacts from `ci`'s and a warm worktree's (`cargo xtask warm`) workspace builds, so it would +/// recompile ~130 crates. Doc tests are left to the full `ci`. +fn test_selected(root: &Path, packages: &std::collections::BTreeSet, skip_log: &Path) -> Result<(), String> { + let manifests: std::collections::BTreeMap = metadata()?["packages"] + .as_array() + .into_iter() + .flatten() + .filter_map(|p| Some((p["manifest_path"].as_str()?.to_owned(), p["name"].as_str()?.to_owned()))) + .filter(|(_, name)| packages.contains(name)) + .collect(); + let what = "cargo test --workspace --no-run"; + eprintln!("$ {what} (then the test binaries of {})", packages.iter().map(String::as_str).collect::>().join(", ")); + let out = cargo() + .args(["test", "--workspace", "--no-run", "--message-format=json-render-diagnostics"]) + .stderr(std::process::Stdio::inherit()) + .output() + .map_err(|e| format!("{what}: failed to spawn: {e}"))?; + if !out.status.success() { + return Err(format!("{what}: exited with {}", out.status)); + } + let mut binaries = Vec::new(); + for line in String::from_utf8_lossy(&out.stdout).lines() { + let Ok(msg) = serde_json::from_str::(line) else { continue }; + if msg["reason"] != "compiler-artifact" || msg["profile"]["test"] != true { + continue; + } + let (Some(exe), Some(manifest)) = (msg["executable"].as_str(), msg["manifest_path"].as_str()) else { continue }; + if let Some(name) = manifests.get(manifest) { + binaries.push((name.clone(), manifest.to_owned(), exe.to_owned())); + } + } + if binaries.is_empty() { + println!("no test binaries in {}", packages.iter().map(String::as_str).collect::>().join(", ")); + return Ok(()); + } + let mut failed = Vec::new(); + for (name, manifest, exe) in &binaries { + let dir = Path::new(manifest).parent().unwrap_or(root); + let mut c = Command::new(exe); + c.current_dir(dir).env("CARGO_MANIFEST_DIR", dir).env("CARGO_PKG_NAME", name).env(ci_report::SKIP_LOG_ENV, skip_log); + if std::env::var_os("RUST_TEST_THREADS").is_none() { + // as the cargo runs of ci (`cargo()`): more threads make the frame-budget tests flaky + c.env("RUST_TEST_THREADS", std::thread::available_parallelism().map_or(4, |n| n.get()).min(4).to_string()); + } + let file = Path::new(exe).file_name().map(|f| f.to_string_lossy().into_owned()).unwrap_or_default(); + if run(c, &format!("{name}: {file}")).is_err() { + failed.push(format!("{name} ({file})")); + } + } + if failed.is_empty() { Ok(()) } else { Err(format!("tests failed in {}", failed.join(", "))) } +} + pub fn run(mut cmd: Command, what: &str) -> Result<(), String> { eprintln!("$ {what}"); let status = cmd.status().map_err(|e| format!("{what}: failed to spawn: {e}"))?; @@ -347,11 +401,6 @@ fn cmd_ci(args: &[&str]) -> Result<(), String> { _ => true, }, }; - let mut test_args = vec!["test".to_owned()]; - match &plan { - Some(p) if !p.workspace_wide => test_args.extend(p.packages.iter().flat_map(|n| ["-p".to_owned(), n.clone()])), - _ => test_args.push("--workspace".to_owned()), - } let mut steps: Vec = vec![ ( "fmt", @@ -383,9 +432,9 @@ fn cmd_ci(args: &[&str]) -> Result<(), String> { ), ( "test", - Box::new(|| { - let a: Vec<&str> = test_args.iter().map(String::as_str).collect(); - test(&a, "test") + Box::new(|| match &plan { + Some(p) if !p.workspace_wide => test_selected(&root, &p.packages, &ci_report::skip_log(&root, "test")), + _ => test(&["test", "--workspace"], "test"), }), ), ];