Tests: the command sweeps and import fuzz tests fail when a command panics

The engine's guard turns a panic inside a command into
EngineError::Internal and rolls the document back. The panic checks in
command_sweep.rs counted that error like any other error. They include
probe, which the empty-params, junk-params and structured-junk sweeps
call. The tests in import_fuzz.rs that open, place or paste a file
through a command counted it the same way. None of them reported the
panic.

Each of these checks now passes the command's result through no_panic,
which the library file tests in import_fuzz.rs already use, and the
change adds a copy of it to command_sweep.rs. A case whose command
panics now fails. The panic from no_panic reads "<command> panicked:
<message>", and the checks that print a panic's message show it. Each
check handles any other error as before.
This commit is contained in:
dotCooCoo authored and bflatastic committed 2026-10-12 00:35:12 +07:00
1 parent 99959e4b21
commit d875efd6e1
2 files changed
+42 -31

No files matched your search

+26 -15
View File
@@ -17,10 +17,19 @@ fn safe_path() -> String {
vectorcraft_testkit::temp_dir("sweep").join("out.bin").to_string_lossy().to_string()
}
/// `r`, after failing the case if the command panicked. The engine's guard reports a panic as
/// `EngineError::Internal`.
fn no_panic(r: Result<Value, vectorcraft_engine::EngineError>) -> Result<Value, vectorcraft_engine::EngineError> {
if let Err(vectorcraft_engine::EngineError::Internal { cmd, msg }) = &r {
panic!("{cmd} panicked: {msg}");
}
r
}
/// Run one call; returns a failure description or None.
fn probe(fx: Fixture, id: &str, params: &Value) -> Option<String> {
let mut s = fx.session();
let r = catch_quiet(|| s.execute(id, params));
let r = catch_quiet(|| no_panic(s.execute(id, params)));
match r {
Err(msg) => Some(format!("PANIC {id} {params} [{fx:?}]: {msg}")),
Ok(_) => {
@@ -68,7 +77,7 @@ fn every_command_without_a_document() {
let mut failures = vec![];
for c in command_specs() {
let mut s = Session::new();
match catch_quiet(|| s.execute(c.id, &json!({}))) {
match catch_quiet(|| no_panic(s.execute(c.id, &json!({})))) {
Err(p) => failures.push(format!("PANIC {} with no document: {p}", c.id)),
// Anything that needs a document must say so via enablement, not fail later.
Ok(Err(vectorcraft_engine::EngineError::NoDocument)) => failures.push(format!("{}: enabled without a document but needs one", c.id)),
@@ -106,7 +115,7 @@ fn bug_huge_transform_makes_document_unloadable() {
("object.distributeSpacing", json!({"spacing": 1e308})),
] {
let mut s = Fixture::Multi.session();
if s.execute(id, &p).is_ok() {
if no_panic(s.execute(id, &p)).is_ok() {
vectorcraft_testkit::invariants::check_native_roundtrip(&s.doc().unwrap().doc).unwrap_or_else(|e| panic!("{id} {p}: {e}"));
}
}
@@ -117,7 +126,7 @@ fn bug_artboard_huge_props_break_svg() {
for key in ["x", "y", "width", "height"] {
let mut s = Fixture::Single.session();
let p = json!({"index": 0, key: 1e308});
if s.execute("artboard.setProps", &p).is_ok() {
if no_panic(s.execute("artboard.setProps", &p)).is_ok() {
vectorcraft_testkit::invariants::check_svg_roundtrip(&s.doc().unwrap().doc).unwrap_or_else(|e| panic!("{p}: {}", &e[..e.len().min(200)]));
}
}
@@ -412,14 +421,14 @@ fn area_options_fit_junk() {
for (size, (w, h)) in [(12.0, (120.0, 40.0)), (0.1, (1.0, 1.0)), (1296.0, (100_000.0, 1.0))] {
for p in &cases {
let mut s = Fixture::Multi.session();
let made = s.execute("text.create", &json!({"x": 10, "y": 10, "size": size, "text": story, "area": {"width": w, "height": h}}));
let made = no_panic(s.execute("text.create", &json!({"x": 10, "y": 10, "size": size, "text": story, "area": {"width": w, "height": h}})));
let Ok(made) = made else { continue };
let id = made["id"].as_u64().unwrap_or(0);
let r = catch_quiet(|| s.execute("text.areaOptions", p));
let r = catch_quiet(|| no_panic(s.execute("text.areaOptions", p)));
match r {
Err(m) => failures.push(format!("PANIC text.areaOptions {p} [{size} pt, {w}×{h}]: {m}")),
Ok(_) => {
let typed = catch_quiet(|| s.execute("text.editRange", &json!({"id": id, "start": 0, "insert": "More words. "})));
let typed = catch_quiet(|| no_panic(s.execute("text.editRange", &json!({"id": id, "start": 0, "insert": "More words. "}))));
if typed.is_err() {
failures.push(format!("PANIC typing after text.areaOptions {p}"));
}
@@ -447,11 +456,13 @@ fn polygon_properties_junk() {
for scale in [None, Some((300.0, 20.0)), Some((1e-300, 100.0))] {
for p in &cases {
let mut s = Fixture::Multi.session();
let Ok(_) = s.execute("shape.polygon", &json!({"cx": 100, "cy": 100, "radius": 40, "sides": 7, "rotation": 10})) else { continue };
let Ok(_) = no_panic(s.execute("shape.polygon", &json!({"cx": 100, "cy": 100, "radius": 40, "sides": 7, "rotation": 10}))) else {
continue;
};
if let Some((sx, sy)) = scale {
let _ = s.execute("object.scale", &json!({"sx": sx, "sy": sy}));
let _ = no_panic(s.execute("object.scale", &json!({"sx": sx, "sy": sy})));
}
match catch_quiet(|| s.execute("object.setLiveShape", p)) {
match catch_quiet(|| no_panic(s.execute("object.setLiveShape", p))) {
Err(m) => failures.push(format!("PANIC object.setLiveShape {p} [{scale:?}]: {m}")),
Ok(_) => {
if let Err(e) = catch_quiet(|| check_all(&mut s)).unwrap_or_else(|m| Err(format!("panic in checks: {m}"))) {
@@ -504,7 +515,7 @@ fn every_command_undoes_exactly() {
let depth = s.doc().unwrap().history.undo.len();
let Ok(Ok(_)) = catch_quiet(|| s.execute(c.id, &json!({}))) else { continue };
if s.doc().unwrap().history.undo.len() > depth {
let _ = s.execute("edit.undo", &json!({}));
let _ = no_panic(s.execute("edit.undo", &json!({})));
let after = doc_json(&s.doc().unwrap().doc);
if after != before {
failures.push(format!("{}: {}", c.id, vectorcraft_testkit::invariants::first_diff(&before, &after, "$")));
@@ -543,7 +554,7 @@ fn recovery_commands_with_a_damaged_store_and_junk_params() {
for p in std::iter::once(json!({})).chain(junk_params(spec.params, &path)) {
let mut s = fx.session();
s.recovery.set_store(store.clone());
let r = catch_quiet(|| s.execute(id, &p));
let r = catch_quiet(|| no_panic(s.execute(id, &p)));
assert!(r.is_ok(), "PANIC {id} {p} [{fx:?}]");
check_session(&s).unwrap_or_else(|e| panic!("{id} {p} [{fx:?}]: {e}"));
}
@@ -576,9 +587,9 @@ fn envelope_commands_with_junk_params_on_envelopes() {
continue;
}
if editing {
let _ = s.execute("object.envelope.editContents", &json!({"editing": true}));
let _ = no_panic(s.execute("object.envelope.editContents", &json!({"editing": true})));
}
let r = catch_quiet(|| s.execute(id, &p));
let r = catch_quiet(|| no_panic(s.execute(id, &p)));
if r.is_err() {
failures.push(format!("PANIC {id} {p} [{make}, editing {editing}]"));
} else if s.in_interaction() {
@@ -646,7 +657,7 @@ fn text_commands_with_junk_params_on_inline_graphics() {
let mut failures = vec![];
let mut run = |id: &str, p: &Value| {
let (mut s, _) = setup();
match catch_quiet(|| s.execute(id, p)) {
match catch_quiet(|| no_panic(s.execute(id, p))) {
Err(msg) => failures.push(format!("PANIC {id} {p}: {msg}")),
Ok(r) => {
if s.in_interaction() {
+16 -16
View File
@@ -260,8 +260,8 @@ proptest! {
let mut s = vectorcraft_engine::Session::new();
s.execute("file.new", &json!({"width":100,"height":100})).unwrap();
let p = json!({"name":"renamed.png", "dataBase64":vectorcraft_format::base64_encode(&bytes)});
for cmd in ["file.place", "file.place.info"] { assert!(s.execute(cmd, &p).is_err()); }
assert!(s.execute("file.place.queue", &json!({"files":[p]})).is_err());
for cmd in ["file.place", "file.place.info"] { assert!(no_panic(s.execute(cmd, &p)).is_err()); }
assert!(no_panic(s.execute("file.place.queue", &json!({"files":[p]}))).is_err());
});
prop_assert!(r.is_ok(), "Affinity placement panicked: {:?}", r.err());
}
@@ -669,8 +669,8 @@ proptest! {
let r = catch_quiet(|| {
let mut s = vectorcraft_engine::Session::new();
let p = json!({"name": "x.pdf", "dataBase64": b64, "pages": pages, "cropTo": crop, "password": password, "thumbnail": 1, "thumbnailSize": 64});
let _ = s.execute("document.pdfInfo", &p);
if s.execute("document.open", &p).is_ok() {
let _ = no_panic(s.execute("document.pdfInfo", &p));
if no_panic(s.execute("document.open", &p)).is_ok() {
let d = (*s.doc().unwrap().doc).clone();
survive("pdf options", || Some(d)).unwrap();
}
@@ -1037,8 +1037,8 @@ proptest! {
let mut s = vectorcraft_engine::Session::new();
s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
let p = json!({"name": name, "dataBase64": vectorcraft_format::base64_encode(&bytes), "thumbnail": 8});
let _ = s.execute("file.place.info", &p);
let _ = s.execute("file.place", &p);
let _ = no_panic(s.execute("file.place.info", &p));
let _ = no_panic(s.execute("file.place", &p));
});
prop_assert!(r.is_ok(), "{name}: panicked: {:?}", r.err());
}
@@ -1133,10 +1133,10 @@ fn paste_from_elsewhere(cmd: &str, p: Value) -> Result<(), TestCaseError> {
let r = catch_quiet(|| {
let mut s = vectorcraft_engine::Session::new();
s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
if s.execute(cmd, &p).is_ok() {
if no_panic(s.execute(cmd, &p)).is_ok() {
// Paste may refuse what it was given, e.g. a damaged PDF whose art spans more than the
// canvas ("result would exceed the canvas"): an error is a fine outcome, a panic isn't.
let _ = s.execute("edit.paste", &json!({"center": [50, 50]}));
let _ = no_panic(s.execute("edit.paste", &json!({"center": [50, 50]})));
}
});
prop_assert!(r.is_ok(), "{cmd}: panicked: {:?}", r.err());
@@ -1294,7 +1294,7 @@ proptest! {
survive("hostile PostScript", || vectorcraft_engine::cmd::fileio::load("x.eps", ps.as_bytes()).ok().map(|l| l.doc))?;
let r = catch_quiet(|| {
let mut s = rich_session();
let _ = s.execute("file.place", &json!({"name": "x.eps", "dataBase64": vectorcraft_format::base64_encode(ps.as_bytes())}));
let _ = no_panic(s.execute("file.place", &json!({"name": "x.eps", "dataBase64": vectorcraft_format::base64_encode(ps.as_bytes())})));
});
r.map_err(|msg| TestCaseError::fail(format!("placing hostile PostScript panicked: {msg}")))?;
}
@@ -1390,8 +1390,8 @@ proptest! {
let r = catch_quiet(|| {
let mut s = vectorcraft_engine::Session::new();
let p = json!({"name": "x.pdf", "dataBase64": b64, "password": password});
let _ = s.execute("document.pdfInfo", &p);
if s.execute("document.open", &p).is_ok() {
let _ = no_panic(s.execute("document.pdfInfo", &p));
if no_panic(s.execute("document.open", &p)).is_ok() {
let d = (*s.doc().unwrap().doc).clone();
survive("encrypted pdf", || Some(d)).unwrap();
}
@@ -1460,11 +1460,11 @@ fn survive_dxf(what: &str, bytes: &[u8], fit: bool) -> Result<(), TestCaseError>
let _ = vectorcraft_cad::is_dxf(bytes);
survive(what, || {
let mut s = vectorcraft_engine::Session::new();
s.execute("file.new", &json!({"width": 300, "height": 200})).ok()?;
no_panic(s.execute("file.new", &json!({"width": 300, "height": 200}))).ok()?;
let p = json!({"name": "x.dxf", "dataBase64": vectorcraft_format::base64_encode(bytes), "dxf": {"fit": fit}, "thumbnail": 8});
let _ = s.execute("file.place.info", &p);
let _ = s.execute("file.place", &p);
let _ = s.execute("document.dxfInfo", &p);
let _ = no_panic(s.execute("file.place.info", &p));
let _ = no_panic(s.execute("file.place", &p));
let _ = no_panic(s.execute("document.dxfInfo", &p));
Some((*s.doc().ok()?.doc).clone())
})
}
@@ -1569,7 +1569,7 @@ fn survive_metafile(what: &str, name: &str, bytes: &[u8]) -> Result<(), TestCase
let r = catch_quiet(|| {
let mut s = vectorcraft_engine::Session::new();
s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
let _ = s.execute("file.place", &json!({"name": name, "dataBase64": vectorcraft_format::base64_encode(bytes)}));
let _ = no_panic(s.execute("file.place", &json!({"name": name, "dataBase64": vectorcraft_format::base64_encode(bytes)})));
});
prop_assert!(r.is_ok(), "{what}: placing panicked: {:?}", r.err());
Ok(())