1#[cfg(feature = "rustc")]
2use crate::{
3 aux_builds::AuxBuilder, custom_flags::edition::Edition,
4 custom_flags::revision_args::RustcRevisionArgs, custom_flags::run::Run,
5 custom_flags::rustfix::RustfixMode, custom_flags::Flag, filter::Match,
6};
7use crate::{
8 diagnostics::{self, Diagnostics},
9 parser::CommandParserFunc,
10 per_test_config::{Comments, Condition, TestConfig},
11 CommandBuilder, Error, Errored, Errors,
12};
13use color_eyre::eyre::Result;
14use regex::bytes::Regex;
15use spanned::Spanned;
16use std::{
17 collections::BTreeMap,
18 num::NonZeroUsize,
19 path::{Path, PathBuf},
20 process::{Command, Output},
21 sync::{atomic::AtomicBool, Arc},
22};
23
24mod args;
25pub use args::{Args, Format};
26
27#[derive(Debug, Clone)]
28/// Central datastructure containing all information to run the tests.
29pub struct Config {
30 /// Host triple; usually will be auto-detected.
31 pub host: Option<String>,
32 /// `None` to run on the host, otherwise a target triple
33 pub target: Option<String>,
34 /// The folder in which to start searching for .rs files
35 pub root_dir: PathBuf,
36 /// The binary to actually execute.
37 pub program: CommandBuilder,
38 /// What to do in case the stdout/stderr output differs from the expected one.
39 pub output_conflict_handling: OutputConflictHandling,
40 /// The recommended command to bless failing tests.
41 pub bless_command: Option<String>,
42 /// Where to dump files like the binaries compiled from tests.
43 /// Defaults to `target/ui/index_of_config` in the current directory.
44 pub out_dir: PathBuf,
45 /// Skip test files whose names contain any of these entries.
46 pub skip_files: Vec<String>,
47 /// Only test files whose names contain any of these entries.
48 pub filter_files: Vec<String>,
49 /// Override the number of threads to use.
50 pub threads: Option<NonZeroUsize>,
51 /// Nextest emulation: only list the test itself, not its components.
52 pub list: bool,
53 /// Only run the tests that are ignored.
54 pub run_only_ignored: bool,
55 /// Filters must match exactly instead of just checking for substrings.
56 pub filter_exact: bool,
57 /// The default settings settable via `@` comments
58 pub comment_defaults: Comments,
59 /// The symbol(s) that signify the start of a comment.
60 pub comment_start: &'static str,
61 /// Custom comment parsers
62 pub custom_comments: BTreeMap<&'static str, CommandParserFunc>,
63 /// Custom diagnostic extractor (invoked on the output of tests)
64 pub diagnostic_extractor: fn(&Path, &[u8]) -> Diagnostics,
65 /// Handle to the global abort check.
66 pub abort_check: AbortCheck,
67}
68
69/// An atomic bool that can be set to `true` to abort all tests.
70/// Will not cancel child processes, but if set from a Ctrl+C handler,
71/// the pressing of Ctrl+C will already have cancelled child processes.
72#[derive(Clone, Debug, Default)]
73pub struct AbortCheck(Arc<AtomicBool>);
74
75impl AbortCheck {
76 /// Whether any test has been aborted.
77 pub fn aborted(&self) -> bool {
78 self.0.load(order:std::sync::atomic::Ordering::Relaxed)
79 }
80
81 /// Inform everyone that an abort has been requested
82 pub fn abort(&self) {
83 self.0.store(val:true, order:std::sync::atomic::Ordering::Relaxed)
84 }
85}
86
87/// Function that performs the actual output conflict handling.
88pub type OutputConflictHandling = fn(&Path, &[u8], &mut Errors, &TestConfig);
89
90impl Config {
91 /// Create a blank configuration that doesn't do anything interesting
92 pub fn dummy() -> Self {
93 let mut comment_defaults = Comments::default();
94 comment_defaults.base().require_annotations = Spanned::dummy(true).into();
95 Self {
96 host: Default::default(),
97 target: Default::default(),
98 root_dir: Default::default(),
99 program: CommandBuilder::cmd(""),
100 output_conflict_handling: error_on_output_conflict,
101 bless_command: Default::default(),
102 out_dir: Default::default(),
103 skip_files: Default::default(),
104 filter_files: Default::default(),
105 threads: Default::default(),
106 list: Default::default(),
107 run_only_ignored: Default::default(),
108 filter_exact: Default::default(),
109 comment_defaults,
110 comment_start: "//",
111 custom_comments: Default::default(),
112 diagnostic_extractor: diagnostics::default_diagnostics_extractor,
113 abort_check: Default::default(),
114 }
115 }
116
117 /// Create a configuration for testing the output of running
118 /// `rustc` on the test files.
119 #[cfg(feature = "rustc")]
120 pub fn rustc(root_dir: impl Into<PathBuf>) -> Self {
121 let mut comment_defaults = Comments::default();
122
123 #[derive(Debug)]
124 struct NeedsAsmSupport;
125
126 impl Flag for NeedsAsmSupport {
127 fn must_be_unique(&self) -> bool {
128 true
129 }
130 fn clone_inner(&self) -> Box<dyn Flag> {
131 Box::new(NeedsAsmSupport)
132 }
133 fn test_condition(
134 &self,
135 config: &Config,
136 _comments: &Comments,
137 _revision: &str,
138 ) -> bool {
139 let target = config.target.as_ref().unwrap();
140 static ASM_SUPPORTED_ARCHS: &[&str] = &[
141 "x86", "x86_64", "arm", "aarch64", "riscv32",
142 "riscv64",
143 // These targets require an additional asm_experimental_arch feature.
144 // "nvptx64", "hexagon", "mips", "mips64", "spirv", "wasm32",
145 ];
146 !ASM_SUPPORTED_ARCHS.iter().any(|arch| target.contains(arch))
147 }
148 }
149
150 comment_defaults
151 .base()
152 .add_custom("rustc-revision-args", RustcRevisionArgs);
153 comment_defaults
154 .base()
155 .add_custom("edition", Edition("2021".into()));
156 comment_defaults
157 .base()
158 .add_custom("rustfix", RustfixMode::MachineApplicable);
159 let filters = vec![
160 (Match::PathBackslash, b"/".to_vec()),
161 #[cfg(windows)]
162 (Match::Exact(vec![b'\r']), b"".to_vec()),
163 #[cfg(windows)]
164 (Match::Exact(br"\\?\".to_vec()), b"".to_vec()),
165 ];
166 comment_defaults
167 .base()
168 .normalize_stderr
169 .clone_from(&filters);
170 comment_defaults.base().normalize_stdout = filters;
171 comment_defaults.base().exit_status = Spanned::dummy(1).into();
172 comment_defaults.base().require_annotations = Spanned::dummy(true).into();
173 let mut config = Self {
174 host: None,
175 target: None,
176 root_dir: root_dir.into(),
177 program: CommandBuilder::rustc(),
178 output_conflict_handling: error_on_output_conflict,
179 bless_command: None,
180 out_dir: std::env::var_os("CARGO_TARGET_DIR")
181 .map(PathBuf::from)
182 .unwrap_or_else(|| std::env::current_dir().unwrap().join("target"))
183 .join("ui"),
184 skip_files: Vec::new(),
185 filter_files: Vec::new(),
186 threads: None,
187 list: false,
188 run_only_ignored: false,
189 filter_exact: false,
190 comment_defaults,
191 comment_start: "//",
192 custom_comments: Default::default(),
193 diagnostic_extractor: diagnostics::rustc::rustc_diagnostics_extractor,
194 abort_check: Default::default(),
195 };
196 config
197 .custom_comments
198 .insert("no-rustfix", |parser, _args, span| {
199 // args are ignored (can be used as comment)
200 parser.set_custom_once("no-rustfix", (), span);
201 });
202
203 config
204 .custom_comments
205 .insert("edition", |parser, args, _span| {
206 parser.set_custom_once("edition", Edition((*args).into()), args.span());
207 });
208
209 config
210 .custom_comments
211 .insert("needs-asm-support", |parser, _args, span| {
212 // args are ignored (can be used as comment)
213 parser.set_custom_once("needs-asm-support", NeedsAsmSupport, span);
214 });
215
216 config.custom_comments.insert("run", |parser, args, span| {
217 let set = |exit_code| {
218 parser.set_custom_once(
219 "run",
220 Run {
221 exit_code,
222 output_conflict_handling: None,
223 },
224 args.span(),
225 );
226 parser.exit_status = Spanned::new(0, span.clone()).into();
227 parser.require_annotations = Spanned::new(false, span.clone()).into();
228
229 let prev = parser
230 .custom
231 .insert("no-rustfix", Spanned::new(vec![Box::new(())], span.clone()));
232 parser.check(span, prev.is_none(), "`run` implies `no-rustfix`");
233 };
234 if args.is_empty() {
235 set(0);
236 } else {
237 match args.content.parse() {
238 Ok(exit_code) => {
239 set(exit_code);
240 }
241 Err(err) => parser.error(args.span(), err.to_string()),
242 }
243 }
244 });
245 config.custom_comments.insert("aux-build", |parser, args, span| {
246 let name = match args.split_once(":") {
247 Some((name, rest)) => {
248 parser.error(rest.span(), "proc macros are now auto-detected, you can remove the `:proc-macro` after the file name");
249 name
250 },
251 None => args,
252 };
253
254 parser
255 .add_custom_spanned("aux-build", AuxBuilder { aux_file: name.map(|n| n.into())}, span);
256 });
257 config
258 }
259
260 /// Create a configuration for testing the output of running
261 /// `cargo` on the test `Cargo.toml` files.
262 #[cfg(feature = "rustc")]
263 pub fn cargo(root_dir: impl Into<PathBuf>) -> Self {
264 let mut this = Self {
265 program: CommandBuilder::cargo(),
266 custom_comments: Default::default(),
267 diagnostic_extractor: diagnostics::rustc::cargo_diagnostics_extractor,
268 comment_start: "#",
269 ..Self::rustc(root_dir)
270 };
271 this.comment_defaults.base().custom.clear();
272 this
273 }
274
275 /// Populate the config with the values from parsed command line arguments.
276 pub fn with_args(&mut self, args: &Args) {
277 let Args {
278 ref filters,
279 check,
280 bless,
281 list,
282 exact,
283 ignored,
284 format: _,
285 threads,
286 ref skip,
287 } = *args;
288
289 self.threads = threads.or(self.threads);
290
291 self.filter_files.extend_from_slice(filters);
292 self.skip_files.extend_from_slice(skip);
293 self.run_only_ignored = ignored;
294 self.filter_exact = exact;
295
296 self.list = list;
297
298 if check {
299 self.output_conflict_handling = error_on_output_conflict;
300 } else if bless {
301 self.output_conflict_handling = bless_output_files;
302 }
303 }
304
305 /// Replace all occurrences of a path in stderr/stdout with a byte string.
306 #[track_caller]
307 pub fn path_filter(&mut self, path: &Path, replacement: &'static (impl AsRef<[u8]> + ?Sized)) {
308 self.path_stderr_filter(path, replacement);
309 self.path_stdout_filter(path, replacement);
310 }
311
312 /// Replace all occurrences of a path in stderr with a byte string.
313 #[track_caller]
314 pub fn path_stderr_filter(
315 &mut self,
316 path: &Path,
317 replacement: &'static (impl AsRef<[u8]> + ?Sized),
318 ) {
319 let pattern = path.canonicalize().unwrap();
320 self.comment_defaults.base().normalize_stderr.push((
321 pattern.parent().unwrap().into(),
322 replacement.as_ref().to_owned(),
323 ));
324 }
325
326 /// Replace all occurrences of a path in stdout with a byte string.
327 #[track_caller]
328 pub fn path_stdout_filter(
329 &mut self,
330 path: &Path,
331 replacement: &'static (impl AsRef<[u8]> + ?Sized),
332 ) {
333 let pattern = path.canonicalize().unwrap();
334 self.comment_defaults.base().normalize_stdout.push((
335 pattern.parent().unwrap().into(),
336 replacement.as_ref().to_owned(),
337 ));
338 }
339
340 /// Replace all occurrences of a regex pattern in stderr/stdout with a byte string.
341 #[track_caller]
342 pub fn filter(&mut self, pattern: &str, replacement: &'static (impl AsRef<[u8]> + ?Sized)) {
343 self.stderr_filter(pattern, replacement);
344 self.stdout_filter(pattern, replacement);
345 }
346
347 /// Replace all occurrences of a regex pattern in stderr with a byte string.
348 #[track_caller]
349 pub fn stderr_filter(
350 &mut self,
351 pattern: &str,
352 replacement: &'static (impl AsRef<[u8]> + ?Sized),
353 ) {
354 self.comment_defaults.base().normalize_stderr.push((
355 Regex::new(pattern).unwrap().into(),
356 replacement.as_ref().to_owned(),
357 ));
358 }
359
360 /// Replace all occurrences of a regex pattern in stdout with a byte string.
361 #[track_caller]
362 pub fn stdout_filter(
363 &mut self,
364 pattern: &str,
365 replacement: &'static (impl AsRef<[u8]> + ?Sized),
366 ) {
367 self.comment_defaults.base().normalize_stdout.push((
368 Regex::new(pattern).unwrap().into(),
369 replacement.as_ref().to_owned(),
370 ));
371 }
372
373 /// Make sure we have the host and target triples.
374 pub fn fill_host_and_target(&mut self) -> Result<()> {
375 if self.host.is_none() {
376 self.host = Some(
377 rustc_version::VersionMeta::for_command(std::process::Command::new(
378 &self.program.program,
379 ))
380 .map_err(|err| {
381 color_eyre::eyre::Report::new(err).wrap_err(format!(
382 "failed to parse rustc version info: {}",
383 self.program.display().to_string().replace('\\', "/")
384 ))
385 })?
386 .host,
387 );
388 }
389 if self.target.is_none() {
390 self.target = Some(self.host.clone().unwrap());
391 }
392 Ok(())
393 }
394
395 /// Check whether the host is the specified string
396 pub fn host_matches_target(&self) -> bool {
397 self.host.as_ref().expect("host should have been filled in")
398 == self
399 .target
400 .as_ref()
401 .expect("target should have been filled in")
402 }
403
404 pub(crate) fn get_pointer_width(&self) -> u8 {
405 // Taken 1:1 from compiletest-rs
406 fn get_pointer_width(triple: &str) -> u8 {
407 if (triple.contains("64")
408 && !triple.ends_with("gnux32")
409 && !triple.ends_with("gnu_ilp32"))
410 || triple.starts_with("s390x")
411 {
412 64
413 } else if triple.starts_with("avr") {
414 16
415 } else {
416 32
417 }
418 }
419 get_pointer_width(self.target.as_ref().unwrap())
420 }
421
422 pub(crate) fn test_condition(&self, condition: &Condition) -> bool {
423 let target = self.target.as_ref().unwrap();
424 match condition {
425 Condition::Bitwidth(bits) => bits.iter().any(|bits| self.get_pointer_width() == *bits),
426 Condition::Target(t) => t.iter().any(|t| target.contains(&**t)),
427 Condition::Host(t) => t.iter().any(|t| self.host.as_ref().unwrap().contains(&**t)),
428 Condition::OnHost => self.host_matches_target(),
429 }
430 }
431
432 /// Returns whether according to the in-file conditions, this file should be run.
433 pub fn test_file_conditions(&self, comments: &Comments, revision: &str) -> bool {
434 if comments
435 .for_revision(revision)
436 .flat_map(|r| r.ignore.iter())
437 .any(|c| self.test_condition(c))
438 {
439 return self.run_only_ignored;
440 }
441 if comments.for_revision(revision).any(|r| {
442 r.custom.values().any(|flags| {
443 flags
444 .content
445 .iter()
446 .any(|flag| flag.test_condition(self, comments, revision))
447 })
448 }) {
449 return self.run_only_ignored;
450 }
451 comments
452 .for_revision(revision)
453 .flat_map(|r| r.only.iter())
454 .all(|c| self.test_condition(c))
455 ^ self.run_only_ignored
456 }
457
458 pub(crate) fn aborted(&self) -> Result<(), Errored> {
459 if self.abort_check.aborted() {
460 Err(Errored::aborted())
461 } else {
462 Ok(())
463 }
464 }
465
466 pub(crate) fn run_command(&self, cmd: &mut Command) -> Result<Output, Errored> {
467 self.aborted()?;
468
469 let output = cmd.output().map_err(|err| Errored {
470 errors: vec![],
471 stderr: err.to_string().into_bytes(),
472 stdout: format!("could not spawn `{:?}` as a process", cmd.get_program()).into_bytes(),
473 command: format!("{cmd:?}"),
474 })?;
475
476 self.aborted()?;
477
478 Ok(output)
479 }
480}
481
482/// Fail the test when mismatches are found, if provided the command string
483/// in [`Config::bless_command`] will be suggested as a way to bless the
484/// test.
485pub fn error_on_output_conflict(
486 path: &Path,
487 output: &[u8],
488 errors: &mut Errors,
489 config: &TestConfig,
490) {
491 let normalized: Vec = config.normalize(text:output, &path.extension().unwrap().to_string_lossy());
492 let expected: Vec = std::fs::read(path).unwrap_or_default();
493 if normalized != expected {
494 errors.push(Error::OutputDiffers {
495 path: path.to_path_buf(),
496 actual: normalized,
497 output: output.to_vec(),
498 expected,
499 bless_command: config.config.bless_command.clone(),
500 });
501 }
502}
503
504/// Ignore mismatches in the stderr/stdout files.
505pub fn ignore_output_conflict(
506 _path: &Path,
507 _output: &[u8],
508 _errors: &mut Errors,
509 _config: &TestConfig,
510) {
511}
512
513/// Instead of erroring if the stderr/stdout differs from the expected
514/// automatically replace it with the found output (after applying filters).
515pub fn bless_output_files(path: &Path, output: &[u8], _errors: &mut Errors, config: &TestConfig) {
516 if output.is_empty() {
517 let _ = std::fs::remove_file(path);
518 } else {
519 let actual: Vec = config.normalize(text:output, &path.extension().unwrap().to_string_lossy());
520 std::fs::write(path, contents:actual).unwrap();
521 }
522}
523