Faster tests: hostile_params_never_panic shares its commands across threads (154 s to 20 s), the design-file fuzz test tries its seeded cases on all cores (21 s to 7.5 s), the kernel's tangent-primitive boolean test runs its three seeds at once, and the headless scenario harness skips the software viewport render it threw away every frame (scenarios 63 s to 26 s). kill_mid_save waits for the saver to reach a varying round instead of sleeping a fixed time, so it holds on a busy machine

This commit is contained in:
CypherNaugh_0x authored and bherbruck committed 2026-10-10 13:24:08 -04:00
1 parent 4a41de72ec
commit aa268f0f0e
6 files changed
+90 -41

No files matched your search

+24 -9
View File
@@ -153,7 +153,8 @@ fn try_bytes(bytes: &[u8]) {
#[test]
fn damaged_and_hostile_design_files_never_panic() {
let mut rng = Rng(0x5eed_cafe_f00d_d00d);
let mut failures = Vec::new();
// The cases come from one seeded sequence; trying them is independent, so threads share it.
let mut all_cases: Vec<(String, String, Vec<u8>)> = Vec::new();
for p in corpus() {
let name = p.file_name().unwrap_or_default().to_string_lossy().to_string();
let bytes = std::fs::read(&p).unwrap();
@@ -198,14 +199,28 @@ fn damaged_and_hostile_design_files_never_panic() {
cases.push(("features repeated".into(), v.to_string().into_bytes()));
cases.push(("deep nesting".into(), format!("{}{}", "[".repeat(100_000), "]".repeat(100_000)).into_bytes()));
cases.push(("binary".into(), (0..4096u32).map(|i| (i.wrapping_mul(2_654_435_761) >> 24) as u8).collect()));
for (what, b) in cases {
if std::env::var_os("CORPUS_TRACE").is_some() {
eprintln!("{name}: {what}");
}
if std::panic::catch_unwind(|| try_bytes(&b)).is_err() {
failures.push(format!("{name}: {what}"));
}
}
all_cases.extend(cases.into_iter().map(|(what, b)| (name.clone(), what, b)));
}
let next = std::sync::atomic::AtomicUsize::new(0);
let threads = std::thread::available_parallelism().map_or(4, |n| n.get());
let failures: Vec<String> = std::thread::scope(|scope| {
let workers: Vec<_> = (0..threads)
.map(|_| {
scope.spawn(|| {
let mut failures = Vec::new();
while let Some((name, what, b)) = all_cases.get(next.fetch_add(1, std::sync::atomic::Ordering::Relaxed)) {
if std::env::var_os("CORPUS_TRACE").is_some() {
eprintln!("{name}: {what}");
}
if std::panic::catch_unwind(|| try_bytes(b)).is_err() {
failures.push(format!("{name}: {what}"));
}
}
failures
})
})
.collect();
workers.into_iter().flat_map(|w| w.join().unwrap()).collect()
});
assert!(failures.is_empty(), "panics:\n{}", failures.join("\n"));
}
+12 -2
View File
@@ -22,10 +22,20 @@ fn killed_mid_save_the_old_file_survives() {
assert_eq!(round_of(&path).as_deref(), Some("0"));
let mut kills = 0;
let mut seen = std::collections::BTreeSet::new();
let modified = || std::fs::metadata(&path).and_then(|m| m.modified()).ok();
for i in 0..25u64 {
let before = modified();
let mut child = Command::new(exe).args(["save-stress", &path.to_string_lossy(), "100000"]).spawn().unwrap();
// Let it get into its save loop (building the design takes a moment), then kill it.
std::thread::sleep(Duration::from_millis(150 + (i * 37) % 200));
// Wait until it is in its save loop (its first save replaced the file) and has reached a
// round that varies from kill to kill, however busy the machine is; then kill it a
// varying while later.
let target = i % 10;
let reached = || modified() != before && round_of(&path).and_then(|r| r.parse::<u64>().ok()).is_some_and(|r| r >= target);
let t0 = std::time::Instant::now();
while !reached() && t0.elapsed() < Duration::from_secs(20) && child.try_wait().unwrap().is_none() {
std::thread::sleep(Duration::from_millis(2));
}
std::thread::sleep(Duration::from_millis((i * 7) % 30));
if child.try_wait().unwrap().is_none() {
child.kill().unwrap();
kills += 1;
+41 -28
View File
@@ -279,8 +279,6 @@ fn hostile_params_never_panic() {
"chain",
"at",
"curve",
"major",
"minor",
"minor_radius",
"apex",
"rho",
@@ -298,32 +296,47 @@ fn hostile_params_never_panic() {
"kind",
"prefix",
];
let mut internal = Vec::new();
for spec in command_specs() {
if matches!(spec.id, "doc.open" | "file.save" | "file.save_as" | "file.export" | "file.save_mesh" | "sketch.export_dxf") {
continue; // file system side effects are covered by their own tests
}
for v in hostile_values() {
for k in keys {
if k == "path" && spec.id.starts_with("parameters.") {
continue; // parameter files: covered by their own tests (no stray files)
}
let mut s = Session::default();
// A model with a sketch, a body and an active sketch, so most commands get far.
let _ = s.execute("solid.box", &json!({"length": 10, "width": 10, "height": 10}));
let _ = s.execute("sketch.create", &json!({"plane": "XY"}));
let _ = s.execute("sketch.rectangle.two_point", &json!({"p0": [0, 0], "p1": [5, 5]}));
let p = json!({ k: v.clone() });
if let Err(EngineError::Internal(id, msg)) = s.execute(spec.id, &p) {
internal.push(format!("{id} {p}: {msg}"));
}
// Whole-value hostility too.
if let Err(EngineError::Internal(id, msg)) = s.execute(spec.id, &v) {
internal.push(format!("{id} {v}: {msg}"));
}
}
}
}
// Commands are independent; worker threads take them one at a time (their costs vary a lot).
let ids: Vec<&str> = command_specs()
.iter()
.map(|spec| spec.id)
.filter(|id| !matches!(*id, "doc.open" | "file.save" | "file.save_as" | "file.export" | "file.save_mesh" | "sketch.export_dxf")) // file system side effects are covered by their own tests
.collect();
let next = std::sync::atomic::AtomicUsize::new(0);
let threads = std::thread::available_parallelism().map_or(4, |n| n.get());
let internal: Vec<String> = std::thread::scope(|scope| {
let workers: Vec<_> = (0..threads)
.map(|_| {
scope.spawn(|| {
let mut internal = Vec::new();
while let Some(&id) = ids.get(next.fetch_add(1, std::sync::atomic::Ordering::Relaxed)) {
for v in hostile_values() {
for k in keys {
if k == "path" && id.starts_with("parameters.") {
continue; // parameter files: covered by their own tests (no stray files)
}
let mut s = Session::default();
// A model with a sketch, a body and an active sketch, so most commands get far.
let _ = s.execute("solid.box", &json!({"length": 10, "width": 10, "height": 10}));
let _ = s.execute("sketch.create", &json!({"plane": "XY"}));
let _ = s.execute("sketch.rectangle.two_point", &json!({"p0": [0, 0], "p1": [5, 5]}));
let p = json!({ k: v.clone() });
if let Err(EngineError::Internal(id, msg)) = s.execute(id, &p) {
internal.push(format!("{id} {p}: {msg}"));
}
// Whole-value hostility too.
if let Err(EngineError::Internal(id, msg)) = s.execute(id, &v) {
internal.push(format!("{id} {v}: {msg}"));
}
}
}
}
internal
})
})
.collect();
workers.into_iter().flat_map(|w| w.join().unwrap()).collect()
});
assert!(internal.is_empty(), "commands panicked:\n{}", internal.join("\n"));
}
+6 -2
View File
@@ -1361,8 +1361,12 @@ fn partial_revolve_on_the_axis() {
/// exactly where the other body cut them).
#[test]
fn boolean_identity_on_tangent_primitives() {
for seed in [1, 32, 169] {
let c = crate::fuzz::boolean_case(seed);
// Independent seeds, one thread each.
let cases: Vec<_> = std::thread::scope(|scope| {
let workers = [1, 32, 169].map(|seed| scope.spawn(move || (seed, crate::fuzz::boolean_case(seed))));
workers.into_iter().map(|w| w.join().unwrap()).collect()
});
for (seed, c) in cases {
assert_eq!(c.outcome, "ok", "seed {seed}: {} {:?} {:?}", c.detail, c.a, c.b);
}
}
+2
View File
@@ -170,6 +170,8 @@ impl Harness {
/// File pickers answer like a person would: save into the scenario's folder, open the file
/// the scenario chose.
fn install(&mut self) {
// Frames are laid out, never painted.
self.app.viewport.no_pixels = true;
let dir = self.dir.clone();
self.app.services.pick_save = Some(Box::new(move |name, _| Some(dir.join(name).to_string_lossy().into_owned())));
let next = self.next_file.clone();
+5
View File
@@ -35,6 +35,8 @@ pub struct ViewportState {
hl_slot: SceneSlot,
hl_key: u64,
cpu: Option<(u64, egui::TextureHandle)>,
/// Nothing will look at the frame (the headless scenario harness): skip the software render.
pub no_pixels: bool,
pub nav: Option<NavMode>,
/// What a click would pick now (after the active command's filter).
pub hover: Option<Hit>,
@@ -1927,6 +1929,9 @@ fn overlays(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
}
fn cpu_render(app: &mut SolveApp, ctx: &egui::Context, painter: &egui::Painter, rect: Rect, proj: &Proj) {
if app.viewport.no_pixels {
return;
}
let mut h = std::collections::hash_map::DefaultHasher::new();
scene_key(app).hash(&mut h);
for v in [proj.cam.yaw, proj.cam.pitch, proj.cam.distance, proj.cam.target.x, proj.cam.target.y, proj.cam.target.z] {