about summary refs log tree commit diff
diff options
context:
space:
mode:
authorGuillaume Gomez <guillaume.gomez@huawei.com>2024-06-08 22:55:52 +0200
committerGuillaume Gomez <guillaume.gomez@huawei.com>2024-08-13 20:14:53 +0200
commit96051f20e2124bf5254dfbe8deeaf7a635170d85 (patch)
tree0b653c62b07906412351a73b0eafb0c3f4591709
parent39f029a852418952716cbd28f8d0d922584198e2 (diff)
downloadrust-96051f20e2124bf5254dfbe8deeaf7a635170d85.tar.gz
rust-96051f20e2124bf5254dfbe8deeaf7a635170d85.zip
Split standalone and mergeable doctests
-rw-r--r--src/librustdoc/doctest.rs363
-rw-r--r--src/librustdoc/doctest/make.rs19
-rw-r--r--src/librustdoc/doctest/markdown.rs8
-rw-r--r--src/librustdoc/doctest/runner.rs188
-rw-r--r--src/librustdoc/doctest/rust.rs12
-rw-r--r--src/librustdoc/html/markdown.rs4
6 files changed, 440 insertions, 154 deletions
diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs
index 65ddaedf26c..f00aef491f5 100644
--- a/src/librustdoc/doctest.rs
+++ b/src/librustdoc/doctest.rs
@@ -1,5 +1,6 @@
 mod make;
 mod markdown;
+mod runner;
 mod rust;
 
 use std::fs::File;
@@ -164,40 +165,54 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, options: RustdocOptions) -> Result<()
     let args_path = temp_dir.path().join("rustdoc-cfgs");
     crate::wrap_return(dcx, generate_args_file(&args_path, &options))?;
 
-    // FIXME: use mergeable tests!
-    let (standalone_tests, unused_extern_reports, compiling_test_count) =
-        interface::run_compiler(config, |compiler| {
-            compiler.enter(|queries| {
-                let collector = queries.global_ctxt()?.enter(|tcx| {
-                    let crate_name = tcx.crate_name(LOCAL_CRATE).to_string();
-                    let crate_attrs = tcx.hir().attrs(CRATE_HIR_ID);
-                    let opts = scrape_test_config(crate_name, crate_attrs, args_path);
-                    let enable_per_target_ignores = options.enable_per_target_ignores;
-
-                    let mut collector = CreateRunnableDoctests::new(options, opts);
-                    let hir_collector = HirCollector::new(
-                        &compiler.sess,
-                        tcx.hir(),
-                        ErrorCodes::from(compiler.sess.opts.unstable_features.is_nightly_build()),
-                        enable_per_target_ignores,
-                        tcx,
-                    );
-                    let tests = hir_collector.collect_crate();
-                    tests.into_iter().for_each(|t| collector.add_test(t));
-
-                    collector
-                });
-                if compiler.sess.dcx().has_errors().is_some() {
-                    FatalError.raise();
-                }
+    let CreateRunnableDoctests {
+        standalone_tests,
+        mergeable_tests,
+        rustdoc_options,
+        opts,
+        unused_extern_reports,
+        compiling_test_count,
+        ..
+    } = interface::run_compiler(config, |compiler| {
+        compiler.enter(|queries| {
+            let collector = queries.global_ctxt()?.enter(|tcx| {
+                let crate_name = tcx.crate_name(LOCAL_CRATE).to_string();
+                let crate_attrs = tcx.hir().attrs(CRATE_HIR_ID);
+                let opts = scrape_test_config(crate_name, crate_attrs, args_path);
+                let enable_per_target_ignores = options.enable_per_target_ignores;
+
+                let mut collector = CreateRunnableDoctests::new(options, opts);
+                let hir_collector = HirCollector::new(
+                    &compiler.sess,
+                    tcx.hir(),
+                    ErrorCodes::from(compiler.sess.opts.unstable_features.is_nightly_build()),
+                    enable_per_target_ignores,
+                    tcx,
+                );
+                let tests = hir_collector.collect_crate();
+                tests.into_iter().for_each(|t| collector.add_test(t));
+
+                collector
+            });
+            if compiler.sess.dcx().has_errors().is_some() {
+                FatalError.raise();
+            }
 
-                let unused_extern_reports = collector.unused_extern_reports.clone();
-                let compiling_test_count = collector.compiling_test_count.load(Ordering::SeqCst);
-                Ok((collector.standalone_tests, unused_extern_reports, compiling_test_count))
-            })
-        })?;
+            Ok(collector)
+        })
+    })?;
+
+    run_tests(
+        test_args,
+        nocapture,
+        opts,
+        rustdoc_options,
+        &unused_extern_reports,
+        standalone_tests,
+        mergeable_tests,
+    );
 
