speculoos 0.11.0

Fluent test assertions
Documentation
#![allow(clippy::wrong_self_convention)]

//! Fluent test assertions in Rust
//!
//! Speculoos is a testing framework designed to make your assertions read like plain English.
//! This allows you to more easily expose the intent of your test, rather than having it shrouded by
//! assertions which work, but are opaque on their meaning.
//!
//! Methods available to assert with are dependent upon the type of the subject under test.
//! Assertions are available for some basic types, but there is still a great deal missing from the
//! standard library.
//!
//! ## Usage
//!
//! Add the dependency to your `Cargo.toml`:
//!
//! ```toml
//! [dependencies]
//! speculoos = "0.6.0"
//! ```
//!
//! Then add this to your crate:
//!
//! ```rust
//! extern crate speculoos;
//! ```
//!
//! If you want macro support, include `#[macro_use]` to the declaration:
//!
//! ```rust, ignore
//! #[macro_use]
//! extern crate speculoos;
//! ```
//!
//! To quickly start using assertions, `use` the prelude module:
//!
//! ```rust
//! use speculoos::prelude::*;
//! ```
//!
//! ## Example
//!
//! We're going to make a few assertions on a `String` we create. Normally you would
//! want to assert on the output of something, but we'll just pretend that something created it.
//!
//! First, we'll create a new test with our `String`.
//!
//! ```rust
//! #[test]
//! pub fn should_be_the_correct_string() {
//!     let subject = "Hello World!";
//! }
//! ```
//!
//! Note that it is good practice to make sure that you name your test in a way that actually
//! explains what it is trying to test. When you have a number of tests, and one of them fails,
//! something like this is easier to understand:
//!
//! ```rust
//! #[test]
//! pub fn should_return_false_if_condition_does_not_hold() {
//!     // ...
//! }
//! ```
//!
//! Rather than if you have a test like this:
//!
//! ```rust
//! #[test]
//! pub fn should_work() {
//!     // ...
//! }
//! ```
//!
//! Unfortunately, our test isn't named very well at the moment, but given the lack of context,
//! it'll have to do for now.
//!
//! Now that we have something to test, we need to actually start asserting on it. The first part
//! to that is to provide it to the `assert_that` function. Note that we need to provide it as a
//! reference.
//!
//! ```rust
//! # use speculoos::prelude::*;
//! #[test]
//! pub fn should_be_the_correct_string() {
//!     let subject = "Hello World!";
//!     assert_that(&subject);
//! }
//! ```
//!
//! If we run that with `cargo test`, we'll see the following output:
//!
//! ```bash
//! running 1 test
//! test should_be_the_correct_string ... ok
//! ```
//!
//! Our test compiles and passes, but we still haven't made any assertions. Let's make a simple one
//! to start with. We'll check to see that it starts with the letter 'H'.
//!
//! ```rust
//! # use speculoos::prelude::*;
//! #[test]
//! pub fn should_be_the_correct_string() {
//!     let subject = "Hello World!";
//!     assert_that(&subject).starts_with(&"H");
//! }
//! ```
//!
//! Once you run this, you'll notice that the test still passes. That's because we've just proven
//! something that was already true. Usually you'll want to start with a failing test, and then
//! change your code to make it pass, rather than writing the test after the implementation.
//!
//! But for the purpose of exploration, let's break the actual value. We'll change "Hello World!"
//! to be "ello World!".
//!
//! ```rust
//! # use speculoos::prelude::*;
//! #[test]
//! pub fn should_be_the_correct_string() {
//!     let subject = "ello World!";
//!     assert_that(&subject).starts_with(&"H");
//! }
//! ```
//!
//! This time, we see that the test fails, and we also get some output from our assertion to tell
//! us what it was, and what it was expected to be:
//!
//! ```bash
//! running 1 test
//! test should_be_the_correct_string ... FAILED
//!
//! failures:
//!
//! ---- should_be_the_correct_string stdout ----
//!     thread 'should_be_the_correct_string' panicked at 'expected string starting with <"H"> but
//!     was <"ello World!">', src/lib.rs:204
//! ```
//!
//! Great! So we've just encountered a failing test. This particular case is quite easy to fix up
//! (just add the letter 'H' back to the start of the `String`), but we can also see that the panic
//! message tells us enough information to work that out as well.
//!
//! Now, this was just a simple example, and there's a number of features not demonstrated, but
//! hopefully it's enough to start you off with writing assertions in your tests using Speculoos.

