use std::fmt;
use super::commit::{BranchCommitError, CommitDurability, CommitRequest, commit_branch};
use super::conflict::ConflictPolicy;
use super::handle::{BranchError, DEFAULT_SHARD_ID};
use super::lifecycle::{create_branch, open_branch};
use super::merge::{MergeError, merge_with_report};
use super::prune::{PruneError, prune};
use super::refstore::{BranchRefError, BranchRefStore};
use super::registry::BranchRegistry;
use super::snapshot::{SnapshotError, SnapshotRegistry};
use crate::store::{DiskStore, NodeStore, StoreError};
use crate::tree::{Cursor, Hash, LeafNode, Node, NodeError, TreeError, insert};
#[derive(Debug)]
enum TestError {
Commit(BranchCommitError),
Refs(BranchRefError),
Prune(PruneError),
Snapshot(SnapshotError),
Merge(MergeError),
Branch(BranchError),
Store(StoreError),
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::Merge(error) => write!(formatter, "merge error: {error}"),
Self::Branch(error) => write!(formatter, "branch error: {error}"),
Self::Store(error) => write!(formatter, "disk store 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<MergeError> for TestError {
fn from(error: MergeError) -> Self {
Self::Merge(error)
}
}
impl From<BranchError> for TestError {
fn from(error: BranchError) -> Self {
Self::Branch(error)
}
}
impl From<StoreError> for TestError {
fn from(error: StoreError) -> Self {
Self::Store(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 read_at(store: &DiskStore, root: Hash, key: &[u8]) -> Result<Option<Vec<u8>>, TestError> {
Cursor::new(store, root).get(key).map_err(TestError::from)
}
fn session_before_the_crash(
store_dir: &std::path::Path,
refs_dir: &std::path::Path,
) -> Result<(Hash, Hash, Hash), TestError> {
let mut store = DiskStore::new(store_dir)?;
let empty = store.put(&Node::Leaf(LeafNode::new(Vec::new())?))?;
let anchor = insert(&mut store, empty, b"base", b"shared history")?;
let junk_root = insert(&mut store, anchor, b"junk", b"reclaim me")?;
store.sync_dirty_dirs()?;
let mut refs = BranchRefStore::open(refs_dir)?;
let registry = BranchRegistry::new();
let branch = create_branch(
"sess",
[(DEFAULT_SHARD_ID, anchor)],
&mut refs,
®istry,
10,
)?;
branch.put(DEFAULT_SHARD_ID, b"checkpoint", b"intra-cadence")?;
commit_branch(
&branch,
&mut store,
®istry,
CommitRequest {
durability: CommitDurability::Volatile,
extra_parents: &[],
timestamp: 20,
},
)?;
for (round, key) in [b"first".as_slice(), b"second".as_slice()]
.into_iter()
.enumerate()
{
branch.put(DEFAULT_SHARD_ID, key, b"landed")?;
let timestamp = 30 + u64::try_from(round).unwrap_or(u64::MAX);
commit_branch(
&branch,
&mut store,
®istry,
CommitRequest {
durability: CommitDurability::Durable { refs: &mut refs },
extra_parents: &[],
timestamp,
},
)?;
}
Ok((anchor, junk_root, branch.current_root()))
}
#[test]
fn reopened_branch_survives_a_prune_and_merges_at_its_recorded_ancestor() -> Result<(), TestError> {
let dir = tempfile::tempdir()?;
let (store_dir, refs_dir) = (dir.path().join("store"), dir.path().join("refs"));
let (anchor, junk_root, durable_head) = session_before_the_crash(&store_dir, &refs_dir)?;
let mut store = DiskStore::new(&store_dir)?;
let refs = BranchRefStore::open(&refs_dir)?;
let registry = BranchRegistry::new();
let reopened = open_branch("sess", &refs, ®istry)?;
assert_eq!(reopened.fork_point(), anchor, "recorded merge ancestor");
assert_eq!(reopened.current_root(), durable_head, "last durable head");
let record = refs.get("sess").ok_or(TestError::Missing("sess record"))?;
assert_eq!(record.seq, 2);
assert_eq!(
reopened.get(DEFAULT_SHARD_ID, &store, b"checkpoint")?,
Some(b"intra-cadence".to_vec())
);
assert_eq!(
reopened.get(DEFAULT_SHARD_ID, &store, b"second")?,
Some(b"landed".to_vec())
);
let mut snapshots = SnapshotRegistry::new();
snapshots.name_at("junk", junk_root, 40)?;
let report = prune(&store, ®istry, &refs, &mut snapshots, "junk")?;
assert!(
report.node_count > 0,
"the junk root itself must be reclaimed"
);
assert_eq!(
read_at(&store, anchor, b"base")?,
Some(b"shared history".to_vec())
);
assert_eq!(
read_at(&store, durable_head, b"second")?,
Some(b"landed".to_vec())
);
let trunk = insert(&mut store, anchor, b"trunk", b"parallel work")?;
let report = merge_with_report(
&mut store,
trunk,
reopened.current_root(),
reopened.fork_point(),
&ConflictPolicy::Lww,
)?;
assert!(
report.conflicts.is_empty(),
"disjoint edits must not conflict"
);
for (key, value) in [
(b"base".as_slice(), b"shared history".as_slice()),
(b"trunk".as_slice(), b"parallel work".as_slice()),
(b"checkpoint".as_slice(), b"intra-cadence".as_slice()),
(b"first".as_slice(), b"landed".as_slice()),
(b"second".as_slice(), b"landed".as_slice()),
] {
assert_eq!(
read_at(&store, report.merged_root, key)?,
Some(value.to_vec()),
"merged tree must carry both lines' keys"
);
}
Ok(())
}