blob: 06a1b3f9d2a7d4c49dea86e38689a88af9689d53 [file] [log] [blame]
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Module for testing and checking the invariants of a CRDT implementation.
//!
//! There are two main types of tests provided in this module:
//! * [`MergeInvariantTest`] tests the invariants required for state-based CRDTs directly on
//! arbitrary instances of the given `CrdtState`.
//! * [`Simulation`][simulation::Simulation] tests the CRDT implementations by defining valid
//! operations on a CRDT implementation, arbitrarily shuffling the resulting update messages, and
//! check to ensure that the implementations converge.
//!
//! Note that both of these tests only test for invariants that apply to all CRDT implementations,
//! namely eventual consistency. Unit tests are still required to ensure that the resulting state
//! matches the user intent.
//!
//! _Requires the `checker` feature_.
use std::fmt::Debug;
use crate::CrdtState;
use arbitrary::{Arbitrary, Unstructured};
pub(crate) mod eventual_delivery;
pub mod simulation;
pub mod test_crdt_union;
pub mod test_fakes;
pub mod utils;
/// Test case for the invariant properties required for `merge` functions of state-based CRDTs.
///
/// This type implements `Arbitrary` and can be used in property-testing and fuzzing in order to
/// verify these invariants automatically (see the `derive-fuzztest` crate).
///
/// This test checks against collections of state that are valid, according to
/// [`CrdtState::is_valid_collection`], which by default is any instance allowed by the type system.
/// If there are additional conditions for convergence, (e.g. if your CRDT relies on globally unique
/// timestamps), you should implement `is_valid_collection`. The helper functions
/// [`is_uniq`][crate::checker::utils::IterExt::is_uniq] and
/// [`iter_pairs`][crate::checker::utils::IterExt::iter_pairs_unordered] may be useful.
///
/// # Example
///
/// This example implements a last-writer-wins register that requires globally unique timestamps.
///
/// ```
/// use crdt::CrdtState;
/// use crdt::checker::MergeInvariantTest;
///
/// #[derive(Debug, Clone, PartialEq, Eq)]
/// pub struct LwwValue { timestamp: u64, value: String }
///
/// impl CrdtState for LwwValue {
/// fn merge(a: &Self, b: &Self) -> Self {
/// std::cmp::max_by_key(a, b, |v| v.timestamp).clone()
/// }
///
/// #[cfg(any(test, feature = "checker"))]
/// fn is_valid_collection<'a>(collection: impl IntoIterator<Item = &'a Self>) -> bool
/// where
/// Self: 'a,
/// {
/// use crdt::checker::utils::IterExt as _;
///
/// collection.into_iter().map(|s| &s.timestamp).is_uniq()
/// }
/// }
///
/// #[derive_fuzztest::fuzztest]
/// fn invariant_test(test: MergeInvariantTest<LwwValue>) {
/// test.test()
/// }
/// ```
#[derive(Clone, Debug)]
pub enum MergeInvariantTest<S: CrdtState + Eq + Debug> {
/// Test associativity `merge(merge(a, b), c) == merge(a, merge(b, c))`
Associativity(S, S, S),
/// Test commutativity `merge(a, b) == merge(b, a)`
Commutativity(S, S),
/// Test idempotence `merge(a, a) == a`
Idempotence(S),
}
impl<S: CrdtState + Eq + Debug> MergeInvariantTest<S> {
/// Run the test, panicking if the invariant is violated.
pub fn test(self) {
match self {
Self::Associativity(s1, s2, s3) => {
assert_eq!(
S::merge(&S::merge(&s1, &s2), &s3),
S::merge(&s1, &S::merge(&s2, &s3)),
"Associativity violated:\ns1={s1:#?}\ns2={s2:#?}\ns3={s3:#?}",
)
}
Self::Commutativity(s1, s2) => {
assert_eq!(
S::merge(&s1, &s2),
S::merge(&s2, &s1),
"Commutativity violated:\ns1={s1:#?}\ns2={s2:#?}"
)
}
Self::Idempotence(s1) => {
assert_eq!(S::merge(&s1, &s1), s1, "Idempotence violated:\ns1={s1:#?}")
}
}
}
}
impl<'a, S> Arbitrary<'a> for MergeInvariantTest<S>
where
S: CrdtState + Eq + Debug + Arbitrary<'a>,
{
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
match u.int_in_range(0..=2)? {
0 => {
let s1 = S::arbitrary(u)?;
let s2 = S::arbitrary(u)?;
let s3 = S::arbitrary(u)?;
if S::is_valid_collection([&s1, &s2, &s3]) {
Ok(MergeInvariantTest::Associativity(s1, s2, s3))
} else {
Err(arbitrary::Error::IncorrectFormat)
}
}
1 => {
let s1 = S::arbitrary(u)?;
let s2 = S::arbitrary(u)?;
if S::is_valid_collection([&s1, &s2]) {
Ok(MergeInvariantTest::Commutativity(s1, s2))
} else {
Err(arbitrary::Error::IncorrectFormat)
}
}
2 => {
let s = S::arbitrary(u)?;
if S::is_valid_collection([&s]) {
Ok(MergeInvariantTest::Idempotence(s))
} else {
Err(arbitrary::Error::IncorrectFormat)
}
}
_ => Err(arbitrary::Error::IncorrectFormat),
}
}
}
#[cfg(test)]
pub mod tests;