use std::error::Error;
use std::fs;
use super::report::{ShardPresence, SweepBlocker, VacuumMode};
use super::test_support::{build_store, snapshot_tree, stats};
use super::{VacuumOptions, vacuum_stats};
#[test]
fn stats_reconciles_and_reports_version_garbage() -> Result<(), Box<dyn Error>> {
let temp = tempfile::tempdir()?;
build_store(temp.path(), 2, &[b"alpha", b"beta", b"gamma"], 5)?;
let report = stats(temp.path())?;
assert_eq!(report.mode, VacuumMode::Stats);
assert_eq!(report.shard_count, 2);
assert!(!report.lock_anchor_created, "create() made the anchor");
let totals = report.totals;
assert!(totals.enumerated_nodes > 0, "store must hold nodes");
assert_eq!(
totals.enumerated_nodes,
totals.marked_nodes + totals.unmarked_nodes,
"node counts must reconcile"
);
assert_eq!(
totals.enumerated_bytes,
totals.marked_bytes + totals.unmarked_bytes,
"byte counts must reconcile"
);
assert!(totals.marked_nodes > 0, "live roots must mark something");
assert!(
totals.unmarked_nodes > 0,
"five versions of each key must leave superseded nodes"
);
for shard in &report.shards {
assert_eq!(
shard.nodes.enumerated_nodes,
shard.nodes.marked_nodes + shard.nodes.unmarked_nodes,
"per-shard counts must reconcile"
);
}
assert!(
report.verified_nodes >= totals.marked_nodes,
"each marked node requires at least one verification"
);
assert!(
report
.sweep_blockers
.iter()
.any(|blocker| matches!(blocker, SweepBlocker::MetadataUnattested)),
"missing manifest+attestation must be reported as a sweep blocker"
);
assert!(
!report.trust_boundary.is_empty(),
"§6: the trust boundary is named in every report"
);
Ok(())
}
#[test]
fn lazy_shards_classified_not_refused() -> Result<(), Box<dyn Error>> {
let temp = tempfile::tempdir()?;
build_store(temp.path(), 8, &[b"solo"], 2)?;
let report = stats(temp.path())?;
let materialised = report.shards.len();
let lazy = report.never_materialised_shards;
assert_eq!(materialised + lazy, 8);
assert!(materialised >= 1, "the touched shard materialised");
assert!(
lazy >= 1,
"an 8-shard db with one key must have lazy shards"
);
for shard in &report.shards {
assert_eq!(shard.presence, ShardPresence::Materialised);
}
Ok(())
}
#[test]
fn stats_mutates_nothing_full_scope() -> Result<(), Box<dyn Error>> {
let temp = tempfile::tempdir()?;
build_store(temp.path(), 2, &[b"key-a", b"key-b"], 3)?;
let refs_dir = temp.path().join(super::CANONICAL_REFS_DIR);
write_branch_record(&refs_dir, temp.path(), 0)?;
fs::write(refs_dir.join(".branch-orphan.tmp"), b"crash leftover")?;
let absent_refs = temp.path().join("nonexistent-refs");
let before = snapshot_tree(temp.path())?;
let mut options = VacuumOptions::new(temp.path().to_path_buf());
options.refs_dirs.push(absent_refs.clone());
let report = vacuum_stats(&options)?;
let after = snapshot_tree(temp.path())?;
assert_eq!(before, after, "stats must not change one byte or mtime");
assert!(!absent_refs.exists(), "the reader must never create dirs");
assert!(
report.metadata.supplied_missing.contains(&absent_refs),
"the absent supplied path is reported, not silently skipped"
);
assert!(!report.lock_anchor_created);
let consulted = report
.metadata
.sources
.iter()
.find(|source| source.path == refs_dir)
.ok_or("canonical refs dir must be consulted")?;
assert_eq!(consulted.records, 1);
assert_eq!(
consulted.temp_debris_files, 1,
"debris counted, not cleaned"
);
Ok(())
}
#[test]
fn legacy_dir_lock_anchor_is_sole_delta() -> Result<(), Box<dyn Error>> {
let temp = tempfile::tempdir()?;
build_store(temp.path(), 1, &[b"legacy"], 2)?;
let lock_path = temp.path().join("writer.lock");
fs::remove_file(&lock_path)?;
let mut before = snapshot_tree(temp.path())?;
let report = stats(temp.path())?;
let after = snapshot_tree(temp.path())?;
assert!(report.lock_anchor_created, "the report names the anchor");
let anchor = after
.get(std::path::Path::new("writer.lock"))
.ok_or("anchor must exist after the run")?;
let bytes = anchor.0.as_ref().ok_or("anchor is a file")?;
assert!(bytes.is_empty(), "the anchor is content-free");
before.insert(std::path::PathBuf::from("writer.lock"), anchor.clone());
assert_eq!(before, after, "the anchor must be the SOLE delta");
Ok(())
}
#[test]
fn top_level_inventory_enumerates_and_flags() -> Result<(), Box<dyn Error>> {
let temp = tempfile::tempdir()?;
build_store(temp.path(), 2, &[b"key"], 2)?;
fs::write(temp.path().join("stray-notes.txt"), b"operator scribble")?;
fs::create_dir(temp.path().join("foreign-dir"))?;
fs::write(temp.path().join("foreign-dir/inner"), b"12345")?;
let beyond = temp.path().join("shard-7");
fs::create_dir(&beyond)?;
fs::write(beyond.join("shard.wal"), b"junk")?;
fs::write(temp.path().join("shard-9"), b"just a file")?;
let report = stats(temp.path())?;
let names: Vec<&str> = report
.uninventoried
.iter()
.map(|entry| entry.name.as_str())
.collect();
assert!(names.contains(&"stray-notes.txt"));
assert!(names.contains(&"foreign-dir"));
let dir_entry = report
.uninventoried
.iter()
.find(|entry| entry.name == "foreign-dir")
.ok_or("foreign dir enumerated")?;
assert_eq!(dir_entry.size_bytes, Some(5), "recursive size accounted");
assert!(
report.sweep_blockers.iter().any(|blocker| matches!(
blocker,
SweepBlocker::ShardBeyondCount {
id: 7,
shard_count: 2
}
)),
"beyond-count shard dir is a reported sweep refusal; stats completes"
);
let beyond_entry = report
.uninventoried
.iter()
.find(|entry| entry.name == "shard-7")
.ok_or("beyond-count dir enumerated with kind and size")?;
assert_eq!(beyond_entry.size_bytes, Some(4));
assert!(names.contains(&"shard-9"));
assert!(
!report
.sweep_blockers
.iter()
.any(|blocker| matches!(blocker, SweepBlocker::ShardBeyondCount { id: 9, .. })),
"a file is not a shard"
);
assert_eq!(
report.shards.len() + report.never_materialised_shards,
2,
"beyond-count dirs never join the shard model"
);
Ok(())
}
#[test]
fn non_canonical_shard_names_are_uninventoried() -> Result<(), Box<dyn Error>> {
let temp = tempfile::tempdir()?;
build_store(temp.path(), 2, &[b"key"], 1)?;
fs::create_dir(temp.path().join("shard-01"))?;
fs::create_dir(temp.path().join("shard-x"))?;
let report = stats(temp.path())?;
let names: Vec<&str> = report
.uninventoried
.iter()
.map(|entry| entry.name.as_str())
.collect();
assert!(names.contains(&"shard-01"), "leading zero is not canonical");
assert!(names.contains(&"shard-x"));
Ok(())
}
fn write_branch_record(
refs_dir: &std::path::Path,
data_dir: &std::path::Path,
shard_id: usize,
) -> Result<(), Box<dyn Error>> {
use crate::branch::refstore::BranchRefStore;
use crate::branch::{BranchRefRecord, BranchShardRef};
let root = super::test_support::committed_root(data_dir, shard_id)
.ok_or("shard must have a committed root")?;
let mut store = BranchRefStore::open(refs_dir)?;
store.create(BranchRefRecord {
name: "pinned".to_owned(),
created: 1,
kind: crate::branch::BranchKind::Work,
namespace_lineage: None,
seq: 1,
timestamp: 1,
shards: vec![BranchShardRef {
shard_id,
fork_anchor: root,
head: root,
}],
parents: Vec::new(),
})?;
Ok(())
}