trycmd/
spec.rs

1use std::collections::BTreeMap;
2
3#[derive(Debug)]
4pub(crate) struct RunnerSpec {
5    cases: Vec<CaseSpec>,
6    include: Option<Vec<String>>,
7    default_bin: Option<crate::schema::Bin>,
8    timeout: Option<std::time::Duration>,
9    env: crate::schema::Env,
10}
11
12impl RunnerSpec {
13    pub(crate) fn new() -> Self {
14        Self {
15            cases: Default::default(),
16            include: None,
17            default_bin: None,
18            timeout: Default::default(),
19            env: Default::default(),
20        }
21    }
22
23    pub(crate) fn case(
24        &mut self,
25        glob: &std::path::Path,
26        #[cfg_attr(miri, allow(unused_variables))] expected: Option<crate::schema::CommandStatus>,
27    ) {
28        self.cases.push(CaseSpec {
29            glob: glob.into(),
30            #[cfg(not(miri))]
31            expected,
32            #[cfg(miri)]
33            expected: Some(crate::schema::CommandStatus::Skipped),
34        });
35    }
36
37    pub(crate) fn include(&mut self, include: Option<Vec<String>>) {
38        self.include = include;
39    }
40
41    pub(crate) fn default_bin(&mut self, bin: Option<crate::schema::Bin>) {
42        self.default_bin = bin;
43    }
44
45    pub(crate) fn timeout(&mut self, time: Option<std::time::Duration>) {
46        self.timeout = time;
47    }
48
49    pub(crate) fn env(&mut self, key: impl Into<String>, value: impl Into<String>) {
50        self.env.add.insert(key.into(), value.into());
51    }
52
53    pub(crate) fn prepare(&mut self) -> crate::Runner {
54        let mut runner = crate::Runner::new();
55
56        // Both sort and let the last writer win to allow overriding specific cases within a glob
57        let mut cases: BTreeMap<std::path::PathBuf, crate::Case> = BTreeMap::new();
58
59        for spec in &self.cases {
60            if let Some(glob) = get_glob(&spec.glob) {
61                match ::glob::glob(glob) {
62                    Ok(paths) => {
63                        for path in paths {
64                            match path {
65                                Ok(path) => {
66                                    cases.insert(
67                                        path.clone(),
68                                        crate::Case {
69                                            path,
70                                            expected: spec.expected,
71                                            default_bin: self.default_bin.clone(),
72                                            timeout: self.timeout,
73                                            env: self.env.clone(),
74                                            error: None,
75                                        },
76                                    );
77                                }
78                                Err(err) => {
79                                    let path = err.path().to_owned();
80                                    let err = crate::Error::new(err.into_error().to_string());
81                                    cases.insert(path.clone(), crate::Case::with_error(path, err));
82                                }
83                            }
84                        }
85                    }
86                    Err(err) => {
87                        let err = crate::Error::new(err.to_string());
88                        cases.insert(
89                            spec.glob.clone(),
90                            crate::Case::with_error(spec.glob.clone(), err),
91                        );
92                    }
93                }
94            } else {
95                let path = spec.glob.as_path();
96                cases.insert(
97                    path.into(),
98                    crate::Case {
99                        path: path.into(),
100                        expected: spec.expected,
101                        default_bin: self.default_bin.clone(),
102                        timeout: self.timeout,
103                        env: self.env.clone(),
104                        error: None,
105                    },
106                );
107            }
108        }
109
110        for case in cases.into_values() {
111            if self.is_included(&case) {
112                runner.case(case);
113            }
114        }
115
116        runner
117    }
118
119    fn is_included(&self, case: &crate::Case) -> bool {
120        if let Some(include) = self.include.as_deref() {
121            include
122                .iter()
123                .any(|i| case.path.to_string_lossy().contains(i))
124        } else {
125            true
126        }
127    }
128}
129
130impl Default for RunnerSpec {
131    fn default() -> Self {
132        Self::new()
133    }
134}
135
136#[derive(Debug)]
137struct CaseSpec {
138    glob: std::path::PathBuf,
139    expected: Option<crate::schema::CommandStatus>,
140}
141
142fn get_glob(path: &std::path::Path) -> Option<&str> {
143    if let Some(utf8) = path.to_str() {
144        if utf8.contains('*') {
145            return Some(utf8);
146        }
147    }
148
149    None
150}