haematite 0.6.2

Content-addressed, branchable, actor-native storage engine
Documentation
//! Shared fixtures for the vacuum test suites.

use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::SystemTime;

use crate::db::{Database, DatabaseConfig};
use crate::tree::Hash;
use crate::wal::WalRecovery;

use super::{VacuumOptions, VacuumReport, vacuum_stats};

pub fn config_for(path: &Path, shard_count: usize) -> DatabaseConfig {
    DatabaseConfig {
        data_dir: path.to_path_buf(),
        shard_count,
        distributed: None,
    }
}

/// Create a database, append `versions` successive events to each of `keys`,
/// and close it — leaving committed roots plus version garbage on disk.
pub fn build_store(
    data_dir: &Path,
    shard_count: usize,
    keys: &[&[u8]],
    versions: usize,
) -> Result<(), Box<dyn std::error::Error>> {
    let db = Database::create(config_for(data_dir, shard_count))?;
    for version in 0..versions {
        for key in keys {
            db.append(
                key.to_vec(),
                vec![format!("v{version}").into_bytes()],
                version as u64,
            )?;
        }
    }
    drop(db);
    Ok(())
}

/// Stats with default options (canonical locations only, no attestation).
pub fn stats(data_dir: &Path) -> Result<VacuumReport, super::VacuumError> {
    vacuum_stats(&VacuumOptions::new(data_dir.to_path_buf()))
}

/// Assert a run REFUSED, yielding the typed refusal for inspection.
pub fn expect_refusal(
    result: Result<VacuumReport, super::VacuumError>,
) -> Result<super::VacuumError, Box<dyn std::error::Error>> {
    match result {
        Err(error) => Ok(error),
        Ok(_report) => Err("expected a typed refusal, got a completed report".into()),
    }
}

/// One entry's identity: byte contents (`None` for directories) + mtime —
/// directories carry their mtime too, so a create/unlink INSIDE any
/// directory shows up even if the file comparison were somehow blind to it.
type EntryIdentity = (Option<Vec<u8>>, SystemTime);

/// Recursive full-scope snapshot of a directory tree: relative path →
/// identity. Byte- and mtime-exact for files AND directories, so any write,
/// unlink, create, or rewrite anywhere under `root` changes it.
pub fn snapshot_tree(root: &Path) -> Result<BTreeMap<PathBuf, EntryIdentity>, std::io::Error> {
    let mut snapshot = BTreeMap::new();
    walk_into(root, root, &mut snapshot)?;
    Ok(snapshot)
}

fn walk_into(
    root: &Path,
    dir: &Path,
    snapshot: &mut BTreeMap<PathBuf, EntryIdentity>,
) -> Result<(), std::io::Error> {
    for entry in fs::read_dir(dir)? {
        let entry = entry?;
        let path = entry.path();
        let relative = path
            .strip_prefix(root)
            .unwrap_or(path.as_path())
            .to_path_buf();
        let metadata = fs::symlink_metadata(&path)?;
        if metadata.is_dir() {
            snapshot.insert(relative, (None, metadata.modified()?));
            walk_into(root, &path, snapshot)?;
        } else {
            let bytes = fs::read(&path)?;
            snapshot.insert(relative, (Some(bytes), metadata.modified()?));
        }
    }
    Ok(())
}

/// The committed root recorded in one shard's WAL, if any.
pub fn committed_root(data_dir: &Path, shard_id: usize) -> Option<Hash> {
    let wal_path = data_dir.join(format!("shard-{shard_id}")).join("shard.wal");
    let mut recovery = WalRecovery::open(wal_path).ok()?;
    recovery
        .recover_unverified()
        .ok()
        .and_then(|recovered| recovered.committed_root())
}

/// The store path of one shard.
pub fn store_dir(data_dir: &Path, shard_id: usize) -> PathBuf {
    data_dir.join(format!("shard-{shard_id}")).join("store")
}

/// The canonical node-file path for `hash` inside `store`.
pub fn node_path(store: &Path, hash: Hash) -> PathBuf {
    let hex = hash.to_string();
    let (prefix, name) = hex.split_at(2);
    store.join(prefix).join(name)
}