Skip to content

Commit 28c8cf6

Browse files
authored
Rollup merge of rust-lang#143900 - GuillaumeGomez:fix-no-run, r=fmease
[rustdoc] Correctly handle `should_panic` doctest attribute and fix `--no-run` test flag on the 2024 edition Fixes rust-lang#143009. Fixes rust-lang#143858. Since it includes fixes from rust-lang#143453, it's taking it over (commits 2, 3 and 4 are from rust-lang#143453). For `--no-run`, we forgot to check the "global" options in the 2024 edition, fixed in the first commit. For `should_panic` fix, the exit code check has been fixed. cc ```@TroyKomodo``` (thanks so much for providing such a complete test, made my life a lot easier!) r? ```@notriddle```
2 parents 61efd19 + 6060bcc commit 28c8cf6

File tree

12 files changed

+323
-34
lines changed

12 files changed

+323
-34
lines changed

library/std/src/error.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ use crate::fmt::{self, Write};
123123
/// the `Debug` output means `Report` is an ideal starting place for formatting errors returned
124124
/// from `main`.
125125
///
126-
/// ```should_panic
126+
/// ```
127127
/// #![feature(error_reporter)]
128128
/// use std::error::Report;
129129
/// # use std::error::Error;
@@ -154,10 +154,14 @@ use crate::fmt::{self, Write};
154154
/// # Err(SuperError { source: SuperErrorSideKick })
155155
/// # }
156156
///
157-
/// fn main() -> Result<(), Report<SuperError>> {
157+
/// fn run() -> Result<(), Report<SuperError>> {
158158
/// get_super_error()?;
159159
/// Ok(())
160160
/// }
161+
///
162+
/// fn main() {
163+
/// assert!(run().is_err());
164+
/// }
161165
/// ```
162166
///
163167
/// This example produces the following output:
@@ -170,7 +174,7 @@ use crate::fmt::{self, Write};
170174
/// output format. If you want to make sure your `Report`s are pretty printed and include backtrace
171175
/// you will need to manually convert and enable those flags.
172176
///
173-
/// ```should_panic
177+
/// ```
174178
/// #![feature(error_reporter)]
175179
/// use std::error::Report;
176180
/// # use std::error::Error;
@@ -201,12 +205,16 @@ use crate::fmt::{self, Write};
201205
/// # Err(SuperError { source: SuperErrorSideKick })
202206
/// # }
203207
///
204-
/// fn main() -> Result<(), Report<SuperError>> {
208+
/// fn run() -> Result<(), Report<SuperError>> {
205209
/// get_super_error()
206210
/// .map_err(Report::from)
207211
/// .map_err(|r| r.pretty(true).show_backtrace(true))?;
208212
/// Ok(())
209213
/// }
214+
///
215+
/// fn main() {
216+
/// assert!(run().is_err());
217+
/// }
210218
/// ```
211219
///
212220
/// This example produces the following output:

src/librustdoc/doctest.rs

Lines changed: 89 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ mod rust;
77
use std::fs::File;
88
use std::hash::{Hash, Hasher};
99
use std::io::{self, Write};
10+
#[cfg(unix)]
11+
use std::os::unix::process::ExitStatusExt;
1012
use std::path::{Path, PathBuf};
1113
use std::process::{self, Command, Stdio};
1214
use std::sync::atomic::{AtomicUsize, Ordering};
@@ -351,7 +353,7 @@ pub(crate) fn run_tests(
351353
);
352354

