mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-15 14:43:28 +00:00
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:
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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user