Skip to content

Commit 73038c0

Browse files
authored
Rollup merge of #122840 - GuillaumeGomez:rustdoc-test-too-many-args, r=notriddle,Urgau,jieyouxu
`rustdoc --test`: Prevent reaching the maximum size of command-line by using files for arguments if there are too many Fixes #122722. Thanks to this I discovered that rust was using ``@`` to add arguments from a file, quite convenient. If there are too many `cfg` arguments given to `rustdoc --test`, it'll now put them into a temporary file and passing it as argument to the rustc command. I added a test with 100_000 `cfg` arguments to ensure it'll not break again. r? `@notrid`
2 parents 2dcc968 + bc4f169 commit 73038c0

File tree

6 files changed

+211
-79
lines changed

6 files changed

+211
-79
lines changed

src/librustdoc/doctest.rs

+141-72
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ use rustc_span::source_map::SourceMap;
2020
use rustc_span::symbol::sym;
2121
use rustc_span::{BytePos, FileName, Pos, Span, DUMMY_SP};
2222
use rustc_target::spec::{Target, TargetTriple};
23-
use tempfile::Builder as TempFileBuilder;
2423

2524
use std::env;
25+
use std::fs::File;
2626
use std::io::{self, Write};
2727
use std::panic;
2828
use std::path::{Path, PathBuf};
@@ -31,6 +31,8 @@ use std::str;
3131
use std::sync::atomic::{AtomicUsize, Ordering};
3232
use std::sync::{Arc, Mutex};
3333

34+
use tempfile::{Builder as TempFileBuilder, TempDir};
35+
3436
use crate::clean::{types::AttributesExt, Attributes};
3537
use crate::config::Options as RustdocOptions;
3638
use crate::html::markdown::{self, ErrorCodes, Ignore, LangString};
@@ -48,7 +50,55 @@ pub(crate) struct GlobalTestOptions {
4850
pub(crate) attrs: Vec<String>,
4951
}
5052

