exonum-testkit 0.1.1

Testkit for Exonum blockchain framework, allowing to test service APIs synchronously.
Documentation
//! Routines for comparison between 2 states.

use exonum::storage::Snapshot;

/// Facilitation of comparison between 2 states.
#[derive(Debug)]
pub struct Comparison<T> {
    old: T,
    new: T,
}

impl<T> Comparison<T> {
    /// Creates a comparison between 2 states.
    pub fn new(old: T, new: T) -> Self {
        Comparison { old, new }
    }

    /// Maps this comparison to another type of objects.
    ///
    /// # Examples
    ///
    /// ```
    /// # use exonum_testkit::compare::Comparison;
    /// let comparison = Comparison::new(vec![1, 2, 3], vec![4, 5])
    ///     .map(Vec::len);
    /// // Now, this is a comparison between `3` and `2`
    /// ```
    pub fn map<'a, F, U>(&'a self, f: F) -> Comparison<U>
    where
        F: Fn(&'a T) -> U,
    {
        Comparison::new(f(&self.old), f(&self.new))
    }

    /// Asserts a statement about the older state in this comparison.
    ///
    /// # Panics
    ///
    /// If the statement does not hold.
    ///
    /// # Examples
    ///
    /// ```
    /// # use exonum_testkit::compare::Comparison;
    /// Comparison::new(vec![1, 2, 3], vec![4, 5])
    ///     .map(Vec::len)
    ///     .assert_before("Array length <= 5", |&len| len <= 5);
    /// ```
    pub fn assert_before<P>(&self, message: &str, pred: P) -> &Self
    where
        P: Fn(&T) -> bool,
    {
        assert!(
            pred(&self.old),
            format!("Precondition does not hold: {}", message)
        );
        self
    }

    /// Asserts a statement about the newer state in this comparison.
    ///
    /// # Panics
    ///
    /// If the statement does not hold.
    ///
    /// # Examples
    ///
    /// ```
    /// # use exonum_testkit::compare::Comparison;
    /// Comparison::new(vec![1, 2, 3], vec![4, 5])
    ///     .assert_after("The second element is greater than first", |v| {
    ///         v[1] > v[0]
    ///     });
    /// ```
    pub fn assert_after<P>(&self, message: &str, pred: P) -> &Self
    where
        P: Fn(&T) -> bool,
    {
        assert!(
            pred(&self.new),
            format!("Postcondition does not hold: {}", message)
        );
        self
    }

    /// Asserts a statement about the both states in this comparison.
    ///
    /// # Panics
    ///
    /// If the statement does not hold.
    ///
    /// # Examples
    ///
    /// ```
    /// # use exonum_testkit::compare::Comparison;
    /// Comparison::new(vec![1, 2, 3], vec![4, 5])
    ///     .map(Vec::len)
    ///     .assert("Less elements after", |&old, &new| old > new);
    /// ```
    pub fn assert<P>(&self, message: &str, pred: P) -> &Self
    where
        P: Fn(&T, &T) -> bool,
    {
        assert!(
            pred(&self.old, &self.new),
            format!("Comparison does not hold: {}", message)
        );
        self
    }

    /// Asserts a statement that should hold true for both states in this comparison.
    ///
    /// # Panics
    ///
    /// If the statement does not hold for either of states.
    ///
    /// # Examples
    ///
    /// ```
    /// # use exonum_testkit::compare::Comparison;
    /// Comparison::new(vec![1, 2, 3], vec![4, 5])
    ///     .map(Vec::len)
    ///     .assert_inv("More than 1 element in array", |&len| len > 1);
    /// ```
    pub fn assert_inv<P>(&self, message: &str, pred: P) -> &Self
    where
        P: Fn(&T) -> bool,
    {
        assert!(
            pred(&self.old),
            format!("Invariant does not hold for the older state: {}", message)
        );
        assert!(
            pred(&self.new),
            format!("Invariant does not hold for the newer state: {}", message)
        );
        self
    }
}

