test_fork_core/
fork.rs

1//-
2// Copyright 2018 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
10use std::env;
11use std::io;
12use std::io::BufRead;
13use std::io::Read;
14use std::panic;
15use std::process;
16use std::process::Child;
17use std::process::ExitCode;
18use std::process::Stdio;
19use std::process::Termination;
20
21use crate::cmdline;
22use crate::error::*;
23
24const OCCURS_ENV: &str = "TEST_FORK_OCCURS";
25const OCCURS_TERM_LENGTH: usize = 17; /* ':' plus 16 hexits */
26
27
28/// Simulate a process fork.
29///
30/// Since this is not a true process fork, the calling code must be structured
31/// to ensure that the child process, upon starting from the same entry point,
32/// also reaches this same `fork()` call. Recursive forks are supported; the
33/// child branch is taken from all child processes of the fork even if it is
34/// not directly the child of a particular branch. However, encountering the
35/// same fork point more than once in a single execution sequence of a child
36/// process is not (e.g., putting this call in a recursive function) and
37/// results in unspecified behaviour.
38///
39/// `test_name` must exactly match the full path of the test function being
40/// run.
41///
42/// `fork_id` is a unique identifier identifying this particular fork location.
43/// This *must* be stable across processes of the same executable; pointers are
44/// not suitable stable, and string constants may not be suitably unique. The
45/// [`fork_id!()`] macro is the recommended way to supply this
46/// parameter.
47///
48/// If this is the parent process, `in_parent` is invoked, and the return value
49/// becomes the return value from this function. The callback is passed a
50/// handle to the file which receives the child's output. If is the callee's
51/// responsibility to wait for the child to exit. If this is the child process,
52/// `in_child` is invoked, and when the callback returns, the child process
53/// exits.
54///
55/// If `in_parent` returns or panics before the child process has terminated,
56/// the child process is killed.
57///
58/// If `in_child` panics, the child process exits with a failure code
59/// immediately rather than let the panic propagate out of the `fork()` call.
60///
61/// `process_modifier` is invoked on the `std::process::Command` immediately
62/// before spawning the new process. The callee may modify the process
63/// parameters if desired, but should not do anything that would modify or
64/// remove any environment variables beginning with `RUSTY_FORK_`.
65///
66/// ## Panics
67///
68/// Panics if the environment indicates that there are already at least 16
69/// levels of fork nesting.
70///
71/// Panics if `std::env::current_exe()` fails determine the path to the current
72/// executable.
73///
74/// Panics if any argument to the current process is not valid UTF-8.
75pub fn fork<MODIFIER, PARENT, CHILD, R, T>(
76    test_name: &str,
77    fork_id: &str,
78    process_modifier: MODIFIER,
79    in_parent: PARENT,
80    in_child: CHILD,
81) -> Result<R>
82where
83    MODIFIER: FnOnce(&mut process::Command),
84    PARENT: FnOnce(&mut Child) -> R,
85    T: Termination,
86    CHILD: FnOnce() -> T,
87{
88    // Erase the generics so we don't instantiate the actual implementation for
89    // every single test
90    let mut return_value = None;
91    let mut process_modifier = Some(process_modifier);
92    let mut in_parent = Some(in_parent);
93    let mut in_child = Some(in_child);
94
95    fork_impl(
96        test_name,
97        fork_id,
98        &mut |cmd| process_modifier.take().unwrap()(cmd),
99        &mut |child| return_value = Some(in_parent.take().unwrap()(child)),
100        &mut || in_child.take().unwrap()(),
101    )
102    .map(|_| return_value.unwrap())
103}
104
105fn fork_impl<T: Termination>(
106    test_name: &str,
107    fork_id: &str,
108    process_modifier: &mut dyn FnMut(&mut process::Command),
109    in_parent: &mut dyn FnMut(&mut Child),
110    in_child: &mut dyn FnMut() -> T,
111) -> Result<()> {
112    let mut occurs = env::var(OCCURS_ENV).unwrap_or_else(|_| String::new());
113    if occurs.contains(fork_id) {
114        match panic::catch_unwind(panic::AssertUnwindSafe(in_child)) {
115            Ok(test_result) => {
116                let rc = if test_result.report() == ExitCode::SUCCESS {
117                    0
118                } else {
119                    70
120                };
121                process::exit(rc)
122            }
123            // Assume that the default panic handler already printed something
124            //
125            // We don't use process::abort() since it produces core dumps on
126            // some systems and isn't something more special than a normal
127            // panic.
128            Err(_) => process::exit(70 /* EX_SOFTWARE */),
129        }
130    } else {
131        // Prevent misconfiguration creating a fork bomb
132        if occurs.len() > 16 * OCCURS_TERM_LENGTH {
133            panic!("test-fork: Not forking due to >=16 levels of recursion");
134        }
135
136        struct KillOnDrop(Child);
137        impl Drop for KillOnDrop {
138            fn drop(&mut self) {
139                // Kill the child if it hasn't exited yet
140                let _ = self.0.kill();
141
142                // Copy the child's output to our own
143                // Awkwardly, `print!()` and `println!()` are our only gateway
144                // to putting things in the captured output. Generally test
145                // output really is text, so work on that assumption and read
146                // line-by-line, converting lossily into UTF-8 so we can
147                // println!() it.
148
149                fn drain(read: &mut dyn Read, stderr: bool) {
150                    let mut buf = Vec::new();
151                    let mut br = io::BufReader::new(read);
152                    loop {
153                        // We can't use read_line() or lines() since they break if
154                        // there's any non-UTF-8 output at all. \n occurs at the
155                        // end of the line endings on all major platforms, so we
156                        // can just use that as a delimiter.
157                        if br.read_until(b'\n', &mut buf).is_err() {
158                            break;
159                        }
160                        if buf.is_empty() {
161                            break;
162                        }
163
164                        // not println!() because we already have a line ending
165                        // from above.
166                        let s = String::from_utf8_lossy(&buf);
167                        if stderr {
168                            eprint!("{s}");
169                        } else {
170                            print!("{s}");
171                        }
172                        buf.clear();
173                    }
174                }
175
176                if let Some(stdout) = self.0.stdout.as_mut() {
177                    let () = drain(stdout, false);
178                }
179
180                if let Some(stderr) = self.0.stderr.as_mut() {
181                    let () = drain(stderr, true);
182                }
183            }
184        }
185
186        occurs.push_str(fork_id);
187        let mut command =
188            process::Command::new(env::current_exe().expect("current_exe() failed, cannot fork"));
189        command
190            .args(cmdline::strip_cmdline(env::args())?)
191            .args(cmdline::RUN_TEST_ARGS)
192            .arg(test_name)
193            .env(OCCURS_ENV, &occurs)
194            .stdin(Stdio::null())
195            .stdout(Stdio::piped())
196            .stderr(Stdio::piped());
197        process_modifier(&mut command);
198
199        let mut child = command.spawn().map(KillOnDrop)?;
200        let () = in_parent(&mut child.0);
201
202        Ok(())
203    }
204}
205
206
207#[cfg(test)]
208mod test {
209    use super::*;
210
211    use std::io::Read;
212    use std::thread;
213
214    use crate::fork_id;
215
216
217    fn sleep(ms: u64) {
218        thread::sleep(::std::time::Duration::from_millis(ms));
219    }
220
221    fn capturing_output(cmd: &mut process::Command) {
222        cmd.stdout(Stdio::piped()).stderr(Stdio::inherit());
223    }
224
225    fn inherit_output(cmd: &mut process::Command) {
226        cmd.stdout(Stdio::inherit()).stderr(Stdio::inherit());
227    }
228
229    fn wait_for_child_output(child: &mut Child) -> String {
230        let mut output = String::new();
231        child
232            .stdout
233            .as_mut()
234            .unwrap()
235            .read_to_string(&mut output)
236            .unwrap();
237        assert!(child.wait().unwrap().success());
238        output
239    }
240
241    fn wait_for_child(child: &mut Child) {
242        assert!(child.wait().unwrap().success());
243    }
244
245    #[test]
246    fn fork_basically_works() {
247        let status = fork(
248            "fork::test::fork_basically_works",
249            fork_id!(),
250            |_| (),
251            |child| child.wait().unwrap(),
252            || println!("hello from child"),
253        )
254        .unwrap();
255        assert!(status.success());
256    }
257
258    #[test]
259    fn child_output_captured_and_repeated() {
260        let output = fork(
261            "fork::test::child_output_captured_and_repeated",
262            fork_id!(),
263            capturing_output,
264            wait_for_child_output,
265            || {
266                fork(
267                    "fork::test::child_output_captured_and_repeated",
268                    fork_id!(),
269                    |_| (),
270                    wait_for_child,
271                    || println!("hello from child"),
272                )
273                .unwrap()
274            },
275        )
276        .unwrap();
277        assert!(output.contains("hello from child"));
278    }
279
280    #[test]
281    fn child_killed_if_parent_exits_first() {
282        let output = fork(
283            "fork::test::child_killed_if_parent_exits_first",
284            fork_id!(),
285            capturing_output,
286            wait_for_child_output,
287            || {
288                fork(
289                    "fork::test::child_killed_if_parent_exits_first",
290                    fork_id!(),
291                    inherit_output,
292                    |_| (),
293                    || {
294                        sleep(100);
295                        println!("hello from child");
296                    },
297                )
298                .unwrap()
299            },
300        )
301        .unwrap();
302
303        sleep(200);
304        assert!(
305            !output.contains("hello from child"),
306            "Had unexpected output:\n{}",
307            output
308        );
309    }
310
311    #[test]
312    fn child_killed_if_parent_panics_first() {
313        let output = fork(
314            "fork::test::child_killed_if_parent_panics_first",
315            fork_id!(),
316            capturing_output,
317            wait_for_child_output,
318            || {
319                assert!(panic::catch_unwind(panic::AssertUnwindSafe(|| fork(
320                    "fork::test::child_killed_if_parent_panics_first",
321                    fork_id!(),
322                    inherit_output,
323                    |_| panic!("testing a panic, nothing to see here"),
324                    || {
325                        sleep(100);
326                        println!("hello from child");
327                    }
328                )
329                .unwrap()))
330                .is_err());
331            },
332        )
333        .unwrap();
334
335        sleep(200);
336        assert!(
337            !output.contains("hello from child"),
338            "Had unexpected output:\n{}",
339            output
340        );
341    }
342
343    #[test]
344    fn child_aborted_if_panics() {
345        let status = fork::<_, _, _, _, ()>(
346            "fork::test::child_aborted_if_panics",
347            fork_id!(),
348            |_| (),
349            |child| child.wait().unwrap(),
350            || panic!("testing a panic, nothing to see here"),
351        )
352        .unwrap();
353        assert_eq!(70, status.code().unwrap());
354    }
355}