-
Notifications
You must be signed in to change notification settings - Fork 13.8k
[rustdoc] Correctly handle should_panic
doctest attribute and fix --no-run
test flag on the 2024 edition
#143900
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
07d41a7
796c4ef
11b7070
21a4d9d
5f2ae4f
b001ba6
030b664
560d450
b70e20a
6060bcc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,8 @@ mod rust; | |
use std::fs::File; | ||
use std::hash::{Hash, Hasher}; | ||
use std::io::{self, Write}; | ||
#[cfg(unix)] | ||
use std::os::unix::process::ExitStatusExt; | ||
use std::path::{Path, PathBuf}; | ||
use std::process::{self, Command, Stdio}; | ||
use std::sync::atomic::{AtomicUsize, Ordering}; | ||
|
@@ -358,7 +360,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, | ||
|
@@ -463,8 +465,8 @@ enum TestFailure { | |
/// | ||
/// 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, | ||
/// The test is marked `should_panic` but the test binary didn't panic. | ||
NoPanic(Option<String>), | ||
} | ||
|
||
enum DirState { | ||
|
@@ -803,6 +805,25 @@ fn run_test( | |
let duration = instant.elapsed(); | ||
if doctest.no_run { | ||
return (duration, Ok(())); | ||
} else if doctest.langstr.should_panic | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we leave a FIXME somewhere that we (probably) don't handle Maybe we do already? As I've PM'ed you, on master I actually get illegal instruction for panics under this new strategy and I don't know if that's intentional or not. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe that's due to llvm lowering |
||
// Equivalent of: | ||
// | ||
// ``` | ||
// (cfg!(target_family = "wasm") || cfg!(target_os = "zkvm")) | ||
// && !cfg!(target_os = "emscripten") | ||
// ``` | ||
// | ||
// FIXME: All this code is terrible and doesn't take into account `TargetTuple::TargetJson`. | ||
// If `libtest` doesn't allow to handle this case, we'll need to use a rustc's API instead. | ||
&& let TargetTuple::TargetTuple(ref s) = rustdoc_options.target | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We both know that this check is super brittle and icky :D We can ignore that for now I guess ^^' Okay, so I'm not super familiar with target specifications but ideally this would also handle If what I write makes sense, could you at least leave a FIXME that we should somehow support Ideally, we would use some preexisting compiler API for querying this (one that provides a richer representation of the target), maybe there is one we could use? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes. T_T
Yeah you're right, adding a FIXME.
Hopefully yes. I'll take time to investigate how to make this all clean afterwards. Adding FIXME comments. |
||
&& let mut iter = s.split('-') | ||
&& let Some(arch) = iter.next() | ||
&& iter.next().is_some() | ||
&& let os = iter.next() | ||
&& (arch.starts_with("wasm") || os == Some("zkvm")) && os != Some("emscripten") | ||
{ | ||
// We cannot correctly handle `should_panic` in some wasm targets so we exit early. | ||
return (duration, Ok(())); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So this marks them as OK? Wouldn't it be more ideal to mark them as ignored (somehow)? Does that make any sense? I didn't have the time to refamiliarize myself with these parts of the doctest impl. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They are compiled, so it's not |
||
} | ||
|
||
// Run the code! | ||
|
@@ -833,12 +854,68 @@ fn run_test( | |
} else { | ||
cmd.output() | ||
}; | ||
|
||
// FIXME: Make `test::get_result_from_exit_code` public and use this code instead of this. | ||
// | ||
// On Zircon (the Fuchsia kernel), an abort from userspace calls the | ||
// LLVM implementation of __builtin_trap(), e.g., ud2 on x86, which | ||
// raises a kernel exception. If a userspace process does not | ||
// otherwise arrange exception handling, the kernel kills the process | ||
// with this return code. | ||
#[cfg(target_os = "fuchsia")] | ||
const ZX_TASK_RETCODE_EXCEPTION_KILL: i32 = -1028; | ||
// On Windows we use __fastfail to abort, which is documented to use this | ||
// exception code. | ||
#[cfg(windows)] | ||
const STATUS_FAIL_FAST_EXCEPTION: i32 = 0xC0000409u32 as i32; | ||
#[cfg(unix)] | ||
const SIGABRT: std::ffi::c_int = 6; | ||
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() { | ||
if langstr.should_panic { | ||
match out.status.code() { | ||
Some(test::ERROR_EXIT_CODE) => {} | ||
#[cfg(windows)] | ||
Some(STATUS_FAIL_FAST_EXCEPTION) => {} | ||
#[cfg(unix)] | ||
None => match out.status.signal() { | ||
Some(SIGABRT) => {} | ||
Some(signal) => { | ||
return ( | ||
duration, | ||
Err(TestFailure::NoPanic(Some(format!( | ||
"Test didn't panic, but it's marked `should_panic` (exit signal: {signal}).", | ||
)))), | ||
); | ||
} | ||
None => { | ||
return ( | ||
duration, | ||
Err(TestFailure::NoPanic(Some(format!( | ||
"Test didn't panic, but it's marked `should_panic` and exited with no error code and no signal.", | ||
)))), | ||
); | ||
} | ||
}, | ||
#[cfg(not(unix))] | ||
None => return (duration, Err(TestFailure::NoPanic(None))), | ||
// Upon an abort, Fuchsia returns the status code | ||
// `ZX_TASK_RETCODE_EXCEPTION_KILL`. | ||
#[cfg(target_os = "fuchsia")] | ||
Some(ZX_TASK_RETCODE_EXCEPTION_KILL) => {} | ||
Some(exit_code) => { | ||
let err_msg = if !out.status.success() { | ||
Some(format!( | ||
"Test didn't panic, but it's marked `should_panic` (exit status: {exit_code}).", | ||
)) | ||
} else { | ||
None | ||
}; | ||
return (duration, Err(TestFailure::NoPanic(err_msg))); | ||
} | ||
} | ||
} else if !out.status.success() { | ||
return (duration, Err(TestFailure::ExecutionFailure(out))); | ||
} | ||
} | ||
|
@@ -1145,8 +1222,12 @@ fn doctest_run_fn( | |
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::NoPanic(msg) => { | ||
if let Some(msg) = msg { | ||
eprint!("{msg}"); | ||
} else { | ||
eprint!("Test didn't panic, but it's marked `should_panic`."); | ||
} | ||
} | ||
TestFailure::MissingErrorCodes(codes) => { | ||
eprint!("Some expected error codes were not found: {codes:?}"); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
// Ensure that `should_panic` doctests only succeed if the test actually panicked. | ||
// Regression test for <https://github.com/rust-lang/rust/issues/143009>. | ||
|
||
//@ 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` (exit status: 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); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you at least consolidate b001ba6 and 030b664?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While your PR is technically only fixing bugs, at least the
should_panic
changes are actually breaking changes (rustdoc never performed these checks before) as can be seen by the modifications that you had to do to the standard library.Because of that I've now added
relnotes
, so users will at least kind of get to know what's up when their doctests "break" which will inevitably happen (I still don't know what the stability policies / guarantees are for rustdoc ^^' they seem to be laxer compared to the language ones).