impl<T: PartialEq + ::std::fmt::Debug> Comparison<T> {
    /// Asserts that the states are equal (by the `PartialEq` definition).
    ///
    /// # Panics
    ///
    /// If the states are not equal.
    ///
    /// # Examples
    ///
    /// ```
    /// # use exonum_testkit::compare::Comparison;
    /// Comparison::new(vec![1, 2, 3], vec![4, 5, 6])
    ///     .map(Vec::len)
    ///     .assert_eq("Array length doesn't change");
    /// ```
    pub fn assert_eq(&self, message: &str) -> &Self {
        assert_eq!(self.old, self.new, "Invariant does not hold: {}", message);
        self
    }

    /// Asserts that the states are equal (by the `PartialEq` definition).
    ///
    /// # Panics
    ///
    /// If the states are equal.
    ///
    /// # Examples
    ///
    /// ```
    /// # use exonum_testkit::compare::Comparison;
    /// Comparison::new(vec![1, 2, 3], vec![4, 5])
    ///     .map(Vec::len)
    ///     .assert_ne("Array length should change");
    /// ```
    pub fn assert_ne(&self, message: &str) -> &Self {
        assert_ne!(self.old, self.new, "Expected change: {}", message);
        self
    }
}

/// Trait facilitating comparison between 2 `Snapshot`s taken at different times.
///
/// # Examples
///
/// Typical usage involves `map`ping the resulting comparison through the schema:
///
/// ```ignore
/// let mut testkit = ...;
/// let old_snapshot = testkit.snapshot();
/// // Mutate the testkit state somehow...
///
/// testkit.snapshot()
///     .compare(old_snapshot)
///     .map(ServiceSchema::new)
///     .assert("Something about the schema", |old, schema| {
///         // Assertions...
///     });
/// ```
///
/// Here `ServiceSchema` is a public struct defined in a service libarary that has public `new`
/// method with a signature like `fn<S: AsRef<Snapshot>>(view: S) -> Self`.
pub trait ComparableSnapshot<S> {
    /// Compares this snapshot with an older one.
    fn compare(self, old: S) -> Comparison<Box<Snapshot>>;
}

impl ComparableSnapshot<Box<Snapshot>> for Box<Snapshot> {
    fn compare(self, old: Box<Snapshot>) -> Comparison<Box<Snapshot>> {
        Comparison::new(old, self)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_assertions() {
        let comp = Comparison::new(vec![1, 2, 3], vec![4, 5, 6]);
        comp.assert_before("Array should have length 3", |old| old.len() == 3)
            .assert_after("Array should have length 3", |new| new.len() == 3)
            .assert("Array should be transformed", |old, new| {
                new.iter().enumerate().all(|(i, &x)| x == old[i] + 3)
            })
            .map(Vec::len)
            .assert("Lengths should be the same", |old, new| old == new);
    }

    #[test]
    #[should_panic(expected = "Precondition does not hold: Array should have length 3")]
    fn test_assertion_precondition_failure() {
        let comp = Comparison::new(vec![1, 2], vec![4, 5, 6]);
        comp.assert_before("Array should have length 3", |old| old.len() == 3);
    }

    #[test]
    #[should_panic(expected = "Postcondition does not hold: Array should have length 3")]
    fn test_assertion_postcondition_failure() {
        let comp = Comparison::new(vec![1, 2, 3], vec![4, 5]);
        comp.assert_after("Array should have length 3", |new| new.len() == 3);
    }

    #[test]
    #[should_panic(expected = "Comparison does not hold: Array should be transformed")]
    fn test_assertion_transform_failure() {
        let comp = Comparison::new(vec![1, 2, 3], vec![4, 5, 7]);
        comp.assert("Array should be transformed", |old, new| {
            new.iter().enumerate().all(|(i, &x)| x == old[i] + 3)
        });
    }

    #[test]
    #[should_panic(expected = "Array length more than 1")]
    fn test_assertion_invariant_failure_pre() {
        let comp = Comparison::new(vec![1], vec![2, 3, 4]);
        comp.assert_inv("Array length more than 1", |v| v.len() > 1);
    }

    #[test]
    #[should_panic(expected = "Array length more than 1")]
    fn test_assertion_invariant_failure_post() {
        let comp = Comparison::new(vec![1, 2, 3], vec![4]);
        comp.assert_inv("Array length more than 1", |v| v.len() > 1);
    }
}