diff --git a/apps/solidcraft-cli/src/kernel_cases.rs b/apps/solidcraft-cli/src/kernel_cases.rs index eeddf32e07..549ad11cd2 100644 --- a/apps/solidcraft-cli/src/kernel_cases.rs +++ b/apps/solidcraft-cli/src/kernel_cases.rs @@ -129,7 +129,7 @@ fn case_name(dir: &str) -> (String, String) { (name(p.parent()), name(Some(p))) } -fn check_case(dir: &str) -> Value { +fn check_case(dir: &str, dumps: Option<&std::path::Path>) -> Value { let (area, name) = case_name(dir); let mut out = json!({"case": name, "area": area, "kernel": kernel()}); let want = match read_json(&format!("{dir}/expected.json")) { @@ -167,6 +167,18 @@ fn check_case(dir: &str) -> Value { let outcome = want["outcome"].as_str().unwrap_or("solid"); let refused = outcome == "refused"; let may_refuse = refused || outcome == "solid_or_refused"; + // The SMLib calls that fail in this case leave their inputs in its own folder. + if let Some(root) = dumps { + let folder = root.join(format!("{area}-{name}")); + if let Err(e) = std::fs::create_dir_all(&folder) + .map_err(|e| e.to_string()) + .and_then(|()| solidcraft_engine::kernel::smlib_dump_dir(Some(&folder)).map_err(|e| e.to_string())) + { + out["status"] = json!("fail"); + out["error"] = json!(format!("--dumps: {}: {e}", folder.display())); + return out; + } + } let mut s = solidcraft_engine::Session::default(); let clock = std::time::Instant::now(); let ran = s.run_script(&commands); @@ -331,7 +343,15 @@ pub fn run(args: &[String]) -> Result<(), String> { if dirs.is_empty() { return Err("kernel-cases: give one or more case directories (each with recipe.json and expected.json)".into()); } - let results: Vec = dirs.iter().map(|d| check_case(d)).collect(); + // `--dumps=DIR`: each case's failing SMLib calls leave their inputs in DIR/-. + let dumps = args.iter().find_map(|a| a.strip_prefix("--dumps=")).map(std::path::PathBuf::from); + if dumps.as_ref().is_some_and(|d| d.as_os_str().is_empty()) { + return Err("kernel-cases: --dumps= needs a folder".into()); + } + let results: Vec = dirs.iter().map(|d| check_case(d, dumps.as_deref())).collect(); + if dumps.is_some() { + solidcraft_engine::kernel::smlib_dump_dir(None).map_err(|e| format!("kernel-cases: --dumps: {e}"))?; + } report(args, &results); let bad = results.iter().filter(|r| matches!(r["status"].as_str(), Some("fail" | "crash"))).count(); if bad > 0 { Err(format!("{bad} kernel case(s) failed")) } else { Ok(()) } diff --git a/apps/solidcraft-cli/src/main.rs b/apps/solidcraft-cli/src/main.rs index 4fffaff44d..1bd2627a12 100644 --- a/apps/solidcraft-cli/src/main.rs +++ b/apps/solidcraft-cli/src/main.rs @@ -15,7 +15,7 @@ //! solidcraft-cli commands the command registry as JSON //! solidcraft-cli recipe translate an oracle recipe to a script //! solidcraft-cli oracle ... replay recipes and compare with expected.json -//! solidcraft-cli kernel-cases [--json] ... the kernel regression rig (tests/kernel-cases) +//! solidcraft-cli kernel-cases [--json] [--dumps=DIR] ... the kernel regression rig (tests/kernel-cases) //! solidcraft-cli mcp [--headless] [--in design|script] [--connect HOST:PORT [--token T]] [--allow-path DIR]... [--allow-any-path] //! [--param-check off|warn|strict] //! MCP server on stdio (docs/mcp.md) @@ -82,7 +82,7 @@ const USAGE_RECIPE: &str = " solidcraft-cli recipe \n"; /// `oracle`'s lines in the usage. const USAGE_ORACLE: &str = " solidcraft-cli oracle ... [--json]\n"; /// `kernel-cases`'s lines in the usage. -const USAGE_KERNEL_CASES: &str = " solidcraft-cli kernel-cases ... [--json]\n (the kernel regression rig, tests/kernel-cases; SOLIDCRAFT_KERNEL picks the kernel)\n"; +const USAGE_KERNEL_CASES: &str = " solidcraft-cli kernel-cases ... [--json] [--dumps=DIR]\n (the kernel regression rig, tests/kernel-cases; SOLIDCRAFT_KERNEL picks the kernel; --dumps: each case's\n failing SMLib calls leave their inputs for replay in DIR/-)\n"; /// `fuzz`'s lines in the usage. const USAGE_FUZZ: &str = " solidcraft-cli fuzz [--from N] [--count N] [--jobs J] [--timeout S] [--steps N] [--minimise] [--out DIR] [--report FILE]\n"; diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index 0c53bbb1c3..b6c1e40fd7 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -217,7 +217,7 @@ pub use sweep_path::{RailFit, SweepControl, SweepOrientation, sweep_path, sweep_ pub use topo::{CylinderFace, cylinder_end_at, cylinder_face, cylinder_face_at}; pub use topo::{TopoCounts, merged_topology, seam_flags}; pub use tspline::tspline_sheet; -pub use validity::{SmlibCheck, Validity, smlib_built, validity}; +pub use validity::{SmlibCheck, Validity, smlib_built, smlib_dump_dir, validity}; pub use whole_face::{WholeFace, cut_takes_only_the_face, cut_tool, face_prism, matching_face}; #[derive(Debug, Clone, PartialEq, thiserror::Error)] diff --git a/crates/kernel/src/validity.rs b/crates/kernel/src/validity.rs index 52cb851295..e5dd336ee2 100644 --- a/crates/kernel/src/validity.rs +++ b/crates/kernel/src/validity.rs @@ -59,3 +59,17 @@ pub fn smlib_built() -> bool { cfg!(feature = "smlib") } +/// Where every SMLib call that fails from now on leaves its inputs for replay (the C API's +/// `smlib_set_dump_dir`: the call's arguments and B-reps, which `smlib_brep_read` reads back); +/// `None` stops it. A build without SMLib makes no calls to dump. +pub fn smlib_dump_dir(dir: Option<&std::path::Path>) -> crate::Result<()> { + #[cfg(feature = "smlib")] + { + solidcraft_smlib::set_dump_dir(dir).map_err(|e| crate::KernelError::Failed(format!("SMLib dump folder: {e}"))) + } + #[cfg(not(feature = "smlib"))] + { + let _ = dir; + Ok(()) + } +} diff --git a/tests/kernel-cases/README.md b/tests/kernel-cases/README.md index 67733cf247..119e22bd40 100644 --- a/tests/kernel-cases/README.md +++ b/tests/kernel-cases/README.md @@ -8,6 +8,7 @@ them. `cargo xtask kernel-cases` runs them and writes [`docs/kernel-cases.md`](. cargo xtask kernel-cases # the default kernel cargo xtask kernel-cases --kernel both --cli target/release/solidcraft-cli # and SMLib cargo run -p solidcraft-cli -- kernel-cases tests/kernel-cases/booleans/ # one case +cargo run -p solidcraft-cli --features smlib -- kernel-cases --dumps=/tmp/dumps # failing SMLib calls leave replayable inputs ``` For the `smlib` column, build `solidcraft-cli` with `--features smlib` (after