Files
2026-10-14 09:28:23 -03:00

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");
}