cob: Start testing history permutations

Signed-off-by: Alexis Sellier <self@cloudhead.io>
This commit is contained in:
Alexis Sellier 2023-01-01 23:28:44 +01:00
parent 139f095d51
commit 67f9c3554d
No known key found for this signature in database
4 changed files with 87 additions and 8 deletions

View File

@ -62,6 +62,18 @@ pub struct Op<A> {
pub timestamp: clock::Physical, pub timestamp: clock::Physical,
} }
impl<A: Eq> PartialOrd for Op<A> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.id().partial_cmp(&other.id())
}
}
impl<A: Eq> Ord for Op<A> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.id().cmp(&other.id())
}
}
impl<A: Serialize> Op<A> { impl<A: Serialize> Op<A> {
pub fn new( pub fn new(
action: A, action: A,

View File

@ -54,6 +54,15 @@ pub trait FromHistory: Sized + Default {
Ok((obj, history.clock().into())) Ok((obj, history.clock().into()))
} }
/// Create an object from individual operations.
/// Returns an error if any of the operations fails to apply.
fn from_ops(ops: impl IntoIterator<Item = Op<Self::Action>>) -> Result<Self, Self::Error> {
let mut state = Self::default();
state.apply(ops)?;
Ok(state)
}
} }
/// Store error. /// Store error.

View File

@ -1,25 +1,31 @@
use std::collections::BTreeSet;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::ops::Deref; use std::ops::{ControlFlow, Deref};
use nonempty::NonEmpty; use nonempty::NonEmpty;
use serde::Serialize; use serde::Serialize;
use crate::cob::op::Op; use crate::cob::op::{Op, Ops};
use crate::cob::store::encoding; use crate::cob::store::encoding;
use crate::cob::History; use crate::cob::History;
use crate::git::Oid; use crate::git::Oid;
use crate::test::arbitrary; use crate::test::arbitrary;
use super::store::FromHistory;
/// Convenience type for building histories. /// Convenience type for building histories.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct HistoryBuilder<A> { pub struct HistoryBuilder<T> {
history: History, history: History,
witness: PhantomData<A>,
resource: Oid, resource: Oid,
witness: PhantomData<T>,
} }
impl<A: Serialize> HistoryBuilder<A> { impl<T: FromHistory> HistoryBuilder<T>
pub fn new(op: &Op<A>) -> HistoryBuilder<A> { where
T::Action: Serialize + Eq,
{
pub fn new(op: &Op<T::Action>) -> HistoryBuilder<T> {
let entry = arbitrary::oid(); let entry = arbitrary::oid();
let resource = arbitrary::oid(); let resource = arbitrary::oid();
let contents = encoding::encode(&op.action).unwrap(); let contents = encoding::encode(&op.action).unwrap();
@ -37,7 +43,7 @@ impl<A: Serialize> HistoryBuilder<A> {
} }
} }
pub fn append(&mut self, op: &Op<A>) -> &mut Self { pub fn append(&mut self, op: &Op<T::Action>) -> &mut Self {
self.history.extend( self.history.extend(
arbitrary::oid(), arbitrary::oid(),
op.author, op.author,
@ -51,6 +57,27 @@ impl<A: Serialize> HistoryBuilder<A> {
pub fn merge(&mut self, other: Self) { pub fn merge(&mut self, other: Self) {
self.history.merge(other.history); self.history.merge(other.history);
} }
/// Return a sorted list of operations by traversing the history in topological order.
pub fn sorted(&self) -> Vec<Op<T::Action>> {
self.history.traverse(Vec::new(), |mut acc, entry| {
let Ops(ops) =
Ops::try_from(entry).expect("HistoryBuilder::sorted: operations must be valid");
acc.extend(ops);
ControlFlow::Continue(acc)
})
}
/// Return `n` permutations of the topological ordering of operations.
/// *This function will never return if less than `n` permutations exist.*
pub fn permutations(&self, n: usize) -> impl IntoIterator<Item = Vec<Op<T::Action>>> {
let mut permutations = BTreeSet::new();
while permutations.len() < n {
permutations.insert(self.sorted());
}
permutations.into_iter()
}
} }
impl<A> Deref for HistoryBuilder<A> { impl<A> Deref for HistoryBuilder<A> {
@ -62,6 +89,9 @@ impl<A> Deref for HistoryBuilder<A> {
} }
/// Create a new test history. /// Create a new test history.
pub fn history<A: Serialize>(op: &Op<A>) -> HistoryBuilder<A> { pub fn history<T: FromHistory>(op: &Op<T::Action>) -> HistoryBuilder<T>
where
T::Action: Serialize + Eq,
{
HistoryBuilder::new(op) HistoryBuilder::new(op)
} }

View File

@ -373,6 +373,7 @@ mod tests {
use super::*; use super::*;
use crate as radicle; use crate as radicle;
use crate::cob::store::FromHistory; use crate::cob::store::FromHistory;
use crate::cob::test;
use crate::crypto::test::signer::MockSigner; use crate::crypto::test::signer::MockSigner;
#[derive(Clone)] #[derive(Clone)]
@ -641,6 +642,33 @@ mod tests {
); );
} }
#[test]
fn test_histories() {
let mut alice = Actor::<MockSigner>::default();
let mut bob = Actor::<MockSigner>::default();
let mut eve = Actor::<MockSigner>::default();
let a0 = alice.comment("Alice's comment", None);
let b0 = bob.comment("Bob's reply", Some(a0.id())); // Bob and Eve's replies are concurrent.
let e0 = eve.comment("Eve's reply", Some(a0.id()));
let mut a = test::history::<Thread>(&a0);
let mut b = a.clone();
let mut e = a.clone();
b.append(&b0);
e.append(&e0);
a.merge(b);
a.merge(e);
let (expected, _) = Thread::from_history(&a).unwrap();
for permutation in a.permutations(2) {
let actual = Thread::from_ops(permutation).unwrap();
assert_eq!(actual, expected);
}
}
#[test] #[test]
fn prop_invariants() { fn prop_invariants() {
fn property(log: Changes<3>) -> TestResult { fn property(log: Changes<3>) -> TestResult {