cargo xtask test runs every test binary at once (cargo test runs them one after another): 323 s to 75 s on 12 cores. cargo xtask ci runs the quick gates (fmt, assets, licences, layers) first, then clippy, then the tests with the wasm check and doc tests alongside; the wasm check uses the ci profile to reuse clippy's host build scripts and proc macros. GitHub CI runs its tests through cargo xtask test

This commit is contained in:
CypherNaugh_0x authored and bherbruck committed 2026-10-10 13:24:08 -04:00
1 parent aa268f0f0e
commit 29796762de
4 files changed
+143 -18

No files matched your search

+28 -12
View File
@@ -8,6 +8,7 @@ mod book;
mod layers;
mod licences;
mod parity;
mod test;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode};
@@ -33,8 +34,12 @@ commands:
licences [--check]
write THIRD-PARTY-LICENSES.txt (the licences of every shipped crate, embedded
in the programs); --check fails when it is out of date
test [CARGO ARGS] [-- TEST ARGS]
build the tests with the ci profile, then run every test binary at once
(and the doc tests alongside)
wasm cargo check every library crate and the web app for wasm32-unknown-unknown
ci fmt --check, clippy -D warnings, test, assets, licences --check, layers, wasm (stops at first failure)
ci fmt --check, assets, licences --check, layers, clippy -D warnings, then the tests
with the wasm check alongside (stops at the first failing step)
";
fn main() -> ExitCode {
@@ -48,6 +53,10 @@ fn main() -> ExitCode {
Some("step-corpus") => cmd_step_corpus(rest.iter().find(|a| !a.starts_with("--")).copied(), rest.contains(&"--iges")),
Some("book") => book::run(&root(), !rest.contains(&"--no-build")),
Some("licences") => licences::run(&root(), rest.contains(&"--check")),
Some("test") => {
let (cargo_args, test_args) = rest.iter().position(|a| *a == "--").map_or((&rest[..], &[][..]), |i| (&rest[..i], &rest[i + 1..]));
test::run(cargo_args, test_args, vec![test::doc_tests(cargo_args, test_args)])
}
Some("ci") => cmd_ci(),
Some("wasm") => cmd_wasm(),
Some("-h" | "--help" | "help") | None => {
@@ -201,18 +210,25 @@ fn cmd_oracle() -> Result<(), String> {
/// Never break wasm: every layered crate (everything below the apps) and the web app must
/// build for the browser.
fn cmd_wasm() -> Result<(), String> {
let (c, n) = wasm_check()?;
run(c, &format!("cargo check --target wasm32-unknown-unknown ({n} crates)"))?;
println!("wasm: {n} crates build for wasm32-unknown-unknown");
Ok(())
}
/// The wasm check and how many crates it covers. It uses the ci profile so the host build
/// scripts and proc macros built for clippy and the tests are reused.
fn wasm_check() -> Result<(Command, usize), String> {
let crates = layers::from_metadata(&metadata()?)?;
let mut set: Vec<String> =
crates.iter().filter(|c| matches!(layers::classify(&c.name), Some(layers::Class::Layer(_)))).map(|c| c.name.clone()).collect();
set.push("solvecraft-web".into());
let mut c = cargo();
c.args(["check", "--target", "wasm32-unknown-unknown"]);
c.args(["check", "--target", "wasm32-unknown-unknown", "--profile", "ci"]);
for p in &set {
c.args(["-p", p]);
}
run(c, &format!("cargo check --target wasm32-unknown-unknown ({} crates)", set.len()))?;
println!("wasm: {} crates build for wasm32-unknown-unknown", set.len());
Ok(())
Ok((c, set.len()))
}
/// Import every oracle STEP file (exported by Fusion) and compare with Fusion's measurements;
@@ -317,20 +333,20 @@ a manifold solid B-rep on the same surfaces a STEP export uses, with its name an
if pass == list.len() { Ok(()) } else { Err(format!("{} file(s) failed", list.len() - pass)) }
}
/// The quick gates first, so they fail fast; the wasm check (and doc tests) run while the test
/// binaries do, which leaves cargo's build lock free.
fn cmd_ci() -> Result<(), String> {
let mut c = cargo();
c.args(["fmt", "--all", "--", "--check"]);
run(c, "cargo fmt --check")?;
let mut c = cargo();
c.args(["clippy", "--workspace", "--all-targets", "--profile", "ci", "--", "-D", "warnings"]);
run(c, "cargo clippy -D warnings")?;
let mut c = cargo();
c.args(["test", "--workspace", "--profile", "ci"]);
run(c, "cargo test")?;
assets::run(&root())?;
licences::run(&root(), true)?;
cmd_layers()?;
cmd_wasm()?;
let mut c = cargo();
c.args(["clippy", "--workspace", "--all-targets", "--profile", "ci", "--", "-D", "warnings"]);
run(c, "cargo clippy -D warnings")?;
let (wasm, n) = wasm_check()?;
test::run(&[], &[], vec![(format!("wasm32-unknown-unknown check ({n} crates)"), wasm), test::doc_tests(&[], &[])])?;
eprintln!("ci: all gates passed");
Ok(())
}
+105
View File
@@ -0,0 +1,105 @@
//! `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<String, String>, 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<String, String> {
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<Vec<TestBin>, 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<TestBin> = Vec::new();
for line in String::from_utf8_lossy(&out.stdout).lines() {
let Ok(m) = serde_json::from_str::<serde_json::Value>(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)
}