about summary refs log tree commit diff
path: root/src/tools
diff options
context:
space:
mode:
authorLeón Orell Valerian Liehr <me@fmease.dev>2025-07-13 07:21:25 +0200
committerGitHub <noreply@github.com>2025-07-13 07:21:25 +0200
commitbb330bbc50852bc432417bfc659e82e58d26cb28 (patch)
tree45a06fb8502e736604b6fec51c6f6ea3d02928f4 /src/tools
parent2cbd0bc27cc51d985ff625059bcb011976bcd048 (diff)
parent6f8a7f00005c8b1c13c0fc0f903f0f0013530c14 (diff)
downloadrust-bb330bbc50852bc432417bfc659e82e58d26cb28.tar.gz
rust-bb330bbc50852bc432417bfc659e82e58d26cb28.zip
Rollup merge of #143823 - jieyouxu:compiletest-maintenance-5, r=Kobzol
[COMPILETEST-UNTANGLE 5/N] Test mode adjustments and other assorted cleanups

This is part of a patch series to untangle `compiletest` to hopefully nudge it towards being more maintainable.

This PR should contain no functional changes modulo the removed debugger version warning.

- Commit 1: Removes a very outdated debugger version warning.
- Commit 2: Moves `string_enum` out of `common` into `util` module.
- Commit 3: Remove `#[derive(Default)` for `Mode` and `Config`. It is very important for correctness that we *don't* `#[derive(Default)]`, because there are no sensible defaults, so stop pretending there is.
- Commit 4: Rename `Mode` -> `TestMode`, because I would like to introduce a `TestSuite` enum to stop using stringly-typed test suite names where test mode names can be the same as test suite names, and we also have a bunch of other "modes" in compiletest. Make this as unambiguous as possible. A corollary is that now it's more natural to reference via intra-doc links as ``[`TestMode`]``.
- Commit 5: Ditto on `TestSuite`, stop glob-reexporting `TestMode::*` variants, and always use `EnumName::VariantName` form.
- Commit 6: Apparently, `src/tools/rustdoc-gui-test/` depends on `compiletest` for `//@ {compile,run}-paths` directive parsing and extraction, which involves creating a dummy `compiletest` config (hence the existence of the default impls removed in Commit 3). Make this a specific associated function with a FIXME pointing to rust-lang/rust#143827 as I think this setup is quite questionable.

Commits {4, 5} are also intended to help improve the self-consistency in nomenclature used within compiletest.

Best reviewed commit-by-commit.
Diffstat (limited to 'src/tools')
-rw-r--r--src/tools/compiletest/src/common.rs182
-rw-r--r--src/tools/compiletest/src/debuggers.rs11
-rw-r--r--src/tools/compiletest/src/directives.rs40
-rw-r--r--src/tools/compiletest/src/directives/tests.rs4
-rw-r--r--src/tools/compiletest/src/lib.rs20
-rw-r--r--src/tools/compiletest/src/runtest.rs113
-rw-r--r--src/tools/compiletest/src/tests.rs6
-rw-r--r--src/tools/compiletest/src/util.rs39
-rw-r--r--src/tools/rustdoc-gui-test/src/main.rs6
9 files changed, 253 insertions, 168 deletions
diff --git a/src/tools/compiletest/src/common.rs b/src/tools/compiletest/src/common.rs
index 7122746fa87..849f44031b7 100644
--- a/src/tools/compiletest/src/common.rs
+++ b/src/tools/compiletest/src/common.rs
@@ -1,63 +1,20 @@
 use std::collections::{BTreeSet, HashMap, HashSet};
+use std::iter;
 use std::process::Command;
-use std::str::FromStr;
 use std::sync::OnceLock;
-use std::{fmt, iter};
 
 use build_helper::git::GitConfig;
 use camino::{Utf8Path, Utf8PathBuf};
 use semver::Version;
 use serde::de::{Deserialize, Deserializer, Error as _};
 
-pub use self::Mode::*;
 use crate::executor::{ColorConfig, OutputFormat};
 use crate::fatal;
