Tests: check what each operation is for, not how many faces it makes

The kernel switch broke these tests only through their counts, which record how a kernel splits
faces rather than whether the result is right. Each now checks its intent:
- a fitted circle is the exact circle (centre, radius, a full turn), not "one segment";
- the tight bend sweeps a valid solid of Pappus' volume, πr²(40 + 5π/2), not a count of faces,
  edges and vertices;
- the Pi case keeps each outer side one face, stays valid after every feature and rebuilds the
  same topology at 32 whether built there or edited there, without a hard-coded table of totals;
- a certified paint stays on the wall's radius and covers as many faces after an edit, a copy
  and reopening as when it was painted;
- the offset sleeve and the extended strip are checked by their exact areas only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-29 23:39:41 -04:00
committed by Irakli Kourtis
parent 0c4f54edf3
commit 74001e5325
5 changed files with 39 additions and 40 deletions
+10 -16
View File
@@ -139,21 +139,14 @@ fn a_parameter_edit_rebuilds_like_a_fresh_build_and_reverts_exactly() {
assert!(problems.is_empty(), "{}", problems.join("\n"));
}
/// The README demo's Raspberry Pi case (`examples/demo/enclosure_ui_mcp.py --story pi`): its
/// face count after each feature, and each outer side (the four walls and the bottom) one face
/// throughout: (a) built at height 24, (b) built at 24 and changed to 32 in Parameters, (c)
/// built at 32. The port cut-outs and the floor vents used to split the long wall (or the end
/// wall at 24) into strips along the openings' tops and bottoms.
/// The README demo's Raspberry Pi case (`examples/demo/enclosure_ui_mcp.py --story pi`): after
/// each feature, each outer side (the four walls and the bottom) is one face and the case is as
/// tall as its height, and a height changed in Parameters rebuilds the same topology as one
/// built at that height: (a) built at height 24, (b) built at 24 and changed to 32, (c) built at
/// 32. The port cut-outs and the floor vents used to split the long wall (or the end wall at 24)
/// into strips along the openings' tops and bottoms.
#[test]
fn pi_case_faces_per_step_at_both_heights() {
// Sketch1 (1) .. RectangularPattern2 (14): the faces once features 1..=k are built. The box
// (6), its four corner rounds (10), shelled open at the top (outside 9, inside 9, rim 1:
// 19); each standoff (a ring, Ø6 round a Ø2.5 hole) adds its outer and inner wall (each one
// cylinder, closed along its seam), its top and the floor inside its hole (4, so 23, and 35
// for four); the three port openings through a wall 4 walls each (47); the end cut-out 4
// (51); each of five floor vents 4 (55, 71).
const FACES: [(usize, usize); 13] =
[(2, 6), (3, 10), (4, 19), (5, 19), (6, 23), (7, 35), (8, 35), (9, 47), (10, 47), (11, 51), (12, 51), (13, 55), (14, 71)];
fn pi_case_sides_stay_whole_at_both_heights() {
let dir = scratch_dir("pi");
let design: Value = serde_json::from_slice(&std::fs::read(root().join("tests/action_matrix/designs/pi-case.solidcraft")).unwrap()).unwrap();
let at = |height: &str, file: &str| {
@@ -176,12 +169,13 @@ fn pi_case_faces_per_step_at_both_heights() {
for (case, s, h) in [("(a) at 24", a, 24.), ("(b) 24 changed to 32", b, 32.), ("(c) at 32", c, 32.)] {
assert!(errors(&s).is_empty(), "{case}: features fail: {:?}", errors(&s));
let mut steps = Vec::new();
for (k, want) in FACES {
// Sketch1 (1) is no body; the box (2) .. RectangularPattern2 (14).
for k in 2..=14 {
let mut t = s.scratch();
t.execute("timeline.rollback", &json!({"position": k})).unwrap();
let st = t.world_state().unwrap();
let body = st.bodies.iter().find(|b| b.name == "Body1").unwrap();
assert_eq!(body.body.face_count(), want, "{case}: faces after feature {k}");
assert!(body.body.validity().is_empty(), "{case}: after feature {k}: {:?}", body.body.validity());
// Each outer side: the faces in its plane.
for (n, d) in [([0., -1., 0.], 33.), ([0., 1., 0.], 33.), ([-1., 0., 0.], 47.5), ([1., 0., 0.], 47.5), ([0., 0., -1.], 0.)] {
let on = (0..body.body.face_count())
@@ -15,6 +15,8 @@ fn paint(session: &mut Session, body: &str, point: Value) {
run(session, "config.row", json!({"name":"Painted","values":{key:"ABS - Red"}}));
run(session, "config.activate", json!({"row":"Painted"}));
}
/// Every painted face of `name` lies on the cylinder of `radius` round its axis; how many faces
/// are painted, to compare a copy with its original.
fn painted_radius(session: &Session, name: &str, radius: f64) -> usize {
let state = session.model.state();
let body =
@@ -30,7 +32,7 @@ fn painted_radius(session: &Session, name: &str, radius: f64) -> usize {
}
}
}
assert!(!painted.is_empty());
assert!(!painted.is_empty(), "{name}: nothing painted");
painted.len()
}
@@ -49,22 +51,22 @@ fn certified_annulus_side_rows_follow_radius_edits_and_clone() {
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "solid.extrude", json!({"distance":10,"profiles":[{"point":[9.5,0]}],"body_name":"Ring"}));
paint(&mut s, "Ring", json!([10, 0, 5]));
assert_eq!(painted_radius(&s, "Ring", 10.), 1, "the outer circle's wall is one face, closed along its seam");
let pieces = painted_radius(&s, "Ring", 10.);
let Column::FaceAppearance(target) = &s.doc.configs.columns[0] else { panic!() };
assert_eq!(target.certificate, Some(FaceCertificate::ExtrudeSide { sketch, curve: "outer".into() }));
run(&mut s, "parameters.change", json!({"name":dimensions[0],"expression":"12 mm"}));
run(&mut s, "parameters.change", json!({"name":dimensions[1],"expression":"9.25 mm"}));
run(&mut s, "config.activate", json!({"row":"Painted"}));
assert_eq!(painted_radius(&s, "Ring", 12.), 1);
assert_eq!(painted_radius(&s, "Ring", 12.), pieces);
run(&mut s, "component.paste_new", json!({"component":part,"translate":[30,0,0]}));
assert_eq!(s.doc.configs.columns.len(), 2);
run(&mut s, "config.activate", json!({"row":"Painted"}));
assert_eq!(painted_radius(&s, "Ring (2)", 12.), 1);
assert_eq!(painted_radius(&s, "Ring (2)", 12.), pieces);
let reopened = solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap();
let mut reopened = Session::new(reopened);
run(&mut reopened, "config.activate", json!({"row":"Painted"}));
assert_eq!(painted_radius(&reopened, "Ring", 12.), 1);
assert_eq!(painted_radius(&reopened, "Ring (2)", 12.), 1);
assert_eq!(painted_radius(&reopened, "Ring", 12.), pieces);
assert_eq!(painted_radius(&reopened, "Ring (2)", 12.), pieces);
run(&mut reopened, "config.activate", json!({"row":"Default"}));
assert!(reopened.doc.appearances.faces.is_empty());
}
@@ -227,11 +229,11 @@ fn certified_side_transfer_preserves_geometry_and_unsupported_taper_refuses_atom
run(&mut s, "occurrence.move", json!({"occurrence":occurrence,"translate":[30,20,0],"angle":90,"axis":[0,0,1],"capture":false}));
run(&mut s, "component.move_bodies", json!({"component":destination,"bodies":["Round"]}));
run(&mut s, "config.activate", json!({"row":"Painted"}));
assert_eq!(painted_radius(&s, "Round", 10.), 1);
let pieces = painted_radius(&s, "Round", 10.);
let before = s.world_state().unwrap().body("Round").unwrap().mesh().bounds();
let mut s = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
run(&mut s, "config.activate", json!({"row":"Painted"}));
assert_eq!(painted_radius(&s, "Round", 10.), 1);
assert_eq!(painted_radius(&s, "Round", 10.), pieces);
assert!(
s.world_state().unwrap().body("Round").unwrap().mesh().bounds().min.dist(before.min) < 1e-8,
"before {before:?}, reopened {:?}",
+1 -3
View File
@@ -1101,7 +1101,7 @@ mod tests {
run(&mut s, "surface.offset", json!({"faces": [[10, 0, 10]], "chain": true, "distance": 2, "body": "Can", "body_name": "Sleeve"}));
let st = s.world_state().unwrap();
let sleeve = &st.body("Sleeve").unwrap().body;
assert!(sleeve.is_surface() && sleeve.face_count() == 1);
assert!(sleeve.is_surface());
let want = 2.0 * std::f64::consts::PI * 12.0 * 20.0;
assert!((area(&s, "Sleeve") - want).abs() < want * 1e-3, "{} vs {want}", area(&s, "Sleeve")); // (measured on meshes)
assert!(st.body("Can").is_some(), "the source body stays");
@@ -1326,8 +1326,6 @@ mod tests {
let parabola = 2.5 * (1.0 + 25.0 * k * k).sqrt() + (5.0 * k).asinh() / (2.0 * k);
for (mode, run_off) in [("tangent", 5.0), ("curvature", parabola)] {
run(&mut s, "surface.extend", json!({"body": "Strip", "edges": [side], "distance": 5, "mode": mode}));
let st = s.world_state().unwrap();
assert_eq!(st.body("Strip").unwrap().body.face_count(), 1, "{mode}");
let want = 10.0 * pi / 2.0 * 20.0 + run_off * 20.0;
assert!((area(&s, "Strip") - want).abs() < want * 1e-3, "{mode}: {} vs {want}", area(&s, "Strip")); // (measured on meshes)
let cold = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
+9 -4
View File
@@ -478,8 +478,14 @@ mod tests {
})
.collect();
assert_eq!(fit_loop(&points, 0.001, 1e-6).segs.len(), 16);
// A circle is one closed arc.
assert_eq!(fit_loop(&points, 0.2, 1e-6).segs.len(), 1);
assert_exact_circle(&fit_loop(&points, 0.2, 1e-6), Vec2::ZERO, 10.);
}
/// The loop is the exact circle: one full turn of an arc round `center` at `radius`.
fn assert_exact_circle(fitted: &Loop2, center: Vec2, radius: f64) {
let [solidcraft_geom::Seg2::Arc { center: c, radius: r, sweep, .. }] = fitted.segs.as_slice() else { panic!("not one arc: {fitted:?}") };
assert!(c.dist(center) < 1e-9 && (r - radius).abs() < 1e-9, "centre {c:?}, radius {r}");
assert!((sweep.abs() - std::f64::consts::TAU).abs() < 1e-9, "sweep {sweep}");
}
#[test]
@@ -550,8 +556,7 @@ mod tests {
.collect();
let inner = Loop2::polygon(&points);
let fitted = fit_loop(&points, 0.05, 1e-6);
// Fitted as a circle: one closed arc.
assert_eq!(fitted.segs.len(), 1);
assert_exact_circle(&fitted, Vec2::ZERO, 1.02);
assert!(certify_nesting(&[outer.clone(), inner], &[outer, fitted], 1e-6, 3.).is_err());
}
+9 -9
View File
@@ -2309,10 +2309,11 @@ fn tangent_holes_pinch_alike() {
assert_eq!(m.merged.faces, 12, "{:?}", m.merged);
}
/// A d10 tube bent at R5: the inside of the bend is a horn torus pinched to a point. One torus
/// face, with a degenerate loop at the pinch (5 faces, 6 edges, 4 vertices).
/// A d10 tube bent at R5: the inside of the bend is a horn torus pinched to a point, so the
/// sweep just doesn't fold over itself. It is a valid solid of Pappus' volume, πr² times the
/// path's length (20 + 5π/2 + 20).
#[test]
fn tight_bend_counts() {
fn a_bend_as_tight_as_the_tube_sweeps_a_valid_solid() {
use crate::build::PathSeg;
let region = Region2 { outer: Loop2::circle(Vec2::ZERO, 5.0), holes: vec![] };
let path = [
@@ -2326,12 +2327,11 @@ fn tight_bend_counts() {
PathSeg::Line { a: Vec3::new(5.0, 0.0, 25.0), b: Vec3::new(25.0, 0.0, 25.0) },
];
let b = sweep(&Plane::XY, &region, &path).unwrap();
let m = measure(&b).unwrap().merged;
// Two caps and a face per path segment. The circle starts (one closed arc) at (5, 0), which
// runs up the first straight to (5, 0, 20) on the bend's axis: the turn leaves it there, so
// the bend has no seam, and the second straight's seam runs on from it to (25, 0, 20). Edges:
// the four circles and the two straights' seams; vertices: the seams' ends.
assert_eq!((m.faces, m.edges, m.vertices), (5, 6, 3), "{m:?}");
assert!(b.validity().is_empty(), "{:?}", b.validity());
let pi = std::f64::consts::PI;
let want = pi * 25.0 * (40.0 + 2.5 * pi);
let v = measure(&b).unwrap().volume;
assert!(rel(v, want) < 1e-6, "{v} vs {want}");
}
#[test]