diff --git a/library/std/src/keyword_docs.rs b/library/std/src/keyword_docs.rs index 8415f36eba251..66dbef3a36b1a 100644 --- a/library/std/src/keyword_docs.rs +++ b/library/std/src/keyword_docs.rs @@ -236,7 +236,7 @@ mod continue_keyword {} /// fundamental compilation unit of Rust code, and can be seen as libraries or projects. More can /// be read about crates in the [Reference]. /// -/// ```rust ignore +/// ```rust,ignore (code sample) /// extern crate rand; /// extern crate my_crate as thing; /// extern crate std; // implicitly added to the root of every Rust project diff --git a/src/doc/rustdoc/src/write-documentation/documentation-tests.md b/src/doc/rustdoc/src/write-documentation/documentation-tests.md index a7d3186fb78b7..225d777a85502 100644 --- a/src/doc/rustdoc/src/write-documentation/documentation-tests.md +++ b/src/doc/rustdoc/src/write-documentation/documentation-tests.md @@ -376,6 +376,58 @@ that the code sample should be compiled using the respective edition of Rust. # fn foo() {} ``` +Starting the 2024 edition[^edition-note], compatible doctests will be merged as one before being +run. It means that they will share the process, so any change global/static variables will now +impact the other doctests. + +[^edition-note]: This is based on the edition of the whole crate, not the edition of the individual +test case that may be specified in its code attribute. + +For example, if you have: + +```rust +//! ``` +//! foo::init(); +//! ``` + +/// ``` +/// foo::init(); +/// ``` +pub fn init() { + static mut IS_INIT: bool = false; + + unsafe { + assert!(!IS_INIT); + IS_INIT = true; + } +} +``` + +If you run `rustdoc --test` on this code, it'll panic on the second doctest being +run because `IS_INIT` value is not `false` anymore. + +This is where the `standalone` attribute comes in: it tells `rustdoc` that a doctest +should not be merged with the others and should be run in its own process. So the +previous code should use it: + +```rust +//! ```standalone +//! foo::init(); +//! ``` + +/// ```standalone +/// foo::init(); +/// ``` +pub fn init() { + static mut IS_INIT: bool = false; + + unsafe { + assert!(!IS_INIT); + IS_INIT = true; + } +} +``` + ## Syntax reference The *exact* syntax for code blocks, including the edge cases, can be found diff --git a/src/doc/unstable-book/src/language-features/link-arg-attribute.md b/src/doc/unstable-book/src/language-features/link-arg-attribute.md index 09915a7f2748d..405e89cb9d1cc 100644 --- a/src/doc/unstable-book/src/language-features/link-arg-attribute.md +++ b/src/doc/unstable-book/src/language-features/link-arg-attribute.md @@ -8,7 +8,7 @@ The `link_arg_attribute` feature allows passing arguments into the linker from inside of the source code. Order is preserved for link attributes as they were defined on a single extern block: -```rust,no_run +```rust,ignore (linking to "c" segfaults) #![feature(link_arg_attribute)] #[link(kind = "link-arg", name = "--start-group")] diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index 82f9cf1feaeb5..16db255f3db25 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -21,7 +21,9 @@ use rustc_span::symbol::sym; use rustc_span::{BytePos, FileName, Pos, Span, DUMMY_SP}; use rustc_target::spec::{Target, TargetTriple}; +use std::borrow::Cow; use std::env; +use std::fmt::Write as _; use std::fs::File; use std::io::{self, Write}; use std::panic; @@ -33,6 +35,8 @@ use std::sync::{Arc, Mutex}; use tempfile::{Builder as TempFileBuilder, TempDir}; +use test::TestDescAndFn; + use crate::clean::{types::AttributesExt, Attributes}; use crate::config::Options as RustdocOptions; use crate::html::markdown::{self, ErrorCodes, Ignore, LangString}; @@ -157,6 +161,7 @@ pub(crate) fn run( let test_args = options.test_args.clone(); let nocapture = options.nocapture; + let edition = options.edition; let externs = options.externs.clone(); let json_unused_externs = options.json_unused_externs; @@ -169,7 +174,7 @@ pub(crate) fn run( let file_path = temp_dir.path().join("rustdoc-cfgs"); crate::wrap_return(dcx, generate_args_file(&file_path, &options))?; - let (tests, unused_extern_reports, compiling_test_count) = + let (tests, unused_extern_reports, compiling_test_count, opts) = interface::run_compiler(config, |compiler| { compiler.enter(|queries| { let collector = queries.global_ctxt()?.enter(|tcx| { @@ -212,11 +217,17 @@ pub(crate) fn run( let unused_extern_reports = collector.unused_extern_reports.clone(); let compiling_test_count = collector.compiling_test_count.load(Ordering::SeqCst); - Ok((collector.tests, unused_extern_reports, compiling_test_count)) + Ok((collector.tests, unused_extern_reports, compiling_test_count, collector.opts)) }) })?; - run_tests(test_args, nocapture, tests); + tests.run_tests( + test_args, + nocapture, + opts, + edition >= Edition::Edition2024, + &unused_extern_reports, + ); // Collect and warn about unused externs, but only if we've gotten // reports for each doctest @@ -259,19 +270,6 @@ pub(crate) fn run( Ok(()) } -pub(crate) fn run_tests( - mut test_args: Vec, - nocapture: bool, - mut tests: Vec, -) { - test_args.insert(0, "rustdoctest".to_string()); - if nocapture { - test_args.push("--nocapture".to_string()); - } - tests.sort_by(|a, b| a.desc.name.as_slice().cmp(&b.desc.name.as_slice())); - test::test_main(&test_args, tests, None); -} - // Look for `#![doc(test(no_crate_inject))]`, used by crates in the std facade. fn scrape_test_config(attrs: &[ast::Attribute]) -> GlobalTestOptions { use rustc_ast_pretty::pprust; @@ -322,6 +320,7 @@ enum TestFailure { UnexpectedRunPass, } +#[derive(Debug)] enum DirState { Temp(tempfile::TempDir), Perm(PathBuf), @@ -341,7 +340,7 @@ impl DirState { // We could unify this struct the one in rustc but they have different // ownership semantics, so doing so would create wasteful allocations. #[derive(serde::Serialize, serde::Deserialize)] -struct UnusedExterns { +pub(crate) struct UnusedExterns { /// Lint level of the unused_crate_dependencies lint lint_level: String, /// List of unused externs by their names. @@ -370,30 +369,60 @@ fn wrapped_rustc_command(rustc_wrappers: &[PathBuf], rustc_binary: &Path) -> Com command } -fn run_test( - test: &str, - crate_name: &str, +pub(crate) struct DocTestInfo { + line_offset: usize, line: usize, - rustdoc_options: IndividualTestOptions, - mut lang_string: LangString, - no_run: bool, - opts: &GlobalTestOptions, - edition: Edition, path: PathBuf, +} + +fn build_test_dir(outdir: &Arc, is_multiple_tests: bool, test_id: &str) -> PathBuf { + // Make sure we emit well-formed executable names for our target. + let is_perm_dir = matches!(**outdir, DirState::Perm(..)); + let out_dir = outdir.path(); + let dir; + let out_dir = if !is_multiple_tests && is_perm_dir && !test_id.is_empty() { + dir = out_dir.join(test_id); + &dir + } else { + out_dir + }; + if is_perm_dir && let Err(err) = std::fs::create_dir_all(&out_dir) { + eprintln!("Couldn't create directory for doctest executables: {err}"); + panic::resume_unwind(Box::new(())); + } + out_dir.into() +} + +struct RunTestInfo { + test_code: String, + supports_color: bool, + is_multiple_tests: bool, + edition: Edition, + no_run: bool, + lang_string: LangString, + out_dir: PathBuf, +} + +fn run_test( + run_test_info: RunTestInfo, + test_info: Option, + rustdoc_options: Arc, report_unused_externs: impl Fn(UnusedExterns), + // Used to prevent overwriting a binary in case `--persist-doctests` is used. + binary_extra: Option<&str>, ) -> Result<(), TestFailure> { - let (test, line_offset, supports_color) = make_test( - test, - Some(crate_name), - lang_string.test_harness, - opts, + let RunTestInfo { + test_code, + supports_color, + is_multiple_tests, edition, - Some(&rustdoc_options.test_id), - ); - - // Make sure we emit well-formed executable names for our target. - let rust_out = add_exe_suffix("rust_out".to_owned(), &rustdoc_options.target); - let output_file = rustdoc_options.outdir.path().join(rust_out); + no_run, + mut lang_string, + out_dir, + } = run_test_info; + let rust_out = + add_exe_suffix(format!("rust_out{}", binary_extra.unwrap_or("")), &rustdoc_options.target); + let output_file = out_dir.join(rust_out); let rustc_binary = rustdoc_options .test_builder @@ -408,8 +437,17 @@ fn run_test( } compiler.arg("--edition").arg(&edition.to_string()); - compiler.env("UNSTABLE_RUSTDOC_TEST_PATH", path); - compiler.env("UNSTABLE_RUSTDOC_TEST_LINE", format!("{}", line as isize - line_offset as isize)); + if is_multiple_tests { + // It makes the compilation failure much faster if it is for a combined doctest. + compiler.arg("--error-format=short"); + } + if let Some(test_info) = test_info { + compiler.env("UNSTABLE_RUSTDOC_TEST_PATH", test_info.path); + compiler.env( + "UNSTABLE_RUSTDOC_TEST_LINE", + format!("{}", test_info.line as isize - test_info.line_offset as isize), + ); + } compiler.arg("-o").arg(&output_file); if lang_string.test_harness { compiler.arg("--test"); @@ -421,13 +459,13 @@ fn run_test( compiler.arg("-Z").arg("unstable-options"); } - if no_run && !lang_string.compile_fail && rustdoc_options.should_persist_doctests { + if no_run && !lang_string.compile_fail && rustdoc_options.persist_doctests.is_none() { compiler.arg("--emit=metadata"); } compiler.arg("--target").arg(match rustdoc_options.target { - TargetTriple::TargetTriple(s) => s, - TargetTriple::TargetJson { path_for_rustdoc, .. } => { - path_for_rustdoc.to_str().expect("target path must be valid unicode").to_string() + TargetTriple::TargetTriple(ref s) => s.as_str(), + TargetTriple::TargetJson { ref path_for_rustdoc, .. } => { + path_for_rustdoc.to_str().expect("target path must be valid unicode") } }); if let ErrorOutputType::HumanReadable(kind) = rustdoc_options.error_format { @@ -450,18 +488,32 @@ fn run_test( } } - compiler.arg("-"); - compiler.stdin(Stdio::piped()); - compiler.stderr(Stdio::piped()); + if is_multiple_tests { + let out_source = out_dir.join(&format!("doctest{}.rs", binary_extra.unwrap_or("combined"))); + if std::fs::write(&out_source, &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 Err(TestFailure::CompileError); + } + compiler.arg(out_source); + compiler.stderr(Stdio::null()); + } else { + compiler.arg("-"); + compiler.stdin(Stdio::piped()); + compiler.stderr(Stdio::piped()); + } debug!("compiler invocation for doctest: {compiler:?}"); let mut child = compiler.spawn().expect("Failed to spawn rustc process"); - { + let output = if is_multiple_tests { + let status = child.wait().expect("Failed to wait"); + process::Output { status, stdout: Vec::new(), stderr: Vec::new() } + } else { let stdin = child.stdin.as_mut().expect("Failed to open stdin"); - stdin.write_all(test.as_bytes()).expect("could write out test sources"); - } - let output = child.wait_with_output().expect("Failed to read stdout"); + stdin.write_all(test_code.as_bytes()).expect("could write out test sources"); + child.wait_with_output().expect("Failed to read stdout") + }; struct Bomb<'a>(&'a str); impl Drop for Bomb<'_> { @@ -520,19 +572,19 @@ fn run_test( let mut cmd; let output_file = make_maybe_absolute_path(output_file); - if let Some(tool) = rustdoc_options.runtool { + if let Some(ref tool) = rustdoc_options.runtool { let tool = make_maybe_absolute_path(tool.into()); cmd = Command::new(tool); - cmd.args(rustdoc_options.runtool_args); + cmd.args(&rustdoc_options.runtool_args); cmd.arg(output_file); } else { cmd = Command::new(output_file); } - if let Some(run_directory) = rustdoc_options.test_run_directory { + if let Some(ref run_directory) = rustdoc_options.test_run_directory { cmd.current_dir(run_directory); } - let result = if rustdoc_options.nocapture { + let result = if is_multiple_tests || rustdoc_options.nocapture { cmd.status().map(|status| process::Output { status, stdout: Vec::new(), @@ -542,17 +594,17 @@ fn run_test( cmd.output() }; match result { - Err(e) => return Err(TestFailure::ExecutionError(e)), + Err(e) => Err(TestFailure::ExecutionError(e)), Ok(out) => { if lang_string.should_panic && out.status.success() { - return Err(TestFailure::UnexpectedRunPass); + Err(TestFailure::UnexpectedRunPass) } else if !lang_string.should_panic && !out.status.success() { - return Err(TestFailure::ExecutionFailure(out)); + Err(TestFailure::ExecutionFailure(out)) + } else { + Ok(()) } } } - - Ok(()) } /// Converts a path intended to use as a command to absolute if it is @@ -569,229 +621,617 @@ fn make_maybe_absolute_path(path: PathBuf) -> PathBuf { } } -/// Transforms a test into code that can be compiled into a Rust binary, and returns the number of -/// lines before the test code begins as well as if the output stream supports colors or not. -pub(crate) fn make_test( - s: &str, - crate_name: Option<&str>, - dont_insert_main: bool, - opts: &GlobalTestOptions, - edition: Edition, - test_id: Option<&str>, -) -> (String, usize, bool) { - let (crate_attrs, everything_else, crates) = partition_source(s, edition); - let everything_else = everything_else.trim(); - let mut line_offset = 0; - let mut prog = String::new(); - let mut supports_color = false; - - if opts.attrs.is_empty() { - // If there aren't any attributes supplied by #![doc(test(attr(...)))], then allow some - // lints that are commonly triggered in doctests. The crate-level test attributes are - // commonly used to make tests fail in case they trigger warnings, so having this there in - // that case may cause some tests to pass when they shouldn't have. - prog.push_str("#![allow(unused)]\n"); - line_offset += 1; - } +pub(crate) struct DocTest { + test_code: String, + supports_color: bool, + already_has_extern_crate: bool, + main_fn_span: Option, + crate_attrs: String, + crates: String, + everything_else: String, + ignore: bool, + crate_name: Option, + name: String, + lang_string: LangString, + line: usize, + // Path that will be displayed if the test failed to compile. + path: PathBuf, + file: String, + failed_ast: bool, + test_id: String, + outdir: Arc, + rustdoc_test_options: Arc, + no_run: bool, +} - // Next, any attributes that came from the crate root via #![doc(test(attr(...)))]. - for attr in &opts.attrs { - prog.push_str(&format!("#![{attr}]\n")); - line_offset += 1; - } +impl DocTest { + pub(crate) fn generate_unique_doctest( + &self, + dont_insert_main: bool, + opts: &GlobalTestOptions, + // If `test_id` is `None`, it means we're generating code for a code example "run" link. + test_id: Option<&str>, + ) -> (String, usize) { + if self.failed_ast { + // If the AST failed to compile, no need to go generate a complete doctest, the error + // will be better this way. + return (self.everything_else.clone(), 0); + } + let mut line_offset = 0; + let mut prog = String::with_capacity( + self.test_code.len() + self.crate_attrs.len() + self.crates.len(), + ); - // Now push any outer attributes from the example, assuming they - // are intended to be crate attributes. - prog.push_str(&crate_attrs); - prog.push_str(&crates); + Self::push_attrs(&mut prog, opts, &mut line_offset); - // Uses librustc_ast to parse the doctest and find if there's a main fn and the extern - // crate already is included. - let result = rustc_driver::catch_fatal_errors(|| { - rustc_span::create_session_if_not_set_then(edition, |_| { - use rustc_errors::emitter::{Emitter, HumanEmitter}; - use rustc_errors::DiagCtxt; - use rustc_parse::parser::ForceCollect; - use rustc_span::source_map::FilePathMapping; + // Now push any outer attributes from the example, assuming they + // are intended to be crate attributes. + prog.push_str(&self.crate_attrs); + prog.push_str(&self.crates); - let filename = FileName::anon_source_code(s); - let source = crates + everything_else; + // Don't inject `extern crate std` because it's already injected by the + // compiler. + if !self.already_has_extern_crate && + !opts.no_crate_inject && + let Some(ref crate_name) = self.crate_name && + crate_name != "std" && + // Don't inject `extern crate` if the crate is never used. + // NOTE: this is terribly inaccurate because it doesn't actually + // parse the source, but only has false positives, not false + // negatives. + self.test_code.contains(crate_name) + { + // rustdoc implicitly inserts an `extern crate` item for the own crate + // which may be unused, so we need to allow the lint. + prog.push_str("#[allow(unused_extern_crates)]\n"); - // Any errors in parsing should also appear when the doctest is compiled for real, so just - // send all the errors that librustc_ast emits directly into a `Sink` instead of stderr. - let sm = Lrc::new(SourceMap::new(FilePathMapping::empty())); - let fallback_bundle = rustc_errors::fallback_fluent_bundle( - rustc_driver::DEFAULT_LOCALE_RESOURCES.to_vec(), - false, - ); - supports_color = - HumanEmitter::new(stderr_destination(ColorConfig::Auto), fallback_bundle.clone()) - .supports_color(); + prog.push_str(&format!("extern crate r#{crate_name};\n")); + line_offset += 1; + } - let emitter = HumanEmitter::new(Box::new(io::sink()), fallback_bundle); + // FIXME: This code cannot yet handle no_std test cases yet + if dont_insert_main || self.main_fn_span.is_some() || prog.contains("![no_std]") { + prog.push_str(&self.everything_else); + } else { + let returns_result = self.everything_else.trim_end().ends_with("(())"); + // Give each doctest main function a unique name. + // This is for example needed for the tooling around `-C instrument-coverage`. + let inner_fn_name = if let Some(test_id) = test_id { + format!("_doctest_main_{test_id}") + } else { + "_inner".into() + }; + let inner_attr = if test_id.is_some() { "#[allow(non_snake_case)] " } else { "" }; + let (main_pre, main_post) = if returns_result { + ( + format!( + "fn main() {{ {inner_attr}fn {inner_fn_name}() -> Result<(), impl core::fmt::Debug> {{\n", + ), + format!("\n}} {inner_fn_name}().unwrap() }}"), + ) + } else if test_id.is_some() { + ( + format!("fn main() {{ {inner_attr}fn {inner_fn_name}() {{\n",), + format!("\n}} {inner_fn_name}() }}"), + ) + } else { + ("fn main() {\n".into(), "\n}".into()) + }; + // Note on newlines: We insert a line/newline *before*, and *after* + // the doctest and adjust the `line_offset` accordingly. + // In the case of `-C instrument-coverage`, this means that the generated + // inner `main` function spans from the doctest opening codeblock to the + // closing one. For example + // /// ``` <- start of the inner main + // /// <- code under doctest + // /// ``` <- end of the inner main + line_offset += 1; + + // add extra 4 spaces for each line to offset the code block + let content = if opts.insert_indent_space { + self.everything_else + .lines() + .map(|line| format!(" {}", line)) + .collect::>() + .join("\n") + } else { + self.everything_else.to_string() + }; + prog.extend([&main_pre, content.as_str(), &main_post].iter().cloned()); + } + debug!("final doctest:\n{prog}"); + (prog, line_offset) + } - // FIXME(misdreavus): pass `-Z treat-err-as-bug` to the doctest parser - let dcx = DiagCtxt::new(Box::new(emitter)).disable_warnings(); - let psess = ParseSess::with_dcx(dcx, sm); + fn push_attrs(prog: &mut String, opts: &GlobalTestOptions, line_offset: &mut usize) { + if opts.attrs.is_empty() { + // If there aren't any attributes supplied by #![doc(test(attr(...)))], then allow some + // lints that are commonly triggered in doctests. The crate-level test attributes are + // commonly used to make tests fail in case they trigger warnings, so having this there in + // that case may cause some tests to pass when they shouldn't have. + prog.push_str("#![allow(unused)]\n"); + *line_offset += 1; + } - let mut found_main = false; - let mut found_extern_crate = crate_name.is_none(); - let mut found_macro = false; + // Next, any attributes that came from the crate root via #![doc(test(attr(...)))]. + for attr in &opts.attrs { + prog.push_str(&format!("#![{attr}]\n")); + *line_offset += 1; + } + } - let mut parser = match maybe_new_parser_from_source_str(&psess, filename, source) { - Ok(p) => p, - Err(errs) => { - errs.into_iter().for_each(|err| err.cancel()); - return (found_main, found_extern_crate, found_macro); - } - }; + fn generate_test_desc_and_fn( + mut self, + opts: &GlobalTestOptions, + edition: Edition, + unused_externs: Arc>>, + ) -> TestDescAndFn { + let (code, line_offset) = + self.generate_unique_doctest(self.lang_string.test_harness, opts, Some(&self.test_id)); + let Self { + supports_color, rustdoc_test_options, lang_string, outdir, path, no_run, .. + } = self; + let out_dir = build_test_dir(&outdir, false, &self.test_id); + TestDescAndFn { + desc: test::TestDesc { + name: test::DynTestName(std::mem::replace(&mut self.name, String::new())), + ignore: self.ignore, + ignore_message: None, + source_file: "", + start_line: 0, + start_col: 0, + end_line: 0, + end_col: 0, + // compiler failures are test failures + should_panic: test::ShouldPanic::No, + compile_fail: lang_string.compile_fail, + no_run, + test_type: test::TestType::DocTest, + }, + testfn: test::DynTestFn(Box::new(move || { + let report_unused_externs = |uext| { + unused_externs.lock().unwrap().push(uext); + }; + let res = run_test( + RunTestInfo { + test_code: code, + supports_color, + is_multiple_tests: false, + edition, + no_run, + lang_string, + out_dir, + }, + Some(DocTestInfo { line_offset, line: self.line, path }), + rustdoc_test_options, + report_unused_externs, + None, + ); + // We need to move `outdir` into the closure to ensure the `TempDir` struct won't + // be dropped before all tests have been run. + // + // The call to `drop` is only to make use of `outdir`. + drop(outdir); - loop { - match parser.parse_item(ForceCollect::No) { - Ok(Some(item)) => { - if !found_main - && let ast::ItemKind::Fn(..) = item.kind - && item.ident.name == sym::main - { - found_main = true; + 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); - if !found_extern_crate - && let ast::ItemKind::ExternCrate(original) = item.kind - { - // This code will never be reached if `crate_name` is none because - // `found_extern_crate` is initialized to `true` if it is none. - let crate_name = crate_name.unwrap(); + // 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(); - match original { - Some(name) => found_extern_crate = name.as_str() == crate_name, - None => found_extern_crate = item.ident.as_str() == crate_name, + if !stdout.is_empty() || !stderr.is_empty() { + eprintln!(); + + if !stdout.is_empty() { + eprintln!("stdout:\n{stdout}"); + } + + if !stderr.is_empty() { + eprintln!("stderr:\n{stderr}"); + } } } + } - if !found_macro && let ast::ItemKind::MacCall(..) = item.kind { - found_macro = true; - } + panic::resume_unwind(Box::new(())); + } + Ok(()) + })), + } + } + + fn generate_test_desc(&self, id: usize, output: &mut String) -> String { + let test_id = format!("__doctest_{id}"); + + if self.ignore { + // We generate nothing else. + writeln!(output, "mod {test_id} {{\n").unwrap(); + } else { + writeln!(output, "mod {test_id} {{\n{}", self.crates).unwrap(); + if self.main_fn_span.is_some() { + output.push_str(&self.everything_else); + } else { + let returns_result = if self.everything_else.trim_end().ends_with("(())") { + "-> Result<(), impl core::fmt::Debug>" + } else { + "" + }; + write!( + output, + "\ + fn main() {returns_result} {{ + {} + }}", + self.everything_else + ) + .unwrap(); + } + } + writeln!( + output, + " +#[rustc_test_marker = {test_name:?}] +pub const TEST: test::TestDescAndFn = test::TestDescAndFn {{ + desc: test::TestDesc {{ + name: test::StaticTestName({test_name:?}), + ignore: {ignore}, + ignore_message: None, + source_file: {file:?}, + start_line: {line}, + start_col: 0, + end_line: 0, + end_col: 0, + compile_fail: false, + no_run: {no_run}, + should_panic: test::ShouldPanic::{should_panic}, + test_type: test::TestType::UnitTest, + }}, + testfn: test::StaticTestFn( + #[coverage(off)] + || test::assert_test_result({runner}), + ) +}}; +}}", + test_name = self.name, + ignore = self.ignore, + file = self.file, + line = self.line, + no_run = self.no_run, + should_panic = if !self.no_run && self.lang_string.should_panic { "Yes" } else { "No" }, + // 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 self.no_run || self.ignore { "Ok::<(), String>(())" } else { "self::main()" }, + ) + .unwrap(); + test_id + } +} + +pub(crate) struct MakeTestArgs<'a, 'b> { + pub source_code: String, + pub crate_name: Option>, + pub edition: Edition, + pub name: String, + pub lang_string: LangString, + pub line: usize, + pub file: String, + pub rustdoc_test_options: Arc, + pub test_id: String, + pub target_str: &'b str, + pub path: PathBuf, + pub no_run: bool, +} + +#[derive(PartialEq, Eq, Debug)] +enum ParsingResult { + Failed, + AstError, + Ok, +} + +fn cancel_error_count(psess: &ParseSess) { + // Reset errors so that they won't be reported as compiler bugs when dropping the + // dcx. Any errors in the tests will be reported when the test file is compiled, + // Note that we still need to cancel the errors above otherwise `Diag` will panic on + // drop. + psess.dcx.reset_err_count(); +} - if found_main && found_extern_crate { - break; +fn parse_source( + filename: FileName, + source: String, + found_main_span: &mut Option, + found_extern_crate: &mut bool, + found_macro: &mut bool, + crate_name: &Option, + supports_color: &mut bool, +) -> ParsingResult { + use rustc_errors::emitter::{Emitter, HumanEmitter}; + use rustc_errors::DiagCtxt; + use rustc_parse::parser::ForceCollect; + use rustc_span::source_map::FilePathMapping; + + // Any errors in parsing should also appear when the doctest is compiled for real, so just + // send all the errors that librustc_ast emits directly into a `Sink` instead of stderr. + let sm = Lrc::new(SourceMap::new(FilePathMapping::empty())); + let fallback_bundle = rustc_errors::fallback_fluent_bundle( + rustc_driver::DEFAULT_LOCALE_RESOURCES.to_vec(), + false, + ); + *supports_color = + HumanEmitter::new(stderr_destination(ColorConfig::Auto), fallback_bundle.clone()) + .supports_color(); + + let emitter = HumanEmitter::new(Box::new(io::sink()), fallback_bundle); + + // FIXME(misdreavus): pass `-Z treat-err-as-bug` to the doctest parser + let dcx = DiagCtxt::new(Box::new(emitter)).disable_warnings(); + let psess = ParseSess::with_dcx(dcx, sm); + + let mut parser = match maybe_new_parser_from_source_str(&psess, filename, source) { + Ok(p) => p, + Err(errs) => { + errs.into_iter().for_each(|err| err.cancel()); + cancel_error_count(&psess); + return ParsingResult::Failed; + } + }; + let mut parsing_result = ParsingResult::Ok; + + // Recurse through functions body. It is necessary because the doctest source code is + // wrapped in a function to limit the number of AST errors. If we don't recurse into + // functions, we would thing all top-level items (so basically nothing). + fn check_item( + item: &ast::Item, + found_main_span: &mut Option, + found_extern_crate: &mut bool, + found_macro: &mut bool, + crate_name: &Option, + ) { + match item.kind { + ast::ItemKind::Fn(ref fn_item) if found_main_span.is_none() => { + if item.ident.name == sym::main { + *found_main_span = Some(item.span); + } + if let Some(ref body) = fn_item.body { + for stmt in &body.stmts { + match stmt.kind { + ast::StmtKind::Item(ref item) => check_item( + item, + found_main_span, + found_extern_crate, + found_macro, + crate_name, + ), + ast::StmtKind::MacCall(..) => *found_macro = true, + _ => {} } } - Ok(None) => break, - Err(e) => { - e.cancel(); - break; - } } + } + ast::ItemKind::ExternCrate(original) => { + if !*found_extern_crate && let Some(ref crate_name) = crate_name { + *found_extern_crate = match original { + Some(name) => name.as_str() == crate_name, + None => item.ident.as_str() == crate_name, + }; + } + } + ast::ItemKind::MacCall(..) => *found_macro = true, + _ => {} + } + } - // The supplied item is only used for diagnostics, - // which are swallowed here anyway. - parser.maybe_consume_incorrect_semicolon(None); + loop { + match parser.parse_item(ForceCollect::No) { + Ok(Some(item)) => { + check_item(&item, found_main_span, found_extern_crate, found_macro, crate_name); + + if found_main_span.is_some() && *found_extern_crate { + break; + } + } + Ok(None) => break, + Err(e) => { + parsing_result = ParsingResult::AstError; + e.cancel(); + break; } + } + + // The supplied slice is only used for diagnostics, + // which are swallowed here anyway. + parser.maybe_consume_incorrect_semicolon(None); + } + + cancel_error_count(&psess); + parsing_result +} + +/// Transforms a test into code that can be compiled into a Rust binary, and returns the number of +/// lines before the test code begins as well as if the output stream supports colors or not. +pub(crate) fn make_test(test_args: MakeTestArgs<'_, '_>) -> DocTest { + let MakeTestArgs { + source_code, + crate_name, + edition, + name, + mut lang_string, + line, + file, + rustdoc_test_options, + test_id, + target_str, + path, + no_run, + } = test_args; + let outdir = Arc::new(if let Some(ref path) = rustdoc_test_options.persist_doctests { + DirState::Perm(path.clone()) + } else { + DirState::Temp(get_doctest_dir().expect("rustdoc needs a tempdir")) + }); + + // FIXME: This partition source is pretty bad. Something like + // would be + // a much better approach. + let (crate_attrs, everything_else, crates) = partition_source(&source_code, edition); + let mut supports_color = false; + let crate_name = crate_name.map(|c| c.into_owned()); + + // Uses librustc_ast to parse the doctest and find if there's a main fn and the extern + // crate already is included. + let result = rustc_driver::catch_fatal_errors(|| { + rustc_span::create_session_if_not_set_then(edition, |_| { + let mut found_main_span = None; + let mut found_extern_crate = crate_name.is_none(); + let mut found_macro = false; - // Reset errors so that they won't be reported as compiler bugs when dropping the - // dcx. Any errors in the tests will be reported when the test file is compiled, - // Note that we still need to cancel the errors above otherwise `Diag` will panic on - // drop. - psess.dcx.reset_err_count(); + let mut parsing_result = parse_source( + FileName::anon_source_code(&source_code), + format!("{crates}{everything_else}"), + &mut found_main_span, + &mut found_extern_crate, + &mut found_macro, + &crate_name, + &mut supports_color, + ); + // No need to double-check this if the "merged doctests" feature isn't enabled (so + // before the 2024 edition). + if edition >= Edition::Edition2024 && parsing_result != ParsingResult::Ok { + // If we found an AST error, we want to ensure it's because of an expression being + // used outside of a function. + // + // To do so, we wrap in a function in order to make sure that the doctest AST is + // correct. For example, if your doctest is `foo::bar()`, if we don't wrap it in a + // block, it would emit an AST error, which would be problematic for us since we + // want to filter out such errors which aren't "real" errors. + // + // The end goal is to be able to merge as many doctests as possible as one for much + // faster doctests run time. + parsing_result = parse_source( + FileName::anon_source_code(&source_code), + format!("{crates}\nfn __doctest_wrap(){{{everything_else}\n}}"), + &mut found_main_span, + &mut found_extern_crate, + &mut found_macro, + &crate_name, + &mut supports_color, + ); + } - (found_main, found_extern_crate, found_macro) + (found_main_span, found_extern_crate, found_macro, parsing_result) }) }); - let Ok((already_has_main, already_has_extern_crate, found_macro)) = result else { - // If the parser panicked due to a fatal error, pass the test code through unchanged. - // The error will be reported during compilation. - return (s.to_owned(), 0, false); + + let ignore = match lang_string.ignore { + Ignore::All => true, + Ignore::None => false, + Ignore::Some(ref ignores) => ignores.iter().any(|s| target_str.contains(s)), + }; + let (mut main_fn_span, already_has_extern_crate, found_macro, parsing_result) = match result { + Err(..) | Ok((_, _, _, ParsingResult::Failed)) => { + // If the parser panicked due to a fatal error, pass the test code through unchanged. + // The error will be reported during compilation. + return DocTest { + test_code: source_code, + supports_color: false, + main_fn_span: None, + crate_attrs, + crates, + everything_else, + already_has_extern_crate: false, + ignore, + crate_name, + name, + lang_string, + line, + file, + failed_ast: true, + rustdoc_test_options, + outdir, + test_id, + path, + no_run, + }; + } + Ok((main_fn_span, already_has_extern_crate, found_macro, parsing_result)) => { + (main_fn_span, already_has_extern_crate, found_macro, parsing_result) + } }; // If a doctest's `fn main` is being masked by a wrapper macro, the parsing loop above won't // see it. In that case, run the old text-based scan to see if they at least have a main // function written inside a macro invocation. See // https://github.com/rust-lang/rust/issues/56898 - let already_has_main = if found_macro && !already_has_main { - s.lines() + if found_macro + && main_fn_span.is_none() + && source_code + .lines() .map(|line| { let comment = line.find("//"); if let Some(comment_begins) = comment { &line[0..comment_begins] } else { line } }) .any(|code| code.contains("fn main")) - } else { - already_has_main - }; - - // Don't inject `extern crate std` because it's already injected by the - // compiler. - if !already_has_extern_crate && !opts.no_crate_inject && crate_name != Some("std") { - if let Some(crate_name) = crate_name { - // Don't inject `extern crate` if the crate is never used. - // NOTE: this is terribly inaccurate because it doesn't actually - // parse the source, but only has false positives, not false - // negatives. - if s.contains(crate_name) { - // rustdoc implicitly inserts an `extern crate` item for the own crate - // which may be unused, so we need to allow the lint. - prog.push_str("#[allow(unused_extern_crates)]\n"); - - prog.push_str(&format!("extern crate r#{crate_name};\n")); - line_offset += 1; - } - } + { + main_fn_span = Some(DUMMY_SP); } - // FIXME: This code cannot yet handle no_std test cases yet - if dont_insert_main || already_has_main || prog.contains("![no_std]") { - prog.push_str(everything_else); - } else { - let returns_result = everything_else.trim_end().ends_with("(())"); - // Give each doctest main function a unique name. - // This is for example needed for the tooling around `-C instrument-coverage`. - let inner_fn_name = if let Some(test_id) = test_id { - format!("_doctest_main_{test_id}") - } else { - "_inner".into() - }; - let inner_attr = if test_id.is_some() { "#[allow(non_snake_case)] " } else { "" }; - let (main_pre, main_post) = if returns_result { - ( - format!( - "fn main() {{ {inner_attr}fn {inner_fn_name}() -> Result<(), impl core::fmt::Debug> {{\n", - ), - format!("\n}} {inner_fn_name}().unwrap() }}"), - ) - } else if test_id.is_some() { - ( - format!("fn main() {{ {inner_attr}fn {inner_fn_name}() {{\n",), - format!("\n}} {inner_fn_name}() }}"), - ) - } else { - ("fn main() {\n".into(), "\n}".into()) - }; - // Note on newlines: We insert a line/newline *before*, and *after* - // the doctest and adjust the `line_offset` accordingly. - // In the case of `-C instrument-coverage`, this means that the generated - // inner `main` function spans from the doctest opening codeblock to the - // closing one. For example - // /// ``` <- start of the inner main - // /// <- code under doctest - // /// ``` <- end of the inner main - line_offset += 1; - - // add extra 4 spaces for each line to offset the code block - let content = if opts.insert_indent_space { - everything_else - .lines() - .map(|line| format!(" {}", line)) - .collect::>() - .join("\n") - } else { - everything_else.to_string() - }; - prog.extend([&main_pre, content.as_str(), &main_post].iter().cloned()); + if parsing_result == ParsingResult::AstError { + // If we have any doubt about whether the AST of this doctest is invalid, we consider it + // as a standalone test so it doesn't impact other merged doctests. + lang_string.standalone = true; + } + DocTest { + test_code: source_code, + supports_color, + main_fn_span, + crate_attrs, + crates, + everything_else, + already_has_extern_crate, + ignore, + crate_name, + name, + lang_string, + line, + file, + failed_ast: false, + rustdoc_test_options, + outdir, + test_id, + path, + no_run, } - - debug!("final doctest:\n{prog}"); - - (prog, line_offset, supports_color) } fn check_if_attr_is_complete(source: &str, edition: Edition) -> bool { @@ -929,59 +1369,58 @@ fn partition_source(s: &str, edition: Edition) -> (String, String, String) { debug!("crates:\n{crates}"); debug!("after:\n{after}"); - (before, after, crates) + (before, after.trim().to_owned(), crates) } pub(crate) struct IndividualTestOptions { test_builder: Option, test_builder_wrappers: Vec, is_json_unused_externs_enabled: bool, - should_persist_doctests: bool, + persist_doctests: Option, error_format: ErrorOutputType, test_run_directory: Option, nocapture: bool, arg_file: PathBuf, - outdir: DirState, runtool: Option, runtool_args: Vec, target: TargetTriple, - test_id: String, maybe_sysroot: Option, } impl IndividualTestOptions { - fn new(options: &RustdocOptions, arg_file: &Path, test_id: String) -> Self { - let outdir = if let Some(ref path) = options.persist_doctests { - let mut path = path.clone(); - path.push(&test_id); - - if let Err(err) = std::fs::create_dir_all(&path) { - eprintln!("Couldn't create directory for doctest executables: {err}"); - panic::resume_unwind(Box::new(())); - } - - DirState::Perm(path) - } else { - DirState::Temp(get_doctest_dir().expect("rustdoc needs a tempdir")) - }; - + fn new(options: &RustdocOptions, arg_file: PathBuf) -> Self { Self { test_builder: options.test_builder.clone(), test_builder_wrappers: options.test_builder_wrappers.clone(), is_json_unused_externs_enabled: options.json_unused_externs.is_enabled(), - should_persist_doctests: options.persist_doctests.is_none(), + persist_doctests: options.persist_doctests.clone(), error_format: options.error_format, test_run_directory: options.test_run_directory.clone(), nocapture: options.nocapture, - arg_file: arg_file.into(), - outdir, + arg_file, runtool: options.runtool.clone(), runtool_args: options.runtool_args.clone(), target: options.target.clone(), - test_id, maybe_sysroot: options.maybe_sysroot.clone(), } } + + pub(crate) fn empty() -> Self { + Self { + test_builder: Default::default(), + test_builder_wrappers: Default::default(), + is_json_unused_externs_enabled: Default::default(), + persist_doctests: Default::default(), + error_format: Default::default(), + test_run_directory: Default::default(), + nocapture: Default::default(), + arg_file: Default::default(), + runtool: Default::default(), + runtool_args: Default::default(), + target: TargetTriple::from_triple(""), + maybe_sysroot: Default::default(), + } + } } pub(crate) trait Tester { @@ -992,8 +1431,231 @@ pub(crate) trait Tester { fn register_header(&mut self, _name: &str, _level: u32) {} } +/// Convenient type to merge compatible doctests into one. +struct DocTestRunner { + crate_attrs: FxHashSet, + ids: String, + output: String, + supports_color: bool, + nb_tests: usize, + doctests: Vec, +} + +impl DocTestRunner { + fn new() -> Self { + Self { + crate_attrs: FxHashSet::default(), + ids: String::new(), + output: String::new(), + supports_color: true, + nb_tests: 0, + doctests: Vec::with_capacity(10), + } + } + + fn add_test(&mut self, doctest: DocTest) { + if !doctest.ignore { + for line in doctest.crate_attrs.split('\n') { + self.crate_attrs.insert(line.to_string()); + } + } + if !self.ids.is_empty() { + self.ids.push(','); + } + self.ids.push_str(&format!( + "{}::TEST", + doctest.generate_test_desc(self.nb_tests, &mut self.output) + )); + self.supports_color &= doctest.supports_color; + self.nb_tests += 1; + self.doctests.push(doctest); + } + + fn run_tests( + &mut self, + rustdoc_test_options: Arc, + edition: Edition, + opts: &GlobalTestOptions, + test_args: &[String], + outdir: &Arc, + ) -> Result { + let mut code = "\ +#![allow(unused_extern_crates)] +#![allow(internal_features)] +#![feature(test)] +#![feature(rustc_attrs)] +#![feature(coverage_attribute)]\n" + .to_string(); + + for crate_attr in &self.crate_attrs { + code.push_str(crate_attr); + code.push('\n'); + } + + DocTest::push_attrs(&mut code, opts, &mut 0); + code.push_str("extern crate test;\n"); + + let test_args = + test_args.iter().map(|arg| format!("{arg:?}.to_string(),")).collect::(); + write!( + code, + "\ +{output} +#[rustc_main] +#[coverage(off)] +fn main() {{ +test::test_main(&[{test_args}], vec![{ids}], None); +}}", + output = self.output, + ids = self.ids, + ) + .expect("failed to generate test code"); + let out_dir = build_test_dir(outdir, true, ""); + let ret = run_test( + RunTestInfo { + test_code: code, + supports_color: self.supports_color, + is_multiple_tests: true, + edition, + no_run: false, + lang_string: LangString::empty_for_test(), + out_dir, + }, + None, + rustdoc_test_options, + |_: UnusedExterns| {}, + // To prevent writing over an existing doctest + Some(&format!("_{}", edition)), + ); + if let Err(TestFailure::CompileError) = ret { Err(()) } else { Ok(ret.is_ok()) } + } +} + +fn add_standalone_tests( + standalone: &mut Vec, + doctests: Vec, + opts: &GlobalTestOptions, + edition: Edition, + unused_externs: &Arc>>, +) { + for doctest in doctests { + standalone.push(doctest.generate_test_desc_and_fn( + opts, + edition, + Arc::clone(unused_externs), + )); + } +} + +#[derive(Default)] +pub(crate) struct DocTestKinds { + /// Tests that cannot be run together with the rest (`compile_fail` and `test_harness`). + standalone: Vec, + others: FxHashMap>, +} + +impl DocTestKinds { + pub(crate) fn add_doctest( + &mut self, + doctest: DocTest, + opts: &GlobalTestOptions, + edition: Edition, + rustdoc_options: &RustdocOptions, + unused_externs: &Arc>>, + ) { + if doctest.failed_ast + || doctest.lang_string.compile_fail + || doctest.lang_string.test_harness + || doctest.lang_string.standalone + || rustdoc_options.nocapture + || rustdoc_options.test_args.iter().any(|arg| arg == "--show-output") + || doctest.crate_attrs.contains("#![no_std]") + { + self.standalone.push(doctest.generate_test_desc_and_fn( + opts, + edition, + Arc::clone(unused_externs), + )); + } else { + self.others.entry(edition).or_default().push(doctest); + } + } + + pub(crate) fn run_tests( + self, + mut test_args: Vec, + nocapture: bool, + opts: GlobalTestOptions, + can_merge_doctests: bool, + unused_externs: &Arc>>, + ) { + test_args.insert(0, "rustdoctest".to_string()); + if nocapture { + test_args.push("--nocapture".to_string()); + } + let Self { mut standalone, others } = self; + let mut ran_edition_tests = 0; + let mut nb_errors = 0; + + for (edition, mut doctests) in others { + if doctests.is_empty() { + continue; + } + if can_merge_doctests { + doctests.sort_by(|a, b| a.name.cmp(&b.name)); + let outdir = Arc::clone(&doctests[0].outdir); + + let mut tests_runner = DocTestRunner::new(); + let rustdoc_test_options = Arc::clone(&doctests[0].rustdoc_test_options); + + for doctest in doctests { + tests_runner.add_test(doctest); + } + if let Ok(success) = tests_runner.run_tests( + rustdoc_test_options, + edition, + &opts, + &test_args, + &outdir, + ) { + ran_edition_tests += 1; + if !success { + nb_errors += 1; + } + continue; + } else { + // We failed to compile all compatible tests as one so we push them into the + // "standalone" doctests. + debug!( + "Failed to compile compatible doctests for edition {} all at once", + edition + ); + add_standalone_tests( + &mut standalone, + tests_runner.doctests, + &opts, + edition, + unused_externs, + ); + } + } else { + add_standalone_tests(&mut standalone, doctests, &opts, edition, unused_externs); + } + } + + if ran_edition_tests == 0 || !standalone.is_empty() { + standalone.sort_by(|a, b| a.desc.name.as_slice().cmp(&b.desc.name.as_slice())); + test::test_main(&test_args, standalone, None); + } + if nb_errors != 0 { + // libtest::ERROR_EXIT_CODE is not public but it's the same value. + std::process::exit(101); + } + } +} + pub(crate) struct Collector { - pub(crate) tests: Vec, + pub(crate) tests: DocTestKinds, // The name of the test displayed to the user, separated by `::`. // @@ -1028,7 +1690,7 @@ pub(crate) struct Collector { visited_tests: FxHashMap<(String, usize), usize>, unused_extern_reports: Arc>>, compiling_test_count: AtomicUsize, - arg_file: PathBuf, + rustdoc_test_options: Arc, } impl Collector { @@ -1042,8 +1704,9 @@ impl Collector { enable_per_target_ignores: bool, arg_file: PathBuf, ) -> Collector { + let rustdoc_test_options = Arc::new(IndividualTestOptions::new(&rustdoc_options, arg_file)); Collector { - tests: Vec::new(), + tests: Default::default(), names: Vec::new(), rustdoc_options, use_headers, @@ -1056,7 +1719,7 @@ impl Collector { visited_tests: FxHashMap::default(), unused_extern_reports: Default::default(), compiling_test_count: AtomicUsize::new(0), - arg_file, + rustdoc_test_options, } } @@ -1096,11 +1759,10 @@ impl Tester for Collector { fn add_test(&mut self, test: String, config: LangString, line: usize) { let filename = self.get_filename(); let name = self.generate_name(line, &filename); - let crate_name = self.crate_name.clone(); + let crate_name = Cow::Borrowed(self.crate_name.as_str()); let opts = self.opts.clone(); let edition = config.edition.unwrap_or(self.rustdoc_options.edition); let target_str = self.rustdoc_options.target.to_string(); - let unused_externs = self.unused_extern_reports.clone(); let no_run = config.no_run || self.rustdoc_options.no_run; if !config.compile_fail { self.compiling_test_count.fetch_add(1, Ordering::SeqCst); @@ -1115,7 +1777,7 @@ impl Tester for Collector { unreachable!("doctest from a different crate"); } } - _ => PathBuf::from(r"doctest.rs"), + _ => PathBuf::from("doctest.rs"), }; // For example `module/file.rs` would become `module_file_rs` @@ -1127,8 +1789,6 @@ impl Tester for Collector { .collect::(); let test_id = format!( "{file}_{line}_{number}", - file = file, - line = line, number = { // Increases the current test number, if this file already // exists or it creates a new entry with a test number of 0. @@ -1136,101 +1796,28 @@ impl Tester for Collector { }, ); - let rustdoc_test_options = - IndividualTestOptions::new(&self.rustdoc_options, &self.arg_file, test_id); - debug!("creating test {name}: {test}"); - self.tests.push(test::TestDescAndFn { - desc: test::TestDesc { - name: test::DynTestName(name), - ignore: match config.ignore { - Ignore::All => true, - Ignore::None => false, - Ignore::Some(ref ignores) => ignores.iter().any(|s| target_str.contains(s)), - }, - ignore_message: None, - source_file: "", - start_line: 0, - start_col: 0, - end_line: 0, - end_col: 0, - // compiler failures are test failures - should_panic: test::ShouldPanic::No, - compile_fail: config.compile_fail, - no_run, - test_type: test::TestType::DocTest, - }, - testfn: test::DynTestFn(Box::new(move || { - let report_unused_externs = |uext| { - unused_externs.lock().unwrap().push(uext); - }; - let res = run_test( - &test, - &crate_name, - line, - rustdoc_test_options, - config, - no_run, - &opts, - edition, - path, - 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}"); - } - } - } - } - - panic::resume_unwind(Box::new(())); - } - Ok(()) - })), + let doctest = make_test(MakeTestArgs { + source_code: test, + crate_name: Some(crate_name), + edition, + name, + lang_string: config, + line, + file, + rustdoc_test_options: Arc::clone(&self.rustdoc_test_options), + test_id, + target_str: &target_str, + path, + no_run, }); + self.tests.add_doctest( + doctest, + &opts, + edition, + &self.rustdoc_options, + &self.unused_extern_reports, + ); } fn get_line(&self) -> usize { diff --git a/src/librustdoc/doctest/tests.rs b/src/librustdoc/doctest/tests.rs index 9629acb31eb68..37306274b4c90 100644 --- a/src/librustdoc/doctest/tests.rs +++ b/src/librustdoc/doctest/tests.rs @@ -1,17 +1,38 @@ -use super::{make_test, GlobalTestOptions}; +use super::{DocTest, GlobalTestOptions, IndividualTestOptions}; +use crate::html::markdown::LangString; use rustc_span::edition::DEFAULT_EDITION; +use std::path::PathBuf; +use std::sync::Arc; + +fn make_test(input: String, krate: Option<&str>) -> DocTest { + super::make_test(crate::doctest::MakeTestArgs { + source_code: input, + crate_name: krate.map(|k| k.into()), + edition: DEFAULT_EDITION, + name: String::new(), + lang_string: LangString::empty_for_test(), + line: 0, + file: String::new(), + rustdoc_test_options: Arc::new(IndividualTestOptions::empty()), + test_id: String::new(), + target_str: "", + path: PathBuf::new(), + no_run: true, + }) +} #[test] fn make_test_basic() { //basic use: wraps with `fn main`, adds `#![allow(unused)]` let opts = GlobalTestOptions::default(); - let input = "assert_eq!(2+2, 4);"; + let input = "assert_eq!(2+2, 4);".to_string(); let expected = "#![allow(unused)] fn main() { assert_eq!(2+2, 4); }" .to_string(); - let (output, len, _) = make_test(input, None, false, &opts, DEFAULT_EDITION, None); + let krate = None; + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -20,13 +41,14 @@ fn make_test_crate_name_no_use() { // If you give a crate name but *don't* use it within the test, it won't bother inserting // the `extern crate` statement. let opts = GlobalTestOptions::default(); - let input = "assert_eq!(2+2, 4);"; + let input = "assert_eq!(2+2, 4);".to_string(); let expected = "#![allow(unused)] fn main() { assert_eq!(2+2, 4); }" .to_string(); - let (output, len, _) = make_test(input, Some("asdf"), false, &opts, DEFAULT_EDITION, None); + let krate = Some("asdf"); + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -36,7 +58,8 @@ fn make_test_crate_name() { // statement before `fn main`. let opts = GlobalTestOptions::default(); let input = "use asdf::qwop; -assert_eq!(2+2, 4);"; +assert_eq!(2+2, 4);" + .to_string(); let expected = "#![allow(unused)] #[allow(unused_extern_crates)] extern crate r#asdf; @@ -45,7 +68,8 @@ use asdf::qwop; assert_eq!(2+2, 4); }" .to_string(); - let (output, len, _) = make_test(input, Some("asdf"), false, &opts, DEFAULT_EDITION, None); + let krate = Some("asdf"); + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 3)); } @@ -56,14 +80,16 @@ fn make_test_no_crate_inject() { let opts = GlobalTestOptions { no_crate_inject: true, attrs: vec![], insert_indent_space: false }; let input = "use asdf::qwop; -assert_eq!(2+2, 4);"; +assert_eq!(2+2, 4);" + .to_string(); let expected = "#![allow(unused)] fn main() { use asdf::qwop; assert_eq!(2+2, 4); }" .to_string(); - let (output, len, _) = make_test(input, Some("asdf"), false, &opts, DEFAULT_EDITION, None); + let krate = Some("asdf"); + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -74,14 +100,16 @@ fn make_test_ignore_std() { // compiler! let opts = GlobalTestOptions::default(); let input = "use std::*; -assert_eq!(2+2, 4);"; +assert_eq!(2+2, 4);" + .to_string(); let expected = "#![allow(unused)] fn main() { use std::*; assert_eq!(2+2, 4); }" .to_string(); - let (output, len, _) = make_test(input, Some("std"), false, &opts, DEFAULT_EDITION, None); + let krate = Some("std"); + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -92,7 +120,8 @@ fn make_test_manual_extern_crate() { let opts = GlobalTestOptions::default(); let input = "extern crate asdf; use asdf::qwop; -assert_eq!(2+2, 4);"; +assert_eq!(2+2, 4);" + .to_string(); let expected = "#![allow(unused)] extern crate asdf; fn main() { @@ -100,7 +129,8 @@ use asdf::qwop; assert_eq!(2+2, 4); }" .to_string(); - let (output, len, _) = make_test(input, Some("asdf"), false, &opts, DEFAULT_EDITION, None); + let krate = Some("asdf"); + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -109,7 +139,8 @@ fn make_test_manual_extern_crate_with_macro_use() { let opts = GlobalTestOptions::default(); let input = "#[macro_use] extern crate asdf; use asdf::qwop; -assert_eq!(2+2, 4);"; +assert_eq!(2+2, 4);" + .to_string(); let expected = "#![allow(unused)] #[macro_use] extern crate asdf; fn main() { @@ -117,7 +148,8 @@ use asdf::qwop; assert_eq!(2+2, 4); }" .to_string(); - let (output, len, _) = make_test(input, Some("asdf"), false, &opts, DEFAULT_EDITION, None); + let krate = Some("asdf"); + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -128,7 +160,8 @@ fn make_test_opts_attrs() { let mut opts = GlobalTestOptions::default(); opts.attrs.push("feature(sick_rad)".to_string()); let input = "use asdf::qwop; -assert_eq!(2+2, 4);"; +assert_eq!(2+2, 4);" + .to_string(); let expected = "#![feature(sick_rad)] #[allow(unused_extern_crates)] extern crate r#asdf; @@ -137,7 +170,9 @@ use asdf::qwop; assert_eq!(2+2, 4); }" .to_string(); - let (output, len, _) = make_test(input, Some("asdf"), false, &opts, DEFAULT_EDITION, None); + let krate = Some("asdf"); + let (output, len) = + make_test(input.clone(), krate.clone()).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 3)); // Adding more will also bump the returned line offset. @@ -151,7 +186,7 @@ use asdf::qwop; assert_eq!(2+2, 4); }" .to_string(); - let (output, len, _) = make_test(input, Some("asdf"), false, &opts, DEFAULT_EDITION, None); + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 4)); } @@ -161,14 +196,16 @@ fn make_test_crate_attrs() { // them outside the generated main function. let opts = GlobalTestOptions::default(); let input = "#![feature(sick_rad)] -assert_eq!(2+2, 4);"; +assert_eq!(2+2, 4);" + .to_string(); let expected = "#![allow(unused)] #![feature(sick_rad)] fn main() { assert_eq!(2+2, 4); }" .to_string(); - let (output, len, _) = make_test(input, None, false, &opts, DEFAULT_EDITION, None); + let krate = None; + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -178,13 +215,15 @@ fn make_test_with_main() { let opts = GlobalTestOptions::default(); let input = "fn main() { assert_eq!(2+2, 4); -}"; +}" + .to_string(); let expected = "#![allow(unused)] fn main() { assert_eq!(2+2, 4); }" .to_string(); - let (output, len, _) = make_test(input, None, false, &opts, DEFAULT_EDITION, None); + let krate = None; + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 1)); } @@ -193,14 +232,16 @@ fn make_test_fake_main() { // ... but putting it in a comment will still provide a wrapper. let opts = GlobalTestOptions::default(); let input = "//Ceci n'est pas une `fn main` -assert_eq!(2+2, 4);"; +assert_eq!(2+2, 4);" + .to_string(); let expected = "#![allow(unused)] //Ceci n'est pas une `fn main` fn main() { assert_eq!(2+2, 4); }" .to_string(); - let (output, len, _) = make_test(input, None, false, &opts, DEFAULT_EDITION, None); + let krate = None; + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -209,12 +250,14 @@ fn make_test_dont_insert_main() { // Even with that, if you set `dont_insert_main`, it won't create the `fn main` wrapper. let opts = GlobalTestOptions::default(); let input = "//Ceci n'est pas une `fn main` -assert_eq!(2+2, 4);"; +assert_eq!(2+2, 4);" + .to_string(); let expected = "#![allow(unused)] //Ceci n'est pas une `fn main` assert_eq!(2+2, 4);" .to_string(); - let (output, len, _) = make_test(input, None, true, &opts, DEFAULT_EDITION, None); + let krate = None; + let (output, len) = make_test(input, krate).generate_unique_doctest(true, &opts, None); assert_eq!((output, len), (expected, 1)); } @@ -223,7 +266,8 @@ fn make_test_issues_21299_33731() { let opts = GlobalTestOptions::default(); let input = "// fn main -assert_eq!(2+2, 4);"; +assert_eq!(2+2, 4);" + .to_string(); let expected = "#![allow(unused)] // fn main @@ -232,11 +276,13 @@ assert_eq!(2+2, 4); }" .to_string(); - let (output, len, _) = make_test(input, None, false, &opts, DEFAULT_EDITION, None); + let krate = None; + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); let input = "extern crate hella_qwop; -assert_eq!(asdf::foo, 4);"; +assert_eq!(asdf::foo, 4);" + .to_string(); let expected = "#![allow(unused)] extern crate hella_qwop; @@ -247,7 +293,8 @@ assert_eq!(asdf::foo, 4); }" .to_string(); - let (output, len, _) = make_test(input, Some("asdf"), false, &opts, DEFAULT_EDITION, None); + let krate = Some("asdf"); + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 3)); } @@ -257,7 +304,8 @@ fn make_test_main_in_macro() { let input = "#[macro_use] extern crate my_crate; test_wrapper! { fn main() {} -}"; +}" + .to_string(); let expected = "#![allow(unused)] #[macro_use] extern crate my_crate; test_wrapper! { @@ -265,7 +313,8 @@ test_wrapper! { }" .to_string(); - let (output, len, _) = make_test(input, Some("my_crate"), false, &opts, DEFAULT_EDITION, None); + let krate = Some("my_crate"); + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 1)); } @@ -276,7 +325,8 @@ fn make_test_returns_result() { let input = "use std::io; let mut input = String::new(); io::stdin().read_line(&mut input)?; -Ok::<(), io:Error>(())"; +Ok::<(), io:Error>(())" + .to_string(); let expected = "#![allow(unused)] fn main() { fn _inner() -> Result<(), impl core::fmt::Debug> { use std::io; @@ -285,7 +335,8 @@ io::stdin().read_line(&mut input)?; Ok::<(), io:Error>(()) } _inner().unwrap() }" .to_string(); - let (output, len, _) = make_test(input, None, false, &opts, DEFAULT_EDITION, None); + let krate = None; + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -293,14 +344,15 @@ Ok::<(), io:Error>(()) fn make_test_named_wrapper() { // creates an inner function with a specific name let opts = GlobalTestOptions::default(); - let input = "assert_eq!(2+2, 4);"; + let input = "assert_eq!(2+2, 4);".to_string(); let expected = "#![allow(unused)] fn main() { #[allow(non_snake_case)] fn _doctest_main__some_unique_name() { assert_eq!(2+2, 4); } _doctest_main__some_unique_name() }" .to_string(); - let (output, len, _) = - make_test(input, None, false, &opts, DEFAULT_EDITION, Some("_some_unique_name")); + let krate = None; + let (output, len) = + make_test(input, krate).generate_unique_doctest(false, &opts, Some("_some_unique_name")); assert_eq!((output, len), (expected, 2)); } @@ -312,7 +364,8 @@ fn make_test_insert_extra_space() { let input = "use std::*; assert_eq!(2+2, 4); eprintln!(\"hello anan\"); -"; +" + .to_string(); let expected = "#![allow(unused)] fn main() { use std::*; @@ -320,7 +373,8 @@ fn main() { eprintln!(\"hello anan\"); }" .to_string(); - let (output, len, _) = make_test(input, None, false, &opts, DEFAULT_EDITION, None); + let krate = None; + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -333,7 +387,8 @@ fn make_test_insert_extra_space_fn_main() { fn main() { assert_eq!(2+2, 4); eprintln!(\"hello anan\"); -}"; +}" + .to_string(); let expected = "#![allow(unused)] use std::*; fn main() { @@ -341,6 +396,7 @@ fn main() { eprintln!(\"hello anan\"); }" .to_string(); - let (output, len, _) = make_test(input, None, false, &opts, DEFAULT_EDITION, None); + let krate = None; + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 1)); } diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index 5c5651f3ef0e6..f4bee1d0c8969 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -41,11 +41,11 @@ use std::fmt::Write; use std::iter::Peekable; use std::ops::{ControlFlow, Range}; use std::str::{self, CharIndices}; -use std::sync::OnceLock; +use std::sync::{Arc, OnceLock}; use crate::clean::RenderedLink; use crate::doctest; -use crate::doctest::GlobalTestOptions; +use crate::doctest::{GlobalTestOptions, IndividualTestOptions}; use crate::html::escape::Escape; use crate::html::format::Buffer; use crate::html::highlight; @@ -292,7 +292,6 @@ impl<'a, I: Iterator>> Iterator for CodeBlocks<'_, 'a, I> { let edition = edition.unwrap_or(self.edition); let playground_button = self.playground.as_ref().and_then(|playground| { - let krate = &playground.crate_name; let url = &playground.url; if url.is_empty() { return None; @@ -302,11 +301,25 @@ impl<'a, I: Iterator>> Iterator for CodeBlocks<'_, 'a, I> { .map(|l| map_line(l).for_code()) .intersperse("\n".into()) .collect::(); - let krate = krate.as_ref().map(|s| s.as_str()); + let krate = playground.crate_name.as_ref().map(|s| Cow::Borrowed(s.as_str())); let mut opts: GlobalTestOptions = Default::default(); opts.insert_indent_space = true; - let (test, _, _) = doctest::make_test(&test, krate, false, &opts, edition, None); + let (test, _) = doctest::make_test(crate::doctest::MakeTestArgs { + source_code: test, + crate_name: krate, + edition, + name: String::new(), + lang_string: LangString::empty_for_test(), + line: 0, + file: String::new(), + rustdoc_test_options: Arc::new(IndividualTestOptions::empty()), + test_id: String::new(), + target_str: "", + path: "doctest.rs".into(), + no_run: true, + }) + .generate_unique_doctest(false, &opts, None); let channel = if test.contains("#![feature(") { "&version=nightly" } else { "" }; let test_escaped = small_url_encode(test); @@ -866,12 +879,32 @@ pub(crate) struct LangString { pub(crate) rust: bool, pub(crate) test_harness: bool, pub(crate) compile_fail: bool, + pub(crate) standalone: bool, pub(crate) error_codes: Vec, pub(crate) edition: Option, pub(crate) added_classes: Vec, pub(crate) unknown: Vec, } +impl LangString { + pub(crate) fn empty_for_test() -> Self { + Self { + original: String::new(), + should_panic: false, + no_run: false, + ignore: Ignore::None, + rust: true, + test_harness: false, + compile_fail: false, + standalone: false, + error_codes: Vec::new(), + edition: None, + added_classes: Vec::new(), + unknown: Vec::new(), + } + } +} + #[derive(Eq, PartialEq, Clone, Debug)] pub(crate) enum Ignore { All, @@ -1200,6 +1233,7 @@ impl Default for LangString { rust: true, test_harness: false, compile_fail: false, + standalone: false, error_codes: Vec::new(), edition: None, added_classes: Vec::new(), @@ -1281,6 +1315,10 @@ impl LangString { seen_rust_tags = !seen_other_tags || seen_rust_tags; data.no_run = true; } + LangStringToken::LangToken("standalone") => { + data.standalone = true; + seen_rust_tags = !seen_other_tags || seen_rust_tags; + } LangStringToken::LangToken(x) if x.starts_with("edition") => { data.edition = x[7..].parse::().ok(); } diff --git a/src/librustdoc/markdown.rs b/src/librustdoc/markdown.rs index 7289ed56dc7a2..f49915a76db2a 100644 --- a/src/librustdoc/markdown.rs +++ b/src/librustdoc/markdown.rs @@ -2,6 +2,7 @@ use std::fmt::Write as _; use std::fs::{create_dir_all, read_to_string, File}; use std::io::prelude::*; use std::path::Path; +use std::sync::{Arc, Mutex}; use tempfile::tempdir; @@ -164,7 +165,7 @@ pub(crate) fn test(options: Options) -> Result<(), String> { options.input.filestem().to_string(), options.clone(), true, - opts, + opts.clone(), None, options.input.opt_path().map(ToOwned::to_owned), options.enable_per_target_ignores, @@ -183,6 +184,7 @@ pub(crate) fn test(options: Options) -> Result<(), String> { false, ); - crate::doctest::run_tests(options.test_args, options.nocapture, collector.tests); + let unused_externs = Arc::new(Mutex::new(Vec::new())); + collector.tests.run_tests(options.test_args, options.nocapture, opts, false, &unused_externs); Ok(()) } diff --git a/tests/run-make/doctests-keep-binaries/rmake.rs b/tests/run-make/doctests-keep-binaries/rmake.rs index 0613ef4839b14..d6611f70ec4a0 100644 --- a/tests/run-make/doctests-keep-binaries/rmake.rs +++ b/tests/run-make/doctests-keep-binaries/rmake.rs @@ -14,8 +14,8 @@ fn setup_test_env(callback: F) { } fn check_generated_binaries() { - run("doctests/t_rs_2_0/rust_out"); - run("doctests/t_rs_8_0/rust_out"); + run("doctests/t_rs_12_0/rust_out"); + run("doctests/rust_out_2024"); } fn main() { @@ -27,6 +27,8 @@ fn main() { .arg("--persist-doctests") .arg(out_dir) .extern_("t", extern_path) + .arg("--edition") + .arg("2024") .run(); check_generated_binaries(); }); @@ -38,6 +40,8 @@ fn main() { .arg("--persist-doctests") .arg(out_dir) .extern_("t", extern_path) + .arg("--edition") + .arg("2024") .arg("--no-run") .run(); check_generated_binaries(); @@ -58,6 +62,8 @@ fn main() { .arg("--test-run-directory") .arg(run_dir) .extern_("t", "libt.rlib") + .arg("--edition") + .arg("2024") .run(); remove_dir_all(run_dir_path); diff --git a/tests/run-make/doctests-keep-binaries/t.rs b/tests/run-make/doctests-keep-binaries/t.rs index c38cf0a0b25d4..806f54764779d 100644 --- a/tests/run-make/doctests-keep-binaries/t.rs +++ b/tests/run-make/doctests-keep-binaries/t.rs @@ -8,4 +8,12 @@ pub fn foople() {} /// ``` /// t::florp(); /// ``` +/// +/// ``` +/// #![no_std] +/// +/// fn main() { +/// let x = 12; +/// } +/// ``` pub fn florp() {} diff --git a/tests/run-make/doctests-merge/doctest-failure.stdout b/tests/run-make/doctests-merge/doctest-failure.stdout new file mode 100644 index 0000000000000..5a0a5a950d87f --- /dev/null +++ b/tests/run-make/doctests-merge/doctest-failure.stdout @@ -0,0 +1,26 @@ + +running 2 tests +test doctest.rs - (line 4) ... ok +test doctest.rs - init (line 8) ... FAILED + +failures: + +---- doctest.rs - init (line 8) stdout ---- +thread 'doctest.rs - init (line 8)' panicked at doctest.rs:15:9: +assertion failed: !IS_INIT +stack backtrace: + 0: rust_begin_unwind + 1: core::panicking::panic_fmt + 2: core::panicking::panic + 3: foo::init + 4: doctest_2024::__doctest_1::main + 5: doctest_2024::__doctest_1::TEST::{{closure}} + 6: core::ops::function::FnOnce::call_once +note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace. + + +failures: + doctest.rs - init (line 8) + +test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME + diff --git a/tests/run-make/doctests-merge/doctest-standalone.rs b/tests/run-make/doctests-merge/doctest-standalone.rs new file mode 100644 index 0000000000000..134ffb58285e8 --- /dev/null +++ b/tests/run-make/doctests-merge/doctest-standalone.rs @@ -0,0 +1,18 @@ +#![crate_name = "foo"] +#![crate_type = "lib"] + +//! ```standalone +//! foo::init(); +//! ``` + +/// ```standalone +/// foo::init(); +/// ``` +pub fn init() { + static mut IS_INIT: bool = false; + + unsafe { + assert!(!IS_INIT); + IS_INIT = true; + } +} diff --git a/tests/run-make/doctests-merge/doctest-standalone.stdout b/tests/run-make/doctests-merge/doctest-standalone.stdout new file mode 100644 index 0000000000000..ee9f62326ab02 --- /dev/null +++ b/tests/run-make/doctests-merge/doctest-standalone.stdout @@ -0,0 +1,7 @@ + +running 2 tests +test doctest-standalone.rs - (line 4) ... ok +test doctest-standalone.rs - init (line 8) ... ok + +test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME + diff --git a/tests/run-make/doctests-merge/doctest-success.stdout b/tests/run-make/doctests-merge/doctest-success.stdout new file mode 100644 index 0000000000000..7da08d68faae3 --- /dev/null +++ b/tests/run-make/doctests-merge/doctest-success.stdout @@ -0,0 +1,7 @@ + +running 2 tests +test doctest.rs - (line 4) ... ok +test doctest.rs - init (line 8) ... ok + +test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME + diff --git a/tests/run-make/doctests-merge/doctest.rs b/tests/run-make/doctests-merge/doctest.rs new file mode 100644 index 0000000000000..66a5d88db67f4 --- /dev/null +++ b/tests/run-make/doctests-merge/doctest.rs @@ -0,0 +1,18 @@ +#![crate_name = "foo"] +#![crate_type = "lib"] + +//! ``` +//! foo::init(); +//! ``` + +/// ``` +/// foo::init(); +/// ``` +pub fn init() { + static mut IS_INIT: bool = false; + + unsafe { + assert!(!IS_INIT); + IS_INIT = true; + } +} diff --git a/tests/run-make/doctests-merge/rmake.rs b/tests/run-make/doctests-merge/rmake.rs new file mode 100644 index 0000000000000..f2f3923731f58 --- /dev/null +++ b/tests/run-make/doctests-merge/rmake.rs @@ -0,0 +1,47 @@ +extern crate run_make_support; + +use run_make_support::{diff, rustc, rustdoc, tmp_dir}; +use std::path::Path; + +fn test_and_compare( + input_file: &str, + stdout_file: &str, + edition: &str, + should_succeed: bool, + dep: &Path, +) { + let mut cmd = rustdoc(); + + cmd.input(input_file) + .arg("--test") + .arg("-Zunstable-options") + .arg("--edition") + .arg(edition) + .arg("--test-args=--test-threads=1") + .arg("--extern") + .arg(format!("foo={}", dep.display())) + .env("RUST_BACKTRACE", "short"); + let output = if should_succeed { cmd.run() } else { cmd.run_fail() }; + + diff() + .expected_file(stdout_file) + .actual_text("output", output.stdout) + .normalize(r#"finished in \d+\.\d+s"#, "finished in $$TIME") + .run(); +} + +fn main() { + let out_file = tmp_dir().join("libfoo.rlib"); + + rustc().input("doctest.rs").crate_type("rlib").arg("-o").arg(&out_file).run(); + + // First we ensure that running with the 2024 edition will fail at runtime. + test_and_compare("doctest.rs", "doctest-failure.stdout", "2024", false, &out_file); + + // Then we ensure that running with an edition < 2024 will not fail at runtime. + test_and_compare("doctest.rs", "doctest-success.stdout", "2021", true, &out_file); + + // Now we check with the standalone attribute which should succeed in all cases. + test_and_compare("doctest-standalone.rs", "doctest-standalone.stdout", "2024", true, &out_file); + test_and_compare("doctest-standalone.rs", "doctest-standalone.stdout", "2021", true, &out_file); +} diff --git a/tests/rustdoc-ui/doctest/failed-doctest-should-panic-2021.rs b/tests/rustdoc-ui/doctest/failed-doctest-should-panic-2021.rs new file mode 100644 index 0000000000000..ad78bb545533d --- /dev/null +++ b/tests/rustdoc-ui/doctest/failed-doctest-should-panic-2021.rs @@ -0,0 +1,12 @@ +// FIXME: if/when the output of the test harness can be tested on its own, this test should be +// adapted to use that, and that normalize line can go away + +//@ compile-flags:--test --edition 2021 +//@ normalize-stdout-test: "tests/rustdoc-ui/doctest" -> "$$DIR" +//@ normalize-stdout-test "finished in \d+\.\d+s" -> "finished in $$TIME" +//@ failure-status: 101 + +/// ```should_panic +/// println!("Hello, world!"); +/// ``` +pub struct Foo; diff --git a/tests/rustdoc-ui/doctest/failed-doctest-should-panic-2021.stdout b/tests/rustdoc-ui/doctest/failed-doctest-should-panic-2021.stdout new file mode 100644 index 0000000000000..63d987de8a9fa --- /dev/null +++ b/tests/rustdoc-ui/doctest/failed-doctest-should-panic-2021.stdout @@ -0,0 +1,14 @@ + +running 1 test +test $DIR/failed-doctest-should-panic-2021.rs - Foo (line 9) ... FAILED + +failures: + +---- $DIR/failed-doctest-should-panic-2021.rs - Foo (line 9) stdout ---- +Test executable succeeded, but it's marked `should_panic`. + +failures: + $DIR/failed-doctest-should-panic-2021.rs - Foo (line 9) + +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME + diff --git a/tests/rustdoc-ui/doctest/failed-doctest-should-panic.rs b/tests/rustdoc-ui/doctest/failed-doctest-should-panic.rs index 6426fd353a7f7..687ed9fb55cbc 100644 --- a/tests/rustdoc-ui/doctest/failed-doctest-should-panic.rs +++ b/tests/rustdoc-ui/doctest/failed-doctest-should-panic.rs @@ -1,7 +1,7 @@ // FIXME: if/when the output of the test harness can be tested on its own, this test should be // adapted to use that, and that normalize line can go away -//@ compile-flags:--test +//@ compile-flags:--test -Z unstable-options --edition 2024 //@ normalize-stdout-test: "tests/rustdoc-ui/doctest" -> "$$DIR" //@ normalize-stdout-test "finished in \d+\.\d+s" -> "finished in $$TIME" //@ failure-status: 101 diff --git a/tests/rustdoc-ui/doctest/failed-doctest-should-panic.stdout b/tests/rustdoc-ui/doctest/failed-doctest-should-panic.stdout index 57a20092a5d6c..71b0b10fa72e2 100644 --- a/tests/rustdoc-ui/doctest/failed-doctest-should-panic.stdout +++ b/tests/rustdoc-ui/doctest/failed-doctest-should-panic.stdout @@ -1,11 +1,12 @@ running 1 test -test $DIR/failed-doctest-should-panic.rs - Foo (line 9) ... FAILED +test $DIR/failed-doctest-should-panic.rs - Foo (line 9) - should panic ... FAILED failures: ---- $DIR/failed-doctest-should-panic.rs - Foo (line 9) stdout ---- -Test executable succeeded, but it's marked `should_panic`. +Hello, world! +note: test did not panic as expected failures: $DIR/failed-doctest-should-panic.rs - Foo (line 9) diff --git a/tests/rustdoc-ui/doctest/no-run-flag.rs b/tests/rustdoc-ui/doctest/no-run-flag.rs index bdb977b5504d4..96a1b2959d649 100644 --- a/tests/rustdoc-ui/doctest/no-run-flag.rs +++ b/tests/rustdoc-ui/doctest/no-run-flag.rs @@ -1,7 +1,7 @@ // test the behavior of the --no-run flag //@ check-pass -//@ compile-flags:-Z unstable-options --test --no-run --test-args=--test-threads=1 +//@ compile-flags:-Z unstable-options --test --no-run --test-args=--test-threads=1 --edition 2024 //@ normalize-stdout-test: "tests/rustdoc-ui/doctest" -> "$$DIR" //@ normalize-stdout-test "finished in \d+\.\d+s" -> "finished in $$TIME" diff --git a/tests/rustdoc-ui/doctest/no-run-flag.stdout b/tests/rustdoc-ui/doctest/no-run-flag.stdout index 02f28aaf60da0..6838be8e6c435 100644 --- a/tests/rustdoc-ui/doctest/no-run-flag.stdout +++ b/tests/rustdoc-ui/doctest/no-run-flag.stdout @@ -1,12 +1,17 @@ -running 7 tests +running 5 tests test $DIR/no-run-flag.rs - f (line 11) - compile ... ok -test $DIR/no-run-flag.rs - f (line 14) ... ignored test $DIR/no-run-flag.rs - f (line 17) - compile ... ok -test $DIR/no-run-flag.rs - f (line 23) - compile fail ... ok test $DIR/no-run-flag.rs - f (line 28) - compile ... ok test $DIR/no-run-flag.rs - f (line 32) - compile ... ok test $DIR/no-run-flag.rs - f (line 8) - compile ... ok -test result: ok. 6 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in $TIME +test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME + + +running 2 tests +test $DIR/no-run-flag.rs - f (line 14) ... ignored +test $DIR/no-run-flag.rs - f (line 23) - compile fail ... ok + +test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in $TIME diff --git a/tests/rustdoc-ui/doctest/test-type.rs b/tests/rustdoc-ui/doctest/test-type.rs index d18143368e86a..f53ccc3780c63 100644 --- a/tests/rustdoc-ui/doctest/test-type.rs +++ b/tests/rustdoc-ui/doctest/test-type.rs @@ -1,4 +1,4 @@ -//@ compile-flags: --test --test-args=--test-threads=1 +//@ compile-flags: --test --test-args=--test-threads=1 -Z unstable-options --edition 2024 //@ check-pass //@ normalize-stdout-test: "tests/rustdoc-ui/doctest" -> "$$DIR" //@ normalize-stdout-test "finished in \d+\.\d+s" -> "finished in $$TIME" diff --git a/tests/rustdoc-ui/doctest/test-type.stdout b/tests/rustdoc-ui/doctest/test-type.stdout index a66fd240d34c4..4a6c5463833f1 100644 --- a/tests/rustdoc-ui/doctest/test-type.stdout +++ b/tests/rustdoc-ui/doctest/test-type.stdout @@ -1,10 +1,15 @@ -running 5 tests -test $DIR/test-type.rs - f (line 12) ... ignored +running 3 tests test $DIR/test-type.rs - f (line 15) - compile ... ok -test $DIR/test-type.rs - f (line 21) - compile fail ... ok test $DIR/test-type.rs - f (line 6) ... ok -test $DIR/test-type.rs - f (line 9) ... ok +test $DIR/test-type.rs - f (line 9) - should panic ... ok + +test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME + + +running 2 tests +test $DIR/test-type.rs - f (line 12) ... ignored +test $DIR/test-type.rs - f (line 21) - compile fail ... ok -test result: ok. 4 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in $TIME +test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in $TIME diff --git a/tests/rustdoc-ui/doctest/wrong-ast.rs b/tests/rustdoc-ui/doctest/wrong-ast.rs new file mode 100644 index 0000000000000..b3fbf630c327c --- /dev/null +++ b/tests/rustdoc-ui/doctest/wrong-ast.rs @@ -0,0 +1,20 @@ +//@ compile-flags:--test --test-args=--test-threads=1 +//@ normalize-stdout-test: "tests/rustdoc-ui/doctest" -> "$$DIR" +//@ normalize-stdout-test "finished in \d+\.\d+s" -> "finished in $$TIME" +//@ normalize-stdout-test "wrong-ast.rs:\d+:\d+" -> "wrong-ast.rs:$$LINE:$$COL" +//@ failure-status: 101 + +/// ``` +/// /* plop +/// ``` +pub fn one() {} + +/// ``` +/// } mod __doctest_1 { fn main() { +/// ``` +pub fn two() {} + +/// ```should_panic +/// panic!() +/// ``` +pub fn three() {} diff --git a/tests/rustdoc-ui/doctest/wrong-ast.stdout b/tests/rustdoc-ui/doctest/wrong-ast.stdout new file mode 100644 index 0000000000000..c827254d8c0f5 --- /dev/null +++ b/tests/rustdoc-ui/doctest/wrong-ast.stdout @@ -0,0 +1,36 @@ + +running 3 tests +test $DIR/wrong-ast.rs - one (line 7) ... FAILED +test $DIR/wrong-ast.rs - three (line 17) ... ok +test $DIR/wrong-ast.rs - two (line 12) ... FAILED + +failures: + +---- $DIR/wrong-ast.rs - one (line 7) stdout ---- +error[E0758]: unterminated block comment + --> $DIR/wrong-ast.rs:$LINE:$COL + | +LL | /* plop + | ^^^^^^^ + +error: aborting due to 1 previous error + +For more information about this error, try `rustc --explain E0758`. +Couldn't compile the test. +---- $DIR/wrong-ast.rs - two (line 12) stdout ---- +error: unexpected closing delimiter: `}` + --> $DIR/wrong-ast.rs:$LINE:$COL + | +LL | } mod __doctest_1 { fn main() { + | ^ unexpected closing delimiter + +error: aborting due to 1 previous error + +Couldn't compile the test. + +failures: + $DIR/wrong-ast.rs - one (line 7) + $DIR/wrong-ast.rs - two (line 12) + +test result: FAILED. 1 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME +