#![allow(
clippy::print_stderr,
reason = "diagnostic harness: prints a per-case comparison table"
)]
use std::panic::{self, AssertUnwindSafe};
use s2rst::r3::Vector;
use s2rst::s2::{Loop, Point, Polygon};
const INPUTS: &str = concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/data/boolean_op_inputs.txt"
);
const ORACLE: &str = concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/data/cpp_boolean_op_vectors.txt"
);
const MIN_MATCHES: usize = 13;
struct Case {
name: String,
op: String,
normalize: bool,
a: Vec<Vec<[f64; 3]>>,
b: Vec<Vec<[f64; 3]>>,
}
struct CppResult {
empty: bool,
loops: usize,
vertices: usize,
}
fn parse_inputs(text: &str) -> Vec<Case> {
let mut toks = text
.lines()
.filter(|l| !l.trim_start().starts_with('#'))
.flat_map(str::split_whitespace);
let mut cases = Vec::new();
while let Some(tok) = toks.next() {
assert_eq!(tok, "CASE");
let name = toks.next().unwrap().to_string();
let op = toks.next().unwrap().to_string();
let normalize = toks.next().unwrap() == "1";
let operand = |toks: &mut dyn Iterator<Item = &str>| {
let _label = toks.next().unwrap(); let num_loops: usize = toks.next().unwrap().parse().unwrap();
let mut loops = Vec::with_capacity(num_loops);
for _ in 0..num_loops {
assert_eq!(toks.next().unwrap(), "LOOP");
let nv: usize = toks.next().unwrap().parse().unwrap();
let mut verts = Vec::with_capacity(nv);
for _ in 0..nv {
let x = toks.next().unwrap().parse().unwrap();
let y = toks.next().unwrap().parse().unwrap();
let z = toks.next().unwrap().parse().unwrap();
verts.push([x, y, z]);
}
loops.push(verts);
}
loops
};
let a = operand(&mut toks);
let b = operand(&mut toks);
assert_eq!(toks.next().unwrap(), "END");
cases.push(Case {
name,
op,
normalize,
a,
b,
});
}
cases
}
fn parse_oracle(text: &str) -> std::collections::HashMap<String, CppResult> {
let mut map = std::collections::HashMap::new();
let mut lines = text.lines().filter(|l| !l.trim_start().starts_with('#'));
while let Some(case_line) = lines.next() {
let mut it = case_line.split_whitespace();
assert_eq!(it.next(), Some("CASE"));
let name = it.next().unwrap().to_string();
let result_line = lines.next().unwrap();
let kv = |key: &str| -> &str {
result_line
.split_whitespace()
.find_map(|t| t.strip_prefix(key))
.unwrap()
};
map.insert(
name,
CppResult {
empty: kv("empty=") == "1",
loops: kv("loops=").parse().unwrap(),
vertices: kv("vertices=").parse().unwrap(),
},
);
}
map
}
fn build(loops: &[Vec<[f64; 3]>], normalize: bool) -> Polygon {
let rust_loops = loops
.iter()
.map(|verts| {
let pts = verts
.iter()
.map(|&[x, y, z]| {
if normalize {
Point::from_coords(x, y, z)
} else {
Point::new(Vector::new(x, y, z))
}
})
.collect();
Loop::new(pts)
})
.collect();
Polygon::from_loops(rust_loops)
}
enum Outcome {
Panic,
Empty,
NonEmpty { loops: usize, vertices: usize },
}
fn run_case(case: &Case) -> Outcome {
let result = panic::catch_unwind(AssertUnwindSafe(|| {
let mut a = build(&case.a, case.normalize);
let mut b = build(&case.b, case.normalize);
match case.op.as_str() {
"union" => Polygon::union(&mut a, &mut b),
"intersection" => Polygon::intersection(&mut a, &mut b),
"difference" => Polygon::difference(&mut a, &mut b),
_ => Polygon::symmetric_difference(&mut a, &mut b),
}
}));
match result {
Err(_) => Outcome::Panic,
Ok(poly) if poly.is_empty_polygon() => Outcome::Empty,
Ok(poly) => Outcome::NonEmpty {
loops: poly.num_loops(),
vertices: poly.num_vertices(),
},
}
}
#[test]
fn boolean_op_matches_cpp_reference() {
let cases = parse_inputs(&std::fs::read_to_string(INPUTS).expect("read inputs"));
let oracle = parse_oracle(&std::fs::read_to_string(ORACLE).expect("read oracle"));
assert_eq!(cases.len(), 13, "expected 13 regression cases");
let prev_hook = panic::take_hook();
panic::set_hook(Box::new(|_| {}));
let mut matches = 0;
let mut rows = Vec::new();
for case in &cases {
let cpp = &oracle[&case.name];
let outcome = run_case(case);
let (rust_str, ok) = match &outcome {
Outcome::Panic => ("PANIC".to_string(), false),
Outcome::Empty => ("empty".to_string(), false),
Outcome::NonEmpty { loops, vertices } => {
(format!("nonempty {loops}L/{vertices}v"), !cpp.empty)
}
};
if ok {
matches += 1;
}
rows.push(format!(
" {:<34} {:<6} cpp={}L/{}v rust={:<18} {}",
case.name,
case.op,
cpp.loops,
cpp.vertices,
rust_str,
if ok { "MATCH" } else { "DIFF" }
));
}
panic::set_hook(prev_hook);
eprintln!(
"\nBoolean-op vs C++ oracle (upstream union is non-empty for all {}):",
cases.len()
);
for row in &rows {
eprintln!("{row}");
}
eprintln!(
"matches: {}/{} (floor {MIN_MATCHES})\n",
matches,
cases.len()
);
assert!(
matches >= MIN_MATCHES,
"boolean-op/C++ matches regressed: {matches} < {MIN_MATCHES}"
);
}