Files
OneCAD-Simulation/xtask/src/test.rs
T
5e5a2ae251 Fast PR CI: small hostile-test caps, heavy tests in a separate job (#43)
* test: hostile-params test runs under small session work caps

Session gains Limits, the defaults of mesh.tet.generate's max_steiner and
max_tets (production values unchanged; a parameter still overrides them;
file.open keeps them). The registry-wide hostile-params test runs on
sessions with small caps, so the needle-triangle tessellation it meshes is
refused at once instead of meshing to the 50k Steiner cap: the engine test
binary drops from ~350 s to ~20 s locally.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* ci: move heavy tests to a heavy job on main, nightly and on demand

Four slow tests are #[ignore = "heavy: ..."] (every corpus recipe, the
cantilever convergence study, the explicit-vs-modal cantilever period, the
generic-predicate rational cross-check); fast representative tests of each
area stay in the PR gate. cargo xtask test --heavy runs every ignored test
(benchmarks included) and the doc tests; cargo xtask test and ci run the
rest without the doc tests. The new heavy CI job runs it on pushes to main,
nightly and on workflow_dispatch. Branch jobs restore main's rust cache and
no longer save their own. AGENTS.md documents the rule.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* test: the three slow LBM verification cases run in the heavy job

The Stokes-drag sphere array, the staircase-pipe convergence study and the
developing-channel case are heavy (21 s, 11 s, 6 s alone); plane
Poiseuille, Couette, Taylor-Green, reproducibility and the stability
limits stay in the PR gate.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

---------

Co-authored-by: jyviko <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-13 21:21:49 -04:00

106 lines
4.7 KiB
Rust

//! `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;
//! `side` jobs (the wasm check, the doc tests) 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)
}