-use crate::util::{Utf8PathBufExt, add_dylib_path};
-
-macro_rules! string_enum {
-    ($(#[$meta:meta])* $vis:vis enum $name:ident { $($variant:ident => $repr:expr,)* }) => {
-        $(#[$meta])*
-        $vis enum $name {
-            $($variant,)*
-        }
-
-        impl $name {
-            $vis const VARIANTS: &'static [Self] = &[$(Self::$variant,)*];
-            $vis const STR_VARIANTS: &'static [&'static str] = &[$(Self::$variant.to_str(),)*];
-
-            $vis const fn to_str(&self) -> &'static str {
-                match self {
-                    $(Self::$variant => $repr,)*
-                }
-            }
-        }
-
-        impl fmt::Display for $name {
-            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-                fmt::Display::fmt(self.to_str(), f)
-            }
-        }
-
-        impl FromStr for $name {
-            type Err = String;
-
-            fn from_str(s: &str) -> Result<Self, Self::Err> {
-                match s {
-                    $($repr => Ok(Self::$variant),)*
-                    _ => Err(format!(concat!("unknown `", stringify!($name), "` variant: `{}`"), s)),
-                }
-            }
-        }
-    }
-}
-
-// Make the macro visible outside of this module, for tests.
-#[cfg(test)]
-pub(crate) use string_enum;
+use crate::util::{Utf8PathBufExt, add_dylib_path, string_enum};
 
 string_enum! {
     #[derive(Clone, Copy, PartialEq, Debug)]
-    pub enum Mode {
+    pub enum TestMode {
         Pretty => "pretty",
         DebugInfo => "debuginfo",
         Codegen => "codegen",
@@ -76,18 +33,12 @@ string_enum! {
     }
 }
 
-impl Default for Mode {
-    fn default() -> Self {
-        Mode::Ui
-    }
-}
-
-impl Mode {
+impl TestMode {
     pub fn aux_dir_disambiguator(self) -> &'static str {
         // Pretty-printing tests could run concurrently, and if they do,
         // they need to keep their output segregated.
         match self {
-            Pretty => ".pretty",
+            TestMode::Pretty => ".pretty",
             _ => "",
         }
     }
@@ -96,7 +47,7 @@ impl Mode {
         // Coverage tests use the same test files for multiple test modes,
         // so each mode should have a separate output directory.
         match self {
-            CoverageMap | CoverageRun => self.to_str(),
+            TestMode::CoverageMap | TestMode::CoverageRun => self.to_str(),
             _ => "",
         }
     }
@@ -193,9 +144,9 @@ pub enum Sanitizer {
 ///
 /// FIXME: audit these options to make sure we are not hashing less than necessary for build stamp
 /// (for changed test detection).
-#[derive(Debug, Default, Clone)]
+#[derive(Debug, Clone)]
 pub struct Config {
-    /// Some test [`Mode`]s support [snapshot testing], where a *reference snapshot* of outputs (of
+    /// Some [`TestMode`]s support [snapshot testing], where a *reference snapshot* of outputs (of
     /// `stdout`, `stderr`, or other form of artifacts) can be compared to the *actual output*.
     ///
     /// This option can be set to `true` to update the *reference snapshots* in-place, otherwise
@@ -317,20 +268,20 @@ pub struct Config {
     /// FIXME: reconsider this string; this is hashed for test build stamp.
     pub stage_id: String,
 
-    /// The test [`Mode`]. E.g. [`Mode::Ui`]. Each test mode can correspond to one or more test
+    /// The [`TestMode`]. E.g. [`TestMode::Ui`]. Each test mode can correspond to one or more test
     /// suites.
     ///
     /// FIXME: stop using stringly-typed test suites!
-    pub mode: Mode,
+    pub mode: TestMode,
 
     /// The test suite.
     ///
-    /// Example: `tests/ui/` is the "UI" test *suite*, which happens to also be of the [`Mode::Ui`]
-    /// test *mode*.
+    /// Example: `tests/ui/` is the "UI" test *suite*, which happens to also be of the
+    /// [`TestMode::Ui`] test *mode*.
     ///
     /// Note that the same test directory (e.g. `tests/coverage/`) may correspond to multiple test
-    /// modes, e.g. `tests/coverage/` can be run under both [`Mode::CoverageRun`] and
-    /// [`Mode::CoverageMap`].
+    /// modes, e.g. `tests/coverage/` can be run under both [`TestMode::CoverageRun`] and
+    /// [`TestMode::CoverageMap`].
     ///
     /// FIXME: stop using stringly-typed test suites!
     pub suite: String,
@@ -586,8 +537,8 @@ pub struct Config {
     // Configuration for various run-make tests frobbing things like C compilers or querying about
     // various LLVM component information.
     //
-    // FIXME: this really should be better packaged together.
-    // FIXME: these need better docs, e.g. for *host*, or for *target*?
+    // FIXME: this really should be better packaged together. FIXME: these need better docs, e.g.
+    // for *host*, or for *target*?
     pub cc: String,
     pub cxx: String,
     pub cflags: String,
@@ -653,6 +604,107 @@ pub struct Config {
 }
 
 impl Config {
+    /// Incomplete config intended for `src/tools/rustdoc-gui-test` **only** as
+    /// `src/tools/rustdoc-gui-test` wants to reuse `compiletest`'s directive -> test property
+    /// handling for `//@ {compile,run}-flags`, do not use for any other purpose.
+    ///
+    /// FIXME(#143827): this setup feels very hacky. It so happens that `tests/rustdoc-gui/`
+    /// **only** uses `//@ {compile,run}-flags` for now and not any directives that actually rely on
+    /// info that is assumed available in a fully populated [`Config`].
+    pub fn incomplete_for_rustdoc_gui_test() -> Config {
+        // FIXME(#143827): spelling this out intentionally, because this is questionable.
+        //
+        // For instance, `//@ ignore-stage1` will not work at all.
+        Config {
+            mode: TestMode::Rustdoc,
+
+            // Dummy values.
+            edition: Default::default(),
+            bless: Default::default(),
+            fail_fast: Default::default(),
+            compile_lib_path: Utf8PathBuf::default(),
+            run_lib_path: Utf8PathBuf::default(),
+            rustc_path: Utf8PathBuf::default(),
+            cargo_path: Default::default(),
+            stage0_rustc_path: Default::default(),
+            rustdoc_path: Default::default(),
+            coverage_dump_path: Default::default(),
+            python: Default::default(),
+            jsondocck_path: Default::default(),
+            jsondoclint_path: Default::default(),
+            llvm_filecheck: Default::default(),
+            llvm_bin_dir: Default::default(),
+            run_clang_based_tests_with: Default::default(),
+            src_root: Utf8PathBuf::default(),
+            src_test_suite_root: Utf8PathBuf::default(),
+            build_root: Utf8PathBuf::default(),
+            build_test_suite_root: Utf8PathBuf::default(),
+            sysroot_base: Utf8PathBuf::default(),
+            stage: Default::default(),
+            stage_id: String::default(),
+            suite: Default::default(),
+            debugger: Default::default(),
+            run_ignored: Default::default(),
+            with_rustc_debug_assertions: Default::default(),
+            with_std_debug_assertions: Default::default(),
+            filters: Default::default(),
+            skip: Default::default(),
+            filter_exact: Default::default(),
+            force_pass_mode: Default::default(),
+            run: Default::default(),
+            runner: Default::default(),
+            host_rustcflags: Default::default(),
+            target_rustcflags: Default::default(),
+            rust_randomized_layout: Default::default(),
+            optimize_tests: Default::default(),
+            target: Default::default(),
+            host: Default::default(),
+            cdb: Default::default(),
+            cdb_version: Default::default(),
+            gdb: Default::default(),
+            gdb_version: Default::default(),
+            lldb_version: Default::default(),
+            llvm_version: Default::default(),
+            system_llvm: Default::default(),
+            android_cross_path: Default::default(),
+            adb_path: Default::default(),
+            adb_test_dir: Default::default(),
+            adb_device_status: Default::default(),
+            lldb_python_dir: Default::default(),
+            verbose: Default::default(),
+            format: Default::default(),
+            color: Default::default(),
+            remote_test_client: Default::default(),
+            compare_mode: Default::default(),
+            rustfix_coverage: Default::default(),
+            has_html_tidy: Default::default(),
+            has_enzyme: Default::default(),
+            channel: Default::default(),
+            git_hash: Default::default(),
+            cc: Default::default(),
+            cxx: Default::default(),
+            cflags: Default::default(),
+            cxxflags: Default::default(),
+            ar: Default::default(),
+            target_linker: Default::default(),
+            host_linker: Default::default(),
+            llvm_components: Default::default(),
+            nodejs: Default::default(),
+            npm: Default::default(),
+            force_rerun: Default::default(),
+            only_modified: Default::default(),
+            target_cfgs: Default::default(),
+            builtin_cfg_names: Default::default(),
+            supported_crate_types: Default::default(),
+            nocapture: Default::default(),
+            nightly_branch: Default::default(),
+            git_merge_commit_email: Default::default(),
+            profiler_runtime: Default::default(),
+            diff_command: Default::default(),
+            minicore_path: Default::default(),
+        }
+    }
+
     /// FIXME: this run scheme is... confusing.
     pub fn run_enabled(&self) -> bool {
         self.run.unwrap_or_else(|| {
diff --git a/src/tools/compiletest/src/debuggers.rs b/src/tools/compiletest/src/debuggers.rs
index 0edc3d82d4f..8afe3289fa4 100644
--- a/src/tools/compiletest/src/debuggers.rs
+++ b/src/tools/compiletest/src/debuggers.rs
@@ -51,17 +51,6 @@ pub(crate) fn configure_gdb(config: &Config) -> Option<Arc<Config>> {
 pub(crate) fn configure_lldb(config: &Config) -> Option<Arc<Config>> {
     config.lldb_python_dir.as_ref()?;
 
-    // FIXME: this is super old
-    if let Some(350) = config.lldb_version {
-        println!(
-            "WARNING: The used version of LLDB (350) has a \
-             known issue that breaks debuginfo tests. See \
-             issue #32520 for more information. Skipping all \
-             LLDB-based tests!",
-        );
-        return None;
-    }
-
     Some(Arc::new(Config { debugger: Some(Debugger::Lldb), ..config.clone() }))
 }
 
diff --git a/src/tools/compiletest/src/directives.rs b/src/tools/compiletest/src/directives.rs
index a6242cf0c22..7fadb4dae2a 100644
--- a/src/tools/compiletest/src/directives.rs
+++ b/src/tools/compiletest/src/directives.rs
@@ -9,7 +9,7 @@ use camino::{Utf8Path, Utf8PathBuf};
 use semver::Version;
 use tracing::*;
 
-use crate::common::{Config, Debugger, FailMode, Mode, PassMode};
+use crate::common::{Config, Debugger, FailMode, PassMode, TestMode};
 use crate::debuggers::{extract_cdb_version, extract_gdb_version};
 use crate::directives::auxiliary::{AuxProps, parse_and_update_aux};
 use crate::directives::needs::CachedNeedsConditions;
@@ -328,7 +328,7 @@ impl TestProps {
         props.exec_env.push(("RUSTC".to_string(), config.rustc_path.to_string()));
 
         match (props.pass_mode, props.fail_mode) {
-            (None, None) if config.mode == Mode::Ui => props.fail_mode = Some(FailMode::Check),
+            (None, None) if config.mode == TestMode::Ui => props.fail_mode = Some(FailMode::Check),
             (Some(_), Some(_)) => panic!("cannot use a *-fail and *-pass mode together"),
             _ => {}
         }
@@ -609,11 +609,11 @@ impl TestProps {
             self.failure_status = Some(101);
         }
 
-        if config.mode == Mode::Incremental {
+        if config.mode == TestMode::Incremental {
             self.incremental = true;
         }
 
-        if config.mode == Mode::Crashes {
+        if config.mode == TestMode::Crashes {
             // we don't want to pollute anything with backtrace-files
             // also turn off backtraces in order to save some execution
             // time on the tests; we only need to know IF it crashes
@@ -641,11 +641,11 @@ impl TestProps {
     fn update_fail_mode(&mut self, ln: &str, config: &Config) {
         let check_ui = |mode: &str| {
             // Mode::Crashes may need build-fail in order to trigger llvm errors or stack overflows
-            if config.mode != Mode::Ui && config.mode != Mode::Crashes {
+            if config.mode != TestMode::Ui && config.mode != TestMode::Crashes {
                 panic!("`{}-fail` directive is only supported in UI tests", mode);
             }
         };
-        if config.mode == Mode::Ui && config.parse_name_directive(ln, "compile-fail") {
+        if config.mode == TestMode::Ui && config.parse_name_directive(ln, "compile-fail") {
             panic!("`compile-fail` directive is useless in UI tests");
         }
         let fail_mode = if config.parse_name_directive(ln, "check-fail") {
@@ -669,10 +669,10 @@ impl TestProps {
 
     fn update_pass_mode(&mut self, ln: &str, revision: Option<&str>, config: &Config) {
         let check_no_run = |s| match (config.mode, s) {
-            (Mode::Ui, _) => (),
-            (Mode::Crashes, _) => (),
-            (Mode::Codegen, "build-pass") => (),
-            (Mode::Incremental, _) => {
+            (TestMode::Ui, _) => (),
+            (TestMode::Crashes, _) => (),
+            (TestMode::Codegen, "build-pass") => (),
+            (TestMode::Incremental, _) => {
                 if revision.is_some() && !self.revisions.iter().all(|r| r.starts_with("cfail")) {
                     panic!("`{s}` directive is only supported in `cfail` incremental tests")
                 }
@@ -715,7 +715,7 @@ impl TestProps {
     pub fn update_add_core_stubs(&mut self, ln: &str, config: &Config) {
         let add_core_stubs = config.parse_name_directive(ln, directives::ADD_CORE_STUBS);
         if add_core_stubs {
-            if !matches!(config.mode, Mode::Ui | Mode::Codegen | Mode::Assembly) {
+            if !matches!(config.mode, TestMode::Ui | TestMode::Codegen | TestMode::Assembly) {
                 panic!(
                     "`add-core-stubs` is currently only supported for ui, codegen and assembly test modes"
                 );
@@ -833,7 +833,7 @@ pub(crate) struct CheckDirectiveResult<'ln> {
 
 pub(crate) fn check_directive<'a>(
     directive_ln: &'a str,
-    mode: Mode,
+    mode: TestMode,
     original_line: &str,
 ) -> CheckDirectiveResult<'a> {
     let (directive_name, post) = directive_ln.split_once([':', ' ']).unwrap_or((directive_ln, ""));
@@ -842,11 +842,11 @@ pub(crate) fn check_directive<'a>(
     let is_known = |s: &str| {
         KNOWN_DIRECTIVE_NAMES.contains(&s)
             || match mode {
-                Mode::Rustdoc | Mode::RustdocJson => {
+                TestMode::Rustdoc | TestMode::RustdocJson => {
                     original_line.starts_with("//@")
                         && match mode {
-                            Mode::Rustdoc => KNOWN_HTMLDOCCK_DIRECTIVE_NAMES,
-                            Mode::RustdocJson => KNOWN_JSONDOCCK_DIRECTIVE_NAMES,
+                            TestMode::Rustdoc => KNOWN_HTMLDOCCK_DIRECTIVE_NAMES,
+                            TestMode::RustdocJson => KNOWN_JSONDOCCK_DIRECTIVE_NAMES,
                             _ => unreachable!(),
                         }
                         .contains(&s)
@@ -868,7 +868,7 @@ pub(crate) fn check_directive<'a>(
 const COMPILETEST_DIRECTIVE_PREFIX: &str = "//@";
 
 fn iter_directives(
-    mode: Mode,
+    mode: TestMode,
     _suite: &str,
     poisoned: &mut bool,
     testfile: &Utf8Path,
@@ -883,7 +883,7 @@ fn iter_directives(
     // specify them manually in every test file.
     //
     // FIXME(jieyouxu): I feel like there's a better way to do this, leaving for later.
-    if mode == Mode::CoverageRun {
+    if mode == TestMode::CoverageRun {
         let extra_directives: &[&str] = &[
             "needs-profiler-runtime",
             // FIXME(pietroalbini): this test currently does not work on cross-compiled targets
@@ -964,7 +964,7 @@ impl Config {
             ["CHECK", "COM", "NEXT", "SAME", "EMPTY", "NOT", "COUNT", "DAG", "LABEL"];
 
         if let Some(raw) = self.parse_name_value_directive(line, "revisions") {
-            if self.mode == Mode::RunMake {
+            if self.mode == TestMode::RunMake {
                 panic!("`run-make` tests do not support revisions: {}", testfile);
             }
 
@@ -981,7 +981,7 @@ impl Config {
                     );
                 }
 
-                if matches!(self.mode, Mode::Assembly | Mode::Codegen | Mode::MirOpt)
+                if matches!(self.mode, TestMode::Assembly | TestMode::Codegen | TestMode::MirOpt)
                     && FILECHECK_FORBIDDEN_REVISION_NAMES.contains(&revision)
                 {
                     panic!(
@@ -1443,7 +1443,7 @@ pub(crate) fn make_test_description<R: Read>(
     // since we run the pretty printer across all tests by default.
     // If desired, we could add a `should-fail-pretty` annotation.
     let should_panic = match config.mode {
-        crate::common::Pretty => ShouldPanic::No,
+        TestMode::Pretty => ShouldPanic::No,
         _ if should_fail => ShouldPanic::Yes,
         _ => ShouldPanic::No,
     };
diff --git a/src/tools/compiletest/src/directives/tests.rs b/src/tools/compiletest/src/directives/tests.rs
index d4570f82677..9c5751b416b 100644
--- a/src/tools/compiletest/src/directives/tests.rs
+++ b/src/tools/compiletest/src/directives/tests.rs
@@ -7,7 +7,7 @@ use super::{
     DirectivesCache, EarlyProps, extract_llvm_version, extract_version_range, iter_directives,
     parse_normalize_rule,
 };
-use crate::common::{Config, Debugger, Mode};
+use crate::common::{Config, Debugger, TestMode};
 use crate::executor::{CollectedTestDesc, ShouldPanic};
 
 fn make_test_description<R: Read>(
@@ -785,7 +785,7 @@ fn threads_support() {
 
 fn run_path(poisoned: &mut bool, path: &Utf8Path, buf: &[u8]) {
     let rdr = std::io::Cursor::new(&buf);
-    iter_directives(Mode::Ui, "ui", poisoned, path, rdr, &mut |_| {});
+    iter_directives(TestMode::Ui, "ui", poisoned, path, rdr, &mut |_| {});
 }
 
 #[test]
diff --git a/src/tools/compiletest/src/lib.rs b/src/tools/compiletest/src/lib.rs
index 9819079e284..be82f8cb480 100644
--- a/src/tools/compiletest/src/lib.rs
+++ b/src/tools/compiletest/src/lib.rs
@@ -39,8 +39,8 @@ use walkdir::WalkDir;
 
 use self::directives::{EarlyProps, make_test_description};
 use crate::common::{
-    CompareMode, Config, Debugger, Mode, PassMode, TestPaths, UI_EXTENSIONS, expected_output_path,
-    output_base_dir, output_relative_path,
+    CompareMode, Config, Debugger, PassMode, TestMode, TestPaths, UI_EXTENSIONS,
+    expected_output_path, output_base_dir, output_relative_path,
 };
 use crate::directives::DirectivesCache;
 use crate::executor::{CollectedTest, ColorConfig, OutputFormat};
@@ -268,7 +268,7 @@ pub fn parse_config(args: Vec<String>) -> Config {
     let with_rustc_debug_assertions = matches.opt_present("with-rustc-debug-assertions");
     let with_std_debug_assertions = matches.opt_present("with-std-debug-assertions");
     let mode = matches.opt_str("mode").unwrap().parse().expect("invalid mode");
-    let has_html_tidy = if mode == Mode::Rustdoc {
+    let has_html_tidy = if mode == TestMode::Rustdoc {
         Command::new("tidy")
             .arg("--version")
             .stdout(Stdio::null())
@@ -279,7 +279,7 @@ pub fn parse_config(args: Vec<String>) -> Config {
         false
     };
     let has_enzyme = matches.opt_present("has-enzyme");
-    let filters = if mode == Mode::RunMake {
+    let filters = if mode == TestMode::RunMake {
         matches
             .free
             .iter()
@@ -545,7 +545,7 @@ pub fn run_tests(config: Arc<Config>) {
     unsafe { env::set_var("TARGET", &config.target) };
 
     let mut configs = Vec::new();
-    if let Mode::DebugInfo = config.mode {
+    if let TestMode::DebugInfo = config.mode {
         // Debugging emscripten code doesn't make sense today
         if !config.target.contains("emscripten") {
             match config.debugger {
@@ -783,7 +783,7 @@ fn collect_tests_from_dir(
     }
 
     // For run-make tests, a "test file" is actually a directory that contains an `rmake.rs`.
-    if cx.config.mode == Mode::RunMake {
+    if cx.config.mode == TestMode::RunMake {
         let mut collector = TestCollector::new();
         if dir.join("rmake.rs").exists() {
             let paths = TestPaths {
@@ -869,7 +869,7 @@ fn make_test(cx: &TestCollectorCx, collector: &mut TestCollector, testpaths: &Te
     // For run-make tests, each "test file" is actually a _directory_ containing an `rmake.rs`. But
     // for the purposes of directive parsing, we want to look at that recipe file, not the directory
     // itself.
-    let test_path = if cx.config.mode == Mode::RunMake {
+    let test_path = if cx.config.mode == TestMode::RunMake {
         testpaths.file.join("rmake.rs")
     } else {
         testpaths.file.clone()
@@ -884,7 +884,7 @@ fn make_test(cx: &TestCollectorCx, collector: &mut TestCollector, testpaths: &Te
     // - Incremental tests inherently can't run their revisions in parallel, so
     //   we treat them like non-revisioned tests here. Incremental revisions are
     //   handled internally by `runtest::run` instead.
-    let revisions = if early_props.revisions.is_empty() || cx.config.mode == Mode::Incremental {
+    let revisions = if early_props.revisions.is_empty() || cx.config.mode == TestMode::Incremental {
         vec![None]
     } else {
         early_props.revisions.iter().map(|r| Some(r.as_str())).collect()
@@ -1116,11 +1116,11 @@ fn check_for_overlapping_test_paths(found_path_stems: &HashSet<Utf8PathBuf>) {
 }
 
 pub fn early_config_check(config: &Config) {
-    if !config.has_html_tidy && config.mode == Mode::Rustdoc {
+    if !config.has_html_tidy && config.mode == TestMode::Rustdoc {
         warning!("`tidy` (html-tidy.org) is not installed; diffs will not be generated");
     }
 
-    if !config.profiler_runtime && config.mode == Mode::CoverageRun {
+    if !config.profiler_runtime && config.mode == TestMode::CoverageRun {
         let actioned = if config.bless { "blessed" } else { "checked" };
         warning!("profiler runtime is not available, so `.coverage` files won't be {actioned}");
         help!("try setting `profiler = true` in the `[build]` section of `bootstrap.toml`");
diff --git a/src/tools/compiletest/src/runtest.rs b/src/tools/compiletest/src/runtest.rs
index 933a32392bd..12111e9c6ef 100644
--- a/src/tools/compiletest/src/runtest.rs
+++ b/src/tools/compiletest/src/runtest.rs
@@ -16,11 +16,10 @@ use regex::{Captures, Regex};
 use tracing::*;
 
 use crate::common::{
-    Assembly, Codegen, CodegenUnits, CompareMode, Config, CoverageMap, CoverageRun, Crashes,
-    DebugInfo, Debugger, FailMode, Incremental, MirOpt, PassMode, Pretty, RunMake, Rustdoc,
-    RustdocJs, RustdocJson, TestPaths, UI_EXTENSIONS, UI_FIXED, UI_RUN_STDERR, UI_RUN_STDOUT,
-    UI_STDERR, UI_STDOUT, UI_SVG, UI_WINDOWS_SVG, Ui, expected_output_path, incremental_dir,
-    output_base_dir, output_base_name, output_testname_unique,
+    CompareMode, Config, Debugger, FailMode, PassMode, TestMode, TestPaths, UI_EXTENSIONS,
+    UI_FIXED, UI_RUN_STDERR, UI_RUN_STDOUT, UI_STDERR, UI_STDOUT, UI_SVG, UI_WINDOWS_SVG,
+    expected_output_path, incremental_dir, output_base_dir, output_base_name,
+    output_testname_unique,
 };
 use crate::compute_diff::{DiffLine, make_diff, write_diff, write_filtered_diff};
 use crate::directives::TestProps;
@@ -154,7 +153,7 @@ pub fn run(config: Arc<Config>, testpaths: &TestPaths, revision: Option<&str>) {
         cx.init_incremental_test();
     }
 
-    if config.mode == Incremental {
+    if config.mode == TestMode::Incremental {
         // Incremental tests are special because they cannot be run in
         // parallel.
         assert!(!props.revisions.is_empty(), "Incremental tests require revisions.");
@@ -203,7 +202,7 @@ pub fn compute_stamp_hash(config: &Config) -> String {
         None => {}
     }
 
-    if let Ui = config.mode {
+    if config.mode == TestMode::Ui {
         config.force_pass_mode.hash(&mut hash);
     }
 
@@ -251,25 +250,28 @@ impl<'test> TestCx<'test> {
     /// Code executed for each revision in turn (or, if there are no
     /// revisions, exactly once, with revision == None).
     fn run_revision(&self) {
-        if self.props.should_ice && self.config.mode != Incremental && self.config.mode != Crashes {
+        if self.props.should_ice
+            && self.config.mode != TestMode::Incremental
+            && self.config.mode != TestMode::Crashes
+        {
             self.fatal("cannot use should-ice in a test that is not cfail");
         }
         match self.config.mode {
-            Pretty => self.run_pretty_test(),
-            DebugInfo => self.run_debuginfo_test(),
-            Codegen => self.run_codegen_test(),
-            Rustdoc => self.run_rustdoc_test(),
-            RustdocJson => self.run_rustdoc_json_test(),
-            CodegenUnits => self.run_codegen_units_test(),
-            Incremental => self.run_incremental_test(),
-            RunMake => self.run_rmake_test(),
-            Ui => self.run_ui_test(),
-            MirOpt => self.run_mir_opt_test(),
-            Assembly => self.run_assembly_test(),
-            RustdocJs => self.run_rustdoc_js_test(),
-            CoverageMap => self.run_coverage_map_test(), // see self::coverage
-            CoverageRun => self.run_coverage_run_test(), // see self::coverage
-            Crashes => self.run_crash_test(),
+            TestMode::Pretty => self.run_pretty_test(),
+            TestMode::DebugInfo => self.run_debuginfo_test(),
+            TestMode::Codegen => self.run_codegen_test(),
+            TestMode::Rustdoc => self.run_rustdoc_test(),
+            TestMode::RustdocJson => self.run_rustdoc_json_test(),
+            TestMode::CodegenUnits => self.run_codegen_units_test(),
+            TestMode::Incremental => self.run_incremental_test(),
+            TestMode::RunMake => self.run_rmake_test(),
+            TestMode::Ui => self.run_ui_test(),
+            TestMode::MirOpt => self.run_mir_opt_test(),
+            TestMode::Assembly => self.run_assembly_test(),
+            TestMode::RustdocJs => self.run_rustdoc_js_test(),
+            TestMode::CoverageMap => self.run_coverage_map_test(), // see self::coverage
+            TestMode::CoverageRun => self.run_coverage_run_test(), // see self::coverage
+            TestMode::Crashes => self.run_crash_test(),
         }
     }
 
@@ -279,9 +281,13 @@ impl<'test> TestCx<'test> {
 
     fn should_run(&self, pm: Option<PassMode>) -> WillExecute {
         let test_should_run = match self.config.mode {
-            Ui if pm == Some(PassMode::Run) || self.props.fail_mode == Some(FailMode::Run) => true,
-            MirOpt if pm == Some(PassMode::Run) => true,
-            Ui | MirOpt => false,
+            TestMode::Ui
+                if pm == Some(PassMode::Run) || self.props.fail_mode == Some(FailMode::Run) =>
+            {
+                true
+            }
+            TestMode::MirOpt if pm == Some(PassMode::Run) => true,
+            TestMode::Ui | TestMode::MirOpt => false,
             mode => panic!("unimplemented for mode {:?}", mode),
         };
         if test_should_run { self.run_if_enabled() } else { WillExecute::No }
@@ -293,17 +299,17 @@ impl<'test> TestCx<'test> {
 
     fn should_run_successfully(&self, pm: Option<PassMode>) -> bool {
         match self.config.mode {
-            Ui | MirOpt => pm == Some(PassMode::Run),
+            TestMode::Ui | TestMode::MirOpt => pm == Some(PassMode::Run),
             mode => panic!("unimplemented for mode {:?}", mode),
         }
     }
 
     fn should_compile_successfully(&self, pm: Option<PassMode>) -> bool {
         match self.config.mode {
-            RustdocJs => true,
-            Ui => pm.is_some() || self.props.fail_mode > Some(FailMode::Build),
-            Crashes => false,
-            Incremental => {
+            TestMode::RustdocJs => true,
+            TestMode::Ui => pm.is_some() || self.props.fail_mode > Some(FailMode::Build),
+            TestMode::Crashes => false,
+            TestMode::Incremental => {
                 let revision =
                     self.revision.expect("incremental tests require a list of revisions");
                 if revision.starts_with("cpass")
@@ -349,7 +355,7 @@ impl<'test> TestCx<'test> {
             if proc_res.status.success() {
                 {
                     self.error(&format!("{} test did not emit an error", self.config.mode));
-                    if self.config.mode == crate::common::Mode::Ui {
+                    if self.config.mode == crate::common::TestMode::Ui {
                         println!("note: by default, ui tests are expected not to compile");
                     }
                     proc_res.fatal(None, || ());
@@ -892,7 +898,9 @@ impl<'test> TestCx<'test> {
 
     fn should_emit_metadata(&self, pm: Option<PassMode>) -> Emit {
         match (pm, self.props.fail_mode, self.config.mode) {
-            (Some(PassMode::Check), ..) | (_, Some(FailMode::Check), Ui) => Emit::Metadata,
+            (Some(PassMode::Check), ..) | (_, Some(FailMode::Check), TestMode::Ui) => {
+                Emit::Metadata
+            }
             _ => Emit::None,
         }
     }
@@ -926,7 +934,7 @@ impl<'test> TestCx<'test> {
         };
 
         let allow_unused = match self.config.mode {
-            Ui => {
+            TestMode::Ui => {
                 // UI tests tend to have tons of unused code as
                 // it's just testing various pieces of the compile, but we don't
                 // want to actually assert warnings about all this code. Instead
@@ -1021,7 +1029,7 @@ impl<'test> TestCx<'test> {
             .args(&self.props.compile_flags)
             .args(&self.props.doc_flags);
 
-        if self.config.mode == RustdocJson {
+        if self.config.mode == TestMode::RustdocJson {
             rustdoc.arg("--output-format").arg("json").arg("-Zunstable-options");
         }
 
@@ -1372,7 +1380,7 @@ impl<'test> TestCx<'test> {
             || self.is_vxworks_pure_static()
             || self.config.target.contains("bpf")
             || !self.config.target_cfg().dynamic_linking
-            || matches!(self.config.mode, CoverageMap | CoverageRun)
+            || matches!(self.config.mode, TestMode::CoverageMap | TestMode::CoverageRun)
         {
             // We primarily compile all auxiliary libraries as dynamic libraries
             // to avoid code size bloat and large binaries as much as possible
@@ -1562,14 +1570,14 @@ impl<'test> TestCx<'test> {
                 rustc.args(&["-Z", "incremental-verify-ich"]);
             }
 
-            if self.config.mode == CodegenUnits {
+            if self.config.mode == TestMode::CodegenUnits {
                 rustc.args(&["-Z", "human_readable_cgu_names"]);
             }
         }
 
         if self.config.optimize_tests && !is_rustdoc {
             match self.config.mode {
-                Ui => {
+                TestMode::Ui => {
                     // If optimize-tests is true we still only want to optimize tests that actually get
                     // executed and that don't specify their own optimization levels.
                     // Note: aux libs don't have a pass-mode, so they won't get optimized
@@ -1585,8 +1593,8 @@ impl<'test> TestCx<'test> {
                         rustc.arg("-O");
                     }
                 }
-                DebugInfo => { /* debuginfo tests must be unoptimized */ }
-                CoverageMap | CoverageRun => {
+                TestMode::DebugInfo => { /* debuginfo tests must be unoptimized */ }
+                TestMode::CoverageMap | TestMode::CoverageRun => {
                     // Coverage mappings and coverage reports are affected by
                     // optimization level, so they ignore the optimize-tests
                     // setting and set an optimization level in their mode's
@@ -1607,7 +1615,7 @@ impl<'test> TestCx<'test> {
         };
 
         match self.config.mode {
-            Incremental => {
+            TestMode::Incremental => {
                 // If we are extracting and matching errors in the new
                 // fashion, then you want JSON mode. Old-skool error
                 // patterns still match the raw compiler output.
@@ -1620,7 +1628,7 @@ impl<'test> TestCx<'test> {
                 rustc.arg("-Zui-testing");
                 rustc.arg("-Zdeduplicate-diagnostics=no");
             }
-            Ui => {
+            TestMode::Ui => {
                 if !self.props.compile_flags.iter().any(|s| s.starts_with("--error-format")) {
                     rustc.args(&["--error-format", "json"]);
                     rustc.args(&["--json", "future-incompat"]);
@@ -1633,7 +1641,7 @@ impl<'test> TestCx<'test> {
                 // FIXME: use this for other modes too, for perf?
                 rustc.arg("-Cstrip=debuginfo");
             }
-            MirOpt => {
+            TestMode::MirOpt => {
                 // We check passes under test to minimize the mir-opt test dump
                 // if files_for_miropt_test parses the passes, we dump only those passes
                 // otherwise we conservatively pass -Zdump-mir=all
@@ -1663,7 +1671,7 @@ impl<'test> TestCx<'test> {
 
                 set_mir_dump_dir(&mut rustc);
             }
-            CoverageMap => {
+            TestMode::CoverageMap => {
                 rustc.arg("-Cinstrument-coverage");
                 // These tests only compile to LLVM IR, so they don't need the
                 // profiler runtime to be present.
@@ -1673,23 +1681,28 @@ impl<'test> TestCx<'test> {
                 // by `compile-flags`.
                 rustc.arg("-Copt-level=2");
             }
-            CoverageRun => {
+            TestMode::CoverageRun => {
                 rustc.arg("-Cinstrument-coverage");
                 // Coverage reports are sometimes sensitive to optimizations,
                 // and the current snapshots assume `opt-level=2` unless
                 // overridden by `compile-flags`.
                 rustc.arg("-Copt-level=2");
             }
-            Assembly | Codegen => {
+            TestMode::Assembly | TestMode::Codegen => {
                 rustc.arg("-Cdebug-assertions=no");
             }
-            Crashes => {
+            TestMode::Crashes => {
                 set_mir_dump_dir(&mut rustc);
             }
-            CodegenUnits => {
+            TestMode::CodegenUnits => {
                 rustc.arg("-Zprint-mono-items");
             }
-            Pretty | DebugInfo | Rustdoc | RustdocJson | RunMake | RustdocJs => {
+            TestMode::Pretty
+            | TestMode::DebugInfo
+            | TestMode::Rustdoc
+            | TestMode::RustdocJson
+            | TestMode::RunMake
+            | TestMode::RustdocJs => {
                 // do not use JSON output
             }
         }
@@ -1962,7 +1975,7 @@ impl<'test> TestCx<'test> {
     /// The revision, ignored for incremental compilation since it wants all revisions in
     /// the same directory.
     fn safe_revision(&self) -> Option<&str> {
-        if self.config.mode == Incremental { None } else { self.revision }
+        if self.config.mode == TestMode::Incremental { None } else { self.revision }
     }
 
     /// Gets the absolute path to the directory where all output for the given
diff --git a/src/tools/compiletest/src/tests.rs b/src/tools/compiletest/src/tests.rs
index e3e4a81755d..174ec9381f6 100644
--- a/src/tools/compiletest/src/tests.rs
+++ b/src/tools/compiletest/src/tests.rs
@@ -67,11 +67,7 @@ fn is_test_test() {
 
 #[test]
 fn string_enums() {
-    // These imports are needed for the macro-generated code
-    use std::fmt;
-    use std::str::FromStr;
-
-    crate::common::string_enum! {
+    crate::util::string_enum! {
         #[derive(Clone, Copy, Debug, PartialEq)]
         enum Animal {
             Cat => "meow",
diff --git a/src/tools/compiletest/src/util.rs b/src/tools/compiletest/src/util.rs
index 202582bea8c..fb047548c45 100644
--- a/src/tools/compiletest/src/util.rs
+++ b/src/tools/compiletest/src/util.rs
@@ -104,3 +104,42 @@ macro_rules! static_regex {
     }};
 }
 pub(crate) use static_regex;
+
+macro_rules! string_enum {
+    ($(#[$meta:meta])* $vis:vis enum $name:ident { $($variant:ident => $repr:expr,)* }) => {
+        $(#[$meta])*
+        $vis enum $name {
+            $($variant,)*
+        }
+
+        impl $name {
+            $vis const VARIANTS: &'static [Self] = &[$(Self::$variant,)*];
+            $vis const STR_VARIANTS: &'static [&'static str] = &[$(Self::$variant.to_str(),)*];
+
+            $vis const fn to_str(&self) -> &'static str {
+                match self {
+                    $(Self::$variant => $repr,)*
+                }
+            }
+        }
+
+        impl ::std::fmt::Display for $name {
+            fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
+                ::std::fmt::Display::fmt(self.to_str(), f)
+            }
+        }
+
+        impl ::std::str::FromStr for $name {
+            type Err = String;
+
+            fn from_str(s: &str) -> Result<Self, Self::Err> {
+                match s {
+                    $($repr => Ok(Self::$variant),)*
+                    _ => Err(format!(concat!("unknown `", stringify!($name), "` variant: `{}`"), s)),
+                }
+            }
+        }
+    }
+}
+
+pub(crate) use string_enum;
diff --git a/src/tools/rustdoc-gui-test/src/main.rs b/src/tools/rustdoc-gui-test/src/main.rs
index 6461f38f527..0e35861fbf7 100644
--- a/src/tools/rustdoc-gui-test/src/main.rs
+++ b/src/tools/rustdoc-gui-test/src/main.rs
@@ -112,11 +112,7 @@ If you want to install the `browser-ui-test` dependency, run `npm install browse
                 .current_dir(path);
 
             if let Some(librs) = find_librs(entry.path()) {
-                let compiletest_c = compiletest::common::Config {
-                    edition: None,
-                    mode: compiletest::common::Mode::Rustdoc,
-                    ..Default::default()
-                };
+                let compiletest_c = compiletest::common::Config::incomplete_for_rustdoc_gui_test();
 
                 let test_props = TestProps::from_file(
                     &camino::Utf8PathBuf::try_from(librs).unwrap(),