-    run_tests(test_args, nocapture, standalone_tests);
+    let compiling_test_count = compiling_test_count.load(Ordering::SeqCst);
 
     // Collect and warn about unused externs, but only if we've gotten
     // reports for each doctest
@@ -243,14 +258,74 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, options: RustdocOptions) -> Result<()
 pub(crate) fn run_tests(
     mut test_args: Vec<String>,
     nocapture: bool,
-    mut tests: Vec<test::TestDescAndFn>,
+    opts: GlobalTestOptions,
+    rustdoc_options: RustdocOptions,
+    unused_extern_reports: &Arc<Mutex<Vec<UnusedExterns>>>,
+    mut standalone_tests: Vec<test::TestDescAndFn>,
+    mut mergeable_tests: FxHashMap<Edition, Vec<(DocTest, ScrapedDoctest)>>,
 ) {
     test_args.insert(0, "rustdoctest".to_string());
     if nocapture {
         test_args.push("--nocapture".to_string());
     }
-    tests.sort_by(|a, b| a.desc.name.as_slice().cmp(&b.desc.name.as_slice()));
-    test::test_main(&test_args, tests, None);
+
+    let mut nb_errors = 0;
+
+    for (edition, mut doctests) in mergeable_tests {
+        if doctests.is_empty() {
+            continue;
+        }
+        doctests.sort_by(|(_, a), (_, b)| a.name.cmp(&b.name));
+        let outdir = Arc::clone(&doctests[0].outdir);
+
+        let mut tests_runner = runner::DocTestRunner::new();
+
+        let rustdoc_test_options = IndividualTestOptions::new(
+            &rustdoc_options,
+            format!("merged_doctest"),
+            PathBuf::from(r"doctest.rs"),
+        );
+
+        for (doctest, scraped_test) in &doctests {
+            tests_runner.add_test(doctest, scraped_test);
+        }
+        if let Ok(success) =
+            tests_runner.run_tests(rustdoc_test_options, edition, &opts, &test_args, &outdir)
+        {
+            if !success {
+                nb_errors += 1;
+            }
+            continue;
+        } else {
+            // We failed to compile all compatible tests as one so we push them into the
+            // `standalone_tests` doctests.
+            debug!("Failed to compile compatible doctests for edition {} all at once", edition);
+            for (doctest, scraped_test) in doctests {
+                doctest.generate_unique_doctest(
+                    &scraped_test.text,
+                    scraped_test.langstr.test_harness,
+                    &opts,
+                    Some(&opts.crate_name),
+                );
+                standalone_tests.push(generate_test_desc_and_fn(
+                    doctest,
+                    scraped_test,
+                    opts.clone(),
+                    rustdoc_test_options.clone(),
+                    unused_extern_reports.clone(),
+                ));
+            }
+        }
+    }
+
+    if !standalone_tests.is_empty() {
+        standalone_tests.sort_by(|a, b| a.desc.name.as_slice().cmp(&b.desc.name.as_slice()));
+        test::test_main(&test_args, standalone_tests, None);
+    }
+    if nb_errors != 0 {
+        // libtest::ERROR_EXIT_CODE is not public but it's the same value.
+        std::process::exit(101);
+    }
 }
 
 // Look for `#![doc(test(no_crate_inject))]`, used by crates in the std facade.
