haematite 0.6.1

Content-addressed, branchable, actor-native storage engine
Documentation
//! Unit tests for the active-branch registry. Split out of `registry.rs` via
//! `#[path]` so that file stays within the branch module's 500-line cap as the
//! A1 advance/replace coverage grows.

use super::BranchRegistry;
use crate::branch::{fork_registered, fork_shards_registered};
use crate::tree::Hash;

fn hash(byte: u8) -> Hash {
    Hash::from_bytes([byte; 32])
}

#[test]
fn new_registry_has_no_live_roots() {
    let registry = BranchRegistry::new();

    assert!(registry.live_roots().is_empty());
}

#[test]
fn registered_fork_root_is_live_until_handle_drops() {
    let registry = BranchRegistry::new();
    let root = hash(1);

    {
        let branch = fork_registered(root, &registry);
        assert_eq!(branch.fork_point(), root);
        assert!(registry.live_roots().contains(&root));
    }

    assert!(!registry.live_roots().contains(&root));
}

#[test]
fn cloned_handle_keeps_registered_root_live() {
    let registry = BranchRegistry::new();
    let root = hash(2);
    let branch = fork_registered(root, &registry);
    let clone = branch.clone();

    drop(branch);
    assert!(registry.live_roots().contains(&root));

    drop(clone);
    assert!(!registry.live_roots().contains(&root));
}

#[test]
fn duplicate_roots_are_reference_counted() {
    let registry = BranchRegistry::new();
    let root = hash(3);
    let first = fork_registered(root, &registry);
    let second = fork_registered(root, &registry);

    drop(first);
    assert!(registry.live_roots().contains(&root));

    drop(second);
    assert!(!registry.live_roots().contains(&root));
}

#[test]
fn registered_multi_shard_fork_tracks_every_shard_root() -> Result<(), crate::branch::BranchError> {
    let registry = BranchRegistry::new();
    let shard_three = hash(4);
    let shard_five = hash(5);

    {
        let branch = fork_shards_registered([(3, shard_three), (5, shard_five)], &registry)?;
        assert_eq!(branch.shard_count(), 2);
        let live = registry.live_roots();
        assert!(live.contains(&shard_three));
        assert!(live.contains(&shard_five));
    }

    assert!(registry.live_roots().is_empty());
    Ok(())
}

#[test]
fn advance_swaps_a_singly_pinned_root() {
    let registry = BranchRegistry::new();
    let old = hash(6);
    let new = hash(7);
    registry.register(old);

    registry.advance(old, new);

    let live = registry.live_roots();
    assert!(!live.contains(&old));
    assert!(live.contains(&new));
}

#[test]
fn advance_preserves_a_refcounted_old_root() {
    // The §14.1 anchor pattern: a hash registered twice (anchor role + head
    // role) survives one advance with refcount 1 — the anchor stays pinned.
    let registry = BranchRegistry::new();
    let anchor = hash(8);
    let head = hash(9);
    registry.register(anchor);
    registry.register(anchor);

    registry.advance(anchor, head);

    let live = registry.live_roots();
    assert!(live.contains(&anchor));
    assert!(live.contains(&head));

    registry.deregister(anchor);
    assert!(!registry.live_roots().contains(&anchor));
}

#[test]
fn live_roots_snapshot_never_contains_neither_root_during_advance() {
    // Spy loop for §14.3(a)'s ordering half: `advance` does register-new +
    // deregister-old under ONE counts guard, so every `live_roots` snapshot
    // interleaved with a storm of advances contains the old or the new root.
    // A two-lock advance (deregister in one guard, register in another) fails
    // this probabilistically within a handful of iterations.
    let registry = BranchRegistry::new();
    let old = hash(10);
    let new = hash(11);
    registry.register(old);

    let flipper = {
        let registry = registry.clone();
        std::thread::spawn(move || {
            for _ in 0..2_000 {
                registry.advance(old, new);
                registry.advance(new, old);
            }
        })
    };

    for _ in 0..2_000 {
        let live = registry.live_roots();
        assert!(
            live.contains(&old) || live.contains(&new),
            "live_roots snapshot observed neither the superseded nor the advanced root"
        );
    }

    assert!(flipper.join().is_ok());
    let live = registry.live_roots();
    assert!(live.contains(&old) || live.contains(&new));
}

#[test]
fn guard_replace_retargets_the_pin_released_on_drop() {
    // §5 step 5, last bullet: after a commit advances a head, the handle's
    // guard must release the CURRENT pin on drop, not the stale one.
    let registry = BranchRegistry::new();
    let old = hash(12);
    let new = hash(13);
    let guard = registry.register_roots([old]);

    registry.advance(old, new);
    guard.replace(old, new);
    assert!(registry.live_roots().contains(&new));

    drop(guard);
    // A guard still holding the stale pin would deregister `old` (idempotent
    // no-op) and leak `new` here.
    assert!(registry.live_roots().is_empty());
}

#[test]
fn refcount_two_anchor_pattern_survives_one_advance_and_drop() {
    // End-to-end §14.1: registering one hash in both the anchor and head
    // roles (refcount 2), advancing the head role, then dropping the guard
    // releases everything — no first-advance special case anywhere.
    let registry = BranchRegistry::new();
    let anchor = hash(14);
    let head = hash(15);
    let guard = registry.register_roots([anchor, anchor]);

    registry.advance(anchor, head);
    guard.replace(anchor, head);

    let live = registry.live_roots();
    assert!(live.contains(&anchor), "anchor-role pin must survive");
    assert!(live.contains(&head));

    drop(guard);
    assert!(registry.live_roots().is_empty());
}

#[test]
fn guard_replace_of_an_unpinned_root_still_tracks_the_new_pin() {
    // Defensive leak-safety: replacing a root the guard never pinned appends
    // the new pin so drop always releases the advanced root.
    let registry = BranchRegistry::new();
    let old = hash(16);
    let new = hash(17);
    let guard = registry.register_roots([hash(18)]);

    registry.advance(old, new);
    guard.replace(old, new);
    assert!(registry.live_roots().contains(&new));

    drop(guard);
    assert!(registry.live_roots().is_empty());
}