Skip to main content

compiletest/
lib.rs

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