test4a 0.1.1

Testing library that provides some tools to apply "Advanced" Arrange-Act-Assert testing design.
Documentation
//! Defines the type `Runner`.

use crate::messages::MessageType;
use crate::runners::act::Act;
use crate::{Assert, Message, PanicWhen};
use std::panic;
use std::panic::AssertUnwindSafe;

const NO_PANIC_ERROR: &str = "Code has not panicked";
const PANIC_ERROR: &str = "Code has panicked";

/// Describes and runs multiple tests.
///
/// # Examples
///
/// ```rust
/// use test4a::{Message, Equal, Runner, PanicWhen};
///
/// fn add_0(message: &mut Message, value: &mut usize) {
///     message.set("Add 0");
///     *value += 0;
/// }
///
/// fn add_1(message: &mut Message, value: &mut usize) {
///     message.set("Add 1");
///     *value += 1;
/// }
///
/// fn subtract_1(message: &mut Message, value: &mut usize) {
///     message.set("Subtract 1");
///     *value -= 1;
/// }
///
/// fn value_expected(
///     message: &mut Message,
///     value: usize,
///     expected: Expected,
/// ) -> Equal<usize> {
///     message.set("Value is expected");
///     Equal::new(value, expected.value)
/// }
///
/// struct Expected {
///     value: usize
/// }
///
///
/// // Test #1
/// Runner::arrange(|message| {
///     message.set("Initial value of 1");
///     1
/// })
/// .act(add_0, || Expected { value: 1 })
/// .act(add_1, || Expected { value: 2 })
/// .act(subtract_1, || Expected { value: 0 })
/// .assert(value_expected);
///
/// // Test #2
/// Runner::arrange(|message| {
///     message.set("Initial value of 42");
///     42
/// })
/// .act(add_0, || Expected { value: 42 })
/// .act(add_1, || Expected { value: 43 })
/// .act(subtract_1, || Expected { value: 41 })
/// .assert(value_expected);
///
/// // ...
/// ```
pub struct Runner<'a, T, E> {
    arrange_function: Box<Fn(&mut Message) -> T + 'a>,
    actions: Vec<Act<'a, T, E>>,
}

impl<'a, T, E> Runner<'a, T, E> {
    /// Constructor.
    ///
    /// `function` defines how the value to test should be initialized.<br>
    /// It isn't called immediately, but only for each defined test.
    pub fn arrange(function: impl Fn(&mut Message) -> T + 'a) -> Self {
        Self {
            arrange_function: Box::new(function),
            actions: Vec::new(),
        }
    }

    /// Define an action to execute after the value to test has been created.
    ///
    /// The Arrange step is executed before each defined Act step to have
    /// independent tests.
    pub fn act(
        &mut self,
        action: impl Fn(&mut Message, &mut T) + 'a,
        expectation: impl Fn() -> E + 'a,
    ) -> &mut Self {
        self.actions.push(Act::new(action, expectation));
        self
    }

    /// Define an action that should panic.
    ///
    /// The check is done immediately on method call.<br>
    /// The `when` parameter allows you to specify when the panic occurs.<br>
    /// For example, integer overflow panics only in debug mode.
    pub fn act_panic(
        &self,
        when: PanicWhen,
        function: impl Fn(&mut Message, &mut T) + 'a,
    ) -> &Self {
        let mut message = Message::new();
        let mut value = (self.arrange_function)(&mut message);
        message.set_current_type(MessageType::Act);
        let result = panic::catch_unwind(AssertUnwindSafe(|| {
            function(&mut message, &mut value);
        }));
        if result.is_ok() == Self::should_panic(when) {
            message.set_current_type(MessageType::Assert);
            message.set("** Skipped **");
            if result.is_ok() {
                panic!(Self::format_error_message(&message, NO_PANIC_ERROR));
            } else {
                panic!(Self::format_error_message(&message, PANIC_ERROR));
            }
        }
        self
    }

    /// Define an assertion to check.
    ///
    /// The check is done immediately on method call.<br>
    /// The assertion is tested independently with all defined actions.
    pub fn assert<A: Assert>(
        &self,
        function: impl Fn(&mut Message, T, E) -> A + 'a,
    ) -> &Self {
        for act in &self.actions {
            let mut message = Message::new();
            let mut value = (self.arrange_function)(&mut message);
            message.set_current_type(MessageType::Act);
            act.execute(&mut message, &mut value);
            let expected = act.expect();
            message.set_current_type(MessageType::Assert);
            let result = function(&mut message, value, expected);
            if !result.success() {
                panic!(Self::format_error_message(
                    &message,
                    &result.error_message()
                ));
            }
        }
        self
    }

