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#"
93Arguments:
94 [FILTER]... Skip tests whose name does not match one of the filters
95
96Options:
97 --fail-fast Don't start new tests after the first failure
98 --skip FILTER Skip tests whose names contain FILTER
99 (this flag can be used multiple times)
100 --exact Exactly match filters rather than by substring
101 --ignored Run only ignored tests
102 --include-ignored
103 Run ignored and not ignored tests
104 --test Run tests and not benchmarks
105 --bench Run benchmarks instead of tests
106 --no-capture don't capture stdout/stderr of each task,
107 allow printing directly
108 --show-output Show captured stdout of successful tests
109 --list List all tests and benchmarks
110 --test-threads NUM
111 Number of threads used for running tests in parallel
112 (default is >1)
113 --format <pretty|terse|json>
114 Configure formatting of output:
115 - pretty: Print verbose output
116 - terse: Display one character per test
117 - json: Output a json document
118 -q, --quiet Display one character per test instead of one line
119 (alias to --format=terse)
120 --color <auto|always|never>
121 Configure coloring of output:
122 - auto: detect terminal support (default)
123 - always: always colorize output
124 - never: never colorize output
125 -Z FLAG Enable nightly-only flags:
126 - unstable-options: Allow use of experimental features
127"#;
128
129pub const AFTER_HELP: &str = r#""#;
130
131#[derive(Debug, Default)]
135pub struct TestOptsBuilder {
136 opts: TestOpts,
137 quiet: bool,
138 format: Option<OutputFormat>,
139 include_ignored: bool,
140 ignored: bool,
141}
142
143impl TestOptsBuilder {
144 pub fn new() -> Self {
145 Default::default()
146 }
147
148 pub fn parse_next<'a>(
150 &mut self,
151 parser: &mut lexarg::Parser<'a>,
152 arg: Arg<'a>,
153 ) -> Result<Option<Arg<'a>>, LexError<'a>> {
154 use lexarg::prelude::*;
155
156 match arg {
157 Long("include-ignored") => {
158 self.include_ignored = true;
159 }
160 Long("ignored") => self.ignored = true,
161 Long("test") => {
162 self.opts.run_tests = true;
163 }
164 Long("bench") => {
165 self.opts.bench_benchmarks = true;
166 }
167 Long("list") => {
168 self.opts.list = true;
169 }
170 Long("no-capture") => {
171 self.opts.no_capture = true;
172 }
173 Long("test-threads") => {
174 let test_threads = parser
175 .next_flag_value()
176 .ok_or_missing(Value(std::ffi::OsStr::new("NUM")))
177 .parse()
178 .within(arg)?;
179 self.opts.test_threads = Some(test_threads);
180 }
181 Long("skip") => {
182 let filter = parser
183 .next_flag_value()
184 .ok_or_missing(Value(std::ffi::OsStr::new("NAME")))
185 .string("NAME")
186 .within(arg)?;
187 self.opts.skip.push(filter.to_owned());
188 }
189 Long("exact") => {
190 self.opts.filter_exact = true;
191 }
192 Long("fail-fast") => {
193 self.opts.fail_fast = true;
194 }
195 Long("color") => {
196 let color = parser
197 .next_flag_value()
198 .ok_or_missing(Value(std::ffi::OsStr::new("WHEN")))
199 .one_of(&["auto", "always", "never"])
200 .within(arg)?;
201 self.opts.color = match color {
202 "auto" => ColorConfig::AutoColor,
203 "always" => ColorConfig::AlwaysColor,
204 "never" => ColorConfig::NeverColor,
205 _ => unreachable!("`one_of` should prevent this"),
206 };
207 }
208 Short("q") | Long("quiet") => {
209 self.format = None;
210 self.quiet = true;
211 }
212 Long("format") => {
213 self.quiet = false;
214 let format = parser
215 .next_flag_value()
216 .ok_or_missing(Value(std::ffi::OsStr::new("FORMAT")))
217 .one_of(&["pretty", "terse", "json"])
218 .within(arg)?;
219 self.format = Some(match format {
220 "pretty" => OutputFormat::Pretty,
221 "terse" => OutputFormat::Terse,
222 "json" => OutputFormat::Json,
223 _ => unreachable!("`one_of` should prevent this"),
224 });
225 }
226 Long("show-output") => {
227 self.opts.show_output = true;
228 }
229 Short("Z") => {
230 let feature = parser
231 .next_flag_value()
232 .ok_or_missing(Value(std::ffi::OsStr::new("FEATURE")))
233 .string("FEATURE")
234 .within(arg)?;
235 if !is_nightly() {
236 return Err(LexError::msg("expected nightly compiler").unexpected(arg));
237 }
238 self.opts.allowed_unstable.push(feature.to_owned());
240 }
241 Value(filter) => {
242 let filter = filter.string("FILTER")?;
243 self.opts.filters.push(filter.to_owned());
244 }
245 _ => {
246 return Ok(Some(arg));
247 }
248 }
249 Ok(None)
250 }
251
252 pub fn finish(mut self) -> Result<TestOpts, LexError<'static>> {
254 let allow_unstable_options = self
255 .opts
256 .allowed_unstable
257 .iter()
258 .any(|f| f == UNSTABLE_OPTIONS);
259
260 if self.format.is_some() && !allow_unstable_options {
261 return Err(LexError::msg("`--format` requires `-Zunstable-options`"));
262 }
263 if let Some(format) = self.format {
264 self.opts.format = format;
265 } else if self.quiet {
266 self.opts.format = OutputFormat::Terse;
267 }
268
269 self.opts.run_tests |= !self.opts.bench_benchmarks;
270
271 self.opts.run_ignored = match (self.include_ignored, self.ignored) {
272 (true, true) => {
273 return Err(LexError::msg(
274 "`--include-ignored` and `--ignored` are mutually exclusive",
275 ))
276 }
277 (true, false) => RunIgnored::Yes,
278 (false, true) => RunIgnored::Only,
279 (false, false) => RunIgnored::No,
280 };
281
282 let opts = self.opts;
283 Ok(opts)
284 }
285}
286
287fn is_nightly() -> bool {
289 let disable_unstable_features = option_env!("CFG_DISABLE_UNSTABLE_FEATURES")
291 .map(|s| s != "0")
292 .unwrap_or(false);
293 let bootstrap = std::env::var("RUSTC_BOOTSTRAP").is_ok();
295
296 bootstrap || !disable_unstable_features
297}
298
299#[doc = include_str!("../README.md")]
300#[cfg(doctest)]
301pub struct ReadmeDoctests;