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