use std::error::Error;
use crate::store::MemoryStore;
use crate::tree::{Cursor, Hash, LeafNode, Node, TreeError, TreePolicy, batch_mutate};
use super::super::commit::{CommitDurability, CommitRequest, commit_branch};
use super::super::handle::DEFAULT_SHARD_ID;
use super::super::lifecycle::fork_at;
use super::super::prune::prune;
use super::super::refstore::BranchRefStore;
use super::super::registry::{BranchRegistry, CrossRegisterError};
use super::super::snapshot::SnapshotRegistry;
type Res = Result<(), Box<dyn Error>>;
fn hash(byte: u8) -> Hash {
Hash::from_bytes([byte; 32])
}
fn v2() -> TreePolicy {
TreePolicy::v2(48, 48)
}
fn oversized(seed: u8) -> Vec<u8> {
vec![seed; 40]
}
fn map() -> Vec<(Vec<u8>, Option<Vec<u8>>)> {
(0u8..4)
.map(|n| (format!("k{n:02}").into_bytes(), Some(oversized(n))))
.collect()
}
fn other_map() -> Vec<(Vec<u8>, Option<Vec<u8>>)> {
(0u8..3)
.map(|n| {
(
format!("other-{n:02}").into_bytes(),
Some(oversized(200 + n)),
)
})
.collect()
}
fn build_from(
store: &mut MemoryStore,
entries: &[(Vec<u8>, Option<Vec<u8>>)],
policy: TreePolicy,
) -> Result<Hash, Box<dyn Error>> {
let empty = store.put(&Node::Leaf(LeafNode::new(Vec::new())?));
Ok(batch_mutate(store, empty, entries, policy)?)
}
fn build_root(store: &mut MemoryStore, policy: TreePolicy) -> Result<Hash, Box<dyn Error>> {
build_from(store, &map(), policy)
}
#[test]
fn fork_at_crosses_v1_anchor_to_canonical_v2() -> Res {
let mut store = MemoryStore::new();
let v1_anchor = build_root(&mut store, TreePolicy::V1_DEFAULT)?;
let v2_canonical = build_root(&mut store, v2())?;
assert_ne!(
v1_anchor, v2_canonical,
"the v2 shape must differ from v1 for the witness to bite"
);
let registry = BranchRegistry::new();
let dir = tempfile::tempdir()?;
let refs = BranchRefStore::open(dir.path())?;
let mut snapshots = SnapshotRegistry::new();
snapshots.name("anchor-pin", v1_anchor)?;
let handle = fork_at(v1_anchor, &mut store, v2(), ®istry, &refs, &snapshots)?;
assert_eq!(
handle.fork_point(),
v1_anchor,
"the fork lineage must stay at the original v1 anchor (merge ancestor)"
);
assert_eq!(
handle.current_root(),
v2_canonical,
"the mutation base must be the fresh-build canonical v2 root, never the v1 anchor"
);
Ok(())
}
#[test]
fn crossed_fork_commits_pure_v2() -> Res {
let mut store = MemoryStore::new();
let v1_anchor = build_root(&mut store, TreePolicy::V1_DEFAULT)?;
let registry = BranchRegistry::new();
let dir = tempfile::tempdir()?;
let refs = BranchRefStore::open(dir.path())?;
let mut snapshots = SnapshotRegistry::new();
snapshots.name("anchor-pin", v1_anchor)?;
let handle = fork_at(v1_anchor, &mut store, v2(), ®istry, &refs, &snapshots)?;
handle.put(DEFAULT_SHARD_ID, b"k04", oversized(9))?;
commit_branch(
&handle,
&mut store,
®istry,
CommitRequest {
durability: CommitDurability::Volatile,
extra_parents: &[],
timestamp: 1,
},
v2(),
)?;
let empty = store.put(&Node::Leaf(LeafNode::new(Vec::new())?));
let mut expected_map = map();
expected_map.push((b"k04".to_vec(), Some(oversized(9))));
let expected = batch_mutate(&mut store, empty, &expected_map, v2())?;
assert_eq!(
handle.current_root(),
expected,
"a commit off the crossed base must equal the fresh-build canonical v2 tree"
);
Ok(())
}
#[test]
fn fork_at_of_v2_anchor_is_identity() -> Res {
let mut store = MemoryStore::new();
let v2_anchor = build_root(&mut store, v2())?;
let registry = BranchRegistry::new();
let dir = tempfile::tempdir()?;
let refs = BranchRefStore::open(dir.path())?;
let mut snapshots = SnapshotRegistry::new();
snapshots.name("anchor-pin", v2_anchor)?;
let handle = fork_at(v2_anchor, &mut store, v2(), ®istry, &refs, &snapshots)?;
assert_eq!(
handle.current_root(),
v2_anchor,
"re-crossing an already-v2 anchor is the identity"
);
assert_eq!(handle.fork_point(), v2_anchor);
Ok(())
}
#[test]
fn fork_at_under_v1_policy_skips_the_crossing() -> Res {
let mut store = MemoryStore::new();
let v1_anchor = build_root(&mut store, TreePolicy::V1_DEFAULT)?;
let registry = BranchRegistry::new();
let dir = tempfile::tempdir()?;
let refs = BranchRefStore::open(dir.path())?;
let mut snapshots = SnapshotRegistry::new();
snapshots.name("anchor-pin", v1_anchor)?;
let handle = fork_at(
v1_anchor,
&mut store,
TreePolicy::V1_DEFAULT,
®istry,
&refs,
&snapshots,
)?;
assert_eq!(
handle.current_root(),
v1_anchor,
"v1 keeps today's behaviour: the working root is the anchor, no rebuild"
);
assert_eq!(handle.fork_point(), v1_anchor);
Ok(())
}
#[test]
fn crossed_working_root_survives_prune_of_a_disjoint_snapshot() -> Res {
let mut store = MemoryStore::new();
let v1_anchor = build_root(&mut store, TreePolicy::V1_DEFAULT)?;
let registry = BranchRegistry::new();
let dir = tempfile::tempdir()?;
let refs = BranchRefStore::open(dir.path())?;
let mut snapshots = SnapshotRegistry::new();
snapshots.name("anchor-pin", v1_anchor)?;
let handle = fork_at(v1_anchor, &mut store, v2(), ®istry, &refs, &snapshots)?;
let working = handle.current_root();
assert!(
registry.live_roots().contains(&working),
"the rebuilt working root must be pinned in the registry"
);
let scratch = build_from(&mut store, &other_map(), v2())?;
snapshots.name("scratch", scratch)?;
let report = prune(&store, ®istry, &refs, &mut snapshots, "scratch")?;
assert!(
report.node_count > 0,
"the disjoint snapshot must actually reclaim nodes (non-vacuous prune)"
);
assert!(
Cursor::new(&store, working).get(b"k00")?.is_some(),
"the crossed working root must survive the prune"
);
drop(handle);
assert!(
!registry.live_roots().contains(&working),
"dropping the handle must release the working-root pin"
);
Ok(())
}
#[test]
fn crossed_working_root_survives_prune_of_a_dedup_colliding_snapshot() -> Res {
let mut store = MemoryStore::new();
let v1_anchor = build_root(&mut store, TreePolicy::V1_DEFAULT)?;
let registry = BranchRegistry::new();
let dir = tempfile::tempdir()?;
let refs = BranchRefStore::open(dir.path())?;
let mut snapshots = SnapshotRegistry::new();
snapshots.name("anchor-pin", v1_anchor)?;
let handle = fork_at(v1_anchor, &mut store, v2(), ®istry, &refs, &snapshots)?;
let working = handle.current_root();
let collision = build_root(&mut store, v2())?;
assert_eq!(
collision, working,
"the collision snapshot must equal the working root"
);
snapshots.name("collision", collision)?;
prune(&store, ®istry, &refs, &mut snapshots, "collision")?;
assert!(
Cursor::new(&store, working).get(b"k00")?.is_some(),
"a dedup-colliding prune must not reclaim the pinned working root's nodes"
);
Ok(())
}
#[test]
fn cross_and_register_refuses_a_mismatched_working_root_count() {
let registry = BranchRegistry::new();
let anchors = [hash(1), hash(2)];
let short: Result<_, CrossRegisterError<TreeError>> =
registry.cross_and_register(&anchors, |_| true, |_| Ok(vec![hash(9)]));
assert!(
matches!(
short,
Err(CrossRegisterError::WorkingRootCountMismatch {
expected: 2,
got: 1
})
),
"a short build must be refused as a count mismatch"
);
assert!(
registry.live_roots().is_empty(),
"a short-build mismatch must register NOTHING (no partial pins)"
);
let long: Result<_, CrossRegisterError<TreeError>> = registry.cross_and_register(
&anchors,
|_| true,
|_| Ok(vec![hash(9), hash(10), hash(11)]),
);
assert!(
matches!(
long,
Err(CrossRegisterError::WorkingRootCountMismatch {
expected: 2,
got: 3
})
),
"a long build must be refused as a count mismatch"
);
assert!(
registry.live_roots().is_empty(),
"a long-build mismatch must register NOTHING"
);
}