Skip to content

Commit 5413818

Browse files
authored
Unrolled build for #149565
Rollup merge of #149565 - jyn514:force-doctest-merge, r=notriddle 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 opting in even on earlier editions. 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. Helps with #141240. ``@epage`` said in that issue he would like a per-doctest opt-in, and that seems useful to me in addition to this flag, but I think it's a separate use case from changing the default.
2 parents 5b150d2 + 415953a commit 5413818

20 files changed

+270
-33
lines changed

src/librustdoc/config.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,15 @@ pub(crate) enum InputMode {
6363
HasFile(Input),
6464
}
6565

66+
/// Whether to run multiple doctests in the same binary.
67+
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
68+
pub(crate) enum MergeDoctests {
69+
#[default]
70+
Never,
71+
Always,
72+
Auto,
73+
}
74+
6675
/// Configuration options for rustdoc.
6776
#[derive(Clone)]
6877
pub(crate) struct Options {
@@ -121,6 +130,8 @@ pub(crate) struct Options {
121130
/// Optional path to persist the doctest executables to, defaults to a
122131
/// temporary directory if not set.
123132
pub(crate) persist_doctests: Option<PathBuf>,
133+
/// Whether to merge
134+
pub(crate) merge_doctests: MergeDoctests,
124135
/// Runtool to run doctests with
125136
pub(crate) test_runtool: Option<String>,
126137
/// Arguments to pass to the runtool
@@ -801,6 +812,8 @@ impl Options {
801812
Ok(result) => result,
802813
Err(e) => dcx.fatal(format!("--merge option error: {e}")),
803814
};
815+
let merge_doctests = parse_merge_doctests(matches, edition, dcx);
816+
tracing::debug!("merge_doctests: {merge_doctests:?}");
804817

805818
if generate_link_to_definition && (show_coverage || output_format != OutputFormat::Html) {
806819
dcx.struct_warn(
@@ -852,6 +865,7 @@ impl Options {
852865
crate_version,
853866
test_run_directory,
854867
persist_doctests,
868+
merge_doctests,
855869
test_runtool,
856870
test_runtool_args,
857871
test_builder,
@@ -1048,3 +1062,20 @@ fn parse_merge(m: &getopts::Matches) -> Result<ShouldMerge, &'static str> {
10481062
Some(_) => Err("argument to --merge must be `none`, `shared`, or `finalize`"),
10491063
}
10501064
}
1065+
1066+
fn parse_merge_doctests(
1067+
m: &getopts::Matches,
1068+
edition: Edition,
1069+
dcx: DiagCtxtHandle<'_>,
1070+
) -> MergeDoctests {
1071+
match m.opt_str("merge-doctests").as_deref() {
1072+
Some("y") | Some("yes") | Some("on") | Some("true") => MergeDoctests::Always,
1073+
Some("n") | Some("no") | Some("off") | Some("false") => MergeDoctests::Never,
1074+
Some("auto") => MergeDoctests::Auto,
1075+
None if edition < Edition::Edition2024 => MergeDoctests::Never,
1076+
None => MergeDoctests::Auto,
1077+
Some(_) => {
1078+
dcx.fatal("argument to --merge-doctests must be a boolean (true/false) or 'auto'")
1079+
}
1080+
}
1081+
}

src/librustdoc/doctest.rs

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ use tempfile::{Builder as TempFileBuilder, TempDir};
3535
use tracing::debug;
3636

3737
use self::rust::HirCollector;
38-
use crate::config::{Options as RustdocOptions, OutputFormat};
38+
use crate::config::{MergeDoctests, Options as RustdocOptions, OutputFormat};
3939
use crate::html::markdown::{ErrorCodes, Ignore, LangString, MdRelLine};
4040
use crate::lint::init_lints;
4141

@@ -125,8 +125,13 @@ pub(crate) fn generate_args_file(file_path: &Path, options: &RustdocOptions) ->
125125
Ok(())
126126
}
127127

128-
fn get_doctest_dir() -> io::Result<TempDir> {
129-
TempFileBuilder::new().prefix("rustdoctest").tempdir()
128+
fn get_doctest_dir(opts: &RustdocOptions) -> io::Result<TempDir> {
129+
let mut builder = TempFileBuilder::new();
130+
builder.prefix("rustdoctest");
131+
if opts.codegen_options.save_temps {
132+
builder.disable_cleanup(true);
133+
}
134+
builder.tempdir()
130135
}
131136

132137
pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions) {
@@ -199,7 +204,7 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions
199204
let externs = options.externs.clone();
200205
let json_unused_externs = options.json_unused_externs;
201206

202-
let temp_dir = match get_doctest_dir()
207+
let temp_dir = match get_doctest_dir(&options)
203208
.map_err(|error| format!("failed to create temporary directory: {error:?}"))
204209
{
205210
Ok(temp_dir) => temp_dir,
@@ -209,6 +214,7 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions
209214
crate::wrap_return(dcx, generate_args_file(&args_path, &options));
210215

211216
let extract_doctests = options.output_format == OutputFormat::Doctest;
217+
let save_temps = options.codegen_options.save_temps;
212218
let result = interface::run_compiler(config, |compiler| {
213219
let krate = rustc_interface::passes::parse(&compiler.sess);
214220

@@ -260,12 +266,15 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions
260266
eprintln!("{error}");
261267
// Since some files in the temporary folder are still owned and alive, we need
262268
// to manually remove the folder.
263-
let _ = std::fs::remove_dir_all(temp_dir.path());
269+
if !save_temps {
270+
let _ = std::fs::remove_dir_all(temp_dir.path());
271+
}
264272
std::process::exit(1);
265273
}
266274
};
267275

268276
run_tests(
277+
dcx,
269278
opts,
270279
&rustdoc_options,
271280
&unused_extern_reports,
@@ -317,6 +326,7 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions
317326
}
318327

319328
pub(crate) fn run_tests(
329+
dcx: DiagCtxtHandle<'_>,
320330
opts: GlobalTestOptions,
321331
rustdoc_options: &Arc<RustdocOptions>,
322332
unused_extern_reports: &Arc<Mutex<Vec<UnusedExterns>>>,
@@ -369,6 +379,13 @@ pub(crate) fn run_tests(
369379
}
370380
continue;
371381
}
382+
383+
if rustdoc_options.merge_doctests == MergeDoctests::Always {
384+
let mut diag = dcx.struct_fatal("failed to merge doctests");
385+
diag.note("requested explicitly on the command line with `--merge-doctests=yes`");
386+
diag.emit();
387+
}
388+
372389
// We failed to compile all compatible tests as one so we push them into the
373390
// `standalone_tests` doctests.
374391
debug!("Failed to compile compatible doctests for edition {} all at once", edition);
@@ -653,9 +670,9 @@ fn run_test(
653670
// tested as standalone tests.
654671
return (Duration::default(), Err(TestFailure::CompileError));
655672
}
656-
if !rustdoc_options.no_capture {
657-
// If `no_capture` is disabled, then we don't display rustc's output when compiling
658-
// the merged doctests.
673+
if !rustdoc_options.no_capture && rustdoc_options.merge_doctests == MergeDoctests::Auto {
674+
// If `no_capture` is disabled, and we might fallback to standalone tests, then we don't
675+
// display rustc's output when compiling the merged doctests.
659676
compiler.stderr(Stdio::null());
660677
}
661678
// bundled tests are an rlib, loaded by a separate runner executable
@@ -736,10 +753,12 @@ fn run_test(
736753
// tested as standalone tests.
737754
return (instant.elapsed(), Err(TestFailure::CompileError));
738755
}
739-
if !rustdoc_options.no_capture {
740-
// If `no_capture` is disabled, then we don't display rustc's output when compiling
741-
// the merged doctests.
756+
if !rustdoc_options.no_capture && rustdoc_options.merge_doctests == MergeDoctests::Auto {
757+
// If `no_capture` is disabled and we're autodetecting whether to merge,
758+
// we don't display rustc's output when compiling the merged doctests.
742759
runner_compiler.stderr(Stdio::null());
760+
} else {
761+
runner_compiler.stderr(Stdio::inherit());
743762
}
744763
runner_compiler.arg("--error-format=short");
745764
debug!("compiler invocation for doctest runner: {runner_compiler:?}");
@@ -896,7 +915,7 @@ impl IndividualTestOptions {
896915

897916
DirState::Perm(path)
898917
} else {
899-
DirState::Temp(get_doctest_dir().expect("rustdoc needs a tempdir"))
918+
DirState::Temp(get_doctest_dir(options).expect("rustdoc needs a tempdir"))
900919
};
901920

902921
Self { outdir, path: test_path }
@@ -985,21 +1004,20 @@ struct CreateRunnableDocTests {
9851004
visited_tests: FxHashMap<(String, usize), usize>,
9861005
unused_extern_reports: Arc<Mutex<Vec<UnusedExterns>>>,
9871006
compiling_test_count: AtomicUsize,
988-
can_merge_doctests: bool,
1007+
can_merge_doctests: MergeDoctests,
9891008
}
9901009

9911010
impl CreateRunnableDocTests {
9921011
fn new(rustdoc_options: RustdocOptions, opts: GlobalTestOptions) -> CreateRunnableDocTests {
993-
let can_merge_doctests = rustdoc_options.edition >= Edition::Edition2024;
9941012
CreateRunnableDocTests {
9951013
standalone_tests: Vec::new(),
9961014
mergeable_tests: FxIndexMap::default(),
997-
rustdoc_options: Arc::new(rustdoc_options),
9981015
opts,
9991016
visited_tests: FxHashMap::default(),
10001017
unused_extern_reports: Default::default(),
10011018
compiling_test_count: AtomicUsize::new(0),
1002-
can_merge_doctests,
1019+
can_merge_doctests: rustdoc_options.merge_doctests,
1020+
rustdoc_options: Arc::new(rustdoc_options),
10031021
}
10041022
}
10051023

src/librustdoc/doctest/make.rs

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use rustc_span::{DUMMY_SP, FileName, Span, kw};
2020
use tracing::debug;
2121

2222
use super::GlobalTestOptions;
23+
use crate::config::MergeDoctests;
2324
use crate::display::Joined as _;
2425
use crate::html::markdown::LangString;
2526

@@ -41,7 +42,7 @@ pub(crate) struct BuildDocTestBuilder<'a> {
4142
source: &'a str,
4243
crate_name: Option<&'a str>,
4344
edition: Edition,
44-
can_merge_doctests: bool,
45+
can_merge_doctests: MergeDoctests,
4546
// If `test_id` is `None`, it means we're generating code for a code example "run" link.
4647
test_id: Option<String>,
4748
lang_str: Option<&'a LangString>,
@@ -55,7 +56,7 @@ impl<'a> BuildDocTestBuilder<'a> {
5556
source,
5657
crate_name: None,
5758
edition: DEFAULT_EDITION,
58-
can_merge_doctests: false,
59+
can_merge_doctests: MergeDoctests::Never,
5960
test_id: None,
6061
lang_str: None,
6162
span: DUMMY_SP,
@@ -70,7 +71,7 @@ impl<'a> BuildDocTestBuilder<'a> {
7071
}
7172

7273
#[inline]
73-
pub(crate) fn can_merge_doctests(mut self, can_merge_doctests: bool) -> Self {
74+
pub(crate) fn can_merge_doctests(mut self, can_merge_doctests: MergeDoctests) -> Self {
7475
self.can_merge_doctests = can_merge_doctests;
7576
self
7677
}
@@ -117,10 +118,6 @@ impl<'a> BuildDocTestBuilder<'a> {
117118
span,
118119
global_crate_attrs,
119120
} = self;
120-
let can_merge_doctests = can_merge_doctests
121-
&& lang_str.is_some_and(|lang_str| {
122-
!lang_str.compile_fail && !lang_str.test_harness && !lang_str.standalone_crate
123-
});
124121

125122
let result = rustc_driver::catch_fatal_errors(|| {
126123
rustc_span::create_session_if_not_set_then(edition, |_| {
@@ -155,14 +152,27 @@ impl<'a> BuildDocTestBuilder<'a> {
155152
debug!("crate_attrs:\n{crate_attrs}{maybe_crate_attrs}");
156153
debug!("crates:\n{crates}");
157154
debug!("after:\n{everything_else}");
158-
159-
// If it contains `#[feature]` or `#[no_std]`, we don't want it to be merged either.
160-
let can_be_merged = can_merge_doctests
161-
&& !has_global_allocator
162-
&& crate_attrs.is_empty()
163-
// If this is a merged doctest and a defined macro uses `$crate`, then the path will
164-
// not work, so better not put it into merged doctests.
165-
&& !(has_macro_def && everything_else.contains("$crate"));
155+
debug!("merge-doctests: {can_merge_doctests:?}");
156+
157+
// Up until now, we've been dealing with settings for the whole crate.
158+
// Now, infer settings for this particular test.
159+
//
160+
// Avoid tests with incompatible attributes.
161+
let opt_out = lang_str.is_some_and(|lang_str| {
162+
lang_str.compile_fail || lang_str.test_harness || lang_str.standalone_crate
163+
});
164+
let can_be_merged = if can_merge_doctests == MergeDoctests::Auto {
165+
// We try to look at the contents of the test to detect whether it should be merged.
166+
// This is not a complete list of possible failures, but it catches many cases.
167+
let will_probably_fail = has_global_allocator
168+
|| !crate_attrs.is_empty()
169+
// If this is a merged doctest and a defined macro uses `$crate`, then the path will
170+
// not work, so better not put it into merged doctests.
171+
|| (has_macro_def && everything_else.contains("$crate"));
172+
!opt_out && !will_probably_fail
173+
} else {
174+
can_merge_doctests != MergeDoctests::Never && !opt_out
175+
};
166176
DocTestBuilder {
167177
supports_color,
168178
has_main_fn,

src/librustdoc/doctest/markdown.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use std::fs::read_to_string;
44
use std::sync::{Arc, Mutex};
55

6+
use rustc_errors::DiagCtxtHandle;
67
use rustc_session::config::Input;
78
use rustc_span::{DUMMY_SP, FileName};
89
use tempfile::tempdir;
@@ -78,7 +79,7 @@ impl DocTestVisitor for MdCollector {
7879
}
7980

8081
/// Runs any tests/code examples in the markdown file `options.input`.
81-
pub(crate) fn test(input: &Input, options: Options) -> Result<(), String> {
82+
pub(crate) fn test(input: &Input, options: Options, dcx: DiagCtxtHandle<'_>) -> Result<(), String> {
8283
let input_str = match input {
8384
Input::File(path) => {
8485
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> {
118119
let CreateRunnableDocTests { opts, rustdoc_options, standalone_tests, mergeable_tests, .. } =
119120
collector;
120121
crate::doctest::run_tests(
122+
dcx,
121123
opts,
122124
&rustdoc_options,
123125
&Arc::new(Mutex::new(Vec::new())),

src/librustdoc/lib.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,14 @@ fn opts() -> Vec<RustcOptGroup> {
544544
"[toolchain-shared-resources,invocation-specific,dep-info]",
545545
),
546546
opt(Unstable, FlagMulti, "", "no-run", "Compile doctests without running them", ""),
547+
opt(
548+
Unstable,
549+
Opt,
550+
"",
551+
"merge-doctests",
552+
"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.",
553+
"yes|no|auto",
554+
),
547555
opt(
548556
Unstable,
549557
Multi,
@@ -822,7 +830,7 @@ fn main_args(early_dcx: &mut EarlyDiagCtxt, at_args: &[String]) {
822830
options.should_test || output_format == config::OutputFormat::Doctest,
823831
config::markdown_input(&input),
824832
) {
825-
(true, Some(_)) => return wrap_return(dcx, doctest::test_markdown(&input, options)),
833+
(true, Some(_)) => return wrap_return(dcx, doctest::test_markdown(&input, options, dcx)),
826834
(true, None) => return doctest::run(dcx, input, options),
827835
(false, Some(md_input)) => {
828836
let md_input = md_input.to_owned();

tests/run-make/rustdoc-default-output/output-default.stdout

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ Options:
154154
Comma separated list of types of output for rustdoc to
155155
emit
156156
--no-run Compile doctests without running them
157+
--merge-doctests yes|no|auto
158+
Force all doctests to be compiled as a single binary,
159+
instead of one binary per test. If merging fails,
160+
rustdoc will emit a hard error.
157161
--remap-path-prefix FROM=TO
158162
Remap source names in compiler messages
159163
--show-type-layout
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//@ check-pass
2+
//@ edition: 2024
3+
//@ compile-flags: --test --test-args=--test-threads=1 --merge-doctests=yes -Z unstable-options
4+
//@ normalize-stdout: "tests/rustdoc-ui/doctest" -> "$$DIR"
5+
//@ normalize-stdout: "finished in \d+\.\d+s" -> "finished in $$TIME"
6+
//@ normalize-stdout: "ran in \d+\.\d+s" -> "ran in $$TIME"
7+
//@ normalize-stdout: "compilation took \d+\.\d+s" -> "compilation took $$TIME"
8+
//@ normalize-stdout: ".rs:\d+:\d+" -> ".rs:$$LINE:$$COL"
9+
10+
// FIXME: compiletest doesn't support `// RAW` for doctests because the progress messages aren't
11+
// emitted as JSON. Instead the .stderr file tests that this doesn't contains a
12+
// "merged compilation took ..." message.
13+
14+
/// ```standalone_crate
15+
/// let x = 12;
16+
/// ```
17+
///
18+
/// These two doctests should be not be merged, even though this passes `--merge-doctests=yes`.
19+
///
20+
/// ```standalone_crate
21+
/// fn main() {
22+
/// println!("owo");
23+
/// }
24+
/// ```
25+
pub struct Foo;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
2+
running 2 tests
3+
test $DIR/force-merge-default-not-override.rs - Foo (line 14) ... ok
4+
test $DIR/force-merge-default-not-override.rs - Foo (line 20) ... ok
5+
6+
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME
7+

0 commit comments

Comments
 (0)