triblespace 0.46.2

The Triblespace: A lightweight knowledge base for rust.
Documentation
use anybytes::Bytes;
use proptest::prelude::*;
use std::collections::HashMap;
use std::collections::HashSet;
use triblespace::core::blob::encodings::UnknownBlob;
use triblespace::core::repo::PushResult;
use triblespace::prelude::blobencodings::SimpleArchive;
use triblespace::prelude::inlineencodings::Handle;
use triblespace::prelude::*;

#[derive(Debug, Clone)]
enum Op {
    Put(Vec<u8>),
    Flush,
    Refresh,
    Restore,
    Get(usize),
    BranchUpdate { branch: usize, handle: usize },
    BranchHead(usize),
    BranchList,
}

#[derive(Debug, Clone)]
enum ActorOp {
    Run { actor: usize, op: Op },
    Check,
}

#[derive(Debug, Clone)]
struct Scenario {
    actors: usize,
    ops: Vec<ActorOp>,
}

fn actor_op_strategy(actors: usize, branches: usize) -> impl Strategy<Value = ActorOp> {
    let data = prop::collection::vec(any::<u8>(), 0..32);
    let idx = 0usize..20;
    prop_oneof![
        (0..actors, data.clone()).prop_map(|(actor, data)| ActorOp::Run {
            actor,
            op: Op::Put(data)
        }),
        (0..actors).prop_map(|actor| ActorOp::Run {
            actor,
            op: Op::Flush
        }),
        (0..actors).prop_map(|actor| ActorOp::Run {
            actor,
            op: Op::Refresh
        }),
        (0..actors).prop_map(|actor| ActorOp::Run {
            actor,
            op: Op::Restore
        }),
        (0..actors, idx.clone()).prop_map(|(actor, i)| ActorOp::Run {
            actor,
            op: Op::Get(i)
        }),
        (0..actors, 0..branches, idx.clone()).prop_map(|(actor, branch, i)| ActorOp::Run {
            actor,
            op: Op::BranchUpdate { branch, handle: i }
        }),
        (0..actors, 0..branches).prop_map(|(actor, branch)| ActorOp::Run {
            actor,
            op: Op::BranchHead(branch)
        }),
        (0..actors).prop_map(|actor| ActorOp::Run {
            actor,
            op: Op::BranchList
        }),
        Just(ActorOp::Check),
    ]
}

fn scenario_strategy(max_actors: usize) -> impl Strategy<Value = Scenario> {
    (1..=max_actors, 1usize..=4).prop_flat_map(move |(actors, branches)| {
        let op = actor_op_strategy(actors, branches);
        prop::collection::vec(op, 1..20).prop_map(move |ops| Scenario { actors, ops })
    })
}

fn branch_id(idx: usize) -> Id {
    let mut raw = [0u8; 16];
    raw[0] = (idx as u8).saturating_add(1);
    Id::new(raw).unwrap()
}

