Files
2026-10-15 12:48:15 -03:00

113 lines
3.8 KiB
Rust

//! 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);
}
}
}