compiletest/
lib.rs

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