compiletest/
lib.rs

1#![crate_name = "compiletest"]
2
3#[cfg(test)]
4mod tests;
5
6pub mod cli;
7mod common;
8mod debuggers;
9mod diagnostics;
10mod directives;
11mod edition;
12mod errors;
13mod executor;
14mod json;
15mod output_capture;
16mod panic_hook;
17mod raise_fd_limit;
18mod read2;
19mod runtest;
20pub mod rustdoc_gui_test;
21mod util;
22
23use core::panic;
24use std::collections::HashSet;
25use std::fmt::Write;
26use std::io::{self, ErrorKind};
27use std::process::{Command, Stdio};
28use std::sync::{Arc, OnceLock};
29use std::time::SystemTime;
30use std::{env, fs, vec};
31
32use build_helper::git::{get_git_modified_files, get_git_untracked_files};
33use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
34use getopts::Options;
35use rayon::iter::{ParallelBridge, ParallelIterator};
36use tracing::debug;
37use walkdir::WalkDir;
38
39use self::directives::{EarlyProps, make_test_description};
40use crate::common::{
41    CodegenBackend, CompareMode, Config, Debugger, PassMode, TestMode, TestPaths, UI_EXTENSIONS,
42    expected_output_path, output_base_dir, output_relative_path,
43};
44use crate::directives::{AuxProps, DirectivesCache, FileDirectives};
45use crate::edition::parse_edition;
46use crate::executor::{CollectedTest, ColorConfig};
47
48/// Creates the `Config` instance for this invocation of compiletest.
49///
50/// The config mostly reflects command-line arguments, but there might also be
51/// some code here that inspects environment variables or even runs executables
52/// (e.g. when discovering debugger versions).
53fn parse_config(args: Vec<String>) -> Config {
54    let mut opts = Options::new();
55    opts.reqopt("", "compile-lib-path", "path to host shared libraries", "PATH")
56        .reqopt("", "run-lib-path", "path to target shared libraries", "PATH")
57        .reqopt("", "rustc-path", "path to rustc to use for compiling", "PATH")
58        .optopt("", "cargo-path", "path to cargo to use for compiling", "PATH")
59        .optopt(
60            "",
61            "stage0-rustc-path",
62            "path to rustc to use for compiling run-make recipes",
63            "PATH",
64        )
65        .optopt(
66            "",
67            "query-rustc-path",
68            "path to rustc to use for querying target information (defaults to `--rustc-path`)",
69            "PATH",
70        )
71        .optopt("", "rustdoc-path", "path to rustdoc to use for compiling", "PATH")
72        .optopt("", "coverage-dump-path", "path to coverage-dump to use in tests", "PATH")
73        .reqopt("", "python", "path to python to use for doc tests", "PATH")
74        .optopt("", "jsondocck-path", "path to jsondocck to use for doc tests", "PATH")
75        .optopt("", "jsondoclint-path", "path to jsondoclint to use for doc tests", "PATH")
76        .optopt("", "run-clang-based-tests-with", "path to Clang executable", "PATH")
77        .optopt("", "llvm-filecheck", "path to LLVM's FileCheck binary", "DIR")
78        .reqopt("", "src-root", "directory containing sources", "PATH")
79        .reqopt("", "src-test-suite-root", "directory containing test suite sources", "PATH")
80        .reqopt("", "build-root", "path to root build directory", "PATH")
81        .reqopt("", "build-test-suite-root", "path to test suite specific build directory", "PATH")
82        .reqopt("", "sysroot-base", "directory containing the compiler sysroot", "PATH")
83        .reqopt("", "stage", "stage number under test", "N")
84        .reqopt("", "stage-id", "the target-stage identifier", "stageN-TARGET")
85        .reqopt(
86            "",
87            "mode",
88            "which sort of compile tests to run",
89            "pretty | debug-info | codegen | rustdoc \
90            | rustdoc-json | codegen-units | incremental | run-make | ui \
91            | rustdoc-js | mir-opt | assembly | crashes",
92        )
93        .reqopt(
94            "",
95            "suite",
96            "which suite of compile tests to run. used for nicer error reporting.",
97            "SUITE",
98        )
99        .optopt(
100            "",
101            "pass",
102            "force {check,build,run}-pass tests to this mode.",
103            "check | build | run",
104        )
105        .optopt("", "run", "whether to execute run-* tests", "auto | always | never")
106        .optflag("", "ignored", "run tests marked as ignored")
107        .optflag("", "has-enzyme", "run tests that require enzyme")
108        .optflag("", "with-rustc-debug-assertions", "whether rustc was built with debug assertions")
109        .optflag("", "with-std-debug-assertions", "whether std was built with debug assertions")
110        .optmulti(
111            "",
112            "skip",
113            "skip tests matching SUBSTRING. Can be passed multiple times",
114            "SUBSTRING",
115        )
116        .optflag("", "exact", "filters match exactly")
117        .optopt(
118            "",
119            "runner",
120            "supervisor program to run tests under \
121             (eg. emulator, valgrind)",
122            "PROGRAM",
123        )
124        .optmulti("", "host-rustcflags", "flags to pass to rustc for host", "FLAGS")
125        .optmulti("", "target-rustcflags", "flags to pass to rustc for target", "FLAGS")
126        .optflag(
127            "",
128            "rust-randomized-layout",
129            "set this when rustc/stdlib were compiled with randomized layouts",
130        )
131        .optflag("", "optimize-tests", "run tests with optimizations enabled")
132        .optflag("", "verbose", "run tests verbosely, showing all output")
133        .optflag(
134            "",
135            "bless",
136            "overwrite stderr/stdout files instead of complaining about a mismatch",
137        )
138        .optflag("", "fail-fast", "stop as soon as possible after any test fails")
139        .optopt("", "color", "coloring: auto, always, never", "WHEN")
140        .optopt("", "target", "the target to build for", "TARGET")
141        .optopt("", "host", "the host to build for", "HOST")
142        .optopt("", "cdb", "path to CDB to use for CDB debuginfo tests", "PATH")
143        .optopt("", "gdb", "path to GDB to use for GDB debuginfo tests", "PATH")
144        .optopt("", "lldb-version", "the version of LLDB used", "VERSION STRING")
145        .optopt("", "llvm-version", "the version of LLVM used", "VERSION STRING")
146        .optflag("", "system-llvm", "is LLVM the system LLVM")
147        .optopt("", "android-cross-path", "Android NDK standalone path", "PATH")
148        .optopt("", "adb-path", "path to the android debugger", "PATH")
149        .optopt("", "adb-test-dir", "path to tests for the android debugger", "PATH")
150        .optopt("", "lldb-python-dir", "directory containing LLDB's python module", "PATH")
151        .reqopt("", "cc", "path to a C compiler", "PATH")
152        .reqopt("", "cxx", "path to a C++ compiler", "PATH")
153        .reqopt("", "cflags", "flags for the C compiler", "FLAGS")
154        .reqopt("", "cxxflags", "flags for the CXX compiler", "FLAGS")
155        .optopt("", "ar", "path to an archiver", "PATH")
156        .optopt("", "target-linker", "path to a linker for the target", "PATH")
157        .optopt("", "host-linker", "path to a linker for the host", "PATH")
158        .reqopt("", "llvm-components", "list of LLVM components built in", "LIST")
159        .optopt("", "llvm-bin-dir", "Path to LLVM's `bin` directory", "PATH")
160        .optopt("", "nodejs", "the name of nodejs", "PATH")
161        .optopt("", "npm", "the name of npm", "PATH")
162        .optopt("", "remote-test-client", "path to the remote test client", "PATH")
163        .optopt(
164            "",
165            "compare-mode",
166            "mode describing what file the actual ui output will be compared to",
167            "COMPARE MODE",
168        )
169        .optflag(
170            "",
171            "rustfix-coverage",
172            "enable this to generate a Rustfix coverage file, which is saved in \
173            `./<build_test_suite_root>/rustfix_missing_coverage.txt`",
174        )
175        .optflag("", "force-rerun", "rerun tests even if the inputs are unchanged")
176        .optflag("", "only-modified", "only run tests that result been modified")
177        // FIXME: Temporarily retained so we can point users to `--no-capture`
178        .optflag("", "nocapture", "")
179        .optflag("", "no-capture", "don't capture stdout/stderr of tests")
180        .optflag("", "profiler-runtime", "is the profiler runtime enabled for this target")
181        .optflag("h", "help", "show this message")
182        .reqopt("", "channel", "current Rust channel", "CHANNEL")
183        .optflag(
184            "",
185            "git-hash",
186            "run tests which rely on commit version being compiled into the binaries",
187        )
188        .optopt("", "edition", "default Rust edition", "EDITION")
189        .reqopt("", "nightly-branch", "name of the git branch for nightly", "BRANCH")
190        .reqopt(
191            "",
192            "git-merge-commit-email",
193            "email address used for finding merge commits",
194            "EMAIL",
195        )
196        .optopt(
197            "",
198            "compiletest-diff-tool",
199            "What custom diff tool to use for displaying compiletest tests.",
200            "COMMAND",
201        )
202        .reqopt("", "minicore-path", "path to minicore aux library", "PATH")
203        .optopt(
204            "",
205            "debugger",
206            "only test a specific debugger in debuginfo tests",
207            "gdb | lldb | cdb",
208        )
209        .optopt(
210            "",
211            "default-codegen-backend",
212            "the codegen backend currently used",
213            "CODEGEN BACKEND NAME",
214        )
215        .optopt(
216            "",
217            "override-codegen-backend",
218            "the codegen backend to use instead of the default one",
219            "CODEGEN BACKEND [NAME | PATH]",
220        )
221        .optflag("", "bypass-ignore-backends", "ignore `//@ ignore-backends` directives");
222
223    let (argv0, args_) = args.split_first().unwrap();
224    if args.len() == 1 || args[1] == "-h" || args[1] == "--help" {
225        let message = format!("Usage: {} [OPTIONS] [TESTNAME...]", argv0);
226        println!("{}", opts.usage(&message));
227        println!();
228        panic!()
229    }
230
231    let matches = &match opts.parse(args_) {
232        Ok(m) => m,
233        Err(f) => panic!("{:?}", f),
234    };
235
236    if matches.opt_present("h") || matches.opt_present("help") {
237        let message = format!("Usage: {} [OPTIONS]  [TESTNAME...]", argv0);
238        println!("{}", opts.usage(&message));
239        println!();
240        panic!()
241    }
242
243    fn make_absolute(path: Utf8PathBuf) -> Utf8PathBuf {
244        if path.is_relative() {
245            Utf8PathBuf::try_from(env::current_dir().unwrap()).unwrap().join(path)
246        } else {
247            path
248        }
249    }
250
251    fn opt_path(m: &getopts::Matches, nm: &str) -> Utf8PathBuf {
252        match m.opt_str(nm) {
253            Some(s) => Utf8PathBuf::from(&s),
254            None => panic!("no option (=path) found for {}", nm),
255        }
256    }
257
258    let target = opt_str2(matches.opt_str("target"));
259    let android_cross_path = opt_path(matches, "android-cross-path");
260    // FIXME: `cdb_version` is *derived* from cdb, but it's *not* technically a config!
261    let cdb = debuggers::discover_cdb(matches.opt_str("cdb"), &target);
262    let cdb_version = cdb.as_deref().and_then(debuggers::query_cdb_version);
263    // FIXME: `gdb_version` is *derived* from gdb, but it's *not* technically a config!
264    let gdb = debuggers::discover_gdb(matches.opt_str("gdb"), &target, &android_cross_path);
265    let gdb_version = gdb.as_deref().and_then(debuggers::query_gdb_version);
266    // FIXME: `lldb_version` is *derived* from lldb, but it's *not* technically a config!
267    let lldb_version =
268        matches.opt_str("lldb-version").as_deref().and_then(debuggers::extract_lldb_version);
269    let color = match matches.opt_str("color").as_deref() {
270        Some("auto") | None => ColorConfig::AutoColor,
271        Some("always") => ColorConfig::AlwaysColor,
272        Some("never") => ColorConfig::NeverColor,
273        Some(x) => panic!("argument for --color must be auto, always, or never, but found `{}`", x),
274    };
275    // FIXME: this is very questionable, we really should be obtaining LLVM version info from
276    // `bootstrap`, and not trying to be figuring out that in `compiletest` by running the
277    // `FileCheck` binary.
278    let llvm_version =
279        matches.opt_str("llvm-version").as_deref().map(directives::extract_llvm_version).or_else(
280            || directives::extract_llvm_version_from_binary(&matches.opt_str("llvm-filecheck")?),
281        );
282
283    let default_codegen_backend = match matches.opt_str("default-codegen-backend").as_deref() {
284        Some(backend) => match CodegenBackend::try_from(backend) {
285            Ok(backend) => backend,
286            Err(error) => {
287                panic!("invalid value `{backend}` for `--defalt-codegen-backend`: {error}")
288            }
289        },
290        // By default, it's always llvm.
291        None => CodegenBackend::Llvm,
292    };
293    let override_codegen_backend = matches.opt_str("override-codegen-backend");
294
295    let run_ignored = matches.opt_present("ignored");
296    let with_rustc_debug_assertions = matches.opt_present("with-rustc-debug-assertions");
297    let with_std_debug_assertions = matches.opt_present("with-std-debug-assertions");
298    let mode = matches.opt_str("mode").unwrap().parse().expect("invalid mode");
299    let has_html_tidy = if mode == TestMode::Rustdoc {
300        Command::new("tidy")
301            .arg("--version")
302            .stdout(Stdio::null())
303            .status()
304            .map_or(false, |status| status.success())
305    } else {
306        // Avoid spawning an external command when we know html-tidy won't be used.
307        false
308    };
309    let has_enzyme = matches.opt_present("has-enzyme");
310    let filters = if mode == TestMode::RunMake {
311        matches
312            .free
313            .iter()
314            .map(|f| {
315                // Here `f` is relative to `./tests/run-make`. So if you run
316                //
317                //   ./x test tests/run-make/crate-loading
318                //
319                //  then `f` is "crate-loading".
320                let path = Utf8Path::new(f);
321                let mut iter = path.iter().skip(1);
322
323                if iter.next().is_some_and(|s| s == "rmake.rs") && iter.next().is_none() {
324                    // Strip the "rmake.rs" suffix. For example, if `f` is
325                    // "crate-loading/rmake.rs" then this gives us "crate-loading".
326                    path.parent().unwrap().to_string()
327                } else {
328                    f.to_string()
329                }
330            })
331            .collect::<Vec<_>>()
332    } else {
333        // Note that the filters are relative to the root dir of the different test
334        // suites. For example, with:
335        //
336        //   ./x test tests/ui/lint/unused
337        //
338        // the filter is "lint/unused".
339        matches.free.clone()
340    };
341    let compare_mode = matches.opt_str("compare-mode").map(|s| {
342        s.parse().unwrap_or_else(|_| {
343            let variants: Vec<_> = CompareMode::STR_VARIANTS.iter().copied().collect();
344            panic!(
345                "`{s}` is not a valid value for `--compare-mode`, it should be one of: {}",
346                variants.join(", ")
347            );
348        })
349    });
350    if matches.opt_present("nocapture") {
351        panic!("`--nocapture` is deprecated; please use `--no-capture`");
352    }
353
354    let stage = match matches.opt_str("stage") {
355        Some(stage) => stage.parse::<u32>().expect("expected `--stage` to be an unsigned integer"),
356        None => panic!("`--stage` is required"),
357    };
358
359    let src_root = opt_path(matches, "src-root");
360    let src_test_suite_root = opt_path(matches, "src-test-suite-root");
361    assert!(
362        src_test_suite_root.starts_with(&src_root),
363        "`src-root` must be a parent of `src-test-suite-root`: `src-root`=`{}`, `src-test-suite-root` = `{}`",
364        src_root,
365        src_test_suite_root
366    );
367
368    let build_root = opt_path(matches, "build-root");
369    let build_test_suite_root = opt_path(matches, "build-test-suite-root");
370    assert!(build_test_suite_root.starts_with(&build_root));
371
372    Config {
373        bless: matches.opt_present("bless"),
374        fail_fast: matches.opt_present("fail-fast")
375            || env::var_os("RUSTC_TEST_FAIL_FAST").is_some(),
376
377        compile_lib_path: make_absolute(opt_path(matches, "compile-lib-path")),
378        run_lib_path: make_absolute(opt_path(matches, "run-lib-path")),
379        rustc_path: opt_path(matches, "rustc-path"),
380        cargo_path: matches.opt_str("cargo-path").map(Utf8PathBuf::from),
381        stage0_rustc_path: matches.opt_str("stage0-rustc-path").map(Utf8PathBuf::from),
382        query_rustc_path: matches.opt_str("query-rustc-path").map(Utf8PathBuf::from),
383        rustdoc_path: matches.opt_str("rustdoc-path").map(Utf8PathBuf::from),
384        coverage_dump_path: matches.opt_str("coverage-dump-path").map(Utf8PathBuf::from),
385        python: matches.opt_str("python").unwrap(),
386        jsondocck_path: matches.opt_str("jsondocck-path"),
387        jsondoclint_path: matches.opt_str("jsondoclint-path"),
388        run_clang_based_tests_with: matches.opt_str("run-clang-based-tests-with"),
389        llvm_filecheck: matches.opt_str("llvm-filecheck").map(Utf8PathBuf::from),
390        llvm_bin_dir: matches.opt_str("llvm-bin-dir").map(Utf8PathBuf::from),
391
392        src_root,
393        src_test_suite_root,
394
395        build_root,
396        build_test_suite_root,
397
398        sysroot_base: opt_path(matches, "sysroot-base"),
399
400        stage,
401        stage_id: matches.opt_str("stage-id").unwrap(),
402
403        mode,
404        suite: matches.opt_str("suite").unwrap().parse().expect("invalid suite"),
405        debugger: matches.opt_str("debugger").map(|debugger| {
406            debugger
407                .parse::<Debugger>()
408                .unwrap_or_else(|_| panic!("unknown `--debugger` option `{debugger}` given"))
409        }),
410        run_ignored,
411        with_rustc_debug_assertions,
412        with_std_debug_assertions,
413        filters,
414        skip: matches.opt_strs("skip"),
415        filter_exact: matches.opt_present("exact"),
416        force_pass_mode: matches.opt_str("pass").map(|mode| {
417            mode.parse::<PassMode>()
418                .unwrap_or_else(|_| panic!("unknown `--pass` option `{}` given", mode))
419        }),
420        // FIXME: this run scheme is... confusing.
421        run: matches.opt_str("run").and_then(|mode| match mode.as_str() {
422            "auto" => None,
423            "always" => Some(true),
424            "never" => Some(false),
425            _ => panic!("unknown `--run` option `{}` given", mode),
426        }),
427        runner: matches.opt_str("runner"),
428        host_rustcflags: matches.opt_strs("host-rustcflags"),
429        target_rustcflags: matches.opt_strs("target-rustcflags"),
430        optimize_tests: matches.opt_present("optimize-tests"),
431        rust_randomized_layout: matches.opt_present("rust-randomized-layout"),
432        target,
433        host: opt_str2(matches.opt_str("host")),
434        cdb,
435        cdb_version,
436        gdb,
437        gdb_version,
438        lldb_version,
439        llvm_version,
440        system_llvm: matches.opt_present("system-llvm"),
441        android_cross_path,
442        adb_path: opt_str2(matches.opt_str("adb-path")),
443        adb_test_dir: opt_str2(matches.opt_str("adb-test-dir")),
444        adb_device_status: opt_str2(matches.opt_str("target")).contains("android")
445            && "(none)" != opt_str2(matches.opt_str("adb-test-dir"))
446            && !opt_str2(matches.opt_str("adb-test-dir")).is_empty(),
447        lldb_python_dir: matches.opt_str("lldb-python-dir"),
448        verbose: matches.opt_present("verbose"),
449        only_modified: matches.opt_present("only-modified"),
450        color,
451        remote_test_client: matches.opt_str("remote-test-client").map(Utf8PathBuf::from),
452        compare_mode,
453        rustfix_coverage: matches.opt_present("rustfix-coverage"),
454        has_html_tidy,
455        has_enzyme,
456        channel: matches.opt_str("channel").unwrap(),
457        git_hash: matches.opt_present("git-hash"),
458        edition: matches.opt_str("edition").as_deref().map(parse_edition),
459
460        cc: matches.opt_str("cc").unwrap(),
461        cxx: matches.opt_str("cxx").unwrap(),
462        cflags: matches.opt_str("cflags").unwrap(),
463        cxxflags: matches.opt_str("cxxflags").unwrap(),
464        ar: matches.opt_str("ar").unwrap_or_else(|| String::from("ar")),
465        target_linker: matches.opt_str("target-linker"),
466        host_linker: matches.opt_str("host-linker"),
467        llvm_components: matches.opt_str("llvm-components").unwrap(),
468        nodejs: matches.opt_str("nodejs"),
469
470        force_rerun: matches.opt_present("force-rerun"),
471
472        target_cfgs: OnceLock::new(),
473        builtin_cfg_names: OnceLock::new(),
474        supported_crate_types: OnceLock::new(),
475
476        nocapture: matches.opt_present("no-capture"),
477
478        nightly_branch: matches.opt_str("nightly-branch").unwrap(),
479        git_merge_commit_email: matches.opt_str("git-merge-commit-email").unwrap(),
480
481        profiler_runtime: matches.opt_present("profiler-runtime"),
482
483        diff_command: matches.opt_str("compiletest-diff-tool"),
484
485        minicore_path: opt_path(matches, "minicore-path"),
486
487        default_codegen_backend,
488        override_codegen_backend,
489        bypass_ignore_backends: matches.opt_present("bypass-ignore-backends"),
490    }
491}
492
493fn opt_str2(maybestr: Option<String>) -> String {
494    match maybestr {
495        None => "(none)".to_owned(),
496        Some(s) => s,
497    }
498}
499
500/// Called by `main` after the config has been parsed.
501fn run_tests(config: Arc<Config>) {
502    debug!(?config, "run_tests");
503
504    panic_hook::install_panic_hook();
505
506    // If we want to collect rustfix coverage information,
507    // we first make sure that the coverage file does not exist.
508    // It will be created later on.
509    if config.rustfix_coverage {
510        let mut coverage_file_path = config.build_test_suite_root.clone();
511        coverage_file_path.push("rustfix_missing_coverage.txt");
512        if coverage_file_path.exists() {
513            if let Err(e) = fs::remove_file(&coverage_file_path) {
514                panic!("Could not delete {} due to {}", coverage_file_path, e)
515            }
516        }
517    }
518
519    // sadly osx needs some file descriptor limits raised for running tests in
520    // parallel (especially when we have lots and lots of child processes).
521    // For context, see #8904
522    unsafe {
523        raise_fd_limit::raise_fd_limit();
524    }
525    // Prevent issue #21352 UAC blocking .exe containing 'patch' etc. on Windows
526    // If #11207 is resolved (adding manifest to .exe) this becomes unnecessary
527    //
528    // SAFETY: at this point we're still single-threaded.
529    unsafe { env::set_var("__COMPAT_LAYER", "RunAsInvoker") };
530
531    let mut configs = Vec::new();
532    if let TestMode::DebugInfo = config.mode {
533        // Debugging emscripten code doesn't make sense today
534        if !config.target.contains("emscripten") {
535            match config.debugger {
536                Some(Debugger::Cdb) => configs.extend(debuggers::configure_cdb(&config)),
537                Some(Debugger::Gdb) => configs.extend(debuggers::configure_gdb(&config)),
538                Some(Debugger::Lldb) => configs.extend(debuggers::configure_lldb(&config)),
539                // FIXME: the *implicit* debugger discovery makes it really difficult to control
540                // which {`cdb`, `gdb`, `lldb`} are used. These should **not** be implicitly
541                // discovered by `compiletest`; these should be explicit `bootstrap` configuration
542                // options that are passed to `compiletest`!
543                None => {
544                    configs.extend(debuggers::configure_cdb(&config));
545                    configs.extend(debuggers::configure_gdb(&config));
546                    configs.extend(debuggers::configure_lldb(&config));
547                }
548            }
549        }
550    } else {
551        configs.push(config.clone());
552    };
553
554    // Discover all of the tests in the test suite directory, and build a `CollectedTest`
555    // structure for each test (or each revision of a multi-revision test).
556    let mut tests = Vec::new();
557    for c in configs {
558        tests.extend(collect_and_make_tests(c));
559    }
560
561    tests.sort_by(|a, b| Ord::cmp(&a.desc.name, &b.desc.name));
562
563    // Delegate to the executor to filter and run the big list of test structures
564    // created during test discovery. When the executor decides to run a test,
565    // it will return control to the rest of compiletest by calling `runtest::run`.
566    let ok = executor::run_tests(&config, tests);
567
568    // Check the outcome reported by the executor.
569    if !ok {
570        // We want to report that the tests failed, but we also want to give
571        // some indication of just what tests we were running. Especially on
572        // CI, where there can be cross-compiled tests for a lot of
573        // architectures, without this critical information it can be quite
574        // easy to miss which tests failed, and as such fail to reproduce
575        // the failure locally.
576
577        let mut msg = String::from("Some tests failed in compiletest");
578        write!(msg, " suite={}", config.suite).unwrap();
579
580        if let Some(compare_mode) = config.compare_mode.as_ref() {
581            write!(msg, " compare_mode={}", compare_mode).unwrap();
582        }
583
584        if let Some(pass_mode) = config.force_pass_mode.as_ref() {
585            write!(msg, " pass_mode={}", pass_mode).unwrap();
586        }
587
588        write!(msg, " mode={}", config.mode).unwrap();
589        write!(msg, " host={}", config.host).unwrap();
590        write!(msg, " target={}", config.target).unwrap();
591
592        println!("{msg}");
593
594        std::process::exit(1);
595    }
596}
597
598/// Read-only context data used during test collection.
599struct TestCollectorCx {
600    config: Arc<Config>,
601    cache: DirectivesCache,
602    common_inputs_stamp: Stamp,
603    modified_tests: Vec<Utf8PathBuf>,
604}
605
606/// Mutable state used during test collection.
607struct TestCollector {
608    tests: Vec<CollectedTest>,
609    found_path_stems: HashSet<Utf8PathBuf>,
610    poisoned: bool,
611}
612
613impl TestCollector {
614    fn new() -> Self {
615        TestCollector { tests: vec![], found_path_stems: HashSet::new(), poisoned: false }
616    }
617
618    fn merge(&mut self, mut other: Self) {
619        self.tests.append(&mut other.tests);
620        self.found_path_stems.extend(other.found_path_stems);
621        self.poisoned |= other.poisoned;
622    }
623}
624
625/// Creates test structures for every test/revision in the test suite directory.
626///
627/// This always inspects _all_ test files in the suite (e.g. all 17k+ ui tests),
628/// regardless of whether any filters/tests were specified on the command-line,
629/// because filtering is handled later by code that was copied from libtest.
630///
631/// FIXME(Zalathar): Now that we no longer rely on libtest, try to overhaul
632/// test discovery to take into account the filters/tests specified on the
633/// command-line, instead of having to enumerate everything.
634fn collect_and_make_tests(config: Arc<Config>) -> Vec<CollectedTest> {
635    debug!("making tests from {}", config.src_test_suite_root);
636    let common_inputs_stamp = common_inputs_stamp(&config);
637    let modified_tests =
638        modified_tests(&config, &config.src_test_suite_root).unwrap_or_else(|err| {
639            fatal!("modified_tests: {}: {err}", config.src_test_suite_root);
640        });
641    let cache = DirectivesCache::load(&config);
642
643    let cx = TestCollectorCx { config, cache, common_inputs_stamp, modified_tests };
644    let collector = collect_tests_from_dir(&cx, &cx.config.src_test_suite_root, Utf8Path::new(""))
645        .unwrap_or_else(|reason| {
646            panic!("Could not read tests from {}: {reason}", cx.config.src_test_suite_root)
647        });
648
649    let TestCollector { tests, found_path_stems, poisoned } = collector;
650
651    if poisoned {
652        eprintln!();
653        panic!("there are errors in tests");
654    }
655
656    check_for_overlapping_test_paths(&found_path_stems);
657
658    tests
659}
660
661/// Returns the most recent last-modified timestamp from among the input files
662/// that are considered relevant to all tests (e.g. the compiler, std, and
663/// compiletest itself).
664///
665/// (Some of these inputs aren't actually relevant to _all_ tests, but they are
666/// common to some subset of tests, and are hopefully unlikely to be modified
667/// while working on other tests.)
668fn common_inputs_stamp(config: &Config) -> Stamp {
669    let src_root = &config.src_root;
670
671    let mut stamp = Stamp::from_path(&config.rustc_path);
672
673    // Relevant pretty printer files
674    let pretty_printer_files = [
675        "src/etc/rust_types.py",
676        "src/etc/gdb_load_rust_pretty_printers.py",
677        "src/etc/gdb_lookup.py",
678        "src/etc/gdb_providers.py",
679        "src/etc/lldb_batchmode.py",
680        "src/etc/lldb_lookup.py",
681        "src/etc/lldb_providers.py",
682    ];
683    for file in &pretty_printer_files {
684        let path = src_root.join(file);
685        stamp.add_path(&path);
686    }
687
688    stamp.add_dir(&src_root.join("src/etc/natvis"));
689
690    stamp.add_dir(&config.run_lib_path);
691
692    if let Some(ref rustdoc_path) = config.rustdoc_path {
693        stamp.add_path(&rustdoc_path);
694        stamp.add_path(&src_root.join("src/etc/htmldocck.py"));
695    }
696
697    // Re-run coverage tests if the `coverage-dump` tool was modified,
698    // because its output format might have changed.
699    if let Some(coverage_dump_path) = &config.coverage_dump_path {
700        stamp.add_path(coverage_dump_path)
701    }
702
703    stamp.add_dir(&src_root.join("src/tools/run-make-support"));
704
705    // Compiletest itself.
706    stamp.add_dir(&src_root.join("src/tools/compiletest"));
707
708    stamp
709}
710
711/// Returns a list of modified/untracked test files that should be run when
712/// the `--only-modified` flag is in use.
713///
714/// (Might be inaccurate in some cases.)
715fn modified_tests(config: &Config, dir: &Utf8Path) -> Result<Vec<Utf8PathBuf>, String> {
716    // If `--only-modified` wasn't passed, the list of modified tests won't be
717    // used for anything, so avoid some work and just return an empty list.
718    if !config.only_modified {
719        return Ok(vec![]);
720    }
721
722    let files = get_git_modified_files(
723        &config.git_config(),
724        Some(dir.as_std_path()),
725        &vec!["rs", "stderr", "fixed"],
726    )?;
727    // Add new test cases to the list, it will be convenient in daily development.
728    let untracked_files = get_git_untracked_files(Some(dir.as_std_path()))?.unwrap_or(vec![]);
729
730    let all_paths = [&files[..], &untracked_files[..]].concat();
731    let full_paths = {
732        let mut full_paths: Vec<Utf8PathBuf> = all_paths
733            .into_iter()
734            .map(|f| Utf8PathBuf::from(f).with_extension("").with_extension("rs"))
735            .filter_map(
736                |f| if Utf8Path::new(&f).exists() { f.canonicalize_utf8().ok() } else { None },
737            )
738            .collect();
739        full_paths.dedup();
740        full_paths.sort_unstable();
741        full_paths
742    };
743    Ok(full_paths)
744}
745
746/// Recursively scans a directory to find test files and create test structures
747/// that will be handed over to the executor.
748fn collect_tests_from_dir(
749    cx: &TestCollectorCx,
750    dir: &Utf8Path,
751    relative_dir_path: &Utf8Path,
752) -> io::Result<TestCollector> {
753    // Ignore directories that contain a file named `compiletest-ignore-dir`.
754    if dir.join("compiletest-ignore-dir").exists() {
755        return Ok(TestCollector::new());
756    }
757
758    let mut components = dir.components().rev();
759    if let Some(Utf8Component::Normal(last)) = components.next()
760        && let Some(("assembly" | "codegen", backend)) = last.split_once('-')
761        && let Some(Utf8Component::Normal(parent)) = components.next()
762        && parent == "tests"
763        && let Ok(backend) = CodegenBackend::try_from(backend)
764        && backend != cx.config.default_codegen_backend
765    {
766        // We ignore asm tests which don't match the current codegen backend.
767        warning!(
768            "Ignoring tests in `{dir}` because they don't match the configured codegen \
769             backend (`{}`)",
770            cx.config.default_codegen_backend.as_str(),
771        );
772        return Ok(TestCollector::new());
773    }
774
775    // For run-make tests, a "test file" is actually a directory that contains an `rmake.rs`.
776    if cx.config.mode == TestMode::RunMake {
777        let mut collector = TestCollector::new();
778        if dir.join("rmake.rs").exists() {
779            let paths = TestPaths {
780                file: dir.to_path_buf(),
781                relative_dir: relative_dir_path.parent().unwrap().to_path_buf(),
782            };
783            make_test(cx, &mut collector, &paths);
784            // This directory is a test, so don't try to find other tests inside it.
785            return Ok(collector);
786        }
787    }
788
789    // If we find a test foo/bar.rs, we have to build the
790    // output directory `$build/foo` so we can write
791    // `$build/foo/bar` into it. We do this *now* in this
792    // sequential loop because otherwise, if we do it in the
793    // tests themselves, they race for the privilege of
794    // creating the directories and sometimes fail randomly.
795    let build_dir = output_relative_path(&cx.config, relative_dir_path);
796    fs::create_dir_all(&build_dir).unwrap();
797
798    // Add each `.rs` file as a test, and recurse further on any
799    // subdirectories we find, except for `auxiliary` directories.
800    // FIXME: this walks full tests tree, even if we have something to ignore
801    // use walkdir/ignore like in tidy?
802    fs::read_dir(dir.as_std_path())?
803        .par_bridge()
804        .map(|file| {
805            let mut collector = TestCollector::new();
806            let file = file?;
807            let file_path = Utf8PathBuf::try_from(file.path()).unwrap();
808            let file_name = file_path.file_name().unwrap();
809
810            if is_test(file_name)
811                && (!cx.config.only_modified || cx.modified_tests.contains(&file_path))
812            {
813                // We found a test file, so create the corresponding test structures.
814                debug!(%file_path, "found test file");
815
816                // Record the stem of the test file, to check for overlaps later.
817                let rel_test_path = relative_dir_path.join(file_path.file_stem().unwrap());
818                collector.found_path_stems.insert(rel_test_path);
819
820                let paths =
821                    TestPaths { file: file_path, relative_dir: relative_dir_path.to_path_buf() };
822                make_test(cx, &mut collector, &paths);
823            } else if file_path.is_dir() {
824                // Recurse to find more tests in a subdirectory.
825                let relative_file_path = relative_dir_path.join(file_name);
826                if file_name != "auxiliary" {
827                    debug!(%file_path, "found directory");
828                    collector.merge(collect_tests_from_dir(cx, &file_path, &relative_file_path)?);
829                }
830            } else {
831                debug!(%file_path, "found other file/directory");
832            }
833            Ok(collector)
834        })
835        .reduce(
836            || Ok(TestCollector::new()),
837            |a, b| {
838                let mut a = a?;
839                a.merge(b?);
840                Ok(a)
841            },
842        )
843}
844
845/// Returns true if `file_name` looks like a proper test file name.
846fn is_test(file_name: &str) -> bool {
847    if !file_name.ends_with(".rs") {
848        return false;
849    }
850
851    // `.`, `#`, and `~` are common temp-file prefixes.
852    let invalid_prefixes = &[".", "#", "~"];
853    !invalid_prefixes.iter().any(|p| file_name.starts_with(p))
854}
855
856/// For a single test file, creates one or more test structures (one per revision) that can be
857/// handed over to the executor to run, possibly in parallel.
858fn make_test(cx: &TestCollectorCx, collector: &mut TestCollector, testpaths: &TestPaths) {
859    // For run-make tests, each "test file" is actually a _directory_ containing an `rmake.rs`. But
860    // for the purposes of directive parsing, we want to look at that recipe file, not the directory
861    // itself.
862    let test_path = if cx.config.mode == TestMode::RunMake {
863        testpaths.file.join("rmake.rs")
864    } else {
865        testpaths.file.clone()
866    };
867
868    // Scan the test file to discover its revisions, if any.
869    let file_contents =
870        fs::read_to_string(&test_path).expect("reading test file for directives should succeed");
871    let file_directives = FileDirectives::from_file_contents(&test_path, &file_contents);
872    let early_props = EarlyProps::from_file_directives(&cx.config, &file_directives);
873
874    // Normally we create one structure per revision, with two exceptions:
875    // - If a test doesn't use revisions, create a dummy revision (None) so that
876    //   the test can still run.
877    // - Incremental tests inherently can't run their revisions in parallel, so
878    //   we treat them like non-revisioned tests here. Incremental revisions are
879    //   handled internally by `runtest::run` instead.
880    let revisions = if early_props.revisions.is_empty() || cx.config.mode == TestMode::Incremental {
881        vec![None]
882    } else {
883        early_props.revisions.iter().map(|r| Some(r.as_str())).collect()
884    };
885
886    // For each revision (or the sole dummy revision), create and append a
887    // `CollectedTest` that can be handed over to the test executor.
888    collector.tests.extend(revisions.into_iter().map(|revision| {
889        // Create a test name and description to hand over to the executor.
890        let (test_name, filterable_path) =
891            make_test_name_and_filterable_path(&cx.config, testpaths, revision);
892
893        // While scanning for ignore/only/needs directives, also collect aux
894        // paths for up-to-date checking.
895        let mut aux_props = AuxProps::default();
896
897        // Create a description struct for the test/revision.
898        // This is where `ignore-*`/`only-*`/`needs-*` directives are handled,
899        // because they historically needed to set the libtest ignored flag.
900        let mut desc = make_test_description(
901            &cx.config,
902            &cx.cache,
903            test_name,
904            &test_path,
905            &filterable_path,
906            &file_directives,
907            revision,
908            &mut collector.poisoned,
909            &mut aux_props,
910        );
911
912        // If a test's inputs haven't changed since the last time it ran,
913        // mark it as ignored so that the executor will skip it.
914        if !desc.ignore
915            && !cx.config.force_rerun
916            && is_up_to_date(cx, testpaths, &aux_props, revision)
917        {
918            desc.ignore = true;
919            // Keep this in sync with the "up-to-date" message detected by bootstrap.
920            // FIXME(Zalathar): Now that we are no longer tied to libtest, we could
921            // find a less fragile way to communicate this status to bootstrap.
922            desc.ignore_message = Some("up-to-date".into());
923        }
924
925        let config = Arc::clone(&cx.config);
926        let testpaths = testpaths.clone();
927        let revision = revision.map(str::to_owned);
928
929        CollectedTest { desc, config, testpaths, revision }
930    }));
931}
932
933/// The path of the `stamp` file that gets created or updated whenever a
934/// particular test completes successfully.
935fn stamp_file_path(config: &Config, testpaths: &TestPaths, revision: Option<&str>) -> Utf8PathBuf {
936    output_base_dir(config, testpaths, revision).join("stamp")
937}
938
939/// Returns a list of files that, if modified, would cause this test to no
940/// longer be up-to-date.
941///
942/// (Might be inaccurate in some cases.)
943fn files_related_to_test(
944    config: &Config,
945    testpaths: &TestPaths,
946    aux_props: &AuxProps,
947    revision: Option<&str>,
948) -> Vec<Utf8PathBuf> {
949    let mut related = vec![];
950
951    if testpaths.file.is_dir() {
952        // run-make tests use their individual directory
953        for entry in WalkDir::new(&testpaths.file) {
954            let path = entry.unwrap().into_path();
955            if path.is_file() {
956                related.push(Utf8PathBuf::try_from(path).unwrap());
957            }
958        }
959    } else {
960        related.push(testpaths.file.clone());
961    }
962
963    for aux in aux_props.all_aux_path_strings() {
964        // FIXME(Zalathar): Perform all `auxiliary` path resolution in one place.
965        // FIXME(Zalathar): This only finds auxiliary files used _directly_ by
966        // the test file; if a transitive auxiliary is modified, the test might
967        // be treated as "up-to-date" even though it should run.
968        let path = testpaths.file.parent().unwrap().join("auxiliary").join(aux);
969        related.push(path);
970    }
971
972    // UI test files.
973    for extension in UI_EXTENSIONS {
974        let path = expected_output_path(testpaths, revision, &config.compare_mode, extension);
975        related.push(path);
976    }
977
978    // `minicore.rs` test auxiliary: we need to make sure tests get rerun if this changes.
979    related.push(config.src_root.join("tests").join("auxiliary").join("minicore.rs"));
980
981    related
982}
983
984/// Checks whether a particular test/revision is "up-to-date", meaning that no
985/// relevant files/settings have changed since the last time the test succeeded.
986///
987/// (This is not very reliable in some circumstances, so the `--force-rerun`
988/// flag can be used to ignore up-to-date checking and always re-run tests.)
989fn is_up_to_date(
990    cx: &TestCollectorCx,
991    testpaths: &TestPaths,
992    aux_props: &AuxProps,
993    revision: Option<&str>,
994) -> bool {
995    let stamp_file_path = stamp_file_path(&cx.config, testpaths, revision);
996    // Check the config hash inside the stamp file.
997    let contents = match fs::read_to_string(&stamp_file_path) {
998        Ok(f) => f,
999        Err(ref e) if e.kind() == ErrorKind::InvalidData => panic!("Can't read stamp contents"),
1000        // The test hasn't succeeded yet, so it is not up-to-date.
1001        Err(_) => return false,
1002    };
1003    let expected_hash = runtest::compute_stamp_hash(&cx.config);
1004    if contents != expected_hash {
1005        // Some part of compiletest configuration has changed since the test
1006        // last succeeded, so it is not up-to-date.
1007        return false;
1008    }
1009
1010    // Check the timestamp of the stamp file against the last modified time
1011    // of all files known to be relevant to the test.
1012    let mut inputs_stamp = cx.common_inputs_stamp.clone();
1013    for path in files_related_to_test(&cx.config, testpaths, aux_props, revision) {
1014        inputs_stamp.add_path(&path);
1015    }
1016
1017    // If no relevant files have been modified since the stamp file was last
1018    // written, the test is up-to-date.
1019    inputs_stamp < Stamp::from_path(&stamp_file_path)
1020}
1021
1022/// The maximum of a set of file-modified timestamps.
1023#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
1024struct Stamp {
1025    time: SystemTime,
1026}
1027
1028impl Stamp {
1029    /// Creates a timestamp holding the last-modified time of the specified file.
1030    fn from_path(path: &Utf8Path) -> Self {
1031        let mut stamp = Stamp { time: SystemTime::UNIX_EPOCH };
1032        stamp.add_path(path);
1033        stamp
1034    }
1035
1036    /// Updates this timestamp to the last-modified time of the specified file,
1037    /// if it is later than the currently-stored timestamp.
1038    fn add_path(&mut self, path: &Utf8Path) {
1039        let modified = fs::metadata(path.as_std_path())
1040            .and_then(|metadata| metadata.modified())
1041            .unwrap_or(SystemTime::UNIX_EPOCH);
1042        self.time = self.time.max(modified);
1043    }
1044
1045    /// Updates this timestamp to the most recent last-modified time of all files
1046    /// recursively contained in the given directory, if it is later than the
1047    /// currently-stored timestamp.
1048    fn add_dir(&mut self, path: &Utf8Path) {
1049        let path = path.as_std_path();
1050        for entry in WalkDir::new(path) {
1051            let entry = entry.unwrap();
1052            if entry.file_type().is_file() {
1053                let modified = entry
1054                    .metadata()
1055                    .ok()
1056                    .and_then(|metadata| metadata.modified().ok())
1057                    .unwrap_or(SystemTime::UNIX_EPOCH);
1058                self.time = self.time.max(modified);
1059            }
1060        }
1061    }
1062}
1063
1064/// Creates a name for this test/revision that can be handed over to the executor.
1065fn make_test_name_and_filterable_path(
1066    config: &Config,
1067    testpaths: &TestPaths,
1068    revision: Option<&str>,
1069) -> (String, Utf8PathBuf) {
1070    // Print the name of the file, relative to the sources root.
1071    let path = testpaths.file.strip_prefix(&config.src_root).unwrap();
1072    let debugger = match config.debugger {
1073        Some(d) => format!("-{}", d),
1074        None => String::new(),
1075    };
1076    let mode_suffix = match config.compare_mode {
1077        Some(ref mode) => format!(" ({})", mode.to_str()),
1078        None => String::new(),
1079    };
1080
1081    let name = format!(
1082        "[{}{}{}] {}{}",
1083        config.mode,
1084        debugger,
1085        mode_suffix,
1086        path,
1087        revision.map_or("".to_string(), |rev| format!("#{}", rev))
1088    );
1089
1090    // `path` is the full path from the repo root like, `tests/ui/foo/bar.rs`.
1091    // Filtering is applied without the `tests/ui/` part, so strip that off.
1092    // First strip off "tests" to make sure we don't have some unexpected path.
1093    let mut filterable_path = path.strip_prefix("tests").unwrap().to_owned();
1094    // Now strip off e.g. "ui" or "run-make" component.
1095    filterable_path = filterable_path.components().skip(1).collect();
1096
1097    (name, filterable_path)
1098}
1099
1100/// Checks that test discovery didn't find any tests whose name stem is a prefix
1101/// of some other tests's name.
1102///
1103/// For example, suppose the test suite contains these two test files:
1104/// - `tests/rustdoc/primitive.rs`
1105/// - `tests/rustdoc/primitive/no_std.rs`
1106///
1107/// The test runner might put the output from those tests in these directories:
1108/// - `$build/test/rustdoc/primitive/`
1109/// - `$build/test/rustdoc/primitive/no_std/`
1110///
1111/// Because one output path is a subdirectory of the other, the two tests might
1112/// interfere with each other in unwanted ways, especially if the test runner
1113/// decides to delete test output directories to clean them between runs.
1114/// To avoid problems, we forbid test names from overlapping in this way.
1115///
1116/// See <https://github.com/rust-lang/rust/pull/109509> for more context.
1117fn check_for_overlapping_test_paths(found_path_stems: &HashSet<Utf8PathBuf>) {
1118    let mut collisions = Vec::new();
1119    for path in found_path_stems {
1120        for ancestor in path.ancestors().skip(1) {
1121            if found_path_stems.contains(ancestor) {
1122                collisions.push((path, ancestor));
1123            }
1124        }
1125    }
1126    if !collisions.is_empty() {
1127        collisions.sort();
1128        let collisions: String = collisions
1129            .into_iter()
1130            .map(|(path, check_parent)| format!("test {path} clashes with {check_parent}\n"))
1131            .collect();
1132        panic!(
1133            "{collisions}\n\
1134            Tests cannot have overlapping names. Make sure they use unique prefixes."
1135        );
1136    }
1137}
1138
1139fn early_config_check(config: &Config) {
1140    if !config.has_html_tidy && config.mode == TestMode::Rustdoc {
1141        warning!("`tidy` (html-tidy.org) is not installed; diffs will not be generated");
1142    }
1143
1144    if !config.profiler_runtime && config.mode == TestMode::CoverageRun {
1145        let actioned = if config.bless { "blessed" } else { "checked" };
1146        warning!("profiler runtime is not available, so `.coverage` files won't be {actioned}");
1147        help!("try setting `profiler = true` in the `[build]` section of `bootstrap.toml`");
1148    }
1149
1150    // `RUST_TEST_NOCAPTURE` is a libtest env var, but we don't callout to libtest.
1151    if env::var("RUST_TEST_NOCAPTURE").is_ok() {
1152        warning!("`RUST_TEST_NOCAPTURE` is not supported; use the `--no-capture` flag instead");
1153    }
1154}