compiletest/
lib.rs

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