tests/
command.rs

1// SPDX-License-Identifier: Apache-2.0
2
3/// Module to run a binary in a forked process, check its output,
4/// and optionally return the output captured in stdout and stderr
5use std::process::{Command, Output};
6
7use derive_builder::Builder;
8use libtest_mimic::Failed;
9
10/// Create a [Failed] with a message and the stdout and stderr of the output
11fn command_err(msg: String, output: Output) -> Result<(String, String), Failed> {
12    let mut err = msg;
13    err.push_str(&format!(
14        "\nstdout: \"{}\"",
15        &String::from_utf8(output.stdout)?
16    ));
17    err.push_str(&format!(
18        "\nstderr: \"{}\"",
19        &String::from_utf8(output.stderr)?
20    ));
21    Err(err.into())
22}
23
24/// Structure which holds all of the information necessary to run a specific
25/// binary and (optionally) check its output for expected values
26#[derive(Builder, Default)]
27#[builder(setter(strip_option))]
28pub struct TestCommand<'a> {
29    /// The binary to run in a forked process
30    program: &'a str,
31    /// Optional: The arguments to pass to the binary
32    #[builder(default)]
33    args: Option<&'a [&'a str]>,
34    /// Optional: The expected return code
35    #[builder(default)]
36    expected_rc: Option<i32>,
37    /// Optional: Expected string to find in stdout
38    #[builder(default)]
39    expected_stdout: Option<&'a str>,
40    /// Optional: Expected string to find in stderr
41    #[builder(default)]
42    expected_stderr: Option<&'a str>,
43}
44
45impl TestCommand<'_> {
46    /// Runs the command as configured and returns the output captured in stdout and stderr
47    ///
48    /// It also checks for the expected values (if set in the builder)
49    pub fn test_result(&self) -> Result<(String, String), Failed> {
50        let output = if let Some(args) = self.args {
51            Command::new(self.program).args(args).output()?
52        } else {
53            Command::new(self.program).output()?
54        };
55        // Check the return code from the binary
56        if let Some(exp_rc) = self.expected_rc {
57            match output.status.code() {
58                Some(errno) => {
59                    if errno != exp_rc {
60                        return command_err(
61                            format!(
62                                "Unexpected errno from {} {:?}\n got: {}, expected: {}",
63                                self.program, self.args, errno, exp_rc
64                            ),
65                            output,
66                        );
67                    }
68                }
69                None => {
70                    return command_err(format!("{} terminated by signal", self.program), output)
71                }
72            }
73        }
74        // Check the output captured in stdout
75        let stdout = String::from_utf8(output.stdout.clone())?;
76        if let Some(exp_stdout) = self.expected_stdout {
77            if !stdout.contains(exp_stdout) {
78                return command_err(
79                    format!("Unexpected stdout, wanted: \"{exp_stdout}\""),
80                    output,
81                );
82            }
83        }
84        // Check the output captured in stderr
85        let stderr = String::from_utf8(output.stderr.clone())?;
86        if let Some(exp_stderr) = self.expected_stderr {
87            if !stderr.contains(exp_stderr) {
88                return command_err(
89                    format!("Unexpected stderr, wanted: \"{exp_stderr}\""),
90                    output,
91                );
92            }
93        }
94        Ok((stdout, stderr))
95    }
96
97    /// Convenience method to run [TestCommand::test_result] but discarding the pipe output
98    pub fn test(&self) -> Result<(), Failed> {
99        match self.test_result() {
100            Ok(_) => Ok(()),
101            Err(e) => Err(e),
102        }
103    }
104}