264 lines
10 KiB
Rust
264 lines
10 KiB
Rust
use super::*;
|
|
|
|
const VALID: &str = concat!(
|
|
"arcadia 7;\r\n",
|
|
"project repair \"Repair 救援\" id \"9e8f0000-0000-4000-8000-000000000001\" {\r\n",
|
|
" oa_activity assess \"Assess\" id \"9e8f0000-0000-4000-8000-000000000002\" {}\r\n",
|
|
"}\r\n",
|
|
);
|
|
|
|
fn open(app: &mut App, path: &std::path::Path) {
|
|
let _ = app.update(Message::Loaded(Ok(Some(
|
|
read_snapshot(path.to_owned()).unwrap(),
|
|
))));
|
|
}
|
|
|
|
fn edit(app: &mut App, text: &str) {
|
|
let _ = app.update(Message::SourceEdit(text_editor::Action::SelectAll));
|
|
let _ = app.update(Message::SourceEdit(text_editor::Action::Edit(
|
|
text_editor::Edit::Paste(Arc::new(text.into())),
|
|
)));
|
|
}
|
|
|
|
fn save(app: &mut App, path: &std::path::Path) {
|
|
let receipt = write_snapshot(
|
|
app.canvas_document,
|
|
app.document.revision(),
|
|
app.document.snapshot(),
|
|
path.to_owned(),
|
|
app.disk_project.clone(),
|
|
)
|
|
.unwrap();
|
|
let _ = app.update(Message::Saved(Ok(Some(receipt))));
|
|
}
|
|
|
|
#[test]
|
|
fn repair_to_previous_saved_bytes_requires_save_at_new_destination_and_guards_close() {
|
|
for extra in [
|
|
" include \"missing.arcadia\";\r\n",
|
|
" oa_activity assess \"Duplicate\" id \"9e8f0000-0000-4000-8000-000000000003\" {}\r\n",
|
|
] {
|
|
let first = tempfile::tempdir().unwrap();
|
|
let second = tempfile::tempdir().unwrap();
|
|
let first_path = first.path().canonicalize().unwrap().join("project.arcadia");
|
|
let second_path = second
|
|
.path()
|
|
.canonicalize()
|
|
.unwrap()
|
|
.join("project.arcadia");
|
|
let invalid = VALID.strip_suffix("}\r\n").unwrap().to_owned() + extra + "}\r\n";
|
|
std::fs::write(&first_path, VALID).unwrap();
|
|
std::fs::write(&second_path, &invalid).unwrap();
|
|
let mut app = App::with_project(Project::empty());
|
|
open(&mut app, &first_path);
|
|
assert!(!app.dirty());
|
|
let before = app.document.snapshot();
|
|
let revision = app.document.revision();
|
|
open(&mut app, &second_path);
|
|
assert!(!app.source_workspace.applied);
|
|
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
|
|
edit(&mut app, VALID);
|
|
assert!(app.apply_source(), "{}", app.status);
|
|
assert!(app.source_workspace.applied);
|
|
assert!(!app.source_dirty());
|
|
assert_eq!(app.document.bundle(), &before.bundle);
|
|
assert_eq!(app.project(), &before.decoded.project);
|
|
assert_eq!(app.document.revision(), revision);
|
|
assert!(
|
|
!app.document.can_undo(),
|
|
"Equal content creates no history step"
|
|
);
|
|
assert!(app.document.dirty());
|
|
assert_eq!(app.hosted_label(), "Unsaved changes");
|
|
assert_eq!(std::fs::read(&second_path).unwrap(), invalid.as_bytes());
|
|
assert_eq!(std::fs::read(&first_path).unwrap(), VALID.as_bytes());
|
|
let close = window::Id::unique();
|
|
let task = app.update(Message::Request(Pending::Close(close)));
|
|
assert!(iced_runtime::task::into_stream(task).is_none());
|
|
assert!(matches!(app.pending, Some(Pending::Close(id)) if id == close));
|
|
let _ = app.update(Message::Cancel);
|
|
save(&mut app, &second_path);
|
|
assert!(!app.dirty());
|
|
assert_eq!(app.hosted_label(), "All changes saved");
|
|
assert_eq!(std::fs::read(&second_path).unwrap(), VALID.as_bytes());
|
|
assert_eq!(std::fs::read(&first_path).unwrap(), VALID.as_bytes());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn repair_history_compares_to_new_disk_baseline_and_then_successful_save() {
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let path = directory
|
|
.path()
|
|
.canonicalize()
|
|
.unwrap()
|
|
.join("project.arcadia");
|
|
std::fs::write(&path, VALID).unwrap();
|
|
let mut app = App::with_project(Project::empty());
|
|
open(&mut app, &path);
|
|
let before = app.document.snapshot();
|
|
let invalid = format!("{VALID} unfinished {{");
|
|
std::fs::write(&path, &invalid).unwrap();
|
|
open(&mut app, &path);
|
|
let repaired = format!("{VALID}// Repaired rationale\r\n");
|
|
edit(&mut app, &repaired);
|
|
assert!(app.apply_source());
|
|
assert!(app.dirty());
|
|
let _ = app.update(Message::Undo);
|
|
assert_eq!(app.document.bundle(), &before.bundle);
|
|
assert!(
|
|
app.dirty(),
|
|
"Old saved bytes are not saved at the repair destination"
|
|
);
|
|
let _ = app.update(Message::Redo);
|
|
assert!(app.dirty());
|
|
assert_eq!(std::fs::read(&path).unwrap(), invalid.as_bytes());
|
|
|
|
std::fs::write(&path, "external edit").unwrap();
|
|
let failed = write_snapshot(
|
|
app.canvas_document,
|
|
app.document.revision(),
|
|
app.document.snapshot(),
|
|
path.clone(),
|
|
app.disk_project.clone(),
|
|
)
|
|
.unwrap_err();
|
|
let _ = app.update(Message::Saved(Err(failed)));
|
|
assert!(app.dirty());
|
|
assert_eq!(std::fs::read(&path).unwrap(), b"external edit");
|
|
std::fs::write(&path, &invalid).unwrap();
|
|
save(&mut app, &path);
|
|
assert!(!app.dirty());
|
|
let _ = app.update(Message::Undo);
|
|
assert!(app.dirty());
|
|
assert_eq!(app.document.bundle(), &before.bundle);
|
|
let _ = app.update(Message::Redo);
|
|
assert!(!app.dirty(), "Redo to the new saved snapshot is clean");
|
|
assert_eq!(std::fs::read(&path).unwrap(), repaired.as_bytes());
|
|
}
|
|
|
|
#[test]
|
|
fn valid_equal_content_at_distinct_destinations_stays_clean() {
|
|
let first = tempfile::tempdir().unwrap();
|
|
let second = tempfile::tempdir().unwrap();
|
|
let first_path = first.path().canonicalize().unwrap().join("project.arcadia");
|
|
let second_path = second
|
|
.path()
|
|
.canonicalize()
|
|
.unwrap()
|
|
.join("project.arcadia");
|
|
std::fs::write(&first_path, VALID).unwrap();
|
|
std::fs::write(&second_path, VALID).unwrap();
|
|
let mut app = App::with_project(Project::empty());
|
|
open(&mut app, &first_path);
|
|
let before = app.document.bundle().clone();
|
|
open(&mut app, &second_path);
|
|
assert_eq!(app.document.bundle(), &before);
|
|
assert_eq!(app.path.as_ref(), Some(&second_path));
|
|
assert!(!app.dirty());
|
|
assert_eq!(app.hosted_label(), "All changes saved");
|
|
}
|
|
|
|
#[test]
|
|
fn recovering_applied_repair_rebinds_rolled_back_disk_bytes_without_changing_model() {
|
|
let first = tempfile::tempdir().unwrap();
|
|
let second = tempfile::tempdir().unwrap();
|
|
let first_path = first.path().canonicalize().unwrap().join("project.arcadia");
|
|
let root = second.path().canonicalize().unwrap();
|
|
let path = root.join("project.arcadia");
|
|
let before = VALID.replace("Repair 救援", "Before 救援");
|
|
std::fs::write(&first_path, VALID).unwrap();
|
|
std::fs::write(&path, VALID).unwrap();
|
|
let journal = root.join(".arcadia-save-project.arcadia.journal");
|
|
std::fs::create_dir(&journal).unwrap();
|
|
std::fs::write(
|
|
journal.join("journal.json"),
|
|
br#"{"version":1,"entry":"project.arcadia","files":[{"path":"project.arcadia","before":true,"write":true}]}"#,
|
|
)
|
|
.unwrap();
|
|
std::fs::write(journal.join("0.before"), &before).unwrap();
|
|
std::fs::write(journal.join("0.after"), VALID).unwrap();
|
|
let mut app = App::with_project(Project::empty());
|
|
open(&mut app, &first_path);
|
|
let accepted = app.document.snapshot();
|
|
open(&mut app, &path);
|
|
assert_eq!(app.pending_recovery.as_ref(), Some(&path));
|
|
assert!(!app.source_workspace.applied);
|
|
assert!(app.apply_source());
|
|
assert!(
|
|
!app.dirty(),
|
|
"Current bytes still match disk before rollback"
|
|
);
|
|
let _ = app.update(Message::SourceRecovered(recover_snapshot(path.clone())));
|
|
assert!(app.pending_recovery.is_none());
|
|
assert!(!journal.exists());
|
|
assert_eq!(std::fs::read(&path).unwrap(), before.as_bytes());
|
|
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
|
assert!(!app.source_dirty());
|
|
assert!(app.dirty(), "Recovery changed disk, not the accepted model");
|
|
assert_eq!(app.hosted_label(), "Unsaved changes");
|
|
save(&mut app, &path);
|
|
assert!(!app.dirty());
|
|
assert_eq!(std::fs::read(&path).unwrap(), VALID.as_bytes());
|
|
}
|
|
|
|
#[test]
|
|
fn recovery_missing_entry_or_failed_reload_preserves_current_destination_save_authority() {
|
|
for (current_destination, failed_reload) in
|
|
[(true, false), (false, false), (true, true), (false, true)]
|
|
{
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let root = directory.path().canonicalize().unwrap();
|
|
let path = root.join("project.arcadia");
|
|
std::fs::write(&path, VALID).unwrap();
|
|
let mut app = App::with_project(Project::empty());
|
|
open(&mut app, &path);
|
|
let accepted = app.document.snapshot();
|
|
let journal = root.join(".arcadia-save-project.arcadia.journal");
|
|
std::fs::create_dir(&journal).unwrap();
|
|
std::fs::write(
|
|
journal.join("journal.json"),
|
|
br#"{"version":1,"entry":"project.arcadia","files":[{"path":"project.arcadia","before":false,"write":true}]}"#,
|
|
)
|
|
.unwrap();
|
|
std::fs::write(journal.join("0.after"), VALID).unwrap();
|
|
open(&mut app, &path);
|
|
assert!(app.apply_source());
|
|
assert!(!app.dirty());
|
|
let destination = tempfile::tempdir().unwrap();
|
|
let saved_elsewhere = destination
|
|
.path()
|
|
.canonicalize()
|
|
.unwrap()
|
|
.join("project.arcadia");
|
|
if !current_destination {
|
|
save(&mut app, &saved_elsewhere);
|
|
assert_eq!(app.path.as_ref(), Some(&saved_elsewhere));
|
|
assert_eq!(app.pending_recovery.as_ref(), Some(&path));
|
|
}
|
|
let mut receipt = recover_snapshot(path.clone()).unwrap();
|
|
assert!(
|
|
receipt.disk.as_ref().unwrap().bundle.files.is_empty(),
|
|
"Missing entry reloads as an empty repair workspace"
|
|
);
|
|
if failed_reload {
|
|
// Separately exercise the adapter's successful-recovery/failed-read
|
|
// boundary; first-save rollback itself does not cause a read error.
|
|
receipt.disk = Err("Injected post-recovery reload failure".into());
|
|
}
|
|
let _ = app.update(Message::SourceRecovered(Ok(receipt)));
|
|
assert!(!path.exists());
|
|
assert!(!journal.exists());
|
|
assert!(app.pending_recovery.is_none());
|
|
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
|
assert_eq!(app.dirty(), current_destination);
|
|
if current_destination {
|
|
let _ = app.update(Message::Request(Pending::New));
|
|
assert!(matches!(app.pending, Some(Pending::New)));
|
|
} else {
|
|
assert_eq!(app.path.as_ref(), Some(&saved_elsewhere));
|
|
assert_eq!(std::fs::read(&saved_elsewhere).unwrap(), VALID.as_bytes());
|
|
}
|
|
}
|
|
}
|