1#![allow(
2 clippy::enum_variant_names,
3 clippy::useless_format,
4 clippy::too_many_arguments,
5 rustc::internal
6)]
7#![deny(missing_docs)]
8
9//! A crate to run the Rust compiler (or other binaries) and test their command line output.
10
11use bstr::ByteSlice;
12pub use color_eyre;
13use color_eyre::eyre::{eyre, Result};
14use crossbeam_channel::{unbounded, Receiver, Sender};
15use dependencies::{Build, BuildManager};
16use lazy_static::lazy_static;
17use parser::{ErrorMatch, MaybeSpanned, OptWithLine, Revisioned, Spanned};
18use regex::bytes::{Captures, Regex};
19use rustc_stderr::{Level, Message, Span};
20use status_emitter::{StatusEmitter, TestStatus};
21use std::borrow::Cow;
22use std::collections::{HashSet, VecDeque};
23use std::ffi::OsString;
24use std::num::NonZeroUsize;
25use std::path::{Component, Path, PathBuf, Prefix};
26use std::process::{Command, ExitStatus, Output};
27use std::thread;
28
29use crate::parser::{Comments, Condition};
30
31mod cmd;
32mod config;
33mod dependencies;
34mod diff;
35mod error;
36pub mod github_actions;
37mod mode;
38mod parser;
39mod rustc_stderr;
40pub mod status_emitter;
41#[cfg(test)]
42mod tests;
43
44pub use cmd::*;
45pub use config::*;
46pub use error::*;
47pub use mode::*;
48
49/// A filter's match rule.
50#[derive(Clone, Debug)]
51pub enum Match {
52 /// If the regex matches, the filter applies
53 Regex(Regex),
54 /// If the exact byte sequence is found, the filter applies
55 Exact(Vec<u8>),
56 /// Uses a heuristic to find backslashes in windows style paths
57 PathBackslash,
58}
59impl Match {
60 fn replace_all<'a>(&self, text: &'a [u8], replacement: &[u8]) -> Cow<'a, [u8]> {
61 match self {
62 Match::Regex(regex) => regex.replace_all(text, replacement),
63 Match::Exact(needle) => text.replace(needle, replacement).into(),
64 Match::PathBackslash => {
65 lazy_static! {
66 static ref PATH_RE: Regex = Regex::new(
67 r"(?x)
68 (?:
69 # Match paths to files with extensions that don't include spaces
70 \\(?:[\pL\pN.\-_']+[/\\])*[\pL\pN.\-_']+\.\pL+
71 |
72 # Allow spaces in absolute paths
73 [A-Z]:\\(?:[\pL\pN.\-_'\ ]+[/\\])+
74 )",
75 )
76 .unwrap();
77 }
78
79 PATH_RE.replace_all(text, |caps: &Captures<'_>| {
80 caps[0].replace(r"\", replacement)
81 })
82 }
83 }
84 }
85}
86
87impl From<&'_ Path> for Match {
88 fn from(v: &Path) -> Self {
89 let mut v: String = v.display().to_string();
90 // Normalize away windows canonicalized paths.
91 if v.starts_with(r"\\?\") {
92 v.drain(range:0..4);
93 }
94 let mut v: Vec = v.into_bytes();
95 // Normalize paths on windows to use slashes instead of backslashes,
96 // So that paths are rendered the same on all systems.
97 for c: &mut u8 in &mut v {
98 if *c == b'\\' {
99 *c = b'/';
100 }
101 }
102 Self::Exact(v)
103 }
104}
105
106impl From<Regex> for Match {
107 fn from(v: Regex) -> Self {
108 Self::Regex(v)
109 }
110}
111
112/// Replacements to apply to output files.
113pub type Filter = Vec<(Match, &'static [u8])>;
114
115/// Run all tests as described in the config argument.
116/// Will additionally process command line arguments.
117pub fn run_tests(mut config: Config) -> Result<()> {
118 let args: Args = Args::test()?;
119 if let Format::Pretty = args.format {
120 println!("Compiler: {}", config.program.display());
121 }
122
123 let name: String = config.root_dir.display().to_string();
124
125 let text: Text = match args.format {
126 Format::Terse => status_emitter::Text::quiet(),
127 Format::Pretty => status_emitter::Text::verbose(),
128 };
129 config.with_args(&args, default_bless:true);
130
131 run_tests_generic(
132 configs:vec![config],
133 default_file_filter,
134 default_per_file_config,
135 (text, status_emitter::Gha::<true> { name }),
136 )
137}
138
139/// The filter used by `run_tests` to only run on `.rs` files that are
140/// specified by [`Config::filter_files`] and [`Config::skip_files`].
141pub fn default_file_filter(path: &Path, config: &Config) -> bool {
142 path.extension().is_some_and(|ext: &OsStr| ext == "rs") && default_any_file_filter(path, config)
143}
144
145/// Run on all files that are specified by [`Config::filter_files`] and
146/// [`Config::skip_files`].
147///
148/// To only include rust files see [`default_file_filter`].
149pub fn default_any_file_filter(path: &Path, config: &Config) -> bool {
150 let path: String = path.display().to_string();
151 let contains_path: impl Fn(&[String]) -> bool = |files: &[String]| {
152 files.iter().any(|f: &String| {
153 if config.filter_exact {
154 *f == path
155 } else {
156 path.contains(f)
157 }
158 })
159 };
160
161 if contains_path(&config.skip_files) {
162 return false;
163 }
164
165 config.filter_files.is_empty() || contains_path(&config.filter_files)
166}
167
168/// The default per-file config used by `run_tests`.
169pub fn default_per_file_config(config: &mut Config, _path: &Path, file_contents: &[u8]) {
170 // Heuristic:
171 // * if the file contains `#[test]`, automatically pass `--cfg test`.
172 // * if the file does not contain `fn main()` or `#[start]`, automatically pass `--crate-type=lib`.
173 // This avoids having to spam `fn main() {}` in almost every test.
174 if file_contents.find(needle:b"#[proc_macro").is_some() {
175 config.program.args.push("--crate-type=proc-macro".into())
176 } else if file_contents.find(needle:b"#[test]").is_some() {
177 config.program.args.push("--test".into());
178 } else if file_contents.find(needle:b"fn main()").is_none()
179 && file_contents.find(needle:b"#[start]").is_none()
180 {
181 config.program.args.push("--crate-type=lib".into());
182 }
183}
184
185/// Create a command for running a single file, with the settings from the `config` argument.
186/// Ignores various settings from `Config` that relate to finding test files.
187pub fn test_command(mut config: Config, path: &Path) -> Result<Command> {
188 config.fill_host_and_target()?;
189 let extra_args: Vec = config.build_dependencies()?;
190
191 let comments: Comments =
192 Comments::parse_file(path)?.map_err(|errors: Vec| color_eyre::eyre::eyre!("{errors:#?}"))?;
193 let mut result: Command = build_command(path, &config, revision:"", &comments).unwrap();
194 result.args(extra_args);
195
196 Ok(result)
197}
198
199/// The possible non-failure results a single test can have.
200pub enum TestOk {
201 /// The test passed
202 Ok,
203 /// The test was ignored due to a rule (`//@only-*` or `//@ignore-*`)
204 Ignored,
205 /// The test was filtered with the `file_filter` argument.
206 Filtered,
207}
208
209/// The possible results a single test can have.
210pub type TestResult = Result<TestOk, Errored>;
211
212/// Information about a test failure.
213#[derive(Debug)]
214pub struct Errored {
215 /// Command that failed
216 command: Command,
217 /// The errors that were encountered.
218 errors: Vec<Error>,
219 /// The full stderr of the test run.
220 stderr: Vec<u8>,
221 /// The full stdout of the test run.
222 stdout: Vec<u8>,
223}
224
225struct TestRun {
226 result: TestResult,
227 status: Box<dyn status_emitter::TestStatus>,
228}
229
230/// A version of `run_tests` that allows more fine-grained control over running tests.
231///
232/// All `configs` are being run in parallel.
233/// If multiple configs are provided, the [`Config::threads`] value of the first one is used;
234/// the thread count of all other configs is ignored.
235pub fn run_tests_generic(
236 mut configs: Vec<Config>,
237 file_filter: impl Fn(&Path, &Config) -> bool + Sync,
238 per_file_config: impl Fn(&mut Config, &Path, &[u8]) + Sync,
239 status_emitter: impl StatusEmitter + Send,
240) -> Result<()> {
241 // Nexttest emulation: we act as if we are one single test.
242 if configs.iter().any(|c| c.list) {
243 if configs.iter().any(|c| !c.run_only_ignored) {
244 println!("ui_test: test");
245 }
246 return Ok(());
247 }
248 for config in &mut configs {
249 if config.filter_exact
250 && config.filter_files.len() == 1
251 && config.filter_files[0] == "ui_test"
252 {
253 config.filter_exact = false;
254 config.filter_files.clear();
255 }
256 }
257
258 for config in &mut configs {
259 config.fill_host_and_target()?;
260 }
261
262 let build_manager = BuildManager::new(&status_emitter);
263
264 let mut results = vec![];
265
266 let num_threads = match configs.first().and_then(|config| config.threads) {
267 Some(threads) => threads,
268 None => match std::env::var_os("RUST_TEST_THREADS") {
269 Some(n) => n
270 .to_str()
271 .ok_or_else(|| eyre!("could not parse RUST_TEST_THREADS env var"))?
272 .parse()?,
273 None => std::thread::available_parallelism()?,
274 },
275 };
276
277 run_and_collect(
278 num_threads,
279 |submit| {
280 let mut todo = VecDeque::new();
281 for config in &configs {
282 todo.push_back((config.root_dir.clone(), config));
283 }
284 while let Some((path, config)) = todo.pop_front() {
285 if path.is_dir() {
286 if path.file_name().unwrap() == "auxiliary" {
287 continue;
288 }
289 // Enqueue everything inside this directory.
290 // We want it sorted, to have some control over scheduling of slow tests.
291 let mut entries = std::fs::read_dir(path)
292 .unwrap()
293 .map(|e| e.unwrap().path())
294 .collect::<Vec<_>>();
295 entries.sort_by(|a, b| a.file_name().cmp(&b.file_name()));
296 for entry in entries {
297 todo.push_back((entry, config));
298 }
299 } else if file_filter(&path, config) {
300 let status = status_emitter.register_test(path);
301 // Forward .rs files to the test workers.
302 submit.send((status, config)).unwrap();
303 }
304 }
305 },
306 |receive, finished_files_sender| -> Result<()> {
307 for (status, config) in receive {
308 let path = status.path();
309 let file_contents = std::fs::read(path).unwrap();
310 let mut config = config.clone();
311 per_file_config(&mut config, path, &file_contents);
312 let result = match std::panic::catch_unwind(|| {
313 parse_and_test_file(&build_manager, &status, config, file_contents)
314 }) {
315 Ok(Ok(res)) => res,
316 Ok(Err(err)) => {
317 finished_files_sender.send(TestRun {
318 result: Err(err),
319 status,
320 })?;
321 continue;
322 }
323 Err(err) => {
324 finished_files_sender.send(TestRun {
325 result: Err(Errored {
326 command: Command::new("<unknown>"),
327 errors: vec![Error::Bug(
328 *Box::<dyn std::any::Any + Send + 'static>::downcast::<String>(
329 err,
330 )
331 .unwrap(),
332 )],
333 stderr: vec![],
334 stdout: vec![],
335 }),
336 status,
337 })?;
338 continue;
339 }
340 };
341 for result in result {
342 finished_files_sender.send(result)?;
343 }
344 }
345 Ok(())
346 },
347 |finished_files_recv| {
348 for run in finished_files_recv {
349 run.status.done(&run.result);
350
351 results.push(run);
352 }
353 },
354 )?;
355
356 let mut failures = vec![];
357 let mut succeeded = 0;
358 let mut ignored = 0;
359 let mut filtered = 0;
360
361 for run in results {
362 match run.result {
363 Ok(TestOk::Ok) => succeeded += 1,
364 Ok(TestOk::Ignored) => ignored += 1,
365 Ok(TestOk::Filtered) => filtered += 1,
366 Err(errored) => failures.push((run.status, errored)),
367 }
368 }
369
370 let mut failure_emitter = status_emitter.finalize(failures.len(), succeeded, ignored, filtered);
371 for (
372 status,
373 Errored {
374 command,
375 errors,
376 stderr,
377 stdout,
378 },
379 ) in &failures
380 {
381 let _guard = status.failed_test(command, stderr, stdout);
382 failure_emitter.test_failure(status, errors);
383 }
384
385 if failures.is_empty() {
386 Ok(())
387 } else {
388 Err(eyre!("tests failed"))
389 }
390}
391
392/// A generic multithreaded runner that has a thread for producing work,
393/// a thread for collecting work, and `num_threads` threads for doing the work.
394pub fn run_and_collect<SUBMISSION: Send, RESULT: Send>(
395 num_threads: NonZeroUsize,
396 submitter: impl FnOnce(Sender<SUBMISSION>) + Send,
397 runner: impl Sync + Fn(&Receiver<SUBMISSION>, Sender<RESULT>) -> Result<()>,
398 collector: impl FnOnce(Receiver<RESULT>) + Send,
399) -> Result<()> {
400 // A channel for files to process
401 let (submit, receive) = unbounded();
402
403 thread::scope(|s| {
404 // Create a thread that is in charge of walking the directory and submitting jobs.
405 // It closes the channel when it is done.
406 s.spawn(|| submitter(submit));
407
408 // A channel for the messages emitted by the individual test threads.
409 // Used to produce live updates while running the tests.
410 let (finished_files_sender, finished_files_recv) = unbounded();
411
412 s.spawn(|| collector(finished_files_recv));
413
414 let mut threads = vec![];
415
416 // Create N worker threads that receive files to test.
417 for _ in 0..num_threads.get() {
418 let finished_files_sender = finished_files_sender.clone();
419 threads.push(s.spawn(|| runner(&receive, finished_files_sender)));
420 }
421
422 for thread in threads {
423 thread.join().unwrap()?;
424 }
425 Ok(())
426 })
427}
428
429fn parse_and_test_file(
430 build_manager: &BuildManager<'_>,
431 status: &dyn TestStatus,
432 mut config: Config,
433 file_contents: Vec<u8>,
434) -> Result<Vec<TestRun>, Errored> {
435 let comments = parse_comments(&file_contents)?;
436 const EMPTY: &[String] = &[String::new()];
437 // Run the test for all revisions
438 let revisions = comments.revisions.as_deref().unwrap_or(EMPTY);
439 let mut built_deps = false;
440 Ok(revisions
441 .iter()
442 .map(|revision| {
443 let status = status.for_revision(revision);
444 // Ignore file if only/ignore rules do (not) apply
445 if !status.test_file_conditions(&comments, &config) {
446 return TestRun {
447 result: Ok(TestOk::Ignored),
448 status,
449 };
450 }
451
452 if !built_deps {
453 status.update_status("waiting for dependencies to finish building".into());
454 match build_manager.build(Build::Dependencies, &config) {
455 Ok(extra_args) => config.program.args.extend(extra_args),
456 Err(err) => {
457 return TestRun {
458 result: Err(err),
459 status,
460 }
461 }
462 }
463 status.update_status(String::new());
464 built_deps = true;
465 }
466
467 let result = status.run_test(build_manager, &config, &comments);
468 TestRun { result, status }
469 })
470 .collect())
471}
472
473fn parse_comments(file_contents: &[u8]) -> Result<Comments, Errored> {
474 match Comments::parse(content:file_contents) {
475 Ok(comments: Comments) => Ok(comments),
476 Err(errors: Vec) => Err(Errored {
477 command: Command::new(program:"parse comments"),
478 errors,
479 stderr: vec![],
480 stdout: vec![],
481 }),
482 }
483}
484
485fn build_command(
486 path: &Path,
487 config: &Config,
488 revision: &str,
489 comments: &Comments,
490) -> Result<Command, Errored> {
491 let mut cmd = config.program.build(&config.out_dir);
492 cmd.arg(path);
493 if !revision.is_empty() {
494 cmd.arg(format!("--cfg={revision}"));
495 }
496 for arg in comments
497 .for_revision(revision)
498 .flat_map(|r| r.compile_flags.iter())
499 {
500 cmd.arg(arg);
501 }
502 let edition = comments.edition(revision, config)?;
503
504 if let Some(edition) = edition {
505 cmd.arg("--edition").arg(&*edition);
506 }
507
508 cmd.envs(
509 comments
510 .for_revision(revision)
511 .flat_map(|r| r.env_vars.iter())
512 .map(|(k, v)| (k, v)),
513 );
514
515 Ok(cmd)
516}
517
518fn build_aux(
519 aux_file: &Path,
520 config: &Config,
521 build_manager: &BuildManager<'_>,
522) -> std::result::Result<Vec<OsString>, Errored> {
523 let file_contents = std::fs::read(aux_file).map_err(|err| Errored {
524 command: Command::new(format!("reading aux file `{}`", aux_file.display())),
525 errors: vec![],
526 stderr: err.to_string().into_bytes(),
527 stdout: vec![],
528 })?;
529 let comments = parse_comments(&file_contents)?;
530 assert_eq!(
531 comments.revisions, None,
532 "aux builds cannot specify revisions"
533 );
534
535 let mut config = config.clone();
536
537 // Strip any `crate-type` flags from the args, as we need to set our own,
538 // and they may conflict (e.g. `lib` vs `proc-macro`);
539 let mut prev_was_crate_type = false;
540 config.program.args.retain(|arg| {
541 if prev_was_crate_type {
542 prev_was_crate_type = false;
543 return false;
544 }
545 if arg == "--test" {
546 false
547 } else if arg == "--crate-type" {
548 prev_was_crate_type = true;
549 false
550 } else if let Some(arg) = arg.to_str() {
551 !arg.starts_with("--crate-type=")
552 } else {
553 true
554 }
555 });
556
557 default_per_file_config(&mut config, aux_file, &file_contents);
558
559 // Put aux builds into a separate directory per path so that multiple aux files
560 // from different directories (but with the same file name) don't collide.
561 let relative = strip_path_prefix(aux_file.parent().unwrap(), &config.out_dir);
562
563 config.out_dir.extend(relative);
564
565 let mut aux_cmd = build_command(aux_file, &config, "", &comments)?;
566
567 let mut extra_args = build_aux_files(
568 aux_file.parent().unwrap(),
569 &comments,
570 "",
571 &config,
572 build_manager,
573 )?;
574 // Make sure we see our dependencies
575 aux_cmd.args(extra_args.iter());
576
577 aux_cmd.arg("--emit=link");
578 let filename = aux_file.file_stem().unwrap().to_str().unwrap();
579 let output = aux_cmd.output().unwrap();
580 if !output.status.success() {
581 let error = Error::Command {
582 kind: "compilation of aux build failed".to_string(),
583 status: output.status,
584 };
585 return Err(Errored {
586 command: aux_cmd,
587 errors: vec![error],
588 stderr: rustc_stderr::process(aux_file, &output.stderr).rendered,
589 stdout: output.stdout,
590 });
591 }
592
593 // Now run the command again to fetch the output filenames
594 aux_cmd.arg("--print").arg("file-names");
595 let output = aux_cmd.output().unwrap();
596 assert!(output.status.success());
597
598 for file in output.stdout.lines() {
599 let file = std::str::from_utf8(file).unwrap();
600 let crate_name = filename.replace('-', "_");
601 let path = config.out_dir.join(file);
602 extra_args.push("--extern".into());
603 let mut cname = OsString::from(&crate_name);
604 cname.push("=");
605 cname.push(path);
606 extra_args.push(cname);
607 // Help cargo find the crates added with `--extern`.
608 extra_args.push("-L".into());
609 extra_args.push(config.out_dir.as_os_str().to_os_string());
610 }
611 Ok(extra_args)
612}
613
614impl dyn TestStatus {
615 fn run_test(
616 &self,
617 build_manager: &BuildManager<'_>,
618 config: &Config,
619 comments: &Comments,
620 ) -> TestResult {
621 let path = self.path();
622 let revision = self.revision();
623
624 let extra_args = build_aux_files(
625 &path.parent().unwrap().join("auxiliary"),
626 comments,
627 revision,
628 config,
629 build_manager,
630 )?;
631
632 let mut config = config.clone();
633
634 // Put aux builds into a separate directory per path so that multiple aux files
635 // from different directories (but with the same file name) don't collide.
636 let relative = strip_path_prefix(path.parent().unwrap(), &config.out_dir);
637
638 config.out_dir.extend(relative);
639
640 let mut cmd = build_command(path, &config, revision, comments)?;
641 cmd.args(&extra_args);
642 let stdin = path.with_extension(if revision.is_empty() {
643 "stdin".into()
644 } else {
645 format!("{revision}.stdin")
646 });
647 if stdin.exists() {
648 cmd.stdin(std::fs::File::open(stdin).unwrap());
649 }
650
651 let (cmd, status, stderr, stdout) = self.run_command(cmd)?;
652
653 let mode = config.mode.maybe_override(comments, revision)?;
654 let cmd = check_test_result(
655 cmd,
656 match *mode {
657 Mode::Run { .. } => Mode::Pass,
658 _ => *mode,
659 },
660 path,
661 &config,
662 revision,
663 comments,
664 status,
665 &stdout,
666 &stderr,
667 )?;
668
669 if let Mode::Run { .. } = *mode {
670 return run_test_binary(mode, path, revision, comments, cmd, &config);
671 }
672
673 run_rustfix(
674 &stderr, &stdout, path, comments, revision, &config, *mode, extra_args,
675 )?;
676 Ok(TestOk::Ok)
677 }
678
679 /// Run a command, and if it takes more than 100ms, start appending the last stderr/stdout
680 /// line to the current status spinner.
681 fn run_command(
682 &self,
683 mut cmd: Command,
684 ) -> Result<(Command, ExitStatus, Vec<u8>, Vec<u8>), Errored> {
685 match cmd.output() {
686 Err(err) => Err(Errored {
687 errors: vec![],
688 stderr: err.to_string().into_bytes(),
689 stdout: format!("could not spawn `{:?}` as a process", cmd.get_program())
690 .into_bytes(),
691 command: cmd,
692 }),
693 Ok(Output {
694 status,
695 stdout,
696 stderr,
697 }) => Ok((cmd, status, stderr, stdout)),
698 }
699 }
700}
701
702fn build_aux_files(
703 aux_dir: &Path,
704 comments: &Comments,
705 revision: &str,
706 config: &Config,
707 build_manager: &BuildManager<'_>,
708) -> Result<Vec<OsString>, Errored> {
709 let mut extra_args = vec![];
710 for rev in comments.for_revision(revision) {
711 for aux in &rev.aux_builds {
712 let line = aux.line();
713 let aux = &**aux;
714 let aux_file = if aux.starts_with("..") {
715 aux_dir.parent().unwrap().join(aux)
716 } else {
717 aux_dir.join(aux)
718 };
719 extra_args.extend(
720 build_manager
721 .build(
722 Build::Aux {
723 aux_file: strip_path_prefix(
724 &aux_file.canonicalize().map_err(|err| Errored {
725 command: Command::new(format!(
726 "canonicalizing path `{}`",
727 aux_file.display()
728 )),
729 errors: vec![],
730 stderr: err.to_string().into_bytes(),
731 stdout: vec![],
732 })?,
733 &std::env::current_dir().unwrap(),
734 )
735 .collect(),
736 },
737 config,
738 )
739 .map_err(
740 |Errored {
741 command,
742 errors,
743 stderr,
744 stdout,
745 }| Errored {
746 command,
747 errors: vec![Error::Aux {
748 path: aux_file,
749 errors,
750 line,
751 }],
752 stderr,
753 stdout,
754 },
755 )?,
756 );
757 }
758 }
759 Ok(extra_args)
760}
761
762fn run_test_binary(
763 mode: MaybeSpanned<Mode>,
764 path: &Path,
765 revision: &str,
766 comments: &Comments,
767 mut cmd: Command,
768 config: &Config,
769) -> TestResult {
770 let revision = if revision.is_empty() {
771 "run".to_string()
772 } else {
773 format!("run.{revision}")
774 };
775 cmd.arg("--print").arg("file-names");
776 let output = cmd.output().unwrap();
777 assert!(output.status.success());
778
779 let mut files = output.stdout.lines();
780 let file = files.next().unwrap();
781 assert_eq!(files.next(), None);
782 let file = std::str::from_utf8(file).unwrap();
783 let exe_file = config.out_dir.join(file);
784 let mut exe = Command::new(&exe_file);
785 let stdin = path.with_extension(format!("{revision}.stdin"));
786 if stdin.exists() {
787 exe.stdin(std::fs::File::open(stdin).unwrap());
788 }
789 let output = exe
790 .output()
791 .unwrap_or_else(|err| panic!("exe file: {}: {err}", exe_file.display()));
792
793 let mut errors = vec![];
794
795 check_test_output(
796 path,
797 &mut errors,
798 &revision,
799 config,
800 comments,
801 &output.stdout,
802 &output.stderr,
803 );
804
805 errors.extend(mode.ok(output.status).err());
806 if errors.is_empty() {
807 Ok(TestOk::Ok)
808 } else {
809 Err(Errored {
810 command: exe,
811 errors,
812 stderr: vec![],
813 stdout: vec![],
814 })
815 }
816}
817
818fn run_rustfix(
819 stderr: &[u8],
820 stdout: &[u8],
821 path: &Path,
822 comments: &Comments,
823 revision: &str,
824 config: &Config,
825 mode: Mode,
826 extra_args: Vec<OsString>,
827) -> Result<(), Errored> {
828 let no_run_rustfix =
829 comments.find_one_for_revision(revision, "`no-rustfix` annotations", |r| r.no_rustfix)?;
830
831 let global_rustfix = match mode {
832 Mode::Pass | Mode::Run { .. } | Mode::Panic => RustfixMode::Disabled,
833 Mode::Fail { rustfix, .. } | Mode::Yolo { rustfix } => rustfix,
834 };
835
836 let fixed_code = (no_run_rustfix.is_none() && global_rustfix.enabled())
837 .then_some(())
838 .and_then(|()| {
839 let suggestions = std::str::from_utf8(stderr)
840 .unwrap()
841 .lines()
842 .flat_map(|line| {
843 if !line.starts_with('{') {
844 return vec![];
845 }
846 rustfix::get_suggestions_from_json(
847 line,
848 &HashSet::new(),
849 if global_rustfix == RustfixMode::Everything {
850 rustfix::Filter::Everything
851 } else {
852 rustfix::Filter::MachineApplicableOnly
853 },
854 )
855 .unwrap_or_else(|err| {
856 panic!("could not deserialize diagnostics json for rustfix {err}:{line}")
857 })
858 })
859 .collect::<Vec<_>>();
860 if suggestions.is_empty() {
861 None
862 } else {
863 Some(rustfix::apply_suggestions(
864 &std::fs::read_to_string(path).unwrap(),
865 &suggestions,
866 ))
867 }
868 })
869 .transpose()
870 .map_err(|err| Errored {
871 command: Command::new(format!("rustfix {}", path.display())),
872 errors: vec![Error::Rustfix(err)],
873 stderr: stderr.into(),
874 stdout: stdout.into(),
875 })?;
876
877 let edition = comments.edition(revision, config)?;
878 let edition = edition
879 .map(|mwl| {
880 let line = mwl.span().unwrap_or(Span::INVALID);
881 Spanned::new(mwl.into_inner(), line)
882 })
883 .into();
884 let rustfix_comments = Comments {
885 revisions: None,
886 revisioned: std::iter::once((
887 vec![],
888 Revisioned {
889 span: Span::INVALID,
890 ignore: vec![],
891 only: vec![],
892 stderr_per_bitwidth: false,
893 compile_flags: comments
894 .for_revision(revision)
895 .flat_map(|r| r.compile_flags.iter().cloned())
896 .collect(),
897 env_vars: comments
898 .for_revision(revision)
899 .flat_map(|r| r.env_vars.iter().cloned())
900 .collect(),
901 normalize_stderr: vec![],
902 normalize_stdout: vec![],
903 error_in_other_files: vec![],
904 error_matches: vec![],
905 require_annotations_for_level: Default::default(),
906 aux_builds: comments
907 .for_revision(revision)
908 .flat_map(|r| r.aux_builds.iter().cloned())
909 .collect(),
910 edition,
911 mode: OptWithLine::new(Mode::Pass, Span::INVALID),
912 no_rustfix: OptWithLine::new((), Span::INVALID),
913 needs_asm_support: false,
914 },
915 ))
916 .collect(),
917 };
918
919 let run = fixed_code.is_some();
920 let mut errors = vec![];
921 let rustfix_path = check_output(
922 // Always check for `.fixed` files, even if there were reasons not to run rustfix.
923 // We don't want to leave around stray `.fixed` files
924 fixed_code.unwrap_or_default().as_bytes(),
925 path,
926 &mut errors,
927 "fixed",
928 &Filter::default(),
929 config,
930 &rustfix_comments,
931 revision,
932 );
933 if !errors.is_empty() {
934 return Err(Errored {
935 command: Command::new(format!("checking {}", path.display())),
936 errors,
937 stderr: vec![],
938 stdout: vec![],
939 });
940 }
941
942 if !run {
943 return Ok(());
944 }
945
946 let mut cmd = build_command(&rustfix_path, config, revision, &rustfix_comments)?;
947 cmd.args(extra_args);
948 // picking the crate name from the file name is problematic when `.revision_name` is inserted
949 cmd.arg("--crate-name").arg(
950 path.file_stem()
951 .unwrap()
952 .to_str()
953 .unwrap()
954 .replace('-', "_"),
955 );
956 let output = cmd.output().unwrap();
957 if output.status.success() {
958 Ok(())
959 } else {
960 Err(Errored {
961 command: cmd,
962 errors: vec![Error::Command {
963 kind: "rustfix".into(),
964 status: output.status,
965 }],
966 stderr: rustc_stderr::process(&rustfix_path, &output.stderr).rendered,
967 stdout: output.stdout,
968 })
969 }
970}
971
972fn revised(revision: &str, extension: &str) -> String {
973 if revision.is_empty() {
974 extension.to_string()
975 } else {
976 format!("{revision}.{extension}")
977 }
978}
979
980fn check_test_result(
981 command: Command,
982 mode: Mode,
983 path: &Path,
984 config: &Config,
985 revision: &str,
986 comments: &Comments,
987 status: ExitStatus,
988 stdout: &[u8],
989 stderr: &[u8],
990) -> Result<Command, Errored> {
991 let mut errors = vec![];
992 errors.extend(mode.ok(status).err());
993 // Always remove annotation comments from stderr.
994 let diagnostics = rustc_stderr::process(path, stderr);
995 check_test_output(
996 path,
997 &mut errors,
998 revision,
999 config,
1000 comments,
1001 stdout,
1002 &diagnostics.rendered,
1003 );
1004 // Check error annotations in the source against output
1005 check_annotations(
1006 diagnostics.messages,
1007 diagnostics.messages_from_unknown_file_or_line,
1008 path,
1009 &mut errors,
1010 config,
1011 revision,
1012 comments,
1013 )?;
1014 if errors.is_empty() {
1015 Ok(command)
1016 } else {
1017 Err(Errored {
1018 command,
1019 errors,
1020 stderr: diagnostics.rendered,
1021 stdout: stdout.into(),
1022 })
1023 }
1024}
1025
1026fn check_test_output(
1027 path: &Path,
1028 errors: &mut Vec<Error>,
1029 revision: &str,
1030 config: &Config,
1031 comments: &Comments,
1032 stdout: &[u8],
1033 stderr: &[u8],
1034) {
1035 // Check output files (if any)
1036 // Check output files against actual output
1037 check_output(
1038 output:stderr,
1039 path,
1040 errors,
1041 kind:"stderr",
1042 &config.stderr_filters,
1043 config,
1044 comments,
1045 revision,
1046 );
1047 check_output(
1048 output:stdout,
1049 path,
1050 errors,
1051 kind:"stdout",
1052 &config.stdout_filters,
1053 config,
1054 comments,
1055 revision,
1056 );
1057}
1058
1059fn check_annotations(
1060 mut messages: Vec<Vec<Message>>,
1061 mut messages_from_unknown_file_or_line: Vec<Message>,
1062 path: &Path,
1063 errors: &mut Errors,
1064 config: &Config,
1065 revision: &str,
1066 comments: &Comments,
1067) -> Result<(), Errored> {
1068 let error_patterns = comments
1069 .for_revision(revision)
1070 .flat_map(|r| r.error_in_other_files.iter());
1071
1072 let mut seen_error_match = None;
1073 for error_pattern in error_patterns {
1074 seen_error_match = Some(error_pattern.span());
1075 // first check the diagnostics messages outside of our file. We check this first, so that
1076 // you can mix in-file annotations with //@error-in-other-file annotations, even if there is overlap
1077 // in the messages.
1078 if let Some(i) = messages_from_unknown_file_or_line
1079 .iter()
1080 .position(|msg| error_pattern.matches(&msg.message))
1081 {
1082 messages_from_unknown_file_or_line.remove(i);
1083 } else {
1084 errors.push(Error::PatternNotFound {
1085 pattern: error_pattern.clone(),
1086 expected_line: None,
1087 });
1088 }
1089 }
1090
1091 // The order on `Level` is such that `Error` is the highest level.
1092 // We will ensure that *all* diagnostics of level at least `lowest_annotation_level`
1093 // are matched.
1094 let mut lowest_annotation_level = Level::Error;
1095 for &ErrorMatch {
1096 ref pattern,
1097 level,
1098 line,
1099 } in comments
1100 .for_revision(revision)
1101 .flat_map(|r| r.error_matches.iter())
1102 {
1103 seen_error_match = Some(pattern.span());
1104 // If we found a diagnostic with a level annotation, make sure that all
1105 // diagnostics of that level have annotations, even if we don't end up finding a matching diagnostic
1106 // for this pattern.
1107 if lowest_annotation_level > level {
1108 lowest_annotation_level = level;
1109 }
1110
1111 if let Some(msgs) = messages.get_mut(line.get()) {
1112 let found = msgs
1113 .iter()
1114 .position(|msg| pattern.matches(&msg.message) && msg.level == level);
1115 if let Some(found) = found {
1116 msgs.remove(found);
1117 continue;
1118 }
1119 }
1120
1121 errors.push(Error::PatternNotFound {
1122 pattern: pattern.clone(),
1123 expected_line: Some(line),
1124 });
1125 }
1126
1127 let required_annotation_level = comments.find_one_for_revision(
1128 revision,
1129 "`require_annotations_for_level` annotations",
1130 |r| r.require_annotations_for_level,
1131 )?;
1132
1133 let required_annotation_level =
1134 required_annotation_level.map_or(lowest_annotation_level, |l| *l);
1135 let filter = |mut msgs: Vec<Message>| -> Vec<_> {
1136 msgs.retain(|msg| msg.level >= required_annotation_level);
1137 msgs
1138 };
1139
1140 let mode = config.mode.maybe_override(comments, revision)?;
1141
1142 if !matches!(config.mode, Mode::Yolo { .. }) {
1143 let messages_from_unknown_file_or_line = filter(messages_from_unknown_file_or_line);
1144 if !messages_from_unknown_file_or_line.is_empty() {
1145 errors.push(Error::ErrorsWithoutPattern {
1146 path: None,
1147 msgs: messages_from_unknown_file_or_line,
1148 });
1149 }
1150
1151 for (line, msgs) in messages.into_iter().enumerate() {
1152 let msgs = filter(msgs);
1153 if !msgs.is_empty() {
1154 let line = NonZeroUsize::new(line).expect("line 0 is always empty");
1155 errors.push(Error::ErrorsWithoutPattern {
1156 path: Some(Spanned::new(
1157 path.to_path_buf(),
1158 Span {
1159 line_start: line,
1160 ..Span::INVALID
1161 },
1162 )),
1163 msgs,
1164 });
1165 }
1166 }
1167 }
1168
1169 match (*mode, seen_error_match) {
1170 (Mode::Pass, Some(span)) | (Mode::Panic, Some(span)) => {
1171 errors.push(Error::PatternFoundInPassTest {
1172 mode: mode.span(),
1173 span,
1174 })
1175 }
1176 (
1177 Mode::Fail {
1178 require_patterns: true,
1179 ..
1180 },
1181 None,
1182 ) => errors.push(Error::NoPatternsFound),
1183 _ => {}
1184 }
1185 Ok(())
1186}
1187
1188fn check_output(
1189 output: &[u8],
1190 path: &Path,
1191 errors: &mut Errors,
1192 kind: &'static str,
1193 filters: &Filter,
1194 config: &Config,
1195 comments: &Comments,
1196 revision: &str,
1197) -> PathBuf {
1198 let target = config.target.as_ref().unwrap();
1199 let output = normalize(path, output, filters, comments, revision, kind);
1200 let path = output_path(path, comments, revised(revision, kind), target, revision);
1201 match &config.output_conflict_handling {
1202 OutputConflictHandling::Error(bless_command) => {
1203 let expected_output = std::fs::read(&path).unwrap_or_default();
1204 if output != expected_output {
1205 errors.push(Error::OutputDiffers {
1206 path: path.clone(),
1207 actual: output.clone(),
1208 expected: expected_output,
1209 bless_command: bless_command.clone(),
1210 });
1211 }
1212 }
1213 OutputConflictHandling::Bless => {
1214 if output.is_empty() {
1215 let _ = std::fs::remove_file(&path);
1216 } else {
1217 std::fs::write(&path, &output).unwrap();
1218 }
1219 }
1220 OutputConflictHandling::Ignore => {}
1221 }
1222 path
1223}
1224
1225fn output_path(
1226 path: &Path,
1227 comments: &Comments,
1228 kind: String,
1229 target: &str,
1230 revision: &str,
1231) -> PathBuf {
1232 if commentsimpl Iterator
1233 .for_revision(revision)
1234 .any(|r: &Revisioned| r.stderr_per_bitwidth)
1235 {
1236 return path.with_extension(format!("{}bit.{kind}", get_pointer_width(target)));
1237 }
1238 path.with_extension(kind)
1239}
1240
1241fn test_condition(condition: &Condition, config: &Config) -> bool {
1242 let target: &String = config.target.as_ref().unwrap();
1243 match condition {
1244 Condition::Bitwidth(bits: &u8) => get_pointer_width(triple:target) == *bits,
1245 Condition::Target(t: &String) => target.contains(t),
1246 Condition::Host(t: &String) => config.host.as_ref().unwrap().contains(t),
1247 Condition::OnHost => target == config.host.as_ref().unwrap(),
1248 }
1249}
1250
1251impl dyn TestStatus {
1252 /// Returns whether according to the in-file conditions, this file should be run.
1253 fn test_file_conditions(&self, comments: &Comments, config: &Config) -> bool {
1254 let revision: &str = self.revision();
1255 if commentsimpl Iterator
1256 .for_revision(revision)
1257 .flat_map(|r: &Revisioned| r.ignore.iter())
1258 .any(|c: &Condition| test_condition(condition:c, config))
1259 {
1260 return config.run_only_ignored;
1261 }
1262 if commentsimpl Iterator
1263 .for_revision(revision)
1264 .any(|r: &Revisioned| r.needs_asm_support && !config.has_asm_support())
1265 {
1266 return config.run_only_ignored;
1267 }
1268 commentsimpl Iterator
1269 .for_revision(revision)
1270 .flat_map(|r: &Revisioned| r.only.iter())
1271 .all(|c: &Condition| test_condition(condition:c, config))
1272 ^ config.run_only_ignored
1273 }
1274}
1275
1276// Taken 1:1 from compiletest-rs
1277fn get_pointer_width(triple: &str) -> u8 {
1278 if (triple.contains("64") && !triple.ends_with("gnux32") && !triple.ends_with("gnu_ilp32"))
1279 || triple.starts_with("s390x")
1280 {
1281 64
1282 } else if triple.starts_with("avr") {
1283 16
1284 } else {
1285 32
1286 }
1287}
1288
1289fn normalize(
1290 path: &Path,
1291 text: &[u8],
1292 filters: &Filter,
1293 comments: &Comments,
1294 revision: &str,
1295 kind: &'static str,
1296) -> Vec<u8> {
1297 // Useless paths
1298 let path_filter: (Match, &[u8]) = (Match::from(path.parent().unwrap()), b"$DIR" as &[u8]);
1299 let filters: impl Iterator = filters.iter().chain(std::iter::once(&path_filter));
1300 let mut text: Vec = text.to_owned();
1301 if let Some(lib_path: &str) = option_env!("RUSTC_LIB_PATH") {
1302 text = text.replace(needle:lib_path, replacement:"RUSTLIB");
1303 }
1304
1305 for (rule: &Match, replacement: &&[u8]) in filters {
1306 text = rule.replace_all(&text, replacement).into_owned();
1307 }
1308
1309 for (from: &Regex, to: &Vec) in comments.for_revision(revision).flat_map(|r: &Revisioned| match kind {
1310 "fixed" => &[] as &[_],
1311 "stderr" => &r.normalize_stderr,
1312 "stdout" => &r.normalize_stdout,
1313 _ => unreachable!(),
1314 }) {
1315 text = from.replace_all(&text, rep:to).into_owned();
1316 }
1317 text
1318}
1319/// Remove the common prefix of this path and the `root_dir`.
1320fn strip_path_prefix<'a>(path: &'a Path, prefix: &Path) -> impl Iterator<Item = Component<'a>> {
1321 let mut components: Components<'_> = path.components();
1322 for c: Component<'_> in prefix.components() {
1323 // Windows has some funky paths. This is probably wrong, but works well in practice.
1324 let deverbatimize: impl Fn(Component<'_>) -> … = |c: Component<'_>| match c {
1325 Component::Prefix(prefix: PrefixComponent<'_>) => Err(match prefix.kind() {
1326 Prefix::VerbatimUNC(a: &OsStr, b: &OsStr) => Prefix::UNC(a, b),
1327 Prefix::VerbatimDisk(d: u8) => Prefix::Disk(d),
1328 other: Prefix<'_> => other,
1329 }),
1330 c: Component<'_> => Ok(c),
1331 };
1332 let c2: Option> = components.next();
1333 if Some(deverbatimize(c)) == c2.map(deverbatimize) {
1334 continue;
1335 }
1336 return c2.into_iter().chain(components);
1337 }
1338 None.into_iter().chain(components)
1339}
1340