Triangle tool: tests for hostile and degenerate boxes and for the triangle_live field (#13350)

The hostile-radii test now tells the command guard's internal error from a graceful one, and a new test sends hostile boxes. The vector tests pin zero-size, sliver and tall thin boxes. The format tests cover both fields present, a field holding another kind, and junk in triangle_live.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.2 briefed; Sonnet 5.5 wrote at medium
This commit is contained in:
Trevor BrownandClaude Sonnet 5.5 committed 2026-11-12 20:19:03 -05:00
1 parent 303be52228
commit cbd389a504
3 files changed
+100 -1

No files matched your search

@@ -102,17 +102,41 @@ fn editing_the_radius_or_the_box_keeps_the_triangle() {
assert_eq!(info(&mut s, id)["live"], before);
}
/// `Session::execute` turns a command's panic into an `Err`, so a hostile-input test has to tell
/// that error from a graceful one.
fn graceful(r: &crate::Result<Value>) -> bool {
!matches!(r, Err(e) if e.to_string().contains("internal error"))
}
#[test]
fn hostile_radii_never_panic() {
let mut s = session();
let id = create(&mut s, json!({"kind": "triangle", "rect": [400, 300, 200, 100]}));
for r in [json!(-5), json!(1e300), json!("x")] {
let _ = s.execute("shape.edit", json!({"layer": id, "cornerRadius": r}));
let result = s.execute("shape.edit", json!({"layer": id, "cornerRadius": r}));
assert!(graceful(&result), "{result:?}");
let r = info(&mut s, id)["live"]["corner_radius"].as_f64().unwrap();
assert!(r.is_finite() && r >= 0.0, "{r}");
}
}
#[test]
fn hostile_boxes_never_panic() {
let mut s = session();
let layers = s.active().unwrap().doc.layer_count();
// Out of range, or not a box: refused, and no layer is added.
for rect in [json!([0, 0, 1e300, 1e300]), json!([1e300, 1e300, 10, 10]), json!([0, 0, 10]), json!([0, 0, "w", 10]), json!(null)] {
let result = s.execute("shape.create", json!({"kind": "triangle", "rect": rect, "cornerRadius": 20}));
assert!(result.is_err() && graceful(&result), "{result:?}");
}
assert_eq!(s.active().unwrap().doc.layer_count(), layers);
// A box with no width or no height has no corner to round.
for rect in [[5, 5, 0, 0], [5, 5, 0, 50], [5, 5, 50, 0]] {
let result = s.execute("shape.create", json!({"kind": "triangle", "rect": rect, "cornerRadius": 20}));
assert!(graceful(&result), "{rect:?}: {result:?}");
}
}
#[test]
fn a_scale_keeps_the_triangle_live_and_a_flip_makes_it_a_path() {
let mut s = session();
+55
View File
@@ -73,3 +73,58 @@ fn the_triangle_kind_uses_these_json_names() {
let bare = live(json!({"kind": "triangle", "rect": [0.0, 0.0, 10.0, 10.0]}));
assert_eq!(serde_json::to_value(bare).unwrap()["corner_radius"], json!(0.0));
}
/// `doc` saved, with `members` set on its first layer's content: what no save writes.
fn saved_with(doc: &Document, members: &Value) -> Vec<u8> {
let bytes = save_to_bytes(doc, &SaveOptions::default()).unwrap();
let bundle = zip::ZipReader::new(&bytes).unwrap();
let mut out = zip::ZipWriter::new();
for entry in &bundle.entries {
let mut data = bundle.read(entry, usize::MAX).unwrap();
if entry.name == "manifest.json" {
let mut manifest: Value = serde_json::from_slice(&data).unwrap();
for (key, value) in members.as_object().unwrap() {
manifest["document"]["layers"][0]["content"][key] = value.clone();
}
data = serde_json::to_vec(&manifest).unwrap();
}
out.add(&entry.name, &data).unwrap();
}
out.finish().unwrap()
}
fn loaded_live(bytes: &[u8]) -> Option<LiveShape> {
match &load_from_bytes(bytes).unwrap().layers[0].content {
LayerContent::Shape(shape) => shape.live.clone(),
_ => panic!("a shape layer"),
}
}
/// No save writes these, but a hand-made or damaged file can hold them: the arrow field wins over
/// the triangle field, which wins over `live`; a field holding another kind is ignored; and a
/// value that is no live shape fails the load with an error.
#[test]
fn both_fields_a_wrong_kind_and_junk_never_panic() {
let doc = document(live(triangle_json()));
let arrow = json!({"kind": "arrowLine", "from": [0.0, 0.0], "to": [9.0, 0.0], "weight": 1.0, "start": false, "end": true, "width": 5.0, "length": 5.0, "concavity": 0.0});
let rect = json!({"kind": "rect", "rect": [1.0, 2.0, 3.0, 4.0], "radii": [0.0, 0.0, 0.0, 0.0]});
// Both new fields: the arrow wins. The triangle field wins over `live`.
assert_eq!(loaded_live(&saved_with(&doc, &json!({"arrow_live": arrow}))), Some(live(arrow.clone())));
assert_eq!(loaded_live(&saved_with(&doc, &json!({"live": rect}))), Some(live(triangle_json())));
// A field holding another kind is ignored, and `live` is read instead.
assert_eq!(loaded_live(&saved_with(&doc, &json!({"triangle_live": rect}))), None);
assert_eq!(loaded_live(&saved_with(&doc, &json!({"triangle_live": arrow, "live": rect}))), Some(live(rect.clone())));
assert_eq!(loaded_live(&saved_with(&doc, &json!({"arrow_live": triangle_json(), "triangle_live": null}))), None);
// A value that is no live shape fails the load with an error.
for junk in [
json!(7),
json!("x"),
json!({}),
json!({"kind": "hexagon"}),
json!({"kind": "triangle"}),
json!({"kind": "triangle", "rect": [1, 2, 3]}),
json!({"kind": "triangle", "rect": [0, 0, "w", 10]}),
] {
assert!(load_from_bytes(&saved_with(&doc, &json!({"triangle_live": junk}))).is_err(), "{junk}");
}
}
@@ -67,3 +67,23 @@ fn a_three_sided_polygon_keeps_its_inscribed_geometry() {
let p = live_path(&LiveShape::Polygon { rect: [400.0, 300.0, 200.0, 100.0], sides: 3, star_ratio: 1.0, corner_radius: 0.0 });
close(&anchors(&p), &[(500.0, 300.0), (586.603, 375.0), (413.397, 375.0)], 1e-3);
}
#[test]
fn a_box_with_no_width_or_height_has_no_corner_to_round() {
for rect in [[5.0, 5.0, 0.0, 0.0], [5.0, 5.0, 0.0, 50.0], [5.0, 5.0, 50.0, 0.0]] {
let [x, y, w, h] = rect;
let p = live_path(&LiveShape::Triangle { rect, corner_radius: 20.0 });
assert_eq!(p, triangle(x, y, w, h), "{rect:?}");
assert_eq!(p.subpaths[0].knots.len(), 3);
}
// A sliver: the apex is too sharp to round, and the path stays finite.
let p = live_path(&LiveShape::Triangle { rect: [0.0, 0.0, 1.0e-7, 1000.0], corner_radius: 20.0 });
assert!(p.subpaths[0].knots.iter().all(|k| [k.anchor, k.in_ctrl, k.out_ctrl].iter().all(|q| q.x.is_finite() && q.y.is_finite())));
}
#[test]
fn a_tall_thin_box_caps_each_corner_on_its_own_edges() {
// The apex's tangent points stop at half of each side, the base corners' at half of the base.
let p = live_path(&LiveShape::Triangle { rect: [0.0, 0.0, 2.0, 1000.0], corner_radius: 20.0 });
close(&anchors(&p), &[(0.5, 500.0), (1.5, 500.0), (1.999, 999.0), (1.0, 1000.0), (1.0, 1000.0), (0.001, 999.0)], 1e-3);
}