    /// Defines an assertion that should panic.
    ///
    /// The check is done immediately on method call.<br>
    /// The assertion is tested independently with all defined actions.
    pub fn assert_panic<A: Assert>(
        &self,
        when: PanicWhen,
        function: impl Fn(&mut Message, T, E) -> A + 'a,
    ) -> &Self {
        for act in &self.actions {
            let mut message = Message::new();
            let mut value = (self.arrange_function)(&mut message);
            message.set_current_type(MessageType::Act);
            act.execute(&mut message, &mut value);
            let expected = act.expect();
            message.set_current_type(MessageType::Assert);
            let result = panic::catch_unwind(AssertUnwindSafe(|| {
                function(&mut message, value, expected)
            }));
            if result.is_ok() == Self::should_panic(when) {
                if result.is_ok() {
                    panic!(Self::format_error_message(
                        &message,
                        NO_PANIC_ERROR
                    ));
                } else {
                    panic!(Self::format_error_message(&message, PANIC_ERROR));
                }
            }
        }
        self
    }

    /// Format the error message to print when a test has failed.
    fn format_error_message(message: &Message, error: &str) -> String {
        "\n--------------------------------------------\n".to_string()
            + &format!("Arrange:\n    {}\n", message.arrange_message())
            + &format!("Act:\n    {}\n", message.act_message())
            + &format!("Assert:\n    {}\n", message.assert_message())
            + &format!("Error:\n    {}\n", error.replace("\n", "\n    "))
            + "--------------------------------------------\n"
    }

    /// Tell whether the code should actually panic with the current config.
    ///
    /// For example, PanicWhen::Debug will be chosen to test integer overflow.
    /// So this method will return `true` in debug mode and `false` in release
    /// mode.
    fn should_panic(panic_when: PanicWhen) -> bool {
        #[cfg(debug_assertions)]
        match panic_when {
            PanicWhen::Debug | PanicWhen::DebugAndRelease => true,
            PanicWhen::Release => false,
        }
        #[cfg(not(debug_assertions))]
        match panic_when {
            PanicWhen::Release | PanicWhen::DebugAndRelease => true,
            PanicWhen::Debug => false,
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::{Equal, PanicWhen, Runner};

    const VALUE_EXAMPLE: usize = 10;

    struct Expected {
        value: usize,
    }

    #[test]
    fn test_ok() {
        Runner::arrange(|message| {
            message.set("Initialization");
            0
        })
        .act(
            |message, value| {
                message.set("Action");
                *value = VALUE_EXAMPLE
            },
            || Expected {
                value: VALUE_EXAMPLE,
            },
        )
        .act_panic(PanicWhen::Debug, |message, value| {
            message.set("Other action");
            *value -= 1;
        })
        .assert_panic(PanicWhen::Debug, |message, mut _value, _expected| {
            message.set("Evaluation");
            _value -= VALUE_EXAMPLE + 1;
            Equal::new(0, 0)
        })
        .assert(|message, value, expected| {
            message.set("Other evaluation");
            Equal::new(value, expected.value)
        });
    }

    #[test]
    #[should_panic]
    #[cfg(debug_assertions)]
    fn test_act_panic_fail() {
        Runner::arrange(|message| {
            message.set("Initialization");
            0
        })
        .act(
            |message, value| {
                message.set("Action");
                *value = VALUE_EXAMPLE
            },
            || Expected {
                value: VALUE_EXAMPLE,
            },
        )
        .act_panic(PanicWhen::Debug, |message, value| {
            message.set("Other action");
            *value -= 0;
        })
        .assert_panic(PanicWhen::Debug, |message, mut _value, _expected| {
            message.set("Evaluation");
            _value -= VALUE_EXAMPLE + 1;
            Equal::new(0, 0)
        })
        .assert(|message, value, expected| {
            message.set("Other evaluation");
            Equal::new(value, expected.value)
        });
    }

    #[test]
    #[should_panic]
    #[cfg(debug_assertions)]
    fn test_assert_panic_fail() {
        Runner::arrange(|message| {
            message.set("Initialization");
            0
        })
        .act(
            |message, value| {
                message.set("Action");
                *value = VALUE_EXAMPLE
            },
            || Expected {
                value: VALUE_EXAMPLE,
            },
        )
        .act_panic(PanicWhen::Debug, |message, value| {
            message.set("Other action");
            *value -= 1;
        })
        .assert_panic(PanicWhen::Debug, |message, mut _value, _expected| {
            message.set("Evaluation");
            _value -= 0;
            Equal::new(0, 0)
        })
        .assert(|message, value, expected| {
            message.set("Other evaluation");
            Equal::new(value, expected.value)
        });
    }

    #[test]
    #[should_panic]
    fn test_assert_fail() {
        Runner::arrange(|message| {
            message.set("Initialization");
            0
        })
        .act(
            |message, value| {
                message.set("Action");
                *value = VALUE_EXAMPLE
            },
            || Expected {
                value: VALUE_EXAMPLE,
            },
        )
        .act_panic(PanicWhen::Debug, |message, value| {
            message.set("Other action");
            *value -= 1;
        })
        .assert_panic(PanicWhen::Debug, |message, mut _value, _expected| {
            message.set("Evaluation");
            _value -= VALUE_EXAMPLE + 1;
            Equal::new(0, 0)
        })
        .assert(|message, value, expected| {
            message.set("Other evaluation");
            Equal::new(value, expected.value + 1)
        });
    }
}