use std::fmt;
use super::commit::{BranchCommitError, CommitDurability, CommitRequest, commit_branch};
use super::fork::fork_registered;
use super::handle::{BranchHandle, DEFAULT_SHARD_ID};
use super::lifecycle::create_branch;
use super::prune::{PruneError, prune};
use super::refstore::{BranchRefError, BranchRefStore};
use super::registry::BranchRegistry;
use super::snapshot::{SnapshotError, SnapshotRegistry};
use crate::store::MemoryStore;
use crate::tree::{Hash, LeafNode, Node, NodeError, TreeError};
#[derive(Debug)]
enum TestError {
Commit(BranchCommitError),
Refs(BranchRefError),
Prune(PruneError),
Snapshot(SnapshotError),
Branch(super::handle::BranchError),
Tree(TreeError),
Node(NodeError),
Io(std::io::Error),
}
impl fmt::Display for TestError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Commit(error) => write!(formatter, "commit error: {error}"),
Self::Refs(error) => write!(formatter, "ref store error: {error}"),
Self::Prune(error) => write!(formatter, "prune error: {error}"),
Self::Snapshot(error) => write!(formatter, "snapshot error: {error}"),
Self::Branch(error) => write!(formatter, "branch error: {error}"),
Self::Tree(error) => write!(formatter, "tree error: {error}"),
Self::Node(error) => write!(formatter, "node error: {error}"),
Self::Io(error) => write!(formatter, "I/O error: {error}"),
}
}
}
impl std::error::Error for TestError {}
impl From<BranchCommitError> for TestError {
fn from(error: BranchCommitError) -> Self {
Self::Commit(error)
}
}
impl From<BranchRefError> for TestError {
fn from(error: BranchRefError) -> Self {
Self::Refs(error)
}
}
impl From<PruneError> for TestError {
fn from(error: PruneError) -> Self {
Self::Prune(error)
}
}
impl From<SnapshotError> for TestError {
fn from(error: SnapshotError) -> Self {
Self::Snapshot(error)
}
}
impl From<super::handle::BranchError> for TestError {
fn from(error: super::handle::BranchError) -> Self {
Self::Branch(error)
}
}
impl From<TreeError> for TestError {
fn from(error: TreeError) -> Self {
Self::Tree(error)
}
}
impl From<NodeError> for TestError {
fn from(error: NodeError) -> Self {
Self::Node(error)
}
}
impl From<std::io::Error> for TestError {
fn from(error: std::io::Error) -> Self {
Self::Io(error)
}
}
fn empty_root(store: &mut MemoryStore) -> Result<Hash, TestError> {
let leaf = LeafNode::new(Vec::new())?;
Ok(store.put(&Node::Leaf(leaf)))
}
fn volatile() -> CommitRequest<'static> {
CommitRequest {
durability: CommitDurability::Volatile,
extra_parents: &[],
timestamp: 1,
}
}
fn scratch_head(store: &mut MemoryStore, entries: &[(&[u8], &[u8])]) -> Result<Hash, TestError> {
let registry = BranchRegistry::new();
let scratch = fork_registered(empty_root(store)?, ®istry);
for (key, value) in entries {
scratch.put(DEFAULT_SHARD_ID, key, value)?;
}
commit_branch(&scratch, store, ®istry, volatile())?;
Ok(scratch.current_root())
}
fn buffer_entries(branch: &BranchHandle, entries: &[(&[u8], &[u8])]) -> Result<(), TestError> {
for (key, value) in entries {
branch.put(DEFAULT_SHARD_ID, key, value)?;
}
Ok(())
}
const COLLISION: &[(&[u8], &[u8])] = &[(b"dup", b"payload")];
#[test]
fn prune_before_commit_dedup_casualty_is_recreated_by_the_commit() -> Result<(), TestError> {
let mut store = MemoryStore::new();
let dir = tempfile::tempdir()?;
let refs = BranchRefStore::open(dir.path())?;
let mut snapshots = SnapshotRegistry::new();
let registry = BranchRegistry::new();
let doomed = scratch_head(&mut store, COLLISION)?;
snapshots.name_at("old", doomed, 5)?;
let report = prune(&store, ®istry, &refs, &mut snapshots, "old")?;
assert_eq!(report.node_count, 1, "the collision node must be reclaimed");
assert!(store.get(&doomed).is_none());
let branch = fork_registered(empty_root(&mut store)?, ®istry);
buffer_entries(&branch, COLLISION)?;
commit_branch(&branch, &mut store, ®istry, volatile())?;
assert_eq!(
branch.current_root(),
doomed,
"the commit must re-derive the identical (dedup-colliding) head"
);
assert!(
store.get(&doomed).is_some(),
"the commit's put must have recreated the reclaimed node"
);
assert_eq!(
branch.get(DEFAULT_SHARD_ID, &store, b"dup")?,
Some(b"payload".to_vec())
);
Ok(())
}
#[test]
fn prune_after_volatile_commit_spares_the_identical_live_head() -> Result<(), TestError> {
let mut store = MemoryStore::new();
let dir = tempfile::tempdir()?;
let refs = BranchRefStore::open(dir.path())?;
let mut snapshots = SnapshotRegistry::new();
let registry = BranchRegistry::new();
let doomed = scratch_head(&mut store, COLLISION)?;
snapshots.name_at("old", doomed, 5)?;
let branch = fork_registered(empty_root(&mut store)?, ®istry);
buffer_entries(&branch, COLLISION)?;
commit_branch(&branch, &mut store, ®istry, volatile())?;
assert_eq!(branch.current_root(), doomed);
let report = prune(&store, ®istry, &refs, &mut snapshots, "old")?;
assert_eq!(
report.node_count, 0,
"the live registry pin must protect the byte-identical head"
);
assert!(store.get(&doomed).is_some());
assert_eq!(
branch.get(DEFAULT_SHARD_ID, &store, b"dup")?,
Some(b"payload".to_vec())
);
Ok(())
}
#[test]
fn prune_after_durable_commit_spares_the_identical_recorded_head() -> Result<(), TestError> {
let mut store = MemoryStore::new();
let dir = tempfile::tempdir()?;
let mut refs = BranchRefStore::open(dir.path())?;
let mut snapshots = SnapshotRegistry::new();
let doomed = scratch_head(&mut store, COLLISION)?;
snapshots.name_at("old", doomed, 5)?;
{
let registry = BranchRegistry::new();
let anchor = empty_root(&mut store)?;
let branch = create_branch(
"layer",
[(DEFAULT_SHARD_ID, anchor)],
&mut refs,
®istry,
10,
)?;
buffer_entries(&branch, COLLISION)?;
commit_branch(
&branch,
&mut store,
®istry,
CommitRequest {
durability: CommitDurability::Durable { refs: &mut refs },
extra_parents: &[],
timestamp: 20,
},
)?;
assert_eq!(branch.current_root(), doomed);
}
let refs = BranchRefStore::open(dir.path())?;
let registry = BranchRegistry::new();
let report = prune(&store, ®istry, &refs, &mut snapshots, "old")?;
assert_eq!(
report.node_count, 0,
"the durable record must protect the byte-identical head across restart"
);
assert!(store.get(&doomed).is_some());
Ok(())
}