| //- |
| // Copyright 2018 Jason Lingle |
| // |
| // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or |
| // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license |
| // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your |
| // option. This file may not be copied, modified, or distributed |
| // except according to those terms. |
| |
| use std::fs; |
| use std::env; |
| use std::hash::{Hash, Hasher}; |
| use std::io::{self, BufRead, Seek}; |
| use std::panic; |
| use std::process; |
| |
| use fnv; |
| use tempfile; |
| |
| use cmdline; |
| use error::*; |
| use child_wrapper::ChildWrapper; |
| |
| const OCCURS_ENV: &str = "RUSTY_FORK_OCCURS"; |
| const OCCURS_TERM_LENGTH: usize = 17; /* ':' plus 16 hexits */ |
| |
| /// Simulate a process fork. |
| /// |
| /// The function documentation here only lists information unique to calling it |
| /// directly; please see the crate documentation for more details on how the |
| /// forking process works. |
| /// |
| /// Since this is not a true process fork, the calling code must be structured |
| /// to ensure that the child process, upon starting from the same entry point, |
| /// also reaches this same `fork()` call. Recursive forks are supported; the |
| /// child branch is taken from all child processes of the fork even if it is |
| /// not directly the child of a particular branch. However, encountering the |
| /// same fork point more than once in a single execution sequence of a child |
| /// process is not (e.g., putting this call in a recursive function) and |
| /// results in unspecified behaviour. |
| /// |
| /// The child's output is buffered into an anonymous temporary file. Before |
| /// this call returns, this output is copied to the parent's standard output |
| /// (passing through the redirect mechanism Rust test uses). |
| /// |
| /// `test_name` must exactly match the full path of the test function being |
| /// run. |
| /// |
| /// `fork_id` is a unique identifier identifying this particular fork location. |
| /// This *must* be stable across processes of the same executable; pointers are |
| /// not suitable stable, and string constants may not be suitably unique. The |
| /// [`rusty_fork_id!()`](macro.rusty_fork_id.html) macro is the recommended way |
| /// to supply this parameter. |
| /// |
| /// If this is the parent process, `in_parent` is invoked, and the return value |
| /// becomes the return value from this function. The callback is passed a |
| /// handle to the file which receives the child's output. If is the callee's |
| /// responsibility to wait for the child to exit. If this is the child process, |
| /// `in_child` is invoked, and when the callback returns, the child process |
| /// exits. |
| /// |
| /// If `in_parent` returns or panics before the child process has terminated, |
| /// the child process is killed. |
| /// |
| /// If `in_child` panics, the child process exits with a failure code |
| /// immediately rather than let the panic propagate out of the `fork()` call. |
| /// |
| /// `process_modifier` is invoked on the `std::process::Command` immediately |
| /// before spawning the new process. The callee may modify the process |
| /// parameters if desired, but should not do anything that would modify or |
| /// remove any environment variables beginning with `RUSTY_FORK_`. |
| /// |
| /// ## Panics |
| /// |
| /// Panics if the environment indicates that there are already at least 16 |
| /// levels of fork nesting. |
| /// |
| /// Panics if `std::env::current_exe()` fails determine the path to the current |
| /// executable. |
| /// |
| /// Panics if any argument to the current process is not valid UTF-8. |
| pub fn fork<ID, MODIFIER, PARENT, CHILD, R>( |
| test_name: &str, |
| fork_id: ID, |
| process_modifier: MODIFIER, |
| in_parent: PARENT, |
| in_child: CHILD) -> Result<R> |
| where |
| ID : Hash, |
| MODIFIER : FnOnce (&mut process::Command), |
| PARENT : FnOnce (&mut ChildWrapper, &mut fs::File) -> R, |
| CHILD : FnOnce () |
| { |
| let fork_id = id_str(fork_id); |
| |
| // Erase the generics so we don't instantiate the actual implementation for |
| // every single test |
| let mut return_value = None; |
| let mut process_modifier = Some(process_modifier); |
| let mut in_parent = Some(in_parent); |
| let mut in_child = Some(in_child); |
| |
| fork_impl(test_name, fork_id, |
| &mut |cmd| process_modifier.take().unwrap()(cmd), |
| &mut |child, file| return_value = Some( |
| in_parent.take().unwrap()(child, file)), |
| &mut || in_child.take().unwrap()()) |
| .map(|_| return_value.unwrap()) |
| } |
| |
| fn fork_impl(test_name: &str, fork_id: String, |
| process_modifier: &mut FnMut (&mut process::Command), |
| in_parent: &mut FnMut (&mut ChildWrapper, &mut fs::File), |
| in_child: &mut FnMut ()) -> Result<()> { |
| let mut occurs = env::var(OCCURS_ENV).unwrap_or_else(|_| String::new()); |
| if occurs.contains(&fork_id) { |
| match panic::catch_unwind(panic::AssertUnwindSafe(in_child)) { |
| Ok(_) => process::exit(0), |
| // Assume that the default panic handler already printed something |
| // |
| // We don't use process::abort() since it produces core dumps on |
| // some systems and isn't something more special than a normal |
| // panic. |
| Err(_) => process::exit(70 /* EX_SOFTWARE */), |
| } |
| } else { |
| // Prevent misconfiguration creating a fork bomb |
| if occurs.len() > 16 * OCCURS_TERM_LENGTH { |
| panic!("rusty-fork: Not forking due to >=16 levels of recursion"); |
| } |
| |
| let file = tempfile::tempfile()?; |
| |
| struct KillOnDrop(ChildWrapper, fs::File); |
| impl Drop for KillOnDrop { |
| fn drop(&mut self) { |
| // Kill the child if it hasn't exited yet |
| let _ = self.0.kill(); |
| |
| // Copy the child's output to our own |
| // Awkwardly, `print!()` and `println!()` are our only gateway |
| // to putting things in the captured output. Generally test |
| // output really is text, so work on that assumption and read |
| // line-by-line, converting lossily into UTF-8 so we can |
| // println!() it. |
| let _ = self.1.seek(io::SeekFrom::Start(0)); |
| |
| let mut buf = Vec::new(); |
| let mut br = io::BufReader::new(&mut self.1); |
| loop { |
| // We can't use read_line() or lines() since they break if |
| // there's any non-UTF-8 output at all. \n occurs at the |
| // end of the line endings on all major platforms, so we |
| // can just use that as a delimiter. |
| if br.read_until(b'\n', &mut buf).is_err() { |
| break; |
| } |
| if buf.is_empty() { |
| break; |
| } |
| |
| // not println!() because we already have a line ending |
| // from above. |
| print!("{}", String::from_utf8_lossy(&buf)); |
| buf.clear(); |
| } |
| } |
| } |
| |
| occurs.push_str(&fork_id); |
| let mut command = |
| process::Command::new( |
| env::current_exe() |
| .expect("current_exe() failed, cannot fork")); |
| command |
| .args(cmdline::strip_cmdline(env::args())?) |
| .args(cmdline::RUN_TEST_ARGS) |
| .arg(test_name) |
| .env(OCCURS_ENV, &occurs) |
| .stdin(process::Stdio::null()) |
| .stdout(file.try_clone()?) |
| .stderr(file.try_clone()?); |
| process_modifier(&mut command); |
| |
| let mut child = command.spawn().map(ChildWrapper::new) |
| .map(|p| KillOnDrop(p, file))?; |
| |
| let ret = in_parent(&mut child.0, &mut child.1); |
| |
| Ok(ret) |
| } |
| } |
| |
| fn id_str<ID : Hash>(id: ID) -> String { |
| let mut hasher = fnv::FnvHasher::default(); |
| id.hash(&mut hasher); |
| |
| return format!(":{:016X}", hasher.finish()); |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use std::io::Read; |
| use std::thread; |
| |
| use super::*; |
| |
| fn sleep(ms: u64) { |
| thread::sleep(::std::time::Duration::from_millis(ms)); |
| } |
| |
| fn capturing_output(cmd: &mut process::Command) { |
| // Only actually capture stdout since we can't use |
| // wait_with_output() since it for some reason consumes the `Child`. |
| cmd.stdout(process::Stdio::piped()) |
| .stderr(process::Stdio::inherit()); |
| } |
| |
| fn inherit_output(cmd: &mut process::Command) { |
| cmd.stdout(process::Stdio::inherit()) |
| .stderr(process::Stdio::inherit()); |
| } |
| |
| fn wait_for_child_output(child: &mut ChildWrapper, |
| _file: &mut fs::File) -> String { |
| let mut output = String::new(); |
| child.inner_mut().stdout.as_mut().unwrap() |
| .read_to_string(&mut output).unwrap(); |
| assert!(child.wait().unwrap().success()); |
| output |
| } |
| |
| fn wait_for_child(child: &mut ChildWrapper, |
| _file: &mut fs::File) { |
| assert!(child.wait().unwrap().success()); |
| } |
| |
| #[test] |
| fn fork_basically_works() { |
| let status = |
| fork("fork::test::fork_basically_works", rusty_fork_id!(), |
| |_| (), |
| |child, _| child.wait().unwrap(), |
| || println!("hello from child")).unwrap(); |
| assert!(status.success()); |
| } |
| |
| #[test] |
| fn child_output_captured_and_repeated() { |
| let output = fork( |
| "fork::test::child_output_captured_and_repeated", |
| rusty_fork_id!(), |
| capturing_output, wait_for_child_output, |
| || fork( |
| "fork::test::child_output_captured_and_repeated", |
| rusty_fork_id!(), |
| |_| (), wait_for_child, |
| || println!("hello from child")).unwrap()) |
| .unwrap(); |
| assert!(output.contains("hello from child")); |
| } |
| |
| #[test] |
| fn child_killed_if_parent_exits_first() { |
| let output = fork( |
| "fork::test::child_killed_if_parent_exits_first", |
| rusty_fork_id!(), |
| capturing_output, wait_for_child_output, |
| || fork( |
| "fork::test::child_killed_if_parent_exits_first", |
| rusty_fork_id!(), |
| inherit_output, |_, _| (), |
| || { |
| sleep(1_000); |
| println!("hello from child"); |
| }).unwrap()).unwrap(); |
| |
| sleep(2_000); |
| assert!(!output.contains("hello from child"), |
| "Had unexpected output:\n{}", output); |
| } |
| |
| #[test] |
| fn child_killed_if_parent_panics_first() { |
| let output = fork( |
| "fork::test::child_killed_if_parent_panics_first", |
| rusty_fork_id!(), |
| capturing_output, wait_for_child_output, |
| || { |
| assert!( |
| panic::catch_unwind(panic::AssertUnwindSafe(|| fork( |
| "fork::test::child_killed_if_parent_panics_first", |
| rusty_fork_id!(), |
| inherit_output, |
| |_, _| panic!("testing a panic, nothing to see here"), |
| || { |
| sleep(1_000); |
| println!("hello from child"); |
| }).unwrap())).is_err()); |
| }).unwrap(); |
| |
| sleep(2_000); |
| assert!(!output.contains("hello from child"), |
| "Had unexpected output:\n{}", output); |
| } |
| |
| #[test] |
| fn child_aborted_if_panics() { |
| let status = fork( |
| "fork::test::child_aborted_if_panics", |
| rusty_fork_id!(), |
| |_| (), |
| |child, _| child.wait().unwrap(), |
| || panic!("testing a panic, nothing to see here")).unwrap(); |
| assert_eq!(70, status.code().unwrap()); |
| } |
| } |