collective_score_client/validator/
command.rs

1use std::{
2    borrow::Cow,
3    fmt::{self, Display},
4    process::{Command as StdCommand, ExitStatus, Stdio},
5};
6
7use anyhow::{bail, Context, Result};
8use typed_builder::TypedBuilder;
9
10use super::{string_content::StringContent, Validator};
11
12fn check_status(status: ExitStatus, expected: u8, command: &Command) -> Result<()> {
13    let code: u8 = status
14        .code()
15        .with_context(|| format!("Command {command} has no status code!"))?
16        .try_into()
17        .with_context(|| {
18            format!("Failed to convert the status code of the command {command} to u8!")
19        })?;
20
21    if code != expected {
22        bail!("Command status code mismatch!");
23    }
24
25    Ok(())
26}
27
28pub enum StdioVariant {
29    Null,
30}
31
32#[derive(TypedBuilder)]
33pub struct Command<'a> {
34    #[builder(setter(into))]
35    program: Cow<'a, str>,
36    #[builder(default, setter(strip_option))]
37    args: Option<Vec<Cow<'a, str>>>,
38    #[builder(default, setter(strip_option))]
39    stdout: Option<StdioVariant>,
40    #[builder(default, setter(strip_option))]
41    stderr: Option<StdioVariant>,
42    #[builder(default, setter(strip_option))]
43    stdin: Option<StdioVariant>,
44}
45
46impl<'a> Command<'a> {
47    fn std(&self) -> StdCommand {
48        let mut command = StdCommand::new(&*self.program);
49
50        if let Some(args) = &self.args {
51            for arg in args {
52                command.arg(&**arg);
53            }
54        }
55
56        if let Some(stdout) = &self.stdout {
57            match stdout {
58                StdioVariant::Null => {
59                    command.stdout(Stdio::null());
60                }
61            }
62        }
63
64        if let Some(stderr) = &self.stderr {
65            match stderr {
66                StdioVariant::Null => {
67                    command.stderr(Stdio::null());
68                }
69            }
70        }
71
72        if let Some(stdin) = &self.stdin {
73            match stdin {
74                StdioVariant::Null => {
75                    command.stdin(Stdio::null());
76                }
77            }
78        }
79
80        command
81    }
82}
83
84impl<'a> Display for Command<'a> {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        write!(f, "`{}", self.program)?;
87
88        if let Some(args) = &self.args {
89            for arg in args {
90                write!(f, "{arg}")?;
91            }
92        }
93
94        writeln!(f, "`")
95    }
96}
97
98#[derive(TypedBuilder)]
99pub struct CommandStatus<'a> {
100    command: Command<'a>,
101    #[builder(default = 0)]
102    status_code: u8,
103}
104
105impl<'a> Validator for CommandStatus<'a> {
106    fn validate(&self) -> Result<()> {
107        let status = self
108            .command
109            .std()
110            .status()
111            .with_context(|| format!("Failed to run the command {}", self.command))?;
112
113        check_status(status, self.status_code, &self.command)
114    }
115}
116
117// TODO: Use `any` in builder after my PR is finally done :D
118#[derive(TypedBuilder)]
119pub struct CommandOutput<'a> {
120    command: Command<'a>,
121    #[builder(default, setter(strip_option))]
122    status_code: Option<u8>,
123    #[builder(default, setter(strip_option))]
124    stdout: Option<StringContent<'a>>,
125    #[builder(default, setter(strip_option))]
126    stderr: Option<StringContent<'a>>,
127}
128
129impl<'a> Validator for CommandOutput<'a> {
130    fn validate(&self) -> Result<()> {
131        let output = self
132            .command
133            .std()
134            .output()
135            .with_context(|| format!("Failed to run the command {}", self.command))?;
136
137        if let Some(status_code) = self.status_code {
138            check_status(output.status, status_code, &self.command)?;
139        }
140
141        if let Some(stdout) = &self.stdout {
142            if !stdout.is_match(&String::from_utf8_lossy(&output.stdout)) {
143                bail!("stdout mismatch for the command {}!", self.command);
144            }
145        }
146
147        if let Some(stderr) = &self.stderr {
148            if !stderr.is_match(&String::from_utf8_lossy(&output.stderr)) {
149                bail!("stderr mismatch for the command {}!", self.command);
150            }
151        }
152
153        Ok(())
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use crate::{
160        check::{Check, RunnableCheck},
161        validator::string_content::StringContent,
162    };
163
164    use super::{Command, CommandOutput, CommandStatus, StdioVariant};
165
166    #[test]
167    fn status_ok() {
168        Check::builder()
169            .description("Running ls")
170            .validator(
171                CommandStatus::builder()
172                    .command(
173                        Command::builder()
174                            .program("ls")
175                            .args(vec!["-l".into()])
176                            .stdout(StdioVariant::Null)
177                            .stderr(StdioVariant::Null)
178                            .stdin(StdioVariant::Null)
179                            .build(),
180                    )
181                    .build(),
182            )
183            .build()
184            .run()
185            .unwrap();
186    }
187
188    #[test]
189    fn status_not_found() {
190        Check::builder()
191            .description("Running a non-existent command")
192            .validator(
193                CommandStatus::builder()
194                    .command(
195                        Command::builder()
196                            .program("blablablanotfound")
197                            .stdout(StdioVariant::Null)
198                            .stderr(StdioVariant::Null)
199                            .stdin(StdioVariant::Null)
200                            .build(),
201                    )
202                    .status_code(127)
203                    .build(),
204            )
205            .build()
206            .run()
207            .unwrap_err();
208    }
209
210    #[test]
211    fn stdout() {
212        Check::builder()
213            .description("Checking stdout")
214            .validator(
215                CommandOutput::builder()
216                    .command(
217                        Command::builder()
218                            .program("ls")
219                            .args(vec!["-l".into()])
220                            .stderr(StdioVariant::Null)
221                            .stdin(StdioVariant::Null)
222                            .build(),
223                    )
224                    .stdout(StringContent::Part("src"))
225                    .build(),
226            )
227            .build()
228            .run()
229            .unwrap();
230    }
231
232    #[test]
233    fn stderr() {
234        Check::builder()
235            .description("Checking stderr")
236            .validator(
237                CommandOutput::builder()
238                    .command(
239                        Command::builder()
240                            .program("fish")
241                            .args(vec!["-c".into(), "echo ok >&2".into()])
242                            .stdout(StdioVariant::Null)
243                            .stdin(StdioVariant::Null)
244                            .build(),
245                    )
246                    .stderr(StringContent::Full("ok\n"))
247                    .build(),
248            )
249            .build()
250            .run()
251            .unwrap();
252    }
253}