//! `cargo xtask test`: build the workspace's tests once, then run every test binary at the same //! time. `cargo test` runs them one after another, so one slow binary held up all the others; //! the doc tests and any other `side` jobs run alongside on their own thread. use std::process::{Command, Stdio}; use std::sync::mpsc; use std::time::Instant; use crate::{cargo, root}; /// A test executable from `cargo test --no-run`. struct TestBin { label: String, exe: String, dir: String, } /// Build the tests with `cargo_args` (e.g. `--locked`), run each binary with `test_args`, and run /// the `side` jobs one after another meanwhile. Each prints a line when it ends; a failure prints /// its whole output. pub fn run(cargo_args: &[&str], test_args: &[&str], side: Vec<(String, Command)>) -> Result<(), String> { let bins = build(cargo_args)?; let start = Instant::now(); let (tx, rx) = mpsc::channel::<(String, Result, f64)>(); let mut failed = Vec::new(); std::thread::scope(|scope| { for b in &bins { let tx = tx.clone(); scope.spawn(move || { let t0 = Instant::now(); let mut c = Command::new(&b.exe); c.args(test_args).current_dir(&b.dir).env("CARGO_MANIFEST_DIR", &b.dir); let _ = tx.send((b.label.clone(), finish(c), t0.elapsed().as_secs_f64())); }); } let tx_side = tx.clone(); scope.spawn(move || { for (label, c) in side { let t0 = Instant::now(); let _ = tx_side.send((label, finish(c), t0.elapsed().as_secs_f64())); } }); drop(tx); for (label, result, secs) in rx { match result { Ok(summary) => println!("ok {label:<44} {secs:6.1}s {summary}"), Err(output) => { println!("FAIL {label:<44} {secs:6.1}s\n{output}"); failed.push(label); } } } }); println!("test: {} binaries in {:.1}s", bins.len(), start.elapsed().as_secs_f64()); if failed.is_empty() { Ok(()) } else { Err(format!("failed: {}", failed.join(", "))) } } /// Run to the end: the last `test result:` line on success, all output on failure. fn finish(mut c: Command) -> Result { let out = c.stdin(Stdio::null()).output().map_err(|e| format!("failed to spawn: {e}"))?; let stdout = String::from_utf8_lossy(&out.stdout); if out.status.success() { Ok(stdout.lines().rev().find(|l| l.starts_with("test result:")).unwrap_or("").trim_start_matches("test result: ").to_string()) } else { Err(format!("{stdout}{}\n({})", String::from_utf8_lossy(&out.stderr), out.status)) } } /// `cargo test --workspace --profile ci --no-run`, and the executables it built. fn build(cargo_args: &[&str]) -> Result, String> { let mut c = cargo(); c.args(["test", "--workspace", "--profile", "ci", "--no-run", "--message-format=json-render-diagnostics"]).args(cargo_args); eprintln!("$ cargo test --workspace --profile ci --no-run"); let out = c.stderr(Stdio::inherit()).output().map_err(|e| format!("cargo test --no-run: failed to spawn: {e}"))?; if !out.status.success() { return Err(format!("cargo test --no-run: exited with {}", out.status)); } let mut bins: Vec = Vec::new(); for line in String::from_utf8_lossy(&out.stdout).lines() { let Ok(m) = serde_json::from_str::(line) else { continue }; let Some(exe) = m["executable"].as_str().filter(|_| m["reason"] == "compiler-artifact" && m["profile"]["test"] == true) else { continue; }; if bins.iter().any(|b| b.exe == exe) { continue; } let manifest = std::path::Path::new(m["manifest_path"].as_str().unwrap_or_default()).to_path_buf(); let dir = manifest.parent().map(|d| d.to_path_buf()).unwrap_or_else(root); let pkg = dir.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default(); let kind = m["target"]["kind"][0].as_str().unwrap_or(""); let label = format!("{pkg} {kind} {}", m["target"]["name"].as_str().unwrap_or("")); bins.push(TestBin { label, exe: exe.to_string(), dir: dir.to_string_lossy().into_owned() }); } if bins.is_empty() { return Err("cargo test --no-run built no test executables".into()); } Ok(bins) } /// The doc tests (`cargo test --doc`), to run as a side job. pub fn doc_tests(cargo_args: &[&str], test_args: &[&str]) -> (String, Command) { let mut c = cargo(); c.args(["test", "--workspace", "--profile", "ci", "--doc"]).args(cargo_args).arg("--").args(test_args); ("doc tests".into(), c) }