|
|
|
@@ -1,5 +1,12 @@
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
/// [`super::discard`] as the session that wrote or recovered the entry, if one did.
|
|
|
|
|
fn discard(dir: &Path, uid: u64) {
|
|
|
|
|
let key = directory_key(dir).ok().map(|key| (key, uid));
|
|
|
|
|
let owner = key.and_then(|key| WRITTEN.lock().unwrap_or_else(std::sync::PoisonError::into_inner).get(&key).copied());
|
|
|
|
|
super::discard(dir, uid, owner.unwrap_or(0));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn dirty_session() -> Session {
|
|
|
|
|
let mut session = Session::new();
|
|
|
|
|
session.execute("file.new", &json!({})).unwrap();
|
|
|
|
@@ -126,6 +133,727 @@ fn recovery_data_is_private_to_the_user() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The recovery writer is one thread for the whole process, and its results go to whichever
|
|
|
|
|
/// session polls it: tests that submit to it take turns, so one test's write or error is never
|
|
|
|
|
/// drained by another.
|
|
|
|
|
static BACKGROUND: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
|
|
|
|
|
|
|
|
|
fn background() -> std::sync::MutexGuard<'static, ()> {
|
|
|
|
|
BACKGROUND.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Wait for the background writer to have nothing left, then read `file.recovery.progress` as the
|
|
|
|
|
/// app does at the end of a frame.
|
|
|
|
|
fn finished_writes(s: &mut Session) -> usize {
|
|
|
|
|
testing::wait_until_idle();
|
|
|
|
|
let v = s.execute("file.recovery.progress", &json!({})).unwrap();
|
|
|
|
|
assert_eq!(v["errors"], json!([]), "{v}");
|
|
|
|
|
assert_eq!(v["busy"], false, "{v}");
|
|
|
|
|
v["written"].as_u64().unwrap_or(0) as usize
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
#[test]
|
|
|
|
|
fn a_background_save_returns_while_its_write_is_held() {
|
|
|
|
|
let _turn = background();
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let mut s = dirty_session();
|
|
|
|
|
s.recovery_dir = Some(dir.path().to_path_buf());
|
|
|
|
|
let uid = s.docs[0].uid;
|
|
|
|
|
let (file, _) = files(dir.path(), uid);
|
|
|
|
|
let held = testing::hold_next_write(dir.path());
|
|
|
|
|
let queued = s.execute("file.recovery.save", &json!({"background": true})).unwrap();
|
|
|
|
|
assert!(!held.ran_here(), "the write ran on the calling thread");
|
|
|
|
|
assert_eq!(queued["queued"], 1, "{queued}");
|
|
|
|
|
// The writer thread is stopped at the start of the write, and the session goes on.
|
|
|
|
|
held.wait_until_held();
|
|
|
|
|
let v = s.execute("file.recovery.progress", &json!({})).unwrap();
|
|
|
|
|
assert_eq!(v["busy"], true, "{v}");
|
|
|
|
|
s.execute("frame.create", &json!({"rect": [20, 20, 60, 60]})).unwrap();
|
|
|
|
|
assert!(!file.exists(), "nothing is published while the write is held");
|
|
|
|
|
held.release();
|
|
|
|
|
assert_eq!(finished_writes(&mut s), 1);
|
|
|
|
|
assert!(file.exists());
|
|
|
|
|
discard(dir.path(), uid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
#[test]
|
|
|
|
|
fn a_document_saved_while_its_write_is_held_leaves_no_snapshot() {
|
|
|
|
|
let _turn = background();
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let mut s = dirty_session();
|
|
|
|
|
s.recovery_dir = Some(dir.path().to_path_buf());
|
|
|
|
|
let uid = s.docs[0].uid;
|
|
|
|
|
let held = testing::hold_next_write(dir.path());
|
|
|
|
|
s.execute("file.recovery.save", &json!({"background": true})).unwrap();
|
|
|
|
|
held.wait_until_held();
|
|
|
|
|
let saved = dir.path().join("saved.designcraft");
|
|
|
|
|
s.execute("file.saveAs", &json!({"path": saved.to_string_lossy()})).unwrap();
|
|
|
|
|
held.release();
|
|
|
|
|
// Before the session takes the result: a crash now must not find the stale snapshot.
|
|
|
|
|
testing::wait_until_idle();
|
|
|
|
|
assert!(!files(dir.path(), uid).0.exists(), "the stale snapshot was not published");
|
|
|
|
|
assert!(!files(dir.path(), uid).1.exists(), "nor its metadata");
|
|
|
|
|
assert_eq!(finished_writes(&mut s), 0);
|
|
|
|
|
assert!(list(dir.path()).is_empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
#[test]
|
|
|
|
|
fn a_document_saved_while_its_write_to_a_named_folder_is_held_leaves_no_snapshot() {
|
|
|
|
|
// A folder given to `file.recovery.save` is not the session's, and is not in the document's
|
|
|
|
|
// entries until the write's result is taken.
|
|
|
|
|
let _turn = background();
|
|
|
|
|
let named = tempfile::tempdir().unwrap();
|
|
|
|
|
let mut s = dirty_session();
|
|
|
|
|
let uid = s.docs[0].uid;
|
|
|
|
|
let held = testing::hold_next_write(named.path());
|
|
|
|
|
let queued = s.execute("file.recovery.save", &json!({"background": true, "dir": named.path().to_string_lossy()})).unwrap();
|
|
|
|
|
assert_eq!(queued["queued"], 1, "{queued}");
|
|
|
|
|
held.wait_until_held();
|
|
|
|
|
let saved = named.path().join("saved.designcraft");
|
|
|
|
|
s.execute("file.saveAs", &json!({"path": saved.to_string_lossy()})).unwrap();
|
|
|
|
|
held.release();
|
|
|
|
|
// Before the session takes the result: a crash now must not find the stale snapshot.
|
|
|
|
|
testing::wait_until_idle();
|
|
|
|
|
assert!(!files(named.path(), uid).0.exists(), "the stale snapshot was not published");
|
|
|
|
|
assert_eq!(finished_writes(&mut s), 0);
|
|
|
|
|
assert!(list(named.path()).is_empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
#[test]
|
|
|
|
|
fn saving_removes_a_finished_write_to_a_named_folder_whose_result_was_not_taken() {
|
|
|
|
|
let _turn = background();
|
|
|
|
|
let named = tempfile::tempdir().unwrap();
|
|
|
|
|
let mut s = dirty_session();
|
|
|
|
|
let uid = s.docs[0].uid;
|
|
|
|
|
s.execute("file.recovery.save", &json!({"background": true, "dir": named.path().to_string_lossy()})).unwrap();
|
|
|
|
|
testing::wait_until_idle();
|
|
|
|
|
assert!(files(named.path(), uid).0.exists(), "the write finished");
|
|
|
|
|
let saved = named.path().join("saved.designcraft");
|
|
|
|
|
s.execute("file.saveAs", &json!({"path": saved.to_string_lossy()})).unwrap();
|
|
|
|
|
assert!(!files(named.path(), uid).0.exists(), "Save removed the snapshot before its result was taken");
|
|
|
|
|
// Edited again before the result is taken: the removed snapshot is not its checkpoint.
|
|
|
|
|
s.execute("frame.create", &json!({"rect": [20, 20, 60, 60]})).unwrap();
|
|
|
|
|
assert_eq!(finished_writes(&mut s), 0);
|
|
|
|
|
assert!(!s.docs[0].recovery_entries.iter().any(|(d, _)| same_directory(d, named.path())), "no checkpoint for a removed snapshot");
|
|
|
|
|
assert!(list(named.path()).is_empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
#[test]
|
|
|
|
|
fn opening_recovery_leaves_a_finished_background_write_whose_result_was_not_taken() {
|
|
|
|
|
let _turn = background();
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let mut s = dirty_session();
|
|
|
|
|
s.recovery_dir = Some(dir.path().to_path_buf());
|
|
|
|
|
let uid = s.docs[0].uid;
|
|
|
|
|
s.execute("file.recovery.save", &json!({"background": true})).unwrap();
|
|
|
|
|
testing::wait_until_idle();
|
|
|
|
|
assert!(files(dir.path(), uid).0.exists(), "the write finished");
|
|
|
|
|
assert!(open(&mut s, dir.path()).unwrap().is_empty(), "the live document's own snapshot is not recovered");
|
|
|
|
|
assert_eq!(s.docs.len(), 1);
|
|
|
|
|
assert_eq!(finished_writes(&mut s), 1);
|
|
|
|
|
assert_eq!(list(dir.path()).len(), 1);
|
|
|
|
|
discard(dir.path(), uid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
#[test]
|
|
|
|
|
fn an_edit_after_a_save_is_written_after_the_cancelled_write_stops() {
|
|
|
|
|
let _turn = background();
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let mut s = dirty_session();
|
|
|
|
|
s.recovery_dir = Some(dir.path().to_path_buf());
|
|
|
|
|
let uid = s.docs[0].uid;
|
|
|
|
|
let held = testing::hold_next_write(dir.path());
|
|
|
|
|
s.execute("file.recovery.save", &json!({"background": true})).unwrap();
|
|
|
|
|
held.wait_until_held();
|
|
|
|
|
let saved = tempfile::tempdir().unwrap();
|
|
|
|
|
s.execute("file.saveAs", &json!({"path": saved.path().join("saved.designcraft").to_string_lossy()})).unwrap();
|
|
|
|
|
s.execute("frame.create", &json!({"rect": [20, 20, 60, 60]})).unwrap();
|
|
|
|
|
// The held write was cancelled: the new edit is queued behind it instead of waiting for the
|
|
|
|
|
// next timer tick.
|
|
|
|
|
let queued = s.execute("file.recovery.save", &json!({"background": true})).unwrap();
|
|
|
|
|
assert_eq!(queued["queued"], 1, "{queued}");
|
|
|
|
|
held.release();
|
|
|
|
|
assert_eq!(finished_writes(&mut s), 1);
|
|
|
|
|
let mut reopened = Session::new();
|
|
|
|
|
assert_eq!(open(&mut reopened, dir.path()).unwrap().len(), 1);
|
|
|
|
|
assert_eq!(reopened.doc().unwrap().doc.spreads[0].items.len(), 2, "the snapshot after the edit was written");
|
|
|
|
|
discard(dir.path(), reopened.doc().unwrap().uid);
|
|
|
|
|
discard(dir.path(), uid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
#[test]
|
|
|
|
|
fn a_background_result_goes_to_the_session_that_queued_it() {
|
|
|
|
|
let _turn = background();
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let mut queued_by = dirty_session();
|
|
|
|
|
queued_by.recovery_dir = Some(dir.path().to_path_buf());
|
|
|
|
|
let uid = queued_by.docs[0].uid;
|
|
|
|
|
let mut other = dirty_session();
|
|
|
|
|
queued_by.execute("file.recovery.save", &json!({"background": true})).unwrap();
|
|
|
|
|
testing::wait_until_idle();
|
|
|
|
|
// Another session has none of these documents: it must neither take the result nor remove
|
|
|
|
|
// the snapshot as if its document had been closed.
|
|
|
|
|
let v = other.execute("file.recovery.progress", &json!({})).unwrap();
|
|
|
|
|
assert_eq!(v["written"], 0, "{v}");
|
|
|
|
|
assert!(files(dir.path(), uid).0.exists(), "the snapshot is still there");
|
|
|
|
|
assert_eq!(finished_writes(&mut queued_by), 1);
|
|
|
|
|
assert!(queued_by.docs[0].recovery_entries.iter().any(|(d, u)| *u == uid && same_directory(d, dir.path())), "the checkpoint is recorded");
|
|
|
|
|
discard(dir.path(), uid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
#[test]
|
|
|
|
|
fn saving_in_one_session_leaves_another_sessions_write_alone() {
|
|
|
|
|
let _turn = background();
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let mut queued_by = dirty_session();
|
|
|
|
|
queued_by.recovery_dir = Some(dir.path().to_path_buf());
|
|
|
|
|
let uid = queued_by.docs[0].uid;
|
|
|
|
|
// The same uid in another session (recovered documents keep the uids of their entries).
|
|
|
|
|
let mut other = dirty_session();
|
|
|
|
|
other.docs[0].uid = uid;
|
|
|
|
|
let held = testing::hold_next_write(dir.path());
|
|
|
|
|
queued_by.execute("file.recovery.save", &json!({"background": true})).unwrap();
|
|
|
|
|
held.wait_until_held();
|
|
|
|
|
let elsewhere = tempfile::tempdir().unwrap();
|
|
|
|
|
other.execute("file.saveAs", &json!({"path": elsewhere.path().join("other.designcraft").to_string_lossy()})).unwrap();
|
|
|
|
|
held.release();
|
|
|
|
|
assert_eq!(finished_writes(&mut queued_by), 1);
|
|
|
|
|
assert!(files(dir.path(), uid).0.exists(), "the other session's save did not cancel this write");
|
|
|
|
|
discard(dir.path(), uid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
#[test]
|
|
|
|
|
fn saving_in_one_session_leaves_another_sessions_write_to_the_same_folder_alone() {
|
|
|
|
|
let _turn = background();
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let mut queued_by = dirty_session();
|
|
|
|
|
queued_by.recovery_dir = Some(dir.path().to_path_buf());
|
|
|
|
|
let uid = queued_by.docs[0].uid;
|
|
|
|
|
// The same uid and recovery folder in another session.
|
|
|
|
|
let mut other = dirty_session();
|
|
|
|
|
other.recovery_dir = Some(dir.path().to_path_buf());
|
|
|
|
|
other.docs[0].uid = uid;
|
|
|
|
|
let held = testing::hold_next_write(dir.path());
|
|
|
|
|
queued_by.execute("file.recovery.save", &json!({"background": true})).unwrap();
|
|
|
|
|
held.wait_until_held();
|
|
|
|
|
let elsewhere = tempfile::tempdir().unwrap();
|
|
|
|
|
other.execute("file.saveAs", &json!({"path": elsewhere.path().join("other.designcraft").to_string_lossy()})).unwrap();
|
|
|
|
|
assert!(!worker::cancelled(dir.path(), uid), "the other session's save cancelled this write");
|
|
|
|
|
held.release();
|
|
|
|
|
assert_eq!(finished_writes(&mut queued_by), 1);
|
|
|
|
|
assert!(files(dir.path(), uid).0.exists(), "this session's snapshot was kept");
|
|
|
|
|
discard(dir.path(), uid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
#[test]
|
|
|
|
|
fn a_write_now_in_one_session_leaves_another_sessions_write_of_the_same_uid_alone() {
|
|
|
|
|
let _turn = background();
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let mut queued_by = dirty_session();
|
|
|
|
|
queued_by.recovery_dir = Some(dir.path().to_path_buf());
|
|
|
|
|
let uid = queued_by.docs[0].uid;
|
|
|
|
|
// The same uid in another session, with two objects instead of one.
|
|
|
|
|
let mut other = dirty_session();
|
|
|
|
|
other.docs[0].uid = uid;
|
|
|
|
|
other.execute("frame.create", &json!({"rect": [20, 20, 60, 60]})).unwrap();
|
|
|
|
|
let held = testing::hold_next_write(dir.path());
|
|
|
|
|
queued_by.execute("file.recovery.save", &json!({"background": true})).unwrap();
|
|
|
|
|
held.wait_until_held();
|
|
|
|
|
let err = save(&other, dir.path()).unwrap_err();
|
|
|
|
|
assert!(err.to_string().contains("still being written"), "{err}");
|
|
|
|
|
let err = other.execute("file.recovery.save", &json!({"dir": dir.path().to_string_lossy()})).unwrap_err();
|
|
|
|
|
assert!(err.to_string().contains("still being written"), "{err}");
|
|
|
|
|
assert!(!worker::cancelled(dir.path(), uid), "the other session's write now cancelled this write");
|
|
|
|
|
held.release();
|
|
|
|
|
assert_eq!(finished_writes(&mut queued_by), 1);
|
|
|
|
|
let mut reopened = Session::new();
|
|
|
|
|
assert_eq!(open(&mut reopened, dir.path()).unwrap().len(), 1);
|
|
|
|
|
assert_eq!(reopened.doc().unwrap().doc.spreads[0].items.len(), 1, "the queued session's snapshot was kept");
|
|
|
|
|
discard(dir.path(), reopened.doc().unwrap().uid);
|
|
|
|
|
discard(dir.path(), uid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
#[test]
|
|
|
|
|
fn no_other_sessions_write_of_the_uid_is_queued_during_a_write_now() {
|
|
|
|
|
let _turn = background();
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let writing = dirty_session();
|
|
|
|
|
let uid = writing.docs[0].uid;
|
|
|
|
|
let mut other = dirty_session();
|
|
|
|
|
other.recovery_dir = Some(dir.path().to_path_buf());
|
|
|
|
|
other.docs[0].uid = uid;
|
|
|
|
|
let now = worker::write_now(dir.path(), uid, writing.recovery_owner.0).expect("nothing else is in flight");
|
|
|
|
|
let queued = other.execute("file.recovery.save", &json!({"background": true})).unwrap();
|
|
|
|
|
assert_eq!(queued["queued"], 0, "another session's write was queued during the write now: {queued}");
|
|
|
|
|
assert!(worker::write_now(dir.path(), uid, other.recovery_owner.0).is_none(), "a second write now of the uid started");
|
|
|
|
|
drop(now);
|
|
|
|
|
let queued = other.execute("file.recovery.save", &json!({"background": true})).unwrap();
|
|
|
|
|
assert_eq!(queued["queued"], 1, "{queued}");
|
|
|
|
|
assert_eq!(finished_writes(&mut other), 1);
|
|
|
|
|
discard(dir.path(), uid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
#[test]
|
|
|
|
|
fn a_write_now_cancels_a_background_write_to_the_same_folder_spelled_another_way() {
|
|
|
|
|
let _turn = background();
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
std::fs::create_dir(dir.path().join("sub")).unwrap();
|
|
|
|
|
let spelled = dir.path().join("sub").join("..");
|
|
|
|
|
let mut s = dirty_session();
|
|
|
|
|
s.recovery_dir = Some(spelled.clone());
|
|
|
|
|
let uid = s.docs[0].uid;
|
|
|
|
|
let held = testing::hold_next_write(&spelled);
|
|
|
|
|
s.execute("file.recovery.save", &json!({"background": true})).unwrap();
|
|
|
|
|
held.wait_until_held();
|
|
|
|
|
// The newer state, written now under the folder's plain name.
|
|
|
|
|
s.execute("frame.create", &json!({"rect": [20, 20, 60, 60]})).unwrap();
|
|
|
|
|
s.execute("file.recovery.save", &json!({"dir": dir.path().to_string_lossy()})).unwrap();
|
|
|
|
|
assert!(worker::cancelled(&spelled, uid), "the older write is cancelled");
|
|
|
|
|
held.release();
|
|
|
|
|
testing::wait_until_idle();
|
|
|
|
|
let _ = s.execute("file.recovery.progress", &json!({})).unwrap();
|
|
|
|
|
let mut reopened = Session::new();
|
|
|
|
|
assert_eq!(open(&mut reopened, dir.path()).unwrap().len(), 1);
|
|
|
|
|
assert_eq!(reopened.doc().unwrap().doc.spreads[0].items.len(), 2, "the newer snapshot was kept");
|
|
|
|
|
discard(dir.path(), reopened.doc().unwrap().uid);
|
|
|
|
|
discard(dir.path(), uid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
#[test]
|
|
|
|
|
fn queued_background_writes_count_toward_the_checkpoint_limit() {
|
|
|
|
|
let _turn = background();
|
|
|
|
|
let mut s = dirty_session();
|
|
|
|
|
let folders: Vec<tempfile::TempDir> = (0..=MAX_RECOVERY_ENTRIES).map(|_| tempfile::tempdir().unwrap()).collect();
|
|
|
|
|
// The first write is held, so every later one is still queued when the next is asked for.
|
|
|
|
|
let held = testing::hold_next_write(folders[0].path());
|
|
|
|
|
for folder in &folders[..MAX_RECOVERY_ENTRIES] {
|
|
|
|
|
let queued = s.execute("file.recovery.save", &json!({"background": true, "dir": folder.path().to_string_lossy()})).unwrap();
|
|
|
|
|
assert_eq!(queued["queued"], 1, "{queued}");
|
|
|
|
|
}
|
|
|
|
|
let over = folders[MAX_RECOVERY_ENTRIES].path();
|
|
|
|
|
let err = s.execute("file.recovery.save", &json!({"background": true, "dir": over.to_string_lossy()})).unwrap_err();
|
|
|
|
|
assert!(err.to_string().contains("too many recovery checkpoints"), "{err}");
|
|
|
|
|
// Saving cancels the queued writes: they no longer count once the document changes again.
|
|
|
|
|
let saved = folders[0].path().join("saved.designcraft");
|
|
|
|
|
s.execute("file.saveAs", &json!({"path": saved.to_string_lossy()})).unwrap();
|
|
|
|
|
s.execute("frame.create", &json!({"rect": [20, 20, 60, 60]})).unwrap();
|
|
|
|
|
let queued = s.execute("file.recovery.save", &json!({"background": true, "dir": over.to_string_lossy()})).unwrap();
|
|
|
|
|
assert_eq!(queued["queued"], 1, "{queued}");
|
|
|
|
|
held.release();
|
|
|
|
|
assert_eq!(finished_writes(&mut s), 1, "only the write queued after the save is a checkpoint");
|
|
|
|
|
assert_eq!(list(over).len(), 1);
|
|
|
|
|
let uid = s.docs[0].uid;
|
|
|
|
|
for folder in &folders {
|
|
|
|
|
discard(folder.path(), uid);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
#[test]
|
|
|
|
|
fn results_for_a_session_that_is_gone_are_not_kept() {
|
|
|
|
|
let _turn = background();
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let mut gone = dirty_session();
|
|
|
|
|
gone.recovery_dir = Some(dir.path().to_path_buf());
|
|
|
|
|
let owner = gone.recovery_owner.0;
|
|
|
|
|
gone.execute("file.recovery.save", &json!({"background": true})).unwrap();
|
|
|
|
|
testing::wait_until_idle();
|
|
|
|
|
drop(gone);
|
|
|
|
|
let mut other = dirty_session();
|
|
|
|
|
other.execute("file.recovery.progress", &json!({})).unwrap();
|
|
|
|
|
let writer = worker::started().unwrap();
|
|
|
|
|
let kept = writer.undelivered.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
|
|
|
assert!(!kept.iter().any(|d| d.owner == owner), "a result nobody can take was kept");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn a_background_save_rejects_a_background_that_is_not_true_or_false() {
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let mut s = dirty_session();
|
|
|
|
|
s.recovery_dir = Some(dir.path().to_path_buf());
|
|
|
|
|
let err = s.execute("file.recovery.save", &json!({"background": "yes"})).unwrap_err();
|
|
|
|
|
assert!(err.to_string().contains("`background` must be true or false"), "{err}");
|
|
|
|
|
assert!(list(dir.path()).is_empty(), "nothing was written");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn a_background_save_writes_recovery_data_the_app_can_recover() {
|
|
|
|
|
let _turn = background();
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let mut s = dirty_session();
|
|
|
|
|
s.recovery_dir = Some(dir.path().to_path_buf());
|
|
|
|
|
// The timer's call: the interface thread does not wait for the deflate or the file writes.
|
|
|
|
|
let queued = s.execute("file.recovery.save", &json!({"background": true})).unwrap();
|
|
|
|
|
assert_eq!(queued["queued"], 1, "{queued}");
|
|
|
|
|
assert_eq!(finished_writes(&mut s), 1);
|
|
|
|
|
assert_eq!(list(dir.path()).len(), 1);
|
|
|
|
|
assert!(s.docs[0].recovery_entries.iter().any(|(d, _)| same_directory(d, dir.path())), "the checkpoint is recorded");
|
|
|
|
|
// Recovering it gives the unsaved edits back.
|
|
|
|
|
let mut reopened = Session::new();
|
|
|
|
|
assert_eq!(open(&mut reopened, dir.path()).unwrap().len(), 1);
|
|
|
|
|
assert_eq!(reopened.doc().unwrap().doc.spreads[0].items.len(), 1);
|
|
|
|
|
assert!(reopened.doc().unwrap().is_dirty());
|
|
|
|
|
discard(dir.path(), reopened.doc().unwrap().uid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn a_snapshot_that_finishes_after_the_save_is_dropped() {
|
|
|
|
|
// A write of a large document runs for seconds: the user can save while it does. The saved
|
|
|
|
|
// document's checkpoint is consumed then, and the write must not put a stale snapshot back.
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let mut s = dirty_session();
|
|
|
|
|
s.recovery_dir = Some(dir.path().to_path_buf());
|
|
|
|
|
let uid = s.docs[0].uid;
|
|
|
|
|
// The snapshot the writer took before the save.
|
|
|
|
|
let entry = Snapshot::of(&prepare_save(&s, dir.path()).unwrap(), dir.path(), &s.docs[0]);
|
|
|
|
|
let saved = dir.path().join("saved.designcraft");
|
|
|
|
|
s.execute("file.saveAs", &json!({"path": saved.to_string_lossy()})).unwrap();
|
|
|
|
|
assert!(!s.docs[0].is_dirty());
|
|
|
|
|
// The write lands after the save.
|
|
|
|
|
let written = write_snapshot(&entry, || false);
|
|
|
|
|
assert!(written.published && written.error.is_none(), "the snapshot landed");
|
|
|
|
|
let (file, _) = files(dir.path(), uid);
|
|
|
|
|
assert!(file.exists());
|
|
|
|
|
assert!(!record_checkpoint(&mut s, dir.path().to_path_buf(), uid), "a saved document records no checkpoint");
|
|
|
|
|
assert!(!file.exists(), "its snapshot is not left to be recovered");
|
|
|
|
|
assert!(list(dir.path()).is_empty());
|
|
|
|
|
// A document still unsaved does record one.
|
|
|
|
|
s.execute("frame.create", &json!({"rect": [10, 10, 50, 50]})).unwrap();
|
|
|
|
|
let entry = Snapshot::of(&prepare_save(&s, dir.path()).unwrap(), dir.path(), &s.docs[0]);
|
|
|
|
|
let written = write_snapshot(&entry, || false);
|
|
|
|
|
assert!(written.published && written.error.is_none(), "the second snapshot landed");
|
|
|
|
|
assert!(record_checkpoint(&mut s, dir.path().to_path_buf(), uid));
|
|
|
|
|
assert!(s.docs[0].recovery_entries.iter().any(|(d, u)| *u == uid && d == dir.path()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn a_shared_folders_entry_is_only_its_writing_sessions() {
|
|
|
|
|
// A recovered entry keeps the uid it was written under, which a document of another session
|
|
|
|
|
// in the same process can have.
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let writer = dirty_session();
|
|
|
|
|
let uid = writer.docs[0].uid;
|
|
|
|
|
assert_eq!(save(&writer, dir.path()).unwrap(), 1);
|
|
|
|
|
let (file, _) = files(dir.path(), uid);
|
|
|
|
|
let snapshot = std::fs::read(&file).unwrap();
|
|
|
|
|
// Another session's clean document with that uid: its discard leaves the entry alone.
|
|
|
|
|
let mut clean = Session::new();
|
|
|
|
|
clean.execute("file.new", &json!({})).unwrap();
|
|
|
|
|
clean.docs[0].uid = uid;
|
|
|
|
|
assert_eq!(save(&clean, dir.path()).unwrap(), 0);
|
|
|
|
|
assert!(file.exists(), "another session's discard removed the entry");
|
|
|
|
|
// Another session's edited document with that uid: it does not write over the entry.
|
|
|
|
|
let mut edited = dirty_session();
|
|
|
|
|
edited.docs[0].uid = uid;
|
|
|
|
|
assert!(save(&edited, dir.path()).is_err());
|
|
|
|
|
assert_eq!(std::fs::read(&file).unwrap(), snapshot, "another session wrote over the entry");
|
|
|
|
|
super::discard(dir.path(), uid, writer.recovery_owner.0);
|
|
|
|
|
assert!(!file.exists(), "the writing session removes its entry");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn a_snapshot_queued_as_the_sessions_own_is_recorded_when_it_lands() {
|
|
|
|
|
// A write taken while the shared folder's entry was this session's, published after a cleanup
|
|
|
|
|
// dropped that record: Save and Close must still remove what it put there.
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let s = dirty_session();
|
|
|
|
|
let uid = s.docs[0].uid;
|
|
|
|
|
let owner = s.recovery_owner.0;
|
|
|
|
|
assert_eq!(save(&s, dir.path()).unwrap(), 1);
|
|
|
|
|
let entry = Snapshot::of(&prepare_save(&s, dir.path()).unwrap(), dir.path(), &s.docs[0]);
|
|
|
|
|
assert!(entry.own);
|
|
|
|
|
super::discard(dir.path(), uid, owner);
|
|
|
|
|
let (file, _) = files(dir.path(), uid);
|
|
|
|
|
assert!(!file.exists());
|
|
|
|
|
let written = write_snapshot(&entry, || false);
|
|
|
|
|
assert!(written.published && written.error.is_none(), "the snapshot landed");
|
|
|
|
|
super::discard(dir.path(), uid, owner);
|
|
|
|
|
assert!(!file.exists(), "the snapshot it wrote is the session's to remove");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
#[test]
|
|
|
|
|
fn a_snapshot_whose_metadata_failed_is_still_the_documents_to_remove() {
|
|
|
|
|
// The primary document is the recovery data; its metadata is optional. A metadata failure
|
|
|
|
|
// must not leave the published snapshot untracked, or Save and Close would not remove it and
|
|
|
|
|
// it would stay in the folder for ever.
|
|
|
|
|
let root = tempfile::tempdir().unwrap();
|
|
|
|
|
let dir = run_dir(root.path()).unwrap();
|
|
|
|
|
let mut s = dirty_session();
|
|
|
|
|
s.recovery_dir = Some(dir.clone());
|
|
|
|
|
let uid = s.docs[0].uid;
|
|
|
|
|
let (document, metadata) = files(&dir, uid);
|
|
|
|
|
// The folder is this run's own, so the entry is its own; a folder takes the metadata's name,
|
|
|
|
|
// which makes the optional write fail after the primary landed.
|
|
|
|
|
std::fs::create_dir(&metadata).unwrap();
|
|
|
|
|
let err = save_tracked(&mut s, &dir).unwrap_err();
|
|
|
|
|
assert!(err.to_string().contains(&metadata.display().to_string()), "{err}");
|
|
|
|
|
assert!(document.exists(), "the primary landed");
|
|
|
|
|
assert!(s.docs[0].recovery_entries.iter().any(|(d, u)| *u == uid && same_directory(d, &dir)), "and is tracked");
|
|
|
|
|
// Saving the document removes the snapshot this run wrote.
|
|
|
|
|
let saved = root.path().join("saved.designcraft");
|
|
|
|
|
s.execute("file.saveAs", &json!({"path": saved.to_string_lossy()})).unwrap();
|
|
|
|
|
assert!(!document.exists(), "Save removed the snapshot");
|
|
|
|
|
assert!(s.docs[0].recovery_entries.is_empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
#[test]
|
|
|
|
|
fn a_snapshot_whose_folder_sync_failed_is_still_the_documents_to_remove() {
|
|
|
|
|
// The rename put the snapshot in place before the folder sync failed: it is tracked, so Save
|
|
|
|
|
// removes it.
|
|
|
|
|
let root = tempfile::tempdir().unwrap();
|
|
|
|
|
let dir = run_dir(root.path()).unwrap();
|
|
|
|
|
let mut s = dirty_session();
|
|
|
|
|
s.recovery_dir = Some(dir.clone());
|
|
|
|
|
let uid = s.docs[0].uid;
|
|
|
|
|
let (document, _) = files(&dir, uid);
|
|
|
|
|
testing::fail_next_sync(&dir);
|
|
|
|
|
let err = save_tracked(&mut s, &dir).unwrap_err();
|
|
|
|
|
assert!(err.to_string().contains("sync failed"), "{err}");
|
|
|
|
|
assert!(document.exists(), "the snapshot landed");
|
|
|
|
|
assert!(s.docs[0].recovery_entries.iter().any(|(d, u)| *u == uid && same_directory(d, &dir)), "and is tracked");
|
|
|
|
|
let saved = root.path().join("saved.designcraft");
|
|
|
|
|
s.execute("file.saveAs", &json!({"path": saved.to_string_lossy()})).unwrap();
|
|
|
|
|
assert!(!document.exists(), "Save removed the snapshot");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
#[test]
|
|
|
|
|
fn a_document_undone_to_its_saved_state_while_its_write_is_held_leaves_no_snapshot() {
|
|
|
|
|
let _turn = background();
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let mut s = dirty_session();
|
|
|
|
|
s.recovery_dir = Some(dir.path().to_path_buf());
|
|
|
|
|
let uid = s.docs[0].uid;
|
|
|
|
|
let held = testing::hold_next_write(dir.path());
|
|
|
|
|
s.execute("file.recovery.save", &json!({"background": true})).unwrap();
|
|
|
|
|
held.wait_until_held();
|
|
|
|
|
s.execute("edit.undo", &json!({})).unwrap();
|
|
|
|
|
assert!(!s.docs[0].is_dirty(), "back to the state it had");
|
|
|
|
|
// A save of recovery data now drops the clean document's checkpoint, and the held write of
|
|
|
|
|
// the older state must not publish after it.
|
|
|
|
|
save(&s, dir.path()).unwrap();
|
|
|
|
|
held.release();
|
|
|
|
|
testing::wait_until_idle();
|
|
|
|
|
assert!(!files(dir.path(), uid).0.exists(), "the older snapshot was not published");
|
|
|
|
|
let _ = s.execute("file.recovery.progress", &json!({}));
|
|
|
|
|
assert!(list(dir.path()).is_empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
#[test]
|
|
|
|
|
fn a_document_undone_to_its_saved_state_loses_the_snapshot_a_finished_write_left_in_another_folder() {
|
|
|
|
|
let _turn = background();
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let other = tempfile::tempdir().unwrap();
|
|
|
|
|
let mut s = dirty_session();
|
|
|
|
|
s.recovery_dir = Some(other.path().to_path_buf());
|
|
|
|
|
let uid = s.docs[0].uid;
|
|
|
|
|
s.execute("file.recovery.save", &json!({"background": true})).unwrap();
|
|
|
|
|
// Written, but its result not yet taken by the session.
|
|
|
|
|
testing::wait_until_idle();
|
|
|
|
|
assert!(files(other.path(), uid).0.exists());
|
|
|
|
|
s.execute("edit.undo", &json!({})).unwrap();
|
|
|
|
|
save(&s, dir.path()).unwrap();
|
|
|
|
|
assert!(!files(other.path(), uid).0.exists(), "the snapshot of the older state is removed");
|
|
|
|
|
let _ = s.execute("file.recovery.progress", &json!({}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
#[test]
|
|
|
|
|
fn a_clean_document_leaves_another_sessions_write_queued_behind_its_own() {
|
|
|
|
|
let _turn = background();
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let mut a = dirty_session();
|
|
|
|
|
a.recovery_dir = Some(dir.path().to_path_buf());
|
|
|
|
|
let uid = a.docs[0].uid;
|
|
|
|
|
let mut b = dirty_session();
|
|
|
|
|
b.recovery_dir = Some(dir.path().to_path_buf());
|
|
|
|
|
b.docs[0].uid = uid;
|
|
|
|
|
b.execute("frame.create", &json!({"rect": [20, 20, 60, 60]})).unwrap();
|
|
|
|
|
let held = testing::hold_next_write(dir.path());
|
|
|
|
|
a.execute("file.recovery.save", &json!({"background": true})).unwrap();
|
|
|
|
|
held.wait_until_held();
|
|
|
|
|
// A write now cancels the held one. The other session does not write over the entry it left.
|
|
|
|
|
a.execute("file.recovery.save", &json!({"dir": dir.path().to_string_lossy()})).unwrap();
|
|
|
|
|
let err = b.execute("file.recovery.save", &json!({"background": true})).unwrap_err();
|
|
|
|
|
assert!(err.to_string().contains("did not write"), "{err}");
|
|
|
|
|
a.execute("edit.undo", &json!({})).unwrap();
|
|
|
|
|
assert!(!a.docs[0].is_dirty());
|
|
|
|
|
save(&a, dir.path()).unwrap();
|
|
|
|
|
// Once that entry is gone, the other session's write is queued to follow the held one.
|
|
|
|
|
b.execute("file.recovery.save", &json!({"background": true})).unwrap();
|
|
|
|
|
held.release();
|
|
|
|
|
assert_eq!(finished_writes(&mut b), 1, "the other session's write ran");
|
|
|
|
|
let mut reopened = Session::new();
|
|
|
|
|
assert_eq!(open(&mut reopened, dir.path()).unwrap().len(), 1);
|
|
|
|
|
assert_eq!(reopened.doc().unwrap().doc.spreads[0].items.len(), 2, "its snapshot was kept");
|
|
|
|
|
discard(dir.path(), uid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
#[test]
|
|
|
|
|
fn a_background_recovery_write_reports_another_sessions_held_write() {
|
|
|
|
|
let _turn = background();
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let mut first = dirty_session();
|
|
|
|
|
first.recovery_dir = Some(dir.path().to_path_buf());
|
|
|
|
|
let uid = first.docs[0].uid;
|
|
|
|
|
let mut second = dirty_session();
|
|
|
|
|
second.recovery_dir = Some(dir.path().to_path_buf());
|
|
|
|
|
second.docs[0].uid = uid;
|
|
|
|
|
second.execute("frame.create", &json!({"rect": [20, 20, 60, 60]})).unwrap();
|
|
|
|
|
|
|
|
|
|
let held = testing::hold_next_write(dir.path());
|
|
|
|
|
first.execute("file.recovery.save", &json!({"background": true})).unwrap();
|
|
|
|
|
held.wait_until_held();
|
|
|
|
|
|
|
|
|
|
let err = second.execute("file.recovery.save", &json!({"background": true})).unwrap_err();
|
|
|
|
|
assert!(err.to_string().contains("another session"), "{err}");
|
|
|
|
|
|
|
|
|
|
held.release();
|
|
|
|
|
assert_eq!(finished_writes(&mut first), 1);
|
|
|
|
|
assert!(files(dir.path(), uid).0.exists(), "the first session's recovery file remains intact");
|
|
|
|
|
discard(dir.path(), uid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
#[test]
|
|
|
|
|
fn a_clean_document_in_another_session_leaves_this_sessions_write_alone() {
|
|
|
|
|
let _turn = background();
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let mut queued_by = dirty_session();
|
|
|
|
|
queued_by.recovery_dir = Some(dir.path().to_path_buf());
|
|
|
|
|
let uid = queued_by.docs[0].uid;
|
|
|
|
|
// The same uid, clean, in another session.
|
|
|
|
|
let mut other = Session::new();
|
|
|
|
|
other.execute("file.new", &json!({})).unwrap();
|
|
|
|
|
other.docs[0].uid = uid;
|
|
|
|
|
assert!(!other.docs[0].is_dirty());
|
|
|
|
|
let held = testing::hold_next_write(dir.path());
|
|
|
|
|
queued_by.execute("file.recovery.save", &json!({"background": true})).unwrap();
|
|
|
|
|
held.wait_until_held();
|
|
|
|
|
save(&other, dir.path()).unwrap();
|
|
|
|
|
held.release();
|
|
|
|
|
assert_eq!(finished_writes(&mut queued_by), 1);
|
|
|
|
|
assert!(files(dir.path(), uid).0.exists(), "the other session's save did not cancel this write");
|
|
|
|
|
discard(dir.path(), uid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
#[test]
|
|
|
|
|
fn a_background_snapshot_whose_metadata_failed_is_still_the_documents_to_remove() {
|
|
|
|
|
// The background writer's result must be applied the same way: a published primary is the
|
|
|
|
|
// document's snapshot even when its optional metadata fails.
|
|
|
|
|
let _turn = background();
|
|
|
|
|
let root = tempfile::tempdir().unwrap();
|
|
|
|
|
let dir = run_dir(root.path()).unwrap();
|
|
|
|
|
let mut s = dirty_session();
|
|
|
|
|
s.recovery_dir = Some(dir.clone());
|
|
|
|
|
let uid = s.docs[0].uid;
|
|
|
|
|
let (document, metadata) = files(&dir, uid);
|
|
|
|
|
std::fs::create_dir(&metadata).unwrap();
|
|
|
|
|
let queued = s.execute("file.recovery.save", &json!({"background": true})).unwrap();
|
|
|
|
|
assert_eq!(queued["queued"], 1, "{queued}");
|
|
|
|
|
testing::wait_until_idle();
|
|
|
|
|
let v = s.execute("file.recovery.progress", &json!({})).unwrap();
|
|
|
|
|
assert_eq!(v["busy"], false, "{v}");
|
|
|
|
|
let reported = v["errors"].as_array().and_then(|errors| errors.first()).and_then(Value::as_str).map(str::to_string);
|
|
|
|
|
let reported = reported.expect("the metadata failure is reported");
|
|
|
|
|
assert!(reported.contains(&metadata.display().to_string()), "{reported}");
|
|
|
|
|
assert!(document.exists(), "the primary landed");
|
|
|
|
|
assert!(s.docs[0].recovery_entries.iter().any(|(d, u)| *u == uid && same_directory(d, &dir)), "and is tracked");
|
|
|
|
|
// Saving the document removes the snapshot this run wrote.
|
|
|
|
|
let saved = root.path().join("saved.designcraft");
|
|
|
|
|
s.execute("file.saveAs", &json!({"path": saved.to_string_lossy()})).unwrap();
|
|
|
|
|
assert!(!document.exists(), "Save removed the snapshot");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn a_write_cancelled_midway_publishes_nothing() {
|
|
|
|
|
// Save/Revert/Close during a write marks it cancelled; the snapshot must not land after it,
|
|
|
|
|
// even if the app crashes before the next interface frame discards it.
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let s = dirty_session();
|
|
|
|
|
let entry = Snapshot::of(&prepare_save(&s, dir.path()).unwrap(), dir.path(), &s.docs[0]);
|
|
|
|
|
let written = write_snapshot(&entry, || true);
|
|
|
|
|
assert!(!written.published && written.error.is_none(), "a cancelled write is not an error");
|
|
|
|
|
assert!(list(dir.path()).is_empty(), "a cancelled write leaves no snapshot");
|
|
|
|
|
assert!(!files(dir.path(), entry.uid).0.exists(), "nor its document");
|
|
|
|
|
assert!(!files(dir.path(), entry.uid).1.exists(), "nor its metadata");
|
|
|
|
|
// The temporary file it was written to is dropped, not left behind.
|
|
|
|
|
assert!(
|
|
|
|
|
std::fs::read_dir(dir.path()).unwrap().all(|e| e.is_ok_and(|e| !e.file_name().to_string_lossy().starts_with(".designcraft-recovery-"))),
|
|
|
|
|
"no temporary file is left"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn a_cancel_stops_a_write_before_the_package_is_finished() {
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let s = dirty_session();
|
|
|
|
|
let entry = Snapshot::of(&prepare_save(&s, dir.path()).unwrap(), dir.path(), &s.docs[0]);
|
|
|
|
|
// Cancelled once the write has started: the first check passes, every later one fails.
|
|
|
|
|
let checks = std::cell::Cell::new(0);
|
|
|
|
|
let written = write_snapshot(&entry, || {
|
|
|
|
|
checks.set(checks.get() + 1);
|
|
|
|
|
checks.get() > 1
|
|
|
|
|
});
|
|
|
|
|
assert!(!written.published && written.error.is_none(), "a stopped write is not an error");
|
|
|
|
|
assert!(checks.get() >= 2, "the write checked again before it finished");
|
|
|
|
|
assert!(!files(dir.path(), entry.uid).0.exists(), "nothing was published");
|
|
|
|
|
assert!(
|
|
|
|
|
std::fs::read_dir(dir.path()).unwrap().all(|e| e.is_ok_and(|e| !e.file_name().to_string_lossy().starts_with(".designcraft-recovery-"))),
|
|
|
|
|
"no temporary file is left"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
#[test]
|
|
|
|
|
fn only_a_queued_write_is_cancelled() {
|
|
|
|
|
// A mark for a document with no write in flight would stay in the writer's set for ever.
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
worker::cancel_owned(dir.path(), 7, 1);
|
|
|
|
|
assert!(!worker::cancelled(dir.path(), 7), "nothing was queued for it");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
#[test]
|
|
|
|
|
fn a_panicking_recovery_write_is_an_error_and_the_writer_keeps_going() {
|
|
|
|
|
let err = worker::guarded(|| -> Result<()> { panic!("boom") }).unwrap_err();
|
|
|
|
|
assert!(err.to_string().contains("panicked") && err.to_string().contains("boom"), "{err}");
|
|
|
|
|
assert_eq!(worker::guarded(|| Ok(7)).unwrap(), 7, "the next write still runs");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn a_saved_document_is_not_written_in_the_background() {
|
|
|
|
|
// The autosave writes unsaved documents only.
|
|
|
|
|
let _turn = background();
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let mut s = dirty_session();
|
|
|
|
|
s.recovery_dir = Some(dir.path().to_path_buf());
|
|
|
|
|
let saved = dir.path().join("saved.designcraft");
|
|
|
|
|
s.execute("file.saveAs", &json!({"path": saved.to_string_lossy()})).unwrap();
|
|
|
|
|
let queued = s.execute("file.recovery.save", &json!({"background": true})).unwrap();
|
|
|
|
|
assert_eq!(queued["queued"], 0, "{queued}");
|
|
|
|
|
assert_eq!(finished_writes(&mut s), 0);
|
|
|
|
|
assert!(list(dir.path()).is_empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn existing_recovery_directory_is_used() {
|
|
|
|
|
let root = tempfile::tempdir().unwrap();
|
|
|
|
@@ -440,6 +1168,11 @@ fn restart_recovery_child() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A child process starts with a copy of every open file of this one until it runs its program, so
|
|
|
|
|
/// a lock file closed meanwhile stays locked: a test that closes a lock and expects it free does
|
|
|
|
|
/// not run while a child is started.
|
|
|
|
|
static SPAWN: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
|
|
|
|
|
|
|
|
|
fn run_restart_case(mode: &str) {
|
|
|
|
|
let source = restart_fixture();
|
|
|
|
|
if mode == "max_uid" {
|
|
|
|
@@ -448,12 +1181,20 @@ fn run_restart_case(mode: &str) {
|
|
|
|
|
std::fs::rename(doc, max_doc).unwrap();
|
|
|
|
|
std::fs::rename(meta, max_meta).unwrap();
|
|
|
|
|
}
|
|
|
|
|
let output = std::process::Command::new(std::env::current_exe().unwrap())
|
|
|
|
|
.args(["--exact", "recovery::tests::restart_recovery_child", "--ignored", "--nocapture"])
|
|
|
|
|
.env("DESIGNCRAFT_TEST_RECOVERY_SOURCE", source.path())
|
|
|
|
|
.env("DESIGNCRAFT_TEST_RECOVERY_MODE", mode)
|
|
|
|
|
.output()
|
|
|
|
|
.unwrap();
|
|
|
|
|
// `spawn` returns once the child runs its program, and its copies of this process's files
|
|
|
|
|
// are closed by then.
|
|
|
|
|
let child = {
|
|
|
|
|
let _spawning = SPAWN.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
|
|
|
std::process::Command::new(std::env::current_exe().unwrap())
|
|
|
|
|
.args(["--exact", "recovery::tests::restart_recovery_child", "--ignored", "--nocapture"])
|
|
|
|
|
.env("DESIGNCRAFT_TEST_RECOVERY_SOURCE", source.path())
|
|
|
|
|
.env("DESIGNCRAFT_TEST_RECOVERY_MODE", mode)
|
|
|
|
|
.stdout(std::process::Stdio::piped())
|
|
|
|
|
.stderr(std::process::Stdio::piped())
|
|
|
|
|
.spawn()
|
|
|
|
|
.unwrap()
|
|
|
|
|
};
|
|
|
|
|
let output = child.wait_with_output().unwrap();
|
|
|
|
|
assert!(output.status.success(), "fresh child failed: {}{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr));
|
|
|
|
|
let child_stdout = String::from_utf8_lossy(&output.stdout);
|
|
|
|
|
assert!(child_stdout.contains("test result: ok. 1 passed; 0 failed;"), "the fresh-process helper did not run exactly once: {child_stdout}");
|
|
|
|
@@ -684,6 +1425,8 @@ fn documents_of_two_apps_that_ended_are_both_recovered() {
|
|
|
|
|
fn a_running_apps_recovery_data_is_left_alone() {
|
|
|
|
|
let root = tempfile::tempdir().unwrap();
|
|
|
|
|
let running = run_left(root.path(), "run-7-0", "still open", 1);
|
|
|
|
|
// No child starts while the lock is open, so closing it frees it (see `SPAWN`).
|
|
|
|
|
let _no_spawn = SPAWN.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
|
|
|
let lock = std::fs::File::open(running.join(LOCK)).unwrap();
|
|
|
|
|
lock.try_lock().unwrap();
|
|
|
|
|
let own = run_dir(root.path()).unwrap();
|
|
|
|
|