1#![doc = include_str!("../examples/libtest-cli.rs")]
10#![cfg_attr(docsrs, feature(doc_auto_cfg))]
13#![forbid(unsafe_code)]
14#![warn(missing_debug_implementations, elided_lifetimes_in_paths)]
15
16use lexarg::Arg;
17use lexarg_error::LexError;
18
19#[derive(Debug, Default)]
23#[non_exhaustive]
24pub struct TestOpts {
25 pub list: bool,
26 pub filters: Vec<String>,
27 pub filter_exact: bool,
28 pub run_ignored: RunIgnored,
29 pub run_tests: bool,
30 pub bench_benchmarks: bool,
31 pub no_capture: bool,
32 pub show_output: bool,
33 pub color: ColorConfig,
34 pub format: OutputFormat,
35 pub test_threads: Option<std::num::NonZeroUsize>,
36 pub skip: Vec<String>,
37 pub fail_fast: bool,
41 pub allowed_unstable: Vec<String>,
42}
43
44#[derive(Copy, Clone, Debug, PartialEq, Eq)]
46pub enum RunIgnored {
47 Yes,
48 No,
49 Only,
51}
52
53impl Default for RunIgnored {
54 fn default() -> Self {
55 Self::No
56 }
57}
58
59#[derive(Copy, Clone, Debug)]
61pub enum ColorConfig {
62 AutoColor,
63 AlwaysColor,
64 NeverColor,
65}
66
67impl Default for ColorConfig {
68 fn default() -> Self {
69 Self::AutoColor
70 }
71}
72
73#[derive(Copy, Clone, Debug, PartialEq, Eq)]
75pub enum OutputFormat {
76 Pretty,
78 Terse,
80 Json,
82}
83
84impl Default for OutputFormat {
85 fn default() -> Self {
86 Self::Pretty
87 }
88}
89
90pub const UNSTABLE_OPTIONS: &str = "unstable-options";
91
92pub const OPTIONS_HELP: &str = r#"
93Options:
94 --skip FILTER Skip tests whose names contain FILTER (this flag can
95 be used multiple times)
96 --exact Exactly match filters rather than by substring
97 --test Run tests and not benchmarks
98 --bench Run benchmarks instead of tests
99 --ignored Run only ignored tests
100 --include-ignored
101 Run ignored and not ignored tests
102 --fail-fast Don't start new tests after the first failure
103 --no-capture don't capture stdout/stderr of each task, allow
104 printing directly
105 --show-output Show captured stdout of successful tests
106 --test-threads n_threads
107 Number of threads used for running tests in parallel
108 --color auto|always|never
109 Configure coloring of output:
110 auto = colorize if stdout is a tty and tests are run
111 on serially (default);
112 always = always colorize output;
113 never = never colorize output;
114 --format pretty|terse|json
115 Configure formatting of output:
116 pretty = Print verbose output;
117 terse = Display one character per test;
118 json = Output a json document;
119 --list List all tests and benchmarks
120 -q, --quiet Display one character per test instead of one line.
121 Alias to --format=terse
122 -Z unstable-options Enable nightly-only flags:
123 unstable-options = Allow use of experimental features
124"#;
125
126pub const AFTER_HELP: &str = r#"
127The FILTER string is tested against the name of all tests, and only those
128tests whose names contain the filter are run. Multiple filter strings may
129be passed, which will run all tests matching any of the filters.
130
131By default, all tests are run in parallel. This can be altered with the
132--test-threads flag when running
133tests (set it to 1).
134
135All tests have their standard output and standard error captured by default.
136This can be overridden with the --no-capture flag to a value other than "0".
137Logging is not captured by default.
138
139Test Attributes:
140
141 `#[test]` - Indicates a function is a test to be run. This function
142 takes no arguments.
143 `#[bench]` - Indicates a function is a benchmark to be run. This
144 function takes one argument (test::Bencher).
145 `#[should_panic]` - This function (also labeled with `#[test]`) will only pass if
146 the code causes a panic (an assertion failure or panic!)
147 A message may be provided, which the failure string must
148 contain: #[should_panic(expected = "foo")].
149 `#[ignore]` - When applied to a function which is already attributed as a
150 test, then the test runner will ignore these tests during
151 normal test runs. Running with --ignored or --include-ignored will run
152 these tests.
153"#;
154
155#[derive(Debug, Default)]
159pub struct TestOptsBuilder {
160 opts: TestOpts,
161 quiet: bool,
162 format: Option<OutputFormat>,
163 include_ignored: bool,
164 ignored: bool,
165}
166
167impl TestOptsBuilder {
168 pub fn new() -> Self {
169 Default::default()
170 }
171
172 pub fn parse_next<'a>(
174 &mut self,
175 parser: &mut lexarg::Parser<'a>,
176 arg: Arg<'a>,
177 ) -> Result<Option<Arg<'a>>, LexError<'a>> {
178 use lexarg::prelude::*;
179
180 match arg {
181 Long("include-ignored") => {
182 self.include_ignored = true;
183 }
184 Long("ignored") => self.ignored = true,
185 Long("test") => {
186 self.opts.run_tests = true;
187 }
188 Long("bench") => {
189 self.opts.bench_benchmarks = true;
190 }
191 Long("list") => {
192 self.opts.list = true;
193 }
194 Long("no-capture") => {
195 self.opts.no_capture = true;
196 }
197 Long("test-threads") => {
198 let test_threads = parser
199 .next_flag_value()
200 .ok_or_missing(Value(std::ffi::OsStr::new("NUM")))
201 .parse()
202 .within(arg)?;
203 self.opts.test_threads = Some(test_threads);
204 }
205 Long("skip") => {
206 let filter = parser
207 .next_flag_value()
208 .ok_or_missing(Value(std::ffi::OsStr::new("NAME")))
209 .string("NAME")
210 .within(arg)?;
211 self.opts.skip.push(filter.to_owned());
212 }
213 Long("exact") => {
214 self.opts.filter_exact = true;
215 }
216 Long("fail-fast") => {
217 self.opts.fail_fast = true;
218 }
219 Long("color") => {
220 let color = parser
221 .next_flag_value()
222 .ok_or_missing(Value(std::ffi::OsStr::new("WHEN")))
223 .one_of(&["auto", "always", "never"])
224 .within(arg)?;
225 self.opts.color = match color {
226 "auto" => ColorConfig::AutoColor,
227 "always" => ColorConfig::AlwaysColor,
228 "never" => ColorConfig::NeverColor,
229 _ => unreachable!("`one_of` should prevent this"),
230 };
231 }
232 Short("q") | Long("quiet") => {
233 self.format = None;
234 self.quiet = true;
235 }
236 Long("format") => {
237 self.quiet = false;
238 let format = parser
239 .next_flag_value()
240 .ok_or_missing(Value(std::ffi::OsStr::new("FORMAT")))
241 .one_of(&["pretty", "terse", "json"])
242 .within(arg)?;
243 self.format = Some(match format {
244 "pretty" => OutputFormat::Pretty,
245 "terse" => OutputFormat::Terse,
246 "json" => OutputFormat::Json,
247 _ => unreachable!("`one_of` should prevent this"),
248 });
249 }
250 Long("show-output") => {
251 self.opts.show_output = true;
252 }
253 Short("Z") => {
254 let feature = parser
255 .next_flag_value()
256 .ok_or_missing(Value(std::ffi::OsStr::new("FEATURE")))
257 .string("FEATURE")
258 .within(arg)?;
259 if !is_nightly() {
260 return Err(LexError::msg("expected nightly compiler").unexpected(arg));
261 }
262 self.opts.allowed_unstable.push(feature.to_owned());
264 }
265 Value(filter) => {
266 let filter = filter.string("FILTER")?;
267 self.opts.filters.push(filter.to_owned());
268 }
269 _ => {
270 return Ok(Some(arg));
271 }
272 }
273 Ok(None)
274 }
275
276 pub fn finish(mut self) -> Result<TestOpts, LexError<'static>> {
278 let allow_unstable_options = self
279 .opts
280 .allowed_unstable
281 .iter()
282 .any(|f| f == UNSTABLE_OPTIONS);
283
284 if self.format.is_some() && !allow_unstable_options {
285 return Err(LexError::msg("`--format` requires `-Zunstable-options`"));
286 }
287 if let Some(format) = self.format {
288 self.opts.format = format;
289 } else if self.quiet {
290 self.opts.format = OutputFormat::Terse;
291 }
292
293 self.opts.run_tests |= !self.opts.bench_benchmarks;
294
295 self.opts.run_ignored = match (self.include_ignored, self.ignored) {
296 (true, true) => {
297 return Err(LexError::msg(
298 "`--include-ignored` and `--ignored` are mutually exclusive",
299 ))
300 }
301 (true, false) => RunIgnored::Yes,
302 (false, true) => RunIgnored::Only,
303 (false, false) => RunIgnored::No,
304 };
305
306 let opts = self.opts;
307 Ok(opts)
308 }
309}
310
311fn is_nightly() -> bool {
313 let disable_unstable_features = option_env!("CFG_DISABLE_UNSTABLE_FEATURES")
315 .map(|s| s != "0")
316 .unwrap_or(false);
317 let bootstrap = std::env::var("RUSTC_BOOTSTRAP").is_ok();
319
320 bootstrap || !disable_unstable_features
321}
322
323#[doc = include_str!("../README.md")]
324#[cfg(doctest)]
325pub struct ReadmeDoctests;