Skip to main content

compiletest/
lib.rs

1#![crate_name = "compiletest"]
2#![warn(unreachable_pub)]
3
4#[cfg(test)]
5mod tests;
6
7// Public modules needed by the compiletest binary or by `rustdoc-gui-test`.
8pub mod cli;
9pub mod rustdoc_gui_test;
10
11mod common;
12mod debuggers;
13mod diagnostics;
14mod directives;
15mod edition;
16mod errors;
17mod executor;
18mod json;
19mod output_capture;
20mod panic_hook;
21mod raise_fd_limit;
22mod read2;
23mod runtest;
24mod util;
25
26use core::panic;
27use std::collections::HashSet;
28use std::fmt::Write;
29use std::io::{self, ErrorKind};
30use std::sync::Arc;
31use std::time::SystemTime;
32use std::{env, fs, vec};
33
34use build_helper::git::{get_git_modified_files, get_git_untracked_files};
35use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
36use rayon::iter::{ParallelBridge, ParallelIterator};
37use tracing::debug;
38use walkdir::WalkDir;
39
40use self::directives::{EarlyProps, make_test_description};
41use crate::common::{
42    CodegenBackend, Config, Debugger, TestMode, TestPaths, UI_EXTENSIONS, expected_output_path,
43    output_base_dir, output_relative_path,
44};
45use crate::directives::{AuxProps, DirectivesCache, FileDirectives};
46use crate::executor::{CollectedTest, TestVariant};
47
48/// Called by `main` after the config has been parsed.
49fn run_tests(config: Arc<Config>) {
50    debug!(?config, "run_tests");
51
52    panic_hook::install_panic_hook();
53
54    // If we want to collect rustfix coverage information,
55    // we first make sure that the coverage file does not exist.
56    // It will be created later on.
57    if config.rustfix_coverage {
58        let mut coverage_file_path = config.build_test_suite_root.clone();
59        coverage_file_path.push("rustfix_missing_coverage.txt");
60        if coverage_file_path.exists() {
61            if let Err(e) = fs::remove_file(&coverage_file_path) {
62                panic!("Could not delete {} due to {}", coverage_file_path, e)
63            }
64        }
65    }
66
67    // sadly osx needs some file descriptor limits raised for running tests in
68    // parallel (especially when we have lots and lots of child processes).
69    // For context, see #8904
70    unsafe {
71        raise_fd_limit::raise_fd_limit();
72    }
73    // Prevent issue #21352 UAC blocking .exe containing 'patch' etc. on Windows
74    // If #11207 is resolved (adding manifest to .exe) this becomes unnecessary
75    //
76    // SAFETY: at this point we're still single-threaded.
77    unsafe { env::set_var("__COMPAT_LAYER", "RunAsInvoker") };
78
79    // Debugging emscripten code doesn't make sense today
80    let ignore_tests = config.mode == TestMode::DebugInfo && config.target.contains("emscripten");
81
82    if let TestMode::DebugInfo = config.mode {
83        // FIXME: this should ideally happen somewhere else..
84        if config.target.contains("android") {
85            println!("{} debug-info test uses tcp 5039 port. please reserve it", config.target);
86
87            // android debug-info test uses remote debugger so, we test 1 thread
88            // at once as they're all sharing the same TCP port to communicate
89            // over.
90            //
91            // we should figure out how to lift this restriction! (run them all
92            // on different ports allocated dynamically).
93            //
94            // SAFETY: at this point we are still single-threaded.
95            unsafe { env::set_var("RUST_TEST_THREADS", "1") };
96        }
97    };
98
99    // Discover all of the tests in the test suite directory, and build a `CollectedTest`
100    // structure for each test (or each revision of a multi-revision test).
101    let mut tests = Vec::new();
102    if !ignore_tests {
103        tests.extend(collect_and_make_tests(config.clone()));
104    }
105
106    tests.sort_by(|a, b| Ord::cmp(&a.desc.name, &b.desc.name));
107
108    // Delegate to the executor to filter and run the big list of test structures
109    // created during test discovery. When the executor decides to run a test,
110    // it will return control to the rest of compiletest by calling `runtest::run`.
111    let ok = executor::run_tests(&config, tests);
112
113    // Check the outcome reported by the executor.
114    if !ok {
115        // We want to report that the tests failed, but we also want to give
116        // some indication of just what tests we were running. Especially on
117        // CI, where there can be cross-compiled tests for a lot of
118        // architectures, without this critical information it can be quite
119        // easy to miss which tests failed, and as such fail to reproduce
120        // the failure locally.
121
122        let mut msg = String::from("Some tests failed in compiletest");
123        write!(msg, " suite={}", config.suite).unwrap();
124
125        if let Some(compare_mode) = config.compare_mode.as_ref() {
126            write!(msg, " compare_mode={}", compare_mode).unwrap();
127        }
128
129        if let Some(pass_mode) = config.force_pass_mode.as_ref() {
130            write!(msg, " pass_mode={}", pass_mode).unwrap();
131        }
132
133        write!(msg, " mode={}", config.mode).unwrap();
134        write!(msg, " host={}", config.host).unwrap();
135        write!(msg, " target={}", config.target).unwrap();
136
137        println!("{msg}");
138
139        std::process::exit(1);
140    }
141}
142
143/// Read-only context data used during test collection.
144struct TestCollectorCx {
145    config: Arc<Config>,
146    cache: DirectivesCache,
147    common_inputs_stamp: Stamp,
148    modified_tests: Vec<Utf8PathBuf>,
149}
150
151/// Mutable state used during test collection.
152struct TestCollector {
153    tests: Vec<CollectedTest>,
154    found_path_stems: HashSet<Utf8PathBuf>,
155    poisoned: bool,
156}
157
158impl TestCollector {
159    fn new() -> Self {
160        TestCollector { tests: vec![], found_path_stems: HashSet::new(), poisoned: false }
161    }
162
163    fn merge(&mut self, mut other: Self) {
164        self.tests.append(&mut other.tests);
165        self.found_path_stems.extend(other.found_path_stems);
166        self.poisoned |= other.poisoned;
167    }
168}
169
170/// Creates test structures for every test/revision in the test suite directory.
171///
172/// This always inspects _all_ test files in the suite (e.g. all 17k+ ui tests),
173/// regardless of whether any filters/tests were specified on the command-line,
174/// because filtering is handled later by code that was copied from libtest.
175///
176/// FIXME(Zalathar): Now that we no longer rely on libtest, try to overhaul
177/// test discovery to take into account the filters/tests specified on the
178/// command-line, instead of having to enumerate everything.
179fn collect_and_make_tests(config: Arc<Config>) -> Vec<CollectedTest> {
180    debug!("making tests from {}", config.src_test_suite_root);
181    let common_inputs_stamp = common_inputs_stamp(&config);
182    let modified_tests =
183        modified_tests(&config, &config.src_test_suite_root).unwrap_or_else(|err| {
184            fatal!("modified_tests: {}: {err}", config.src_test_suite_root);
185        });
186    let cache = DirectivesCache::load(&config);
187
188    let cx = TestCollectorCx { config, cache, common_inputs_stamp, modified_tests };
189    let collector = collect_tests_from_dir(&cx, &cx.config.src_test_suite_root, Utf8Path::new(""))
190        .unwrap_or_else(|reason| {
191            panic!("Could not read tests from {}: {reason}", cx.config.src_test_suite_root)
192        });
193
194    let TestCollector { tests, found_path_stems, poisoned } = collector;
195
196    if poisoned {
197        eprintln!();
198        panic!("there are errors in tests");
199    }
200
201    check_for_overlapping_test_paths(&found_path_stems);
202
203    tests
204}
205
206/// Returns the most recent last-modified timestamp from among the input files
207/// that are considered relevant to all tests (e.g. the compiler, std, and
208/// compiletest itself).
209///
210/// (Some of these inputs aren't actually relevant to _all_ tests, but they are
211/// common to some subset of tests, and are hopefully unlikely to be modified
212/// while working on other tests.)
213fn common_inputs_stamp(config: &Config) -> Stamp {
214    let src_root = &config.src_root;
215
216    let mut stamp = Stamp::from_path(&config.rustc_path);
217
218    // Relevant pretty printer files
219    let pretty_printer_files = [
220        "src/etc/rust_types.py",
221        "src/etc/gdb_load_rust_pretty_printers.py",
222        "src/etc/gdb_lookup.py",
223        "src/etc/gdb_providers.py",
224        "src/etc/lldb_batchmode",
225        "src/etc/lldb_lookup.py",
226        "src/etc/lldb_providers.py",
227    ];
228    for file in &pretty_printer_files {
229        let path = src_root.join(file);
230        stamp.add_path(&path);
231    }
232
233    stamp.add_dir(&src_root.join("src/etc/natvis"));
234
235    stamp.add_dir(&config.target_run_lib_path);
236
237    if let Some(ref rustdoc_path) = config.rustdoc_path {
238        stamp.add_path(&rustdoc_path);
239        stamp.add_path(&src_root.join("src/etc/htmldocck.py"));
240    }
241
242    // Re-run coverage tests if the `coverage-dump` tool was modified,
243    // because its output format might have changed.
244    if let Some(coverage_dump_path) = &config.coverage_dump_path {
245        stamp.add_path(coverage_dump_path)
246    }
247
248    stamp.add_dir(&src_root.join("src/tools/run-make-support"));
249
250    // Compiletest itself.
251    stamp.add_dir(&src_root.join("src/tools/compiletest"));
252
253    stamp
254}
255
256/// Returns a list of modified/untracked test files that should be run when
257/// the `--only-modified` flag is in use.
258///
259/// (Might be inaccurate in some cases.)
260fn modified_tests(config: &Config, dir: &Utf8Path) -> Result<Vec<Utf8PathBuf>, String> {
261    // If `--only-modified` wasn't passed, the list of modified tests won't be
262    // used for anything, so avoid some work and just return an empty list.
263    if !config.only_modified {
264        return Ok(vec![]);
265    }
266
267    let files = get_git_modified_files(
268        &config.git_config(),
269        Some(dir.as_std_path()),
270        &vec!["rs", "stderr", "fixed"],
271    )?;
272    // Add new test cases to the list, it will be convenient in daily development.
273    let untracked_files = get_git_untracked_files(Some(dir.as_std_path()))?.unwrap_or(vec![]);
274
275    let all_paths = [&files[..], &untracked_files[..]].concat();
276    let full_paths = {
277        let mut full_paths: Vec<Utf8PathBuf> = all_paths
278            .into_iter()
279            .map(|f| Utf8PathBuf::from(f).with_extension("").with_extension("rs"))
280            .filter_map(
281                |f| if Utf8Path::new(&f).exists() { f.canonicalize_utf8().ok() } else { None },
282            )
283            .collect();
284        full_paths.dedup();
285        full_paths.sort_unstable();
286        full_paths
287    };
288    Ok(full_paths)
289}
290
291/// Recursively scans a directory to find test files and create test structures
292/// that will be handed over to the executor.
293fn collect_tests_from_dir(
294    cx: &TestCollectorCx,
295    dir: &Utf8Path,
296    relative_dir_path: &Utf8Path,
297) -> io::Result<TestCollector> {
298    // Ignore directories that contain a file named `compiletest-ignore-dir`.
299    if dir.join("compiletest-ignore-dir").exists() {
300        return Ok(TestCollector::new());
301    }
302
303    let mut components = dir.components().rev();
304    if let Some(Utf8Component::Normal(last)) = components.next()
305        && let Some(("assembly" | "codegen", backend)) = last.split_once('-')
306        && let Some(Utf8Component::Normal(parent)) = components.next()
307        && parent == "tests"
308        && let Ok(backend) = backend.parse::<CodegenBackend>()
309        && backend != cx.config.default_codegen_backend
310    {
311        // We ignore asm tests which don't match the current codegen backend.
312        warning!(
313            "Ignoring tests in `{dir}` because they don't match the configured codegen \
314             backend (`{}`)",
315            cx.config.default_codegen_backend.as_str(),
316        );
317        return Ok(TestCollector::new());
318    }
319
320    // For run-make tests, a "test file" is actually a directory that contains an `rmake.rs`.
321    if cx.config.mode == TestMode::RunMake {
322        let mut collector = TestCollector::new();
323        if dir.join("rmake.rs").exists() {
324            let paths = TestPaths {
325                file: dir.to_path_buf(),
326                relative_dir: relative_dir_path.parent().unwrap().to_path_buf(),
327            };
328            make_test(cx, &mut collector, &paths);
329            // This directory is a test, so don't try to find other tests inside it.
330            return Ok(collector);
331        }
332    }
333
334    // If we find a test foo/bar.rs, we have to build the
335    // output directory `$build/foo` so we can write
336    // `$build/foo/bar` into it. We do this *now* in this
337    // sequential loop because otherwise, if we do it in the
338    // tests themselves, they race for the privilege of
339    // creating the directories and sometimes fail randomly.
340    let build_dir = output_relative_path(&cx.config, relative_dir_path);
341    fs::create_dir_all(&build_dir).unwrap();
342
343    // Add each `.rs` file as a test, and recurse further on any
344    // subdirectories we find, except for `auxiliary` directories.
345    // FIXME: this walks full tests tree, even if we have something to ignore
346    // use walkdir/ignore like in tidy?
347    fs::read_dir(dir.as_std_path())?
348        .par_bridge()
349        .map(|file| {
350            let mut collector = TestCollector::new();
351            let file = file?;
352            let file_path = Utf8PathBuf::try_from(file.path()).unwrap();
353            let file_name = file_path.file_name().unwrap();
354
355            if is_test(file_name)
356                && (!cx.config.only_modified || cx.modified_tests.contains(&file_path))
357            {
358                // We found a test file, so create the corresponding test structures.
359                debug!(%file_path, "found test file");
360
361                // Record the stem of the test file, to check for overlaps later.
362                let rel_test_path = relative_dir_path.join(file_path.file_stem().unwrap());
363                collector.found_path_stems.insert(rel_test_path);
364
365                let paths =
366                    TestPaths { file: file_path, relative_dir: relative_dir_path.to_path_buf() };
367                make_test(cx, &mut collector, &paths);
368            } else if file_path.is_dir() {
369                // Recurse to find more tests in a subdirectory.
370                let relative_file_path = relative_dir_path.join(file_name);
371                if file_name != "auxiliary" {
372                    debug!(%file_path, "found directory");
373                    collector.merge(collect_tests_from_dir(cx, &file_path, &relative_file_path)?);
374                }
375            } else {
376                debug!(%file_path, "found other file/directory");
377            }
378            Ok(collector)
379        })
380        .reduce(
381            || Ok(TestCollector::new()),
382            |a, b| {
383                let mut a = a?;
384                a.merge(b?);
385                Ok(a)
386            },
387        )
388}
389
390/// Returns true if `file_name` looks like a proper test file name.
391fn is_test(file_name: &str) -> bool {
392    if !file_name.ends_with(".rs") {
393        return false;
394    }
395
396    // `.`, `#`, and `~` are common temp-file prefixes.
397    let invalid_prefixes = &[".", "#", "~"];
398    !invalid_prefixes.iter().any(|p| file_name.starts_with(p))
399}
400
401/// For a single test file, creates one or more test structures (one per revision) that can be
402/// handed over to the executor to run, possibly in parallel.
403fn make_test(cx: &TestCollectorCx, collector: &mut TestCollector, testpaths: &TestPaths) {
404    // For run-make tests, each "test file" is actually a _directory_ containing an `rmake.rs`. But
405    // for the purposes of directive parsing, we want to look at that recipe file, not the directory
406    // itself.
407    let test_path = if cx.config.mode == TestMode::RunMake {
408        testpaths.file.join("rmake.rs")
409    } else {
410        testpaths.file.clone()
411    };
412
413    // Scan the test file to discover its revisions, if any.
414    let file_contents =
415        fs::read_to_string(&test_path).expect("reading test file for directives should succeed");
416    let file_directives = FileDirectives::from_file_contents(&test_path, &file_contents);
417
418    if let Err(message) = directives::do_early_directives_check(cx.config.mode, &file_directives) {
419        // FIXME(Zalathar): Overhaul compiletest error handling so that we
420        // don't have to resort to ad-hoc panics everywhere.
421        panic!("directives check failed:\n{message}");
422    }
423    let early_props = EarlyProps::from_file_directives(&cx.config, &file_directives);
424
425    // Normally we create one structure per revision, with two exceptions:
426    // - If a test doesn't use revisions, create a dummy revision (None) so that
427    //   the test can still run.
428    // - Incremental tests inherently can't run their revisions in parallel, so
429    //   we treat them like non-revisioned tests here. Incremental revisions are
430    //   handled internally by `runtest::run` instead.
431    let revisions = if early_props.revisions.is_empty() || cx.config.mode == TestMode::Incremental {
432        vec![None]
433    } else {
434        early_props.revisions.iter().map(|r| Some(r.as_str())).collect()
435    };
436
437    // For debuginfo tests, we have to run them once for each debugger.
438    // We thus create a cartesian product of each revision and each supported debugger here.
439    let debuggers = if cx.config.mode == TestMode::DebugInfo {
440        vec![Some(Debugger::Cdb), Some(Debugger::Gdb), Some(Debugger::Lldb)]
441    } else {
442        vec![None]
443    };
444
445    // For each revision (or the sole dummy revision) and each debugger, create and append a
446    // `CollectedTest` that can be handed over to the test executor.
447    for debugger in debuggers {
448        collector.tests.extend(revisions.iter().map(|&revision| {
449            let revision = revision.map(str::to_owned);
450            let variant = TestVariant { revision, debugger };
451
452            // Create a test name and description to hand over to the executor.
453            let (test_name, filterable_path) =
454                make_test_name_and_filterable_path(&cx.config, testpaths, &variant);
455
456            // While scanning for ignore/only/needs directives, also collect aux
457            // paths for up-to-date checking.
458            let mut aux_props = AuxProps::default();
459
460            // Create a description struct for the test/revision.
461            // This is where `ignore-*`/`only-*`/`needs-*` directives are handled,
462            // because they historically needed to set the libtest ignored flag.
463            let mut desc = make_test_description(
464                &cx.config,
465                &cx.cache,
466                test_name,
467                &test_path,
468                &filterable_path,
469                &file_directives,
470                &variant,
471                &mut collector.poisoned,
472                &mut aux_props,
473            );
474
475            // If a test's inputs haven't changed since the last time it ran,
476            // mark it as ignored so that the executor will skip it.
477            if !desc.is_ignored()
478                && !cx.config.force_rerun
479                && is_up_to_date(cx, testpaths, &aux_props, &variant)
480            {
481                // Keep this in sync with the "up-to-date" message detected by bootstrap.
482                // FIXME(Zalathar): Now that we are no longer tied to libtest, we could
483                // find a less fragile way to communicate this status to bootstrap.
484                desc.ignore_message = Some("up-to-date".into());
485            }
486
487            let config = Arc::clone(&cx.config);
488            let testpaths = testpaths.clone();
489
490            CollectedTest { desc, config, testpaths, variant }
491        }));
492    }
493}
494
495/// The path of the `stamp` file that gets created or updated whenever a
496/// particular test completes successfully.
497fn stamp_file_path(config: &Config, testpaths: &TestPaths, variant: &TestVariant) -> Utf8PathBuf {
498    output_base_dir(config, testpaths, variant).join("stamp")
499}
500
501/// Returns a list of files that, if modified, would cause this test to no
502/// longer be up-to-date.
503///
504/// (Might be inaccurate in some cases.)
505fn files_related_to_test(
506    config: &Config,
507    testpaths: &TestPaths,
508    aux_props: &AuxProps,
509    revision: Option<&str>,
510) -> Vec<Utf8PathBuf> {
511    let mut related = vec![];
512
513    if testpaths.file.is_dir() {
514        // run-make tests use their individual directory
515        for entry in WalkDir::new(&testpaths.file) {
516            let path = entry.unwrap().into_path();
517            if path.is_file() {
518                related.push(Utf8PathBuf::try_from(path).unwrap());
519            }
520        }
521    } else {
522        related.push(testpaths.file.clone());
523    }
524
525    for aux in aux_props.all_aux_path_strings() {
526        // FIXME(Zalathar): Perform all `auxiliary` path resolution in one place.
527        // FIXME(Zalathar): This only finds auxiliary files used _directly_ by
528        // the test file; if a transitive auxiliary is modified, the test might
529        // be treated as "up-to-date" even though it should run.
530        let path = testpaths.file.parent().unwrap().join("auxiliary").join(aux);
531        related.push(path);
532    }
533
534    // UI test files.
535    for extension in UI_EXTENSIONS {
536        let path = expected_output_path(testpaths, revision, &config.compare_mode, extension);
537        related.push(path);
538    }
539
540    // `minicore.rs` test auxiliary: we need to make sure tests get rerun if this changes.
541    related.push(config.src_root.join("tests").join("auxiliary").join("minicore.rs"));
542
543    related
544}
545
546/// Checks whether a particular test/revision is "up-to-date", meaning that no
547/// relevant files/settings have changed since the last time the test succeeded.
548///
549/// (This is not very reliable in some circumstances, so the `--force-rerun`
550/// flag can be used to ignore up-to-date checking and always re-run tests.)
551fn is_up_to_date(
552    cx: &TestCollectorCx,
553    testpaths: &TestPaths,
554    aux_props: &AuxProps,
555    variant: &TestVariant,
556) -> bool {
557    let stamp_file_path = stamp_file_path(&cx.config, testpaths, variant);
558    // Check the config hash inside the stamp file.
559    let contents = match fs::read_to_string(&stamp_file_path) {
560        Ok(f) => f,
561        Err(ref e) if e.kind() == ErrorKind::InvalidData => panic!("Can't read stamp contents"),
562        // The test hasn't succeeded yet, so it is not up-to-date.
563        Err(_) => return false,
564    };
565    let expected_hash = runtest::compute_stamp_hash(&cx.config, variant);
566    if contents != expected_hash {
567        // Some part of compiletest configuration has changed since the test
568        // last succeeded, so it is not up-to-date.
569        return false;
570    }
571
572    // Check the timestamp of the stamp file against the last modified time
573    // of all files known to be relevant to the test.
574    let mut inputs_stamp = cx.common_inputs_stamp.clone();
575    for path in files_related_to_test(&cx.config, testpaths, aux_props, variant.revision()) {
576        inputs_stamp.add_path(&path);
577    }
578
579    // If no relevant files have been modified since the stamp file was last
580    // written, the test is up-to-date.
581    inputs_stamp < Stamp::from_path(&stamp_file_path)
582}
583
584/// The maximum of a set of file-modified timestamps.
585#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
586struct Stamp {
587    time: SystemTime,
588}
589
590impl Stamp {
591    /// Creates a timestamp holding the last-modified time of the specified file.
592    fn from_path(path: &Utf8Path) -> Self {
593        let mut stamp = Stamp { time: SystemTime::UNIX_EPOCH };
594        stamp.add_path(path);
595        stamp
596    }
597
598    /// Updates this timestamp to the last-modified time of the specified file,
599    /// if it is later than the currently-stored timestamp.
600    fn add_path(&mut self, path: &Utf8Path) {
601        let modified = fs::metadata(path.as_std_path())
602            .and_then(|metadata| metadata.modified())
603            .unwrap_or(SystemTime::UNIX_EPOCH);
604        self.time = self.time.max(modified);
605    }
606
607    /// Updates this timestamp to the most recent last-modified time of all files
608    /// recursively contained in the given directory, if it is later than the
609    /// currently-stored timestamp.
610    fn add_dir(&mut self, path: &Utf8Path) {
611        let path = path.as_std_path();
612        for entry in WalkDir::new(path) {
613            let entry = entry.unwrap();
614            if entry.file_type().is_file() {
615                let modified = entry
616                    .metadata()
617                    .ok()
618                    .and_then(|metadata| metadata.modified().ok())
619                    .unwrap_or(SystemTime::UNIX_EPOCH);
620                self.time = self.time.max(modified);
621            }
622        }
623    }
624}
625
626/// Creates a name for this test/revision that can be handed over to the executor.
627fn make_test_name_and_filterable_path(
628    config: &Config,
629    testpaths: &TestPaths,
630    variant: &TestVariant,
631) -> (String, Utf8PathBuf) {
632    // Print the name of the file, relative to the sources root.
633    let path = testpaths.file.strip_prefix(&config.src_root).unwrap();
634    let debugger = match variant.debugger.as_ref() {
635        Some(d) => format!("-{d}"),
636        None => String::new(),
637    };
638    let mode_suffix = match config.compare_mode {
639        Some(ref mode) => format!(" ({})", mode.to_str()),
640        None => String::new(),
641    };
642
643    let name = format!(
644        "[{}{}{}] {}{}",
645        config.mode,
646        debugger,
647        mode_suffix,
648        path,
649        variant.revision().map_or("".to_string(), |rev| format!("#{}", rev))
650    );
651
652    // `path` is the full path from the repo root like, `tests/ui/foo/bar.rs`.
653    // Filtering is applied without the `tests/ui/` part, so strip that off.
654    // First strip off "tests" to make sure we don't have some unexpected path.
655    let mut filterable_path = path.strip_prefix("tests").unwrap().to_owned();
656    // Now strip off e.g. "ui" or "run-make" component.
657    filterable_path = filterable_path.components().skip(1).collect();
658
659    (name, filterable_path)
660}
661
662/// Checks that test discovery didn't find any tests whose name stem is a prefix
663/// of some other tests's name.
664///
665/// For example, suppose the test suite contains these two test files:
666/// - `tests/rustdoc-html/primitive.rs`
667/// - `tests/rustdoc-html/primitive/no_std.rs`
668///
669/// The test runner might put the output from those tests in these directories:
670/// - `$build/test/rustdoc/primitive/`
671/// - `$build/test/rustdoc/primitive/no_std/`
672///
673/// Because one output path is a subdirectory of the other, the two tests might
674/// interfere with each other in unwanted ways, especially if the test runner
675/// decides to delete test output directories to clean them between runs.
676/// To avoid problems, we forbid test names from overlapping in this way.
677///
678/// See <https://github.com/rust-lang/rust/pull/109509> for more context.
679fn check_for_overlapping_test_paths(found_path_stems: &HashSet<Utf8PathBuf>) {
680    let mut collisions = Vec::new();
681    for path in found_path_stems {
682        for ancestor in path.ancestors().skip(1) {
683            if found_path_stems.contains(ancestor) {
684                collisions.push((path, ancestor));
685            }
686        }
687    }
688    if !collisions.is_empty() {
689        collisions.sort();
690        let collisions: String = collisions
691            .into_iter()
692            .map(|(path, check_parent)| format!("test {path} clashes with {check_parent}\n"))
693            .collect();
694        panic!(
695            "{collisions}\n\
696            Tests cannot have overlapping names. Make sure they use unique prefixes."
697        );
698    }
699}
700
701fn early_config_check(config: &Config) {
702    if !config.profiler_runtime && config.mode == TestMode::CoverageRun {
703        let actioned = if config.bless { "blessed" } else { "checked" };
704        warning!("profiler runtime is not available, so `.coverage` files won't be {actioned}");
705        help!("try setting `profiler = true` in the `[build]` section of `bootstrap.toml`");
706    }
707
708    // `RUST_TEST_NOCAPTURE` is a libtest env var, but we don't callout to libtest.
709    if env::var("RUST_TEST_NOCAPTURE").is_ok() {
710        warning!("`RUST_TEST_NOCAPTURE` is not supported; use the `--no-capture` flag instead");
711    }
712}