Files
lightcraft/crates/engine/src/tests_print.rs
T

531 lines
26 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The `print.*` commands: state, cells, templates and the PDF.
use lightcraft_catalog::MemStore;
use serde_json::{Value, json};
use crate::Session;
use crate::library::LibraryStores;
fn run(s: &mut Session, id: &str, p: Value) -> Value {
s.execute(id, &p).unwrap_or_else(|e| panic!("{id} {p}: {e}"))
}
fn fails(s: &mut Session, id: &str, p: Value) -> String {
s.execute(id, &p).expect_err(&format!("{id} {p} should fail")).to_string()
}
/// A scratch folder that goes away with the test, however it ends.
struct Scratch(std::path::PathBuf);
impl Scratch {
fn new(tag: &str) -> Scratch {
let dir = std::env::temp_dir().join(format!("lightcraft-print-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
Scratch(dir)
}
}
impl Drop for Scratch {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn package(s: &mut Session) {
run(s, "print.set", json!({"layout": {"style": "customPackage", "page": {"paper": "letter"}}}));
}
#[test]
fn get_set_and_reset() {
let mut s = Session::with_demo();
let g = run(&mut s, "print.get", json!({}));
assert_eq!(g["settings"]["layout"]["style"], "singleImage");
assert_eq!(g["pageWidth"], 8.5);
assert_eq!(g["printable"]["x"], 0.25);
assert_eq!(g["settings"]["job"]["ppi"], 240.0);
let papers = g["papers"].as_array().unwrap();
assert!(papers.iter().any(|p| p["name"] == "a4"));
// The Japanese photo and postcard papers are listed with their metric sizes in inches.
let paper = |name: &str| papers.iter().find(|p| p["name"] == name).cloned().unwrap_or_else(|| panic!("{name}"));
let l = paper("l");
assert!((l["width"].as_f64().unwrap() - 89.0 / 25.4).abs() < 1e-9 && (l["height"].as_f64().unwrap() - 127.0 / 25.4).abs() < 1e-9);
assert!(papers.iter().any(|p| p["name"] == "postcard") && papers.iter().any(|p| p["name"] == "6cut"));
assert_eq!(g["gridCells"].as_array().unwrap().len(), 1);
let g = run(
&mut s,
"print.set",
json!({"layout": {"style": "contactSheet", "grid": {"columns": 2, "rows": 3}, "page": {"paper": "a4", "orientation": "landscape", "margins": {"left": 1.0}}}, "image": {"zoomToFill": true}, "job": {"ppi": 300.0}}),
);
assert_eq!(g["settings"]["layout"]["page"]["margins"]["left"], 1.0);
assert_eq!(g["settings"]["layout"]["page"]["margins"]["top"], 0.25, "a patch merges");
assert_eq!(g["gridCells"].as_array().unwrap().len(), 6);
assert!((g["pageWidth"].as_f64().unwrap() - 11.693).abs() < 1e-3);
assert_eq!(g["settings"]["image"]["zoomToFill"], true);
// the page count follows the photos: 6 cells per page
let g = run(&mut s, "print.get", json!({"ids": vec![1; 7]}));
assert_eq!(g["pageCount"], 2);
let g = run(&mut s, "print.reset", json!({}));
assert_eq!(g["settings"]["job"]["ppi"], 240.0);
}
#[test]
fn bad_settings_are_refused_and_change_nothing() {
let mut s = Session::with_demo();
let before = run(&mut s, "print.get", json!({}));
for p in [
json!({"typo": 1}),
json!({"layout": {"typo": 1}}),
json!({"layout": {"style": "poster"}}),
json!({"layout": {"cells": []}}),
json!({"layout": {"pages": 3}}),
json!({"layout": {"page": {"paper": "poster"}}}),
json!({"layout": {"page": {"paper": "custom", "customWidth": -1}}}),
json!({"layout": {"page": {"margins": {"top": 1e9}}}}),
json!({"layout": {"page": {"margins": {"top": 6.0, "bottom": 5.0}}}}),
json!({"layout": {"grid": {"columns": 0}}}),
json!({"layout": {"grid": {"columns": 9999}}}),
json!({"layout": {"spacing": -1}}),
json!({"image": {"borderWidth": 99}}),
json!({"image": {"strokeWidthPt": -2}}),
json!({"image": {"borderColor": [1, 2]}}),
json!({"image": {"zoomToFill": "yes"}}),
json!({"guides": {"gridSize": 0}}),
json!({"guides": {"units": "furlong"}}),
json!({"job": {"ppi": 1e30}}),
json!({"job": {"ppi": 10}}),
json!({"job": null}),
] {
fails(&mut s, "print.set", p);
}
assert_eq!(run(&mut s, "print.get", json!({})), before);
assert!(s.execute("print.set", &json!([1])).is_err());
}
#[test]
fn cells_are_commands() {
let mut s = Session::with_demo();
// grid styles have no cells
fails(&mut s, "print.cell.add", json!({"width": 2, "height": 2}));
package(&mut s);
let a = run(&mut s, "print.cell.add", json!({"width": 3.0, "height": 2.0}))["id"].as_u64().unwrap();
let b = run(&mut s, "print.cell.add", json!({"width": 3.0, "height": 2.0, "x": 5.0, "y": 5.0}))["id"].as_u64().unwrap();
let g = run(&mut s, "print.get", json!({}));
assert_eq!(g["settings"]["layout"]["cells"].as_array().unwrap().len(), 2);
assert_eq!(g["overlaps"], false);
// move, clamped inside the margins
let r = run(&mut s, "print.cell.move", json!({"id": a, "x": 99.0, "y": 0.0}));
assert_eq!(r["rect"]["x"], 8.25 - 3.0);
assert_eq!(r["rect"]["y"], 0.25);
// resize keeps what is not given
let r = run(&mut s, "print.cell.resize", json!({"id": b, "width": 4.0}));
assert_eq!((r["rect"]["w"].as_f64().unwrap(), r["rect"]["h"].as_f64().unwrap()), (4.0, 2.0));
// snapping to the other cell's edge
let ra = run(&mut s, "print.get", json!({}))["settings"]["layout"]["cells"][0]["rect"].clone();
let left = ra["x"].as_f64().unwrap();
let r = run(&mut s, "print.cell.move", json!({"id": b, "x": left - 4.0 - 0.04, "y": 5.0, "snap": true}));
assert!((r["rect"]["x"].as_f64().unwrap() - (left - 4.0)).abs() < 1e-9, "{r}");
let r = run(&mut s, "print.cell.move", json!({"id": b, "x": left - 4.0 - 0.04, "y": 5.0}));
assert!((r["rect"]["x"].as_f64().unwrap() - (left - 4.04)).abs() < 1e-9, "no snap unless asked: {r}");
run(&mut s, "print.set", json!({"guides": {"snap": "off"}}));
let r = run(&mut s, "print.cell.move", json!({"id": b, "x": left - 4.0 - 0.04, "y": 5.0, "snap": true}));
assert!((r["rect"]["x"].as_f64().unwrap() - (left - 4.04)).abs() < 1e-9, "snap off");
// stacking
let r = run(&mut s, "print.cell.stack", json!({"id": a, "order": "toFront"}));
assert_eq!(r["order"], json!([b, a]));
fails(&mut s, "print.cell.stack", json!({"id": a, "order": "sideways"}));
// photos and pages
let photo = s.catalog.photos().next().unwrap().id.0;
run(&mut s, "print.cell.photo", json!({"id": a, "photo": photo}));
fails(&mut s, "print.cell.photo", json!({"id": a, "photo": 999_999}));
run(&mut s, "print.layout.newPage", json!({}));
run(&mut s, "print.cell.page", json!({"id": b, "page": 1}));
fails(&mut s, "print.cell.page", json!({"id": b, "page": 7}));
let g = run(&mut s, "print.get", json!({}));
assert_eq!(g["pageCount"], 2);
// delete, clear
run(&mut s, "print.cell.delete", json!({"id": b}));
fails(&mut s, "print.cell.delete", json!({"id": b}));
fails(&mut s, "print.cell.move", json!({"id": 99, "x": 0, "y": 0}));
fails(&mut s, "print.cell.move", json!({"x": 0, "y": 0}));
run(&mut s, "print.layout.clear", json!({}));
let g = run(&mut s, "print.get", json!({}));
assert_eq!((g["settings"]["layout"]["cells"].as_array().unwrap().len(), g["pageCount"].as_u64()), (0, Some(1)));
}
#[test]
fn hostile_cell_numbers_are_refused() {
let mut s = Session::with_demo();
package(&mut s);
for (w, h) in [(0.0, 2.0), (-1.0, 2.0), (1e12, 1e12), (2.0, 1e-9), (50.0, 50.0)] {
fails(&mut s, "print.cell.add", json!({"width": w, "height": h}));
}
fails(&mut s, "print.cell.add", json!({"width": "wide", "height": 2}));
fails(&mut s, "print.cell.add", json!({"width": 2}));
let id = run(&mut s, "print.cell.add", json!({"width": 2, "height": 2}))["id"].as_u64().unwrap();
fails(&mut s, "print.cell.move", json!({"id": id, "x": "left", "y": 0}));
fails(&mut s, "print.cell.resize", json!({"id": id, "width": -4}));
fails(&mut s, "print.cell.resize", json!({"id": id, "width": "x"}));
fails(&mut s, "print.cell.move", json!({"id": id, "x": 1, "y": 1, "snap": "yes"}));
fails(&mut s, "print.cell.move", json!({"id": u64::MAX, "x": 1, "y": 1}));
let r = run(&mut s, "print.cell.move", json!({"id": id, "x": -1e300, "y": 1e300}));
assert_eq!(r["rect"]["x"], 0.25);
}
#[test]
fn auto_layout_fills_pages_without_overlap() {
let mut s = Session::with_demo();
run(&mut s, "print.set", json!({"layout": {"style": "picturePackage"}}));
for (w, h) in [(4.0, 6.0), (4.0, 6.0), (5.0, 7.0), (2.5, 3.5), (2.5, 3.5), (2.5, 3.5), (2.5, 3.5), (8.0, 10.0)] {
run(&mut s, "print.cell.add", json!({"width": w, "height": h}));
}
let r = run(&mut s, "print.layout.autoLayout", json!({}));
assert!(r["pages"].as_u64().unwrap() >= 2);
let g = run(&mut s, "print.get", json!({}));
assert_eq!(g["overlaps"], false);
let again = run(&mut s, "print.layout.autoLayout", json!({}));
assert_eq!(again["cells"], r["cells"], "deterministic");
// an oversize cell that arrived by a paper change is pulled in, not left outside the page
run(&mut s, "print.set", json!({"layout": {"page": {"paper": "4x6"}}}));
let g = run(&mut s, "print.get", json!({}));
for c in g["settings"]["layout"]["cells"].as_array().unwrap() {
assert!(c["rect"]["x"].as_f64().unwrap() + c["rect"]["w"].as_f64().unwrap() <= 4.0 - 0.25 + 1e-9);
}
}
/// The first page's MediaBox width and height in points.
fn media_box(pdf: &[u8]) -> (f64, f64) {
let text = String::from_utf8_lossy(pdf);
let at = text.find("/MediaBox [").expect("a MediaBox") + "/MediaBox [".len();
let nums: Vec<f64> = text[at..].split(']').next().unwrap().split_whitespace().map(|v| v.parse().unwrap()).collect();
(nums[2], nums[3])
}
fn temp_pdf(name: &str) -> String {
std::env::temp_dir().join(format!("lightcraft-print-{name}-{}.pdf", std::process::id())).to_string_lossy().to_string()
}
#[test]
fn export_pdf_prints_the_current_settings() {
let mut s = Session::with_demo();
let ids: Vec<u64> = s.catalog.photos().take(3).map(|p| p.id.0).collect();
run(
&mut s,
"print.set",
json!({"layout": {"style": "contactSheet", "grid": {"columns": 1, "rows": 2}, "page": {"paper": "a5"}}, "job": {"ppi": 100.0}}),
);
let path = temp_pdf("export");
let r = run(&mut s, "print.exportPdf", json!({"path": path, "ids": ids}));
assert_eq!((r["pages"].as_u64(), r["photos"].as_u64()), (Some(2), Some(3)));
let bytes = std::fs::read(&path).unwrap();
assert_eq!(bytes.len() as u64, r["bytes"].as_u64().unwrap());
let text = String::from_utf8_lossy(&bytes);
assert!(text.starts_with("%PDF-") && text.contains("/Count 2"));
// A5 is 148 x 210 mm = 419.53 x 595.28 pt, rounded to whole pixels at 100 ppi (583 x 827)
let (w, h) = media_box(&bytes);
assert!((w - 419.53).abs() < 1.0 && (h - 595.28).abs() < 1.0, "A5, got {w} x {h} pt");
// the row in the activity stack is gone when the print is done
assert!(s.activity.list().is_empty());
fails(&mut s, "print.exportPdf", json!({}));
fails(&mut s, "print.exportPdf", json!({"path": path, "typo": 1}));
fails(&mut s, "print.exportPdf", json!({"path": path, "ids": ["x"]}));
fails(&mut s, "print.exportPdf", json!({"path": path, "ids": [999_999]}));
// a package with no cells has nothing to print
run(&mut s, "print.set", json!({"layout": {"style": "picturePackage"}}));
fails(&mut s, "print.exportPdf", json!({"path": path, "ids": ids}));
}
/// The Japanese photo and postcard papers reach the PDF page at their metric sizes: はがき is
/// 100 × 148 mm (283.46 × 419.53 pt) and L is 89 × 127 mm (252.28 × 359.99 pt). 254 ppi is exactly
/// ten pixels per millimetre, so the page pixels are whole millimetres and the MediaBox is exact.
#[test]
fn export_pdf_uses_the_japanese_paper_sizes() {
let mut s = Session::with_demo();
let id = s.catalog.photos().next().map(|p| p.id.0).unwrap();
for (paper, w_pt, h_pt) in [("postcard", 283.46, 419.53), ("はがき", 283.46, 419.53), ("l", 252.28, 359.99)] {
run(&mut s, "print.set", json!({"layout": {"style": "singleImage", "page": {"paper": paper}}, "job": {"ppi": 254.0}}));
let path = temp_pdf(paper);
run(&mut s, "print.exportPdf", json!({"path": path, "ids": [id]}));
let bytes = std::fs::read(&path).unwrap();
let (w, h) = media_box(&bytes);
assert!((w - w_pt).abs() < 0.1 && (h - h_pt).abs() < 0.1, "{paper}: {w} x {h} pt, wanted {w_pt} x {h_pt}");
}
// The customary names are accepted and the page size follows them; an unknown name is refused.
let g = run(&mut s, "print.set", json!({"layout": {"page": {"paper": "はがき"}}}));
assert!((g["pageWidth"].as_f64().unwrap() - 100.0 / 25.4).abs() < 1e-9);
assert!((g["pageHeight"].as_f64().unwrap() - 148.0 / 25.4).abs() < 1e-9);
fails(&mut s, "print.set", json!({"layout": {"page": {"paper": "poster"}}}));
}
#[test]
fn export_pdf_with_a_printer_profile() {
let mut s = Session::with_demo();
let ids: Vec<u64> = s.catalog.photos().take(1).map(|p| p.id.0).collect();
let dir = std::env::temp_dir().join(format!("lightcraft-print-icc-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let icc = dir.join("Fine Art Matte.icc");
std::fs::write(&icc, lightcraft_codecs::icc::write_named(lightcraft_codecs::NamedSpace::AdobeRgb)).unwrap();
let color = json!({"profile": {"kind": "icc", "path": icc}, "intent": "relative", "sharpen": "high", "media": "matte", "brightness": 10});
run(
&mut s,
"print.set",
json!({"layout": {"style": "contactSheet", "grid": {"columns": 1, "rows": 1}, "page": {"paper": "4x6"}}, "job": {"ppi": 72.0, "color": color}}),
);
assert_eq!(run(&mut s, "print.get", json!({}))["settings"]["job"]["color"]["media"], "matte");
let path = temp_pdf("icc");
run(&mut s, "print.exportPdf", json!({"path": path, "ids": ids}));
let text = String::from_utf8_lossy(&std::fs::read(&path).unwrap()).into_owned();
assert!(text.contains("/OutputIntents") && text.contains("/ICCBased") && text.contains("/BitsPerComponent 16"));
// out-of-range adjustments are refused when set; a missing profile fails before rendering
fails(&mut s, "print.set", json!({"job": {"color": {"contrast": 150}}}));
run(&mut s, "print.set", json!({"job": {"color": {"profile": {"kind": "icc", "path": dir.join("gone.icc")}}}}));
let err = fails(&mut s, "print.exportPdf", json!({"path": path, "ids": ids}));
assert!(err.contains("gone.icc"), "{err}");
assert!(s.activity.list().is_empty(), "nothing started");
}
#[test]
fn custom_package_prints_each_cells_photo() {
let mut s = Session::with_demo();
let ids: Vec<u64> = s.catalog.photos().take(2).map(|p| p.id.0).collect();
run(&mut s, "print.set", json!({"layout": {"style": "customPackage", "page": {"paper": "4x6"}}, "job": {"ppi": 72.0}}));
let a = run(&mut s, "print.cell.add", json!({"width": 3.0, "height": 2.0}))["id"].as_u64().unwrap();
let b = run(&mut s, "print.cell.add", json!({"width": 3.0, "height": 2.0}))["id"].as_u64().unwrap();
run(&mut s, "print.cell.photo", json!({"id": a, "photo": ids[0]}));
run(&mut s, "print.cell.photo", json!({"id": b, "photo": ids[1]}));
let r = run(&mut s, "print.exportPdf", json!({"path": temp_pdf("custom"), "ids": [ids[0]]}));
assert_eq!(r["pages"], 1);
}
#[test]
fn templates_save_apply_delete_and_persist() {
let files = MemStore::new();
let open = |files: &MemStore| {
let mut s = Session::with_demo();
let stores = LibraryStores { dir: "print".into(), catalog: Box::new(MemStore::new()), files: Box::new(files.clone()), on_disk: false };
s.open_library_in(stores, false).unwrap();
s
};
let mut s = open(&files);
run(
&mut s,
"print.set",
json!({"layout": {"style": "contactSheet", "grid": {"columns": 4, "rows": 5}}, "page": {"background": [10, 20, 30]}, "guides": {"gridSize": 1.0}}),
);
let r = run(&mut s, "print.template.save", json!({"name": "Proofs"}));
assert_eq!(r["templates"][0]["name"], "Proofs");
assert!(r["templates"][0]["settings"].get("guides").is_none());
run(&mut s, "print.template.save", json!({"name": "proofs"}));
assert_eq!(run(&mut s, "print.template.list", json!({}))["templates"].as_array().unwrap().len(), 1, "the same name in another case replaces");
run(&mut s, "print.reset", json!({}));
run(&mut s, "print.set", json!({"guides": {"gridSize": 0.5}}));
let g = run(&mut s, "print.template.apply", json!({"name": "PROOFS"}));
assert_eq!(g["settings"]["layout"]["grid"]["columns"], 4);
assert_eq!(g["settings"]["page"]["background"], json!([10, 20, 30]));
assert_eq!(g["settings"]["guides"]["gridSize"], 0.5, "guides stay as they were");
// it survives reopening the library
let mut again = open(&files);
assert_eq!(run(&mut again, "print.template.list", json!({}))["templates"][0]["name"], "proofs");
run(&mut again, "print.template.apply", json!({"name": "Proofs"}));
fails(&mut s, "print.template.save", json!({"name": " "}));
fails(&mut s, "print.template.save", json!({}));
fails(&mut s, "print.template.apply", json!({"name": "Nope"}));
fails(&mut s, "print.template.delete", json!({"name": "Nope"}));
run(&mut s, "print.template.delete", json!({"name": "proofs"}));
assert!(run(&mut s, "print.template.list", json!({}))["templates"].as_array().unwrap().is_empty());
assert!(run(&mut open(&files), "print.template.list", json!({}))["templates"].as_array().unwrap().is_empty());
}
#[test]
fn templates_export_to_a_file_and_import_from_one() {
let files = MemStore::new();
let open = |files: &MemStore| {
let mut s = Session::with_demo();
let stores = LibraryStores { dir: "print".into(), catalog: Box::new(MemStore::new()), files: Box::new(files.clone()), on_disk: false };
s.open_library_in(stores, false).unwrap();
s
};
let scratch = Scratch::new("templates");
let dir = &scratch.0;
let mut s = open(&files);
run(&mut s, "print.set", json!({"layout": {"style": "contactSheet", "grid": {"columns": 3, "rows": 4}}, "page": {"background": [5, 6, 7]}}));
run(&mut s, "print.template.save", json!({"name": "Proofs"}));
// the extension is added when the path has none
let r = run(&mut s, "print.template.export", json!({"name": "proofs", "path": dir.join("proofs").to_string_lossy()}));
assert_eq!(r["name"], "Proofs");
let file = dir.join("proofs.lcprint");
assert_eq!(r["path"], file.to_string_lossy().as_ref());
let doc: Value = serde_json::from_slice(&std::fs::read(&file).unwrap()).unwrap();
assert_eq!((doc["format"].as_str(), doc["version"].as_u64(), doc["name"].as_str()), (Some("lightcraft.printTemplate"), Some(1), Some("Proofs")));
// into the same library: the name is taken, so the copy is renamed
let r = run(&mut s, "print.template.import", json!({"path": file.to_string_lossy()}));
assert_eq!((r["name"].as_str(), r["renamed"].as_bool()), (Some("Proofs (2)"), Some(true)), "{r}");
assert_eq!(r["templates"].as_array().unwrap().len(), 2);
assert_eq!(r["templates"][1]["settings"], r["templates"][0]["settings"]);
// into another library, under its own name, and it is kept there
let other = MemStore::new();
let mut b = open(&other);
let r = run(&mut b, "print.template.import", json!({"path": file.to_string_lossy()}));
assert_eq!((r["name"].as_str(), r["renamed"].as_bool()), (Some("Proofs"), Some(false)));
let g = run(&mut open(&other), "print.template.apply", json!({"name": "Proofs"}));
assert_eq!(g["settings"]["layout"]["grid"]["columns"], 3);
assert_eq!(g["settings"]["page"]["background"], json!([5, 6, 7]));
// refusals leave the templates as they were
let bad_file = dir.join("bad.lcprint");
std::fs::write(&bad_file, br#"{"format": "lightcraft.printTemplate", "version": 1, "name": "Bad", "settings": {"layout": {"pages": 0}}}"#)
.unwrap();
let huge = dir.join("huge.lcprint");
std::fs::write(&huge, vec![b' '; crate::print::template::MAX_FILE as usize + 1]).unwrap();
for (p, why) in [
(json!({}), "missing `path`"),
(json!({"path": " "}), "missing `path`"),
(json!({"path": dir.join("absent.lcprint").to_string_lossy()}), "absent.lcprint"),
(json!({"path": dir.to_string_lossy()}), "not a file"),
(json!({"path": huge.to_string_lossy()}), "too large"),
(json!({"path": bad_file.to_string_lossy()}), "template `Bad`"),
] {
let e = fails(&mut s, "print.template.import", p.clone());
assert!(e.contains(why), "{p}: {e}");
}
assert_eq!(run(&mut s, "print.template.list", json!({}))["templates"].as_array().unwrap().len(), 2);
assert!(fails(&mut s, "print.template.export", json!({"name": "Nope", "path": dir.join("x").to_string_lossy()})).contains("no print template"));
assert!(fails(&mut s, "print.template.export", json!({"name": "Proofs"})).contains("missing `path`"));
assert!(fails(&mut s, "print.template.export", json!({"path": dir.join("x").to_string_lossy()})).contains("missing `name`"));
assert!(!dir.join("x.lcprint").exists());
// a full list takes no more
while s.print_templates.len() < crate::print::template::MAX_TEMPLATES {
let n = s.print_templates.len();
run(&mut s, "print.template.save", json!({"name": format!("T{n}")}));
}
assert!(fails(&mut s, "print.template.import", json!({"path": file.to_string_lossy()})).contains("at most"));
}
/// A library whose `prefs.json` can't be read, so saving the preferences fails.
#[derive(Clone, Default)]
struct PrefsUnreadable(MemStore);
impl lightcraft_catalog::Store for PrefsUnreadable {
fn read(&mut self, name: &str) -> std::io::Result<Option<Vec<u8>>> {
if name == "prefs.json" {
return Err(std::io::Error::other("locked by another process"));
}
self.0.read(name)
}
fn write_atomic(&mut self, name: &str, data: &[u8]) -> std::io::Result<()> {
self.0.write_atomic(name, data)
}
fn append(&mut self, name: &str, data: &[u8]) -> std::io::Result<()> {
self.0.append(name, data)
}
fn truncate(&mut self, name: &str, len: u64) -> std::io::Result<()> {
self.0.truncate(name, len)
}
fn describe(&self) -> String {
"prefs unreadable".into()
}
}
#[test]
fn template_changes_that_cant_be_saved_are_undone() {
let mut s = Session::with_demo();
let stores =
LibraryStores { dir: "print".into(), catalog: Box::new(MemStore::new()), files: Box::new(PrefsUnreadable::default()), on_disk: false };
s.open_library_in(stores, false).unwrap();
s.print_templates.push(crate::print::template::PrintTemplate::new("Proofs", &s.print).unwrap());
let names = |s: &Session| s.print_templates.iter().map(|t| (t.name.clone(), t.settings.clone())).collect::<Vec<_>>();
let kept = names(&s);
let scratch = Scratch::new("unsaved");
let file = scratch.0.join("proofs.lcprint");
run(&mut s, "print.template.export", json!({"name": "Proofs", "path": file.to_string_lossy()}));
run(&mut s, "print.set", json!({"layout": {"style": "contactSheet"}}));
for (id, p) in [
("print.template.save", json!({"name": "Other"})),
("print.template.save", json!({"name": "proofs"})),
("print.template.delete", json!({"name": "Proofs"})),
("print.template.import", json!({"path": file.to_string_lossy()})),
] {
assert!(fails(&mut s, id, p.clone()).contains("prefs.json"), "{id} {p}");
assert_eq!(names(&s), kept, "{id} {p}");
}
}
#[test]
fn a_hostile_template_in_the_preferences_does_not_break_the_library() {
let files = MemStore::new();
let hostile = json!({"printTemplates": [
{"name": "Bad", "settings": {"layout": {"pages": 0}, "extra": true}},
{"name": "Huge", "settings": {"job": {"ppi": 1e300}}},
{"name": "Fine", "settings": {"layout": {"style": "contactSheet"}}}
], "cacheMb": 123});
files.set("prefs.json", serde_json::to_vec(&hostile).unwrap());
let mut s = Session::with_demo();
let stores = LibraryStores { dir: "print".into(), catalog: Box::new(MemStore::new()), files: Box::new(files.clone()), on_disk: false };
s.open_library_in(stores, false).unwrap();
assert!(s.take_library_warnings().is_empty(), "the preferences are not damaged");
assert_eq!(s.cache_mb, 123);
assert_eq!(run(&mut s, "print.template.list", json!({}))["templates"].as_array().unwrap().len(), 3);
fails(&mut s, "print.template.apply", json!({"name": "Bad"}));
fails(&mut s, "print.template.apply", json!({"name": "Huge"}));
run(&mut s, "print.template.apply", json!({"name": "Fine"}));
}
#[test]
fn every_print_command_is_registered_with_a_description() {
for id in [
"print.get",
"print.set",
"print.reset",
"print.cell.add",
"print.cell.move",
"print.cell.resize",
"print.cell.delete",
"print.cell.stack",
"print.cell.photo",
"print.cell.page",
"print.layout.autoLayout",
"print.layout.newPage",
"print.layout.clear",
"print.exportPdf",
"print.template.list",
"print.template.save",
"print.template.apply",
"print.template.delete",
"print.template.export",
"print.template.import",
] {
let c = crate::find_command(id).unwrap_or_else(|| panic!("{id} missing"));
assert!(c.params.len() > 3, "{id}");
}
}
#[test]
fn page_options_are_print_settings_and_template_fields() {
let mut s = Session::with_demo();
let g = run(&mut s, "print.get", json!({}));
assert_eq!(g["settings"]["page"], json!({"background": [255, 255, 255], "pageNumbers": false, "pageInfo": false, "cropMarks": false}));
let g = run(&mut s, "print.set", json!({"page": {"pageNumbers": true, "cropMarks": true}}));
assert_eq!((g["settings"]["page"]["pageNumbers"].as_bool(), g["settings"]["page"]["pageInfo"].as_bool()), (Some(true), Some(false)));
assert_eq!(g["settings"]["page"]["background"], json!([255, 255, 255]), "a patch merges");
fails(&mut s, "print.set", json!({"page": {"pageInfo": "yes"}}));
fails(&mut s, "print.set", json!({"page": {"pageNumber": true}}));
// a template keeps them
run(&mut s, "print.template.save", json!({"name": "Marked"}));
run(&mut s, "print.reset", json!({}));
assert!(!s.print.page.crop_marks);
run(&mut s, "print.template.apply", json!({"name": "Marked"}));
assert!(s.print.page.page_numbers && s.print.page.crop_marks && !s.print.page.page_info);
let set = crate::find_command("print.set").unwrap();
assert!(set.params.contains("pageNumbers") && set.params.contains("pageInfo") && set.params.contains("cropMarks"), "{}", set.params);
}