Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
123 lines
5.3 KiB
Rust
123 lines
5.3 KiB
Rust
//! Cases found by the robustness runs (`solvecraft-cli fuzz`, docs/robustness.md), cut down to
|
|
//! the steps that matter, and a fixed set of seeds that must keep running clean.
|
|
|
|
use serde_json::{Value, json};
|
|
|
|
use crate::Session;
|
|
use crate::fuzz;
|
|
|
|
/// Run a script; every command must succeed and every body be a sound solid.
|
|
fn script(steps: Value) -> Session {
|
|
let mut s = Session::default();
|
|
for st in steps.as_array().into_iter().flatten() {
|
|
let (c, p) = (st["command"].as_str().unwrap_or(""), &st["params"]);
|
|
if let Err(e) = s.execute(c, p) {
|
|
panic!("{c} {p}: {e}");
|
|
}
|
|
}
|
|
for b in &s.model.state().bodies {
|
|
let bad = b.body.validity();
|
|
assert!(bad.is_empty(), "{}: {bad:?}", b.name);
|
|
}
|
|
s
|
|
}
|
|
|
|
/// Shell of a notched plate closed by a shallow arc, opening a notch wall: the arc's cylinder
|
|
/// meets the plates at an angle, so its corners need more than one linear step to move.
|
|
#[test]
|
|
fn shell_with_a_shallow_arc_side() {
|
|
script(json!([
|
|
{"command": "sketch.create", "params": {"plane": "XY"}},
|
|
{"command": "sketch.line", "params": {"points": [[0, 0], [37.5, 0], [37.5, 37.5], [22.5, 37.5], [22.5, 20.0], [12.5, 20.0], [12.5, 37.5], [0, 37.5]]}},
|
|
{"command": "sketch.arc.three_point", "params": {"end": [0, 0], "start": [0, 37.5], "through": [-5.0, 18.75]}},
|
|
{"command": "sketch.finish", "params": {}},
|
|
{"command": "solid.extrude", "params": {"distance": 7.5}},
|
|
{"command": "solid.shell", "params": {"faces": [[12.5, 28.75, 3.75]], "thickness": 2.0}}
|
|
]));
|
|
}
|
|
|
|
/// Shell of a plate with a hole whose wall meets the plate's arc side: the edge where the two
|
|
/// cylinders meet is neither a line nor an arc, and is rebuilt through its moved points.
|
|
#[test]
|
|
fn shell_with_a_hole_through_a_curved_side() {
|
|
script(json!([
|
|
{"command": "sketch.create", "params": {"plane": "XY"}},
|
|
{"command": "sketch.line", "params": {"points": [[0, 0], [42.5, 0], [42.5, 20.0], [0, 20.0]]}},
|
|
{"command": "sketch.arc.three_point", "params": {"end": [0, 0], "start": [0, 20.0], "through": [-10.0, 10.0]}},
|
|
{"command": "sketch.finish", "params": {}},
|
|
{"command": "solid.extrude", "params": {"distance": 27.5}},
|
|
{"command": "solid.cylinder", "params": {"axis": [1.0, 0.0, 0.0], "base": [-10.000000000000002, 12.5, 12.5], "height": 17.5, "operation": "cut", "radius": 5.0}},
|
|
{"command": "solid.shell", "params": {"faces": [[42.5, 10.0, 13.75]], "thickness": 2.0}}
|
|
]));
|
|
}
|
|
|
|
/// Shell of a body in two pieces (a cylinder joined clear of a ring): each piece is shelled.
|
|
#[test]
|
|
fn shell_of_a_body_in_two_pieces() {
|
|
script(json!([
|
|
{"command": "sketch.create", "params": {"plane": "XZ"}},
|
|
{"command": "sketch.rectangle.two_point", "params": {"p0": [2.5, 0], "p1": [22.5, 17.5]}},
|
|
{"command": "sketch.finish", "params": {}},
|
|
{"command": "solid.revolve", "params": {"angle": "360 deg", "axis": "Z"}},
|
|
{"command": "solid.cylinder", "params": {"axis": [0.0, 1.0, 0.0], "base": [-22.5, 22.5, 0.0], "height": 30.0, "operation": "join", "radius": 10.0}},
|
|
{"command": "solid.shell", "params": {"faces": [[-22.5, 22.5, 0.0]], "thickness": 2.0}}
|
|
]));
|
|
}
|
|
|
|
/// A shell thicker than a step under a notch would turn the cavity inside out: a clear "not
|
|
/// supported yet" error instead of a failed boolean.
|
|
#[test]
|
|
fn shell_thicker_than_a_step_says_so() {
|
|
let mut s = Session::default();
|
|
for (c, p) in [
|
|
("solid.box", json!({"height": 7.5, "length": 30.0, "width": 35.0})),
|
|
("solid.box", json!({"corner": [0.0, 30.0, 2.5], "height": 22.5, "length": 22.5, "operation": "cut", "width": 20.0})),
|
|
] {
|
|
s.execute(c, &p).unwrap();
|
|
}
|
|
let e = s.execute("solid.shell", &json!({"faces": [[18.75, 35.0, 2.5]], "thickness": 1.5})).unwrap_err();
|
|
assert!(e.to_string().contains("not supported yet"), "{e}");
|
|
}
|
|
|
|
/// Panics inside the kernel are caught natively, but abort the web build (wasm can't unwind).
|
|
/// Building the samples must not panic at all (the Plastic Enclosure's bosses used to, 8 times).
|
|
#[test]
|
|
fn samples_build_without_panics() {
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
let me = std::thread::current().id();
|
|
let count = Arc::new(AtomicUsize::new(0));
|
|
let c2 = count.clone();
|
|
let prev = std::panic::take_hook();
|
|
std::panic::set_hook(Box::new(move |info| {
|
|
if std::thread::current().id() == me {
|
|
c2.fetch_add(1, Ordering::SeqCst);
|
|
}
|
|
let _ = info;
|
|
}));
|
|
let mut failed = Vec::new();
|
|
for sample in crate::sample::samples() {
|
|
let mut s = Session::default();
|
|
for c in (sample.script)()["commands"].as_array().into_iter().flatten() {
|
|
let _ = s.execute(c["command"].as_str().unwrap_or(""), &c["params"]);
|
|
}
|
|
let n = count.swap(0, Ordering::SeqCst);
|
|
if n > 0 {
|
|
failed.push(format!("{}: {n} panic(s)", sample.name));
|
|
}
|
|
}
|
|
std::panic::set_hook(prev);
|
|
assert!(failed.is_empty(), "{failed:?}");
|
|
}
|
|
|
|
/// Seeds that run clean: no failure, no invalid body, no volume going the wrong way.
|
|
#[test]
|
|
fn fixed_seeds_run_clean() {
|
|
for seed in FIXED_SEEDS {
|
|
let r = fuzz::run(*seed, 8);
|
|
assert!(!r.outcome.is_bug(), "seed {seed}: {:?}\n{}", r.outcome, r.script());
|
|
}
|
|
}
|
|
|
|
const FIXED_SEEDS: &[u64] = &[3, 5, 11, 16, 21, 29, 33, 35];
|