mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-24 09:57:08 -06:00
- gpu: the test modules share one GPU_LOCK, and timing and vector_mask take it too, so one process drives one wgpu device at a time. - ui-egui: the wgpu tests in pattern_preview_canvas, adjust_preview_gpu, marquee_autoscroll and layout_perf take gpu_lock(). - affinity compound_limits: the child process selects its test by the module path (compound_limits::<name>); with --exact, the bare name matches no test and the child passes without running the check. - xtask scorecard reads crates/io/tests/it/corpus.rs. - Docs and comments name the new test paths and the --test it filters.
215 lines
8.9 KiB
Rust
215 lines
8.9 KiB
Rust
//! #6423: compound operands form a graph, not necessarily a tree.
|
|
#![cfg(feature = "synth")]
|
|
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
|
|
|
use photocraft_affinity::model::{Document, Kind};
|
|
use photocraft_affinity::synth::{self, F, Method, tag};
|
|
use photocraft_affinity::{Limits, read};
|
|
use std::time::{Duration, Instant};
|
|
|
|
fn compound(id: u32, children: Vec<F>) -> F {
|
|
F::Def(id, vec![tag(b"Comp")], vec![(tag(b"Chld"), F::Shared(children))])
|
|
}
|
|
|
|
fn rectangle() -> F {
|
|
F::Def(100, vec![tag(b"ShpN")], vec![(tag(b"ShpB"), F::F64s(vec![0.0, 0.0, 10.0, 20.0])), (tag(b"Shpe"), F::Def(101, vec![tag(b"ShNR")], vec![]))])
|
|
}
|
|
|
|
fn document_bytes(definitions: Vec<F>, roots: Vec<F>) -> Vec<u8> {
|
|
let data = synth::stream(&[
|
|
(tag(b"Defs"), F::Shared(definitions)),
|
|
(
|
|
tag(b"DocR"),
|
|
F::Def(
|
|
1,
|
|
vec![tag(b"DocN")],
|
|
vec![(
|
|
tag(b"Chld"),
|
|
F::Shared(vec![F::Def(
|
|
2,
|
|
vec![tag(b"Sprd")],
|
|
vec![(tag(b"SprB"), F::F64s(vec![0.0, 0.0, 400.0, 300.0])), (tag(b"Chld"), F::Shared(roots))],
|
|
)]),
|
|
)],
|
|
),
|
|
),
|
|
]);
|
|
synth::container(&[("doc.dat", &data, Method::Zlib)], None)
|
|
}
|
|
|
|
fn document(definitions: Vec<F>, roots: Vec<F>) -> Document {
|
|
read(&document_bytes(definitions, roots), Limits::default()).unwrap()
|
|
}
|
|
|
|
// A stack overflow aborts rather than unwinds. Keep that failure (and an exponential hang)
|
|
// inside a child process, so the regression fails without killing the entire test runner.
|
|
fn isolated(name: &str, check: impl FnOnce()) {
|
|
if std::env::var("PHOTOCRAFT_COMPOUND_CASE").as_deref() == Ok(name) {
|
|
check();
|
|
return;
|
|
}
|
|
// The harness names a test by its module path below the crate root.
|
|
let test = module_path!().split_once("::").map_or(name.to_string(), |(_, m)| format!("{m}::{name}"));
|
|
let mut child = std::process::Command::new(std::env::current_exe().unwrap())
|
|
.args(["--exact", &test, "--nocapture"])
|
|
.env("PHOTOCRAFT_COMPOUND_CASE", name)
|
|
.spawn()
|
|
.unwrap();
|
|
let start = Instant::now();
|
|
loop {
|
|
if let Some(status) = child.try_wait().unwrap() {
|
|
assert!(status.success(), "compound import failed: {status}");
|
|
break;
|
|
}
|
|
if start.elapsed() > Duration::from_secs(10) {
|
|
child.kill().unwrap();
|
|
child.wait().unwrap();
|
|
panic!("compound import exceeded ten seconds");
|
|
}
|
|
std::thread::sleep(Duration::from_millis(20));
|
|
}
|
|
}
|
|
|
|
fn unsupported(doc: &Document, warning: &str) {
|
|
assert!(matches!(doc.spreads[0].nodes[0].kind, Kind::Unsupported));
|
|
assert!(doc.warnings.iter().any(|w| w.contains(warning)), "{:?}", doc.warnings);
|
|
}
|
|
|
|
#[test]
|
|
fn self_referencing_compound_is_reported_without_aborting() {
|
|
isolated("self_referencing_compound_is_reported_without_aborting", || {
|
|
let doc = document(vec![], vec![compound(10, vec![F::Ref(10)]), rectangle()]);
|
|
unsupported(&doc, "cyclic compound");
|
|
assert!(matches!(doc.spreads[0].nodes[1].kind, Kind::Shape { .. }), "a cycle must not poison unrelated shapes");
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn indirectly_cyclic_compound_is_not_imported_as_a_partial_shape() {
|
|
isolated("indirectly_cyclic_compound_is_not_imported_as_a_partial_shape", || {
|
|
let doc = document(vec![rectangle()], vec![compound(10, vec![F::Ref(100), compound(11, vec![F::Ref(10)])])]);
|
|
unsupported(&doc, "cyclic compound");
|
|
});
|
|
}
|
|
|
|
fn nested(levels: u32, repeated: bool) -> Document {
|
|
// Flat definitions bypass the stream parser's nesting cap: only references form the depth.
|
|
let mut definitions = vec![rectangle()];
|
|
let mut previous = 100;
|
|
for id in 200..200 + levels {
|
|
let children = if repeated { vec![F::Ref(previous), F::Ref(previous)] } else { vec![F::Ref(previous)] };
|
|
definitions.push(compound(id, children));
|
|
previous = id;
|
|
}
|
|
document(definitions, vec![F::Ref(previous)])
|
|
}
|
|
|
|
#[test]
|
|
fn deep_compound_graph_is_bounded_on_a_small_stack() {
|
|
isolated("deep_compound_graph_is_bounded_on_a_small_stack", || {
|
|
std::thread::Builder::new().stack_size(512 * 1024).spawn(|| unsupported(&nested(1000, false), "compound depth")).unwrap().join().unwrap();
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn shared_operands_cannot_expand_exponentially() {
|
|
isolated("shared_operands_cannot_expand_exponentially", || unsupported(&nested(40, true), "compound geometry budget"));
|
|
}
|
|
|
|
#[test]
|
|
fn shared_empty_operands_also_have_a_work_limit() {
|
|
isolated("shared_empty_operands_also_have_a_work_limit", || {
|
|
let mut definitions = vec![compound(100, vec![])];
|
|
let mut previous = 100;
|
|
for id in 200..240 {
|
|
definitions.push(compound(id, vec![F::Ref(previous), F::Ref(previous)]));
|
|
previous = id;
|
|
}
|
|
unsupported(&document(definitions, vec![F::Ref(previous)]), "compound geometry budget");
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn geometry_budget_is_shared_across_layer_roots() {
|
|
isolated("geometry_budget_is_shared_across_layer_roots", || {
|
|
let mut definitions = vec![rectangle()];
|
|
let mut previous = 100;
|
|
for id in 200..214 {
|
|
definitions.push(compound(id, vec![F::Ref(previous), F::Ref(previous)]));
|
|
previous = id;
|
|
}
|
|
let mut roots = vec![F::Ref(previous); 12];
|
|
roots.push(F::Ref(100));
|
|
let doc = document(definitions, roots);
|
|
let nodes = &doc.spreads[0].nodes;
|
|
assert!(matches!(nodes[0].kind, Kind::Shape { .. }), "a modest shared graph still imports");
|
|
assert!(matches!(nodes.last().unwrap().kind, Kind::Unsupported), "later roots cannot reset the exhausted budget");
|
|
assert!(doc.warnings.iter().any(|w| w.contains("compound geometry budget")));
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn ordinary_shared_operands_keep_both_transformed_copies() {
|
|
let mut translated = compound(201, vec![F::Ref(100)]);
|
|
let F::Def(_, _, fields) = &mut translated else { unreachable!() };
|
|
fields.push((tag(b"Xfrm"), F::F64s(vec![1.0, 0.0, 30.0, 0.0, 1.0, 40.0])));
|
|
let doc = document(vec![rectangle(), compound(200, vec![F::Ref(100)]), translated], vec![compound(202, vec![F::Ref(200), F::Ref(201)])]);
|
|
assert!(doc.warnings.is_empty(), "{:?}", doc.warnings);
|
|
let Kind::Shape { path, .. } = &doc.spreads[0].nodes[0].kind else { panic!("expected compound outline") };
|
|
assert_eq!(path.subpaths.len(), 2);
|
|
for (a, b) in path.subpaths[0].segments.iter().flatten().zip(path.subpaths[1].segments.iter().flatten()) {
|
|
assert_eq!(b.x, a.x + 30.0);
|
|
assert_eq!(b.y, a.y + 40.0);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn null_operand_slots_cannot_bypass_geometry_budget() {
|
|
isolated("null_operand_slots_cannot_bypass_geometry_budget", || {
|
|
let bytes = document_bytes(vec![compound(10, vec![F::Null; 1_000_000])], vec![F::Ref(10); 20_000]);
|
|
let start = Instant::now();
|
|
let doc = read(&bytes, Limits::default()).unwrap();
|
|
eprintln!("one million nulls / 20,000 placements: {} bytes, read {:.3}s", bytes.len(), start.elapsed().as_secs_f64());
|
|
unsupported(&doc, "compound geometry budget");
|
|
assert_eq!(doc.spreads[0].nodes.len(), 20_000);
|
|
assert!(doc.spreads[0].nodes.iter().all(|n| matches!(n.kind, Kind::Unsupported)));
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn null_operand_slots_share_budget_across_layer_roots() {
|
|
isolated("null_operand_slots_share_budget_across_layer_roots", || {
|
|
let doc = document(vec![compound(10, vec![F::Null; 1_000])], vec![F::Ref(10); 1_000]);
|
|
assert!(matches!(doc.spreads[0].nodes[0].kind, Kind::Shape { .. }), "a small empty compound remains valid");
|
|
assert!(matches!(doc.spreads[0].nodes.last().unwrap().kind, Kind::Unsupported));
|
|
assert!(doc.warnings.iter().any(|w| w.contains("compound geometry budget")));
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn null_operand_slots_cannot_leave_a_partial_shape() {
|
|
isolated("null_operand_slots_cannot_leave_a_partial_shape", || {
|
|
let mut slots = vec![F::Ref(100)];
|
|
slots.extend(vec![F::Null; 1_000_000]);
|
|
let doc = document(vec![rectangle(), compound(10, slots)], vec![F::Ref(10)]);
|
|
unsupported(&doc, "compound geometry budget");
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn non_object_operand_slots_also_consume_budget() {
|
|
isolated("non_object_operand_slots_also_consume_budget", || {
|
|
let comp = F::Def(10, vec![tag(b"Comp")], vec![(tag(b"Chld"), F::U8s(vec![0; 1_000_000]))]);
|
|
unsupported(&document(vec![comp], vec![F::Ref(10)]), "compound geometry budget");
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn ordinary_null_operand_slots_preserve_shared_contours() {
|
|
let doc = document(vec![rectangle(), compound(10, vec![F::Null, F::Ref(100), F::Null, F::Ref(100), F::Null])], vec![F::Ref(10)]);
|
|
assert!(doc.warnings.is_empty(), "{:?}", doc.warnings);
|
|
let Kind::Shape { path, .. } = &doc.spreads[0].nodes[0].kind else { panic!("expected compound outline") };
|
|
assert_eq!(path.subpaths.len(), 2);
|
|
assert_eq!(path.subpaths[0], path.subpaths[1]);
|
|
}
|