rusty_fork/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_fork::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_fork::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() $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() $body
82 let body: fn () = 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() $body:block
103 )*) => {
104 rusty_fork_test! {
105 #![rusty_fork(timeout_ms = 0)]
106
107 $($(#[$meta])* fn $test_name() $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(
124 concat!(module_path!(), "::", stringify!($function_name)))
125 }
126}
127
128#[allow(missing_docs)]
129#[doc(hidden)]
130pub fn supervise_child(child: &mut ChildWrapper, timeout_ms: u64) {
131 if timeout_ms > 0 {
132 wait_timeout(child, timeout_ms)
133 } else {
134 let status = child.wait().expect("failed to wait for child");
135 assert!(status.success(),
136 "child exited unsuccessfully with {}", status);
137 }
138}
139
140#[allow(missing_docs)]
141#[doc(hidden)]
142pub fn no_configure_child(_child: &mut Command) { }
143
144/// Transform a string representing a qualified path as generated via
145/// `module_path!()` into a qualified path as expected by the standard Rust
146/// test harness.
147pub fn fix_module_path(path: &str) -> &str {
148 path.find("::").map(|ix| &path[ix+2..]).unwrap_or(path)
149}
150
151#[cfg(feature = "timeout")]
152fn wait_timeout(child: &mut ChildWrapper, timeout_ms: u64) {
153 use std::time::Duration;
154
155 let timeout = Duration::from_millis(timeout_ms);
156 let status = child.wait_timeout(timeout).expect("failed to wait for child");
157 if let Some(status) = status {
158 assert!(status.success(),
159 "child exited unsuccessfully with {}", status);
160 } else {
161 panic!("child process exceeded {} ms timeout", timeout_ms);
162 }
163}
164
165#[cfg(not(feature = "timeout"))]
166fn wait_timeout(_: &mut ChildWrapper, _: u64) {
167 panic!("Using the timeout feature of rusty_fork_test! requires \
168 enabling the `timeout` feature on the rusty-fork crate.");
169}
170
171#[cfg(test)]
172mod test {
173 rusty_fork_test! {
174 #[test]
175 fn trivial() { }
176
177 #[test]
178 #[should_panic]
179 fn panicking_child() {
180 panic!("just testing a panic, nothing to see here");
181 }
182
183 #[test]
184 #[should_panic]
185 fn aborting_child() {
186 ::std::process::abort();
187 }
188 }
189
190 rusty_fork_test! {
191 #![rusty_fork(timeout_ms = 1000)]
192
193 #[test]
194 #[cfg(feature = "timeout")]
195 fn timeout_passes() { }
196
197 #[test]
198 #[should_panic]
199 #[cfg(feature = "timeout")]
200 fn timeout_fails() {
201 println!("hello from child");
202 ::std::thread::sleep(
203 ::std::time::Duration::from_millis(10000));
204 println!("goodbye from child");
205 }
206 }
207}