51-
pub(crate) fn run(options: RustdocOptions) -> Result<(), ErrorGuaranteed> {
53+
pub(crate) fn generate_args_file(file_path: &Path, options: &RustdocOptions) -> Result<(), String> {
54+
let mut file = File::create(file_path)
55+
.map_err(|error| format!("failed to create args file: {error:?}"))?;
56+
57+
// We now put the common arguments into the file we created.
58+
let mut content = vec!["--crate-type=bin".to_string()];
59+
60+
for cfg in &options.cfgs {
61+
content.push(format!("--cfg={cfg}"));
62+
}
63+
if !options.check_cfgs.is_empty() {
64+
content.push("-Zunstable-options".to_string());
65+
for check_cfg in &options.check_cfgs {
66+
content.push(format!("--check-cfg={check_cfg}"));
67+
}
68+
}
69+
70+
if let Some(sysroot) = &options.maybe_sysroot {
71+
content.push(format!("--sysroot={}", sysroot.display()));
72+
}
73+
for lib_str in &options.lib_strs {
74+
content.push(format!("-L{lib_str}"));
75+
}
76+
for extern_str in &options.extern_strs {
77+
content.push(format!("--extern={extern_str}"));
78+
}
79+
content.push("-Ccodegen-units=1".to_string());
80+
for codegen_options_str in &options.codegen_options_strs {
81+
content.push(format!("-C{codegen_options_str}"));
82+
}
83+
for unstable_option_str in &options.unstable_opts_strs {
84+
content.push(format!("-Z{unstable_option_str}"));
85+
}
86+
87+
let content = content.join("\n");
88+
89+
file.write(content.as_bytes())
90+
.map_err(|error| format!("failed to write arguments to temporary file: {error:?}"))?;
91+
Ok(())
92+
}
93+
94+
fn get_doctest_dir() -> io::Result<TempDir> {
95+
TempFileBuilder::new().prefix("rustdoctest").tempdir()
96+
}
97+
98+
pub(crate) fn run(
99+
dcx: &rustc_errors::DiagCtxt,
100+
options: RustdocOptions,
101+
) -> Result<(), ErrorGuaranteed> {
52102
let input = config::Input::File(options.input.clone());
53103

54104
let invalid_codeblock_attributes_name = crate::lint::INVALID_CODEBLOCK_ATTRIBUTES.name;
@@ -118,6 +168,15 @@ pub(crate) fn run(options: RustdocOptions) -> Result<(), ErrorGuaranteed> {
118168
let externs = options.externs.clone();
119169
let json_unused_externs = options.json_unused_externs;
120170

171+
let temp_dir = match get_doctest_dir()
172+
.map_err(|error| format!("failed to create temporary directory: {error:?}"))
173+
{
174+
Ok(temp_dir) => temp_dir,
175+
Err(error) => return crate::wrap_return(dcx, Err(error)),
176+
};
177+
let file_path = temp_dir.path().join("rustdoc-cfgs");
178+
crate::wrap_return(dcx, generate_args_file(&file_path, &options))?;
179+
121180
let (tests, unused_extern_reports, compiling_test_count) =
122181
interface::run_compiler(config, |compiler| {
123182
compiler.enter(|queries| {
@@ -134,6 +193,7 @@ pub(crate) fn run(options: RustdocOptions) -> Result<(), ErrorGuaranteed> {
134193
Some(compiler.sess.psess.clone_source_map()),
135194
None,
136195
enable_per_target_ignores,
196+
file_path,
137197
);
138198

139199
let mut hir_collector = HirCollector {
@@ -322,74 +382,53 @@ fn run_test(
322382
test: &str,
323383
crate_name: &str,
324384
line: usize,
325-
rustdoc_options: RustdocOptions,
385+
rustdoc_options: IndividualTestOptions,
326386
mut lang_string: LangString,
327387
no_run: bool,
328-
runtool: Option<String>,
329-
runtool_args: Vec<String>,
330-
target: TargetTriple,
331388
opts: &GlobalTestOptions,
332389
edition: Edition,
333-
outdir: DirState,
334390
path: PathBuf,
335-
test_id: &str,
336391
report_unused_externs: impl Fn(UnusedExterns),
337392
) -> Result<(), TestFailure> {
338-
let (test, line_offset, supports_color) =
339-
make_test(test, Some(crate_name), lang_string.test_harness, opts, edition, Some(test_id));
393+
let (test, line_offset, supports_color) = make_test(
394+
test,
395+
Some(crate_name),
396+
lang_string.test_harness,
397+
opts,
398+
edition,
399+
Some(&rustdoc_options.test_id),
400+
);
340401

341402
// Make sure we emit well-formed executable names for our target.
342-
let rust_out = add_exe_suffix("rust_out".to_owned(), &target);
343-
let output_file = outdir.path().join(rust_out);
403+
let rust_out = add_exe_suffix("rust_out".to_owned(), &rustdoc_options.target);
404+
let output_file = rustdoc_options.outdir.path().join(rust_out);
344405

345406
let rustc_binary = rustdoc_options
346407
.test_builder
347408
.as_deref()
348409
.unwrap_or_else(|| rustc_interface::util::rustc_path().expect("found rustc"));
349410
let mut compiler = wrapped_rustc_command(&rustdoc_options.test_builder_wrappers, rustc_binary);
350-
compiler.arg("--crate-type").arg("bin");
351-
for cfg in &rustdoc_options.cfgs {
352-
compiler.arg("--cfg").arg(&cfg);
353-
}
354-
if !rustdoc_options.check_cfgs.is_empty() {
355-
compiler.arg("-Z").arg("unstable-options");
356-
for check_cfg in &rustdoc_options.check_cfgs {
357-
compiler.arg("--check-cfg").arg(&check_cfg);
358-
}
359-
}
360-
if let Some(sysroot) = rustdoc_options.maybe_sysroot {
361-
compiler.arg("--sysroot").arg(sysroot);
362-
}
411+
412+
compiler.arg(&format!("@{}", rustdoc_options.arg_file.display()));
413+
363414
compiler.arg("--edition").arg(&edition.to_string());
364415
compiler.env("UNSTABLE_RUSTDOC_TEST_PATH", path);
365416
compiler.env("UNSTABLE_RUSTDOC_TEST_LINE", format!("{}", line as isize - line_offset as isize));
366417
compiler.arg("-o").arg(&output_file);
367418
if lang_string.test_harness {
368419
compiler.arg("--test");
369420
}
370-
if rustdoc_options.json_unused_externs.is_enabled() && !lang_string.compile_fail {
421+
if rustdoc_options.is_json_unused_externs_enabled && !lang_string.compile_fail {
371422
compiler.arg("--error-format=json");
372423
compiler.arg("--json").arg("unused-externs");
373-
compiler.arg("-Z").arg("unstable-options");
374424
compiler.arg("-W").arg("unused_crate_dependencies");
425+
compiler.arg("-Z").arg("unstable-options");
375426
}
376-
for lib_str in &rustdoc_options.lib_strs {
377-
compiler.arg("-L").arg(&lib_str);
378-
}
379-
for extern_str in &rustdoc_options.extern_strs {
380-
compiler.arg("--extern").arg(&extern_str);
381-
}
382-
compiler.arg("-Ccodegen-units=1");
383-
for codegen_options_str in &rustdoc_options.codegen_options_strs {
384-
compiler.arg("-C").arg(&codegen_options_str);
385-
}
386-
for unstable_option_str in &rustdoc_options.unstable_opts_strs {
387-
compiler.arg("-Z").arg(&unstable_option_str);
388-
}
389-
if no_run && !lang_string.compile_fail && rustdoc_options.persist_doctests.is_none() {
427+
428+
if no_run && !lang_string.compile_fail && rustdoc_options.should_persist_doctests {
390429
compiler.arg("--emit=metadata");
391430
}
392-
compiler.arg("--target").arg(match target {
431+
compiler.arg("--target").arg(match rustdoc_options.target {
393432
TargetTriple::TargetTriple(s) => s,
394433
TargetTriple::TargetJson { path_for_rustdoc, .. } => {
395434
path_for_rustdoc.to_str().expect("target path must be valid unicode").to_string()
@@ -485,10 +524,10 @@ fn run_test(
485524
let mut cmd;
486525

487526
let output_file = make_maybe_absolute_path(output_file);
488-
if let Some(tool) = runtool {
527+
if let Some(tool) = rustdoc_options.runtool {
489528
let tool = make_maybe_absolute_path(tool.into());
490529
cmd = Command::new(tool);
491-
cmd.args(runtool_args);
530+
cmd.args(rustdoc_options.runtool_args);
492531
cmd.arg(output_file);
493532
} else {
494533
cmd = Command::new(output_file);
@@ -897,6 +936,56 @@ fn partition_source(s: &str, edition: Edition) -> (String, String, String) {
897936
(before, after, crates)
898937
}
899938

939+
pub(crate) struct IndividualTestOptions {
940+
test_builder: Option<PathBuf>,
941+
test_builder_wrappers: Vec<PathBuf>,
942+
is_json_unused_externs_enabled: bool,
943+
should_persist_doctests: bool,
944+
error_format: ErrorOutputType,
945+
test_run_directory: Option<PathBuf>,
946+
nocapture: bool,
947+
arg_file: PathBuf,
948+
outdir: DirState,
949+
runtool: Option<String>,
950+
runtool_args: Vec<String>,
951+
target: TargetTriple,
952+
test_id: String,
953+
}
954+
955+
impl IndividualTestOptions {
956+
fn new(options: &RustdocOptions, arg_file: &Path, test_id: String) -> Self {
957+
let outdir = if let Some(ref path) = options.persist_doctests {
958+
let mut path = path.clone();
959+
path.push(&test_id);
960+
961+
if let Err(err) = std::fs::create_dir_all(&path) {
962+
eprintln!("Couldn't create directory for doctest executables: {err}");
963+
panic::resume_unwind(Box::new(()));
964+
}
965+
966+
DirState::Perm(path)
967+
} else {
968+
DirState::Temp(get_doctest_dir().expect("rustdoc needs a tempdir"))
969+
};
970+
971+
Self {
972+
test_builder: options.test_builder.clone(),
973+
test_builder_wrappers: options.test_builder_wrappers.clone(),
974+
is_json_unused_externs_enabled: options.json_unused_externs.is_enabled(),
975+
should_persist_doctests: options.persist_doctests.is_none(),
976+
error_format: options.error_format,
977+
test_run_directory: options.test_run_directory.clone(),
978+
nocapture: options.nocapture,
979+
arg_file: arg_file.into(),
980+
outdir,
981+
runtool: options.runtool.clone(),
982+
runtool_args: options.runtool_args.clone(),
983+
target: options.target.clone(),
984+
test_id,
985+
}
986+
}
987+
}
988+
900989
pub(crate) trait Tester {
901990
fn add_test(&mut self, test: String, config: LangString, line: usize);
902991
fn get_line(&self) -> usize {
@@ -941,6 +1030,7 @@ pub(crate) struct Collector {
9411030
visited_tests: FxHashMap<(String, usize), usize>,
9421031
unused_extern_reports: Arc<Mutex<Vec<UnusedExterns>>>,
9431032
compiling_test_count: AtomicUsize,
1033+
arg_file: PathBuf,
9441034
}
9451035

9461036
impl Collector {
@@ -952,6 +1042,7 @@ impl Collector {
9521042
source_map: Option<Lrc<SourceMap>>,
9531043
filename: Option<PathBuf>,
9541044
enable_per_target_ignores: bool,
1045+
arg_file: PathBuf,
9551046
) -> Collector {
9561047
Collector {
9571048
tests: Vec::new(),
@@ -967,6 +1058,7 @@ impl Collector {
9671058
visited_tests: FxHashMap::default(),
9681059
unused_extern_reports: Default::default(),
9691060
compiling_test_count: AtomicUsize::new(0),
1061+
arg_file,
9701062
}
9711063
}
9721064

@@ -1009,13 +1101,9 @@ impl Tester for Collector {
10091101
let crate_name = self.crate_name.clone();
10101102
let opts = self.opts.clone();
10111103
let edition = config.edition.unwrap_or(self.rustdoc_options.edition);
1012-
let rustdoc_options = self.rustdoc_options.clone();
1013-
let runtool = self.rustdoc_options.runtool.clone();
1014-
let runtool_args = self.rustdoc_options.runtool_args.clone();
1015-
let target = self.rustdoc_options.target.clone();
1016-
let target_str = target.to_string();
1104+
let target_str = self.rustdoc_options.target.to_string();
10171105
let unused_externs = self.unused_extern_reports.clone();
1018-
let no_run = config.no_run || rustdoc_options.no_run;
1106+
let no_run = config.no_run || self.rustdoc_options.no_run;
10191107
if !config.compile_fail {
10201108
self.compiling_test_count.fetch_add(1, Ordering::SeqCst);
10211109
}
@@ -1049,23 +1137,9 @@ impl Tester for Collector {
10491137
self.visited_tests.entry((file.clone(), line)).and_modify(|v| *v += 1).or_insert(0)
10501138
},
10511139
);
1052-
let outdir = if let Some(mut path) = rustdoc_options.persist_doctests.clone() {
1053-
path.push(&test_id);
10541140

1055-
if let Err(err) = std::fs::create_dir_all(&path) {
1056-
eprintln!("Couldn't create directory for doctest executables: {err}");
1057-
panic::resume_unwind(Box::new(()));
1058-
}
1059-
1060-
DirState::Perm(path)
1061-
} else {
1062-
DirState::Temp(
1063-
TempFileBuilder::new()
1064-
.prefix("rustdoctest")
1065-
.tempdir()
1066-
.expect("rustdoc needs a tempdir"),
1067-
)
1068-
};
1141+
let rustdoc_test_options =
1142+
IndividualTestOptions::new(&self.rustdoc_options, &self.arg_file, test_id);
10691143

10701144
debug!("creating test {name}: {test}");
10711145
self.tests.push(test::TestDescAndFn {
@@ -1096,17 +1170,12 @@ impl Tester for Collector {
10961170
&test,
10971171
&crate_name,
10981172
line,
1099-
rustdoc_options,
1173+
rustdoc_test_options,
11001174
config,
11011175
no_run,
1102-
runtool,
1103-
runtool_args,
1104-
target,
11051176
&opts,
11061177
edition,
1107-
outdir,
11081178
path,
1109-
&test_id,
11101179
report_unused_externs,
11111180
);
11121181

src/librustdoc/lib.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -664,7 +664,7 @@ fn usage(argv0: &str) {
664664
/// A result type used by several functions under `main()`.
665665
type MainResult = Result<(), ErrorGuaranteed>;
666666

667-
fn wrap_return(dcx: &rustc_errors::DiagCtxt, res: Result<(), String>) -> MainResult {
667+
pub(crate) fn wrap_return(dcx: &rustc_errors::DiagCtxt, res: Result<(), String>) -> MainResult {
668668
match res {
669669
Ok(()) => dcx.has_errors().map_or(Ok(()), Err),
670670
Err(err) => Err(dcx.err(err)),
@@ -731,7 +731,7 @@ fn main_args(
731731

732732
match (options.should_test, options.markdown_input()) {
733733
(true, true) => return wrap_return(&diag, markdown::test(options)),
734-
(true, false) => return doctest::run(options),
734+
(true, false) => return doctest::run(&diag, options),
735735
(false, true) => {
736736
let input = options.input.clone();
737737
let edition = options.edition;

0 commit comments

Comments
 (0)