mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 10:16:59 -06:00
Prepare and stage every chapter before a book update writes files. Preserve imported IDML sources, reject open native chapters and report completed paths after a later write failure. Follow-up to #485. PR #615.
189 lines
8.0 KiB
Rust
189 lines
8.0 KiB
Rust
#![cfg(not(target_arch = "wasm32"))]
|
|
|
|
use designcraft_engine::Session;
|
|
use serde_json::json;
|
|
use std::path::Path;
|
|
fn dir(name: &str) -> tempfile::TempDir {
|
|
tempfile::Builder::new().prefix(&format!("dc-book-safety-{name}-")).tempdir().unwrap()
|
|
}
|
|
fn document(start: u32, custom: bool) -> designcraft_doc::Document {
|
|
let mut s = Session::new();
|
|
s.execute("file.new", &json!({"pages":2})).unwrap();
|
|
let mut d = (*s.doc().unwrap().doc).clone();
|
|
d.sections.first_mut().unwrap().start_number = Some(start);
|
|
if custom {
|
|
let mut style = d.styles.paragraph.get(1).unwrap().clone();
|
|
style.name = "Synthetic Book Style".into();
|
|
d.styles_mut().paragraph.push(style);
|
|
}
|
|
d
|
|
}
|
|
fn native(p: &Path, start: u32, custom: bool) {
|
|
std::fs::write(p, designcraft_format::save(&document(start, custom)).unwrap()).unwrap();
|
|
}
|
|
fn idml(p: &Path, start: u32) {
|
|
std::fs::write(p, designcraft_idml::export_idml(&document(start, false))).unwrap();
|
|
assert!(designcraft_idml::import_idml(&std::fs::read(p).unwrap()).is_ok());
|
|
}
|
|
fn book(d: &Path, chapters: &[&Path]) -> Session {
|
|
let mut s = Session::new();
|
|
s.execute("book.new", &json!({"path":d.join("book.dcbook").to_string_lossy()})).unwrap();
|
|
for p in chapters {
|
|
s.execute("book.add", &json!({"path":p.to_string_lossy()})).unwrap();
|
|
}
|
|
s
|
|
}
|
|
#[test]
|
|
fn late_missing_chapter_does_not_partially_paginate_earlier_chapters() {
|
|
let d = dir("late-missing");
|
|
let first = d.path().join("first.designcraft");
|
|
let later = d.path().join("later.designcraft");
|
|
native(&first, 9, false);
|
|
native(&later, 1, false);
|
|
let mut s = book(d.path(), &[&first, &later]);
|
|
let before = std::fs::read(&first).unwrap();
|
|
std::fs::remove_file(later).unwrap();
|
|
let result = s.execute("book.paginate", &json!({}));
|
|
assert!(result.is_err());
|
|
let after = std::fs::read(&first).unwrap();
|
|
assert!(
|
|
before == after,
|
|
"book.paginate changed the first chapter before error: {result:?}; new first number {:?}",
|
|
designcraft_format::load(&after).unwrap().sections.first().unwrap().start_number
|
|
);
|
|
}
|
|
#[test]
|
|
fn late_corrupt_chapter_does_not_partially_sync_earlier_chapters() {
|
|
let d = dir("late-corrupt");
|
|
let source = d.path().join("source.designcraft");
|
|
let first = d.path().join("first.designcraft");
|
|
let later = d.path().join("later.designcraft");
|
|
native(&source, 1, true);
|
|
native(&first, 1, false);
|
|
native(&later, 1, false);
|
|
let mut s = book(d.path(), &[&source, &first, &later]);
|
|
let before = std::fs::read(&first).unwrap();
|
|
std::fs::write(later, b"not a document").unwrap();
|
|
let result = s.execute("book.syncStyles", &json!({}));
|
|
assert!(result.is_err());
|
|
let after = std::fs::read(&first).unwrap();
|
|
assert!(
|
|
before == after,
|
|
"book.syncStyles changed the first target before error: {result:?}; copied custom style {}",
|
|
designcraft_format::load(&after).unwrap().styles.paragraph.iter().any(|s| s.name == "Synthetic Book Style")
|
|
);
|
|
}
|
|
#[test]
|
|
fn paginate_does_not_replace_imported_idml_with_native_format() {
|
|
let d = dir("idml-paginate");
|
|
let chapter = d.path().join("chapter.idml");
|
|
idml(&chapter, 9);
|
|
let mut s = book(d.path(), &[&chapter]);
|
|
let before = std::fs::read(&chapter).unwrap();
|
|
let result = s.execute("book.paginate", &json!({}));
|
|
let after = std::fs::read(&chapter).unwrap();
|
|
assert!(
|
|
before == after,
|
|
"book.paginate rewrote imported IDML: {result:?}; IDML readable after {}, native readable after {}",
|
|
designcraft_idml::import_idml(&after).is_ok(),
|
|
designcraft_format::load(&after).is_ok()
|
|
);
|
|
}
|
|
#[test]
|
|
fn style_sync_does_not_replace_imported_idml_with_native_format() {
|
|
let d = dir("idml-sync");
|
|
let source = d.path().join("source.designcraft");
|
|
let chapter = d.path().join("chapter.idml");
|
|
native(&source, 1, true);
|
|
idml(&chapter, 1);
|
|
let mut s = book(d.path(), &[&source, &chapter]);
|
|
let before = std::fs::read(&chapter).unwrap();
|
|
let result = s.execute("book.syncStyles", &json!({}));
|
|
let after = std::fs::read(&chapter).unwrap();
|
|
assert!(
|
|
before == after,
|
|
"book.syncStyles rewrote imported IDML: {result:?}; IDML readable after {}, native readable after {}",
|
|
designcraft_idml::import_idml(&after).is_ok(),
|
|
designcraft_format::load(&after).is_ok()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn an_open_unsaved_chapter_blocks_batch_writes_before_any_files_change() {
|
|
let d = dir("open-unsaved");
|
|
let first = d.path().join("first.designcraft");
|
|
let later = d.path().join("later.designcraft");
|
|
native(&first, 9, false);
|
|
native(&later, 5, false);
|
|
let mut s = book(d.path(), &[&first, &later]);
|
|
s.execute("file.open", &json!({"path":later.to_string_lossy()})).unwrap();
|
|
s.execute("frame.create", &json!({"rect":[36,36,200,100],"content":"text","text":"Unsaved chapter edit"})).unwrap();
|
|
let first_before = std::fs::read(&first).unwrap();
|
|
let later_before = std::fs::read(&later).unwrap();
|
|
let revision = s.doc().unwrap().revision;
|
|
let result = s.execute("book.paginate", &json!({}));
|
|
assert!(
|
|
result.is_err(),
|
|
"book.paginate accepted an open dirty chapter: {result:?}; first changed {}, open backing file changed {}",
|
|
std::fs::read(&first).unwrap() != first_before,
|
|
std::fs::read(&later).unwrap() != later_before
|
|
);
|
|
assert_eq!(std::fs::read(&first).unwrap(), first_before);
|
|
assert_eq!(std::fs::read(&later).unwrap(), later_before);
|
|
assert_eq!(s.doc().unwrap().revision, revision);
|
|
assert!(s.doc().unwrap().is_dirty());
|
|
let e = result.unwrap_err().to_string();
|
|
assert!(e.contains("later.designcraft") && e.contains("close"), "{e}");
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn an_alias_of_an_open_chapter_blocks_all_batch_writes() {
|
|
let d = dir("open-alias");
|
|
let first = d.path().join("first.designcraft");
|
|
let real = d.path().join("real.designcraft");
|
|
let alias = d.path().join("alias.designcraft");
|
|
native(&first, 9, false);
|
|
native(&real, 5, false);
|
|
std::os::unix::fs::symlink("real.designcraft", &alias).unwrap();
|
|
let mut s = book(d.path(), &[&first, &alias]);
|
|
s.execute("file.open", &json!({"path":real.to_string_lossy()})).unwrap();
|
|
let first_before = std::fs::read(&first).unwrap();
|
|
let real_before = std::fs::read(&real).unwrap();
|
|
let result = s.execute("book.paginate", &json!({}));
|
|
assert!(result.is_err(), "book.paginate accepted alias of open chapter: {result:?}");
|
|
assert_eq!(std::fs::read(&first).unwrap(), first_before);
|
|
assert_eq!(std::fs::read(&real).unwrap(), real_before);
|
|
let e = result.unwrap_err().to_string();
|
|
assert!(e.contains("alias.designcraft") && e.contains("close"), "{e}");
|
|
assert!(std::fs::symlink_metadata(alias).unwrap().file_type().is_symlink());
|
|
}
|
|
#[test]
|
|
fn imported_idml_remains_a_read_only_style_source() {
|
|
let d = dir("idml-style-source");
|
|
let source = d.path().join("source.idml");
|
|
let target = d.path().join("target.designcraft");
|
|
std::fs::write(&source, designcraft_idml::export_idml(&document(1, true))).unwrap();
|
|
native(&target, 1, false);
|
|
let mut s = book(d.path(), &[&source, &target]);
|
|
let before = std::fs::read(&source).unwrap();
|
|
let result = s.execute("book.syncStyles", &json!({})).unwrap();
|
|
assert_eq!(result["synced"], 1);
|
|
assert_eq!(std::fs::read(&source).unwrap(), before);
|
|
assert!(designcraft_idml::import_idml(&before).is_ok());
|
|
let target = designcraft_format::load(&std::fs::read(target).unwrap()).unwrap();
|
|
assert!(target.styles.paragraph.iter().any(|s| s.name == "Synthetic Book Style"));
|
|
}
|
|
#[test]
|
|
fn imported_idml_still_exports_to_book_pdf_without_source_changes() {
|
|
let d = dir("idml-pdf");
|
|
let chapter = d.path().join("chapter.idml");
|
|
idml(&chapter, 1);
|
|
let mut s = book(d.path(), &[&chapter]);
|
|
let before = std::fs::read(&chapter).unwrap();
|
|
let result = s.execute("book.exportPdf", &json!({})).unwrap();
|
|
assert_eq!(result["pages"], 2);
|
|
assert!(result["base64"].as_str().is_some_and(|s| !s.is_empty()));
|
|
assert_eq!(std::fs::read(chapter).unwrap(), before);
|
|
}
|