diff --git a/Cargo.lock b/Cargo.lock index 91f34c5102..c1ca95f85b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4833,6 +4833,7 @@ dependencies = [ "photocraft-psd", "photocraft-raster", "photocraft-raw", + "photocraft-testkit", "photocraft-text", "photocraft-vector", "rayon", diff --git a/crates/algo/src/tests_tilt_shift.rs b/crates/algo/src/tests_tilt_shift.rs index 88066ca9d6..b82dd1c2c4 100644 --- a/crates/algo/src/tests_tilt_shift.rs +++ b/crates/algo/src/tests_tilt_shift.rs @@ -52,11 +52,19 @@ fn tilt_shift_matches_frozen_kernel_at_band_boundaries_and_edges() { } cases.push(params(80.0, (0.5, 0.5), 0.0, 5.0, 0.1)); cases.push(params(80.0, (-5.0, -5.0), 27.0, 0.1, 0.15)); + // The channel count and alpha are independent of the band geometry: a run takes a quarter of + // their combinations, a different quarter each day. + let sweep = photocraft_testkit::Sweep::new(4); + let mut case = 0; for ch in 1..=6 { let src = image(bounds.inflate(12), ch); for alpha in [false, true] { let ctx = Ctx { bounds, mode: ColorMode::Rgb, alpha }; for p in &cases { + case += 1; + if !sweep.includes(case) { + continue; + } check(&src, bounds, &ctx, p); check(&src, Rect::new(-7, 17, 15, 39), &ctx, p); } @@ -146,6 +154,10 @@ fn tilt_shift_depths_models_selection_and_tile_edges_match_reference() { selection.fill_rect(bounds, &[0.4]); selection.fill_rect(Rect::new(3, 19, 35, 44), &[1.0]); selection.fill_rect(Rect::new(10, 20, 20, 30), &[0.0]); + // Pixel formats and tile layouts are independent: a run takes a quarter of their combinations, + // a different quarter each day. + let sweep = photocraft_testkit::Sweep::new(4); + let mut case = 0; for mode in [ ColorMode::Rgb, ColorMode::Grayscale, @@ -167,6 +179,10 @@ fn tilt_shift_depths_models_selection_and_tile_edges_match_reference() { assert_eq!(p.halo_for(bounds), crate::Halo::Radius(radius)); for extent in [None, Some(bounds)] { for tile in [16, 47, 128] { + case += 1; + if !sweep.includes(case) { + continue; + } let got = crate::apply_tiled(&surface, &p, area, bounds, Some(&selection), tile, extent); let mut want = surface.clone(); for y in (area.y0..area.y1).step_by(tile as usize) { diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index ec5b4ac075..2ccb98f81b 100644 --- a/crates/engine/Cargo.toml +++ b/crates/engine/Cargo.toml @@ -60,6 +60,7 @@ photocraft-ipp = { workspace = true } photocraft-affinity = { workspace = true, features = ["synth"] } # Synthetic camera raw files for the open tests. photocraft-raw = { workspace = true, features = ["testgen"] } +photocraft-testkit = { workspace = true } # WebAssembly text -> binary for plug-in test modules (Apache-2.0 WITH LLVM-exception). wat = "1.228" # Synthetic multi-part EXR fixtures for the parts-commands tests. diff --git a/crates/engine/src/pixels_tests.rs b/crates/engine/src/pixels_tests.rs index 5af30d0e76..3b1c25c21e 100644 --- a/crates/engine/src/pixels_tests.rs +++ b/crates/engine/src/pixels_tests.rs @@ -183,19 +183,29 @@ fn tiled_clear_and_fill_match_one_pass_over_the_area() { let sr = Rect::new(0, 0, 600, 400); soft.write_region(sr, &(0..sr.height()).flat_map(|_| (0..sr.width()).map(|x| (1.0 - (x as f32 - 256.0).max(0.0) / 300.0).max(0.0))).collect::>()); let canvas = Rect::new(0, 0, 700, 600); + // Pixel formats and fill settings are independent: a run takes a quarter of their combinations, + // a different quarter each day. + let sweep = photocraft_testkit::Sweep::new(4); + let mut case = 0; + let mut runs = || { + case += 1; + sweep.includes(case) + }; for fmt in formats { let s = clear_layer(fmt); for sel in [None, Some(&soft)] { let area = sel.map_or(canvas, |m| m.content_bounds()); let what = format!("{fmt:?} selection {}", sel.is_some()); - let mut got = s.clone(); - try_clear_surface(&mut got, area, sel).unwrap(); - assert!(same_pixels(&got, &reference_fill_or_clear(&s, area, sel, None)), "clear {what}"); - if fmt.alpha { - assert!(!same_pixels(&got, &s), "clear {what} changes the layer"); + if runs() { + let mut got = s.clone(); + try_clear_surface(&mut got, area, sel).unwrap(); + assert!(same_pixels(&got, &reference_fill_or_clear(&s, area, sel, None)), "clear {what}"); + if fmt.alpha { + assert!(!same_pixels(&got, &s), "clear {what} changes the layer"); + } } for color in [[1.0, 1.0, 1.0, 1.0], [0.3, 0.5, 0.9, 1.0], [0.3, 0.5, 0.9, 0.4]] { - for lock in [true, false] { + for lock in [true, false].into_iter().filter(|_| runs()) { let mut got = s.clone(); try_fill_surface(&mut got, area, color, sel, lock).unwrap(); assert!(same_pixels(&got, &reference_fill_or_clear(&s, area, sel, Some((color, lock)))), "fill {color:?} lock {lock} {what}"); diff --git a/crates/engine/tests/it/precheck.rs b/crates/engine/tests/it/precheck.rs index f549fbb8c8..b737ddea5a 100644 --- a/crates/engine/tests/it/precheck.rs +++ b/crates/engine/tests/it/precheck.rs @@ -7,28 +7,32 @@ use serde_json::{Value, json}; /// Documents in different states: a Background only, a pixel layer active, a group active, a /// selection, no document. -fn states() -> Vec<(&'static str, Session)> { - let mut out = Vec::new(); - out.push(("no document", Session::new())); +const STATES: [&str; 5] = ["no document", "background only", "pixel layer", "group, no background", "selection"]; + +/// A fresh session in `state`. +fn state(state: &str) -> Session { let mut s = Session::new(); - s.execute("file.new", json!({"width": 40, "height": 30, "background": "white"})).unwrap(); - out.push(("background only", s)); - let mut s = Session::new(); - s.execute("file.new", json!({"width": 40, "height": 30, "background": "white"})).unwrap(); - s.execute("layer.new.layer", json!({})).unwrap(); - s.execute("edit.fill", json!({"color": "#ff0000"})).unwrap(); - out.push(("pixel layer", s)); - let mut s = Session::new(); - s.execute("file.new", json!({"width": 40, "height": 30, "background": "transparent"})).unwrap(); - s.execute("layer.new.group", json!({})).unwrap(); - out.push(("group, no background", s)); - let mut s = Session::new(); - s.execute("file.new", json!({"width": 40, "height": 30, "background": "white"})).unwrap(); - s.execute("layer.new.layer", json!({})).unwrap(); - s.execute("edit.fill", json!({"color": "#00ff00"})).unwrap(); - s.execute("select.rect", json!({"x": 2, "y": 2, "width": 10, "height": 10})).unwrap(); - out.push(("selection", s)); - out + if state == "no document" { + return s; + } + let background = if state == "group, no background" { "transparent" } else { "white" }; + s.execute("file.new", json!({"width": 40, "height": 30, "background": background})).unwrap(); + match state { + "pixel layer" => { + s.execute("layer.new.layer", json!({})).unwrap(); + s.execute("edit.fill", json!({"color": "#ff0000"})).unwrap(); + } + "group, no background" => { + s.execute("layer.new.group", json!({})).unwrap(); + } + "selection" => { + s.execute("layer.new.layer", json!({})).unwrap(); + s.execute("edit.fill", json!({"color": "#00ff00"})).unwrap(); + s.execute("select.rect", json!({"x": 2, "y": 2, "width": 10, "height": 10})).unwrap(); + } + _ => {} + } + s } fn params() -> Vec { @@ -63,13 +67,13 @@ fn precheck_agrees_with_execute() { let mut disagreements = Vec::new(); for id in PRECHECKED { for p in params() { - for (state, _) in states() { - // A fresh copy of the state for each call: execute changes it. - let mut s = states().into_iter().find(|(n, _)| *n == state).map(|(_, s)| s).unwrap(); + for name in STATES { + // A fresh session for each call: execute changes it. + let mut s = state(name); let pre = s.precheck(id, &p).is_ok(); let ran = s.execute(id, p.clone()).is_ok(); if pre != ran { - disagreements.push(format!("{id} {p} on {state}: precheck {pre}, execute {ran}")); + disagreements.push(format!("{id} {p} on {name}: precheck {pre}, execute {ran}")); } } }