use super::*; fn saved_multifile_app(directory: &std::path::Path) -> (App, Id, PathBuf) { let mut project = Project::empty(); let activity = project .add_element(Kind::Activity, Layer::Oa, "Assess incident", None) .unwrap(); let mut app = App::with_project(project); let path = directory.join("project.arcadia"); complete_save(&mut app, path.clone()); assert!(app.document.bundle().files.len() > 1); (app, activity, path) } fn rejected_save_preserves_editor(app: &mut App, path: PathBuf) -> String { let snapshot = app.document.snapshot(); let revision = app.document.revision(); let undo = app.document.can_undo(); let redo = app.document.can_redo(); let draft = app.source_workspace.text().to_owned(); let original_path = app.path.clone(); let dirty = app.dirty(); let result = write_snapshot( app.canvas_document, revision, snapshot.clone(), path, app.disk_project.clone(), ); let error = match result { Err(error) => error, Ok(_) => panic!("Expected collision refusal"), }; let _ = app.update(Message::Saved(Err(error.clone()))); assert!(Arc::ptr_eq(&snapshot, &app.document.snapshot())); assert_eq!(app.document.revision(), revision); assert_eq!(app.document.can_undo(), undo); assert_eq!(app.document.can_redo(), redo); assert_eq!(app.source_workspace.text(), draft); assert_eq!(app.path, original_path); assert_eq!(app.dirty(), dirty); error.message } #[test] fn same_root_new_entry_collision_preserves_files_draft_and_history() { let directory = tempfile::tempdir().unwrap(); let (mut app, activity, original) = saved_multifile_app(directory.path()); let bytes: Vec<_> = app .document .bundle() .files .keys() .map(|p| (p.clone(), std::fs::read(directory.path().join(p)).unwrap())) .collect(); app.execute(Command::Rename(activity, "Review incident".into())); app.execute(Command::Rename(activity, "Evaluate incident".into())); let _ = app.update(Message::Undo); assert!(app.document.can_undo() && app.document.can_redo()); let cursor = app.source_workspace.files[&app.source_workspace.entry].cursor; app.source_workspace.edit( format!("{}\n// retained draft", app.source_workspace.text()), cursor, ); let destination = directory.path().join("renamed.arcadia"); let error = rejected_save_preserves_editor(&mut app, destination.clone()); assert!(error.contains("already exist") && error.contains("Save All")); assert!(!error.contains("changed outside")); assert!(!destination.exists()); assert_eq!(app.path, Some(original)); for (path, expected) in bytes { assert_eq!( std::fs::read(directory.path().join(path)).unwrap(), expected ); } } #[test] fn fresh_root_existing_include_is_protected_before_any_bundle_write() { let original = tempfile::tempdir().unwrap(); let destination = tempfile::tempdir().unwrap(); let (mut app, activity, _) = saved_multifile_app(original.path()); app.execute(Command::Rename(activity, "Review incident".into())); let fragment = app.document.compiled().index.declarations[&activity] .path .clone(); let collision = destination.path().join(&fragment); std::fs::create_dir_all(collision.parent().unwrap()).unwrap(); std::fs::write(&collision, b"// independent destination work").unwrap(); let target = destination.path().join("copy.arcadia"); let error = rejected_save_preserves_editor(&mut app, target.clone()); assert!(error.contains(&fragment) && error.contains("fresh directory")); assert!(!error.contains("changed outside")); assert!(!target.exists()); for path in app.document.bundle().files.keys() { let file = destination.path().join(path); if path == &fragment { assert_eq!( std::fs::read(file).unwrap(), b"// independent destination work" ); } else { assert!(!file.exists(), "Unexpected write: {path}"); } } } #[test] fn observed_dependency_change_remains_external_conflict_and_normal_saves_work() { let original = tempfile::tempdir().unwrap(); let fresh = tempfile::tempdir().unwrap(); let (mut app, activity, path) = saved_multifile_app(original.path()); app.execute(Command::Rename(activity, "Review incident".into())); complete_save(&mut app, path.clone()); assert!(!app.dirty()); let fragment = app.document.compiled().index.declarations[&activity] .path .clone(); let entry_before = std::fs::read(&path).unwrap(); let original_fragment = std::fs::read(original.path().join(&fragment)).unwrap(); std::fs::write(original.path().join(&fragment), b"// external edit").unwrap(); app.execute(Command::Rename(activity, "Evaluate incident".into())); let error = rejected_save_preserves_editor(&mut app, path.clone()); assert!(error.contains("changed outside") && error.contains(&fragment)); assert!(!error.contains("fresh directory")); assert_eq!(std::fs::read(&path).unwrap(), entry_before); assert_eq!( std::fs::read(original.path().join(&fragment)).unwrap(), b"// external edit" ); // Restore the observed bytes, then exercise the legitimate existing Save All. std::fs::write(original.path().join(&fragment), original_fragment).unwrap(); complete_save(&mut app, path); assert!(!app.dirty()); let model = app.project().clone(); let target = fresh.path().join("copy.arcadia"); complete_save(&mut app, target.clone()); let saved = source_files::load_project(&target, LIMITS).unwrap(); let compiled = source_project::compile_bundle(&saved.bundle, LIMITS).unwrap(); assert_eq!(compiled.decoded.project, model); assert_eq!(&saved.bundle, app.document.bundle()); assert!(!app.dirty()); } fn complete_save(app: &mut App, path: PathBuf) { let receipt = write_snapshot( app.canvas_document, app.document.revision(), app.document.snapshot(), path, app.disk_project.clone(), ) .unwrap(); let _ = app.update(Message::Saved(Ok(Some(receipt)))); } #[test] fn save_as_receipt_then_one_undo_restores_meaning_and_saving_preserves_redo() { let directory = tempfile::tempdir().unwrap(); let path = directory.path().join("semantic-accepted.arcadia"); let mut app = App::with_project(Project::empty()); app.document.mark_recovered(); let original = app.document.bundle().files[&app.document.bundle().entry].clone(); app.execute(Command::Rename( app.project().id, "Accepted semantic change".into(), )); let after = app.document.bundle().files[&app.document.bundle().entry].clone(); complete_save(&mut app, path.clone()); assert!(!app.dirty()); assert_eq!(app.path.as_ref(), Some(&path)); assert_eq!(std::fs::read_to_string(&path).unwrap(), after); let _ = app.update(Message::Undo); assert_eq!(app.document.bundle().entry, "semantic-accepted.arcadia"); assert_eq!( app.document.bundle().files[&app.document.bundle().entry], original ); assert!( !app.document.can_undo(), "Save as did not insert a history step" ); assert!(app.document.can_redo()); complete_save(&mut app, path.clone()); assert_eq!(std::fs::read_to_string(&path).unwrap(), original); assert!( app.document.can_redo(), "Saving an undone state preserves the future" ); let _ = app.update(Message::Redo); assert_eq!( app.document.bundle().files[&app.document.bundle().entry], after ); assert!(app.dirty()); assert_eq!( source_project::compile_bundle(app.document.bundle(), LIMITS) .unwrap() .decoded .project, *app.project() ); } #[test] fn save_as_preflight_rejects_historical_collision_before_starting_io() { let directory = tempfile::tempdir().unwrap(); let path = directory.path().join("taken.arcadia"); let mut app = App::with_project(Project::empty()); let base = app.document.bundle().clone(); let mut included = base.clone(); let text = included.files.get_mut(&included.entry).unwrap(); text.insert_str(text.rfind('}').unwrap(), "\n include \"taken.arcadia\";\n"); included .files .insert("taken.arcadia".into(), "// retained\n".into()); app.document.apply_bundle(included).unwrap(); app.document.apply_bundle(base).unwrap(); app.sync_source(); let before = app.document.snapshot(); let task = app.save_to(path.clone()); assert!(iced_runtime::task::into_stream(task).is_none()); assert!(!path.exists()); assert!(!app.busy); assert!(app.status.contains("Undo/Redo")); assert!(Arc::ptr_eq(&before, &app.document.snapshot())); let json = app.save_to(directory.path().join("taken.json")); assert!( iced_runtime::task::into_stream(json).is_some(), "JSON exports do not relocate source entries" ); } #[test] fn save_as_late_receipt_preserves_newer_raw_source_draft_and_destination() { let directory = tempfile::tempdir().unwrap(); let mut app = App::with_project(Project::empty()); let entry = app.document.bundle().entry.clone(); let receipt = write_snapshot( app.canvas_document, app.document.revision(), app.document.snapshot(), directory.path().join("accepted.arcadia"), None, ) .unwrap(); let raw = format!("{}\nunfinished {{", app.source_workspace.text()); let cursor = app.source_workspace.files[&entry].cursor; app.source_workspace.edit(raw.clone(), cursor); let _ = app.update(Message::Saved(Ok(Some(receipt)))); assert_eq!(app.source_workspace.text(), raw); assert_eq!(app.document.bundle().entry, entry); assert!(app.path.is_none()); assert!(app.dirty()); assert!(app.status.contains("Newer edits")); }