353355
for (doctest, scraped_test) in &doctests {
354-
tests_runner.add_test(doctest, scraped_test, &target_str);
356+
tests_runner.add_test(doctest, scraped_test, &target_str, rustdoc_options);
355357
}
356358
let (duration, ret) = tests_runner.run_merged_tests(
357359
rustdoc_test_options,
@@ -461,8 +463,8 @@ enum TestFailure {
461463
///
462464
/// This typically means an assertion in the test failed or another form of panic occurred.
463465
ExecutionFailure(process::Output),
464-
/// The test is marked `should_panic` but the test binary executed successfully.
465-
UnexpectedRunPass,
466+
/// The test is marked `should_panic` but the test binary didn't panic.
467+
NoPanic(Option<String>),
466468
}
467469

468470
enum DirState {
@@ -801,6 +803,25 @@ fn run_test(
801803
let duration = instant.elapsed();
802804
if doctest.no_run {
803805
return (duration, Ok(()));
806+
} else if doctest.langstr.should_panic
807+
// Equivalent of:
808+
//
809+
// ```
810+
// (cfg!(target_family = "wasm") || cfg!(target_os = "zkvm"))
811+
// && !cfg!(target_os = "emscripten")
812+
// ```
813+
//
814+
// FIXME: All this code is terrible and doesn't take into account `TargetTuple::TargetJson`.
815+
// If `libtest` doesn't allow to handle this case, we'll need to use a rustc's API instead.
816+
&& let TargetTuple::TargetTuple(ref s) = rustdoc_options.target
817+
&& let mut iter = s.split('-')
818+
&& let Some(arch) = iter.next()
819+
&& iter.next().is_some()
820+
&& let os = iter.next()
821+
&& (arch.starts_with("wasm") || os == Some("zkvm")) && os != Some("emscripten")
822+
{
823+
// We cannot correctly handle `should_panic` in some wasm targets so we exit early.
824+
return (duration, Ok(()));
804825
}
805826

806827
// Run the code!
@@ -831,12 +852,68 @@ fn run_test(
831852
} else {
832853
cmd.output()
833854
};
855+
856+
// FIXME: Make `test::get_result_from_exit_code` public and use this code instead of this.
857+
//
858+
// On Zircon (the Fuchsia kernel), an abort from userspace calls the
859+
// LLVM implementation of __builtin_trap(), e.g., ud2 on x86, which
860+
// raises a kernel exception. If a userspace process does not
861+
// otherwise arrange exception handling, the kernel kills the process
862+
// with this return code.
863+
#[cfg(target_os = "fuchsia")]
864+
const ZX_TASK_RETCODE_EXCEPTION_KILL: i32 = -1028;
865+
// On Windows we use __fastfail to abort, which is documented to use this
866+
// exception code.
867+
#[cfg(windows)]
868+
const STATUS_FAIL_FAST_EXCEPTION: i32 = 0xC0000409u32 as i32;
869+
#[cfg(unix)]
870+
const SIGABRT: std::ffi::c_int = 6;
834871
match result {
835872
Err(e) => return (duration, Err(TestFailure::ExecutionError(e))),
836873
Ok(out) => {
837-
if langstr.should_panic && out.status.success() {
838-
return (duration, Err(TestFailure::UnexpectedRunPass));
839-
} else if !langstr.should_panic && !out.status.success() {
874+
if langstr.should_panic {
875+
match out.status.code() {
876+
Some(test::ERROR_EXIT_CODE) => {}
877+
#[cfg(windows)]
878+
Some(STATUS_FAIL_FAST_EXCEPTION) => {}
879+
#[cfg(unix)]
880+
None => match out.status.signal() {
881+
Some(SIGABRT) => {}
882+
Some(signal) => {
883+
return (
884+
duration,
885+
Err(TestFailure::NoPanic(Some(format!(
886+
"Test didn't panic, but it's marked `should_panic` (exit signal: {signal}).",
887+
)))),
888+
);
889+
}
890+
None => {
891+
return (
892+
duration,
893+
Err(TestFailure::NoPanic(Some(format!(
894+
"Test didn't panic, but it's marked `should_panic` and exited with no error code and no signal.",
895+
)))),
896+
);
897+
}
898+
},
899+
#[cfg(not(unix))]
900+
None => return (duration, Err(TestFailure::NoPanic(None))),
901+
// Upon an abort, Fuchsia returns the status code
902+
// `ZX_TASK_RETCODE_EXCEPTION_KILL`.
903+
#[cfg(target_os = "fuchsia")]
904+
Some(ZX_TASK_RETCODE_EXCEPTION_KILL) => {}
905+
Some(exit_code) => {
906+
let err_msg = if !out.status.success() {
907+
Some(format!(
908+
"Test didn't panic, but it's marked `should_panic` (exit status: {exit_code}).",
909+
))
910+
} else {
911+
None
912+
};
913+
return (duration, Err(TestFailure::NoPanic(err_msg)));
914+
}
915+
}
916+
} else if !out.status.success() {
840917
return (duration, Err(TestFailure::ExecutionFailure(out)));
841918
}
842919
}
@@ -1143,8 +1220,12 @@ fn doctest_run_fn(
11431220
TestFailure::UnexpectedCompilePass => {
11441221
eprint!("Test compiled successfully, but it's marked `compile_fail`.");
11451222
}
1146-
TestFailure::UnexpectedRunPass => {
1147-
eprint!("Test executable succeeded, but it's marked `should_panic`.");
1223+
TestFailure::NoPanic(msg) => {
1224+
if let Some(msg) = msg {
1225+
eprint!("{msg}");
1226+
} else {
1227+
eprint!("Test didn't panic, but it's marked `should_panic`.");
1228+
}
11481229
}
11491230
TestFailure::MissingErrorCodes(codes) => {
11501231
eprint!("Some expected error codes were not found: {codes:?}");

src/librustdoc/doctest/runner.rs

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ impl DocTestRunner {
3939
doctest: &DocTestBuilder,
4040
scraped_test: &ScrapedDocTest,
4141
target_str: &str,
42+
opts: &RustdocOptions,
4243
) {
4344
let ignore = match scraped_test.langstr.ignore {
4445
Ignore::All => true,
@@ -62,6 +63,7 @@ impl DocTestRunner {
6263
self.nb_tests,
6364
&mut self.output,
6465
&mut self.output_merged_tests,
66+
opts,
6567
),
6668
));
6769
self.supports_color &= doctest.supports_color;
@@ -121,26 +123,81 @@ impl DocTestRunner {
121123
{output}
122124
123125
mod __doctest_mod {{
124-
use std::sync::OnceLock;
126+
#[cfg(unix)]
127+
use std::os::unix::process::ExitStatusExt;
125128
use std::path::PathBuf;
126129
use std::process::ExitCode;
130+
use std::sync::OnceLock;
127131
128132
pub static BINARY_PATH: OnceLock<PathBuf> = OnceLock::new();
129133
pub const RUN_OPTION: &str = \"RUSTDOC_DOCTEST_RUN_NB_TEST\";
134+
pub const SHOULD_PANIC_DISABLED: bool = (
135+
cfg!(target_family = \"wasm\") || cfg!(target_os = \"zkvm\")
136+
) && !cfg!(target_os = \"emscripten\");
130137
131138
#[allow(unused)]
132139
pub fn doctest_path() -> Option<&'static PathBuf> {{
133140
self::BINARY_PATH.get()
134141
}}
135142
136143
#[allow(unused)]
137-
pub fn doctest_runner(bin: &std::path::Path, test_nb: usize) -> ExitCode {{
144+
pub fn doctest_runner(bin: &std::path::Path, test_nb: usize, should_panic: bool) -> ExitCode {{
138145
let out = std::process::Command::new(bin)
139146
.env(self::RUN_OPTION, test_nb.to_string())
140147
.args(std::env::args().skip(1).collect::<Vec<_>>())
141148
.output()
142149
.expect(\"failed to run command\");
143-
if !out.status.success() {{
150+
if should_panic {{
151+
// FIXME: Make `test::get_result_from_exit_code` public and use this code instead of this.
152+
//
153+
// On Zircon (the Fuchsia kernel), an abort from userspace calls the
154+
// LLVM implementation of __builtin_trap(), e.g., ud2 on x86, which
155+
// raises a kernel exception. If a userspace process does not
156+
// otherwise arrange exception handling, the kernel kills the process
157+
// with this return code.
158+
#[cfg(target_os = \"fuchsia\")]
159+
const ZX_TASK_RETCODE_EXCEPTION_KILL: i32 = -1028;
160+
// On Windows we use __fastfail to abort, which is documented to use this
161+
// exception code.
162+
#[cfg(windows)]
163+
const STATUS_FAIL_FAST_EXCEPTION: i32 = 0xC0000409u32 as i32;
164+
#[cfg(unix)]
165+
const SIGABRT: std::ffi::c_int = 6;
166+
167+
match out.status.code() {{
168+
Some(test::ERROR_EXIT_CODE) => ExitCode::SUCCESS,
169+
#[cfg(windows)]
170+
Some(STATUS_FAIL_FAST_EXCEPTION) => ExitCode::SUCCESS,
171+
#[cfg(unix)]
172+
None => match out.status.signal() {{
173+
Some(SIGABRT) => ExitCode::SUCCESS,
174+
Some(signal) => {{
175+
eprintln!(\"Test didn't panic, but it's marked `should_panic` (exit signal: {{signal}}).\");
176+
ExitCode::FAILURE
177+
}}
178+
None => {{
179+
eprintln!(\"Test didn't panic, but it's marked `should_panic` and exited with no error code and no signal.\");
180+
ExitCode::FAILURE
181+
}}
182+
}},
183+
#[cfg(not(unix))]
184+
None => {{
185+
eprintln!(\"Test didn't panic, but it's marked `should_panic`.\");
186+
ExitCode::FAILURE
187+
}}
188+
// Upon an abort, Fuchsia returns the status code ZX_TASK_RETCODE_EXCEPTION_KILL.
189+
#[cfg(target_os = \"fuchsia\")]
190+
Some(ZX_TASK_RETCODE_EXCEPTION_KILL) => ExitCode::SUCCESS,
191+
Some(exit_code) => {{
192+
if !out.status.success() {{
193+
eprintln!(\"Test didn't panic, but it's marked `should_panic` (exit status: {{exit_code}}).\");
194+
}} else {{
195+
eprintln!(\"Test didn't panic, but it's marked `should_panic`.\");
196+
}}
197+
ExitCode::FAILURE
198+
}}
199+
}}
200+
}} else if !out.status.success() {{
144201
if let Some(code) = out.status.code() {{
145202
eprintln!(\"Test executable failed (exit status: {{code}}).\");
146203
}} else {{
@@ -223,6 +280,7 @@ fn generate_mergeable_doctest(
223280
id: usize,
224281
output: &mut String,
225282
output_merged_tests: &mut String,
283+
opts: &RustdocOptions,
226284
) -> String {
227285
let test_id = format!("__doctest_{id}");
228286

@@ -256,31 +314,33 @@ fn main() {returns_result} {{
256314
)
257315
.unwrap();
258316
}
259-
let not_running = ignore || scraped_test.langstr.no_run;
317+
let should_panic = scraped_test.langstr.should_panic;
318+
let not_running = ignore || scraped_test.no_run(opts);
260319
writeln!(
261320
output_merged_tests,
262321
"
263322
mod {test_id} {{
264323
pub const TEST: test::TestDescAndFn = test::TestDescAndFn::new_doctest(
265-
{test_name:?}, {ignore}, {file:?}, {line}, {no_run}, {should_panic},
324+
{test_name:?}, {ignore} || ({should_panic} && crate::__doctest_mod::SHOULD_PANIC_DISABLED), {file:?}, {line}, {no_run}, false,
266325
test::StaticTestFn(
267326
|| {{{runner}}},
268327
));
269328
}}",
270329
test_name = scraped_test.name,
271330
file = scraped_test.path(),
272331
line = scraped_test.line,
273-
no_run = scraped_test.langstr.no_run,
274-
should_panic = !scraped_test.langstr.no_run && scraped_test.langstr.should_panic,
332+
no_run = scraped_test.no_run(opts),
275333
// Setting `no_run` to `true` in `TestDesc` still makes the test run, so we simply
276334
// don't give it the function to run.
277335
runner = if not_running {
278336
"test::assert_test_result(Ok::<(), String>(()))".to_string()
279337
} else {
280338
format!(
281339
"
282-
if let Some(bin_path) = crate::__doctest_mod::doctest_path() {{
283-
test::assert_test_result(crate::__doctest_mod::doctest_runner(bin_path, {id}))
340+
if {should_panic} && crate::__doctest_mod::SHOULD_PANIC_DISABLED {{
341+
test::assert_test_result(Ok::<(), String>(()))
342+
}} else if let Some(bin_path) = crate::__doctest_mod::doctest_path() {{
343+
test::assert_test_result(crate::__doctest_mod::doctest_runner(bin_path, {id}, {should_panic}))
284344
}} else {{
285345
test::assert_test_result(doctest_bundle::{test_id}::__main_fn())
286346
}}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Ensure that `should_panic` doctests only succeed if the test actually panicked.
2+
// Regression test for <https://github.com/rust-lang/rust/issues/143009>.
3+
4+
//@ ignore-cross-compile
5+
6+
use run_make_support::rustdoc;
7+
8+
fn check_output(edition: &str, panic_abort: bool) {
9+
let mut rustdoc_cmd = rustdoc();
10+
rustdoc_cmd.input("test.rs").arg("--test").edition(edition);
11+
if panic_abort {
12+
rustdoc_cmd.args(["-C", "panic=abort"]);
13+
}
14+
let output = rustdoc_cmd.run_fail().stdout_utf8();
15+
let should_contain = &[
16+
"test test.rs - bad_exit_code (line 1) ... FAILED",
17+
"test test.rs - did_not_panic (line 6) ... FAILED",
18+
"test test.rs - did_panic (line 11) ... ok",
19+
"---- test.rs - bad_exit_code (line 1) stdout ----
20+
Test executable failed (exit status: 1).",
21+
"---- test.rs - did_not_panic (line 6) stdout ----
22+
Test didn't panic, but it's marked `should_panic` (exit status: 1).",
23+
"test result: FAILED. 1 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out;",
24+
];
25+
for text in should_contain {
26+
assert!(
27+
output.contains(text),
28+
"output (edition: {edition}) doesn't contain {:?}\nfull output: {output}",
29+
text
30+
);
31+
}
32+
}
33+
34+
fn main() {
35+
check_output("2015", false);
36+
37+
// Same check with the merged doctest feature (enabled with the 2024 edition).
38+
check_output("2024", false);
39+
40+
// Checking that `-C panic=abort` is working too.
41+
check_output("2015", true);
42+
check_output("2024", true);
43+
}

0 commit comments

Comments
 (0)