cob: Start testing history permutations
Signed-off-by: Alexis Sellier <self@cloudhead.io>
This commit is contained in:
parent
139f095d51
commit
67f9c3554d
|
|
@ -62,6 +62,18 @@ pub struct Op<A> {
|
|||
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> {
|
||||
pub fn new(
|
||||
action: A,
|
||||
|
|
|
|||
|
|
@ -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<Item = Op<Self::Action>>) -> Result<Self, Self::Error> {
|
||||
let mut state = Self::default();
|
||||
state.apply(ops)?;
|
||||
|
||||
Ok(state)
|
||||
}
|
||||
}
|
||||
|
||||
/// Store error.
|
||||
|
|
|
|||
|
|
@ -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<A> {
|
||||
pub struct HistoryBuilder<T> {
|
||||
history: History,
|
||||
witness: PhantomData<A>,
|
||||
resource: Oid,
|
||||
witness: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<A: Serialize> HistoryBuilder<A> {
|
||||
pub fn new(op: &Op<A>) -> HistoryBuilder<A> {
|
||||
impl<T: FromHistory> HistoryBuilder<T>
|
||||
where
|
||||
T::Action: Serialize + Eq,
|
||||
{
|
||||
pub fn new(op: &Op<T::Action>) -> HistoryBuilder<T> {
|
||||
let entry = arbitrary::oid();
|
||||
let resource = arbitrary::oid();
|
||||
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(
|
||||
arbitrary::oid(),
|
||||
op.author,
|
||||
|
|
@ -51,6 +57,27 @@ impl<A: Serialize> HistoryBuilder<A> {
|
|||
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<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> {
|
||||
|
|
@ -62,6 +89,9 @@ impl<A> Deref for HistoryBuilder<A> {
|
|||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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::<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]
|
||||
fn prop_invariants() {
|
||||
fn property(log: Changes<3>) -> TestResult {
|
||||
|
|
|
|||
Loading…
Reference in New Issue