Fix #6335: the Color Settings persistence tests run in their own process

They switch the process-wide Color Settings away from the defaults, which
every colour the engine's unit tests show or render goes through, so they
move to tests/color_settings.rs as the note on colormgmt's GLOBAL asks. The
round-trip test now also unregisters the loaded profile before the next
session, so it shows the profile store registering it again before the saved
settings name it.
This commit is contained in:
Sven Lafebre
2026-11-10 22:00:19 +01:00
parent a339d9b997
commit 12bc0a49b5
2 changed files with 105 additions and 81 deletions
-81
View File
@@ -202,84 +202,3 @@ fn stored_profiles_register_again_and_removing_one_removes_its_copy() {
assert!(s.execute("color.removeProfile", &json!({"name": cms::SRGB})).is_err(), "built-in profiles stay");
s.execute("color.removeProfile", &json!({"name": other})).unwrap();
}
/// Puts the default Color Settings back, unregisters `profile` and deletes `paths` when dropped,
/// also after a failed assertion, so the next test holding `GLOBAL` starts from a clean state.
struct ColorSettingsReset {
profile: Option<&'static str>,
paths: Vec<std::path::PathBuf>,
}
impl Drop for ColorSettingsReset {
fn drop(&mut self) {
let _ = cms::set_active(&cms::ColorSettings::default());
if let Some(name) = self.profile {
cms::unregister_icc(name);
}
for path in &self.paths {
let _ = std::fs::remove_dir_all(path).or_else(|_| std::fs::remove_file(path));
}
}
}
/// #6335: Edit › Color Settings come back in the next session: the working spaces (a loaded
/// profile among them, registered again by the profile store first), the intent and black point
/// compensation. A session nobody attached a file to keeps nothing, and nothing is written before
/// the settings change.
#[test]
fn color_settings_come_back_in_the_next_session() {
let _g = super::colormgmt::GLOBAL.lock().unwrap_or_else(|e| e.into_inner());
let root = fresh_dir("color-settings");
let (profiles, file) = (root.join("Color Profiles"), root.join("Color Settings.json"));
let loaded = "StoreRGB-6";
let icc = vectorcraft_testkit::temp_dir("store-color-settings").join("loaded.icc");
let _reset = ColorSettingsReset { profile: Some(loaded), paths: vec![root.clone(), icc.clone()] };
std::fs::write(&icc, icc_named(loaded)).unwrap();
let mut a = Session::new();
a.attach_profile_store(profiles.clone());
a.attach_color_settings(file.clone());
a.execute("color.loadProfile", &json!({"path": icc.to_str().unwrap()})).unwrap();
a.execute("edit.colorSettings", &json!({})).unwrap();
assert!(!file.exists(), "nothing to keep before the settings change");
a.execute("edit.colorSettings", &json!({"rgb": loaded, "cmyk": cms::DEVICE_CMYK, "intent": "perceptual", "bpc": false})).unwrap();
let chosen = cms::ColorSettings { rgb: loaded.into(), cmyk: cms::DEVICE_CMYK.into(), intent: cms::Intent::Perceptual, bpc: false };
assert_eq!(cms::active_settings(), chosen);
// The next start begins with the defaults.
cms::set_active(&cms::ColorSettings::default()).unwrap();
let mut b = Session::new();
b.attach_profile_store(profiles);
b.attach_color_settings(file.clone());
assert_eq!(cms::active_settings(), chosen, "the saved settings are the active ones again");
// Without a file, a change is kept only for the process.
let mut c = Session::new();
c.execute("edit.colorSettings", &json!({"bpc": true})).unwrap();
let saved: cms::ColorSettings = serde_json::from_slice(&std::fs::read(&file).unwrap()).unwrap();
assert_eq!(saved, chosen, "the file is left alone");
}
/// #6335: saved Color Settings that can't be used (a profile that is gone, a damaged or oversized
/// file) leave the defaults and the file as it is; the next change writes over it.
#[test]
fn saved_color_settings_that_cannot_be_used_leave_the_defaults() {
let _g = super::colormgmt::GLOBAL.lock().unwrap_or_else(|e| e.into_inner());
let dir = fresh_dir("color-settings-bad");
let _reset = ColorSettingsReset { profile: None, paths: vec![dir.clone()] };
let file = dir.join("Color Settings.json");
std::fs::create_dir_all(&dir).unwrap();
let gone = json!({"rgb": cms::SRGB, "cmyk": "A Profile That Is Gone", "intent": "perceptual", "bpc": false}).to_string().into_bytes();
let oversized = vec![b' '; MAX_PRESET_FILE as usize + 1];
for bytes in [gone, b"not json".to_vec(), oversized] {
cms::set_active(&cms::ColorSettings::default()).unwrap();
std::fs::write(&file, &bytes).unwrap();
let mut s = Session::new();
s.attach_color_settings(file.clone());
assert_eq!(cms::active_settings(), cms::ColorSettings::default());
assert_eq!(std::fs::read(&file).unwrap(), bytes, "left as it is");
s.execute("edit.colorSettings", &json!({"bpc": false})).unwrap();
let saved: cms::ColorSettings = serde_json::from_slice(&std::fs::read(&file).unwrap()).unwrap();
assert!(!saved.bpc && saved.cmyk == cms::GENERIC_CMYK, "written over by the next change: {saved:?}");
}
}
+105
View File
@@ -9,6 +9,7 @@ use serde_json::json;
use vectorcraft_color::cms;
use vectorcraft_engine::Session;
use vectorcraft_engine::cmd::colormgmt::{PROFILES_KEY, doc_profiles};
use vectorcraft_engine::store::MAX_PRESET_FILE;
use vectorcraft_geom::Affine;
use vectorcraft_render::{RenderOptions, Renderer};
use vectorcraft_testkit::fixtures::{exec, id_of, rect, select, session};
@@ -426,3 +427,107 @@ fn loading_the_working_rgb_profile_again_takes_effect_at_once() {
assert!(after.iter().zip(want).all(|(a, b)| (a - b).abs() < 1e-4), "after the reload {after:?}, want {want:?}");
assert!(redrawn, "the canvas redraws");
}
/// A folder of its own for the test `tag`, empty.
fn fresh_dir(tag: &str) -> std::path::PathBuf {
let dir = vectorcraft_testkit::temp_dir(&format!("color-settings-{tag}"));
let _ = std::fs::remove_dir_all(&dir);
dir
}
/// An ICC file of an RGB profile named `name` (a built-in one with its description replaced by a
/// name of the same length).
#[allow(clippy::unwrap_used, reason = "a test helper: a missing built-in profile fails the test")]
fn icc_named(name: &str) -> Vec<u8> {
let builtin = cms::DISPLAY_P3;
assert_eq!(builtin.len(), name.len());
let utf16 = |s: &str| s.encode_utf16().flat_map(u16::to_be_bytes).collect::<Vec<u8>>();
let mut bytes = cms::icc_bytes(builtin).unwrap().to_vec();
let (from, to) = (utf16(builtin), utf16(name));
let at = bytes.windows(from.len()).position(|w| w == from).unwrap();
bytes[at..at + to.len()].copy_from_slice(&to);
bytes
}
/// Puts the default Color Settings back, unregisters `profile` and deletes `paths` when dropped,
/// also after a failed assertion, so the next test starts from the defaults.
struct ColorSettingsReset {
profile: Option<&'static str>,
paths: Vec<std::path::PathBuf>,
}
impl Drop for ColorSettingsReset {
fn drop(&mut self) {
let _ = cms::set_active(&cms::ColorSettings::default());
if let Some(name) = self.profile {
cms::unregister_icc(name);
}
for path in &self.paths {
let _ = std::fs::remove_dir_all(path).or_else(|_| std::fs::remove_file(path));
}
}
}
/// #6335: Edit › Color Settings come back in the next session: the working spaces (a loaded
/// profile among them, registered again by the profile store first), the intent and black point
/// compensation. A session nobody attached a file to keeps nothing, and nothing is written before
/// the settings change.
#[test]
fn color_settings_come_back_in_the_next_session() {
let _g = settings();
let root = fresh_dir("color-settings");
let (profiles, file) = (root.join("Color Profiles"), root.join("Color Settings.json"));
let loaded = "StoreRGB-6";
let icc = vectorcraft_testkit::temp_dir("store-color-settings").join("loaded.icc");
let _reset = ColorSettingsReset { profile: Some(loaded), paths: vec![root.clone(), icc.clone()] };
std::fs::write(&icc, icc_named(loaded)).unwrap();
let mut a = Session::new();
a.attach_profile_store(profiles.clone());
a.attach_color_settings(file.clone());
a.execute("color.loadProfile", &json!({"path": icc.to_str().unwrap()})).unwrap();
a.execute("edit.colorSettings", &json!({})).unwrap();
assert!(!file.exists(), "nothing to keep before the settings change");
a.execute("edit.colorSettings", &json!({"rgb": loaded, "cmyk": cms::DEVICE_CMYK, "intent": "perceptual", "bpc": false})).unwrap();
let chosen = cms::ColorSettings { rgb: loaded.into(), cmyk: cms::DEVICE_CMYK.into(), intent: cms::Intent::Perceptual, bpc: false };
assert_eq!(cms::active_settings(), chosen);
// The next start begins with the defaults and without the loaded profile, which the profile
// store registers again before the settings name it.
cms::set_active(&cms::ColorSettings::default()).unwrap();
assert!(cms::unregister_icc(loaded));
let mut b = Session::new();
b.attach_profile_store(profiles);
b.attach_color_settings(file.clone());
assert_eq!(cms::active_settings(), chosen, "the saved settings are the active ones again");
// Without a file, a change is kept only for the process.
let mut c = Session::new();
c.execute("edit.colorSettings", &json!({"bpc": true})).unwrap();
let saved: cms::ColorSettings = serde_json::from_slice(&std::fs::read(&file).unwrap()).unwrap();
assert_eq!(saved, chosen, "the file is left alone");
}
/// #6335: saved Color Settings that can't be used (a profile that is gone, a damaged or oversized
/// file) leave the defaults and the file as it is; the next change writes over it.
#[test]
fn saved_color_settings_that_cannot_be_used_leave_the_defaults() {
let _g = settings();
let dir = fresh_dir("color-settings-bad");
let _reset = ColorSettingsReset { profile: None, paths: vec![dir.clone()] };
let file = dir.join("Color Settings.json");
std::fs::create_dir_all(&dir).unwrap();
let gone = json!({"rgb": cms::SRGB, "cmyk": "A Profile That Is Gone", "intent": "perceptual", "bpc": false}).to_string().into_bytes();
let oversized = vec![b' '; MAX_PRESET_FILE as usize + 1];
for bytes in [gone, b"not json".to_vec(), oversized] {
cms::set_active(&cms::ColorSettings::default()).unwrap();
std::fs::write(&file, &bytes).unwrap();
let mut s = Session::new();
s.attach_color_settings(file.clone());
assert_eq!(cms::active_settings(), cms::ColorSettings::default());
assert_eq!(std::fs::read(&file).unwrap(), bytes, "left as it is");
s.execute("edit.colorSettings", &json!({"bpc": false})).unwrap();
let saved: cms::ColorSettings = serde_json::from_slice(&std::fs::read(&file).unwrap()).unwrap();
assert!(!saved.bpc && saved.cmyk == cms::GENERIC_CMYK, "written over by the next change: {saved:?}");
}
}