diff --git a/crates/engine/src/cmd/vector_cmds/triangle_box_geometry_tests.rs b/crates/engine/src/cmd/vector_cmds/triangle_box_geometry_tests.rs index 4d5cbf6e49..7dcc5de64f 100644 --- a/crates/engine/src/cmd/vector_cmds/triangle_box_geometry_tests.rs +++ b/crates/engine/src/cmd/vector_cmds/triangle_box_geometry_tests.rs @@ -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) -> 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(); diff --git a/crates/format/tests/it/triangle_live.rs b/crates/format/tests/it/triangle_live.rs index c96c156e76..8fb22020c8 100644 --- a/crates/format/tests/it/triangle_live.rs +++ b/crates/format/tests/it/triangle_live.rs @@ -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 { + 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 { + 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}"); + } +} diff --git a/crates/vector/src/triangle_box_geometry_tests.rs b/crates/vector/src/triangle_box_geometry_tests.rs index 65df1998be..32760fe092 100644 --- a/crates/vector/src/triangle_box_geometry_tests.rs +++ b/crates/vector/src/triangle_box_geometry_tests.rs @@ -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); +}