#![allow(clippy::unwrap_used)]
use super::*;
use crate::c::mock::{
CallBehavior, MockConfig, configured_strict, configured_with_fallback, is_mocking_enabled,
with_mock_system,
};
use errors::CallableDidNotExecuteError;
use errors::CallableExecutedError;
use errors::CallableStatusUnknownError;
use rstest::*;
use serde::{Deserialize, Serialize};
use std::error::Error;
use std::fs;
use std::io;
use std::path::Path;
use std::process;
use std::sync::mpsc::channel;
use std::thread;
use std::time;
use std::time::Duration;
use tempfile::NamedTempFile;
pub(crate) const TEST_TIMEOUT: Duration = Duration::from_secs(1);
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct MyResult {
value: i32,
}
fn error_eio() -> io::Error {
io::Error::from_raw_os_error(libc::EIO)
}
fn error_eagain() -> io::Error {
io::Error::from_raw_os_error(libc::EAGAIN)
}
fn error_enfile() -> io::Error {
io::Error::from_raw_os_error(libc::ENFILE)
}
fn error_eintr() -> io::Error {
io::Error::from_raw_os_error(libc::EINTR)
}
fn write_pid_to_file(path: &Path) -> io::Result<()> {
let pid: u32 = process::id();
fs::write(path, format!("{pid}\n"))
}
#[derive(Debug, PartialEq)]
enum WaitForPidfileToPopulateResult {
Success(i32),
Timeout,
Error(std::num::ParseIntError),
}
fn wait_for_pidfile_to_populate(
path_with_eventual_pid: &Path,
timeout: Duration,
) -> WaitForPidfileToPopulateResult {
let start = time::Instant::now();
loop {
if start.elapsed() >= timeout {
return WaitForPidfileToPopulateResult::Timeout;
}
if let Ok(child_pid) = fs::read_to_string(path_with_eventual_pid) {
if child_pid.ends_with('\n') {
match child_pid.trim().parse::<i32>() {
Ok(pid) => return WaitForPidfileToPopulateResult::Success(pid),
Err(e) => return WaitForPidfileToPopulateResult::Error(e),
}
}
}
thread::sleep(Duration::from_millis(10));
}
}
#[cfg(feature = "tracing")]
#[ctor::ctor]
fn before_tests() {
use tracing_subscriber::EnvFilter;
let env_filter = EnvFilter::builder()
.with_default_directive(Level::INFO.into())
.from_env_lossy();
tracing_subscriber::fmt().with_env_filter(env_filter).init();
}
#[rstest]
#[timeout(TEST_TIMEOUT)]
fn simple_example() {
let result = execute_in_isolated_process(|| MyResult { value: 42 }).unwrap();
assert_eq!(result, MyResult { value: 42 });
}
#[rstest]
#[timeout(TEST_TIMEOUT)]
#[allow(static_mut_refs)]
fn static_memory_mutation_without_isolation() {
static mut MEMORY: bool = false;
let mutate = || unsafe { MEMORY = true };
mutate();
unsafe {
assert!(MEMORY, "Static memory should be modified");
}
}
#[rstest]
#[timeout(TEST_TIMEOUT)]
#[allow(static_mut_refs)]
fn static_memory_mutation_with_isolation() {
static mut MEMORY: bool = false;
let mutate = || unsafe { MEMORY = true };
execute_in_isolated_process(mutate).unwrap();
unsafe {
assert!(
!MEMORY,
"Static memory should remain unmodified in parent process"
);
}
}
#[rstest]
#[timeout(TEST_TIMEOUT)]
fn isolate_memory_leak() {
fn check_memory_exists_and_holds_vec_data(ptr_str: &str) -> bool {
let addr = usize::from_str_radix(ptr_str.trim_start_matches("0x"), 16).unwrap();
let vec_ptr = addr as *const Vec<u8>;
if vec_ptr.is_null() {
return false;
}
std::panic::catch_unwind(|| {
unsafe {
let vec = &*vec_ptr;
if vec.capacity() != 1024 || vec.len() != 1024 {
return false;
}
if vec.first() != Some(&42) {
return false;
}
true
}
})
.unwrap_or(false)
}
let leaky_fn = || {
let data: Vec<u8> = vec![42; 1024];
let data = Box::new(data);
let uh_oh = Box::leak(data);
let leaked_ptr = format!("{uh_oh:p}");
assert!(
check_memory_exists_and_holds_vec_data(&leaked_ptr),
"The memory should exist in `leaky_fn()` where it was leaked"
);
leaked_ptr
};
let leaked_ptr: String = execute_in_isolated_process(leaky_fn).unwrap();
assert!(
!check_memory_exists_and_holds_vec_data(&leaked_ptr),
"The leaked memory doesn't exist out here though"
);
}
#[rstest]
#[timeout(TEST_TIMEOUT)]
fn all_function_types() {
fn function_pointer() -> MyResult {
MyResult { value: 42 }
}
let result = execute_in_isolated_process(function_pointer).unwrap();
assert_eq!(result, MyResult { value: 42 });
let fn_closure = || MyResult { value: 42 };
let result = execute_in_isolated_process(fn_closure).unwrap();
assert_eq!(result, MyResult { value: 42 });
let mut counter = 0;
let fn_mut_closure = || {
counter += 1;
MyResult { value: counter }
};
let result = execute_in_isolated_process(fn_mut_closure).unwrap();
assert_eq!(result, MyResult { value: 1 });
assert_eq!(counter, 0);
let value = String::from("hello");
let fn_once_closure = move || {
MyResult {
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
value: value.len() as i32,
}
};
let result = execute_in_isolated_process(fn_once_closure).unwrap();
assert_eq!(result, MyResult { value: 5 });
}
#[rstest]
#[timeout(TEST_TIMEOUT)]
fn handle_large_result() {
let initial_string = "The quick brown fox jumps over the lazy dog";
let result = execute_in_isolated_process(|| initial_string.repeat(10000)).unwrap();
assert_eq!(result.len(), 10000 * initial_string.len());
}
#[rstest]
#[timeout(TEST_TIMEOUT)]
fn panic_in_child() {
#[allow(clippy::semicolon_if_nothing_returned)]
let error = execute_in_isolated_process(|| {
panic!("Panic in child");
#[allow(clippy::unused_unit)]
#[allow(unreachable_code)]
()
})
.unwrap_err();
eprintln!("error: {error:?}");
if cfg!(target_os = "macos") {
assert!(matches!(
error,
MemIsolateError::CallableStatusUnknown(
CallableStatusUnknownError::ChildProcessKilledBySignal(5),
)
));
} else {
assert!(matches!(
error,
MemIsolateError::CallableStatusUnknown(
CallableStatusUnknownError::CallableProcessDiedDuringExecution,
)
));
}
}
#[rstest]
#[timeout(TEST_TIMEOUT)]
#[allow(clippy::unit_cmp)]
fn empty_result() {
assert_eq!(execute_in_isolated_process(|| {}).unwrap(), ());
}
#[rstest]
#[timeout(TEST_TIMEOUT)]
fn serialization_error() {
#[derive(Debug)]
struct CustomIteratorWrapper {
_data: Vec<i32>,
}
impl Serialize for CustomIteratorWrapper {
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
Err(serde::ser::Error::custom("Fake serialization error"))
}
}
impl<'de> Deserialize<'de> for CustomIteratorWrapper {
fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(CustomIteratorWrapper { _data: vec![] })
}
}
let result = execute_in_isolated_process(|| CustomIteratorWrapper {
_data: vec![1, 2, 3],
});
match result {
Err(MemIsolateError::CallableExecuted(CallableExecutedError::SerializationFailed(err))) => {
assert!(
err.contains("Fake serialization error"),
"Expected error about sequence length, got: {err}"
);
}
other => panic!("Expected SerializationFailed error, got: {other:?}"),
}
}
#[rstest]
#[timeout(TEST_TIMEOUT)]
fn deserialization_error() {
#[derive(Debug, PartialEq)]
struct DeserializationFailer {
data: i32,
}
impl Serialize for DeserializationFailer {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.data.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for DeserializationFailer {
fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Err(serde::de::Error::custom(
"Intentional deserialization failure",
))
}
}
let result = execute_in_isolated_process(|| DeserializationFailer { data: 42 });
match result {
Err(MemIsolateError::CallableExecuted(CallableExecutedError::DeserializationFailed(
err,
))) => {
assert!(
err.contains("Intentional deserialization failure"),
"Expected custom deserialization error, got: {err}"
);
}
other => panic!("Expected DeserializationFailed error, got: {other:?}"),
}
}
#[rstest]
#[timeout(TEST_TIMEOUT)]
fn with_mock_helper() {
with_mock_system(MockConfig::Fallback, |_| {
assert!(is_mocking_enabled());
let result = execute_in_isolated_process(|| MyResult { value: 42 }).unwrap();
assert_eq!(result, MyResult { value: 42 });
});
assert!(!is_mocking_enabled());
}
#[rstest]
#[timeout(TEST_TIMEOUT)]
fn pipe_error() {
with_mock_system(
configured_strict(|mock| {
let pipe_creation_error = error_enfile();
mock.expect_pipe(CallBehavior::Mock(Err(pipe_creation_error)));
}),
|_| {
let result = execute_in_isolated_process(|| MyResult { value: 42 });
let err = result.unwrap_err();
matches!(
err,
MemIsolateError::CallableDidNotExecute(
CallableDidNotExecuteError::PipeCreationFailed(_)
)
);
let pipe_creation_error = error_enfile();
assert_eq!(
err.source().unwrap().source().unwrap().to_string(),
pipe_creation_error.to_string()
);
},
);
}
#[rstest]
#[timeout(TEST_TIMEOUT)]
fn fork_error() {
with_mock_system(
configured_with_fallback(|mock| {
let fork_error = error_eagain();
mock.expect_fork(CallBehavior::Mock(Err(fork_error)));
}),
|_| {
let result = execute_in_isolated_process(|| MyResult { value: 42 });
let err = result.unwrap_err();
matches!(
err,
MemIsolateError::CallableDidNotExecute(CallableDidNotExecuteError::ForkFailed(_))
);
let fork_error = error_eagain();
assert_eq!(
err.source().unwrap().source().unwrap().to_string(),
fork_error.to_string()
);
},
);
}
#[rstest]
#[timeout(TEST_TIMEOUT)]
fn parent_pipe_close_failure() {
with_mock_system(
configured_with_fallback(|mock| {
let close_error = error_eio(); mock.expect_close(CallBehavior::Mock(Err(close_error)));
}),
|_| {
let result = execute_in_isolated_process(|| MyResult { value: 42 });
match result {
Err(MemIsolateError::CallableStatusUnknown(
CallableStatusUnknownError::ParentPipeCloseFailed(err),
)) => {
let expected_error = error_eio();
assert_eq!(err.kind(), expected_error.kind());
assert_eq!(err.raw_os_error(), expected_error.raw_os_error());
}
other => panic!("Expected ParentPipeCloseFailed error, got: {other:?}"),
}
},
);
}
#[rstest]
#[timeout(TEST_TIMEOUT)]
fn waitpid_child_process_exited_on_its_own() {
execute_in_isolated_process(|| {}).unwrap();
}
#[rstest]
#[timeout(Duration::from_secs(3))]
#[allow(clippy::semicolon_if_nothing_returned)]
fn waitpid_child_killed_by_signal() {
let tmp_file = NamedTempFile::new().expect("Failed to create temp file");
let tmp_path_clone = tmp_file.path().to_path_buf().clone();
let callable = move || {
write_pid_to_file(&tmp_path_clone).expect("Failed to write pid to temp file");
loop {
thread::park();
}
#[allow(unreachable_code)]
()
};
let (tx, rx) = channel();
thread::spawn(move || {
let result = execute_in_isolated_process(callable);
tx.send(result)
});
let timeout = Duration::from_secs(2);
if let WaitForPidfileToPopulateResult::Success(child_pid) =
wait_for_pidfile_to_populate(tmp_file.path(), timeout)
{
unsafe {
libc::kill(child_pid, libc::SIGTERM);
}
} else {
panic!("Failed to retrieve child pid from temp file");
}
let result = rx.recv().unwrap();
assert!(matches!(
result,
Err(MemIsolateError::CallableStatusUnknown(
CallableStatusUnknownError::ChildProcessKilledBySignal(libc::SIGTERM)
))
));
}
#[rstest]
#[timeout(Duration::from_secs(3))]
#[allow(clippy::semicolon_if_nothing_returned)]
fn waitpid_child_killed_by_signal_after_suspension_and_continuation() {
let tmp_file = NamedTempFile::new().expect("Failed to create temp file");
let tmp_path_clone = tmp_file.path().to_path_buf().clone();
let callable = move || {
write_pid_to_file(&tmp_path_clone).expect("Failed to write pid to temp file");
loop {
thread::park();
}
#[allow(unreachable_code)]
()
};
let (tx, rx) = channel();
thread::spawn(move || {
let result = execute_in_isolated_process(callable);
tx.send(result)
});
let timeout = Duration::from_secs(2);
if let WaitForPidfileToPopulateResult::Success(child_pid) =
wait_for_pidfile_to_populate(tmp_file.path(), timeout)
{
unsafe {
libc::kill(child_pid, libc::SIGSTOP);
}
thread::sleep(Duration::from_millis(100));
unsafe {
libc::kill(child_pid, libc::SIGCONT);
}
thread::sleep(Duration::from_millis(100));
unsafe {
libc::kill(child_pid, libc::SIGKILL);
}
} else {
panic!("Failed to retrieve child pid from temp file");
}
let result = rx.recv().unwrap();
assert!(matches!(
result,
Err(MemIsolateError::CallableStatusUnknown(
CallableStatusUnknownError::ChildProcessKilledBySignal(libc::SIGKILL)
))
));
}
#[rstest]
#[timeout(TEST_TIMEOUT)]
fn waitpid_interrupted_by_signal_mock() {
with_mock_system(
configured_with_fallback(|mock| {
mock.expect_waitpid(CallBehavior::Mock(Err(error_eintr())));
mock.expect_waitpid(CallBehavior::Mock(Err(error_eintr())));
mock.expect_waitpid(CallBehavior::Mock(Err(error_eintr())));
}),
|_| {
let result = execute_in_isolated_process(|| MyResult { value: 42 });
assert_eq!(result.unwrap(), MyResult { value: 42 });
},
);
}