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}