fbt_lib/
types.rs

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, // 01_basic
286    // if Ok(true) => test passed
287    // if Ok(false) => test skipped
288    // if Err(Failure) => test failed
289    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        // on mac /private is added to temp folders
303        // amitu@MacBook-Pro fbt % ls /var/folders/kf/jfmbkscj7757mmr29mn3rksm0000gn/T/fbt/874862845293569866/input
304        // one
305        // amitu@MacBook-Pro fbt % ls /private/var/folders/kf/jfmbkscj7757mmr29mn3rksm0000gn/T/fbt/874862845293569866/input
306        // one
307        // both of them are the same folder, and we see the former path, but the lauched processes see the later
308
309        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}