ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git), ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
485 lines
18 KiB
Rust
485 lines
18 KiB
Rust
use super::*;
|
|
|
|
mod registry;
|
|
mod revert;
|
|
|
|
fn write(path: &Path, body: &str) {
|
|
std::fs::write(path, body).expect("the fixture file writes");
|
|
}
|
|
|
|
fn saved(session: &mut Session, target: &Path) {
|
|
let ticket = session.begin_save().expect("the save starts");
|
|
write(target, "saved");
|
|
let written = Fingerprint::of(target).expect("the written fingerprint");
|
|
session
|
|
.finish_save(ticket, target.to_path_buf(), written)
|
|
.expect("the save lands");
|
|
}
|
|
|
|
#[test]
|
|
fn a_new_file_has_no_path_until_saved() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let mut session = Session::new_file();
|
|
assert_eq!(session.path(), None);
|
|
session.edit();
|
|
assert_eq!(session.state(), SaveState::Dirty);
|
|
let target = dir.path().join("草稿 01.md");
|
|
saved(&mut session, &target);
|
|
assert_eq!(session.path(), Some(target.as_path()));
|
|
assert_eq!(session.state(), SaveState::Saved);
|
|
}
|
|
|
|
#[test]
|
|
fn an_old_ticket_never_marks_newer_edits_saved() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let target = dir.path().join("r.txt");
|
|
write(&target, "0");
|
|
let mut session = Session::load(target.clone()).expect("the file loads");
|
|
session.edit();
|
|
let stale = session.begin_save().expect("the first save starts");
|
|
session.edit();
|
|
write(&target, "1");
|
|
let written = Fingerprint::of(&target).expect("the written fingerprint");
|
|
session
|
|
.finish_save(stale, target.clone(), written)
|
|
.expect("the stale save lands");
|
|
assert_eq!(session.saved_revision(), 1);
|
|
assert_eq!(session.revision(), 2);
|
|
assert_eq!(session.state(), SaveState::Dirty);
|
|
saved(&mut session, &target);
|
|
assert_eq!(session.state(), SaveState::Saved);
|
|
}
|
|
|
|
#[test]
|
|
fn a_reload_kills_old_tickets() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let target = dir.path().join("k.txt");
|
|
write(&target, "0");
|
|
let mut session = Session::load(target.clone()).expect("the file loads");
|
|
session.edit();
|
|
let stale = session.begin_save().expect("the save starts");
|
|
session.reload().expect("the file reloads");
|
|
session.edit();
|
|
let written = Fingerprint::of(&target).expect("the fingerprint");
|
|
assert_eq!(
|
|
session.finish_save(stale, target, written),
|
|
Err(StaleTicket)
|
|
);
|
|
assert_eq!(session.saved_revision(), 0);
|
|
assert_eq!(session.state(), SaveState::Dirty);
|
|
}
|
|
|
|
#[test]
|
|
fn a_read_only_view_stays_read_only_through_reload() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let target = dir.path().join("v.txt");
|
|
write(&target, "0");
|
|
let mut session = Session::read_only(target.clone()).expect("the view loads");
|
|
write(&target, "1");
|
|
session.external_modified();
|
|
assert_eq!(session.state(), SaveState::ReadOnly);
|
|
session.reload().expect("the view reloads");
|
|
assert_eq!(session.state(), SaveState::ReadOnly);
|
|
assert!(matches!(session.begin_save(), Err(SaveState::ReadOnly)));
|
|
}
|
|
|
|
#[test]
|
|
fn a_save_completion_keeps_a_newer_conflict() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let target = dir.path().join("c.txt");
|
|
write(&target, "0");
|
|
let mut session = Session::load(target.clone()).expect("the file loads");
|
|
session.edit();
|
|
let ticket = session.begin_save().expect("the save starts");
|
|
write(&target, "ours");
|
|
let written = Fingerprint::of(&target).expect("our written fingerprint");
|
|
write(&target, "external content, longer");
|
|
session.external_modified();
|
|
session
|
|
.finish_save(ticket, target.clone(), written)
|
|
.expect("our save lands");
|
|
assert_eq!(session.state(), SaveState::ExternalModified);
|
|
assert_eq!(session.saved_revision(), 1);
|
|
assert_eq!(session.fingerprint(), Some(written));
|
|
assert_ne!(
|
|
session.fingerprint(),
|
|
Fingerprint::of(&target).ok(),
|
|
"the foreign fingerprint is never adopted"
|
|
);
|
|
session.reload().expect("the file reloads");
|
|
assert_eq!(session.state(), SaveState::Saved);
|
|
}
|
|
|
|
#[test]
|
|
fn a_conflict_refuses_a_plain_save() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let target = dir.path().join("m.txt");
|
|
write(&target, "0");
|
|
let mut session = Session::load(target.clone()).expect("the file loads");
|
|
write(&target, "1");
|
|
session.external_modified();
|
|
assert!(matches!(
|
|
session.begin_save(),
|
|
Err(SaveState::ExternalModified)
|
|
));
|
|
let ticket = session
|
|
.begin_save_overwrite()
|
|
.expect("the overwrite starts");
|
|
write(&target, "kept local");
|
|
let written = Fingerprint::of(&target).expect("the fingerprint");
|
|
session
|
|
.finish_save(ticket, target, written)
|
|
.expect("the overwrite lands");
|
|
assert_eq!(session.state(), SaveState::Saved);
|
|
}
|
|
|
|
#[test]
|
|
fn an_overwrite_keeps_the_access_mode_guards() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let target = dir.path().join("g.txt");
|
|
write(&target, "0");
|
|
let mut view = Session::read_only(target.clone()).expect("the view loads");
|
|
assert!(matches!(
|
|
view.begin_save_overwrite(),
|
|
Err(SaveState::ReadOnly)
|
|
));
|
|
let mut loading = Session::loading(target);
|
|
assert!(matches!(
|
|
loading.begin_save_overwrite(),
|
|
Err(SaveState::Loading)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn a_conflict_survives_edits_until_resolved() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let target = dir.path().join("e.txt");
|
|
write(&target, "0");
|
|
let mut session = Session::load(target.clone()).expect("the file loads");
|
|
write(&target, "1");
|
|
session.external_modified();
|
|
session.edit();
|
|
assert_eq!(session.state(), SaveState::ExternalModified);
|
|
assert!(matches!(
|
|
session.begin_save(),
|
|
Err(SaveState::ExternalModified)
|
|
));
|
|
let ticket = session.begin_save_overwrite().expect("keep-local starts");
|
|
write(&target, "kept");
|
|
let written = Fingerprint::of(&target).expect("the fingerprint");
|
|
session
|
|
.finish_save(ticket, target, written)
|
|
.expect("the overwrite lands");
|
|
assert_eq!(session.state(), SaveState::Saved);
|
|
}
|
|
|
|
#[test]
|
|
fn a_conflict_survives_a_failed_save() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let target = dir.path().join("s.txt");
|
|
write(&target, "0");
|
|
let mut session = Session::load(target.clone()).expect("the file loads");
|
|
session.edit();
|
|
let ticket = session.begin_save().expect("the save starts");
|
|
write(&target, "external");
|
|
session.external_modified();
|
|
session.fail_save(ticket).expect("the failure lands");
|
|
assert_eq!(session.state(), SaveState::SaveFailed);
|
|
assert!(matches!(
|
|
session.begin_save(),
|
|
Err(SaveState::ExternalModified)
|
|
));
|
|
session.reload().expect("the file reloads");
|
|
assert_eq!(session.state(), SaveState::Saved);
|
|
}
|
|
|
|
#[test]
|
|
fn a_delayed_load_keeps_a_newer_conflict() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let target = dir.path().join("d.txt");
|
|
write(&target, "fourteen bytes");
|
|
let loaded = Fingerprint::of(&target).expect("the loaded fingerprint");
|
|
let mut session = Session::loading(target.clone());
|
|
write(&target, "external content, much longer than before");
|
|
session.external_modified();
|
|
session.finish_load(loaded).expect("the load lands");
|
|
assert_eq!(session.state(), SaveState::ExternalModified);
|
|
assert_eq!(session.fingerprint(), Some(loaded));
|
|
}
|
|
|
|
#[test]
|
|
fn a_failed_save_keeps_the_unsaved_content() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let target = dir.path().join("f.txt");
|
|
write(&target, "0");
|
|
let mut session = Session::load(target).expect("the file loads");
|
|
session.edit();
|
|
session.edit();
|
|
let ticket = session.begin_save().expect("the save starts");
|
|
session.fail_save(ticket).expect("the failure lands");
|
|
assert_eq!(session.state(), SaveState::SaveFailed);
|
|
assert_eq!(session.revision(), 2);
|
|
let retry = session.begin_save();
|
|
assert!(retry.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn loading_transitions_cover_success_and_failure() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let target = dir.path().join("big.docx");
|
|
write(&target, "0");
|
|
let mut session = Session::loading(target.clone());
|
|
assert_eq!(session.state(), SaveState::Loading);
|
|
assert!(matches!(session.begin_save(), Err(SaveState::Loading)));
|
|
let loaded = Fingerprint::of(&target).expect("the loaded fingerprint");
|
|
session.finish_load(loaded).expect("the load lands");
|
|
assert_eq!(session.state(), SaveState::Saved);
|
|
assert!(session.fingerprint().is_some());
|
|
let mut failed = Session::loading(target);
|
|
failed.fail_load().expect("the failure lands");
|
|
assert_eq!(failed.state(), SaveState::LoadFailed);
|
|
assert!(matches!(failed.begin_save(), Err(SaveState::LoadFailed)));
|
|
}
|
|
|
|
#[test]
|
|
fn a_running_task_refuses_another() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let target = dir.path().join("t.txt");
|
|
write(&target, "0");
|
|
let mut loading = Session::loading(target.clone());
|
|
write(&target, "external");
|
|
loading.external_modified();
|
|
assert!(matches!(
|
|
loading.begin_save_overwrite(),
|
|
Err(SaveState::Loading)
|
|
));
|
|
|
|
let mut session = Session::load(target.clone()).expect("the file loads");
|
|
session.edit();
|
|
let first = session.begin_save().expect("the first save starts");
|
|
write(&target, "external again");
|
|
session.external_modified();
|
|
session.edit();
|
|
assert!(matches!(
|
|
session.begin_save_overwrite(),
|
|
Err(SaveState::Saving)
|
|
));
|
|
write(&target, "first");
|
|
let written = Fingerprint::of(&target).expect("the fingerprint");
|
|
session
|
|
.finish_save(first, target.clone(), written)
|
|
.expect("the first save lands");
|
|
assert_eq!(session.state(), SaveState::ExternalModified);
|
|
let second = session.begin_save_overwrite().expect("keep-local starts");
|
|
write(&target, "second");
|
|
let written = Fingerprint::of(&target).expect("the fingerprint");
|
|
session
|
|
.finish_save(second, target, written)
|
|
.expect("the second save lands");
|
|
assert_eq!(session.state(), SaveState::Saved);
|
|
assert_eq!(session.saved_revision(), session.revision());
|
|
}
|
|
|
|
#[test]
|
|
fn a_stale_completion_releases_the_save_slot() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let target = dir.path().join("slot.txt");
|
|
write(&target, "0");
|
|
let mut session = Session::load(target.clone()).expect("the file loads");
|
|
session.edit();
|
|
let stale = session.begin_save().expect("the save starts");
|
|
write(&target, "external");
|
|
session.external_modified();
|
|
session.reload().expect("the file reloads");
|
|
let written = Fingerprint::of(&target).expect("the fingerprint");
|
|
assert_eq!(
|
|
session.finish_save(stale, target.clone(), written),
|
|
Err(StaleTicket)
|
|
);
|
|
session.edit();
|
|
let fresh = session.begin_save().expect("the slot released");
|
|
write(&target, "fresh");
|
|
let written = Fingerprint::of(&target).expect("the fingerprint");
|
|
session
|
|
.finish_save(fresh, target.clone(), written)
|
|
.expect("the fresh save lands");
|
|
assert_eq!(session.state(), SaveState::Saved);
|
|
let written = Fingerprint::of(&target).expect("the fingerprint");
|
|
assert_eq!(
|
|
session.finish_save(fresh, target, written),
|
|
Err(StaleTicket),
|
|
"a second completion of one ticket is stale"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_failed_load_never_saves_after_external_events() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let target = dir.path().join("fl.txt");
|
|
write(&target, "0");
|
|
let mut session = Session::loading(target.clone());
|
|
session.fail_load().expect("the failure lands");
|
|
write(&target, "external");
|
|
session.external_modified();
|
|
assert!(matches!(
|
|
session.begin_save_overwrite(),
|
|
Err(SaveState::LoadFailed)
|
|
));
|
|
assert!(matches!(session.begin_save(), Err(SaveState::LoadFailed)));
|
|
}
|
|
|
|
#[test]
|
|
fn a_duplicate_stale_completion_keeps_the_running_save() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let target = dir.path().join("dup.txt");
|
|
write(&target, "0");
|
|
let mut session = Session::load(target.clone()).expect("the file loads");
|
|
session.edit();
|
|
let stale = session.begin_save().expect("the old save starts");
|
|
session.reload().expect("the file reloads");
|
|
let written = Fingerprint::of(&target).expect("the fingerprint");
|
|
assert_eq!(
|
|
session.finish_save(stale, target.clone(), written),
|
|
Err(StaleTicket)
|
|
);
|
|
session.edit();
|
|
let fresh = session.begin_save().expect("the new save starts");
|
|
let written = Fingerprint::of(&target).expect("the fingerprint");
|
|
assert_eq!(
|
|
session.finish_save(stale, target.clone(), written),
|
|
Err(StaleTicket),
|
|
"a duplicate stale completion never frees the running save"
|
|
);
|
|
write(&target, "fresh");
|
|
let written = Fingerprint::of(&target).expect("the fingerprint");
|
|
session
|
|
.finish_save(fresh, target, written)
|
|
.expect("the running save lands");
|
|
assert_eq!(session.state(), SaveState::Saved);
|
|
}
|
|
|
|
#[test]
|
|
fn a_failed_load_then_reload_recovers_saving() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let target = dir.path().join("rec.txt");
|
|
write(&target, "0");
|
|
let mut session = Session::loading(target.clone());
|
|
session.fail_load().expect("the failure lands");
|
|
assert!(matches!(session.begin_save(), Err(SaveState::LoadFailed)));
|
|
session.reload().expect("the file reloads");
|
|
assert_eq!(session.state(), SaveState::Saved);
|
|
session.edit();
|
|
let ticket = session.begin_save().expect("the save starts after reload");
|
|
write(&target, "edited");
|
|
let written = Fingerprint::of(&target).expect("the fingerprint");
|
|
session
|
|
.finish_save(ticket, target, written)
|
|
.expect("the save lands");
|
|
assert_eq!(session.state(), SaveState::Saved);
|
|
}
|
|
|
|
#[test]
|
|
fn save_as_tasks_have_unique_identities() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let first = dir.path().join("first.md");
|
|
let second = dir.path().join("second.md");
|
|
write(&first, "0");
|
|
let mut session = Session::load(first.clone()).expect("the file loads");
|
|
session.edit();
|
|
let one = session.begin_save().expect("the first save starts");
|
|
write(&first, "1");
|
|
let written = Fingerprint::of(&first).expect("the fingerprint");
|
|
session
|
|
.finish_save(one, first.clone(), written)
|
|
.expect("the first save lands");
|
|
let two = session.begin_save().expect("the save-as starts");
|
|
let written = Fingerprint::of(&first).expect("the fingerprint");
|
|
assert_eq!(
|
|
session.finish_save(one, first, written),
|
|
Err(StaleTicket),
|
|
"a repeated old callback never frees the save-as task"
|
|
);
|
|
write(&second, "1");
|
|
let written = Fingerprint::of(&second).expect("the fingerprint");
|
|
session
|
|
.finish_save(two, second.clone(), written)
|
|
.expect("the save-as lands");
|
|
assert_eq!(session.path(), Some(second.as_path()));
|
|
assert_eq!(session.state(), SaveState::Saved);
|
|
}
|
|
|
|
#[test]
|
|
fn a_duplicate_failure_keeps_the_running_save() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let target = dir.path().join("df.txt");
|
|
write(&target, "0");
|
|
let mut session = Session::load(target.clone()).expect("the file loads");
|
|
session.edit();
|
|
let old = session.begin_save().expect("task A starts");
|
|
session.fail_save(old).expect("task A fails");
|
|
session.edit();
|
|
let running = session.begin_save().expect("task B starts");
|
|
assert_eq!(
|
|
session.fail_save(old),
|
|
Err(StaleTicket),
|
|
"a repeated old failure never frees task B"
|
|
);
|
|
assert_eq!(session.state(), SaveState::Saving);
|
|
write(&target, "done");
|
|
let written = Fingerprint::of(&target).expect("the fingerprint");
|
|
session
|
|
.finish_save(running, target, written)
|
|
.expect("task B lands");
|
|
assert_eq!(session.state(), SaveState::Saved);
|
|
}
|
|
|
|
#[test]
|
|
fn a_stale_failure_keeps_the_reloaded_state() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let target = dir.path().join("sf.txt");
|
|
write(&target, "0");
|
|
let mut session = Session::load(target.clone()).expect("the file loads");
|
|
session.edit();
|
|
let stale = session.begin_save().expect("the save starts");
|
|
write(&target, "external");
|
|
session.external_modified();
|
|
session.reload().expect("the file reloads");
|
|
assert_eq!(session.fail_save(stale), Err(StaleTicket));
|
|
assert_eq!(session.state(), SaveState::Saved);
|
|
session.edit();
|
|
let fresh = session.begin_save().expect("the slot released");
|
|
write(&target, "fresh");
|
|
let written = Fingerprint::of(&target).expect("the fingerprint");
|
|
session
|
|
.finish_save(fresh, target, written)
|
|
.expect("the fresh save lands");
|
|
assert_eq!(session.state(), SaveState::Saved);
|
|
}
|
|
|
|
#[test]
|
|
fn a_stale_load_callback_keeps_the_reloaded_state() {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let target = dir.path().join("sl.txt");
|
|
write(&target, "0");
|
|
let old = Fingerprint::of(&target).expect("the old fingerprint");
|
|
let mut session = Session::loading(target.clone());
|
|
write(&target, "external, longer");
|
|
session.external_modified();
|
|
session.reload().expect("the file reloads");
|
|
session.edit();
|
|
assert_eq!(session.finish_load(old), Err(StaleTicket));
|
|
assert_eq!(session.state(), SaveState::Dirty);
|
|
assert_ne!(session.fingerprint(), Some(old));
|
|
assert_eq!(session.fail_load(), Err(StaleTicket));
|
|
assert_eq!(session.state(), SaveState::Dirty);
|
|
let ticket = session.begin_save().expect("the save starts");
|
|
write(&target, "current");
|
|
let written = Fingerprint::of(&target).expect("the fingerprint");
|
|
session
|
|
.finish_save(ticket, target, written)
|
|
.expect("the save lands");
|
|
assert_eq!(session.state(), SaveState::Saved);
|
|
}
|
|
|
|
mod failed;
|