Files
vectorcraft/crates/engine/src/tests_store.rs
T
vangel76andClaude Opus 5.5 a8e45f0e2d Persistence: saved PDF presets and loaded ICC profiles survive a restart (#2168, #2174)
PDF presets are kept one file each in a PDF Presets folder, ICC profiles as copies in a Color Profiles folder, next to the preferences; the desktop app and vectorcraft-cli use them. Adds color.removeProfile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:49:48 +07:00

129 lines
6.1 KiB
Rust

//! The stores of PDF presets and ICC profiles (#2168, #2174): what one session saves, the next one
//! finds; damaged or oversized files are skipped, never fatal.
use serde_json::{Value, json};
use vectorcraft_color::cms;
use crate::Session;
use crate::store::{MAX_PRESET_FILE, file_stem};
fn fresh_dir(tag: &str) -> std::path::PathBuf {
let dir = vectorcraft_testkit::temp_dir(&format!("store-{tag}"));
let _ = std::fs::remove_dir_all(&dir);
dir
}
fn saved_names(s: &mut Session) -> Vec<String> {
let v = s.execute("pdf.preset.list", &json!({})).unwrap();
v["presets"].as_array().unwrap().iter().filter(|p| p["builtIn"] == false).map(|p| p["name"].as_str().unwrap().to_string()).collect()
}
#[test]
fn saved_presets_come_back_in_the_next_session_and_renames_and_deletes_follow() {
let dir = fresh_dir("presets").join("PDF Presets");
let mut a = Session::new();
a.attach_pdf_preset_store(dir.clone());
a.execute("pdf.preset.save", &json!({"name": "Press", "description": "for the printer", "preset": "Press Quality"})).unwrap();
a.execute("pdf.preset.save", &json!({"name": "Web"})).unwrap();
a.execute("pdf.preset.save", &json!({"name": "Web", "newName": "Screen"})).unwrap();
a.execute("pdf.preset.save", &json!({"name": "Old"})).unwrap();
a.execute("pdf.preset.delete", &json!({"name": "Old"})).unwrap();
let mut b = Session::new();
b.attach_pdf_preset_store(dir.clone());
assert_eq!(saved_names(&mut b), ["Press", "Screen"]);
let press = b.prefs.pdf_presets.iter().find(|p| p.name == "Press").unwrap();
assert_eq!(press.description, "for the printer");
assert_eq!(press.settings, b.pdf_presets().into_iter().find(|p| p.name == "Press Quality").unwrap().settings);
// The file of a name that is gone is gone.
assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 2);
assert!(!dir.join(format!("{}.vcpdfpresets", file_stem("Old"))).exists());
}
#[test]
fn a_folder_that_does_not_exist_yet_takes_the_presets_the_preferences_held() {
let dir = fresh_dir("seed").join("PDF Presets");
let mut a = Session::new();
a.execute("pdf.preset.save", &json!({"name": "From Preferences"})).unwrap();
a.attach_pdf_preset_store(dir.clone());
assert!(dir.is_dir(), "written at once");
let mut b = Session::new();
b.attach_pdf_preset_store(dir);
assert_eq!(saved_names(&mut b), ["From Preferences"]);
}
#[test]
fn damaged_unsafe_and_oversized_preset_files_are_skipped() {
let dir = fresh_dir("damaged").join("PDF Presets");
let mut a = Session::new();
a.attach_pdf_preset_store(dir.clone());
a.execute("pdf.preset.save", &json!({"name": "Good"})).unwrap();
let write = |name: &str, bytes: &[u8]| std::fs::write(dir.join(name), bytes).unwrap();
write("a-not-json.vcpdfpresets", b"{ not json");
write("b-other-format.vcpdfpresets", br#"{"format": "something else", "presets": []}"#);
let huge = vec![b' '; MAX_PRESET_FILE as usize + 1];
write("c-huge.vcpdfpresets", &huge);
write("d-readme.txt", b"ignored");
let good = std::fs::read_to_string(dir.join(format!("{}.vcpdfpresets", file_stem("Good")))).unwrap();
// A built-in name, an empty name, a second "good" in another case, and values no export accepts.
let mut file: Value = serde_json::from_str(&good).unwrap();
let mut preset = file["presets"][0].clone();
for name in ["High Quality Print", " ", "GOOD"] {
preset["name"] = json!(name);
file["presets"].as_array_mut().unwrap().push(preset.clone());
}
preset["name"] = json!("Bad Values");
preset["settings"]["advanced"]["fontSubsetPercent"] = json!(500);
file["presets"].as_array_mut().unwrap().push(preset);
write("e-mixed.vcpdfpresets", file.to_string().as_bytes());
let mut b = Session::new();
b.attach_pdf_preset_store(dir);
assert_eq!(saved_names(&mut b), ["Good"]);
}
/// An ICC file of an RGB profile named `name` (a built-in one with its description replaced by a
/// name of the same length).
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
}
#[test]
fn stored_profiles_register_again_and_removing_one_removes_its_copy() {
let dir = fresh_dir("profiles").join("Color Profiles");
std::fs::create_dir_all(&dir).unwrap();
let name = "StoreRGB-1";
let file = format!("{}.icc", file_stem(name));
std::fs::write(dir.join("junk.icc"), b"not a profile").unwrap();
std::fs::write(dir.join("empty.icc"), b"").unwrap();
std::fs::write(dir.join(&file), icc_named(name)).unwrap();
assert!(cms::profile(name).is_none());
let mut s = Session::new();
s.attach_profile_store(dir.clone());
assert!(cms::profile(name).is_some_and(|p| !p.builtin), "registered from the store");
// A profile loaded from elsewhere is copied in, whatever the file is called.
let other = "StoreRGB-2";
let loose = vectorcraft_testkit::temp_dir("store-loose").join("whatever.icm");
std::fs::write(&loose, icc_named(other)).unwrap();
let r = s.execute("color.loadProfile", &json!({"path": loose.to_str().unwrap()})).unwrap();
assert_eq!((r["name"].as_str(), r["remembered"].as_bool()), (Some(other), Some(true)));
std::fs::remove_file(&loose).unwrap();
assert_eq!(std::fs::read(dir.join(format!("{}.icc", file_stem(other)))).unwrap(), icc_named(other));
let r = s.execute("color.removeProfile", &json!({"name": name})).unwrap();
assert_eq!(r["removed"], name);
assert!(cms::profile(name).is_none() && !dir.join(&file).exists());
assert!(s.execute("color.removeProfile", &json!({"name": name})).is_err(), "already gone");
assert!(s.execute("color.removeProfile", &json!({"name": cms::SRGB})).is_err(), "built-in profiles stay");
s.execute("color.removeProfile", &json!({"name": other})).unwrap();
}