use std::borrow::Borrow;
use std::cmp::PartialEq;
use std::fmt::Debug;

use colours::{TERM_BOLD, TERM_RED, TERM_RESET};

pub mod boolean;
pub mod hashmap;
pub mod hashset;
pub mod iter;
pub mod numeric;
pub mod option;
pub mod path;
pub mod prelude;
pub mod result;
pub mod string;
pub mod vec;

// Disable colours during tests, otherwise trying to assert on the panic message becomes
// significantly more annoying.
#[cfg(not(test))]
mod colours {
    pub const TERM_RED: &str = "\x1B[31m";
    pub const TERM_BOLD: &str = "\x1B[1m";
    pub const TERM_RESET: &str = "\x1B[0m";
}

#[cfg(test)]
mod colours {
    pub const TERM_RED: &str = "";
    pub const TERM_BOLD: &str = "";
    pub const TERM_RESET: &str = "";
}

#[cfg(feature = "num")]
extern crate num;

#[macro_export]
macro_rules! assert_that {
    (&$subject:tt) => {
        assert_that!($subject)
    };
    ($subject:tt) => {{
        let line = line!();
        let file = file!();
        assert_that(&$subject).at_location(format!("{}:{}", file, line))
    }};
    (&$subject:expr) => {
        assert_that!($subject)
    };
    ($subject:expr) => {{
        let line = line!();
        let file = file!();
        assert_that(&$subject).at_location(format!("{}:{}", file, line))
    }};
}

#[macro_export]
macro_rules! asserting {
    (&$description:tt) => {
        asserting!($description)
    };
    ($description:tt) => {{
        let line = line!();
        let file = file!();
        asserting(&$description).at_location(format!("{}:{}", file, line))
    }};
}

pub trait DescriptiveSpec<'r> {
    fn subject_name(&self) -> Option<&'r str>;
    fn location(&self) -> Option<String>;
    fn description(&self) -> Option<&'r str>;
}

/// A failed assertion.
///
/// This exposes builder methods to construct the final failure message.
#[derive(Debug)]
pub struct AssertionFailure<'r, T: 'r> {
    spec: &'r T,
    expected: Option<String>,
    actual: Option<String>,
}

/// A description for an assertion.
///
/// This is created by the `asserting` function.
#[derive(Debug)]
pub struct SpecDescription<'r> {
    value: &'r str,
    location: Option<String>,
}

/// An assertion.
///
/// This is created by either the `assert_that` function, or by calling `that` on a
/// `SpecDescription`.
#[derive(Debug)]
pub struct Spec<'s, S: 's> {
    pub subject: &'s S,
    pub subject_name: Option<&'s str>,
    pub location: Option<String>,
    pub description: Option<&'s str>,
}

/// Wraps a subject in a `Spec` to provide assertions against it.
///
/// The subject must be a reference.
pub fn assert_that<S>(subject: &S) -> Spec<S> {
    Spec {
        subject,
        subject_name: None,
        location: None,
        description: None,
    }
}

/// Describes an assertion.
pub fn asserting(description: &str) -> SpecDescription {
    SpecDescription {
        value: description,
        location: None,
    }
}

impl<'r> SpecDescription<'r> {
    pub fn at_location(self, location: String) -> Self {
        let mut description = self;

        description.location = Some(location);
        description
    }

    /// Creates a new assertion, passing through its description.
    pub fn that<S>(self, subject: &'r S) -> Spec<'r, S> {
        Spec {
            subject,
            subject_name: None,
            location: self.location,
            description: Some(self.value),
        }
    }
}

impl<'r, T> DescriptiveSpec<'r> for Spec<'r, T> {
    fn subject_name(&self) -> Option<&'r str> {
        self.subject_name
    }

    fn location(&self) -> Option<String> {
        self.location.clone()
    }

    fn description(&self) -> Option<&'r str> {
        self.description
    }
}

