//! Fixtures: throw-away vaults with unique names under the temp directory. //! //! Every test gets its own folder, so tests can run in parallel and a crashed //! one never poisons the next: the folder is removed when [`TempVault`] drops, //! and `notescraft-` prefixed folders older than a day are swept away too. #![allow(dead_code)] // shared test helpers: each test file uses only some of them use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{Duration, SystemTime}; /// Flate-decompression of exported PDF bytes in tests, plus the reader that /// recovers the text a document draws (see `pdf.rs`). pub mod pdf; /// Counter making two vaults of the same test different. static COUNTER: AtomicU64 = AtomicU64::new(0); /// How long a leftover vault folder is kept before the next run sweeps it. const STALE_AFTER: Duration = Duration::from_hours(24); /// A vault folder that deletes itself when it goes out of scope. pub struct TempVault { path: PathBuf, } impl TempVault { /// The vault root to hand to `Session::open`. #[must_use] pub fn path(&self) -> &Path { &self.path } } impl Drop for TempVault { fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.path); } } /// An empty vault folder named after the test and the process. #[must_use] pub fn temp_vault(name: &str) -> TempVault { let unique = COUNTER.fetch_add(1, Ordering::Relaxed); let path = std::env::temp_dir().join(format!("notescraft-engine-{}-{name}-{unique}", std::process::id())); let _ = std::fs::remove_dir_all(&path); std::fs::create_dir_all(&path).expect("create the temp vault"); sweep(&path); TempVault { path } } /// Writes `(path, content)` pairs into `vault`, creating the folders. pub fn seed(vault: &Path, files: &[(&str, &str)]) { for (path, content) in files { let target = vault.join(path); if let Some(parent) = target.parent() { std::fs::create_dir_all(parent).expect("create the temp folder"); } std::fs::write(&target, content).expect("write the fixture file"); } } /// A throw-away folder for a `config.toml`, same mechanics as `temp_vault`: /// unique, self-removing, swept when stale. #[must_use] pub fn temp_config(name: &str) -> TempVault { temp_vault(&format!("config-{name}")) } /// A vault with `count` notes, `note-00.md` …, each linking to the next one. #[must_use] #[allow(dead_code)] // used by the session tests only pub fn linked_vault(name: &str, count: usize) -> TempVault { let vault = temp_vault(name); let files: Vec<(String, String)> = (0..count) .map(|i| { let title = format!("note-{i:02}"); let next = format!("note-{:02}", (i + 1) % count); (format!("{title}.md"), format!("# {title}\n\n[[{next}]] #work/tag\n")) }) .collect(); let borrowed: Vec<(&str, &str)> = files.iter().map(|(path, body)| (path.as_str(), body.as_str())).collect(); seed(vault.path(), &borrowed); vault } /// The text of a note as it is now on disk. #[must_use] pub fn on_disk(vault: &Path, path: &str) -> String { std::fs::read_to_string(vault.join(path)).expect("read the note from disk") } /// Removes the leftovers of older runs. fn sweep(current: &Path) { let Ok(entries) = std::fs::read_dir(std::env::temp_dir()) else { return }; for entry in entries.flatten() { let path = entry.path(); if path == current || !path.to_string_lossy().starts_with("notescraft-engine-") { continue; } let stale = entry .metadata() .and_then(|meta| meta.modified()) .ok() .and_then(|modified| SystemTime::now().duration_since(modified).ok()) .is_some_and(|age| age > STALE_AFTER); if stale { let _ = std::fs::remove_dir_all(&path); } } }