collective_score_client/validator/
command.rs1use 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#[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}