1use std::convert::TryFrom;
2
3#[derive(Debug, Default)]
4pub(crate) struct Config {
5 pub build: Option<String>,
6 cmd: Option<String>,
7 env: Option<std::collections::HashMap<String, String>>,
8 clear_env: bool,
9 output: Option<String>,
10 exit_code: Option<i32>,
11}
12
13impl Config {
14 pub fn parse(s: &str, doc_id: &str) -> ftd::p1::Result<Config> {
15 let parsed = ftd::p1::parse(s, doc_id)?;
16 let mut iter = parsed.iter();
17 let mut c = match iter.next() {
18 Some(p1) => {
19 if p1.name != "fbt" {
20 return Err(ftd::p1::Error::ParseError {
21 message: "first section's name is not 'fbt'".to_string(),
22 doc_id: doc_id.to_string(),
23 line_number: p1.line_number,
24 });
25 }
26
27 Config {
28 build: p1.header.string_optional(doc_id, p1.line_number, "build")?,
29 cmd: p1.header.string_optional(doc_id, p1.line_number, "cmd")?,
30 exit_code: p1
31 .header
32 .i32_optional(doc_id, p1.line_number, "exit-code")?,
33 env: None,
34 clear_env: p1.header.bool_with_default(
35 doc_id,
36 p1.line_number,
37 "clear-env",
38 false,
39 )?,
40 output: p1
41 .header
42 .string_optional(doc_id, p1.line_number, "output")?,
43 }
44 }
45 None => {
46 return Err(ftd::p1::Error::ParseError {
47 message: "no sections found".to_string(),
48 doc_id: doc_id.to_string(),
49 line_number: 0,
50 });
51 }
52 };
53
54 for s in iter {
55 match s.name.as_str() {
56 "env" => {
57 if c.env.is_some() {
58 return Err(ftd::p1::Error::ParseError {
59 message: "env provided more than once".to_string(),
60 doc_id: doc_id.to_string(),
61 line_number: s.line_number,
62 });
63 }
64 c.env = read_env(doc_id, &s.body)?;
65 }
66 _ => {
67 return Err(ftd::p1::Error::ParseError {
68 message: "unknown section".to_string(),
69 doc_id: doc_id.to_string(),
70 line_number: s.line_number,
71 });
72 }
73 }
74 }
75
76 Ok(c)
77 }
78}
79
80fn read_env(
81 doc_id: &str,
82 body: &Option<(usize, String)>,
83) -> ftd::p1::Result<Option<std::collections::HashMap<String, String>>> {
84 Ok(match body {
85 Some((line_number, v)) => {
86 let mut m = std::collections::HashMap::new();
87 for line in v.split('\n') {
88 let mut parts = line.splitn(2, '=');
89 match (parts.next(), parts.next()) {
90 (Some(k), Some(v)) => {
91 m.insert(k.to_string(), v.to_string());
92 }
93 _ => {
94 return Err(ftd::p1::Error::ParseError {
95 message: "invalid line in env".to_string(),
96 doc_id: doc_id.to_string(),
97 line_number: *line_number,
98 });
99 }
100 }
101 }
102 Some(m)
103 }
104 None => None,
105 })
106}
107
108#[derive(Debug)]
109pub(crate) struct TestConfig {
110 pub cmd: String,
111 env: Option<std::collections::HashMap<String, String>>,
112 clear_env: bool,
113 pub skip: Option<String>,
114 pub output: Option<String>,
115 pub stdin: Option<String>,
116 pub exit_code: i32,
117 pub stdout: Option<String>,
118 pub stderr: Option<String>,
119}
120
121impl TestConfig {
122 pub fn cmd(&self) -> std::process::Command {
123 let mut cmd = if cfg!(target_os = "windows") {
124 let mut c = std::process::Command::new("cmd");
125 c.args(&["/C", self.cmd.as_str()]);
126 c
127 } else {
128 let mut c = std::process::Command::new("sh");
129 c.args(&["-c", self.cmd.as_str()]);
130 c
131 };
132
133 if self.clear_env {
134 cmd.env_clear();
135 }
136
137 if let Some(ref env) = self.env {
138 cmd.envs(env.iter());
139 }
140 cmd.env(
141 "FBT_CWD",
142 std::env::current_dir()
143 .map(|v| v.to_string_lossy().to_string())
144 .unwrap_or_else(|_| "".into()),
145 );
146
147 if self.stdin.is_some() {
148 cmd.stdin(std::process::Stdio::piped());
149 }
150
151 cmd.stdout(std::process::Stdio::piped())
152 .stderr(std::process::Stdio::piped());
153
154 cmd
155 }
156
157 pub fn parse(s: &str, doc_id: &str, config: &Config) -> ftd::p1::Result<Self> {
158 let parsed = ftd::p1::parse(s, doc_id)?;
159 let mut iter = parsed.iter();
160 let mut c = match iter.next() {
161 Some(p1) => {
162 if p1.name != "fbt" {
163 return Err(ftd::p1::Error::ParseError {
164 message: "first section's name is not 'fbt'".to_string(),
165 doc_id: doc_id.to_string(),
166 line_number: p1.line_number,
167 });
168 }
169
170 TestConfig {
171 cmd: match p1
172 .header
173 .string_optional(doc_id, p1.line_number, "cmd")?
174 .or_else(|| config.cmd.clone())
175 {
176 Some(v) => v,
177 None => {
178 return Err(ftd::p1::Error::ParseError {
179 message: "cmd not found".to_string(),
180 doc_id: doc_id.to_string(),
181 line_number: p1.line_number,
182 })
183 }
184 },
185 skip: p1.header.string_optional(doc_id, p1.line_number, "skip")?,
186 exit_code: p1
187 .header
188 .i32_optional(doc_id, p1.line_number, "exit-code")?
189 .or(config.exit_code)
190 .unwrap_or(0),
191 stdin: None,
192 stdout: None,
193 stderr: None,
194 env: config.env.clone(),
195 clear_env: p1.header.bool_with_default(
196 doc_id,
197 p1.line_number,
198 "clear-env",
199 config.clear_env,
200 )?,
201 output: p1
202 .header
203 .string_optional(doc_id, p1.line_number, "output")?
204 .or_else(|| config.output.clone()),
205 }
206 }
207 None => {
208 return Err(ftd::p1::Error::ParseError {
209 message: "no sections found".to_string(),
210 doc_id: doc_id.to_string(),
211 line_number: 0,
212 });
213 }
214 };
215
216 for s in iter {
217 match s.name.as_str() {
218 "stdin" => {
219 if c.stdin.is_some() {
220 return Err(ftd::p1::Error::ParseError {
221 message: "stdin provided more than once".to_string(),
222 doc_id: doc_id.to_string(),
223 line_number: s.line_number,
224 });
225 }
226 c.stdin = s.body.as_ref().map(|(_, v)| v.clone());
227 }
228 "stdout" => {
229 if c.stdout.is_some() {
230 return Err(ftd::p1::Error::ParseError {
231 message: "stdout provided more than once".to_string(),
232 doc_id: doc_id.to_string(),
233 line_number: s.line_number,
234 });
235 }
236 c.stdout = s.body.as_ref().map(|(_, v)| v.clone());
237 }
238 "stderr" => {
239 if c.stderr.is_some() {
240 return Err(ftd::p1::Error::ParseError {
241 message: "stderr provided more than once".to_string(),
242 doc_id: doc_id.to_string(),
243 line_number: s.line_number,
244 });
245 }
246 c.stderr = s.body.as_ref().map(|(_, v)| v.clone());
247 }
248 "env" => {
249 c.env = match (read_env(doc_id, &s.body)?, &c.env) {
250 (Some(v), Some(e)) => {
251 let mut e = e.clone();
252 e.extend(v.into_iter());
253 Some(e)
254 }
255 (Some(v), None) => Some(v),
256 (None, v) => v.clone(),
257 };
258 }
259 _ => {
260 return Err(ftd::p1::Error::ParseError {
261 message: "unknown section".to_string(),
262 doc_id: doc_id.to_string(),
263 line_number: s.line_number,
264 });
265 }
266 }
267 }
268
269 Ok(c)
270 }
271}
272
273#[derive(Debug)]
274pub enum Error {
275 TestsFolderMissing,
276 CantReadConfig(std::io::Error),
277 InvalidConfig(ftd::p1::Error),
278 BuildFailedToLaunch(std::io::Error),
279 BuildFailed(std::process::Output),
280 TestsFolderNotReadable(std::io::Error),
281}
282
283#[derive(Debug)]
284pub struct Case {
285 pub id: String, pub result: Result<bool, crate::Failure>,
290 pub duration: std::time::Duration,
291}
292
293#[derive(Debug)]
294pub struct Output {
295 pub exit_code: i32,
296 pub stdout: String,
297 pub stderr: String,
298}
299
300impl Output {
301 pub fn replace(mut self, v: String) -> Self {
302 let private_v = format!("/private{}", v.as_str());
310 self.stdout = self.stdout.replace(private_v.as_str(), "<cwd>");
311 self.stderr = self.stderr.replace(private_v.as_str(), "<cwd>");
312 self.stdout = self.stdout.replace(v.as_str(), "<cwd>");
313 self.stderr = self.stderr.replace(v.as_str(), "<cwd>");
314
315 self
316 }
317}
318
319impl TryFrom<&std::process::Output> for Output {
320 type Error = &'static str;
321
322 fn try_from(o: &std::process::Output) -> std::result::Result<Self, Self::Error> {
323 Ok(Output {
324 exit_code: match o.status.code() {
325 Some(code) => code,
326 None => return Err("cant read exit_code"),
327 },
328 stdout: {
329 std::str::from_utf8(&o.stdout)
330 .unwrap_or("")
331 .trim()
332 .to_string()
333 },
334 stderr: {
335 std::str::from_utf8(&o.stderr)
336 .unwrap_or("")
337 .trim()
338 .to_string()
339 },
340 })
341 }
342}
343
344#[derive(Debug)]
345pub enum Failure {
346 Skipped {
347 reason: String,
348 },
349 CmdFileMissing,
350 CmdFileInvalid {
351 error: ftd::p1::Error,
352 },
353 CantReadCmdFile {
354 error: std::io::Error,
355 },
356 InputIsNotDir,
357 Other {
358 io: std::io::Error,
359 },
360 CommandFailed {
361 io: std::io::Error,
362 reason: &'static str,
363 },
364 UnexpectedStatusCode {
365 expected: i32,
366 output: Output,
367 },
368 CantReadOutput {
369 output: std::process::Output,
370 reason: &'static str,
371 },
372 StdoutMismatch {
373 expected: String,
374 output: Output,
375 },
376 StderrMismatch {
377 expected: String,
378 output: Output,
379 },
380 DirDiffError {
381 error: crate::DirDiffError,
382 },
383 OutputMismatch {
384 diff: crate::DirDiff,
385 },
386 FixMismatch,
387}