@@ -365,7 +440,10 @@ struct RunnableDoctest {
     full_test_line_offset: usize,
     test_opts: IndividualTestOptions,
     global_opts: GlobalTestOptions,
-    scraped_test: ScrapedDoctest,
+    langstr: LangString,
+    line: usize,
+    edition: Edition,
+    no_run: bool,
 }
 
 fn run_test(
@@ -374,8 +452,7 @@ fn run_test(
     supports_color: bool,
     report_unused_externs: impl Fn(UnusedExterns),
 ) -> Result<(), TestFailure> {
-    let scraped_test = &doctest.scraped_test;
-    let langstr = &scraped_test.langstr;
+    let langstr = &doctest.langstr;
     // Make sure we emit well-formed executable names for our target.
     let rust_out = add_exe_suffix("rust_out".to_owned(), &rustdoc_options.target);
     let output_file = doctest.test_opts.outdir.path().join(rust_out);
@@ -392,11 +469,11 @@ fn run_test(
         compiler.arg(format!("--sysroot={}", sysroot.display()));
     }
 
-    compiler.arg("--edition").arg(&scraped_test.edition(rustdoc_options).to_string());
+    compiler.arg("--edition").arg(&doctest.edition.to_string());
     compiler.env("UNSTABLE_RUSTDOC_TEST_PATH", &doctest.test_opts.path);
     compiler.env(
         "UNSTABLE_RUSTDOC_TEST_LINE",
-        format!("{}", scraped_test.line as isize - doctest.full_test_line_offset as isize),
+        format!("{}", doctest.line as isize - doctest.full_test_line_offset as isize),
     );
     compiler.arg("-o").arg(&output_file);
     if langstr.test_harness {
@@ -409,10 +486,7 @@ fn run_test(
         compiler.arg("-Z").arg("unstable-options");
     }
 
-    if scraped_test.no_run(rustdoc_options)
-        && !langstr.compile_fail
-        && rustdoc_options.persist_doctests.is_none()
-    {
+    if doctest.no_run && !langstr.compile_fail && rustdoc_options.persist_doctests.is_none() {
         // FIXME: why does this code check if it *shouldn't* persist doctests
         //        -- shouldn't it be the negation?
         compiler.arg("--emit=metadata");
@@ -493,8 +567,7 @@ fn run_test(
                 // We used to check if the output contained "error[{}]: " but since we added the
                 // colored output, we can't anymore because of the color escape characters before
                 // the ":".
-                let missing_codes: Vec<String> = scraped_test
-                    .langstr
+                let missing_codes: Vec<String> = langstr
                     .error_codes
                     .iter()
                     .filter(|err| !out.contains(&format!("error[{err}]")))
@@ -511,7 +584,7 @@ fn run_test(
         }
     }
 
-    if scraped_test.no_run(rustdoc_options) {
+    if doctest.no_run {
         return Ok(());
     }
 
@@ -600,9 +673,27 @@ struct ScrapedDoctest {
     logical_path: Vec<String>,
     langstr: LangString,
     text: String,
+    name: String,
 }
 
 impl ScrapedDoctest {
+    fn new(
+        filename: FileName,
+        line: usize,
+        logical_path: Vec<String>,
+        langstr: LangString,
+        text: String,
+    ) -> Self {
+        let mut item_path = logical_path.join("::");
+        item_path.retain(|c| c != ' ');
+        if !item_path.is_empty() {
+            item_path.push(' ');
+        }
+        let name =
+            format!("{} - {item_path}(line {line})", filename.prefer_remapped_unconditionaly());
+
+        Self { filename, line, logical_path, langstr, text, name }
+    }
     fn edition(&self, opts: &RustdocOptions) -> Edition {
         self.langstr.edition.unwrap_or(opts.edition)
     }
@@ -641,18 +732,32 @@ impl CreateRunnableDoctests {
         }
     }
 
-    fn generate_name(&self, filename: &FileName, line: usize, logical_path: &[String]) -> String {
-        let mut item_path = logical_path.join("::");
-        item_path.retain(|c| c != ' ');
-        if !item_path.is_empty() {
-            item_path.push(' ');
-        }
-        format!("{} - {item_path}(line {line})", filename.prefer_remapped_unconditionaly())
-    }
-
     fn add_test(&mut self, scraped_test: ScrapedDoctest) {
+        // For example `module/file.rs` would become `module_file_rs`
+        let file = scraped_test
+            .filename
+            .prefer_local()
+            .to_string_lossy()
+            .chars()
+            .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
+            .collect::<String>();
+        let test_id = format!(
+            "{file}_{line}_{number}",
+            file = file,
+            line = scraped_test.line,
+            number = {
+                // Increases the current test number, if this file already
+                // exists or it creates a new entry with a test number of 0.
+                self.visited_tests
+                    .entry((file.clone(), scraped_test.line))
+                    .and_modify(|v| *v += 1)
+                    .or_insert(0)
+            },
+        );
+
         let edition = scraped_test.edition(&self.rustdoc_options);
-        let doctest = DocTest::new(&scraped_test.text, Some(&self.opts.crate_name), edition);
+        let doctest =
+            DocTest::new(&scraped_test.text, Some(&self.opts.crate_name), edition, test_id);
         let is_standalone = scraped_test.langstr.compile_fail
             || scraped_test.langstr.test_harness
             || self.rustdoc_options.nocapture
@@ -671,87 +776,77 @@ impl CreateRunnableDoctests {
         test: DocTest,
         scraped_test: ScrapedDoctest,
     ) -> test::TestDescAndFn {
-        let name = self.generate_name(
-            &scraped_test.filename,
-            scraped_test.line,
-            &scraped_test.logical_path,
-        );
-        let opts = self.opts.clone();
-        let target_str = self.rustdoc_options.target.to_string();
-        let unused_externs = self.unused_extern_reports.clone();
         if !scraped_test.langstr.compile_fail {
             self.compiling_test_count.fetch_add(1, Ordering::SeqCst);
         }
 
-        let path = match &scraped_test.filename {
-            FileName::Real(path) => {
-                if let Some(local_path) = path.local_path() {
-                    local_path.to_path_buf()
-                } else {
-                    // Somehow we got the filename from the metadata of another crate, should never happen
-                    unreachable!("doctest from a different crate");
-                }
-            }
-            _ => PathBuf::from(r"doctest.rs"),
-        };
+        generate_test_desc_and_fn(
+            test,
+            scraped_test,
+            self.opts.clone(),
+            self.rustdoc_options.clone(),
+            self.unused_extern_reports.clone(),
+        )
+    }
+}
 
-        // For example `module/file.rs` would become `module_file_rs`
-        let file = scraped_test
-            .filename
-            .prefer_local()
-            .to_string_lossy()
-            .chars()
-            .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
-            .collect::<String>();
-        let test_id = format!(
-            "{file}_{line}_{number}",
-            file = file,
-            line = scraped_test.line,
-            number = {
-                // Increases the current test number, if this file already
-                // exists or it creates a new entry with a test number of 0.
-                self.visited_tests
-                    .entry((file.clone(), scraped_test.line))
-                    .and_modify(|v| *v += 1)
-                    .or_insert(0)
-            },
-        );
+fn generate_test_desc_and_fn(
+    test: DocTest,
+    scraped_test: ScrapedDoctest,
+    opts: GlobalTestOptions,
+    rustdoc_options: IndividualTestOptions,
+    unused_externs: Arc<Mutex<Vec<UnusedExterns>>>,
+) -> test::TestDescAndFn {
+    let target_str = rustdoc_options.target.to_string();
 
-        let rustdoc_options = self.rustdoc_options.clone();
-        let rustdoc_test_options = IndividualTestOptions::new(&self.rustdoc_options, test_id, path);
-
-        debug!("creating test {name}: {}", scraped_test.text);
-        test::TestDescAndFn {
-            desc: test::TestDesc {
-                name: test::DynTestName(name),
-                ignore: match scraped_test.langstr.ignore {
-                    Ignore::All => true,
-                    Ignore::None => false,
-                    Ignore::Some(ref ignores) => ignores.iter().any(|s| target_str.contains(s)),
-                },
-                ignore_message: None,
-                source_file: "",
-                start_line: 0,
-                start_col: 0,
-                end_line: 0,
-                end_col: 0,
-                // compiler failures are test failures
-                should_panic: test::ShouldPanic::No,
-                compile_fail: scraped_test.langstr.compile_fail,
-                no_run: scraped_test.no_run(&rustdoc_options),
-                test_type: test::TestType::DocTest,
-            },
-            testfn: test::DynTestFn(Box::new(move || {
-                doctest_run_fn(
-                    rustdoc_test_options,
-                    opts,
-                    test,
-                    scraped_test,
-                    rustdoc_options,
-                    unused_externs,
-                )
-            })),
+    let path = match &scraped_test.filename {
+        FileName::Real(path) => {
+            if let Some(local_path) = path.local_path() {
+                local_path.to_path_buf()
+            } else {
+                // Somehow we got the filename from the metadata of another crate, should never happen
+                unreachable!("doctest from a different crate");
+            }
         }
+        _ => PathBuf::from(r"doctest.rs"),
+    };
+
+    let name = &test.name;
+    let rustdoc_test_options =
+        IndividualTestOptions::new(&rustdoc_options, test.test_id.clone(), path);
+    // let rustdoc_options_clone = rustdoc_options.clone();
+
+    debug!("creating test {name}: {}", scraped_test.text);
+    test::TestDescAndFn {
+        desc: test::TestDesc {
+            name: test::DynTestName(name),
+            ignore: match scraped_test.langstr.ignore {
+                Ignore::All => true,
+                Ignore::None => false,
+                Ignore::Some(ref ignores) => ignores.iter().any(|s| target_str.contains(s)),
+            },
+            ignore_message: None,
+            source_file: "",
+            start_line: 0,
+            start_col: 0,
+            end_line: 0,
+            end_col: 0,
+            // compiler failures are test failures
+            should_panic: test::ShouldPanic::No,
+            compile_fail: scraped_test.langstr.compile_fail,
+            no_run: scraped_test.no_run(&rustdoc_options),
+            test_type: test::TestType::DocTest,
+        },
+        testfn: test::DynTestFn(Box::new(move || {
+            doctest_run_fn(
+                rustdoc_test_options,
+                opts,
+                test,
+                scraped_test,
+                rustdoc_options,
+                unused_externs,
+            )
+        })),
     }
 }
 
@@ -770,7 +865,6 @@ fn doctest_run_fn(
         &scraped_test.text,
         scraped_test.langstr.test_harness,
         &global_opts,
-        Some(&test_opts.test_id),
         Some(&global_opts.crate_name),
     );
     let runnable_test = RunnableDoctest {
@@ -778,7 +872,10 @@ fn doctest_run_fn(
         full_test_line_offset,
         test_opts,
         global_opts,
-        scraped_test,
+        langstr: scraped_test.langstr.clone(),
+        line: scraped_test.line,
+        edition: scraped_test.edition(&rustdoc_options),
+        no_run: scraped_test.no_run(&rustdoc_options),
     };
     let res =
         run_test(runnable_test, &rustdoc_options, doctest.supports_color, report_unused_externs);
diff --git a/src/librustdoc/doctest/make.rs b/src/librustdoc/doctest/make.rs
index 759a3e31b23..c1d1e45ff04 100644
--- a/src/librustdoc/doctest/make.rs
+++ b/src/librustdoc/doctest/make.rs
@@ -24,10 +24,17 @@ pub(crate) struct DocTest {
     pub(crate) crate_attrs: String,
     pub(crate) crates: String,
     pub(crate) everything_else: String,
+    pub(crate) test_id: Option<String>,
 }
 
 impl DocTest {
-    pub(crate) fn new(source: &str, crate_name: Option<&str>, edition: Edition) -> Self {
+    pub(crate) fn new(
+        source: &str,
+        crate_name: Option<&str>,
+        edition: Edition,
+        // If `test_id` is `None`, it means we're generating code for a code example "run" link.
+        test_id: Option<String>,
+    ) -> Self {
         let (crate_attrs, everything_else, crates) = partition_source(source, edition);
         let mut supports_color = false;
 
@@ -45,6 +52,7 @@ impl DocTest {
                 crates,
                 everything_else,
                 already_has_extern_crate: false,
+                test_id,
             };
         };
         Self {
@@ -54,6 +62,7 @@ impl DocTest {
             crates,
             everything_else,
             already_has_extern_crate,
+            test_id,
         }
     }
 
@@ -64,8 +73,6 @@ impl DocTest {
         test_code: &str,
         dont_insert_main: bool,
         opts: &GlobalTestOptions,
-        // If `test_id` is `None`, it means we're generating code for a code example "run" link.
-        test_id: Option<&str>,
         crate_name: Option<&str>,
     ) -> (String, usize) {
         let mut line_offset = 0;
@@ -118,12 +125,12 @@ impl DocTest {
             let returns_result = everything_else.ends_with("(())");
             // Give each doctest main function a unique name.
             // This is for example needed for the tooling around `-C instrument-coverage`.
-            let inner_fn_name = if let Some(test_id) = test_id {
+            let inner_fn_name = if let Some(ref test_id) = self.test_id {
                 format!("_doctest_main_{test_id}")
             } else {
                 "_inner".into()
             };
-            let inner_attr = if test_id.is_some() { "#[allow(non_snake_case)] " } else { "" };
+            let inner_attr = if self.test_id.is_some() { "#[allow(non_snake_case)] " } else { "" };
             let (main_pre, main_post) = if returns_result {
                 (
                     format!(
@@ -131,7 +138,7 @@ impl DocTest {
                     ),
                     format!("\n}} {inner_fn_name}().unwrap() }}"),
                 )
-            } else if test_id.is_some() {
+            } else if self.test_id.is_some() {
                 (
                     format!("fn main() {{ {inner_attr}fn {inner_fn_name}() {{\n",),
                     format!("\n}} {inner_fn_name}() }}"),
diff --git a/src/librustdoc/doctest/markdown.rs b/src/librustdoc/doctest/markdown.rs
index ff2adffe5c2..a5514857fff 100644
--- a/src/librustdoc/doctest/markdown.rs
+++ b/src/librustdoc/doctest/markdown.rs
@@ -22,13 +22,7 @@ impl DoctestVisitor for MdCollector {
         let filename = self.filename.clone();
         // First line of Markdown is line 1.
         let line = 1 + rel_line.offset();
-        self.tests.push(ScrapedDoctest {
-            filename,
-            line,
-            logical_path: self.cur_path.clone(),
-            langstr: config,
-            text: test,
-        });
+        self.tests.push(ScrapedDoctest::new(filename, line, self.cur_path.clone(), config, test));
     }
 
     fn visit_header(&mut self, name: &str, level: u32) {
diff --git a/src/librustdoc/doctest/runner.rs b/src/librustdoc/doctest/runner.rs
new file mode 100644
index 00000000000..a672bb1bd9b
--- /dev/null
+++ b/src/librustdoc/doctest/runner.rs
@@ -0,0 +1,188 @@
+use rustc_data_structures::fx::FxHashSet;
+use rustc_span::edition::Edition;
+
+use std::fmt::Write;
+use std::sync::{Arc, Mutex};
+
+use crate::doctest::{
+    run_test, DirState, DocTest, GlobalTestOptions, IndividualTestOptions, RunnableDoctest,
+    RustdocOptions, ScrapedDoctest, TestFailure, UnusedExterns,
+};
+use crate::html::markdown::LangString;
+
+/// Convenient type to merge compatible doctests into one.
+pub(crate) struct DocTestRunner {
+    crate_attrs: FxHashSet<String>,
+    ids: String,
+    output: String,
+    supports_color: bool,
+    nb_tests: usize,
+    doctests: Vec<DocTest>,
+}
+
+impl DocTestRunner {
+    pub(crate) fn new() -> Self {
+        Self {
+            crate_attrs: FxHashSet::default(),
+            ids: String::new(),
+            output: String::new(),
+            supports_color: true,
+            nb_tests: 0,
+            doctests: Vec::with_capacity(10),
+        }
+    }
+
+    pub(crate) fn add_test(&mut self, doctest: &DocTest, scraped_test: &ScrapedDoctest) {
+        if !doctest.ignore {
+            for line in doctest.crate_attrs.split('\n') {
+                self.crate_attrs.insert(line.to_string());
+            }
+        }
+        if !self.ids.is_empty() {
+            self.ids.push(',');
+        }
+        self.ids.push_str(&format!(
+            "{}::TEST",
+            generate_mergeable_doctest(doctest, scraped_test, self.nb_tests, &mut self.output),
+        ));
+        self.supports_color &= doctest.supports_color;
+        self.nb_tests += 1;
+        self.doctests.push(doctest);
+    }
+
+    pub(crate) fn run_tests(
+        &mut self,
+        test_options: IndividualTestOptions,
+        edition: Edition,
+        opts: &GlobalTestOptions,
+        test_args: &[String],
+        outdir: &Arc<DirState>,
+        rustdoc_options: &RustdocOptions,
+        unused_externs: Arc<Mutex<Vec<UnusedExterns>>>,
+    ) -> Result<bool, ()> {
+        let mut code = "\
+#![allow(unused_extern_crates)]
+#![allow(internal_features)]
+#![feature(test)]
+#![feature(rustc_attrs)]
+#![feature(coverage_attribute)]\n"
+            .to_string();
+
+        for crate_attr in &self.crate_attrs {
+            code.push_str(crate_attr);
+            code.push('\n');
+        }
+
+        DocTest::push_attrs(&mut code, opts, &mut 0);
+        code.push_str("extern crate test;\n");
+
+        let test_args =
+            test_args.iter().map(|arg| format!("{arg:?}.to_string(),")).collect::<String>();
+        write!(
+            code,
+            "\
+{output}
+#[rustc_main]
+#[coverage(off)]
+fn main() {{
+test::test_main(&[{test_args}], vec![{ids}], None);
+}}",
+            output = self.output,
+            ids = self.ids,
+        )
+        .expect("failed to generate test code");
+        // let out_dir = build_test_dir(outdir, true, "");
+        let runnable_test = RunnableDoctest {
+            full_test_code: code,
+            full_test_line_offset: 0,
+            test_opts: test_options,
+            global_opts: opts.clone(),
+            langstr: LangString::default(),
+            line: 0,
+            edition,
+            no_run: false,
+        };
+        let ret = run_test(runnable_test, rustdoc_options, self.supports_color, unused_externs);
+        if let Err(TestFailure::CompileError) = ret { Err(()) } else { Ok(ret.is_ok()) }
+    }
+}
+
+/// Push new doctest content into `output`. Returns the test ID for this doctest.
+fn generate_mergeable_doctest(
+    doctest: &DocTest,
+    scraped_test: &ScrapedDoctest,
+    id: usize,
+    output: &mut String,
+) -> String {
+    let test_id = format!("__doctest_{id}");
+
+    if doctest.ignore {
+        // We generate nothing else.
+        writeln!(output, "mod {test_id} {{\n").unwrap();
+    } else {
+        writeln!(output, "mod {test_id} {{\n{}", doctest.crates).unwrap();
+        if doctest.main_fn_span.is_some() {
+            output.push_str(&doctest.everything_else);
+        } else {
+            let returns_result = if doctest.everything_else.trim_end().ends_with("(())") {
+                "-> Result<(), impl core::fmt::Debug>"
+            } else {
+                ""
+            };
+            write!(
+                output,
+                "\
+    fn main() {returns_result} {{
+        {}
+    }}",
+                doctest.everything_else
+            )
+            .unwrap();
+        }
+    }
+    writeln!(
+        output,
+        "
+#[rustc_test_marker = {test_name:?}]
+pub const TEST: test::TestDescAndFn = test::TestDescAndFn {{
+    desc: test::TestDesc {{
+        name: test::StaticTestName({test_name:?}),
+        ignore: {ignore},
+        ignore_message: None,
+        source_file: {file:?},
+        start_line: {line},
+        start_col: 0,
+        end_line: 0,
+        end_col: 0,
+        compile_fail: false,
+        no_run: {no_run},
+        should_panic: test::ShouldPanic::{should_panic},
+        test_type: test::TestType::UnitTest,
+    }},
+    testfn: test::StaticTestFn(
+        #[coverage(off)]
+        || test::assert_test_result({runner}),
+    )
+}};
+}}",
+        test_name = scraped_test.name,
+        ignore = scraped_test.langstr.ignore,
+        file = scraped_test.file,
+        line = scraped_test.line,
+        no_run = scraped_test.langstr.no_run,
+        should_panic = if !scraped_test.langstr.no_run && scraped_test.langstr.should_panic {
+            "Yes"
+        } else {
+            "No"
+        },
+        // Setting `no_run` to `true` in `TestDesc` still makes the test run, so we simply
+        // don't give it the function to run.
+        runner = if scraped_test.langstr.no_run || scraped_test.langstr.ignore {
+            "Ok::<(), String>(())"
+        } else {
+            "self::main()"
+        },
+    )
+    .unwrap();
+    test_id
+}
diff --git a/src/librustdoc/doctest/rust.rs b/src/librustdoc/doctest/rust.rs
index f179f3aa1c9..17c29ba413a 100644
--- a/src/librustdoc/doctest/rust.rs
+++ b/src/librustdoc/doctest/rust.rs
@@ -51,13 +51,13 @@ impl RustCollector {
 impl DoctestVisitor for RustCollector {
     fn visit_test(&mut self, test: String, config: LangString, rel_line: MdRelLine) {
         let line = self.get_base_line() + rel_line.offset();
-        self.tests.push(ScrapedDoctest {
-            filename: self.get_filename(),
+        self.tests.push(ScrapedDoctest::new(
+            self.get_filename(),
             line,
-            logical_path: self.cur_path.clone(),
-            langstr: config,
-            text: test,
-        });
+            self.cur_path.clone(),
+            config,
+            test,
+        ));
     }
 
     fn visit_header(&mut self, _name: &str, _level: u32) {}
diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs
index c1f5f3d7e23..a268a2d704e 100644
--- a/src/librustdoc/html/markdown.rs
+++ b/src/librustdoc/html/markdown.rs
@@ -297,8 +297,8 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for CodeBlocks<'_, 'a, I> {
                 attrs: vec![],
                 args_file: PathBuf::new(),
             };
-            let doctest = doctest::DocTest::new(&test, krate, edition);
-            let (test, _) = doctest.generate_unique_doctest(&test, false, &opts, None, krate);
+            let doctest = doctest::DocTest::new(&test, krate, edition, None);
+            let (test, _) = doctest.generate_unique_doctest(&test, false, &opts, krate);
             let channel = if test.contains("#![feature(") { "&amp;version=nightly" } else { "" };
 
             let test_escaped = small_url_encode(test);