galvanic_test/
lib.rs

1/* Copyright 2017 Christopher Bacher
2 *
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16use std::fmt::Debug;
17use std::ops::Drop;
18
19pub trait TestFixture<'param, P, R>: Drop
20where
21    P: Debug + 'static,
22{
23    fn new(curried_params: &'param P) -> Self;
24
25    fn parameters() -> Option<Box<Iterator<Item = P>>>;
26
27    fn setup(&mut self) -> FixtureBinding<Self, R>
28    where
29        Self: std::marker::Sized;
30
31    fn tear_down(&self) {}
32}
33
34pub struct FixtureBinding<'fixture, F: 'fixture, R> {
35    pub val: R,
36    pub params: &'fixture F,
37}
38
39impl<'fixture, F: 'fixture, R> FixtureBinding<'fixture, F, R> {
40    pub fn decompose(self) -> (R, &'fixture F) {
41        (self.val, self.params)
42    }
43
44    pub fn into_val(self) -> R {
45        self.val
46    }
47
48    pub fn into_params(self) -> &'fixture F {
49        self.params
50    }
51}
52
53/// Creates a new `TestFixture` implementation.
54///
55/// A `fixture!` requires a name, parameters and a
56#[macro_export(local_inner_macros)]
57macro_rules! fixture {
58    ( @impl_drop $name:ident ) => {
59        impl<'param> ::std::ops::Drop for $name<'param> {
60            fn drop(&mut self) {
61                use ::galvanic_test::TestFixture;
62                self.tear_down();
63            }
64        }
65    };
66
67    ( @impl_struct $name:ident Params[$($param:ident : $param_ty:ty),*] Members[$($member:ident : $member_ty:ty),*] ) => {
68        #[allow(non_camel_case_types)]
69        #[derive(Debug)]
70        pub struct $name<'param> {
71            $(pub $param : &'param $param_ty,)*
72            $($member : Option<$member_ty>,)*
73        }
74    };
75
76    ( @new_method Params[$param:ident : $param_ty:ty] Members[$($member:ident),*] ) => {
77        fn new($param : &'param $param_ty) -> Self {
78            Self {
79                $param,
80                $($member: None,)*
81            }
82        }
83    };
84    ( @new_method Params[$($param:ident : $param_ty:ty),+] Members[$($member:ident),*] ) => {
85        fn new(&($(ref $param),*) : &'param ($($param_ty),*)) -> Self {
86            Self {
87                $($param,)*
88                $($member: None,)*
89            }
90        }
91    };
92
93    ( $name:ident ( ) -> $ret_ty:ty {
94          $(members { $($member:ident : Option<$member_ty:ty>),* })*
95          setup(& mut $self_setup:ident) $setup_body:block
96          $(tear_down(&$self_td:ident) $tear_down_body:block)*
97      }
98    ) => {
99        fixture!(@impl_struct $name Params[_phantom : ()] Members[$($($member : $member_ty),*),*]);
100
101        impl<'param> ::galvanic_test::TestFixture<'param, (), $ret_ty> for $name<'param> {
102            fn new(_phantom: &'param ()) -> Self {
103                Self {
104                    _phantom,
105                    $($($member: None),*),*
106                }
107            }
108            fn parameters() -> Option<Box<Iterator<Item=()>>> {
109                Some(Box::new(Some(()).into_iter()))
110            }
111            fn setup(&mut $self_setup) -> ::galvanic_test::FixtureBinding<Self, $ret_ty> {
112                let value = $setup_body;
113                ::galvanic_test::FixtureBinding {
114                    val: value,
115                    params: $self_setup
116                }
117            }
118            $(fn tear_down(&$self_td) $tear_down_body)*
119        }
120
121        fixture!(@impl_drop $name);
122    };
123
124    ( $name:ident ($($param:ident : $param_ty:ty),+) -> $ret_ty:ty {
125          $(members { $($member:ident : Option<$member_ty:ty>),* })*
126          $(params $params_body:block)*
127          setup(& mut $self_setup:ident) $setup_body:block
128          $(tear_down(&$self_td:ident) $tear_down_body:block)*
129      }
130    ) => {
131        fixture!(@impl_struct $name Params[$($param : $param_ty),*] Members[$($($member : $member_ty),*),*]);
132
133        impl<'param> ::galvanic_test::TestFixture<'param, ($($param_ty),*), $ret_ty> for $name<'param> {
134            fixture!(@new_method Params[$($param : $param_ty),*] Members[$($($member),*),*]);
135            fn parameters() -> Option<Box<Iterator<Item=($($param_ty),*)>>> {
136                (None as Option<Box<Iterator<Item=($($param_ty),*)>>>)
137                $(; Some(Box::new($params_body)))*
138            }
139            fn setup(&mut $self_setup) -> ::galvanic_test::FixtureBinding<Self, $ret_ty> {
140                let value = $setup_body;
141                ::galvanic_test::FixtureBinding {
142                    val: value,
143                    params: $self_setup
144                }
145            }
146            $(fn tear_down(&$self_td) $tear_down_body)*
147        }
148
149        fixture!(@impl_drop $name);
150    };
151}
152
153#[macro_export(local_inner_macros)]
154macro_rules! test {
155    ( @parameters | $body:block $test_case_failed:ident ) => { $body };
156
157    ( @parameters | $body:block $test_case_failed:ident $(($fixture_obj:ident, $params:expr, $fixture:ident))+) => {
158        let mut described_parameters = String::from("Test panicked before all fixtures have been assigned.");
159        let result = ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| {
160            let mut described_params = Vec::new();
161            $(
162                let params = &$params;
163                let mut $fixture_obj = $fixture::new(params);
164                described_params.push(_galvanic__format!("{:?}", $fixture_obj));
165                let mut $fixture = $fixture_obj.setup();
166                noop(&$fixture);
167            )*
168            described_parameters = described_params.join(", ");
169            $body
170        }));
171        if result.is_err() {
172            _galvanic__println!("The above error occured with the following parameterisation of the test case:\n    {}\n",
173                     described_parameters);
174            $test_case_failed.set(true);
175        }
176    };
177
178    ( @parameters , $($remainder:tt)+ ) => {
179        test!(@parameters $($remainder)*);
180    };
181
182    ( @parameters $fixture:ident ( $($expr:expr),* ) $($remainder:tt)+ ) => {
183        test!(@parameters $($remainder)* (fixture_obj, ($($expr),*), $fixture));
184    };
185
186    ( @parameters $fixture:ident $($remainder:tt)+ ) => {
187        match $fixture::parameters() {
188            Some(iterator) => {
189                for params in iterator {
190                    test!(@parameters $($remainder)* (fixture_obj, params, $fixture));
191                }
192            },
193            None => _galvanic__panic!(_galvanic__concat!(
194                "If a test fixture should be injected without supplying parameters, ",
195                "it either needs to have no arguments ",
196                "or a `params` block returning an iterator of parameter tuples ",
197                "must be given for the fixture."))
198        }
199    };
200
201    ( $(#[$attr:meta])* $name:ident | $($args_and_body:tt)* ) => {
202        #[test]
203        $(#[$attr])*
204        fn $name() {
205            #[allow(dead_code)]
206            fn noop<F, R>(_: &::galvanic_test::FixtureBinding<F,R>) { }
207            // Cell is a workaround for #![allow(unused_mut)] which would affect the whole fn
208            let test_case_failed = ::std::cell::Cell::new(false);
209            test!(@parameters $($args_and_body)* test_case_failed);
210            if test_case_failed.get() {
211                _galvanic__panic!("Some parameterised test cases failed");
212            }
213        }
214    };
215
216    ( $(#[$attr:meta])* $name:ident $body:block ) => {
217        #[test]
218        $(#[$attr])*
219        fn $name() {
220            $body
221        }
222    };
223}
224
225#[macro_export(local_inner_macros)]
226#[cfg(not(feature = "galvanic_mock_integration"))]
227macro_rules! test_suite {
228    // named test suite
229    ( name $name:ident ; $($remainder:tt)* ) => {
230        #[cfg(test)]
231        mod $name {
232            #[allow(unused_imports)] use ::galvanic_test::TestFixture;
233            galvanic_test::__test_suite_int!(@int $($remainder)*);
234        }
235    };
236
237    // anonymous test suite
238    ( $($remainder:tt)* ) => {
239        #[cfg(test)]
240        mod __galvanic_test {
241            #[allow(unused_imports)] use ::galvanic_test::TestFixture;
242            galvanic_test::__test_suite_int!(@int $($remainder)*);
243        }
244    };
245}
246
247#[macro_export(local_inner_macros)]
248#[cfg(feature = "galvanic_mock_integration")]
249macro_rules! test_suite {
250    // named test suite
251    ( name $name:ident ; $($remainder:tt)* ) => {
252        #[cfg(test)]
253        mod $name {
254            #[allow(unused_imports)] use ::galvanic_mock::use_mocks;
255            #[allow(unused_imports)] use super::*;
256            #[use_mocks]
257            mod with_mocks {
258                #[allow(unused_imports)] use ::galvanic_test::TestFixture;
259                galvanic_test::__test_suite_int!(@int $($remainder)*);
260            }
261        }
262    };
263
264    // anonymous test suite
265    ( $($remainder:tt)* ) => {
266        #[cfg(test)]
267        mod __galvanic_test {
268            #[allow(unused_imports)] use ::galvanic_mock::use_mocks;
269            #[allow(unused_imports)] use super::*;
270            #[use_mocks]
271            mod with_mocks {
272                #[allow(unused_imports)] use ::galvanic_test::TestFixture;
273                galvanic_test::__test_suite_int!(@int $($remainder)*);
274            }
275        }
276    };
277}
278
279#[macro_export(local_inner_macros)]
280macro_rules! __test_suite_int {
281    // internal: fixture in test_suite
282    ( @int $(#[$attr:meta])* fixture $name:ident ($($param:ident : $param_ty:ty),*) -> $ret_ty:ty {
283          $(members { $($member:ident : Option<$member_ty:ty>),* })*
284          $(params $params_body:block)*
285          setup(& mut $self_setup:ident) $setup_body:block
286          $(tear_down(&$self_td:ident) $tear_down_body:block)*
287      } $($remainder:tt)*
288    ) => {
289        fixture!( $(#[$attr])* $name ($($param : $param_ty),*) -> $ret_ty {
290              $(members { $($member : Option<$member_ty>),* })*
291              $(params $params_body)*
292              setup(& mut $self_setup) $setup_body
293              $(tear_down(&$self_td) $tear_down_body)*
294        });
295        galvanic_test::__test_suite_int!(@int $($remainder)*);
296    };
297
298    // internal: test in test_suite
299    ( @int $(#[$attr:meta])* test $name:ident ( $($fixture:ident $(($($expr:expr),*))*),* )
300            $body:block
301            $($remainder:tt)*
302    ) => {
303        test!( $(#[$attr])* $name | $($fixture $(($($expr),*))* ),* | $body);
304        galvanic_test::__test_suite_int!(@int $($remainder)*);
305    };
306
307    // internal: arbitrary item in test suite
308    ( @int $item:item
309            $($remainder:tt)*
310    ) => {
311        $item
312        galvanic_test::__test_suite_int!(@int $($remainder)*);
313    };
314
315    // internal: empty test suite
316    ( @int ) => { };
317}
318
319#[doc(hidden)]
320#[macro_export]
321macro_rules! _galvanic__panic {
322    ($($inner:tt)*) => {
323        panic!($($inner)*)
324    };
325}
326
327#[doc(hidden)]
328#[macro_export]
329macro_rules! _galvanic__format {
330    ($($inner:tt)*) => {
331        format!($($inner)*)
332    };
333}
334
335#[doc(hidden)]
336#[macro_export]
337macro_rules! _galvanic__println {
338    ($($inner:tt)*) => {
339        println!($($inner)*)
340    };
341}
342
343#[doc(hidden)]
344#[macro_export]
345macro_rules! _galvanic__concat {
346    ($($inner:tt)*) => {
347        concat!($($inner)*)
348    };
349}