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,
|
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,
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue