test_process_executor/
lib.rs

1use anyhow::anyhow;
2use std::ffi::OsStr;
3use std::fmt;
4use std::process::Command;
5use std::str::from_utf8;
6
7/// An execution environment, consisting of environment variables
8/// which are provided on the launch of each new process.
9pub struct Executor<K, V>
10where
11    K: AsRef<OsStr> + Clone,
12    V: AsRef<OsStr> + Clone,
13{
14    env: Vec<(K, V)>,
15}
16
17impl<K, V> Executor<K, V>
18where
19    K: AsRef<OsStr> + Clone,
20    V: AsRef<OsStr> + Clone,
21{
22    /// Initializes a new Executor.
23    ///
24    /// All environment variables are provided to processes launched
25    /// with the `run` method.
26    pub fn new(env: Vec<(K, V)>) -> Self {
27        Executor { env }
28    }
29
30    /// Launches a new subprocess and awaits its completion.
31    ///
32    /// Pretty-prints stdout/stderr on failure.
33    ///
34    /// # Panics
35    ///
36    /// This method is a little aggressive about panicking; it
37    /// can totally evolve structured errors if that would be useful.
38    /// However, given that the primary purpose is testing, this
39    /// behavior is *currently* acceptable.
40    ///
41    /// Panics if...
42    /// - `args` is empty.
43    /// - The sub-process fails to execute.
44    /// - The execution of the sub-process returns a non-zero exit code.
45    /// - The sub-process writes invalid UTF-8 stdout/stderr.
46    pub fn run<I, S>(&self, args: I)
47    where
48        I: IntoIterator<Item = S>,
49        S: AsRef<OsStr>,
50    {
51        Execution::run(args, self.env.clone())
52    }
53}
54
55struct Execution<S: AsRef<OsStr>> {
56    cmd: S,
57    args: Vec<S>,
58    result: Option<std::process::Output>,
59}
60
61impl<S: AsRef<OsStr>> Execution<S> {
62    fn run<I, K, V, E>(args: I, envs: E)
63    where
64        I: IntoIterator<Item = S>,
65        K: AsRef<OsStr>,
66        V: AsRef<OsStr>,
67        E: IntoIterator<Item = (K, V)>,
68    {
69        let mut iter = args.into_iter();
70        let mut exec = Execution {
71            cmd: iter
72                .next()
73                .ok_or_else(|| anyhow!("Missing command"))
74                .unwrap(),
75            args: iter.collect::<Vec<S>>(),
76            result: None,
77        };
78
79        exec.result = Some(
80            Command::new(&exec.cmd)
81                .args(&exec.args)
82                .envs(envs)
83                .output()
84                .expect("Failed to execute command"),
85        );
86        assert!(
87            exec.result.as_ref().unwrap().status.success(),
88            format!("{}", exec)
89        );
90    }
91}
92
93impl<S: AsRef<OsStr>> fmt::Display for Execution<S> {
94    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
95        let mut cmd = Vec::new();
96        cmd.push(self.cmd.as_ref());
97        for arg in &self.args {
98            cmd.push(arg.as_ref());
99        }
100        let cmd: Vec<String> = cmd
101            .into_iter()
102            .map(|osstr| osstr.to_string_lossy().to_string())
103            .collect();
104        write!(f, "\x1b[95m{}\x1b[0m", cmd.join(" "))?;
105        if let Some(out) = self.result.as_ref() {
106            if !out.status.success() {
107                write!(f, "\n{}", out.status)?;
108            }
109            if !out.stdout.is_empty() {
110                write!(f, "\n\x1b[92m{}\x1b[0m", from_utf8(&out.stdout).unwrap())?;
111            }
112            if !out.stderr.is_empty() {
113                write!(f, "\n\x1b[91m{}\x1b[0m", from_utf8(&out.stderr).unwrap())?;
114            }
115        }
116        Ok(())
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn export_right_environment_variable() {
126        let executor = Executor::new(vec![("FOO", "BAR")]);
127        executor.run(vec!["/bin/bash", "-c", "[ \"$FOO\" == \"BAR\" ]"]);
128    }
129
130    #[test]
131    #[should_panic]
132    fn export_wrong_environment_variable() {
133        let executor = Executor::new(vec![("FOO", "BAZINGA")]);
134        executor.run(vec!["/bin/bash", "-c", "[ \"$FOO\" == \"BAR\" ]"]);
135    }
136}