proptest! {
    #[test]
    fn pile_operation_sequences_are_consistent(
        scenario in scenario_strategy(4)
    ) {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("sim.pile");
        std::fs::File::create(&path).unwrap();
        let mut piles: Vec<Pile> =
            (0..scenario.actors).map(|_| Pile::open(&path).unwrap()).collect();
        let mut expected: HashMap<Inline<Handle<UnknownBlob>>, Vec<u8>> = HashMap::new();
        let mut handles: Vec<Inline<Handle<UnknownBlob>>> = Vec::new();
        let mut branches: HashMap<Id, Inline<Handle<SimpleArchive>>> = HashMap::new();

        for op in scenario.ops {
            match op {
                ActorOp::Run { actor, op } => match op {
                    Op::Put(data) => {
                        let blob: Blob<UnknownBlob> =
                            Blob::new(Bytes::from_source(data.clone()));
                        let handle = piles[actor].put::<UnknownBlob, _>(blob).unwrap();
                        expected.insert(handle, data);
                        handles.push(handle);
                    }
                    Op::Flush => {
                        piles[actor].flush().unwrap();
                    }
                    Op::Refresh => {
                        let _ = piles[actor].refresh();
                    }
                    Op::Restore => {
                        // Simulate crash recovery: close and reopen with restore
                        let path = dir.path().join("sim.pile");
                        piles[actor] = Pile::open(&path).unwrap();
                        piles[actor].restore().unwrap();
                    }
                    Op::Get(i) => {
                        if !handles.is_empty() {
                            let handle = handles[i % handles.len()];
                            piles[actor].refresh().unwrap();
                            if let Ok(blob) = piles[actor]
                                .reader()
                                .unwrap()
                                .get::<Blob<UnknownBlob>, _>(handle)
                            {
                                prop_assert_eq!(
                                    blob.bytes.as_ref(),
                                    expected.get(&handle).unwrap().as_slice()
                                );
                            }
                        }
                    }
                    Op::BranchUpdate { branch, handle } => {
                        if !handles.is_empty() {
                            let id = branch_id(branch);
                            let h = handles[handle % handles.len()].transmute();
                            let old = branches.get(&id).copied();
                            let res = piles[actor].update(id, old, Some(h)).unwrap();
                            match res {
                                PushResult::Success() => {
                                    branches.insert(id, h);
                                }
                                PushResult::Conflict(c) => {
                                    prop_assert_eq!(c, old);
                                    branches.insert(id, h);
                                }
                            }
                        }
                    }
                    Op::BranchHead(branch) => {
                        let id = branch_id(branch);
                        piles[actor].refresh().unwrap();
                        let head = piles[actor].head(id).unwrap();
                        prop_assert_eq!(head, branches.get(&id).copied());
                    }
                    Op::BranchList => {
                        piles[actor].refresh().unwrap();
                        let iter = piles[actor].pins().unwrap();
                        let found: HashSet<Id> = iter.map(|r| r.unwrap()).collect();
                        let expected_ids: HashSet<Id> = branches.keys().copied().collect();
                        prop_assert_eq!(found, expected_ids);
                    }
                },
                ActorOp::Check => {
                    for pile in &mut piles {
                        pile.refresh().unwrap();
                    }
                    for pile in &mut piles {
                        let reader = pile.reader().unwrap();
                        for (handle, data) in &expected {
                            if let Ok(blob) = reader.get::<Blob<UnknownBlob>, _>(*handle) {
                                prop_assert_eq!(blob.bytes.as_ref(), data.as_slice());
                            }
                        }
                        let iter = pile.pins().unwrap();
                        let found: HashSet<Id> = iter.map(|r| r.unwrap()).collect();
                        let expected_ids: HashSet<Id> = branches.keys().copied().collect();
                        prop_assert_eq!(found, expected_ids);
                        for (id, head) in &branches {
                            let h = pile.head(*id).unwrap();
                            prop_assert_eq!(h, Some(*head));
                        }
                    }
                }
            }
        }

        for pile in &mut piles {
            pile.flush().unwrap();
            pile.refresh().unwrap();
        }
        for pile in piles {
            pile.close().unwrap();
        }
        let mut pile_final: Pile = Pile::open(&path).unwrap();
        pile_final.restore().unwrap();
        let reader = pile_final.reader().unwrap();
        for (handle, data) in &expected {
            let blob = reader.get::<Blob<UnknownBlob>, _>(*handle).unwrap();
            assert_eq!(blob.bytes.as_ref(), data.as_slice());
        }
        let iter = pile_final.pins().unwrap();
        let found: HashSet<Id> = iter.map(|r| r.unwrap()).collect();
        let expected_ids: HashSet<Id> = branches.keys().copied().collect();
        assert_eq!(found, expected_ids);
        for (id, head) in &branches {
            let h = pile_final.head(*id).unwrap();
            assert_eq!(h, Some(*head));
        }
        pile_final.close().unwrap();
    }
}