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;
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    let mut configs = Vec::new();
80    if let TestMode::DebugInfo = config.mode {
81        // Debugging emscripten code doesn't make sense today
82        if !config.target.contains("emscripten") {
83            // FIXME: ideally, we would just have one config, and then have some mechanism of
84            // generating multiple variants of a test, one for each debugger (something like
85            // debuginfo revisions). But for now, we just create three configs.
86            configs.extend([
87                Arc::new(Config { debugger: Some(Debugger::Cdb), ..config.as_ref().clone() }),
88                Arc::new(Config { debugger: Some(Debugger::Gdb), ..config.as_ref().clone() }),
89                Arc::new(Config { debugger: Some(Debugger::Lldb), ..config.as_ref().clone() }),
90            ]);
91
92            // FIXME: this should ideally happen somewhere else..
93            if config.target.contains("android") {
94                println!("{} debug-info test uses tcp 5039 port. please reserve it", config.target);
95
96                // android debug-info test uses remote debugger so, we test 1 thread
97                // at once as they're all sharing the same TCP port to communicate
98                // over.
99                //
100                // we should figure out how to lift this restriction! (run them all
101                // on different ports allocated dynamically).
102                //
103                // SAFETY: at this point we are still single-threaded.
104                unsafe { env::set_var("RUST_TEST_THREADS", "1") };
105            }
106        }
107    } else {
108        configs.push(config.clone());
109    };
110
111    // Discover all of the tests in the test suite directory, and build a `CollectedTest`
112    // structure for each test (or each revision of a multi-revision test).
113    let mut tests = Vec::new();
114    for c in configs {
115        tests.extend(collect_and_make_tests(c));
116    }
117
118    tests.sort_by(|a, b| Ord::cmp(&a.desc.name, &b.desc.name));
119
120    // Delegate to the executor to filter and run the big list of test structures
121    // created during test discovery. When the executor decides to run a test,
122    // it will return control to the rest of compiletest by calling `runtest::run`.
123    let ok = executor::run_tests(&config, tests);
124
125    // Check the outcome reported by the executor.
126    if !ok {
127        // We want to report that the tests failed, but we also want to give
128        // some indication of just what tests we were running. Especially on
129        // CI, where there can be cross-compiled tests for a lot of
130        // architectures, without this critical information it can be quite
131        // easy to miss which tests failed, and as such fail to reproduce
132        // the failure locally.
133
134        let mut msg = String::from("Some tests failed in compiletest");
135        write!(msg, " suite={}", config.suite).unwrap();
136
137        if let Some(compare_mode) = config.compare_mode.as_ref() {
138            write!(msg, " compare_mode={}", compare_mode).unwrap();
139        }
140
141        if let Some(pass_mode) = config.force_pass_mode.as_ref() {
142            write!(msg, " pass_mode={}", pass_mode).unwrap();
143        }
144
145        write!(msg, " mode={}", config.mode).unwrap();
146        write!(msg, " host={}", config.host).unwrap();
147        write!(msg, " target={}", config.target).unwrap();
148
149        println!("{msg}");
150
151        std::process::exit(1);
152    }
153}
154
155/// Read-only context data used during test collection.
156struct TestCollectorCx {
157    config: Arc<Config>,
158    cache: DirectivesCache,
159    common_inputs_stamp: Stamp,
160    modified_tests: Vec<Utf8PathBuf>,
161}
162
163/// Mutable state used during test collection.
164struct TestCollector {
165    tests: Vec<CollectedTest>,
166    found_path_stems: HashSet<Utf8PathBuf>,
167    poisoned: bool,
168}
169
170impl TestCollector {
171    fn new() -> Self {
172        TestCollector { tests: vec![], found_path_stems: HashSet::new(), poisoned: false }
173    }
174
175    fn merge(&mut self, mut other: Self) {
176        self.tests.append(&mut other.tests);
177        self.found_path_stems.extend(other.found_path_stems);
178        self.poisoned |= other.poisoned;
179    }
180}
181
182/// Creates test structures for every test/revision in the test suite directory.
183///
184/// This always inspects _all_ test files in the suite (e.g. all 17k+ ui tests),
185/// regardless of whether any filters/tests were specified on the command-line,
186/// because filtering is handled later by code that was copied from libtest.
187///
188/// FIXME(Zalathar): Now that we no longer rely on libtest, try to overhaul
189/// test discovery to take into account the filters/tests specified on the
190/// command-line, instead of having to enumerate everything.
191fn collect_and_make_tests(config: Arc<Config>) -> Vec<CollectedTest> {
192    debug!("making tests from {}", config.src_test_suite_root);
193    let common_inputs_stamp = common_inputs_stamp(&config);
194    let modified_tests =
195        modified_tests(&config, &config.src_test_suite_root).unwrap_or_else(|err| {
196            fatal!("modified_tests: {}: {err}", config.src_test_suite_root);
197        });
198    let cache = DirectivesCache::load(&config);
199
200    let cx = TestCollectorCx { config, cache, common_inputs_stamp, modified_tests };
201    let collector = collect_tests_from_dir(&cx, &cx.config.src_test_suite_root, Utf8Path::new(""))
202        .unwrap_or_else(|reason| {
203            panic!("Could not read tests from {}: {reason}", cx.config.src_test_suite_root)
204        });
205
206    let TestCollector { tests, found_path_stems, poisoned } = collector;
207
208    if poisoned {
209        eprintln!();
210        panic!("there are errors in tests");
211    }
212
213    check_for_overlapping_test_paths(&found_path_stems);
214
215    tests
216}
217
218/// Returns the most recent last-modified timestamp from among the input files
219/// that are considered relevant to all tests (e.g. the compiler, std, and
220/// compiletest itself).
221///
222/// (Some of these inputs aren't actually relevant to _all_ tests, but they are
223/// common to some subset of tests, and are hopefully unlikely to be modified
224/// while working on other tests.)
225fn common_inputs_stamp(config: &Config) -> Stamp {
226    let src_root = &config.src_root;
227
228    let mut stamp = Stamp::from_path(&config.rustc_path);
229
230    // Relevant pretty printer files
231    let pretty_printer_files = [
232        "src/etc/rust_types.py",
233        "src/etc/gdb_load_rust_pretty_printers.py",
234        "src/etc/gdb_lookup.py",
235        "src/etc/gdb_providers.py",
236        "src/etc/lldb_batchmode",
237        "src/etc/lldb_lookup.py",
238        "src/etc/lldb_providers.py",
239    ];
240    for file in &pretty_printer_files {
241        let path = src_root.join(file);
242        stamp.add_path(&path);
243    }
244
245    stamp.add_dir(&src_root.join("src/etc/natvis"));
246
247    stamp.add_dir(&config.target_run_lib_path);
248
249    if let Some(ref rustdoc_path) = config.rustdoc_path {
250        stamp.add_path(&rustdoc_path);
251        stamp.add_path(&src_root.join("src/etc/htmldocck.py"));
252    }
253
254    // Re-run coverage tests if the `coverage-dump` tool was modified,
255    // because its output format might have changed.
256    if let Some(coverage_dump_path) = &config.coverage_dump_path {
257        stamp.add_path(coverage_dump_path)
258    }
259
260    stamp.add_dir(&src_root.join("src/tools/run-make-support"));
261
262    // Compiletest itself.
263    stamp.add_dir(&src_root.join("src/tools/compiletest"));
264
265    stamp
266}
267
268/// Returns a list of modified/untracked test files that should be run when
269/// the `--only-modified` flag is in use.
270///
271/// (Might be inaccurate in some cases.)
272fn modified_tests(config: &Config, dir: &Utf8Path) -> Result<Vec<Utf8PathBuf>, String> {
273    // If `--only-modified` wasn't passed, the list of modified tests won't be
274    // used for anything, so avoid some work and just return an empty list.
275    if !config.only_modified {
276        return Ok(vec![]);
277    }
278
279    let files = get_git_modified_files(
280        &config.git_config(),
281        Some(dir.as_std_path()),
282        &vec!["rs", "stderr", "fixed"],
283    )?;
284    // Add new test cases to the list, it will be convenient in daily development.
285    let untracked_files = get_git_untracked_files(Some(dir.as_std_path()))?.unwrap_or(vec![]);
286
287    let all_paths = [&files[..], &untracked_files[..]].concat();
288    let full_paths = {
289        let mut full_paths: Vec<Utf8PathBuf> = all_paths
290            .into_iter()
291            .map(|f| Utf8PathBuf::from(f).with_extension("").with_extension("rs"))
292            .filter_map(
293                |f| if Utf8Path::new(&f).exists() { f.canonicalize_utf8().ok() } else { None },
294            )
295            .collect();
296        full_paths.dedup();
297        full_paths.sort_unstable();
298        full_paths
299    };
300    Ok(full_paths)
301}
302
303/// Recursively scans a directory to find test files and create test structures
304/// that will be handed over to the executor.
305fn collect_tests_from_dir(
306    cx: &TestCollectorCx,
307    dir: &Utf8Path,
308    relative_dir_path: &Utf8Path,
309) -> io::Result<TestCollector> {
310    // Ignore directories that contain a file named `compiletest-ignore-dir`.
311    if dir.join("compiletest-ignore-dir").exists() {
312        return Ok(TestCollector::new());
313    }
314
315    let mut components = dir.components().rev();
316    if let Some(Utf8Component::Normal(last)) = components.next()
317        && let Some(("assembly" | "codegen", backend)) = last.split_once('-')
318        && let Some(Utf8Component::Normal(parent)) = components.next()
319        && parent == "tests"
320        && let Ok(backend) = backend.parse::<CodegenBackend>()
321        && backend != cx.config.default_codegen_backend
322    {
323        // We ignore asm tests which don't match the current codegen backend.
324        warning!(
325            "Ignoring tests in `{dir}` because they don't match the configured codegen \
326             backend (`{}`)",
327            cx.config.default_codegen_backend.as_str(),
328        );
329        return Ok(TestCollector::new());
330    }
331
332    // For run-make tests, a "test file" is actually a directory that contains an `rmake.rs`.
333    if cx.config.mode == TestMode::RunMake {
334        let mut collector = TestCollector::new();
335        if dir.join("rmake.rs").exists() {
336            let paths = TestPaths {
337                file: dir.to_path_buf(),
338                relative_dir: relative_dir_path.parent().unwrap().to_path_buf(),
339            };
340            make_test(cx, &mut collector, &paths);
341            // This directory is a test, so don't try to find other tests inside it.
342            return Ok(collector);
343        }
344    }
345
346    // If we find a test foo/bar.rs, we have to build the
347    // output directory `$build/foo` so we can write
348    // `$build/foo/bar` into it. We do this *now* in this
349    // sequential loop because otherwise, if we do it in the
350    // tests themselves, they race for the privilege of
351    // creating the directories and sometimes fail randomly.
352    let build_dir = output_relative_path(&cx.config, relative_dir_path);
353    fs::create_dir_all(&build_dir).unwrap();
354
355    // Add each `.rs` file as a test, and recurse further on any
356    // subdirectories we find, except for `auxiliary` directories.
357    // FIXME: this walks full tests tree, even if we have something to ignore
358    // use walkdir/ignore like in tidy?
359    fs::read_dir(dir.as_std_path())?
360        .par_bridge()
361        .map(|file| {
362            let mut collector = TestCollector::new();
363            let file = file?;
364            let file_path = Utf8PathBuf::try_from(file.path()).unwrap();
365            let file_name = file_path.file_name().unwrap();
366
367            if is_test(file_name)
368                && (!cx.config.only_modified || cx.modified_tests.contains(&file_path))
369            {
370                // We found a test file, so create the corresponding test structures.
371                debug!(%file_path, "found test file");
372
373                // Record the stem of the test file, to check for overlaps later.
374                let rel_test_path = relative_dir_path.join(file_path.file_stem().unwrap());
375                collector.found_path_stems.insert(rel_test_path);
376
377                let paths =
378                    TestPaths { file: file_path, relative_dir: relative_dir_path.to_path_buf() };
379                make_test(cx, &mut collector, &paths);
380            } else if file_path.is_dir() {
381                // Recurse to find more tests in a subdirectory.
382                let relative_file_path = relative_dir_path.join(file_name);
383                if file_name != "auxiliary" {
384                    debug!(%file_path, "found directory");
385                    collector.merge(collect_tests_from_dir(cx, &file_path, &relative_file_path)?);
386                }
387            } else {
388                debug!(%file_path, "found other file/directory");
389            }
390            Ok(collector)
391        })
392        .reduce(
393            || Ok(TestCollector::new()),
394            |a, b| {
395                let mut a = a?;
396                a.merge(b?);
397                Ok(a)
398            },
399        )
400}
401
402/// Returns true if `file_name` looks like a proper test file name.
403fn is_test(file_name: &str) -> bool {
404    if !file_name.ends_with(".rs") {
405        return false;
406    }
407
408    // `.`, `#`, and `~` are common temp-file prefixes.
409    let invalid_prefixes = &[".", "#", "~"];
410    !invalid_prefixes.iter().any(|p| file_name.starts_with(p))
411}
412
413/// For a single test file, creates one or more test structures (one per revision) that can be
414/// handed over to the executor to run, possibly in parallel.
415fn make_test(cx: &TestCollectorCx, collector: &mut TestCollector, testpaths: &TestPaths) {
416    // For run-make tests, each "test file" is actually a _directory_ containing an `rmake.rs`. But
417    // for the purposes of directive parsing, we want to look at that recipe file, not the directory
418    // itself.
419    let test_path = if cx.config.mode == TestMode::RunMake {
420        testpaths.file.join("rmake.rs")
421    } else {
422        testpaths.file.clone()
423    };
424
425    // Scan the test file to discover its revisions, if any.
426    let file_contents =
427        fs::read_to_string(&test_path).expect("reading test file for directives should succeed");
428    let file_directives = FileDirectives::from_file_contents(&test_path, &file_contents);
429
430    if let Err(message) = directives::do_early_directives_check(cx.config.mode, &file_directives) {
431        // FIXME(Zalathar): Overhaul compiletest error handling so that we
432        // don't have to resort to ad-hoc panics everywhere.
433        panic!("directives check failed:\n{message}");
434    }
435    let early_props = EarlyProps::from_file_directives(&cx.config, &file_directives);
436
437    // Normally we create one structure per revision, with two exceptions:
438    // - If a test doesn't use revisions, create a dummy revision (None) so that
439    //   the test can still run.
440    // - Incremental tests inherently can't run their revisions in parallel, so
441    //   we treat them like non-revisioned tests here. Incremental revisions are
442    //   handled internally by `runtest::run` instead.
443    let revisions = if early_props.revisions.is_empty() || cx.config.mode == TestMode::Incremental {
444        vec![None]
445    } else {
446        early_props.revisions.iter().map(|r| Some(r.as_str())).collect()
447    };
448
449    // For each revision (or the sole dummy revision), create and append a
450    // `CollectedTest` that can be handed over to the test executor.
451    collector.tests.extend(revisions.into_iter().map(|revision| {
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, revision);
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            revision,
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, revision)
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        let revision = revision.map(str::to_owned);
490
491        CollectedTest { desc, config, testpaths, revision }
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, revision: Option<&str>) -> Utf8PathBuf {
498    output_base_dir(config, testpaths, revision).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    revision: Option<&str>,
556) -> bool {
557    let stamp_file_path = stamp_file_path(&cx.config, testpaths, revision);
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);
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, 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    revision: Option<&str>,
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 config.debugger {
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        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}