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:
15 files changed
+491
-26
No files matched your search
@@ -0,0 +1,195 @@
|
||||
//! Scoped reports for successful Boolean retries. Discarded attempts never enter
|
||||
//! their caller's report, and independent evaluations cannot inherit old events.
|
||||
|
||||
use std::cell::RefCell;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
const MAX_EVENTS: usize = 1024;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum BooleanStrategy {
|
||||
RefinedTolerance,
|
||||
ShiftedOperands,
|
||||
CoplanarSeparation,
|
||||
CommonBore,
|
||||
PlanarFallback,
|
||||
ToolNudge,
|
||||
Piecewise,
|
||||
}
|
||||
|
||||
/// Numerical adjustments used by a successful retry, in millimetres/radians.
|
||||
/// Operand shifts are reversed on the result; face shifts are temporary working
|
||||
/// offsets. A tool nudge, in contrast, changes the relative operand placement.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
pub struct BooleanFallback {
|
||||
pub strategy: BooleanStrategy,
|
||||
pub translation_mm: f64,
|
||||
pub rotation_rad: f64,
|
||||
pub face_shift_mm: f64,
|
||||
pub tolerance_mm: f64,
|
||||
}
|
||||
|
||||
impl BooleanFallback {
|
||||
pub(crate) fn new(strategy: BooleanStrategy) -> Self {
|
||||
Self { strategy, translation_mm: 0., rotation_rad: 0., face_shift_mm: 0., tolerance_mm: 0. }
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
match self.strategy {
|
||||
BooleanStrategy::RefinedTolerance => format!("refined tolerance {:.6e} mm", self.tolerance_mm),
|
||||
BooleanStrategy::ShiftedOperands => {
|
||||
format!("shifted operands {:.6e} mm (result shifted back), tolerance {:.6e} mm", self.translation_mm, self.tolerance_mm)
|
||||
}
|
||||
BooleanStrategy::CoplanarSeparation => format!("temporary coincident-face separation up to {:.6e} mm per face", self.face_shift_mm),
|
||||
BooleanStrategy::CommonBore => format!("temporary bore offset {:.6e} mm, followed by recutting the original bore", self.face_shift_mm),
|
||||
BooleanStrategy::PlanarFallback => "exact planar fallback (no displacement)".into(),
|
||||
BooleanStrategy::ToolNudge => format!("tool displacement {:.6e} mm, rotation {:.6e} rad", self.translation_mm, self.rotation_rad),
|
||||
BooleanStrategy::Piecewise => "piecewise retry (no displacement)".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
|
||||
pub struct BooleanDiagnostics {
|
||||
pub events: Vec<BooleanFallback>,
|
||||
pub truncated: bool,
|
||||
}
|
||||
|
||||
impl BooleanDiagnostics {
|
||||
pub(crate) fn push(&mut self, event: BooleanFallback) {
|
||||
if self.events.len() < MAX_EVENTS {
|
||||
self.events.push(event);
|
||||
} else {
|
||||
self.truncated = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn warning(&self) -> Option<String> {
|
||||
if self.events.is_empty() && !self.truncated {
|
||||
return None;
|
||||
}
|
||||
let mut descriptions: Vec<String> = self.events.iter().take(4).map(BooleanFallback::description).collect();
|
||||
if self.events.len() > 4 {
|
||||
descriptions.push(format!("{} additional retries", self.events.len() - 4));
|
||||
}
|
||||
if self.truncated {
|
||||
descriptions.push(format!("report capped at {MAX_EVENTS} retries"));
|
||||
}
|
||||
Some(format!("Boolean retry: {}", descriptions.join("; ")))
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static REPORT: RefCell<Option<BooleanDiagnostics>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
struct Scope {
|
||||
previous: Option<Option<BooleanDiagnostics>>,
|
||||
}
|
||||
|
||||
impl Drop for Scope {
|
||||
fn drop(&mut self) {
|
||||
if let Some(previous) = self.previous.take() {
|
||||
REPORT.with(|cell| {
|
||||
if let Ok(mut report) = cell.try_borrow_mut() {
|
||||
*report = previous;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Capture only the events produced by this call. Nested callers explicitly merge
|
||||
/// an accepted attempt; dropping a scope restores its parent even during unwinding.
|
||||
pub fn capture_boolean_diagnostics<T>(run: impl FnOnce() -> T) -> (T, BooleanDiagnostics) {
|
||||
let previous = REPORT.with(|cell| cell.try_borrow_mut().ok().map(|mut report| report.replace(BooleanDiagnostics::default())));
|
||||
let scope = Scope { previous };
|
||||
let result = run();
|
||||
let report = REPORT.with(|cell| cell.try_borrow_mut().ok().and_then(|mut report| report.take())).unwrap_or_default();
|
||||
drop(scope);
|
||||
(result, report)
|
||||
}
|
||||
|
||||
pub(crate) fn record(event: BooleanFallback) {
|
||||
REPORT.with(|cell| {
|
||||
if let Ok(mut report) = cell.try_borrow_mut()
|
||||
&& let Some(report) = report.as_mut()
|
||||
{
|
||||
report.push(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn merge(report: BooleanDiagnostics) {
|
||||
for event in report.events {
|
||||
record(event);
|
||||
}
|
||||
if report.truncated {
|
||||
REPORT.with(|cell| {
|
||||
if let Ok(mut report) = cell.try_borrow_mut()
|
||||
&& let Some(report) = report.as_mut()
|
||||
{
|
||||
report.truncated = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn nested_attempts_are_committed_explicitly_and_do_not_leak() {
|
||||
let (_, report) = capture_boolean_diagnostics(|| {
|
||||
let (_, discarded) = capture_boolean_diagnostics(|| record(BooleanFallback::new(BooleanStrategy::ToolNudge)));
|
||||
assert_eq!(discarded.events.len(), 1);
|
||||
let (_, accepted) = capture_boolean_diagnostics(|| record(BooleanFallback::new(BooleanStrategy::PlanarFallback)));
|
||||
merge(accepted);
|
||||
});
|
||||
assert_eq!(report.events.len(), 1);
|
||||
assert_eq!(report.events[0].strategy, BooleanStrategy::PlanarFallback);
|
||||
assert!(capture_boolean_diagnostics(|| {}).1.events.is_empty());
|
||||
record(BooleanFallback::new(BooleanStrategy::ToolNudge));
|
||||
assert!(capture_boolean_diagnostics(|| {}).1.events.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_operations_and_failed_calls_have_no_retry_report() {
|
||||
use solvecraft_geom::Vec3;
|
||||
let body = crate::box_solid(Vec3::ZERO, Vec3::new(10., 10., 10.)).unwrap();
|
||||
let other = crate::box_solid(Vec3::new(5., 0., 0.), Vec3::new(15., 10., 10.)).unwrap();
|
||||
let (result, diagnostics) = capture_boolean_diagnostics(|| crate::boolean(&body, &other, crate::BoolOp::Union));
|
||||
assert!(result.unwrap().is_some());
|
||||
assert!(diagnostics.warning().is_none(), "{diagnostics:?}");
|
||||
let mesh = crate::mesh_body(&[Vec3::ZERO, Vec3::X, Vec3::Y], &[[0, 1, 2]]).unwrap();
|
||||
let (result, diagnostics) = capture_boolean_diagnostics(|| crate::boolean(&body, &mesh, crate::BoolOp::Cut));
|
||||
assert!(result.is_err());
|
||||
assert!(diagnostics.warning().is_none(), "{diagnostics:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unwinding_restores_parent_and_reports_are_thread_local_and_bounded() {
|
||||
let (_, report) = capture_boolean_diagnostics(|| {
|
||||
let failed = crate::guard("diagnostic test", || {
|
||||
capture_boolean_diagnostics(|| -> crate::Result<()> {
|
||||
record(BooleanFallback::new(BooleanStrategy::ToolNudge));
|
||||
panic!("discard this attempt");
|
||||
})
|
||||
.0
|
||||
});
|
||||
assert!(failed.is_err());
|
||||
let separate = std::thread::spawn(|| capture_boolean_diagnostics(|| record(BooleanFallback::new(BooleanStrategy::ToolNudge))).1);
|
||||
assert_eq!(separate.join().unwrap().events.len(), 1);
|
||||
for _ in 0..MAX_EVENTS + 1 {
|
||||
record(BooleanFallback::new(BooleanStrategy::PlanarFallback));
|
||||
}
|
||||
});
|
||||
assert_eq!(report.events.len(), MAX_EVENTS);
|
||||
assert!(report.truncated);
|
||||
assert!(report.events.iter().all(|event| event.strategy == BooleanStrategy::PlanarFallback));
|
||||
assert!(report.warning().unwrap().contains("capped"));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user