diff --git a/radicle/src/cob/op.rs b/radicle/src/cob/op.rs index 67c044da..f1d2754b 100644 --- a/radicle/src/cob/op.rs +++ b/radicle/src/cob/op.rs @@ -62,6 +62,18 @@ pub struct Op { pub timestamp: clock::Physical, } +impl PartialOrd for Op { + fn partial_cmp(&self, other: &Self) -> Option { + self.id().partial_cmp(&other.id()) + } +} + +impl Ord for Op { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.id().cmp(&other.id()) + } +} + impl Op { pub fn new( action: A, diff --git a/radicle/src/cob/store.rs b/radicle/src/cob/store.rs index 06cc39dc..b92ce6a7 100644 --- a/radicle/src/cob/store.rs +++ b/radicle/src/cob/store.rs @@ -54,6 +54,15 @@ pub trait FromHistory: Sized + Default { 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>) -> Result { + let mut state = Self::default(); + state.apply(ops)?; + + Ok(state) + } } /// Store error. diff --git a/radicle/src/cob/test.rs b/radicle/src/cob/test.rs index a76cd7a9..e444e9cd 100644 --- a/radicle/src/cob/test.rs +++ b/radicle/src/cob/test.rs @@ -1,25 +1,31 @@ +use std::collections::BTreeSet; use std::marker::PhantomData; -use std::ops::Deref; +use std::ops::{ControlFlow, Deref}; use nonempty::NonEmpty; use serde::Serialize; -use crate::cob::op::Op; +use crate::cob::op::{Op, Ops}; use crate::cob::store::encoding; use crate::cob::History; use crate::git::Oid; use crate::test::arbitrary; +use super::store::FromHistory; + /// Convenience type for building histories. #[derive(Debug, Clone)] -pub struct HistoryBuilder { +pub struct HistoryBuilder { history: History, - witness: PhantomData, resource: Oid, + witness: PhantomData, } -impl HistoryBuilder { - pub fn new(op: &Op) -> HistoryBuilder { +impl HistoryBuilder +where + T::Action: Serialize + Eq, +{ + pub fn new(op: &Op) -> HistoryBuilder { let entry = arbitrary::oid(); let resource = arbitrary::oid(); let contents = encoding::encode(&op.action).unwrap(); @@ -37,7 +43,7 @@ impl HistoryBuilder { } } - pub fn append(&mut self, op: &Op) -> &mut Self { + pub fn append(&mut self, op: &Op) -> &mut Self { self.history.extend( arbitrary::oid(), op.author, @@ -51,6 +57,27 @@ impl HistoryBuilder { pub fn merge(&mut self, other: Self) { self.history.merge(other.history); } + + /// Return a sorted list of operations by traversing the history in topological order. + pub fn sorted(&self) -> Vec> { + 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>> { + let mut permutations = BTreeSet::new(); + while permutations.len() < n { + permutations.insert(self.sorted()); + } + permutations.into_iter() + } } impl Deref for HistoryBuilder { @@ -62,6 +89,9 @@ impl Deref for HistoryBuilder { } /// Create a new test history. -pub fn history(op: &Op) -> HistoryBuilder { +pub fn history(op: &Op) -> HistoryBuilder +where + T::Action: Serialize + Eq, +{ HistoryBuilder::new(op) } diff --git a/radicle/src/cob/thread.rs b/radicle/src/cob/thread.rs index fe8e59f8..181e5e82 100644 --- a/radicle/src/cob/thread.rs +++ b/radicle/src/cob/thread.rs @@ -373,6 +373,7 @@ mod tests { use super::*; use crate as radicle; use crate::cob::store::FromHistory; + use crate::cob::test; use crate::crypto::test::signer::MockSigner; #[derive(Clone)] @@ -641,6 +642,33 @@ mod tests { ); } + #[test] + fn test_histories() { + let mut alice = Actor::::default(); + let mut bob = Actor::::default(); + let mut eve = Actor::::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::(&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] fn prop_invariants() { fn property(log: Changes<3>) -> TestResult {