Tests: raster effects draw the same twice while another test assigns a document profile

tests::newdoc::a_profile_brings_the_color_profile_its_file_carries and
tests::saveoptions::an_embedded_profile_is_restored give their front
document a non-sRGB RGB profile. That profile becomes the process-wide
active color settings and stays active until a document without one
comes to the front. Every color the engine test process renders goes
through those settings, so the test
tests::rasterfilters::each_effect_changes_the_canvas_and_draws_the_same_twice
could draw the same art in two colors and fail with "<effect> is
deterministic".

The two tests now run again in a child process of the test binary
through a shared helper, tests::in_own_process. The SVG Drop Shadow test
for a non-sRGB working space uses the helper in place of its own copy,
and the GLOBAL doc comment in tests/colormgmt.rs names this route.

The rasterfilters test now checks that the color settings are the
defaults before it draws and that nothing replaced them while it drew.
A test that changes them in the same process makes it fail with "another
test changed the color settings while this one drew".

With the three tests run together (--test-threads=3) on Linux, the
rasterfilters test failed 10 of 10 runs without the child processes,
each time with that message, and passed 10 of 10 with them.
This commit is contained in:
dotCooCoo
2026-11-14 03:13:56 -08:00
parent edb228ac33
commit 04b90d88d0
6 changed files with 40 additions and 17 deletions
+2 -14
View File
@@ -352,20 +352,8 @@ fn svg_darkness_preserves_source_model_and_gradient_appearance() {
#[test]
fn svg_darkness_uses_rendered_shadow_in_non_srgb_working_space() {
const CHILD: &str = "VECTORCRAFT_TEST_SHADOW_RGB_PROFILE";
if std::env::var_os(CHILD).is_none() {
// Color settings are process-wide: exercise this profile in a separate test process.
let output = std::process::Command::new(std::env::current_exe().unwrap())
.args(["--exact", "cmd::fileio::tests_svg::svg_darkness_uses_rendered_shadow_in_non_srgb_working_space", "--nocapture"])
.env(CHILD, "1")
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
output.status.success() && stdout.contains("test result: ok. 1 passed; 0 failed;"),
"isolated color profile test failed:\n{stdout}\n{}",
String::from_utf8_lossy(&output.stderr),
);
// Color settings are process-wide: exercise this profile in a separate test process.
if !crate::tests::in_own_process("cmd::fileio::tests_svg::svg_darkness_uses_rendered_shadow_in_non_srgb_working_space") {
return;
}
use vectorcraft_color::{Color, Paint, cms};
+19
View File
@@ -241,6 +241,25 @@ pub(crate) fn key_guide(s: &Session) -> Option<usize> {
st.selection.key_guide.and_then(|k| crate::cmd::guide_indexes(&st.doc, &[k]).first().copied())
}
/// Runs the unit test `path` (`tests::newdoc::...`) again in a child process of the test binary
/// → whether this is that child. For a test that changes process-wide state the tests running
/// beside it read, such as the active color settings.
pub(crate) fn in_own_process(path: &str) -> bool {
const CHILD: &str = "VECTORCRAFT_TEST_OWN_PROCESS";
if std::env::var_os(CHILD).is_some() {
return true;
}
let output =
std::process::Command::new(std::env::current_exe().unwrap()).args(["--exact", path, "--nocapture"]).env(CHILD, "1").output().unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
output.status.success() && stdout.contains("test result: ok. 1 passed; 0 failed;"),
"{path} failed in its own process:\n{stdout}\n{}",
String::from_utf8_lossy(&output.stderr),
);
false
}
#[test]
fn create_and_undo_redo() {
let mut s = session();
+4 -1
View File
@@ -16,7 +16,10 @@ use crate::*;
/// Every test here reads or changes the process-wide proof view or colour settings: one at a time.
/// Tests that switch the Color Settings away from the defaults live in `tests/color_settings.rs`
/// (their own process): every colour shown or rendered goes through them, so no lock could cover
/// all the tests that would see the change.
/// all the tests that would see the change. A unit test that assigns its front document a profile
/// other than the working space changes the active settings the same way until another document
/// comes to the front: it starts with `crate::tests::in_own_process`, which runs it again in a
/// child process.
pub(crate) static GLOBAL: Mutex<()> = Mutex::new(());
fn session() -> Session {
+3
View File
@@ -683,6 +683,9 @@ fn the_new_document_links_from_the_profile_file() {
/// is a warning.
#[test]
fn a_profile_brings_the_color_profile_its_file_carries() {
if !super::in_own_process("tests::newdoc::a_profile_brings_the_color_profile_its_file_carries") {
return;
}
use vectorcraft_color::cms;
let (mut s, dir) = with_profiles("icc");
let builtin = cms::profiles().into_iter().find(|p| p.kind == cms::ProfileKind::Rgb && p.builtin && p.name != cms::SRGB).unwrap();
+9 -2
View File
@@ -5,6 +5,7 @@
//! as images of the effected object.
use serde_json::{Value, json};
use vectorcraft_color::cms;
use vectorcraft_doc::NodeKind;
use vectorcraft_geom::Rect;
use vectorcraft_render::{Rendered, Renderer};
@@ -173,16 +174,22 @@ fn each_effect_changes_the_canvas_and_draws_the_same_twice() {
EFFECTS.into_iter().chain(ARTISTIC).chain(BRUSH_STROKES).chain(DISTORT).chain(PIXELATE).chain(SKETCH).chain(TEXTURE).chain(VIDEO)
{
let (mut s, g) = striped();
// Every color drawn goes through the process-wide color settings. A test that changes them
// runs in a process of its own (`crate::tests::in_own_process`).
let colors = cms::active();
assert_eq!(cms::active_settings(), cms::ColorSettings::default(), "{id}: another test left the color settings changed");
let plain = render(&s);
let params: Value = serde_json::from_str(params).unwrap();
s.execute("effect.apply", &json!({"effect": id, "ids": [g], "params": params})).unwrap();
let a = render(&s);
assert!(mean_diff(&plain, &a) > 0.0005, "{id} shows: {}", mean_diff(&plain, &a));
assert_eq!(a.pixels, render(&s).pixels, "{id} is deterministic");
let again = render(&s);
// The multithreaded pipeline draws the same.
let mut mt = Renderer::new();
mt.threads = 4;
let b = mt.render_region(&s.doc().unwrap().doc, Rect::new(0.0, 0.0, 200.0, 200.0), 1.0, true);
assert!(std::sync::Arc::ptr_eq(&colors, &cms::active()), "{id}: another test changed the color settings while this one drew");
assert!(mean_diff(&plain, &a) > 0.0005, "{id} shows: {}", mean_diff(&plain, &a));
assert_eq!(a.pixels, again.pixels, "{id} is deterministic");
assert!(mean_diff(&a, &b) < 0.002, "{id}: multithreaded differs by {}", mean_diff(&a, &b));
}
}
+3
View File
@@ -112,6 +112,9 @@ fn a_linked_pngs_bytes_are_embedded() {
#[test]
fn an_embedded_profile_is_restored() {
if !super::in_own_process("tests::saveoptions::an_embedded_profile_is_restored") {
return;
}
let builtin = cms::profiles().into_iter().find(|p| p.kind == cms::ProfileKind::Rgb && p.builtin && p.name != cms::SRGB).unwrap();
let icc = cms::icc_bytes(&builtin.name).unwrap();
let name = format!("Studio RGB {}", std::process::id());