rusty_forkfork/
fork_test.rs

1//-
2// Copyright 2018, 2020 Jason Lingle
3//
4// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
5// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
6// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
7// option. This file may not be copied, modified, or distributed
8// except according to those terms.
9
10//! Support code for the `rusty_fork_test!` macro and similar.
11//!
12//! Some functionality in this module is useful to other implementors and
13//! unlikely to change. This subset is documented and considered stable.
14
15use std::process::Command;
16
17use crate::child_wrapper::ChildWrapper;
18
19/// Run Rust tests in subprocesses.
20///
21/// The basic usage is to simply put this macro around your `#[test]`
22/// functions.
23///
24/// ```
25/// use rusty_forkfork::rusty_fork_test;
26///
27/// rusty_fork_test! {
28/// # /*
29///     #[test]
30/// # */
31///     fn my_test() {
32///         assert_eq!(2, 1 + 1);
33///     }
34///
35///     // more tests...
36/// }
37/// #
38/// # fn main() { my_test(); }
39/// ```
40///
41/// Each test will be run in its own process. If the subprocess exits
42/// unsuccessfully for any reason, including due to signals, the test fails.
43///
44/// It is also possible to specify a timeout which is applied to all tests in
45/// the block, like so:
46///
47/// ```
48/// use rusty_forkfork::rusty_fork_test;
49///
50/// rusty_fork_test! {
51///     #![rusty_fork(timeout_ms = 1000)]
52/// # /*
53///     #[test]
54/// # */
55///     fn my_test() {
56///         do_some_expensive_computation();
57///     }
58///
59///     // more tests...
60/// }
61/// # fn do_some_expensive_computation() { }
62/// # fn main() { my_test(); }
63/// ```
64///
65/// If any individual test takes more than the given timeout, the child is
66/// terminated and the test panics.
67///
68/// Using the timeout feature requires the `timeout` feature for this crate to
69/// be enabled (which it is by default).
70#[macro_export]
71macro_rules! rusty_fork_test {
72    (#![rusty_fork(timeout_ms = $timeout:expr)]
73     $(
74         $(#[$meta:meta])*
75         fn $test_name:ident() $( -> $test_return:ty )? $body:block
76    )*) => { $(
77        $(#[$meta])*
78        fn $test_name() {
79            // Eagerly convert everything to function pointers so that all
80            // tests use the same instantiation of `fork`.
81            fn body_fn() $( -> $test_return )? $body
82            let body: fn () $( -> $test_return )? = body_fn;
83
84            fn supervise_fn(child: &mut $crate::ChildWrapper,
85                            _file: &mut ::std::fs::File) {
86                $crate::fork_test::supervise_child(child, $timeout)
87            }
88            let supervise:
89                fn (&mut $crate::ChildWrapper, &mut ::std::fs::File) =
90                supervise_fn;
91
92            $crate::fork(
93                $crate::rusty_fork_test_name!($test_name),
94                $crate::rusty_fork_id!(),
95                $crate::fork_test::no_configure_child,
96                supervise, body).expect("forking test failed")
97        }
98    )* };
99
100    ($(
101         $(#[$meta:meta])*
102         fn $test_name:ident() $( -> $test_return:ty )? $body:block
103    )*) => {
104        rusty_fork_test! {
105            #![rusty_fork(timeout_ms = 0)]
106
107            $($(#[$meta])* fn $test_name() $( -> $test_return )?  $body)*
108        }
109    };
110}
111
112/// Given the unqualified name of a `#[test]` function, produce a
113/// `&'static str` corresponding to the name of the test as filtered by the
114/// standard test harness.
115///
116/// This is internally used by `rusty_fork_test!` but is made available since
117/// other test wrapping implementations will likely need it too.
118///
119/// This does not currently produce a constant expression.
120#[macro_export]
121macro_rules! rusty_fork_test_name {
122    ($function_name:ident) => {
123        $crate::fork_test::fix_module_path(concat!(
124            module_path!(),
125            "::",
126            stringify!($function_name)
127        ))
128    };
129}
130
131#[allow(missing_docs)]
132#[doc(hidden)]
133pub fn supervise_child(child: &mut ChildWrapper, timeout_ms: u64) {
134    if timeout_ms > 0 {
135        wait_timeout(child, timeout_ms)
136    } else {
137        let status = child.wait().expect("failed to wait for child");
138        assert!(
139            status.success(),
140            "child exited unsuccessfully with {}",
141            status
142        );
143    }
144}
145
146#[allow(missing_docs)]
147#[doc(hidden)]
148pub fn no_configure_child(_child: &mut Command) {}
149
150/// Transform a string representing a qualified path as generated via
151/// `module_path!()` into a qualified path as expected by the standard Rust
152/// test harness.
153pub fn fix_module_path(path: &str) -> &str {
154    path.find("::").map(|ix| &path[ix + 2..]).unwrap_or(path)
155}
156
157#[cfg(feature = "timeout")]
158fn wait_timeout(child: &mut ChildWrapper, timeout_ms: u64) {
159    use std::time::Duration;
160
161    let timeout = Duration::from_millis(timeout_ms);
162    let status = child
163        .wait_timeout(timeout)
164        .expect("failed to wait for child");
165    if let Some(status) = status {
166        assert!(
167            status.success(),
168            "child exited unsuccessfully with {}",
169            status
170        );
171    } else {
172        panic!("child process exceeded {} ms timeout", timeout_ms);
173    }
174}
175
176#[cfg(not(feature = "timeout"))]
177fn wait_timeout(_: &mut ChildWrapper, _: u64) {
178    panic!(
179        "Using the timeout feature of rusty_fork_test! requires \
180            enabling the `timeout` feature on the rusty-fork crate."
181    );
182}
183
184#[cfg(test)]
185mod test {
186    rusty_fork_test! {
187        #[test]
188        fn trivial() { }
189
190         #[test]
191        fn trivial_with_ok() -> Result<(), &'static str> { Ok(()) }
192
193        #[test]
194        #[should_panic]
195        fn trivial_with_err() -> Result<(), &'static str> { Err("should fail.") }
196
197        #[test]
198        #[should_panic]
199        fn panicking_child() {
200            panic!("just testing a panic, nothing to see here");
201        }
202
203        #[test]
204        #[should_panic]
205        fn aborting_child() {
206            ::std::process::abort();
207        }
208    }
209
210    rusty_fork_test! {
211        #![rusty_fork(timeout_ms = 1000)]
212
213        #[test]
214        #[cfg(feature = "timeout")]
215        fn timeout_passes() { }
216
217        #[test]
218        #[should_panic]
219        #[cfg(feature = "timeout")]
220        fn timeout_fails() {
221            println!("hello from child");
222            ::std::thread::sleep(
223                ::std::time::Duration::from_millis(10000));
224            println!("goodbye from child");
225        }
226    }
227}