//! Lazy open: names first, notes indexed in time-boxed steps, the same index in the end. mod common; use std::time::Duration; use common::{linked_vault, seed, temp_vault}; use notescraft_engine::{IndexProgress, Registry, Session, register_core}; use serde_json::Value; const GENEROUS: Duration = Duration::from_secs(30); fn run_to_end(session: &mut Session) -> IndexProgress { let mut progress = session.index_progress(); while !progress.finished { progress = session.index_step(GENEROUS); } progress } fn run(session: &mut Session, id: &str) { let mut registry = Registry::new(); register_core(&mut registry).expect("register"); registry.run(id, session, &Value::Null).unwrap_or_else(|error| panic!("{id}: {error:?}")); } #[test] fn a_lazy_session_has_the_file_list_but_no_parsed_notes() { let vault = linked_vault("lazy_empty", 5); seed(vault.path(), &[("pic.png", "x")]); let session = Session::open_lazy(vault.path()).expect("lazy"); let progress = session.index_progress(); assert_eq!((progress.total, progress.done, progress.finished), (5, 0, false)); assert!(session.index().is_empty(), "no note is parsed before the first step"); assert!(session.index().files().contains(&"pic.png"), "attachments are registered at open"); } #[test] fn a_zero_budget_still_indexes_one_note_per_step() { let vault = linked_vault("lazy_budget", 4); let mut session = Session::open_lazy(vault.path()).expect("lazy"); for expected in 1..=3 { let progress = session.index_step(Duration::ZERO); assert_eq!((progress.done, progress.finished), (expected, false)); assert_eq!(session.index().len(), expected); } let progress = session.index_step(Duration::ZERO); assert_eq!((progress.done, progress.total), (4, 4)); assert!(progress.finished); } #[test] fn steps_after_the_end_change_nothing() { let vault = linked_vault("lazy_idempotent", 3); let mut session = Session::open_lazy(vault.path()).expect("lazy"); let end = run_to_end(&mut session); assert_eq!(session.index_step(GENEROUS), end); assert_eq!(session.index_step(Duration::ZERO), end); } #[test] fn lazy_open_plus_all_steps_builds_the_same_index_as_eager_open() { let vault = linked_vault("lazy_same", 12); seed( vault.path(), &[("sub/Extra.md", "---\naliases: [Spark]\n---\n# H\n[[Missing]] [[spark]] #deep/tag zebra word\n"), ("pic.png", "x"), ("notes/bad-utf8.md", "ok")], ); std::fs::write(vault.path().join("notes/bad-utf8.md"), [0xFF, 0xFE, 0x00]).expect("write bad bytes"); let eager = Session::open(vault.path()).expect("eager"); drop(eager); let mut lazy = Session::open_lazy(vault.path()).expect("lazy"); let progress = run_to_end(&mut lazy); let eager = Session::open_read_only(vault.path()).expect("eager again"); assert_eq!(progress.done, progress.total); assert_eq!(lazy.index().notes(), eager.index().notes()); assert_eq!(lazy.index().files(), eager.index().files()); assert_eq!(lazy.index().tags().len(), eager.index().tags().len()); assert_eq!(lazy.index().unresolved().len(), eager.index().unresolved().len()); assert_eq!(lazy.index().backlinks("note-03.md").len(), eager.index().backlinks("note-03.md").len()); assert_eq!(lazy.diagnostics().len(), eager.diagnostics().len(), "the unreadable note is a diagnostic in both"); let hits = |session: &Session| session.search("zebra", 10).expect("search").into_iter().map(|hit| hit.path).collect::>(); assert_eq!(hits(&lazy), hits(&eager)); assert_eq!(hits(&lazy), vec!["sub/Extra.md".to_owned()]); } #[test] fn an_empty_vault_finishes_on_the_first_step() { let vault = temp_vault("lazy_empty_vault"); let mut session = Session::open_lazy(vault.path()).expect("lazy"); let progress = session.index_step(GENEROUS); assert_eq!((progress.total, progress.done, progress.finished), (0, 0, true)); } #[test] fn pause_stops_the_steps_and_resume_continues_without_rereading() { let vault = linked_vault("lazy_pause", 6); let mut session = Session::open_lazy(vault.path()).expect("lazy"); session.index_step(Duration::ZERO); run(&mut session, "index.pause"); let paused = session.index_step(GENEROUS); assert!(paused.paused && !paused.finished); assert_eq!(paused.done, 1); assert_eq!(session.index().len(), 1); run(&mut session, "index.resume"); let end = run_to_end(&mut session); assert_eq!((end.done, end.total, end.paused), (6, 6, false)); assert_eq!(session.index().len(), 6); } #[test] fn the_newest_notes_are_read_first() { let vault = temp_vault("lazy_order"); seed(vault.path(), &[("a-old.md", "old"), ("b-new.md", "new"), ("c-mid.md", "mid")]); let now = std::time::SystemTime::now(); for (name, age) in [("a-old.md", 300), ("c-mid.md", 200), ("b-new.md", 100)] { let file = std::fs::File::options().write(true).open(vault.path().join(name)).expect("open"); file.set_modified(now - Duration::from_secs(age)).expect("set mtime"); } let mut session = Session::open_lazy(vault.path()).expect("lazy"); session.index_step(Duration::ZERO); assert_eq!(session.index().notes(), vec!["b-new.md"]); session.index_step(Duration::ZERO); assert_eq!(session.index().notes(), vec!["b-new.md", "c-mid.md"]); } #[test] fn opening_a_buffer_indexes_that_note_at_once() { let vault = linked_vault("lazy_open_buffer", 5); let mut session = Session::open_lazy(vault.path()).expect("lazy"); session.open_buffer("note-03.md").expect("buffer"); assert!(session.index().note("note-03.md").is_some()); let progress = session.index_progress(); assert_eq!((progress.done, progress.finished), (1, false)); let end = run_to_end(&mut session); assert_eq!((end.done, end.total), (5, 5), "the note is not counted twice"); assert_eq!(session.index().len(), 5); } #[test] fn a_note_deleted_before_its_turn_is_skipped_quietly() { let vault = linked_vault("lazy_deleted", 3); let mut session = Session::open_lazy(vault.path()).expect("lazy"); std::fs::remove_file(vault.path().join("note-01.md")).expect("remove"); let end = run_to_end(&mut session); assert_eq!(end.done, 3); assert!(session.diagnostics().is_empty(), "{:?}", session.diagnostics()); assert!(session.index().note("note-01.md").is_none()); } #[test] fn renames_and_moves_are_refused_until_the_index_is_complete() { let vault = linked_vault("lazy_gate", 4); seed(vault.path(), &[("dir/inner.md", "[[note-00]]")]); let mut session = Session::open_lazy(vault.path()).expect("lazy"); let error = session.rename_note("note-00.md", "renamed.md", true).expect_err("refused"); assert_eq!(error.code(), "indexing"); assert!(error.to_string().contains("still indexing"), "{error}"); assert_eq!(session.rename_folder("dir", "moved", true).expect_err("refused").code(), "indexing"); assert!(vault.path().join("note-00.md").exists(), "nothing was renamed"); run_to_end(&mut session); session.rename_note("note-00.md", "renamed.md", true).expect("rename after the scan"); session.rename_folder("dir", "moved", true).expect("move after the scan"); } #[test] fn search_reports_partial_results_until_the_scan_ends() { let vault = linked_vault("lazy_partial", 3); let mut session = Session::open_lazy(vault.path()).expect("lazy"); assert!(session.search_partial()); run_to_end(&mut session); assert!(!session.search_partial()); }