impl<'r, T: DescriptiveSpec<'r>> AssertionFailure<'r, T> {
    /// Construct a new AssertionFailure from a DescriptiveSpec.
    pub fn from_spec(spec: &'r T) -> AssertionFailure<'r, T> {
        AssertionFailure {
            spec,
            expected: None,
            actual: None,
        }
    }

    /// Builder method to add the expected value for the panic message.
    pub fn with_expected(&mut self, expected: String) -> &mut Self {
        let mut assertion = self;
        assertion.expected = Some(expected);

        assertion
    }

    /// Builder method to add the actual value for the panic message.
    pub fn with_actual(&mut self, actual: String) -> &mut Self {
        let mut assertion = self;
        assertion.actual = Some(actual);

        assertion
    }

    /// Builds the failure message with a description (if present), the expected value,
    /// and the actual value and then calls `panic` with the created message.
    pub fn fail(&mut self) {
        assert!(
            !(self.expected.is_none() || self.actual.is_none()),
            "invalid assertion"
        );

        let location = self.maybe_build_location();
        let subject_name = self.maybe_build_subject_name();
        let description = self.maybe_build_description();

        panic!(
            "{}{}\n\t{}expected: {}\n\t but was: {}{}\n{}",
            description,
            subject_name,
            TERM_RED,
            self.expected.clone().unwrap(),
            self.actual.clone().unwrap(),
            TERM_RESET,
            location
        )
    }

    /// Calls `panic` with the provided message, prepending the assertion description
    /// if present.
    fn fail_with_message(&mut self, message: String) {
        let location = self.maybe_build_location();
        let subject_name = self.maybe_build_subject_name();
        let description = self.maybe_build_description();

        panic!(
            "{}{}\n\t{}{}{}\n{}",
            description, subject_name, TERM_RED, message, TERM_RESET, location
        )
    }

    fn maybe_build_location(&self) -> String {
        match self.spec.location() {
            Some(value) => format!("\n\t{}at location: {}{}\n", TERM_BOLD, value, TERM_RESET),
            None => "".to_string(),
        }
    }

    fn maybe_build_description(&self) -> String {
        match self.spec.description() {
            Some(value) => format!("\n\t{}{}:{}", TERM_BOLD, value, TERM_RESET),
            None => "".to_string(),
        }
    }

    fn maybe_build_subject_name(&self) -> String {
        match self.spec.subject_name() {
            Some(value) => format!("\n\t{}for subject [{}]{}", TERM_BOLD, value, TERM_RESET),
            None => "".to_string(),
        }
    }
}

impl<'s, S> Spec<'s, S> {
    /// Provides the actual location of the assertion.
    ///
    /// Usually you would not call this directly, but use the macro forms of `assert_that` and
    /// `asserting`, which will call this on your behalf with the correct location.
    pub fn at_location(self, location: String) -> Self {
        let mut spec = self;
        spec.location = Some(location);

        spec
    }

    /// Associates a name with the subject.
    ///
    /// This will be displayed if the assertion fails.
    pub fn named(self, subject_name: &'s str) -> Self {
        let mut spec = self;
        spec.subject_name = Some(subject_name);

        spec
    }
}

impl<'s, S> Spec<'s, S>
where
    S: Debug + PartialEq,
{
    /// Asserts that the actual value and the expected value are equal. The value type must
    /// implement `PartialEq`.
    ///
    /// ```rust
    /// # use speculoos::prelude::*;
    /// assert_that(&"hello").is_equal_to(&"hello");
    /// ```
    pub fn is_equal_to<E: Borrow<S>>(&mut self, expected: E) {
        let subject = self.subject;
        let borrowed_expected = expected.borrow();

        if !subject.eq(borrowed_expected) {
            AssertionFailure::from_spec(self)
                .with_expected(format!("<{:?}>", borrowed_expected))
                .with_actual(format!("<{:?}>", subject))
                .fail();
        }
    }

    /// Asserts that the actual value and the expected value are not equal. The value type must
    /// implement `PartialEq`.
    ///
    /// ```rust
    /// # use speculoos::prelude::*;
    /// assert_that(&"hello").is_not_equal_to(&"olleh");
    /// ```
    pub fn is_not_equal_to<E: Borrow<S>>(&mut self, expected: E) {
        let subject = self.subject;
        let borrowed_expected = expected.borrow();

        if subject.eq(borrowed_expected) {
            AssertionFailure::from_spec(self)
                .with_expected(format!(
                    "<{:?}> not equal to <{:?}>",
                    subject, borrowed_expected
                ))
                .with_actual("equal".to_string())
                .fail();
        }
    }
}

