trycmd/
cases.rs

1use std::borrow::Cow;
2
3/// Entry point for running tests
4#[derive(Debug, Default)]
5pub struct TestCases {
6    runner: std::cell::RefCell<crate::RunnerSpec>,
7    bins: std::cell::RefCell<crate::BinRegistry>,
8    substitutions: std::cell::RefCell<snapbox::Redactions>,
9    has_run: std::cell::Cell<bool>,
10}
11
12impl TestCases {
13    pub fn new() -> Self {
14        let s = Self::default();
15        s.runner
16            .borrow_mut()
17            .include(parse_include(std::env::args_os()));
18        s
19    }
20
21    /// Load tests from `glob`
22    pub fn case(&self, glob: impl AsRef<std::path::Path>) -> &Self {
23        self.runner.borrow_mut().case(glob.as_ref(), None);
24        self
25    }
26
27    /// Overwrite expected status for a test
28    pub fn pass(&self, glob: impl AsRef<std::path::Path>) -> &Self {
29        self.runner
30            .borrow_mut()
31            .case(glob.as_ref(), Some(crate::schema::CommandStatus::Success));
32        self
33    }
34
35    /// Overwrite expected status for a test
36    pub fn fail(&self, glob: impl AsRef<std::path::Path>) -> &Self {
37        self.runner
38            .borrow_mut()
39            .case(glob.as_ref(), Some(crate::schema::CommandStatus::Failed));
40        self
41    }
42
43    /// Overwrite expected status for a test
44    pub fn interrupted(&self, glob: impl AsRef<std::path::Path>) -> &Self {
45        self.runner.borrow_mut().case(
46            glob.as_ref(),
47            Some(crate::schema::CommandStatus::Interrupted),
48        );
49        self
50    }
51
52    /// Overwrite expected status for a test
53    pub fn skip(&self, glob: impl AsRef<std::path::Path>) -> &Self {
54        self.runner
55            .borrow_mut()
56            .case(glob.as_ref(), Some(crate::schema::CommandStatus::Skipped));
57        self
58    }
59
60    /// Set default bin, by path, for commands
61    pub fn default_bin_path(&self, path: impl AsRef<std::path::Path>) -> &Self {
62        let bin = Some(crate::schema::Bin::Path(path.as_ref().into()));
63        self.runner.borrow_mut().default_bin(bin);
64        self
65    }
66
67    /// Set default bin, by name, for commands
68    pub fn default_bin_name(&self, name: impl AsRef<str>) -> &Self {
69        let bin = Some(crate::schema::Bin::Name(name.as_ref().into()));
70        self.runner.borrow_mut().default_bin(bin);
71        self
72    }
73
74    /// Set default timeout for commands
75    pub fn timeout(&self, time: std::time::Duration) -> &Self {
76        self.runner.borrow_mut().timeout(Some(time));
77        self
78    }
79
80    /// Set default environment variable
81    pub fn env(&self, key: impl Into<String>, value: impl Into<String>) -> &Self {
82        self.runner.borrow_mut().env(key, value);
83        self
84    }
85
86    /// Add a bin to the "PATH" for cases to use
87    pub fn register_bin(
88        &self,
89        name: impl Into<String>,
90        path: impl Into<crate::schema::Bin>,
91    ) -> &Self {
92        self.bins
93            .borrow_mut()
94            .register_bin(name.into(), path.into());
95        self
96    }
97
98    /// Add a series of bins to the "PATH" for cases to use
99    pub fn register_bins<N: Into<String>, B: Into<crate::schema::Bin>>(
100        &self,
101        bins: impl IntoIterator<Item = (N, B)>,
102    ) -> &Self {
103        self.bins
104            .borrow_mut()
105            .register_bins(bins.into_iter().map(|(n, b)| (n.into(), b.into())));
106        self
107    }
108
109    /// Add a variable for normalizing output
110    ///
111    /// Variable names must be
112    /// - Surrounded by `[]`
113    /// - Consist of uppercase letters
114    ///
115    /// Variables will be preserved through `TRYCMD=overwrite` / `TRYCMD=dump`.
116    ///
117    /// **NOTE:** We do basic search/replaces so new any new output will blindly be replaced.
118    ///
119    /// Reserved names:
120    /// - `[..]`
121    /// - `[EXE]`
122    /// - `[CWD]`
123    /// - `[ROOT]`
124    ///
125    /// ## Example
126    ///
127    /// ```rust,no_run
128    /// #[test]
129    /// fn cli_tests() {
130    ///     trycmd::TestCases::new()
131    ///         .case("tests/cmd/*.trycmd")
132    ///         .insert_var("[VAR]", "value");
133    /// }
134    /// ```
135    pub fn insert_var(
136        &self,
137        var: &'static str,
138        value: impl Into<Cow<'static, str>>,
139    ) -> Result<&Self, crate::Error> {
140        let value = value.into();
141        let value = snapbox::filter::normalize_paths(&snapbox::filter::normalize_lines(&value));
142        self.substitutions.borrow_mut().insert(var, value)?;
143        Ok(self)
144    }
145
146    /// Batch add variables for normalizing output
147    ///
148    /// See [`TestCases::insert_var`].
149    pub fn extend_vars(
150        &self,
151        vars: impl IntoIterator<Item = (&'static str, impl Into<Cow<'static, str>>)>,
152    ) -> Result<&Self, crate::Error> {
153        self.substitutions
154            .borrow_mut()
155            .extend(vars.into_iter().map(|(var, value)| {
156                let value = value.into();
157                let value =
158                    snapbox::filter::normalize_paths(&snapbox::filter::normalize_lines(&value));
159                (var, value)
160            }))?;
161        Ok(self)
162    }
163
164    /// Remove an existing var values
165    ///
166    /// See [`TestCases::insert_var`].
167    pub fn clear_var(&self, var: &'static str) -> Result<&Self, crate::Error> {
168        self.substitutions.borrow_mut().remove(var)?;
169        Ok(self)
170    }
171
172    /// Run tests
173    ///
174    /// This will happen on `drop` if not done explicitly
175    pub fn run(&self) {
176        self.has_run.set(true);
177
178        let mode = parse_mode(std::env::var_os("TRYCMD").as_deref());
179        mode.initialize().unwrap();
180
181        let runner = self.runner.borrow_mut().prepare();
182        runner.run(&mode, &self.bins.borrow(), &self.substitutions.borrow());
183    }
184}
185
186impl std::panic::RefUnwindSafe for TestCases {}
187
188#[doc(hidden)]
189impl Drop for TestCases {
190    fn drop(&mut self) {
191        if !self.has_run.get() && !std::thread::panicking() {
192            self.run();
193        }
194    }
195}
196
197// Filter which test cases are run by trybuild.
198//
199//     $ cargo test -- ui trybuild=tuple_structs.rs
200//
201// The first argument after `--` must be the trybuild test name i.e. the name of
202// the function that has the #[test] attribute and calls trybuild. That's to get
203// Cargo to run the test at all. The next argument starting with `trybuild=`
204// provides a filename filter. Only test cases whose filename contains the
205// filter string will be run.
206#[allow(clippy::needless_collect)] // false positive https://github.com/rust-lang/rust-clippy/issues/5991
207fn parse_include(args: impl IntoIterator<Item = std::ffi::OsString>) -> Option<Vec<String>> {
208    let filters = args
209        .into_iter()
210        .flat_map(std::ffi::OsString::into_string)
211        .filter_map(|arg| {
212            const PREFIX: &str = "trycmd=";
213            if let Some(remainder) = arg.strip_prefix(PREFIX) {
214                if remainder.is_empty() {
215                    None
216                } else {
217                    Some(remainder.to_owned())
218                }
219            } else {
220                None
221            }
222        })
223        .collect::<Vec<String>>();
224
225    if filters.is_empty() {
226        None
227    } else {
228        Some(filters)
229    }
230}
231
232fn parse_mode(var: Option<&std::ffi::OsStr>) -> crate::Mode {
233    if var == Some(std::ffi::OsStr::new("overwrite")) {
234        crate::Mode::Overwrite
235    } else if var == Some(std::ffi::OsStr::new("dump")) {
236        crate::Mode::Dump("dump".into())
237    } else {
238        crate::Mode::Fail
239    }
240}