use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};
use super::commit::{BranchCommitError, CommitDurability, CommitRequest, commit_branch};
use super::handle::{BranchError, DEFAULT_SHARD_ID};
use super::lifecycle::{create_branch, open_branch, remove_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(BranchError),
Tree(TreeError),
Node(NodeError),
Io(std::io::Error),
Missing(&'static str),
}
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}"),
Self::Missing(what) => write!(formatter, "missing: {what}"),
}
}
}
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<BranchError> for TestError {
fn from(error: 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 durable(refs: &mut BranchRefStore, timestamp: u64) -> CommitRequest<'_> {
CommitRequest {
durability: CommitDurability::Durable { refs },
extra_parents: &[],
timestamp,
}
}
fn sole_ref_file(refs_dir: &Path) -> Result<PathBuf, TestError> {
for entry in fs::read_dir(refs_dir)? {
let path = entry?.path();
if path.extension().and_then(|extension| extension.to_str()) == Some("ref") {
return Ok(path);
}
}
Err(TestError::Missing("a .ref file"))
}
#[test]
fn tharsis_lease_expiry_snapshot_then_remove_survives_a_crash_between() -> Result<(), TestError> {
let mut store = MemoryStore::new();
let dir = tempfile::tempdir()?;
let refs_dir = dir.path().join("refs");
let snapshots_path = dir.path().join("snapshots.reg");
let registry = BranchRegistry::new();
let head = {
let mut refs = BranchRefStore::open(&refs_dir)?;
let mut snapshots = SnapshotRegistry::open(&snapshots_path)?;
let anchor = empty_root(&mut store)?;
let layer = create_branch(
"layer",
[(DEFAULT_SHARD_ID, anchor)],
&mut refs,
®istry,
10,
)?;
layer.put(DEFAULT_SHARD_ID, b"delta", b"lease work")?;
commit_branch(&layer, &mut store, ®istry, durable(&mut refs, 20))?;
let head = layer.current_root();
snapshots.name_at("layer/final", head, 30)?;
head
};
let mut refs = BranchRefStore::open(&refs_dir)?;
let mut snapshots = SnapshotRegistry::open(&snapshots_path)?;
assert!(refs.get("layer").is_some(), "the record survived the crash");
assert_eq!(snapshots.get("layer/final"), Some(head));
let fresh_registry = BranchRegistry::new();
snapshots.name_at("cull", head, 40)?;
let report = prune(&store, &fresh_registry, &refs, &mut snapshots, "cull")?;
assert_eq!(report.node_count, 0);
let removed = remove_branch("layer", &mut refs)?;
assert!(matches!(removed, Some(record) if record.name == "layer"));
snapshots.name_at("cull-again", head, 50)?;
let report = prune(&store, &fresh_registry, &refs, &mut snapshots, "cull-again")?;
assert_eq!(
report.node_count, 0,
"the named snapshot must carry the head alone"
);
assert!(store.get(&head).is_some());
snapshots
.remove("layer/final")?
.ok_or(TestError::Missing("layer/final snapshot"))?;
snapshots.name_at("cull-last", head, 60)?;
let report = prune(&store, &fresh_registry, &refs, &mut snapshots, "cull-last")?;
assert!(report.node_count > 0);
assert!(store.get(&head).is_none());
Ok(())
}
#[test]
fn stale_handle_cannot_hijack_a_recreated_branch_generation() -> Result<(), TestError> {
let mut store = MemoryStore::new();
let dir = tempfile::tempdir()?;
let registry = BranchRegistry::new();
let mut refs = BranchRefStore::open(dir.path())?;
let anchor = empty_root(&mut store)?;
create_branch("A", [(DEFAULT_SHARD_ID, anchor)], &mut refs, ®istry, 100)?;
let stale = open_branch("A", &refs, ®istry)?;
remove_branch("A", &mut refs)?;
let fresh = create_branch("A", [(DEFAULT_SHARD_ID, anchor)], &mut refs, ®istry, 200)?;
let ref_file = sole_ref_file(dir.path())?;
let bytes_before = fs::read(&ref_file)?;
stale.put(DEFAULT_SHARD_ID, b"key", b"hijack")?;
let result = commit_branch(&stale, &mut store, ®istry, durable(&mut refs, 300));
assert!(matches!(
result,
Err(BranchCommitError::Ref(
BranchRefError::BranchGenerationMismatch {
expected_created: 100,
found_created: 200,
..
}
))
));
assert_eq!(fs::read(&ref_file)?, bytes_before);
assert!(stale.shard_is_dirty(DEFAULT_SHARD_ID)?);
assert_eq!(stale.current_root(), anchor);
fresh.put(DEFAULT_SHARD_ID, b"key", b"legitimate")?;
let outcome = commit_branch(&fresh, &mut store, ®istry, durable(&mut refs, 400))?;
assert_eq!(outcome.seq, Some(1));
assert_eq!(
fresh.get(DEFAULT_SHARD_ID, &store, b"key")?,
Some(b"legitimate".to_vec())
);
Ok(())
}
#[test]
fn bit_rot_in_an_installed_record_fails_open_loud_naming_the_file() -> Result<(), TestError> {
let mut store = MemoryStore::new();
let dir = tempfile::tempdir()?;
let registry = BranchRegistry::new();
let head_bytes = {
let mut refs = BranchRefStore::open(dir.path())?;
let anchor = empty_root(&mut store)?;
let branch = create_branch(
"fragile",
[(DEFAULT_SHARD_ID, anchor)],
&mut refs,
®istry,
10,
)?;
branch.put(DEFAULT_SHARD_ID, b"key", b"value")?;
commit_branch(&branch, &mut store, ®istry, durable(&mut refs, 20))?;
fs::read(sole_ref_file(dir.path())?)?
};
let ref_file = sole_ref_file(dir.path())?;
let file_name = ref_file
.file_name()
.and_then(|name| name.to_str())
.ok_or(TestError::Missing("ref file name"))?
.to_owned();
let mut rotten = head_bytes.clone();
rotten[0] ^= 0x01;
fs::write(&ref_file, &rotten)?;
let result = BranchRefStore::open(dir.path());
let Err(BranchRefError::Corrupt(message)) = result else {
return Err(TestError::Missing("a Corrupt error for the bit-flip"));
};
assert!(
message.contains(&file_name),
"the corruption error must name the offending file: {message}"
);
fs::write(&ref_file, &head_bytes[..head_bytes.len() / 2])?;
assert!(matches!(
BranchRefStore::open(dir.path()),
Err(BranchRefError::Corrupt(_))
));
fs::write(&ref_file, &head_bytes)?;
let refs = BranchRefStore::open(dir.path())?;
assert!(refs.get("fragile").is_some());
Ok(())
}