1use std::borrow::Cow;
2
3#[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 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 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 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 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 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 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 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 pub fn timeout(&self, time: std::time::Duration) -> &Self {
76 self.runner.borrow_mut().timeout(Some(time));
77 self
78 }
79
80 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 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 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 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 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 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 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#[allow(clippy::needless_collect)] fn 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}