86a4cfd56e
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
173 lines
8.7 KiB
Rust
173 lines
8.7 KiB
Rust
//! Integration tests of the open buffers and of the watcher events applied to a
|
|
//! session (SPEC 4.2).
|
|
|
|
mod common;
|
|
|
|
use std::ops::Range;
|
|
|
|
use common::{TempVault, on_disk, seed, temp_vault};
|
|
use notescraft_engine::{Applied, Error, Session};
|
|
use notescraft_vault::VaultEvent;
|
|
|
|
/// A vault with one note, ready to be edited.
|
|
fn one_note(name: &str) -> (TempVault, Session) {
|
|
let vault = temp_vault(name);
|
|
seed(vault.path(), &[("note.md", "hello\n")]);
|
|
let session = Session::open(vault.path()).expect("open the vault");
|
|
(vault, session)
|
|
}
|
|
|
|
/// Opening, editing, saving and closing, and what the disk looks like after.
|
|
#[test]
|
|
fn a_buffer_opens_edits_saves_and_closes() {
|
|
let (vault, mut session) = one_note("buffer");
|
|
let buffer = session.open_buffer("note.md").expect("open");
|
|
assert_eq!(buffer.text(), "hello\n");
|
|
assert!(!buffer.is_dirty());
|
|
assert!(session.buffers().contains_key("note.md"));
|
|
|
|
let buffer = session.buffer_mut("note.md").expect("the buffer is open");
|
|
buffer.edit(0..5, "HELLO").expect("edit");
|
|
assert!(buffer.is_dirty());
|
|
assert_eq!(on_disk(vault.path(), "note.md"), "hello\n", "an edit is not a save");
|
|
|
|
assert!(session.save_buffer("note.md").expect("save"), "the bytes changed");
|
|
assert_eq!(on_disk(vault.path(), "note.md"), "HELLO\n");
|
|
assert!(!session.buffer("note.md").expect("still open").is_dirty());
|
|
assert!(!session.save_buffer("note.md").expect("an unchanged save"), "nothing to write");
|
|
|
|
session.close_buffer("note.md", false).expect("close a clean buffer");
|
|
assert!(session.buffer("note.md").is_none());
|
|
assert_eq!(session.buffers().len(), 0);
|
|
assert_eq!(session.open_buffer("note.md").expect("reopen").text(), "HELLO\n");
|
|
}
|
|
|
|
/// Undo and redo walk the stack of an open buffer.
|
|
#[test]
|
|
fn undo_and_redo_walk_the_buffer_stack() {
|
|
let (_vault, mut session) = one_note("undo");
|
|
session.open_buffer("note.md").expect("open");
|
|
session.buffer_mut("note.md").expect("open").edit(0..0, "a ").expect("insert");
|
|
session.buffer_mut("note.md").expect("open").edit(0..1, "b").expect("replace");
|
|
assert_eq!(session.buffer("note.md").expect("open").text(), "b hello\n");
|
|
|
|
assert!(session.buffer_mut("note.md").expect("open").undo().expect("undo"));
|
|
assert_eq!(session.buffer("note.md").expect("open").text(), "a hello\n");
|
|
assert!(session.buffer_mut("note.md").expect("open").redo().expect("redo"));
|
|
assert_eq!(session.buffer("note.md").expect("open").text(), "b hello\n");
|
|
assert!(session.buffer_mut("note.md").expect("open").undo().expect("undo"));
|
|
assert!(session.buffer_mut("note.md").expect("open").undo().expect("undo"));
|
|
assert!(!session.buffer_mut("note.md").expect("open").undo().expect("undo"), "the stack is empty");
|
|
assert!(!session.buffer("note.md").expect("open").is_dirty(), "back to the saved bytes");
|
|
assert!(session.buffer_mut("note.md").expect("open").redo().expect("redo"));
|
|
assert_eq!(session.buffer("note.md").expect("open").text(), "a hello\n");
|
|
}
|
|
|
|
/// A dirty buffer is only closed when the caller says so.
|
|
#[test]
|
|
fn closing_a_dirty_buffer_needs_force() {
|
|
let (_vault, mut session) = one_note("dirty");
|
|
session.open_buffer("note.md").expect("open");
|
|
session.buffer_mut("note.md").expect("open").edit(0..1, "H").expect("edit");
|
|
|
|
assert_eq!(session.close_buffer("note.md", false), Err(Error::Dirty(String::from("note.md"))));
|
|
assert!(session.buffer("note.md").is_some());
|
|
assert_eq!(session.close_buffer("note.md", true), Ok(()));
|
|
assert_eq!(session.buffers().len(), 0);
|
|
assert_eq!(session.close_buffer("note.md", true), Err(Error::NoBuffer(String::from("note.md"))));
|
|
assert!(session.buffer_mut("note.md").is_err(), "there is no buffer to edit");
|
|
}
|
|
|
|
/// Bad ranges and missing buffers are errors, never a panic.
|
|
#[test]
|
|
fn bad_buffer_operations_are_errors() {
|
|
let (_vault, mut session) = one_note("buffer-errors");
|
|
session.open_buffer("note.md").expect("open");
|
|
let buffer = session.buffer_mut("note.md").expect("open");
|
|
let len = buffer.text().len();
|
|
|
|
for range in [len..len.saturating_add(1), 0..len.saturating_add(9), Range { start: 4, end: 1 }] {
|
|
assert!(matches!(buffer.edit(range.clone(), "x"), Err(Error::InvalidRange(_))), "{range:?} must be refused");
|
|
}
|
|
assert_eq!(buffer.text(), "hello\n", "a refused edit changes nothing");
|
|
assert_eq!(session.save_buffer("nope.md"), Err(Error::NoBuffer(String::from("nope.md"))));
|
|
assert_eq!(session.open_buffer("nope.md").expect_err("missing"), Error::NotFound(String::from("`nope.md` does not exist")));
|
|
assert_eq!(session.open_buffer("notes.txt").expect_err("not a note"), Error::InvalidPath(String::from("`notes.txt` is not a note: it must end in `.md`")));
|
|
}
|
|
|
|
/// A clean buffer reloads when the file changes outside the app.
|
|
#[test]
|
|
fn an_external_change_reloads_a_clean_buffer() {
|
|
let (vault, mut session) = one_note("external-clean");
|
|
session.open_buffer("note.md").expect("open");
|
|
seed(vault.path(), &[("note.md", "# changed\n")]);
|
|
|
|
let applied = session.apply_event(VaultEvent::Modified(String::from("note.md"))).expect("apply");
|
|
assert_eq!(applied, Applied::Indexed);
|
|
assert_eq!(session.buffer("note.md").expect("open").text(), "# changed\n");
|
|
assert!(!session.buffer("note.md").expect("open").is_dirty());
|
|
assert_eq!(session.index().note("note.md").map(|note| note.title.as_str()), Some("note"));
|
|
assert_eq!(session.conflicts(), &[] as &[String]);
|
|
}
|
|
|
|
/// A change landing on a dirty buffer is a conflict, and nothing is overwritten.
|
|
#[test]
|
|
fn an_external_change_on_a_dirty_buffer_is_a_conflict() {
|
|
let (vault, mut session) = one_note("external-dirty");
|
|
session.open_buffer("note.md").expect("open");
|
|
session.buffer_mut("note.md").expect("open").edit(0..5, "MINE").expect("edit");
|
|
seed(vault.path(), &[("note.md", "THEIRS\n")]);
|
|
|
|
let applied = session.apply_event(VaultEvent::Modified(String::from("note.md"))).expect("apply");
|
|
assert_eq!(applied, Applied::Conflict);
|
|
assert_eq!(session.conflicts(), &[String::from("note.md")]);
|
|
assert_eq!(session.buffer("note.md").expect("open").text(), "MINE\n");
|
|
assert_eq!(on_disk(vault.path(), "note.md"), "THEIRS\n");
|
|
assert_eq!(session.index().note("note.md").map(|note| note.tags.len()), Some(0), "the index still has the old content");
|
|
|
|
// The same event twice records one conflict, not two.
|
|
session.apply_event(VaultEvent::Modified(String::from("note.md"))).expect("apply again");
|
|
assert_eq!(session.conflicts().len(), 1);
|
|
}
|
|
|
|
/// Created, removed and renamed events keep the index and the buffers in step.
|
|
#[test]
|
|
fn created_removed_and_renamed_events_are_applied() {
|
|
let (vault, mut session) = one_note("external-events");
|
|
seed(vault.path(), &[("moved.md", "moved body\n")]);
|
|
assert_eq!(session.apply_event(VaultEvent::Created(String::from("moved.md"))).expect("created"), Applied::Indexed);
|
|
assert_eq!(session.index().len(), 2);
|
|
|
|
std::fs::rename(vault.path().join("moved.md"), vault.path().join("other.md")).expect("the file moved on disk");
|
|
assert_eq!(session.apply_event(VaultEvent::Renamed { from: String::from("moved.md"), to: String::from("other.md") }).expect("renamed"), Applied::Indexed);
|
|
assert_eq!(session.index().notes(), vec!["note.md", "other.md"]);
|
|
assert!(session.index().note("moved.md").is_none());
|
|
|
|
assert_eq!(session.apply_event(VaultEvent::Removed(String::from("other.md"))).expect("removed"), Applied::Forgotten);
|
|
assert_eq!(session.index().notes(), vec!["note.md"]);
|
|
|
|
// An attachment is registered on create and forgotten on removal.
|
|
seed(vault.path(), &[("img.png", "x")]);
|
|
assert_eq!(session.apply_event(VaultEvent::Created(String::from("img.png"))).expect("attachment"), Applied::Indexed);
|
|
assert_eq!(session.index().files(), vec!["img.png"]);
|
|
assert_eq!(session.apply_event(VaultEvent::Removed(String::from("img.png"))).expect("attachment gone"), Applied::Forgotten);
|
|
assert!(session.index().files().is_empty(), "removed attachment is forgotten");
|
|
assert!(vault.path().join("img.png").exists());
|
|
}
|
|
|
|
/// A rename of a note with an open buffer moves the buffer with it.
|
|
#[test]
|
|
fn a_rename_moves_the_open_buffer() {
|
|
let (vault, mut session) = one_note("rename-buffer");
|
|
session.open_buffer("note.md").expect("open");
|
|
session.rename_note("note.md", "moved/note.md", false).expect("rename");
|
|
|
|
assert!(session.buffer("note.md").is_none());
|
|
let buffer = session.buffer("moved/note.md").expect("the buffer followed");
|
|
assert_eq!(buffer.path(), "moved/note.md");
|
|
assert_eq!(buffer.text(), "hello\n");
|
|
session.buffer_mut("moved/note.md").expect("open").edit(0..5, "HELLO").expect("edit");
|
|
assert!(session.save_buffer("moved/note.md").expect("save"));
|
|
assert_eq!(on_disk(vault.path(), "moved/note.md"), "HELLO\n");
|
|
}
|