From 29f31f16621f2402d92918896bf3b77d9e2a2683 Mon Sep 17 00:00:00 2001 From: Jynn Nelson Date: Tue, 2 Dec 2025 16:53:12 -0500 Subject: [PATCH 1/3] rustdoc: Add unstable `--merge-doctests=yes/no/auto` flag This is useful for changing the *default* for whether doctests are merged or not. Currently, that default is solely controlled by `edition = 2024`, which adds a high switching cost to get doctest merging. This flag allows opt-ing in even on earlier additions. Unlike the `edition = 2024` default, `--merge-doctests=yes` gives a hard error if merging fails instead of falling back to running standalone tests. The user has explicitly said they want merging, so we shouldn't silently do something else. `--merge-doctests=auto` is equivalent to the current 2024 edition behavior, but available on earlier editions. --- src/librustdoc/config.rs | 31 +++++++++++++++ src/librustdoc/doctest.rs | 34 ++++++++++------ src/librustdoc/doctest/make.rs | 39 ++++++++++++------- src/librustdoc/doctest/markdown.rs | 4 +- src/librustdoc/lib.rs | 10 ++++- .../output-default.stdout | 4 ++ tests/rustdoc-ui/doctest/force-merge-fail.rs | 18 +++++++++ .../doctest/force-merge-fail.stderr | 6 +++ tests/rustdoc-ui/doctest/force-merge.rs | 25 ++++++++++++ tests/rustdoc-ui/doctest/force-merge.stdout | 8 ++++ tests/rustdoc-ui/doctest/force-no-merge.rs | 24 ++++++++++++ .../rustdoc-ui/doctest/force-no-merge.stdout | 7 ++++ .../rustdoc-ui/doctest/merge-doctests-auto.rs | 28 +++++++++++++ .../doctest/merge-doctests-auto.stdout | 8 ++++ .../doctest/merge-doctests-invalid.rs | 2 + .../doctest/merge-doctests-invalid.stderr | 2 + .../doctest/merge-doctests-unstable.rs | 2 + .../doctest/merge-doctests-unstable.stderr | 2 + 18 files changed, 225 insertions(+), 29 deletions(-) create mode 100644 tests/rustdoc-ui/doctest/force-merge-fail.rs create mode 100644 tests/rustdoc-ui/doctest/force-merge-fail.stderr create mode 100644 tests/rustdoc-ui/doctest/force-merge.rs create mode 100644 tests/rustdoc-ui/doctest/force-merge.stdout create mode 100644 tests/rustdoc-ui/doctest/force-no-merge.rs create mode 100644 tests/rustdoc-ui/doctest/force-no-merge.stdout create mode 100644 tests/rustdoc-ui/doctest/merge-doctests-auto.rs create mode 100644 tests/rustdoc-ui/doctest/merge-doctests-auto.stdout create mode 100644 tests/rustdoc-ui/doctest/merge-doctests-invalid.rs create mode 100644 tests/rustdoc-ui/doctest/merge-doctests-invalid.stderr create mode 100644 tests/rustdoc-ui/doctest/merge-doctests-unstable.rs create mode 100644 tests/rustdoc-ui/doctest/merge-doctests-unstable.stderr diff --git a/src/librustdoc/config.rs b/src/librustdoc/config.rs index 5d16dff24c69a..e5a4593260a42 100644 --- a/src/librustdoc/config.rs +++ b/src/librustdoc/config.rs @@ -63,6 +63,15 @@ pub(crate) enum InputMode { HasFile(Input), } +/// Whether to run multiple doctests in the same binary. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] +pub(crate) enum MergeDoctests { + #[default] + Never, + Always, + Auto, +} + /// Configuration options for rustdoc. #[derive(Clone)] pub(crate) struct Options { @@ -121,6 +130,8 @@ pub(crate) struct Options { /// Optional path to persist the doctest executables to, defaults to a /// temporary directory if not set. pub(crate) persist_doctests: Option, + /// Whether to merge + pub(crate) merge_doctests: MergeDoctests, /// Runtool to run doctests with pub(crate) test_runtool: Option, /// Arguments to pass to the runtool @@ -801,6 +812,8 @@ impl Options { Ok(result) => result, Err(e) => dcx.fatal(format!("--merge option error: {e}")), }; + let merge_doctests = parse_merge_doctests(matches, edition, dcx); + tracing::debug!("merge_doctests: {merge_doctests:?}"); if generate_link_to_definition && (show_coverage || output_format != OutputFormat::Html) { dcx.struct_warn( @@ -852,6 +865,7 @@ impl Options { crate_version, test_run_directory, persist_doctests, + merge_doctests, test_runtool, test_runtool_args, test_builder, @@ -1048,3 +1062,20 @@ fn parse_merge(m: &getopts::Matches) -> Result { Some(_) => Err("argument to --merge must be `none`, `shared`, or `finalize`"), } } + +fn parse_merge_doctests( + m: &getopts::Matches, + edition: Edition, + dcx: DiagCtxtHandle<'_>, +) -> MergeDoctests { + match m.opt_str("merge-doctests").as_deref() { + Some("y") | Some("yes") | Some("on") | Some("true") => MergeDoctests::Always, + Some("n") | Some("no") | Some("off") | Some("false") => MergeDoctests::Never, + Some("auto") => MergeDoctests::Auto, + None if edition < Edition::Edition2024 => MergeDoctests::Never, + None => MergeDoctests::Auto, + Some(_) => { + dcx.fatal("argument to --merge-doctests must be a boolean (true/false) or 'auto'") + } + } +} diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index 481aa392007c8..cab65181c9401 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -33,7 +33,7 @@ use tempfile::{Builder as TempFileBuilder, TempDir}; use tracing::debug; use self::rust::HirCollector; -use crate::config::{Options as RustdocOptions, OutputFormat}; +use crate::config::{MergeDoctests, Options as RustdocOptions, OutputFormat}; use crate::html::markdown::{ErrorCodes, Ignore, LangString, MdRelLine}; use crate::lint::init_lints; @@ -265,6 +265,7 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions }; run_tests( + dcx, opts, &rustdoc_options, &unused_extern_reports, @@ -316,6 +317,7 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions } pub(crate) fn run_tests( + dcx: DiagCtxtHandle<'_>, opts: GlobalTestOptions, rustdoc_options: &Arc, unused_extern_reports: &Arc>>, @@ -368,6 +370,13 @@ pub(crate) fn run_tests( } continue; } + + if rustdoc_options.merge_doctests == MergeDoctests::Always { + let mut diag = dcx.struct_fatal("failed to merge doctests"); + diag.note("requested explicitly on the command line with `--merge-doctests=yes`"); + diag.emit(); + } + // 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); @@ -645,9 +654,9 @@ fn run_test( // tested as standalone tests. return (Duration::default(), Err(TestFailure::CompileError)); } - if !rustdoc_options.no_capture { - // If `no_capture` is disabled, then we don't display rustc's output when compiling - // the merged doctests. + if !rustdoc_options.no_capture && rustdoc_options.merge_doctests == MergeDoctests::Auto { + // If `no_capture` is disabled, and we might fallback to standalone tests, then we don't + // display rustc's output when compiling the merged doctests. compiler.stderr(Stdio::null()); } // bundled tests are an rlib, loaded by a separate runner executable @@ -728,10 +737,12 @@ fn run_test( // tested as standalone tests. return (instant.elapsed(), Err(TestFailure::CompileError)); } - if !rustdoc_options.no_capture { - // If `no_capture` is disabled, then we don't display rustc's output when compiling - // the merged doctests. + if !rustdoc_options.no_capture && rustdoc_options.merge_doctests == MergeDoctests::Auto { + // If `no_capture` is disabled and we're autodetecting whether to merge, + // we don't display rustc's output when compiling the merged doctests. runner_compiler.stderr(Stdio::null()); + } else { + runner_compiler.stderr(Stdio::inherit()); } runner_compiler.arg("--error-format=short"); debug!("compiler invocation for doctest runner: {runner_compiler:?}"); @@ -888,7 +899,7 @@ impl IndividualTestOptions { DirState::Perm(path) } else { - DirState::Temp(get_doctest_dir().expect("rustdoc needs a tempdir")) + DirState::Temp(get_doctest_dir(options).expect("rustdoc needs a tempdir")) }; Self { outdir, path: test_path } @@ -977,21 +988,20 @@ struct CreateRunnableDocTests { visited_tests: FxHashMap<(String, usize), usize>, unused_extern_reports: Arc>>, compiling_test_count: AtomicUsize, - can_merge_doctests: bool, + can_merge_doctests: MergeDoctests, } impl CreateRunnableDocTests { fn new(rustdoc_options: RustdocOptions, opts: GlobalTestOptions) -> CreateRunnableDocTests { - let can_merge_doctests = rustdoc_options.edition >= Edition::Edition2024; CreateRunnableDocTests { standalone_tests: Vec::new(), mergeable_tests: FxIndexMap::default(), - rustdoc_options: Arc::new(rustdoc_options), opts, visited_tests: FxHashMap::default(), unused_extern_reports: Default::default(), compiling_test_count: AtomicUsize::new(0), - can_merge_doctests, + can_merge_doctests: rustdoc_options.merge_doctests, + rustdoc_options: Arc::new(rustdoc_options), } } diff --git a/src/librustdoc/doctest/make.rs b/src/librustdoc/doctest/make.rs index e9f5024e494d1..1f5956168d7e8 100644 --- a/src/librustdoc/doctest/make.rs +++ b/src/librustdoc/doctest/make.rs @@ -20,6 +20,7 @@ use rustc_span::{DUMMY_SP, FileName, Span, kw}; use tracing::debug; use super::GlobalTestOptions; +use crate::config::MergeDoctests; use crate::display::Joined as _; use crate::html::markdown::LangString; @@ -41,7 +42,7 @@ pub(crate) struct BuildDocTestBuilder<'a> { source: &'a str, crate_name: Option<&'a str>, edition: Edition, - can_merge_doctests: bool, + can_merge_doctests: MergeDoctests, // If `test_id` is `None`, it means we're generating code for a code example "run" link. test_id: Option, lang_str: Option<&'a LangString>, @@ -55,7 +56,7 @@ impl<'a> BuildDocTestBuilder<'a> { source, crate_name: None, edition: DEFAULT_EDITION, - can_merge_doctests: false, + can_merge_doctests: MergeDoctests::Never, test_id: None, lang_str: None, span: DUMMY_SP, @@ -70,7 +71,7 @@ impl<'a> BuildDocTestBuilder<'a> { } #[inline] - pub(crate) fn can_merge_doctests(mut self, can_merge_doctests: bool) -> Self { + pub(crate) fn can_merge_doctests(mut self, can_merge_doctests: MergeDoctests) -> Self { self.can_merge_doctests = can_merge_doctests; self } @@ -117,10 +118,6 @@ impl<'a> BuildDocTestBuilder<'a> { span, global_crate_attrs, } = self; - let can_merge_doctests = can_merge_doctests - && lang_str.is_some_and(|lang_str| { - !lang_str.compile_fail && !lang_str.test_harness && !lang_str.standalone_crate - }); let result = rustc_driver::catch_fatal_errors(|| { rustc_span::create_session_if_not_set_then(edition, |_| { @@ -155,14 +152,26 @@ impl<'a> BuildDocTestBuilder<'a> { debug!("crate_attrs:\n{crate_attrs}{maybe_crate_attrs}"); debug!("crates:\n{crates}"); debug!("after:\n{everything_else}"); - - // If it contains `#[feature]` or `#[no_std]`, we don't want it to be merged either. - let can_be_merged = can_merge_doctests - && !has_global_allocator - && crate_attrs.is_empty() - // If this is a merged doctest and a defined macro uses `$crate`, then the path will - // not work, so better not put it into merged doctests. - && !(has_macro_def && everything_else.contains("$crate")); + debug!("merge-doctests: {can_merge_doctests:?}"); + + // Up until now, we've been dealing with settings for the whole crate. + // Now, infer settings for this particular test. + let can_be_merged = if can_merge_doctests == MergeDoctests::Auto { + let mut can_merge = false; + // Avoid tests with incompatible attributes. + can_merge |= lang_str.is_some_and(|lang_str| { + !lang_str.compile_fail && !lang_str.test_harness && !lang_str.standalone_crate + }); + // If it contains `#[feature]` or `#[no_std]`, we don't want it to be merged either. + can_merge &= !has_global_allocator + && crate_attrs.is_empty() + // If this is a merged doctest and a defined macro uses `$crate`, then the path will + // not work, so better not put it into merged doctests. + && !(has_macro_def && everything_else.contains("$crate")); + can_merge + } else { + can_merge_doctests != MergeDoctests::Never + }; DocTestBuilder { supports_color, has_main_fn, diff --git a/src/librustdoc/doctest/markdown.rs b/src/librustdoc/doctest/markdown.rs index 7f26605f2562c..45f1e8a7fb988 100644 --- a/src/librustdoc/doctest/markdown.rs +++ b/src/librustdoc/doctest/markdown.rs @@ -3,6 +3,7 @@ use std::fs::read_to_string; use std::sync::{Arc, Mutex}; +use rustc_errors::DiagCtxtHandle; use rustc_session::config::Input; use rustc_span::{DUMMY_SP, FileName}; use tempfile::tempdir; @@ -78,7 +79,7 @@ impl DocTestVisitor for MdCollector { } /// Runs any tests/code examples in the markdown file `options.input`. -pub(crate) fn test(input: &Input, options: Options) -> Result<(), String> { +pub(crate) fn test(input: &Input, options: Options, dcx: DiagCtxtHandle<'_>) -> Result<(), String> { let input_str = match input { Input::File(path) => { read_to_string(path).map_err(|err| format!("{}: {err}", path.display()))? @@ -118,6 +119,7 @@ pub(crate) fn test(input: &Input, options: Options) -> Result<(), String> { let CreateRunnableDocTests { opts, rustdoc_options, standalone_tests, mergeable_tests, .. } = collector; crate::doctest::run_tests( + dcx, opts, &rustdoc_options, &Arc::new(Mutex::new(Vec::new())), diff --git a/src/librustdoc/lib.rs b/src/librustdoc/lib.rs index e4601bfb20d7d..be46c85311a0e 100644 --- a/src/librustdoc/lib.rs +++ b/src/librustdoc/lib.rs @@ -544,6 +544,14 @@ fn opts() -> Vec { "[toolchain-shared-resources,invocation-specific,dep-info]", ), opt(Unstable, FlagMulti, "", "no-run", "Compile doctests without running them", ""), + opt( + Unstable, + Opt, + "", + "merge-doctests", + "Force all doctests to be compiled as a single binary, instead of one binary per test. If merging fails, rustdoc will emit a hard error.", + "yes|no|auto", + ), opt( Unstable, Multi, @@ -822,7 +830,7 @@ fn main_args(early_dcx: &mut EarlyDiagCtxt, at_args: &[String]) { options.should_test || output_format == config::OutputFormat::Doctest, config::markdown_input(&input), ) { - (true, Some(_)) => return wrap_return(dcx, doctest::test_markdown(&input, options)), + (true, Some(_)) => return wrap_return(dcx, doctest::test_markdown(&input, options, dcx)), (true, None) => return doctest::run(dcx, input, options), (false, Some(md_input)) => { let md_input = md_input.to_owned(); diff --git a/tests/run-make/rustdoc-default-output/output-default.stdout b/tests/run-make/rustdoc-default-output/output-default.stdout index 49eaf7e2e1e0e..4e28be347cbb1 100644 --- a/tests/run-make/rustdoc-default-output/output-default.stdout +++ b/tests/run-make/rustdoc-default-output/output-default.stdout @@ -154,6 +154,10 @@ Options: Comma separated list of types of output for rustdoc to emit --no-run Compile doctests without running them + --merge-doctests yes|no|auto + Force all doctests to be compiled as a single binary, + instead of one binary per test. If merging fails, + rustdoc will emit a hard error. --remap-path-prefix FROM=TO Remap source names in compiler messages --show-type-layout diff --git a/tests/rustdoc-ui/doctest/force-merge-fail.rs b/tests/rustdoc-ui/doctest/force-merge-fail.rs new file mode 100644 index 0000000000000..88c36268d072d --- /dev/null +++ b/tests/rustdoc-ui/doctest/force-merge-fail.rs @@ -0,0 +1,18 @@ +//@ edition: 2018 +//@ compile-flags: --test --test-args=--test-threads=1 --merge-doctests=yes -Z unstable-options +//@ normalize-stderr: ".*doctest_bundle_2018.rs:\d+:\d+" -> "doctest_bundle_2018.rs:$$LINE:$$COL" + +//~? ERROR failed to merge doctests + +/// These two doctests will fail to force-merge, and should give a hard error as a result. +/// +/// ``` +/// #![deny(clashing_extern_declarations)] +/// unsafe extern "C" { fn unmangled_name() -> u8; } +/// ``` +/// +/// ``` +/// #![deny(clashing_extern_declarations)] +/// unsafe extern "C" { fn unmangled_name(); } +/// ``` +pub struct Foo; diff --git a/tests/rustdoc-ui/doctest/force-merge-fail.stderr b/tests/rustdoc-ui/doctest/force-merge-fail.stderr new file mode 100644 index 0000000000000..b87807dc5ed7d --- /dev/null +++ b/tests/rustdoc-ui/doctest/force-merge-fail.stderr @@ -0,0 +1,6 @@ +doctest_bundle_2018.rs:$LINE:$COL: error: `unmangled_name` redeclared with a different signature: this signature doesn't match the previous declaration +error: aborting due to 1 previous error +error: failed to merge doctests + | + = note: requested explicitly on the command line with `--merge-doctests=yes` + diff --git a/tests/rustdoc-ui/doctest/force-merge.rs b/tests/rustdoc-ui/doctest/force-merge.rs new file mode 100644 index 0000000000000..bd2f474f8a560 --- /dev/null +++ b/tests/rustdoc-ui/doctest/force-merge.rs @@ -0,0 +1,25 @@ +//@ check-pass +//@ edition: 2018 +//@ compile-flags: --test --test-args=--test-threads=1 --merge-doctests=yes -Z unstable-options +//@ normalize-stdout: "tests/rustdoc-ui/doctest" -> "$$DIR" +//@ normalize-stdout: "finished in \d+\.\d+s" -> "finished in $$TIME" +//@ normalize-stdout: "ran in \d+\.\d+s" -> "ran in $$TIME" +//@ normalize-stdout: "compilation took \d+\.\d+s" -> "compilation took $$TIME" +//@ normalize-stdout: ".rs:\d+:\d+" -> ".rs:$$LINE:$$COL" + +// FIXME: compiletest doesn't support `// RAW` for doctests because the progress messages aren't +// emitted as JSON. Instead the .stderr file tests that this contains a +// "merged compilation took ..." message. + +/// ``` +/// let x = 12; +/// ``` +/// +/// These two doctests should be force-merged, even though this uses edition 2018. +/// +/// ``` +/// fn main() { +/// println!("owo"); +/// } +/// ``` +pub struct Foo; diff --git a/tests/rustdoc-ui/doctest/force-merge.stdout b/tests/rustdoc-ui/doctest/force-merge.stdout new file mode 100644 index 0000000000000..94c7909ae0a68 --- /dev/null +++ b/tests/rustdoc-ui/doctest/force-merge.stdout @@ -0,0 +1,8 @@ + +running 2 tests +test $DIR/force-merge.rs - Foo (line 14) ... ok +test $DIR/force-merge.rs - Foo (line 20) ... ok + +test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME + +all doctests ran in $TIME; merged doctests compilation took $TIME diff --git a/tests/rustdoc-ui/doctest/force-no-merge.rs b/tests/rustdoc-ui/doctest/force-no-merge.rs new file mode 100644 index 0000000000000..9ddea5fc2e55c --- /dev/null +++ b/tests/rustdoc-ui/doctest/force-no-merge.rs @@ -0,0 +1,24 @@ +//@ edition: 2024 +//@ check-pass +//@ compile-flags: --test --test-args=--test-threads=1 --merge-doctests=no -Z unstable-options +//@ normalize-stderr: ".*doctest_bundle_2018.rs:\d+:\d+" -> "doctest_bundle_2018.rs:$$LINE:$$COL" + +//@ normalize-stdout: "tests/rustdoc-ui/doctest" -> "$$DIR" +//@ normalize-stdout: "finished in \d+\.\d+s" -> "finished in $$TIME" +//@ normalize-stdout: "ran in \d+\.\d+s" -> "ran in $$TIME" +//@ normalize-stdout: "compilation took \d+\.\d+s" -> "compilation took $$TIME" +//@ normalize-stdout: ".rs:\d+:\d+" -> ".rs:$$LINE:$$COL" + +/// These two doctests should not force-merge, even though this crate has edition 2024 and the +/// individual doctests are not annotated. +/// +/// ``` +/// #![deny(clashing_extern_declarations)] +/// unsafe extern "C" { fn unmangled_name() -> u8; } +/// ``` +/// +/// ``` +/// #![deny(clashing_extern_declarations)] +/// unsafe extern "C" { fn unmangled_name(); } +/// ``` +pub struct Foo; diff --git a/tests/rustdoc-ui/doctest/force-no-merge.stdout b/tests/rustdoc-ui/doctest/force-no-merge.stdout new file mode 100644 index 0000000000000..513ff77054977 --- /dev/null +++ b/tests/rustdoc-ui/doctest/force-no-merge.stdout @@ -0,0 +1,7 @@ + +running 2 tests +test $DIR/force-no-merge.rs - Foo (line 15) ... ok +test $DIR/force-no-merge.rs - Foo (line 20) ... ok + +test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME + diff --git a/tests/rustdoc-ui/doctest/merge-doctests-auto.rs b/tests/rustdoc-ui/doctest/merge-doctests-auto.rs new file mode 100644 index 0000000000000..2e7d0db7a346f --- /dev/null +++ b/tests/rustdoc-ui/doctest/merge-doctests-auto.rs @@ -0,0 +1,28 @@ +//! `--merge-doctests=auto` should override the edition. + +//@ check-pass +//@ edition: 2018 +//@ compile-flags: --test --test-args=--test-threads=1 --merge-doctests=auto -Z unstable-options + +//@ normalize-stdout: "tests/rustdoc-ui/doctest" -> "$$DIR" +//@ normalize-stdout: "finished in \d+\.\d+s" -> "finished in $$TIME" +//@ normalize-stdout: "ran in \d+\.\d+s" -> "ran in $$TIME" +//@ normalize-stdout: "compilation took \d+\.\d+s" -> "compilation took $$TIME" +//@ normalize-stdout: ".rs:\d+:\d+" -> ".rs:$$LINE:$$COL" + +// FIXME: compiletest doesn't support `// RAW` for doctests because the progress messages aren't +// emitted as JSON. Instead the .stderr file tests that this contains a +// "merged compilation took ..." message. + +/// ``` +/// let x = 12; +/// ``` +/// +/// These two doctests should be auto-merged, even though this uses edition 2018. +/// +/// ``` +/// fn main() { +/// println!("owo"); +/// } +/// ``` +pub struct Foo; diff --git a/tests/rustdoc-ui/doctest/merge-doctests-auto.stdout b/tests/rustdoc-ui/doctest/merge-doctests-auto.stdout new file mode 100644 index 0000000000000..a051ffd606364 --- /dev/null +++ b/tests/rustdoc-ui/doctest/merge-doctests-auto.stdout @@ -0,0 +1,8 @@ + +running 2 tests +test $DIR/merge-doctests-auto.rs - Foo (line 17) ... ok +test $DIR/merge-doctests-auto.rs - Foo (line 23) ... ok + +test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME + +all doctests ran in $TIME; merged doctests compilation took $TIME diff --git a/tests/rustdoc-ui/doctest/merge-doctests-invalid.rs b/tests/rustdoc-ui/doctest/merge-doctests-invalid.rs new file mode 100644 index 0000000000000..cf3a03a901ce7 --- /dev/null +++ b/tests/rustdoc-ui/doctest/merge-doctests-invalid.rs @@ -0,0 +1,2 @@ +//@ compile-flags: --merge-doctests=bad-opt -Zunstable-options +//~? ERROR must be a boolean diff --git a/tests/rustdoc-ui/doctest/merge-doctests-invalid.stderr b/tests/rustdoc-ui/doctest/merge-doctests-invalid.stderr new file mode 100644 index 0000000000000..d232c1b59edbe --- /dev/null +++ b/tests/rustdoc-ui/doctest/merge-doctests-invalid.stderr @@ -0,0 +1,2 @@ +error: argument to --merge-doctests must be a boolean (true/false) or 'auto' + diff --git a/tests/rustdoc-ui/doctest/merge-doctests-unstable.rs b/tests/rustdoc-ui/doctest/merge-doctests-unstable.rs new file mode 100644 index 0000000000000..496e531659a34 --- /dev/null +++ b/tests/rustdoc-ui/doctest/merge-doctests-unstable.rs @@ -0,0 +1,2 @@ +//@ compile-flags: --merge-doctests=no +//~? RAW `-Z unstable-options` flag must also be passed diff --git a/tests/rustdoc-ui/doctest/merge-doctests-unstable.stderr b/tests/rustdoc-ui/doctest/merge-doctests-unstable.stderr new file mode 100644 index 0000000000000..e8d75342bc8ef --- /dev/null +++ b/tests/rustdoc-ui/doctest/merge-doctests-unstable.stderr @@ -0,0 +1,2 @@ +error: the `-Z unstable-options` flag must also be passed to enable the flag `merge-doctests` + From 3aa4ece95d6ad46ced3ea895fcc634371ff32039 Mon Sep 17 00:00:00 2001 From: Jynn Nelson Date: Wed, 3 Dec 2025 15:48:16 -0500 Subject: [PATCH 2/3] Support `-C save-temps` in rustdoc This allows viewing failed merged doctests. --- src/librustdoc/doctest.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index cab65181c9401..6e0025fe70647 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -123,8 +123,13 @@ pub(crate) fn generate_args_file(file_path: &Path, options: &RustdocOptions) -> Ok(()) } -fn get_doctest_dir() -> io::Result { - TempFileBuilder::new().prefix("rustdoctest").tempdir() +fn get_doctest_dir(opts: &RustdocOptions) -> io::Result { + let mut builder = TempFileBuilder::new(); + builder.prefix("rustdoctest"); + if opts.codegen_options.save_temps { + builder.disable_cleanup(true); + } + builder.tempdir() } pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions) { @@ -197,7 +202,7 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions let externs = options.externs.clone(); let json_unused_externs = options.json_unused_externs; - let temp_dir = match get_doctest_dir() + let temp_dir = match get_doctest_dir(&options) .map_err(|error| format!("failed to create temporary directory: {error:?}")) { Ok(temp_dir) => temp_dir, @@ -207,6 +212,7 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions crate::wrap_return(dcx, generate_args_file(&args_path, &options)); let extract_doctests = options.output_format == OutputFormat::Doctest; + let save_temps = options.codegen_options.save_temps; let result = interface::run_compiler(config, |compiler| { let krate = rustc_interface::passes::parse(&compiler.sess); @@ -259,7 +265,9 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions eprintln!("{error}"); // Since some files in the temporary folder are still owned and alive, we need // to manually remove the folder. - let _ = std::fs::remove_dir_all(temp_dir.path()); + if !save_temps { + let _ = std::fs::remove_dir_all(temp_dir.path()); + } std::process::exit(1); } }; From 415953a317f04b0b5ad26b1ded3671a3e2a01532 Mon Sep 17 00:00:00 2001 From: Jynn Nelson Date: Wed, 3 Dec 2025 17:40:10 -0500 Subject: [PATCH 3/3] `--merge-doctests` is a default, not an override --- src/librustdoc/doctest/make.rs | 23 +++++++++-------- .../force-merge-default-not-override.rs | 25 +++++++++++++++++++ .../force-merge-default-not-override.stdout | 7 ++++++ 3 files changed, 44 insertions(+), 11 deletions(-) create mode 100644 tests/rustdoc-ui/doctest/force-merge-default-not-override.rs create mode 100644 tests/rustdoc-ui/doctest/force-merge-default-not-override.stdout diff --git a/src/librustdoc/doctest/make.rs b/src/librustdoc/doctest/make.rs index 1f5956168d7e8..569206d6ec88e 100644 --- a/src/librustdoc/doctest/make.rs +++ b/src/librustdoc/doctest/make.rs @@ -156,21 +156,22 @@ impl<'a> BuildDocTestBuilder<'a> { // Up until now, we've been dealing with settings for the whole crate. // Now, infer settings for this particular test. + // + // Avoid tests with incompatible attributes. + let opt_out = lang_str.is_some_and(|lang_str| { + lang_str.compile_fail || lang_str.test_harness || lang_str.standalone_crate + }); let can_be_merged = if can_merge_doctests == MergeDoctests::Auto { - let mut can_merge = false; - // Avoid tests with incompatible attributes. - can_merge |= lang_str.is_some_and(|lang_str| { - !lang_str.compile_fail && !lang_str.test_harness && !lang_str.standalone_crate - }); - // If it contains `#[feature]` or `#[no_std]`, we don't want it to be merged either. - can_merge &= !has_global_allocator - && crate_attrs.is_empty() + // We try to look at the contents of the test to detect whether it should be merged. + // This is not a complete list of possible failures, but it catches many cases. + let will_probably_fail = has_global_allocator + || !crate_attrs.is_empty() // If this is a merged doctest and a defined macro uses `$crate`, then the path will // not work, so better not put it into merged doctests. - && !(has_macro_def && everything_else.contains("$crate")); - can_merge + || (has_macro_def && everything_else.contains("$crate")); + !opt_out && !will_probably_fail } else { - can_merge_doctests != MergeDoctests::Never + can_merge_doctests != MergeDoctests::Never && !opt_out }; DocTestBuilder { supports_color, diff --git a/tests/rustdoc-ui/doctest/force-merge-default-not-override.rs b/tests/rustdoc-ui/doctest/force-merge-default-not-override.rs new file mode 100644 index 0000000000000..9a1e86ade67f5 --- /dev/null +++ b/tests/rustdoc-ui/doctest/force-merge-default-not-override.rs @@ -0,0 +1,25 @@ +//@ check-pass +//@ edition: 2024 +//@ compile-flags: --test --test-args=--test-threads=1 --merge-doctests=yes -Z unstable-options +//@ normalize-stdout: "tests/rustdoc-ui/doctest" -> "$$DIR" +//@ normalize-stdout: "finished in \d+\.\d+s" -> "finished in $$TIME" +//@ normalize-stdout: "ran in \d+\.\d+s" -> "ran in $$TIME" +//@ normalize-stdout: "compilation took \d+\.\d+s" -> "compilation took $$TIME" +//@ normalize-stdout: ".rs:\d+:\d+" -> ".rs:$$LINE:$$COL" + +// FIXME: compiletest doesn't support `// RAW` for doctests because the progress messages aren't +// emitted as JSON. Instead the .stderr file tests that this doesn't contains a +// "merged compilation took ..." message. + +/// ```standalone_crate +/// let x = 12; +/// ``` +/// +/// These two doctests should be not be merged, even though this passes `--merge-doctests=yes`. +/// +/// ```standalone_crate +/// fn main() { +/// println!("owo"); +/// } +/// ``` +pub struct Foo; diff --git a/tests/rustdoc-ui/doctest/force-merge-default-not-override.stdout b/tests/rustdoc-ui/doctest/force-merge-default-not-override.stdout new file mode 100644 index 0000000000000..24b16ec82f45b --- /dev/null +++ b/tests/rustdoc-ui/doctest/force-merge-default-not-override.stdout @@ -0,0 +1,7 @@ + +running 2 tests +test $DIR/force-merge-default-not-override.rs - Foo (line 14) ... ok +test $DIR/force-merge-default-not-override.rs - Foo (line 20) ... ok + +test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME +