Report accepted Boolean retries on features and fuzz runs (#249)

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-12 10:38:04 -04:00
1 parent 611422e4df
commit f0dda667bb
15 files changed
+491 -26

No files matched your search

+1
View File
@@ -261,6 +261,7 @@ fn inspect_feature(s: &mut Session, p: &Value) -> Result<Value> {
"id": f.id, "name": f.name, "type": f.kind.type_name(), "suppressed": f.suppressed, "rolled_back": rolled_back,
"definition": def, "omitted": omitted, "inputs": inputs,
"error": r.and_then(|r| r.error.clone()), "warning": r.and_then(|r| r.warning.clone()),
"boolean_diagnostics": r.map(|r| &r.boolean_diagnostics),
}))
}
+1
View File
@@ -205,6 +205,7 @@ fn inspect(s: &mut Session, p: &Value) -> Result<Value> {
json!({
"id": f.id, "name": f.name, "type": f.kind.type_name(), "suppressed": f.suppressed, "rolled_back": i >= marker,
"error": r.and_then(|r| r.error.clone()), "warning": r.and_then(|r| r.warning.clone()), "ms": r.map(|r| r.ms),
"boolean_diagnostics": r.map(|r| &r.boolean_diagnostics),
})
})
.collect();
+34 -5
View File
@@ -64,6 +64,8 @@ pub struct Run {
pub unsupported: Vec<(String, String)>,
/// Steps whose generated parameters a command refused.
pub rejected: usize,
pub boolean_fallbacks: std::collections::BTreeMap<solvecraft_kernel::BooleanStrategy, usize>,
pub boolean_reports_truncated: bool,
}
impl Run {
@@ -289,7 +291,15 @@ pub fn run(seed: u64, max_steps: usize) -> Run {
pub fn run_with(seed: u64, max_steps: usize, on_step: &mut dyn FnMut(&Step)) -> Run {
let mut r = Rng::new(seed);
let mut s = Session::default();
let mut run = Run { seed, steps: Vec::new(), outcome: Outcome::ok(), unsupported: Vec::new(), rejected: 0 };
let mut run = Run {
seed,
steps: Vec::new(),
outcome: Outcome::ok(),
unsupported: Vec::new(),
rejected: 0,
boolean_fallbacks: Default::default(),
boolean_reports_truncated: false,
};
let mut last_feature: Option<String> = None;
let n = 2 + r.below(max_steps.max(2) - 1);
let mut tries = 0;
@@ -314,6 +324,12 @@ pub fn run_with(seed: u64, max_steps: usize, on_step: &mut dyn FnMut(&Step)) ->
continue;
}
let kind = st.kind;
for result in s.model.results.iter().skip(before).filter(|result| result.error.is_none()) {
run.boolean_reports_truncated |= result.boolean_diagnostics.truncated;
for event in &result.boolean_diagnostics.events {
*run.boolean_fallbacks.entry(event.strategy).or_default() += 1;
}
}
run.steps.push(st);
if out.kind != "ok" {
run.outcome = out;
@@ -329,14 +345,19 @@ pub fn run_with(seed: u64, max_steps: usize, on_step: &mut dyn FnMut(&Step)) ->
/// Run fixed steps (a recorded or minimised design): the index and outcome of the first step
/// that isn't ok (or the last step's, ok).
pub fn replay(steps: &[Step]) -> (usize, Outcome) {
let (index, outcome, _) = replay_session(steps);
(index, outcome)
}
fn replay_session(steps: &[Step]) -> (usize, Outcome, Session) {
let mut s = Session::default();
for (i, st) in steps.iter().enumerate() {
let out = apply(&mut s, st);
if out.kind != "ok" {
return (i, out);
return (i, out, s);
}
}
(steps.len().saturating_sub(1), Outcome::ok())
(steps.len().saturating_sub(1), Outcome::ok(), s)
}
/// The start of a message, to tell failures apart (numbers vary between versions of a case).
@@ -367,6 +388,14 @@ pub fn minimise(run: &Run) -> Run {
}
}
}
let (_, outcome) = replay(&steps);
Run { seed: run.seed, steps, outcome, unsupported: Vec::new(), rejected: 0 }
let (_, outcome, session) = replay_session(&steps);
let mut boolean_fallbacks = std::collections::BTreeMap::new();
let mut boolean_reports_truncated = false;
for result in &session.model.results {
boolean_reports_truncated |= result.boolean_diagnostics.truncated;
for event in &result.boolean_diagnostics.events {
*boolean_fallbacks.entry(event.strategy).or_default() += 1;
}
}
Run { seed: run.seed, steps, outcome, unsupported: Vec::new(), rejected: 0, boolean_fallbacks, boolean_reports_truncated }
}
+1
View File
@@ -118,6 +118,7 @@ fn join_meeting_nothing_keeps_the_first_body() {
assert!(rel(volume(&mut s), 1064.0) < 1e-6);
}
mod boolean_diagnostics;
mod component_frames;
mod mirror_planes;
mod primitive_points;
@@ -0,0 +1,52 @@
use super::*;
use solvecraft_kernel::BooleanStrategy;
#[test]
fn boolean_retry_warning_and_details_survive_cache_undo_and_reopen() {
let slope = 1f64.to_radians().tan();
let mut session = Session::default();
run(&mut session, "sketch.create", json!({"plane":"XZ","name":"Floor section"}));
run(&mut session, "sketch.line", json!({"points":[[0,0],[100,0],[100,-10.-slope*100.],[0,-10.]],"closed":true}));
run(&mut session, "sketch.finish", json!({}));
run(&mut session, "solid.extrude", json!({"sketch":"Floor section","distance":80,"body_name":"Floor"}));
run(&mut session, "solid.cylinder", json!({"base":[50,40,-5],"radius":10,"height":30,"operation":"cut"}));
run(&mut session, "sketch.create", json!({"plane":"XY","name":"Ring section"}));
run(&mut session, "sketch.circle.center", json!({"center":[50,40],"radius":20}));
run(&mut session, "sketch.circle.center", json!({"center":[50,40],"radius":10}));
run(&mut session, "sketch.finish", json!({}));
run(
&mut session,
"solid.extrude",
json!({"sketch":"Ring section","profiles":[{"point":[65,40]}],"start_offset":-5,"distance":7,"body_name":"Ring"}),
);
let id = run(&mut session, "solid.combine", json!({"target":"Floor","tools":["Ring"],"operation":"join"}))["feature"].as_u64().unwrap();
let check = |session: &Session| {
let result = session.model.result(id).unwrap();
assert!(result.error.is_none());
assert!(result.warning.as_ref().is_some_and(|warning| warning.contains("Boolean retry:")), "{result:?}");
assert!(
result.boolean_diagnostics.events.iter().any(|event| event.strategy == BooleanStrategy::CommonBore),
"{:?}",
result.boolean_diagnostics
);
result.boolean_diagnostics.clone()
};
let original = check(&session);
let expected = (10. + slope * 50.) * (8000. - PI * 100.) + PI * 300. * 5.;
assert!(rel(volume(&mut session), expected) < 2e-5);
let inspected = run(&mut session, "timeline.inspect", json!({"feature":id}));
assert_eq!(inspected["boolean_diagnostics"], serde_json::to_value(&original).unwrap());
session.refresh();
assert_eq!(check(&session), original);
run(&mut session, "edit.undo", json!({}));
run(&mut session, "edit.redo", json!({}));
assert_eq!(check(&session), original);
let reopened_doc = solvecraft_doc::Document::from_json(&session.doc.to_json()).unwrap();
let reopened = Session::new(reopened_doc);
assert_eq!(check(&reopened), original);
// A later independent feature must not inherit the earlier retry's warning.
run(&mut session, "solid.box", json!({"corner":[200,0,0],"length":5,"width":5,"height":5}));
let last = session.model.results.last().unwrap();
assert!(last.warning.is_none());
assert!(last.boolean_diagnostics.events.is_empty());
}