use std::fmt::Debug;
use std::ops::Drop;
pub trait TestFixture<'param, P, R>: Drop
where
P: Debug + 'static,
{
fn new(curried_params: &'param P) -> Self;
fn parameters() -> Option<Box<Iterator<Item = P>>>;
fn setup(&mut self) -> FixtureBinding<Self, R>
where
Self: std::marker::Sized;
fn tear_down(&self) {}
}
pub struct FixtureBinding<'fixture, F: 'fixture, R> {
pub val: R,
pub params: &'fixture F,
}
impl<'fixture, F: 'fixture, R> FixtureBinding<'fixture, F, R> {
pub fn decompose(self) -> (R, &'fixture F) {
(self.val, self.params)
}
pub fn into_val(self) -> R {
self.val
}
pub fn into_params(self) -> &'fixture F {
self.params
}
}
#[macro_export(local_inner_macros)]
macro_rules! fixture {
( @impl_drop $name:ident ) => {
impl<'param> ::std::ops::Drop for $name<'param> {
fn drop(&mut self) {
use ::galvanic_test::TestFixture;
self.tear_down();
}
}
};
( @impl_struct $name:ident Params[$($param:ident : $param_ty:ty),*] Members[$($member:ident : $member_ty:ty),*] ) => {
#[allow(non_camel_case_types)]
#[derive(Debug)]
pub struct $name<'param> {
$(pub $param : &'param $param_ty,)*
$($member : Option<$member_ty>,)*
}
};
( @new_method Params[$param:ident : $param_ty:ty] Members[$($member:ident),*] ) => {
fn new($param : &'param $param_ty) -> Self {
Self {
$param,
$($member: None,)*
}
}
};
( @new_method Params[$($param:ident : $param_ty:ty),+] Members[$($member:ident),*] ) => {
fn new(&($(ref $param),*) : &'param ($($param_ty),*)) -> Self {
Self {
$($param,)*
$($member: None,)*
}
}
};
( $name:ident ( ) -> $ret_ty:ty {
$(members { $($member:ident : Option<$member_ty:ty>),* })*
setup(& mut $self_setup:ident) $setup_body:block
$(tear_down(&$self_td:ident) $tear_down_body:block)*
}
) => {
fixture!(@impl_struct $name Params[_phantom : ()] Members[$($($member : $member_ty),*),*]);
impl<'param> ::galvanic_test::TestFixture<'param, (), $ret_ty> for $name<'param> {
fn new(_phantom: &'param ()) -> Self {
Self {
_phantom,
$($($member: None),*),*
}
}
fn parameters() -> Option<Box<Iterator<Item=()>>> {
Some(Box::new(Some(()).into_iter()))
}
fn setup(&mut $self_setup) -> ::galvanic_test::FixtureBinding<Self, $ret_ty> {
let value = $setup_body;
::galvanic_test::FixtureBinding {
val: value,
params: $self_setup
}
}
$(fn tear_down(&$self_td) $tear_down_body)*
}
fixture!(@impl_drop $name);
};
( $name:ident ($($param:ident : $param_ty:ty),+) -> $ret_ty:ty {
$(members { $($member:ident : Option<$member_ty:ty>),* })*
$(params $params_body:block)*
setup(& mut $self_setup:ident) $setup_body:block
$(tear_down(&$self_td:ident) $tear_down_body:block)*
}
) => {
fixture!(@impl_struct $name Params[$($param : $param_ty),*] Members[$($($member : $member_ty),*),*]);
impl<'param> ::galvanic_test::TestFixture<'param, ($($param_ty),*), $ret_ty> for $name<'param> {
fixture!(@new_method Params[$($param : $param_ty),*] Members[$($($member),*),*]);
fn parameters() -> Option<Box<Iterator<Item=($($param_ty),*)>>> {
(None as Option<Box<Iterator<Item=($($param_ty),*)>>>)
$(; Some(Box::new($params_body)))*
}
fn setup(&mut $self_setup) -> ::galvanic_test::FixtureBinding<Self, $ret_ty> {
let value = $setup_body;
::galvanic_test::FixtureBinding {
val: value,
params: $self_setup
}
}
$(fn tear_down(&$self_td) $tear_down_body)*
}
fixture!(@impl_drop $name);
};
}
#[macro_export(local_inner_macros)]
macro_rules! test {
( @parameters | $body:block $test_case_failed:ident ) => { $body };
( @parameters | $body:block $test_case_failed:ident $(($fixture_obj:ident, $params:expr, $fixture:ident))+) => {
let mut described_parameters = String::from("Test panicked before all fixtures have been assigned.");
let result = ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| {
let mut described_params = Vec::new();
$(
let params = &$params;
let mut $fixture_obj = $fixture::new(params);
described_params.push(_galvanic__format!("{:?}", $fixture_obj));
let mut $fixture = $fixture_obj.setup();
noop(&$fixture);
)*
described_parameters = described_params.join(", ");
$body
}));
if result.is_err() {
_galvanic__println!("The above error occured with the following parameterisation of the test case:\n {}\n",
described_parameters);
$test_case_failed.set(true);
}
};
( @parameters , $($remainder:tt)+ ) => {
test!(@parameters $($remainder)*);
};
( @parameters $fixture:ident ( $($expr:expr),* ) $($remainder:tt)+ ) => {
test!(@parameters $($remainder)* (fixture_obj, ($($expr),*), $fixture));
};
( @parameters $fixture:ident $($remainder:tt)+ ) => {
match $fixture::parameters() {
Some(iterator) => {
for params in iterator {
test!(@parameters $($remainder)* (fixture_obj, params, $fixture));
}
},
None => _galvanic__panic!(_galvanic__concat!(
"If a test fixture should be injected without supplying parameters, ",
"it either needs to have no arguments ",
"or a `params` block returning an iterator of parameter tuples ",
"must be given for the fixture."))
}
};
( $(#[$attr:meta])* $name:ident | $($args_and_body:tt)* ) => {
#[test]
$(#[$attr])*
fn $name() {
#[allow(dead_code)]
fn noop<F, R>(_: &::galvanic_test::FixtureBinding<F,R>) { }
let test_case_failed = ::std::cell::Cell::new(false);
test!(@parameters $($args_and_body)* test_case_failed);
if test_case_failed.get() {
_galvanic__panic!("Some parameterised test cases failed");
}
}
};
( $(#[$attr:meta])* $name:ident $body:block ) => {
#[test]
$(#[$attr])*
fn $name() {
$body
}
};
}
#[macro_export(local_inner_macros)]
#[cfg(not(feature = "galvanic_mock_integration"))]
macro_rules! test_suite {
( name $name:ident ; $($remainder:tt)* ) => {
#[cfg(test)]
mod $name {
#[allow(unused_imports)] use ::galvanic_test::TestFixture;
galvanic_test::__test_suite_int!(@int $($remainder)*);
}
};
( $($remainder:tt)* ) => {
#[cfg(test)]
mod __galvanic_test {
#[allow(unused_imports)] use ::galvanic_test::TestFixture;
galvanic_test::__test_suite_int!(@int $($remainder)*);
}
};
}
#[macro_export(local_inner_macros)]
#[cfg(feature = "galvanic_mock_integration")]
macro_rules! test_suite {
( name $name:ident ; $($remainder:tt)* ) => {
#[cfg(test)]
mod $name {
#[allow(unused_imports)] use ::galvanic_mock::use_mocks;
#[allow(unused_imports)] use super::*;
#[use_mocks]
mod with_mocks {
#[allow(unused_imports)] use ::galvanic_test::TestFixture;
galvanic_test::__test_suite_int!(@int $($remainder)*);
}
}
};
( $($remainder:tt)* ) => {
#[cfg(test)]
mod __galvanic_test {
#[allow(unused_imports)] use ::galvanic_mock::use_mocks;
#[allow(unused_imports)] use super::*;
#[use_mocks]
mod with_mocks {
#[allow(unused_imports)] use ::galvanic_test::TestFixture;
galvanic_test::__test_suite_int!(@int $($remainder)*);
}
}
};
}
#[macro_export(local_inner_macros)]
macro_rules! __test_suite_int {
( @int $(#[$attr:meta])* fixture $name:ident ($($param:ident : $param_ty:ty),*) -> $ret_ty:ty {
$(members { $($member:ident : Option<$member_ty:ty>),* })*
$(params $params_body:block)*
setup(& mut $self_setup:ident) $setup_body:block
$(tear_down(&$self_td:ident) $tear_down_body:block)*
} $($remainder:tt)*
) => {
fixture!( $(#[$attr])* $name ($($param : $param_ty),*) -> $ret_ty {
$(members { $($member : Option<$member_ty>),* })*
$(params $params_body)*
setup(& mut $self_setup) $setup_body
$(tear_down(&$self_td) $tear_down_body)*
});
galvanic_test::__test_suite_int!(@int $($remainder)*);
};
( @int $(#[$attr:meta])* test $name:ident ( $($fixture:ident $(($($expr:expr),*))*),* )
$body:block
$($remainder:tt)*
) => {
test!( $(#[$attr])* $name | $($fixture $(($($expr),*))* ),* | $body);
galvanic_test::__test_suite_int!(@int $($remainder)*);
};
( @int $item:item
$($remainder:tt)*
) => {
$item
galvanic_test::__test_suite_int!(@int $($remainder)*);
};
( @int ) => { };
}
#[doc(hidden)]
#[macro_export]
macro_rules! _galvanic__panic {
($($inner:tt)*) => {
panic!($($inner)*)
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! _galvanic__format {
($($inner:tt)*) => {
format!($($inner)*)
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! _galvanic__println {
($($inner:tt)*) => {
println!($($inner)*)
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! _galvanic__concat {
($($inner:tt)*) => {
concat!($($inner)*)
};
}