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