Save unverified allocation and repair work at user-requested pause
This commit is contained in:
@@ -132,7 +132,7 @@ fn mounted_oa_sa_ordered_incoming_between_outgoing_are_exact_single_history_step
|
||||
let mut f = fixture(layer);
|
||||
let original = f.app.project().clone();
|
||||
let original_source = f.app.document.bundle().clone();
|
||||
drag(&mut f, screen(xy(160., 140.)));
|
||||
drag(&mut f, screen(xy(160., 190.)));
|
||||
assert_eq!(f.app.document.revision(), 1, "{}", f.app.status);
|
||||
let relation = f
|
||||
.app
|
||||
@@ -150,7 +150,7 @@ fn mounted_oa_sa_ordered_incoming_between_outgoing_are_exact_single_history_step
|
||||
assert!(!f.app.document.can_undo());
|
||||
let _ = f.app.update(Message::Redo);
|
||||
assert_eq!(f.app.document.bundle(), &after_in);
|
||||
drag(&mut f, screen(xy(960., 140.)));
|
||||
drag(&mut f, screen(xy(960., 190.)));
|
||||
let reassigned = &f.app.project().relations[&relation.id];
|
||||
assert_eq!(reassigned.source(), f.b, "{}", f.app.status);
|
||||
assert_eq!(reassigned.name, relation.name);
|
||||
@@ -208,7 +208,7 @@ fn mounted_dirty_property_press_does_not_select_apply_or_destroy_draft() {
|
||||
let input = &f.app.draft.as_ref().unwrap().inputs["/name"];
|
||||
let raw = input.raw.clone();
|
||||
let cursor = input.editor.cursor();
|
||||
let events = batch(&f.app, start(&f), screen(xy(160., 140.)));
|
||||
let events = batch(&f.app, start(&f), screen(xy(160., 190.)));
|
||||
assert!(matches!(
|
||||
events.first(),
|
||||
Some(Message::WorkDrag(Event::Refused(..)))
|
||||
@@ -256,7 +256,7 @@ fn source_and_modal_admission_reject_prebuilt_clean_press_without_selecting() {
|
||||
let _ = f.app.update(Message::WorkDrag(Event::Release(
|
||||
p.stamp,
|
||||
p.token,
|
||||
Some(screen(xy(160., 140.))),
|
||||
Some(screen(xy(160., 190.))),
|
||||
)));
|
||||
assert_eq!(f.app.selected, selection);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
@@ -302,7 +302,7 @@ fn modal_roundtrip_equal_fit_undo_resize_and_focus_retire_press_before_replay()
|
||||
let _ = f.app.update(Message::WorkDrag(Event::Release(
|
||||
p.stamp,
|
||||
p.token,
|
||||
Some(screen(xy(160., 140.))),
|
||||
Some(screen(xy(160., 190.))),
|
||||
)));
|
||||
assert_eq!(f.app.document.bundle(), &before);
|
||||
assert!(!f.app.document.can_undo());
|
||||
@@ -321,7 +321,7 @@ fn stale_packets_do_not_cancel_a_newer_token_and_each_release_is_consumed_once()
|
||||
let _ = f.app.update(Message::WorkDrag(Event::Preview(
|
||||
old.stamp,
|
||||
old.token,
|
||||
Some(screen(xy(160., 140.))),
|
||||
Some(screen(xy(160., 190.))),
|
||||
)));
|
||||
assert_eq!(
|
||||
f.app.allocation_drag.active.as_ref().unwrap().press.token,
|
||||
@@ -330,7 +330,7 @@ fn stale_packets_do_not_cancel_a_newer_token_and_each_release_is_consumed_once()
|
||||
let release = Message::WorkDrag(Event::Release(
|
||||
new.stamp,
|
||||
new.token,
|
||||
Some(screen(xy(160., 140.))),
|
||||
Some(screen(xy(160., 190.))),
|
||||
));
|
||||
let _ = f.app.update(release.clone());
|
||||
let revision = f.app.document.revision();
|
||||
@@ -370,7 +370,7 @@ fn arrange_is_one_gesture_and_snapped_noop_preserves_redo_and_source() {
|
||||
#[test]
|
||||
fn outside_release_is_cancel_and_does_not_remove_an_assignment() {
|
||||
let mut f = fixture(Layer::Oa);
|
||||
drag(&mut f, screen(xy(160., 140.)));
|
||||
drag(&mut f, screen(xy(160., 190.)));
|
||||
let before = f.app.document.bundle().clone();
|
||||
let revision = f.app.document.revision();
|
||||
drag(
|
||||
@@ -386,8 +386,8 @@ fn candidate_semantics_are_cached_per_destination_with_stable_ids_and_own_index(
|
||||
let mut f = fixture(Layer::Oa);
|
||||
let p = press(&f);
|
||||
let _ = f.app.update(Message::WorkDrag(Event::Press(p.clone())));
|
||||
let a = screen(xy(160., 140.));
|
||||
let b = screen(xy(960., 140.));
|
||||
let a = screen(xy(160., 190.));
|
||||
let b = screen(xy(960., 190.));
|
||||
let _ = f
|
||||
.app
|
||||
.update(Message::WorkDrag(Event::Preview(p.stamp, p.token, Some(a))));
|
||||
@@ -508,8 +508,8 @@ fn frozen_targets_use_half_open_semantic_priority_and_refuse_incomparable_overla
|
||||
fn release_uses_ordered_native_position_before_later_motion_in_same_batch() {
|
||||
let mut f = fixture(Layer::Oa);
|
||||
let from = start(&f);
|
||||
let a = screen(xy(160., 140.));
|
||||
let b = screen(xy(960., 140.));
|
||||
let a = screen(xy(160., 190.));
|
||||
let b = screen(xy(960., 190.));
|
||||
let mut state = CanvasState::default();
|
||||
let canvas = canvas(&f.app);
|
||||
let events: Vec<_> = [
|
||||
@@ -603,7 +603,7 @@ fn source_refusal_is_atomic_and_a_new_gesture_can_retry_against_live_revision()
|
||||
f.app.document = crate::document::Document::from_bundle(before.clone(), limits).unwrap();
|
||||
f.app.sync();
|
||||
let snapshot = f.app.document.snapshot();
|
||||
drag(&mut f, screen(xy(160., 140.)));
|
||||
drag(&mut f, screen(xy(160., 190.)));
|
||||
assert!(Arc::ptr_eq(&snapshot, &f.app.document.snapshot()));
|
||||
assert_eq!(f.app.document.bundle(), &before);
|
||||
assert!(!f.app.document.can_undo());
|
||||
@@ -611,7 +611,7 @@ fn source_refusal_is_atomic_and_a_new_gesture_can_retry_against_live_revision()
|
||||
assert!(f.app.allocation_drag.active.is_none());
|
||||
f.app.document = crate::document::Document::from_bundle(before, Default::default()).unwrap();
|
||||
f.app.sync();
|
||||
drag(&mut f, screen(xy(160., 140.)));
|
||||
drag(&mut f, screen(xy(160., 190.)));
|
||||
assert_eq!(
|
||||
validation::effective_performer(f.app.project(), f.work),
|
||||
Some(f.a),
|
||||
@@ -656,7 +656,7 @@ fn mounted_role_only_outgoing_refusal_retains_placement_and_same_performer_creat
|
||||
.find(|t| t.element == f.a)
|
||||
.unwrap()
|
||||
.region;
|
||||
drag(&mut f, screen(xy(target.x + 45., target.y + 40.)));
|
||||
drag(&mut f, screen(xy(target.x + 45., target.y + 100.)));
|
||||
assert_eq!(f.app.document.revision(), 1, "{}", f.app.status);
|
||||
assert_eq!(f.app.project().relations, relations);
|
||||
assert!(
|
||||
@@ -686,7 +686,7 @@ fn mounted_uniform_parent_moves_hidden_placements_and_assigns_only_leaves() {
|
||||
.collapsed = true;
|
||||
replace(&mut f, project);
|
||||
let before = f.app.project().diagrams[&f.diagram].nodes.clone();
|
||||
drag(&mut f, screen(xy(960., 140.)));
|
||||
drag(&mut f, screen(xy(960., 190.)));
|
||||
assert_eq!(f.app.document.revision(), 1, "{}", f.app.status);
|
||||
let after = &f.app.project().diagrams[&f.diagram].nodes;
|
||||
let dx = after[&f.node].position.x - before[&f.node].position.x;
|
||||
@@ -805,7 +805,7 @@ fn pending_behavior_draft_refuses_actual_work_press_and_equal_document_replaceme
|
||||
crate::behavior::Event::Creation(crate::behavior::creation::Event::Open(Layer::Oa)),
|
||||
);
|
||||
assert!(f.app.behavior.has_pending());
|
||||
let events = batch(&f.app, start(&f), screen(xy(160., 140.)));
|
||||
let events = batch(&f.app, start(&f), screen(xy(160., 190.)));
|
||||
assert!(matches!(
|
||||
events.first(),
|
||||
Some(Message::WorkDrag(Event::Refused(..)))
|
||||
@@ -823,7 +823,7 @@ fn pending_behavior_draft_refuses_actual_work_press_and_equal_document_replaceme
|
||||
let _ = f.app.update(Message::WorkDrag(Event::Release(
|
||||
p.stamp,
|
||||
p.token,
|
||||
Some(screen(xy(160., 140.))),
|
||||
Some(screen(xy(160., 190.))),
|
||||
)));
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
assert!(!f.app.document.can_undo());
|
||||
@@ -854,7 +854,7 @@ fn actual_canvas_origin_change_cancels_held_work_before_any_legacy_release() {
|
||||
&mut state,
|
||||
&canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
|
||||
moved_bounds,
|
||||
mouse::Cursor::Available(screen(xy(160., 140.))),
|
||||
mouse::Cursor::Available(screen(xy(160., 190.))),
|
||||
)
|
||||
.and_then(|a| a.into_inner().0)
|
||||
.unwrap();
|
||||
@@ -953,3 +953,92 @@ fn toolbar_status_leaves_transformed_top_edge_frozen_target_label_readable() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oa_sa_label_overlap_refuses_preview_release_and_retries_without_partial_source_or_history() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
for (zoom, pan) in [
|
||||
(1., Vector::ZERO),
|
||||
(0.75, Vector::new(37., -15.)),
|
||||
(1.25, Vector::new(-30., 23.)),
|
||||
] {
|
||||
let mut f = fixture(layer);
|
||||
let mut project = f.app.project().clone();
|
||||
project.elements.get_mut(&f.a).unwrap().name = "Receiver".into();
|
||||
replace(&mut f, project);
|
||||
// Refusals must also preserve an existing redo branch.
|
||||
let _ = f.app.update(Message::Command(Command::Rename(f.work, "Temporary".into())));
|
||||
let _ = f.app.update(Message::Undo);
|
||||
f.app.zoom = zoom;
|
||||
f.app.pan = pan;
|
||||
f.app.fit_to_view = false;
|
||||
let screen = |world: Position| Point::new(
|
||||
bounds().x + world.x * zoom + pan.x,
|
||||
bounds().y + world.y * zoom + pan.y,
|
||||
);
|
||||
let from = screen(xy(140., 535.));
|
||||
let overlap = screen(xy(160., 140.));
|
||||
let clear = screen(xy(160., 190.));
|
||||
let accepted = f.app.project().clone();
|
||||
let source = f.app.document.bundle().clone();
|
||||
let snapshot = f.app.document.snapshot();
|
||||
let revision = f.app.document.revision();
|
||||
let mut events = batch(&f.app, from, overlap);
|
||||
let release = events.pop().unwrap();
|
||||
send(&mut f.app, events);
|
||||
assert!(f.app.allocation_drag.preview.is_none());
|
||||
let feedback = f.app.allocation_drag.feedback.as_ref().unwrap();
|
||||
assert!(feedback.refused, "{}", feedback.text);
|
||||
assert!(feedback.text.contains("Aim below or beside"));
|
||||
assert_eq!(feedback.press.frozen.targets[feedback.targets[0]].element, f.a);
|
||||
assert_eq!(f.app.project(), &accepted);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
assert!(Arc::ptr_eq(&snapshot, &f.app.document.snapshot()));
|
||||
assert!(!f.app.document.can_undo());
|
||||
assert!(f.app.document.can_redo());
|
||||
let _ = f.app.update(release);
|
||||
assert!(f.app.allocation_drag.active.is_none());
|
||||
assert!(f.app.allocation_drag.preview.is_none());
|
||||
assert_eq!(f.app.project(), &accepted);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
assert_eq!(f.app.document.revision(), revision);
|
||||
assert!(Arc::ptr_eq(&snapshot, &f.app.document.snapshot()));
|
||||
assert!(!f.app.document.can_undo());
|
||||
assert!(f.app.document.can_redo());
|
||||
|
||||
// In a new gesture, moving out of the collision reuses its semantic
|
||||
// candidate and stable allocation identity; only geometry changes.
|
||||
let mut events = batch(&f.app, from, overlap);
|
||||
events.pop();
|
||||
send(&mut f.app, events);
|
||||
let active = f.app.allocation_drag.active.as_ref().unwrap();
|
||||
let p = active.press.clone();
|
||||
let semantic = active.cache[0].1.as_ref().unwrap().clone();
|
||||
let _ = f.app.update(Message::WorkDrag(Event::Preview(p.stamp, p.token, Some(clear))));
|
||||
let preview = f.app.allocation_drag.preview.as_ref().expect(&f.app.status);
|
||||
assert!(Arc::ptr_eq(&semantic, &preview.semantic));
|
||||
assert_eq!(preview.placement.diagram.nodes[&f.node].position, xy(120., 160.));
|
||||
assert_eq!(preview.placement.view, projection::project_diagram_with_index(
|
||||
semantic.project(), &preview.placement.diagram, semantic.index(),
|
||||
));
|
||||
let mut expected = semantic.project().clone();
|
||||
expected.diagrams.insert(f.diagram, preview.placement.diagram.clone());
|
||||
assert_eq!(f.app.project(), &accepted);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
let _ = f.app.update(Message::WorkDrag(Event::Release(p.stamp, p.token, Some(clear))));
|
||||
assert_eq!(f.app.project(), &expected, "{}", f.app.status);
|
||||
assert_eq!(f.app.document.revision(), revision + 1);
|
||||
assert!(f.app.document.can_undo());
|
||||
assert!(!f.app.document.can_redo());
|
||||
let after = f.app.document.bundle().clone();
|
||||
assert_eq!(oa_core::source_project::compile_bundle(&after, Default::default()).unwrap().decoded.project, expected);
|
||||
let _ = f.app.update(Message::Undo);
|
||||
assert_eq!(f.app.project(), &accepted);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
assert!(!f.app.document.can_undo());
|
||||
let _ = f.app.update(Message::Redo);
|
||||
assert_eq!(f.app.project(), &expected);
|
||||
assert_eq!(f.app.document.bundle(), &after);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user