forked from 1b-agent/fieldcraft-upstream
The oracle binary (modelcraft-testkit) runs each case's recipe through the command engine, measures its quantities by JSON pointers into step outputs or commands, compares them with the cited references and the frozen fixtures, and writes docs/oracle.md. 20 theory cases (patch tests of every solid element, Timoshenko cantilevers, Lame, Kirsch, beam frequencies, torsion and bending through couplings, explicit waves and impact, CDT invariants), 8 published benchmarks (MacNeal-Harder patch, straight and curved cantilevers, thick cylinder, Scordelis-Lo with solids, NAFEMS LE1, LE10, FV32) and 10 pending cases with citations. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
202 lines
8.6 KiB
Rust
202 lines
8.6 KiB
Rust
//! Workspace tooling: `cargo xtask <command>`.
|
|
//!
|
|
//! Pure Rust (std + serde_json). External tools (`cargo`) are invoked through
|
|
//! `std::process::Command`. Adapted from SolidCraft's xtask (MIT OR Apache-2.0), see
|
|
//! ATTRIBUTION.md.
|
|
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
|
|
|
mod app_icon;
|
|
mod assets;
|
|
mod icons;
|
|
mod layers;
|
|
mod licences;
|
|
mod test;
|
|
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::{Command, ExitCode};
|
|
|
|
const USAGE: &str = "\
|
|
usage: cargo xtask <command>
|
|
|
|
commands:
|
|
layers enforce the crate dependency layering (docs/architecture.md)
|
|
assets check that every non-code asset file is attributed in ATTRIBUTION.md
|
|
icons [ID…] draw the toolbar icons from their recipes (assets/icons/src/*.json) with
|
|
VectorCraft (VECTORCRAFT_CLI, else vectorcraft-cli on the PATH): writes each
|
|
icon's .vectorcraft source and .svg export
|
|
app-icon draw the application icon family (assets/app-icon) with VectorCraft, then
|
|
write the iconset, icns, ico and Icon Composer document (macOS: sips,
|
|
iconutil, magick; actool for Assets.car when available)
|
|
licences [--check]
|
|
write THIRD-PARTY-LICENSES.txt (the licences of every shipped crate);
|
|
--check fails when it is out of date
|
|
test [--heavy] [CARGO ARGS] [-- TEST ARGS]
|
|
build the tests with the ci profile, then run every test binary at once;
|
|
--heavy runs only the ignored tests instead (heavy verification and corpus
|
|
tests, and the benchmarks) and the doc tests alongside: the CI heavy job
|
|
corpus [--part NAME] [--no-write]
|
|
build every meshing test corpus part (tests/corpus, analytic shapes), tessellate
|
|
and mesh it, and write the results to docs/meshing/corpus.md (release build)
|
|
oracle [--case NAME]... [--bless NAME|all]... [--quick] [--no-write]
|
|
run the verification and validation cases (tests/oracle, docs/verification.md)
|
|
against their references and fixtures; write docs/oracle.md after a full run;
|
|
--bless freezes a passing case's results in its fixture.json; --quick skips
|
|
cases marked slow (release build)
|
|
wasm cargo check every library crate for wasm32-unknown-unknown
|
|
ci fmt --check, assets, licences --check, layers, clippy -D warnings, then the tests
|
|
(not the heavy ones) with the wasm check alongside (stops at the first failing
|
|
step)
|
|
";
|
|
|
|
fn main() -> ExitCode {
|
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
|
let rest: Vec<&str> = args.iter().skip(1).map(String::as_str).collect();
|
|
let result = match args.first().map(String::as_str) {
|
|
Some("layers") => cmd_layers(),
|
|
Some("assets") => assets::run(&root()),
|
|
Some("icons") => icons::run(&root(), &rest),
|
|
Some("app-icon") => app_icon::run(&root()),
|
|
Some("licences") => licences::run(&root(), rest.contains(&"--check")),
|
|
Some("test") => {
|
|
let (heavy, rest) = match rest.split_first() {
|
|
Some((&"--heavy", after)) => (true, after),
|
|
_ => (false, &rest[..]),
|
|
};
|
|
let (cargo_args, test_args) = rest.iter().position(|a| *a == "--").map_or((rest, &[][..]), |i| (&rest[..i], &rest[i + 1..]));
|
|
if heavy {
|
|
let ignored: Vec<&str> = ["--ignored"].into_iter().chain(test_args.iter().copied()).collect();
|
|
test::run(cargo_args, &ignored, vec![test::doc_tests(cargo_args, test_args)])
|
|
} else {
|
|
test::run(cargo_args, test_args, vec![])
|
|
}
|
|
}
|
|
Some("ci") => cmd_ci(),
|
|
Some("corpus") => {
|
|
// A binary of the testkit, so xtask itself stays free of SolidCraft.
|
|
let mut c = cargo();
|
|
c.args(["run", "--release", "-p", "modelcraft-testkit", "--bin", "corpus", "--"]).args(&rest);
|
|
run(c, "corpus")
|
|
}
|
|
Some("oracle") => {
|
|
// A binary of the testkit, which has the engine and SolidCraft scripts for geometry.
|
|
let mut c = cargo();
|
|
c.args(["run", "--release", "-p", "modelcraft-testkit", "--bin", "oracle", "--"]).args(&rest);
|
|
run(c, "oracle")
|
|
}
|
|
Some("wasm") => cmd_wasm(),
|
|
Some("-h" | "--help" | "help") | None => {
|
|
print!("{USAGE}");
|
|
Ok(())
|
|
}
|
|
Some(other) => Err(format!("unknown command `{other}`\n\n{USAGE}")),
|
|
};
|
|
match result {
|
|
Ok(()) => ExitCode::SUCCESS,
|
|
Err(e) => {
|
|
eprintln!("error: {e}");
|
|
ExitCode::FAILURE
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Workspace root (parent of the xtask crate).
|
|
pub fn root() -> PathBuf {
|
|
let here = Path::new(env!("CARGO_MANIFEST_DIR"));
|
|
here.parent().unwrap_or(here).to_path_buf()
|
|
}
|
|
|
|
pub fn cargo() -> Command {
|
|
let mut c = Command::new(std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()));
|
|
c.current_dir(root());
|
|
c
|
|
}
|
|
|
|
fn run(mut cmd: Command, what: &str) -> Result<(), String> {
|
|
eprintln!("$ {what}");
|
|
let status = cmd.status().map_err(|e| format!("{what}: failed to spawn: {e}"))?;
|
|
if status.success() { Ok(()) } else { Err(format!("{what}: exited with {status}")) }
|
|
}
|
|
|
|
pub fn metadata() -> Result<serde_json::Value, String> {
|
|
let out = cargo().args(["metadata", "--format-version", "1", "--no-deps"]).output().map_err(|e| format!("cargo metadata: {e}"))?;
|
|
if !out.status.success() {
|
|
return Err(format!("cargo metadata failed:\n{}", String::from_utf8_lossy(&out.stderr)));
|
|
}
|
|
serde_json::from_slice(&out.stdout).map_err(|e| format!("cargo metadata: bad JSON: {e}"))
|
|
}
|
|
|
|
fn cmd_layers() -> Result<(), String> {
|
|
let crates = layers::from_metadata(&metadata()?)?;
|
|
println!("Dependency layering (docs/architecture.md)\n");
|
|
println!("{:<28} {:<14} workspace deps", "crate", "layer");
|
|
for c in &crates {
|
|
let ws: Vec<String> = c
|
|
.deps
|
|
.iter()
|
|
.filter(|d| d.workspace)
|
|
.map(|d| {
|
|
let k = match d.kind {
|
|
layers::DepKind::Normal => "",
|
|
layers::DepKind::Dev => " (dev)",
|
|
layers::DepKind::Build => " (build)",
|
|
};
|
|
format!("{}{k}", layers::short_name(&d.name))
|
|
})
|
|
.collect();
|
|
println!("{:<28} {:<14} {}", c.name, layers::describe(layers::classify(&c.name)), ws.join(", "));
|
|
}
|
|
let violations = layers::check(&crates);
|
|
println!();
|
|
if violations.is_empty() {
|
|
println!("OK: {} crates, no layering violations.", crates.len());
|
|
Ok(())
|
|
} else {
|
|
println!("{} violation(s):", violations.len());
|
|
for v in &violations {
|
|
println!(" - {v}");
|
|
}
|
|
Err(format!("{} layering violation(s)", violations.len()))
|
|
}
|
|
}
|
|
|
|
/// Never break wasm: every layered crate (everything below the apps) 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 set: Vec<String> =
|
|
crates.iter().filter(|c| matches!(layers::classify(&c.name), Some(layers::Class::Layer(_)))).map(|c| c.name.clone()).collect();
|
|
let mut c = cargo();
|
|
c.args(["check", "--target", "wasm32-unknown-unknown", "--profile", "ci"]);
|
|
for p in &set {
|
|
c.args(["-p", p]);
|
|
}
|
|
Ok((c, set.len()))
|
|
}
|
|
|
|
/// The quick gates first, so they fail fast; the wasm check runs while the test binaries do,
|
|
/// which leaves cargo's build lock free. The heavy tests and the doc tests are the CI heavy job's
|
|
/// (`cargo xtask test --heavy`).
|
|
fn cmd_ci() -> Result<(), String> {
|
|
let mut c = cargo();
|
|
c.args(["fmt", "--all", "--", "--check"]);
|
|
run(c, "cargo fmt --check")?;
|
|
assets::run(&root())?;
|
|
licences::run(&root(), true)?;
|
|
cmd_layers()?;
|
|
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)])?;
|
|
eprintln!("ci: all gates passed");
|
|
Ok(())
|
|
}
|