From 507a90b79bc14efe0880c67601de9773aeea043f Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Sun, 13 Jul 2025 20:38:12 +0200 Subject: [PATCH 1/5] Correctly handle `--no-run` rustdoc test option --- src/librustdoc/doctest.rs | 2 +- src/librustdoc/doctest/runner.rs | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index 2ab4052fedff9..285c3d7ed9c3d 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -351,7 +351,7 @@ pub(crate) fn run_tests( ); for (doctest, scraped_test) in &doctests { - tests_runner.add_test(doctest, scraped_test, &target_str); + tests_runner.add_test(doctest, scraped_test, &target_str, rustdoc_options); } let (duration, ret) = tests_runner.run_merged_tests( rustdoc_test_options, diff --git a/src/librustdoc/doctest/runner.rs b/src/librustdoc/doctest/runner.rs index fcfa424968e48..5493d56456872 100644 --- a/src/librustdoc/doctest/runner.rs +++ b/src/librustdoc/doctest/runner.rs @@ -39,6 +39,7 @@ impl DocTestRunner { doctest: &DocTestBuilder, scraped_test: &ScrapedDocTest, target_str: &str, + opts: &RustdocOptions, ) { let ignore = match scraped_test.langstr.ignore { Ignore::All => true, @@ -62,6 +63,7 @@ impl DocTestRunner { self.nb_tests, &mut self.output, &mut self.output_merged_tests, + opts, ), )); self.supports_color &= doctest.supports_color; @@ -223,6 +225,7 @@ fn generate_mergeable_doctest( id: usize, output: &mut String, output_merged_tests: &mut String, + opts: &RustdocOptions, ) -> String { let test_id = format!("__doctest_{id}"); @@ -256,7 +259,7 @@ fn main() {returns_result} {{ ) .unwrap(); } - let not_running = ignore || scraped_test.langstr.no_run; + let not_running = ignore || scraped_test.no_run(opts); writeln!( output_merged_tests, " @@ -270,7 +273,7 @@ test::StaticTestFn( test_name = scraped_test.name, file = scraped_test.path(), line = scraped_test.line, - no_run = scraped_test.langstr.no_run, + no_run = scraped_test.no_run(opts), should_panic = !scraped_test.langstr.no_run && scraped_test.langstr.should_panic, // Setting `no_run` to `true` in `TestDesc` still makes the test run, so we simply // don't give it the function to run. From 630702bfa13e7a8de1546e70243143cf49d57d66 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 14 Oct 2025 01:23:15 +0200 Subject: [PATCH 2/5] Fix `should_panic` on merged doctests --- library/test/src/lib.rs | 4 +- library/test/src/test_result.rs | 117 ++++++++++++++++-- src/librustdoc/doctest.rs | 96 ++------------ src/librustdoc/doctest/runner.rs | 34 ++--- .../failed-doctest-should-panic-2021.stdout | 2 +- .../failed-doctest-should-panic.stdout | 4 +- .../rustdoc-ui/doctest/wrong-ast-2024.stdout | 2 +- 7 files changed, 139 insertions(+), 120 deletions(-) diff --git a/library/test/src/lib.rs b/library/test/src/lib.rs index d554807bbde70..8aaf579422f61 100644 --- a/library/test/src/lib.rs +++ b/library/test/src/lib.rs @@ -45,7 +45,9 @@ pub mod test { pub use crate::cli::{TestOpts, parse_opts}; pub use crate::helpers::metrics::{Metric, MetricMap}; pub use crate::options::{Options, RunIgnored, RunStrategy, ShouldPanic}; - pub use crate::test_result::{TestResult, TrFailed, TrFailedMsg, TrIgnored, TrOk}; + pub use crate::test_result::{ + RustdocResult, TestResult, TrFailed, TrFailedMsg, TrIgnored, TrOk, get_rustdoc_result, + }; pub use crate::time::{TestExecTime, TestTimeOptions}; pub use crate::types::{ DynTestFn, DynTestName, StaticBenchFn, StaticTestFn, StaticTestName, TestDesc, diff --git a/library/test/src/test_result.rs b/library/test/src/test_result.rs index 4cb43fc45fd6c..dea1831db0509 100644 --- a/library/test/src/test_result.rs +++ b/library/test/src/test_result.rs @@ -1,7 +1,8 @@ use std::any::Any; #[cfg(unix)] use std::os::unix::process::ExitStatusExt; -use std::process::ExitStatus; +use std::process::{ExitStatus, Output}; +use std::{fmt, io}; pub use self::TestResult::*; use super::bench::BenchSamples; @@ -103,15 +104,14 @@ pub(crate) fn calc_result( result } -/// Creates a `TestResult` depending on the exit code of test subprocess. -pub(crate) fn get_result_from_exit_code( - desc: &TestDesc, +/// Creates a `TestResult` depending on the exit code of test subprocess +pub(crate) fn get_result_from_exit_code_inner( status: ExitStatus, - time_opts: Option<&time::TestTimeOptions>, - exec_time: Option<&time::TestExecTime>, + success_error_code: i32, ) -> TestResult { - let result = match status.code() { - Some(TR_OK) => TestResult::TrOk, + match status.code() { + Some(error_code) if error_code == success_error_code => TestResult::TrOk, + Some(crate::ERROR_EXIT_CODE) => TestResult::TrFailed, #[cfg(windows)] Some(STATUS_FAIL_FAST_EXCEPTION) => TestResult::TrFailed, #[cfg(unix)] @@ -131,7 +131,17 @@ pub(crate) fn get_result_from_exit_code( Some(code) => TestResult::TrFailedMsg(format!("got unexpected return code {code}")), #[cfg(not(any(windows, unix)))] Some(_) => TestResult::TrFailed, - }; + } +} + +/// Creates a `TestResult` depending on the exit code of test subprocess and on its runtime. +pub(crate) fn get_result_from_exit_code( + desc: &TestDesc, + status: ExitStatus, + time_opts: Option<&time::TestTimeOptions>, + exec_time: Option<&time::TestExecTime>, +) -> TestResult { + let result = get_result_from_exit_code_inner(status, TR_OK); // If test is already failed (or allowed to fail), do not change the result. if result != TestResult::TrOk { @@ -147,3 +157,92 @@ pub(crate) fn get_result_from_exit_code( result } + +pub enum RustdocResult { + /// The test failed to compile. + CompileError, + /// The test is marked `compile_fail` but compiled successfully. + UnexpectedCompilePass, + /// The test failed to compile (as expected) but the compiler output did not contain all + /// expected error codes. + MissingErrorCodes(Vec), + /// The test binary was unable to be executed. + ExecutionError(io::Error), + /// The test binary exited with a non-zero exit code. + /// + /// This typically means an assertion in the test failed or another form of panic occurred. + ExecutionFailure(Output), + /// The test is marked `should_panic` but the test binary executed successfully. + NoPanic(Option), +} + +impl fmt::Display for RustdocResult { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::CompileError => { + write!(f, "Couldn't compile the test.") + } + Self::UnexpectedCompilePass => { + write!(f, "Test compiled successfully, but it's marked `compile_fail`.") + } + Self::NoPanic(msg) => { + write!(f, "Test didn't panic, but it's marked `should_panic`")?; + if let Some(msg) = msg { + write!(f, " ({msg})")?; + } + f.write_str(".") + } + Self::MissingErrorCodes(codes) => { + write!(f, "Some expected error codes were not found: {codes:?}") + } + Self::ExecutionError(err) => { + write!(f, "Couldn't run the test: {err}")?; + if err.kind() == io::ErrorKind::PermissionDenied { + f.write_str(" - maybe your tempdir is mounted with noexec?")?; + } + Ok(()) + } + Self::ExecutionFailure(out) => { + writeln!(f, "Test executable failed ({reason}).", reason = out.status)?; + + // FIXME(#12309): An unfortunate side-effect of capturing the test + // executable's output is that the relative ordering between the test's + // stdout and stderr is lost. However, this is better than the + // alternative: if the test executable inherited the parent's I/O + // handles the output wouldn't be captured at all, even on success. + // + // The ordering could be preserved if the test process' stderr was + // redirected to stdout, but that functionality does not exist in the + // standard library, so it may not be portable enough. + let stdout = str::from_utf8(&out.stdout).unwrap_or_default(); + let stderr = str::from_utf8(&out.stderr).unwrap_or_default(); + + if !stdout.is_empty() || !stderr.is_empty() { + writeln!(f)?; + + if !stdout.is_empty() { + writeln!(f, "stdout:\n{stdout}")?; + } + + if !stderr.is_empty() { + writeln!(f, "stderr:\n{stderr}")?; + } + } + Ok(()) + } + } + } +} + +pub fn get_rustdoc_result(output: Output, should_panic: bool) -> Result<(), RustdocResult> { + let result = get_result_from_exit_code_inner(output.status, 0); + match (result, should_panic) { + (TestResult::TrFailed, true) | (TestResult::TrOk, false) => Ok(()), + (TestResult::TrOk, true) => Err(RustdocResult::NoPanic(None)), + (TestResult::TrFailedMsg(msg), true) => Err(RustdocResult::NoPanic(Some(msg))), + (TestResult::TrFailedMsg(_) | TestResult::TrFailed, false) => { + Err(RustdocResult::ExecutionFailure(output)) + } + _ => unreachable!("unexpected status for rustdoc test output"), + } +} diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index 285c3d7ed9c3d..62e67c39ef148 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -30,6 +30,7 @@ use rustc_span::symbol::sym; use rustc_span::{FileName, Span}; use rustc_target::spec::{Target, TargetTuple}; use tempfile::{Builder as TempFileBuilder, TempDir}; +use test::test::{RustdocResult, get_rustdoc_result}; use tracing::debug; use self::rust::HirCollector; @@ -446,25 +447,6 @@ fn scrape_test_config( opts } -/// Documentation test failure modes. -enum TestFailure { - /// The test failed to compile. - CompileError, - /// The test is marked `compile_fail` but compiled successfully. - UnexpectedCompilePass, - /// The test failed to compile (as expected) but the compiler output did not contain all - /// expected error codes. - MissingErrorCodes(Vec), - /// The test binary was unable to be executed. - ExecutionError(io::Error), - /// The test binary exited with a non-zero exit code. - /// - /// This typically means an assertion in the test failed or another form of panic occurred. - ExecutionFailure(process::Output), - /// The test is marked `should_panic` but the test binary executed successfully. - UnexpectedRunPass, -} - enum DirState { Temp(TempDir), Perm(PathBuf), @@ -554,7 +536,7 @@ fn run_test( rustdoc_options: &RustdocOptions, supports_color: bool, report_unused_externs: impl Fn(UnusedExterns), -) -> (Duration, Result<(), TestFailure>) { +) -> (Duration, Result<(), RustdocResult>) { 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); @@ -643,7 +625,7 @@ fn run_test( if std::fs::write(&input_file, &doctest.full_test_code).is_err() { // If we cannot write this file for any reason, we leave. All combined tests will be // tested as standalone tests. - return (Duration::default(), Err(TestFailure::CompileError)); + return (Duration::default(), Err(RustdocResult::CompileError)); } if !rustdoc_options.nocapture { // If `nocapture` is disabled, then we don't display rustc's output when compiling @@ -720,7 +702,7 @@ fn run_test( if std::fs::write(&runner_input_file, merged_test_code).is_err() { // If we cannot write this file for any reason, we leave. All combined tests will be // tested as standalone tests. - return (instant.elapsed(), Err(TestFailure::CompileError)); + return (instant.elapsed(), Err(RustdocResult::CompileError)); } if !rustdoc_options.nocapture { // If `nocapture` is disabled, then we don't display rustc's output when compiling @@ -773,7 +755,7 @@ fn run_test( let _bomb = Bomb(&out); match (output.status.success(), langstr.compile_fail) { (true, true) => { - return (instant.elapsed(), Err(TestFailure::UnexpectedCompilePass)); + return (instant.elapsed(), Err(RustdocResult::UnexpectedCompilePass)); } (true, false) => {} (false, true) => { @@ -789,12 +771,15 @@ fn run_test( .collect(); if !missing_codes.is_empty() { - return (instant.elapsed(), Err(TestFailure::MissingErrorCodes(missing_codes))); + return ( + instant.elapsed(), + Err(RustdocResult::MissingErrorCodes(missing_codes)), + ); } } } (false, false) => { - return (instant.elapsed(), Err(TestFailure::CompileError)); + return (instant.elapsed(), Err(RustdocResult::CompileError)); } } @@ -832,17 +817,9 @@ fn run_test( cmd.output() }; match result { - Err(e) => return (duration, Err(TestFailure::ExecutionError(e))), - Ok(out) => { - if langstr.should_panic && out.status.success() { - return (duration, Err(TestFailure::UnexpectedRunPass)); - } else if !langstr.should_panic && !out.status.success() { - return (duration, Err(TestFailure::ExecutionFailure(out))); - } - } + Err(e) => (duration, Err(RustdocResult::ExecutionError(e))), + Ok(output) => (duration, get_rustdoc_result(output, langstr.should_panic)), } - - (duration, Ok(())) } /// Converts a path intended to use as a command to absolute if it is @@ -1136,54 +1113,7 @@ fn doctest_run_fn( run_test(runnable_test, &rustdoc_options, doctest.supports_color, report_unused_externs); if let Err(err) = res { - match err { - TestFailure::CompileError => { - eprint!("Couldn't compile the test."); - } - TestFailure::UnexpectedCompilePass => { - eprint!("Test compiled successfully, but it's marked `compile_fail`."); - } - TestFailure::UnexpectedRunPass => { - eprint!("Test executable succeeded, but it's marked `should_panic`."); - } - TestFailure::MissingErrorCodes(codes) => { - eprint!("Some expected error codes were not found: {codes:?}"); - } - TestFailure::ExecutionError(err) => { - eprint!("Couldn't run the test: {err}"); - if err.kind() == io::ErrorKind::PermissionDenied { - eprint!(" - maybe your tempdir is mounted with noexec?"); - } - } - TestFailure::ExecutionFailure(out) => { - eprintln!("Test executable failed ({reason}).", reason = out.status); - - // FIXME(#12309): An unfortunate side-effect of capturing the test - // executable's output is that the relative ordering between the test's - // stdout and stderr is lost. However, this is better than the - // alternative: if the test executable inherited the parent's I/O - // handles the output wouldn't be captured at all, even on success. - // - // The ordering could be preserved if the test process' stderr was - // redirected to stdout, but that functionality does not exist in the - // standard library, so it may not be portable enough. - let stdout = str::from_utf8(&out.stdout).unwrap_or_default(); - let stderr = str::from_utf8(&out.stderr).unwrap_or_default(); - - if !stdout.is_empty() || !stderr.is_empty() { - eprintln!(); - - if !stdout.is_empty() { - eprintln!("stdout:\n{stdout}"); - } - - if !stderr.is_empty() { - eprintln!("stderr:\n{stderr}"); - } - } - } - } - + eprint!("{err}"); panic::resume_unwind(Box::new(())); } Ok(()) diff --git a/src/librustdoc/doctest/runner.rs b/src/librustdoc/doctest/runner.rs index 5493d56456872..0a4ca67966242 100644 --- a/src/librustdoc/doctest/runner.rs +++ b/src/librustdoc/doctest/runner.rs @@ -6,7 +6,7 @@ use rustc_span::edition::Edition; use crate::doctest::{ DocTestBuilder, GlobalTestOptions, IndividualTestOptions, RunnableDocTest, RustdocOptions, - ScrapedDocTest, TestFailure, UnusedExterns, run_test, + RustdocResult, ScrapedDocTest, UnusedExterns, run_test, }; use crate::html::markdown::{Ignore, LangString}; @@ -136,29 +136,14 @@ mod __doctest_mod {{ }} #[allow(unused)] - pub fn doctest_runner(bin: &std::path::Path, test_nb: usize) -> ExitCode {{ + pub fn doctest_runner(bin: &std::path::Path, test_nb: usize, should_panic: bool) -> ExitCode {{ let out = std::process::Command::new(bin) .env(self::RUN_OPTION, test_nb.to_string()) .args(std::env::args().skip(1).collect::>()) .output() .expect(\"failed to run command\"); - if !out.status.success() {{ - if let Some(code) = out.status.code() {{ - eprintln!(\"Test executable failed (exit status: {{code}}).\"); - }} else {{ - eprintln!(\"Test executable failed (terminated by signal).\"); - }} - if !out.stdout.is_empty() || !out.stderr.is_empty() {{ - eprintln!(); - }} - if !out.stdout.is_empty() {{ - eprintln!(\"stdout:\"); - eprintln!(\"{{}}\", String::from_utf8_lossy(&out.stdout)); - }} - if !out.stderr.is_empty() {{ - eprintln!(\"stderr:\"); - eprintln!(\"{{}}\", String::from_utf8_lossy(&out.stderr)); - }} + if let Err(err) = test::test::get_rustdoc_result(out, should_panic) {{ + eprint!(\"{{err}}\"); ExitCode::FAILURE }} else {{ ExitCode::SUCCESS @@ -213,7 +198,10 @@ std::process::Termination::report(test::test_main(test_args, tests, None)) }; let (duration, ret) = run_test(runnable_test, rustdoc_options, self.supports_color, |_: UnusedExterns| {}); - (duration, if let Err(TestFailure::CompileError) = ret { Err(()) } else { Ok(ret.is_ok()) }) + ( + duration, + if let Err(RustdocResult::CompileError) = ret { Err(()) } else { Ok(ret.is_ok()) }, + ) } } @@ -265,7 +253,7 @@ fn main() {returns_result} {{ " mod {test_id} {{ pub const TEST: test::TestDescAndFn = test::TestDescAndFn::new_doctest( -{test_name:?}, {ignore}, {file:?}, {line}, {no_run}, {should_panic}, +{test_name:?}, {ignore}, {file:?}, {line}, {no_run}, false, test::StaticTestFn( || {{{runner}}}, )); @@ -274,7 +262,6 @@ test::StaticTestFn( file = scraped_test.path(), line = scraped_test.line, no_run = scraped_test.no_run(opts), - should_panic = !scraped_test.langstr.no_run && scraped_test.langstr.should_panic, // 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 not_running { @@ -283,11 +270,12 @@ test::StaticTestFn( format!( " if let Some(bin_path) = crate::__doctest_mod::doctest_path() {{ - test::assert_test_result(crate::__doctest_mod::doctest_runner(bin_path, {id})) + test::assert_test_result(crate::__doctest_mod::doctest_runner(bin_path, {id}, {should_panic})) }} else {{ test::assert_test_result(doctest_bundle::{test_id}::__main_fn()) }} ", + should_panic = scraped_test.langstr.should_panic, ) }, ) diff --git a/tests/rustdoc-ui/doctest/failed-doctest-should-panic-2021.stdout b/tests/rustdoc-ui/doctest/failed-doctest-should-panic-2021.stdout index 9f4d60e6f4de5..f8413756e3d6d 100644 --- a/tests/rustdoc-ui/doctest/failed-doctest-should-panic-2021.stdout +++ b/tests/rustdoc-ui/doctest/failed-doctest-should-panic-2021.stdout @@ -5,7 +5,7 @@ test $DIR/failed-doctest-should-panic-2021.rs - Foo (line 10) ... FAILED failures: ---- $DIR/failed-doctest-should-panic-2021.rs - Foo (line 10) stdout ---- -Test executable succeeded, but it's marked `should_panic`. +Test didn't panic, but it's marked `should_panic`. failures: $DIR/failed-doctest-should-panic-2021.rs - Foo (line 10) diff --git a/tests/rustdoc-ui/doctest/failed-doctest-should-panic.stdout b/tests/rustdoc-ui/doctest/failed-doctest-should-panic.stdout index 9047fe0dcdd93..8865fb4e40425 100644 --- a/tests/rustdoc-ui/doctest/failed-doctest-should-panic.stdout +++ b/tests/rustdoc-ui/doctest/failed-doctest-should-panic.stdout @@ -1,11 +1,11 @@ running 1 test -test $DIR/failed-doctest-should-panic.rs - Foo (line 12) - should panic ... FAILED +test $DIR/failed-doctest-should-panic.rs - Foo (line 12) ... FAILED failures: ---- $DIR/failed-doctest-should-panic.rs - Foo (line 12) stdout ---- -note: test did not panic as expected at $DIR/failed-doctest-should-panic.rs:12:0 +Test didn't panic, but it's marked `should_panic`. failures: $DIR/failed-doctest-should-panic.rs - Foo (line 12) diff --git a/tests/rustdoc-ui/doctest/wrong-ast-2024.stdout b/tests/rustdoc-ui/doctest/wrong-ast-2024.stdout index 13567b41e51f5..27f9a0157a6cc 100644 --- a/tests/rustdoc-ui/doctest/wrong-ast-2024.stdout +++ b/tests/rustdoc-ui/doctest/wrong-ast-2024.stdout @@ -1,6 +1,6 @@ running 1 test -test $DIR/wrong-ast-2024.rs - three (line 20) - should panic ... ok +test $DIR/wrong-ast-2024.rs - three (line 20) ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME From b292354589f1539e5b2082850858e66deafe459a Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 14 Oct 2025 01:37:31 +0200 Subject: [PATCH 3/5] Add regression tests for `no_run` and `compile_fail` --- tests/run-make/rustdoc-should-panic/rmake.rs | 43 +++++++++++++++++ tests/run-make/rustdoc-should-panic/test.rs | 14 ++++++ .../doctest/failed-doctest-should-panic.rs | 13 ++++-- .../failed-doctest-should-panic.stdout | 14 ++++-- .../doctest/no-run.edition2021.stdout | 12 +++++ .../doctest/no-run.edition2024.stdout | 18 ++++++++ tests/rustdoc-ui/doctest/no-run.rs | 46 +++++++++++++++++++ 7 files changed, 150 insertions(+), 10 deletions(-) create mode 100644 tests/run-make/rustdoc-should-panic/rmake.rs create mode 100644 tests/run-make/rustdoc-should-panic/test.rs create mode 100644 tests/rustdoc-ui/doctest/no-run.edition2021.stdout create mode 100644 tests/rustdoc-ui/doctest/no-run.edition2024.stdout create mode 100644 tests/rustdoc-ui/doctest/no-run.rs diff --git a/tests/run-make/rustdoc-should-panic/rmake.rs b/tests/run-make/rustdoc-should-panic/rmake.rs new file mode 100644 index 0000000000000..07826768b88db --- /dev/null +++ b/tests/run-make/rustdoc-should-panic/rmake.rs @@ -0,0 +1,43 @@ +// Ensure that `should_panic` doctests only succeed if the test actually panicked. +// Regression test for . + +//@ ignore-cross-compile + +use run_make_support::rustdoc; + +fn check_output(edition: &str, panic_abort: bool) { + let mut rustdoc_cmd = rustdoc(); + rustdoc_cmd.input("test.rs").arg("--test").edition(edition); + if panic_abort { + rustdoc_cmd.args(["-C", "panic=abort"]); + } + let output = rustdoc_cmd.run_fail().stdout_utf8(); + let should_contain = &[ + "test test.rs - bad_exit_code (line 1) ... FAILED", + "test test.rs - did_not_panic (line 6) ... FAILED", + "test test.rs - did_panic (line 11) ... ok", + "---- test.rs - bad_exit_code (line 1) stdout ---- +Test executable failed (exit status: 1).", + "---- test.rs - did_not_panic (line 6) stdout ---- +Test didn't panic, but it's marked `should_panic` (got unexpected return code 1).", + "test result: FAILED. 1 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out;", + ]; + for text in should_contain { + assert!( + output.contains(text), + "output (edition: {edition}) doesn't contain {:?}\nfull output: {output}", + text + ); + } +} + +fn main() { + check_output("2015", false); + + // Same check with the merged doctest feature (enabled with the 2024 edition). + check_output("2024", false); + + // Checking that `-C panic=abort` is working too. + check_output("2015", true); + check_output("2024", true); +} diff --git a/tests/run-make/rustdoc-should-panic/test.rs b/tests/run-make/rustdoc-should-panic/test.rs new file mode 100644 index 0000000000000..1eea8e1e1958c --- /dev/null +++ b/tests/run-make/rustdoc-should-panic/test.rs @@ -0,0 +1,14 @@ +/// ``` +/// std::process::exit(1); +/// ``` +fn bad_exit_code() {} + +/// ```should_panic +/// std::process::exit(1); +/// ``` +fn did_not_panic() {} + +/// ```should_panic +/// panic!("yeay"); +/// ``` +fn did_panic() {} diff --git a/tests/rustdoc-ui/doctest/failed-doctest-should-panic.rs b/tests/rustdoc-ui/doctest/failed-doctest-should-panic.rs index 0504c3dc73033..b95e23715175f 100644 --- a/tests/rustdoc-ui/doctest/failed-doctest-should-panic.rs +++ b/tests/rustdoc-ui/doctest/failed-doctest-should-panic.rs @@ -2,14 +2,17 @@ // adapted to use that, and that normalize line can go away //@ edition: 2024 -//@ compile-flags:--test +//@ compile-flags:--test --test-args=--test-threads=1 //@ 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" //@ failure-status: 101 -/// ```should_panic -/// println!("Hello, world!"); -/// ``` -pub struct Foo; +//! ```should_panic +//! println!("Hello, world!"); +//! ``` +//! +//! ```should_panic +//! std::process::exit(2); +//! ``` diff --git a/tests/rustdoc-ui/doctest/failed-doctest-should-panic.stdout b/tests/rustdoc-ui/doctest/failed-doctest-should-panic.stdout index 8865fb4e40425..10172ea79226d 100644 --- a/tests/rustdoc-ui/doctest/failed-doctest-should-panic.stdout +++ b/tests/rustdoc-ui/doctest/failed-doctest-should-panic.stdout @@ -1,15 +1,19 @@ -running 1 test -test $DIR/failed-doctest-should-panic.rs - Foo (line 12) ... FAILED +running 2 tests +test $DIR/failed-doctest-should-panic.rs - (line 12) ... FAILED +test $DIR/failed-doctest-should-panic.rs - (line 16) ... FAILED failures: ----- $DIR/failed-doctest-should-panic.rs - Foo (line 12) stdout ---- +---- $DIR/failed-doctest-should-panic.rs - (line 12) stdout ---- Test didn't panic, but it's marked `should_panic`. +---- $DIR/failed-doctest-should-panic.rs - (line 16) stdout ---- +Test didn't panic, but it's marked `should_panic` (got unexpected return code 2). failures: - $DIR/failed-doctest-should-panic.rs - Foo (line 12) + $DIR/failed-doctest-should-panic.rs - (line 12) + $DIR/failed-doctest-should-panic.rs - (line 16) -test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME +test result: FAILED. 0 passed; 2 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/no-run.edition2021.stdout b/tests/rustdoc-ui/doctest/no-run.edition2021.stdout new file mode 100644 index 0000000000000..2b8232d18eba8 --- /dev/null +++ b/tests/rustdoc-ui/doctest/no-run.edition2021.stdout @@ -0,0 +1,12 @@ + +running 7 tests +test $DIR/no-run.rs - f (line 14) - compile ... ok +test $DIR/no-run.rs - f (line 17) - compile ... ok +test $DIR/no-run.rs - f (line 20) ... ignored +test $DIR/no-run.rs - f (line 23) - compile ... ok +test $DIR/no-run.rs - f (line 31) - compile fail ... ok +test $DIR/no-run.rs - f (line 36) - compile ... ok +test $DIR/no-run.rs - f (line 40) - compile ... ok + +test result: ok. 6 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in $TIME + diff --git a/tests/rustdoc-ui/doctest/no-run.edition2024.stdout b/tests/rustdoc-ui/doctest/no-run.edition2024.stdout new file mode 100644 index 0000000000000..30d9c5d5fc769 --- /dev/null +++ b/tests/rustdoc-ui/doctest/no-run.edition2024.stdout @@ -0,0 +1,18 @@ + +running 5 tests +test $DIR/no-run.rs - f (line 14) - compile ... ok +test $DIR/no-run.rs - f (line 17) - compile ... ok +test $DIR/no-run.rs - f (line 23) - compile ... ok +test $DIR/no-run.rs - f (line 36) - compile ... ok +test $DIR/no-run.rs - f (line 40) - compile ... ok + +test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME + + +running 2 tests +test $DIR/no-run.rs - f (line 20) ... ignored +test $DIR/no-run.rs - f (line 31) - compile fail ... ok + +test result: ok. 1 passed; 0 failed; 1 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/no-run.rs b/tests/rustdoc-ui/doctest/no-run.rs new file mode 100644 index 0000000000000..7b8f0ddc3f07a --- /dev/null +++ b/tests/rustdoc-ui/doctest/no-run.rs @@ -0,0 +1,46 @@ +// This test ensures that the `--no-run` flag works the same between normal and merged doctests. +// Regression test for . + +//@ check-pass +//@ revisions: edition2021 edition2024 +//@ [edition2021]edition:2021 +//@ [edition2024]edition:2024 +//@ compile-flags:-Z unstable-options --test --no-run --test-args=--test-threads=1 +//@ 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" + +/// ``` +/// let a = true; +/// ``` +/// ```should_panic +/// panic!() +/// ``` +/// ```ignore (incomplete-code) +/// fn foo() { +/// ``` +/// ```no_run +/// loop { +/// println!("Hello, world"); +/// } +/// ``` +/// +/// fails to compile +/// +/// ```compile_fail +/// let x = 5; +/// x += 2; // shouldn't compile! +/// ``` +/// Ok the test does not run +/// ``` +/// panic!() +/// ``` +/// Ok the test does not run +/// ```should_panic +/// loop { +/// println!("Hello, world"); +/// panic!() +/// } +/// ``` +pub fn f() {} From a47e09f3903d22393c2f4bb5627a41c5089d426d Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 14 Oct 2025 14:22:30 +0200 Subject: [PATCH 4/5] Fix stage 1 build --- src/librustdoc/doctest.rs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index 62e67c39ef148..117f9999281b1 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -4,6 +4,8 @@ mod markdown; mod runner; mod rust; +#[cfg(bootstrap)] +use std::fmt; use std::fs::File; use std::hash::{Hash, Hasher}; use std::io::{self, Write}; @@ -30,6 +32,7 @@ use rustc_span::symbol::sym; use rustc_span::{FileName, Span}; use rustc_target::spec::{Target, TargetTuple}; use tempfile::{Builder as TempFileBuilder, TempDir}; +#[cfg(not(bootstrap))] use test::test::{RustdocResult, get_rustdoc_result}; use tracing::debug; @@ -38,6 +41,38 @@ use crate::config::{Options as RustdocOptions, OutputFormat}; use crate::html::markdown::{ErrorCodes, Ignore, LangString, MdRelLine}; use crate::lint::init_lints; +#[cfg(bootstrap)] +#[allow(dead_code)] +pub enum RustdocResult { + /// The test failed to compile. + CompileError, + /// The test is marked `compile_fail` but compiled successfully. + UnexpectedCompilePass, + /// The test failed to compile (as expected) but the compiler output did not contain all + /// expected error codes. + MissingErrorCodes(Vec), + /// The test binary was unable to be executed. + ExecutionError(io::Error), + /// The test binary exited with a non-zero exit code. + /// + /// This typically means an assertion in the test failed or another form of panic occurred. + ExecutionFailure(process::Output), + /// The test is marked `should_panic` but the test binary executed successfully. + NoPanic(Option), +} + +#[cfg(bootstrap)] +impl fmt::Display for RustdocResult { + fn fmt(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result { + Ok(()) + } +} + +#[cfg(bootstrap)] +fn get_rustdoc_result(_: process::Output, _: bool) -> Result<(), RustdocResult> { + Ok(()) +} + /// Type used to display times (compilation and total) information for merged doctests. struct MergedDoctestTimes { total_time: Instant, From adaf3e087754dab61c44cb5c33e294708c0653df Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 14 Oct 2025 23:04:54 +0200 Subject: [PATCH 5/5] Update std doctests --- library/std/src/error.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/library/std/src/error.rs b/library/std/src/error.rs index def5f984c88e4..09bfc83ebca6c 100644 --- a/library/std/src/error.rs +++ b/library/std/src/error.rs @@ -123,7 +123,7 @@ use crate::fmt::{self, Write}; /// the `Debug` output means `Report` is an ideal starting place for formatting errors returned /// from `main`. /// -/// ```should_panic +/// ``` /// #![feature(error_reporter)] /// use std::error::Report; /// # use std::error::Error; @@ -154,10 +154,14 @@ use crate::fmt::{self, Write}; /// # Err(SuperError { source: SuperErrorSideKick }) /// # } /// -/// fn main() -> Result<(), Report> { +/// fn run() -> Result<(), Report> { /// get_super_error()?; /// Ok(()) /// } +/// +/// fn main() { +/// assert!(run().is_err()); +/// } /// ``` /// /// This example produces the following output: @@ -170,7 +174,7 @@ use crate::fmt::{self, Write}; /// output format. If you want to make sure your `Report`s are pretty printed and include backtrace /// you will need to manually convert and enable those flags. /// -/// ```should_panic +/// ``` /// #![feature(error_reporter)] /// use std::error::Report; /// # use std::error::Error; @@ -201,12 +205,16 @@ use crate::fmt::{self, Write}; /// # Err(SuperError { source: SuperErrorSideKick }) /// # } /// -/// fn main() -> Result<(), Report> { +/// fn run() -> Result<(), Report> { /// get_super_error() /// .map_err(Report::from) /// .map_err(|r| r.pretty(true).show_backtrace(true))?; /// Ok(()) /// } +/// +/// fn main() { +/// assert!(run().is_err()); +/// } /// ``` /// /// This example produces the following output: