blob: a8b0ea92a0dfa04075c9385ecab6f6a039836be4 [file] [log] [blame]
//-
// 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());
}
}