Skip to main content

libcontainer/
test_utils.rs

1use nix::sys::wait;
2use serde::{Deserialize, Serialize};
3
4// Normally, error types are not implemented as serialize/deserialize, but to
5// pass the error from the child process to the parent process, we need to
6// implement an error type that can be serialized and deserialized.
7#[derive(Debug, Serialize, Deserialize)]
8struct ErrorEnclosure {
9    source: Option<Box<ErrorEnclosure>>,
10    description: String,
11}
12
13impl ErrorEnclosure {
14    pub fn new<T>(e: &T) -> ErrorEnclosure
15    where
16        T: ?Sized + std::error::Error,
17    {
18        ErrorEnclosure {
19            description: e.to_string(),
20            source: e.source().map(|s| Box::new(ErrorEnclosure::new(s))),
21        }
22    }
23}
24
25impl std::fmt::Display for ErrorEnclosure {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        write!(f, "{}", self.description)
28    }
29}
30
31impl std::error::Error for ErrorEnclosure {
32    fn source(&self) -> Option<&(dyn 'static + std::error::Error)> {
33        self.source
34            .as_ref()
35            .map(|source| &**source as &(dyn 'static + std::error::Error))
36    }
37
38    fn description(&self) -> &str {
39        &self.description
40    }
41}
42
43type ClosureResult = Result<(), ErrorEnclosure>;
44
45#[derive(Debug, thiserror::Error)]
46pub enum TestError {
47    #[error("failed to create channel")]
48    Channel(#[from] crate::channel::ChannelError),
49    #[error("failed to fork")]
50    Fork(#[source] nix::Error),
51    #[error("failed to wait for child process")]
52    Wait(#[source] nix::Error),
53    #[error("failed to run function in child process")]
54    Execution(#[source] Box<dyn std::error::Error + Send + Sync>),
55    #[error("the closure caused the child process to panic")]
56    Panic,
57}
58
59#[derive(Debug, thiserror::Error)]
60pub enum TestCallbackError {
61    #[error("{0}")]
62    Custom(String),
63    #[error("{0:?}")]
64    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
65}
66
67impl From<&str> for TestCallbackError {
68    fn from(s: &str) -> Self {
69        TestCallbackError::Custom(s.to_string())
70    }
71}
72
73impl From<String> for TestCallbackError {
74    fn from(s: String) -> Self {
75        TestCallbackError::Custom(s)
76    }
77}
78
79pub fn test_in_child_process<F>(cb: F) -> Result<(), TestError>
80where
81    F: FnOnce() -> Result<(), TestCallbackError> + std::panic::UnwindSafe,
82{
83    let (mut sender, mut receiver) = crate::channel::channel::<ClosureResult>()?;
84    match unsafe { nix::unistd::fork().map_err(TestError::Fork)? } {
85        nix::unistd::ForkResult::Parent { child } => {
86            // Close unused senders
87            sender.close().map_err(TestError::Channel)?;
88            let res = receiver.recv().map_err(TestError::Channel)?;
89            wait::waitpid(child, None).map_err(TestError::Wait)?;
90            res.map_err(|err| TestError::Execution(Box::new(err)))?;
91        }
92        nix::unistd::ForkResult::Child => {
93            // Close unused receiver in the child
94            receiver.close().map_err(TestError::Channel)?;
95            let test_result = match std::panic::catch_unwind(cb) {
96                Ok(ret) => ret.map_err(|err| ErrorEnclosure::new(&err)),
97                Err(_) => Err(ErrorEnclosure::new(&TestError::Panic)),
98            };
99
100            // If we can't send the error to the parent process, there is
101            // nothing we can do other than exit properly.
102            let _ = sender.send(test_result);
103            std::process::exit(0);
104        }
105    };
106
107    Ok(())
108}
109
110#[cfg(test)]
111mod tests {
112    use core::panic;
113
114    use anyhow::{Result, bail};
115
116    use super::*;
117
118    #[test]
119    fn test_child_process() -> Result<()> {
120        if test_in_child_process(|| Err(TestCallbackError::Custom("test error".to_string())))
121            .is_ok()
122        {
123            bail!("expecting the child process to return an error")
124        }
125
126        Ok(())
127    }
128
129    #[test]
130    fn test_panic_child_process() -> Result<()> {
131        let ret = test_in_child_process(|| {
132            panic!("test panic");
133        });
134        if ret.is_ok() {
135            bail!("expecting the child process to panic")
136        }
137
138        Ok(())
139    }
140}