impl<'s, S> Spec<'s, S>
where
    S: Debug,
{
    /// Accepts a function accepting the value type which returns a bool. Returning false will
    /// cause the assertion to fail.
    ///
    /// NOTE: The resultant panic message will only state the actual value. It's recommended that
    /// you write your own assertion rather than relying upon this.
    ///
    /// `matches` returns &mut &Self, making it possible to chain multiple assertions.
    ///
    /// ```rust
    /// # use speculoos::prelude::*;
    /// assert_that(&"hello").matches(|x| x.eq(&"hello"));
    /// ```
    pub fn matches<F>(&mut self, matching_function: F) -> &mut Self
    where
        F: Fn(&'s S) -> bool,
    {
        let subject = self.subject;

        if !matching_function(subject) {
            AssertionFailure::from_spec(self)
                .fail_with_message(format!("expectation failed for value <{:?}>", subject));
        }

        self
    }

    /// Transforms the subject of the `Spec` by passing it through to the provided mapping
    /// function.
    ///
    /// ```rust
    /// # use speculoos::prelude::*;
    /// # #[derive(Debug, PartialEq)]
    /// # struct TestStruct {
    /// #     pub value: u8,
    /// # }
    /// let test_struct = TestStruct { value: 5 };
    /// assert_that(&test_struct).map(|val| &val.value).is_equal_to(&5);
    /// ```
    pub fn map<F, T>(self, mapping_function: F) -> Spec<'s, T>
    where
        F: Fn(&'s S) -> &'s T,
    {
        Spec {
            subject: mapping_function(self.subject),
            subject_name: self.subject_name,
            location: self.location.clone(),
            description: self.description,
        }
    }
}

#[cfg(test)]
mod tests {

    use super::prelude::*;

    #[test]
    fn should_be_able_to_use_macro_form_with_deliberate_reference() {
        let test_vec = vec![1, 2, 3, 4, 5];

        assert_that!(&test_vec).mapped_contains(|val| val * 2, &6);
    }

    #[test]
    fn should_be_able_to_use_macro_form_without_deliberate_reference() {
        let test_vec = vec![1, 2, 3, 4, 5];

        assert_that!(test_vec).mapped_contains(|val| val * 2, &6);
    }

    #[test]
    fn should_be_able_to_use_function_call_with_macro() {
        struct Line {
            x0: i32,
            x1: i32,
        }

        impl Line {
            fn get_delta_x(&self) -> i32 {
                (self.x1 - self.x0).abs()
            }
        }

        let line = Line { x0: 1, x1: 3 };
        assert_that!(line.get_delta_x()).is_equal_to(2);
        assert_that!(&line.get_delta_x()).is_equal_to(2);
    }

    #[test]
    #[should_panic(expected = "\n\ttest condition:\n\texpected: <2>\n\t but was: <1>")]
    fn should_contain_assertion_description_in_panic() {
        asserting("test condition").that(&1).is_equal_to(&2);
    }

    #[test]
    #[should_panic(expected = "\n\tclosure:\n\texpectation failed for value <\"Hello\">")]
    fn should_contain_assertion_description_if_message_is_provided() {
        let value = "Hello";
        asserting("closure")
            .that(&value)
            .matches(|val| val.eq(&"Hi"));
    }

    #[test]
    #[should_panic(expected = "\n\texpected: <2>\n\t but was: <1>\
                   \n\n\tat location: src/lib.rs:")]
    fn should_contain_file_and_line_in_panic_for_assertions() {
        assert_that!(&1).is_equal_to(&2);
    }

    #[test]
    #[should_panic(expected = "\n\texpectation failed for value <\"Hello\">\
                   \n\n\tat location: src/lib.rs:")]
    fn should_contain_file_and_line_for_assertions_if_message_is_provided() {
        let value = "Hello";
        assert_that!(&value).matches(|val| val.eq(&"Hi"));
    }

    #[test]
    #[should_panic(expected = "\n\ttest condition:\n\texpected: <2>\n\t but was: <1>\
                   \n\n\tat location: src/lib.rs:")]
    fn should_contain_file_and_line_in_panic_for_descriptive_assertions() {
        asserting!(&"test condition").that(&1).is_equal_to(&2);
    }

    #[test]
    #[should_panic(expected = "\n\tclosure:\n\texpectation failed for value <\"Hello\">\
                   \n\n\tat location: src/lib.rs:")]
    fn should_contain_file_and_line_for_descriptive_assertions_if_message_is_provided() {
        let value = "Hello";
        asserting!(&"closure")
            .that(&value)
            .matches(|val| val.eq(&"Hi"));
    }

    #[test]
    #[should_panic(
        expected = "\n\tfor subject [number one]\n\texpected: <2>\n\t but was: <1>\
                   \n\n\tat location: src/lib.rs:"
    )]
    fn should_contain_subject_name_in_panic_for_assertions() {
        assert_that!(&1).named("number one").is_equal_to(&2);
    }

    #[test]
    #[should_panic(
        expected = "\n\tfor subject [a word]\n\texpectation failed for value <\"Hello\">\
                   \n\n\tat location: src/lib.rs:"
    )]
    fn should_contain_subject_name_in_panic_for_assertions_if_message_is_provided() {
        let value = "Hello";
        assert_that!(&value)
            .named("a word")
            .matches(|val| val.eq(&"Hi"));
    }

    #[test]
    fn is_equal_to_should_support_multiple_borrow_forms() {
        assert_that(&1).is_equal_to(1);
        assert_that(&1).is_equal_to(&mut 1);
        assert_that(&1).is_equal_to(&1);
    }

    #[test]
    fn should_not_panic_on_equal_subjects() {
        assert_that(&1).is_equal_to(&1);
    }

    #[test]
    #[should_panic(expected = "\n\texpected: <2>\n\t but was: <1>")]
    fn should_panic_on_unequal_subjects() {
        assert_that(&1).is_equal_to(&2);
    }

    #[test]
    fn is_not_equal_to_should_support_multiple_borrow_forms() {
        assert_that(&1).is_not_equal_to(2);
        assert_that(&1).is_not_equal_to(&mut 2);
        assert_that(&1).is_not_equal_to(&2);
    }

    #[test]
    fn should_not_panic_on_unequal_subjects_if_expected() {
        assert_that(&1).is_not_equal_to(&2);
    }

    #[test]
    #[should_panic(expected = "\n\texpected: <1> not equal to <1>\n\t but was: equal")]
    fn should_panic_on_equal_subjects_if_expected_unequal() {
        assert_that(&1).is_not_equal_to(&1);
    }

    #[test]
    fn should_not_panic_if_value_matches() {
        let value = "Hello";
        assert_that(&value).matches(|val| val.eq(&"Hello"));
    }

    #[test]
    #[should_panic(expected = "\n\texpectation failed for value <\"Hello\">")]
    fn should_panic_if_value_does_not_match() {
        let value = "Hello";
        assert_that(&value).matches(|val| val.eq(&"Hi"));
    }

    #[test]
    fn should_permit_chained_matches_calls() {
        let value = ("Hello", "World");
        assert_that(&value)
            .matches(|val| val.0.eq("Hello"))
            .matches(|val| val.1.eq("World"));
    }

    #[test]
    fn should_be_able_to_map_to_inner_field_of_struct_when_matching() {
        let test_struct = TestStruct { value: 5 };
        assert_that(&test_struct)
            .map(|val| &val.value)
            .is_equal_to(&5);
    }

    #[derive(Debug, PartialEq)]
    struct TestStruct {
        pub value: u8,
    }
}