From 86833ee0fce203a3c6be46f5d77a908014d0412e Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Wed, 10 Apr 2024 17:20:15 +0200 Subject: [PATCH 01/24] Clean up rustdoc `make_test` function code --- src/librustdoc/doctest.rs | 45 ++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index 82f9cf1feaeb5..ee3564980a1ae 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -577,6 +577,7 @@ pub(crate) fn make_test( dont_insert_main: bool, opts: &GlobalTestOptions, edition: Edition, + // If `test_id` is `None`, it means we're generating code for a code example "run" link. test_id: Option<&str>, ) -> (String, usize, bool) { let (crate_attrs, everything_else, crates) = partition_source(s, edition); @@ -698,7 +699,7 @@ pub(crate) fn make_test( (found_main, found_extern_crate, found_macro) }) }); - let Ok((already_has_main, already_has_extern_crate, found_macro)) = result else { + let Ok((mut 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); @@ -708,34 +709,34 @@ pub(crate) fn make_test( // 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 && !already_has_main { + already_has_main = s + .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 - }; + .any(|code| code.contains("fn 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; - } - } + if !already_has_extern_crate && + !opts.no_crate_inject && + let Some(crate_name) = 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. + 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; } // FIXME: This code cannot yet handle no_std test cases yet From 587c52201fa3f6fafd045dd7b9c8ed214329a1df Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Thu, 11 Apr 2024 20:01:51 +0200 Subject: [PATCH 02/24] Create DocTest type to handle generation of doctests --- src/librustdoc/doctest.rs | 309 ++++++++++++++++++-------------- src/librustdoc/doctest/tests.rs | 138 +++++++++----- src/librustdoc/html/markdown.rs | 3 +- 3 files changed, 274 insertions(+), 176 deletions(-) diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index ee3564980a1ae..61a139798342b 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -371,7 +371,7 @@ fn wrapped_rustc_command(rustc_wrappers: &[PathBuf], rustc_binary: &Path) -> Com } fn run_test( - test: &str, + test: String, crate_name: &str, line: usize, rustdoc_options: IndividualTestOptions, @@ -382,12 +382,11 @@ fn run_test( path: PathBuf, report_unused_externs: impl Fn(UnusedExterns), ) -> Result<(), TestFailure> { - let (test, line_offset, supports_color) = make_test( - test, + let doctest = make_test(test, Some(crate_name), edition); + let (test, line_offset) = doctest.generate_unique_doctest( Some(crate_name), lang_string.test_harness, opts, - edition, Some(&rustdoc_options.test_id), ); @@ -445,7 +444,11 @@ fn run_test( compiler.arg("--color").arg("always"); } ColorConfig::Auto => { - compiler.arg("--color").arg(if supports_color { "always" } else { "never" }); + compiler.arg("--color").arg(if doctest.supports_color { + "always" + } else { + "never" + }); } } } @@ -569,42 +572,132 @@ 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, - // If `test_id` is `None`, it means we're generating code for a code example "run" link. - 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; +pub(crate) struct DocTest { + test_code: String, + supports_color: bool, + already_has_extern_crate: bool, + main_fn_span: Option, + #[allow(dead_code)] + extern_crates: Vec, + crate_attrs: String, + crates: String, + everything_else: String, +} - 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; - } +impl DocTest { + pub(crate) fn generate_unique_doctest( + &self, + crate_name: Option<&str>, + 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) { + let mut line_offset = 0; + let mut prog = String::with_capacity( + self.test_code.len() + self.crate_attrs.len() + self.crates.len(), + ); + + 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; + } - // 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; + // 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; + } + + // 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); + + // 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(crate_name) = 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"); + + prog.push_str(&format!("extern crate r#{crate_name};\n")); + line_offset += 1; + } + + // 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) } +} - // 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); +/// 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: String, crate_name: Option<&str>, edition: Edition) -> DocTest { + let (crate_attrs, everything_else, crates) = partition_source(&s, edition); + let mut supports_color = false; // Uses librustc_ast to parse the doctest and find if there's a main fn and the extern // crate already is included. @@ -615,8 +708,8 @@ pub(crate) fn make_test( use rustc_parse::parser::ForceCollect; use rustc_span::source_map::FilePathMapping; - let filename = FileName::anon_source_code(s); - let source = crates + everything_else; + let filename = FileName::anon_source_code(&s); + let source = format!("{crates}{everything_else}"); // 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. @@ -635,46 +728,44 @@ pub(crate) fn make_test( let dcx = DiagCtxt::new(Box::new(emitter)).disable_warnings(); let psess = ParseSess::with_dcx(dcx, sm); - let mut found_main = false; + let mut found_main = None; let mut found_extern_crate = crate_name.is_none(); let mut found_macro = false; + let mut extern_crates = Vec::new(); 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); + return (found_main, found_extern_crate, found_macro, Vec::new()); } }; loop { match parser.parse_item(ForceCollect::No) { Ok(Some(item)) => { - if !found_main + if found_main.is_none() && let ast::ItemKind::Fn(..) = item.kind && item.ident.name == sym::main { - found_main = true; + found_main = Some(item.span); } - 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(); - - match original { - Some(name) => found_extern_crate = name.as_str() == crate_name, - None => found_extern_crate = item.ident.as_str() == crate_name, + if let ast::ItemKind::ExternCrate(original) = item.kind { + if !found_extern_crate && let Some(crate_name) = crate_name { + match original { + Some(name) => found_extern_crate = name.as_str() == crate_name, + None => found_extern_crate = item.ident.as_str() == crate_name, + } } + extern_crates.push(item.span); } if !found_macro && let ast::ItemKind::MacCall(..) = item.kind { found_macro = true; } - if found_main && found_extern_crate { + if found_main.is_some() && found_extern_crate { break; } } @@ -696,103 +787,51 @@ pub(crate) fn make_test( // drop. psess.dcx.reset_err_count(); - (found_main, found_extern_crate, found_macro) + (found_main, found_extern_crate, found_macro, extern_crates) }) }); - let Ok((mut already_has_main, already_has_extern_crate, found_macro)) = result else { + let Ok((mut main_fn_span, already_has_extern_crate, found_macro, extern_crates)) = 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); + return DocTest { + test_code: s, + supports_color: false, + main_fn_span: None, + extern_crates: Vec::new(), + crate_attrs, + crates, + everything_else, + already_has_extern_crate: false, + }; }; // 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 - if found_macro && !already_has_main { - already_has_main = s - .lines() + if found_macro + && main_fn_span.is_none() + && s.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")); - } - - // Don't inject `extern crate std` because it's already injected by the - // compiler. - if !already_has_extern_crate && - !opts.no_crate_inject && - let Some(crate_name) = 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. - s.contains(crate_name) + .any(|code| code.contains("fn main")) { - // 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()); + DocTest { + test_code: s, + supports_color, + main_fn_span, + extern_crates, + crate_attrs, + crates, + everything_else, + already_has_extern_crate, } - - debug!("final doctest:\n{prog}"); - - (prog, line_offset, supports_color) } fn check_if_attr_is_complete(source: &str, edition: Edition) -> bool { @@ -930,7 +969,7 @@ 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 { @@ -1166,7 +1205,7 @@ impl Tester for Collector { unused_externs.lock().unwrap().push(uext); }; let res = run_test( - &test, + test, &crate_name, line, rustdoc_test_options, diff --git a/src/librustdoc/doctest/tests.rs b/src/librustdoc/doctest/tests.rs index 9629acb31eb68..e2d5c6dac66a8 100644 --- a/src/librustdoc/doctest/tests.rs +++ b/src/librustdoc/doctest/tests.rs @@ -5,13 +5,15 @@ use rustc_span::edition::DEFAULT_EDITION; 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, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -20,13 +22,15 @@ 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, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -36,7 +40,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 +50,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, krate, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); assert_eq!((output, len), (expected, 3)); } @@ -56,14 +63,17 @@ 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, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -74,14 +84,17 @@ 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, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -92,7 +105,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 +114,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, krate, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -109,7 +125,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 +134,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, krate, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -128,7 +147,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 +157,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, DEFAULT_EDITION) + .generate_unique_doctest(krate, false, &opts, None); assert_eq!((output, len), (expected, 3)); // Adding more will also bump the returned line offset. @@ -151,7 +173,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 (output, len) = + make_test(input, krate, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); assert_eq!((output, len), (expected, 4)); } @@ -161,14 +184,17 @@ 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, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -178,13 +204,16 @@ 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, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); assert_eq!((output, len), (expected, 1)); } @@ -193,14 +222,17 @@ 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, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -209,12 +241,15 @@ 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, DEFAULT_EDITION).generate_unique_doctest(krate, true, &opts, None); assert_eq!((output, len), (expected, 1)); } @@ -223,7 +258,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 +268,14 @@ 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, DEFAULT_EDITION).generate_unique_doctest(krate, 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 +286,9 @@ 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, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); assert_eq!((output, len), (expected, 3)); } @@ -257,7 +298,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 +307,9 @@ 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, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); assert_eq!((output, len), (expected, 1)); } @@ -276,7 +320,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 +330,9 @@ 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, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -293,14 +340,19 @@ 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, DEFAULT_EDITION).generate_unique_doctest( + krate, + 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,9 @@ 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, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -333,7 +388,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 +397,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, DEFAULT_EDITION).generate_unique_doctest(krate, 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..09478af28a9c4 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -306,7 +306,8 @@ impl<'a, I: Iterator>> Iterator for CodeBlocks<'_, 'a, I> { 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(test, krate, edition) + .generate_unique_doctest(krate, false, &opts, None); let channel = if test.contains("#![feature(") { "&version=nightly" } else { "" }; let test_escaped = small_url_encode(test); From 4867760dcc64e3f19419736ee2a6ca7f9913eb64 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Fri, 12 Apr 2024 18:33:36 +0200 Subject: [PATCH 03/24] Split doctests into categories --- src/librustdoc/doctest.rs | 346 ++++++++++++++++++++------------ src/librustdoc/doctest/tests.rs | 103 +++++----- src/librustdoc/html/markdown.rs | 7 +- src/librustdoc/markdown.rs | 2 +- 4 files changed, 272 insertions(+), 186 deletions(-) diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index 61a139798342b..512b7d969b3c6 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -21,6 +21,7 @@ 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::fs::File; use std::io::{self, Write}; @@ -33,6 +34,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}; @@ -216,7 +219,7 @@ pub(crate) fn run( }) })?; - run_tests(test_args, nocapture, tests); + tests.run_tests(test_args, nocapture); // Collect and warn about unused externs, but only if we've gotten // reports for each doctest @@ -259,19 +262,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; @@ -341,7 +331,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. @@ -371,25 +361,16 @@ fn wrapped_rustc_command(rustc_wrappers: &[PathBuf], rustc_binary: &Path) -> Com } fn run_test( + doctest: DocTest, test: String, - crate_name: &str, + line_offset: usize, line: usize, rustdoc_options: IndividualTestOptions, mut lang_string: LangString, - no_run: bool, - opts: &GlobalTestOptions, edition: Edition, path: PathBuf, report_unused_externs: impl Fn(UnusedExterns), ) -> Result<(), TestFailure> { - let doctest = make_test(test, Some(crate_name), edition); - let (test, line_offset) = doctest.generate_unique_doctest( - Some(crate_name), - lang_string.test_harness, - opts, - 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); @@ -420,7 +401,10 @@ fn run_test( compiler.arg("-Z").arg("unstable-options"); } - if no_run && !lang_string.compile_fail && rustdoc_options.should_persist_doctests { + if rustdoc_options.no_run + && !lang_string.compile_fail + && rustdoc_options.should_persist_doctests + { compiler.arg("--emit=metadata"); } compiler.arg("--target").arg(match rustdoc_options.target { @@ -515,7 +499,7 @@ fn run_test( } } - if no_run { + if rustdoc_options.no_run { return Ok(()); } @@ -582,12 +566,14 @@ pub(crate) struct DocTest { crate_attrs: String, crates: String, everything_else: String, + ignore: bool, + crate_name: Option, + name: String, } impl DocTest { pub(crate) fn generate_unique_doctest( &self, - crate_name: Option<&str>, dont_insert_main: bool, opts: &GlobalTestOptions, // If `test_id` is `None`, it means we're generating code for a code example "run" link. @@ -622,7 +608,7 @@ impl DocTest { // compiler. if !self.already_has_extern_crate && !opts.no_crate_inject && - let Some(crate_name) = crate_name && + 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 @@ -691,13 +677,123 @@ impl DocTest { debug!("final doctest:\n{prog}"); (prog, line_offset) } + + fn generate_test_desc_and_fn( + mut self, + config: LangString, + target_str: &str, + line: usize, + rustdoc_test_options: IndividualTestOptions, + opts: &GlobalTestOptions, + edition: Edition, + path: PathBuf, + unused_externs: Arc>>, + ) -> TestDescAndFn { + let (code, line_offset) = self.generate_unique_doctest( + config.test_harness, + opts, + Some(&rustdoc_test_options.test_id), + ); + TestDescAndFn { + desc: test::TestDesc { + name: test::DynTestName(std::mem::replace(&mut self.name, String::new())), + ignore: ignore_to_bool(&config.ignore, target_str), + 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: rustdoc_test_options.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( + self, + code, + line_offset, + line, + rustdoc_test_options, + config, + 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(()) + })), + } + } } /// 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: String, crate_name: Option<&str>, edition: Edition) -> DocTest { +pub(crate) fn make_test( + s: String, + crate_name: Option>, + edition: Edition, + name: String, +) -> DocTest { let (crate_attrs, everything_else, crates) = partition_source(&s, 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. @@ -752,7 +848,7 @@ pub(crate) fn make_test(s: String, crate_name: Option<&str>, edition: Edition) - } if let ast::ItemKind::ExternCrate(original) = item.kind { - if !found_extern_crate && let Some(crate_name) = crate_name { + if !found_extern_crate && let Some(ref crate_name) = crate_name { match original { Some(name) => found_extern_crate = name.as_str() == crate_name, None => found_extern_crate = item.ident.as_str() == crate_name, @@ -803,6 +899,9 @@ pub(crate) fn make_test(s: String, crate_name: Option<&str>, edition: Edition) - crates, everything_else, already_has_extern_crate: false, + ignore: false, + crate_name, + name, }; }; @@ -831,6 +930,9 @@ pub(crate) fn make_test(s: String, crate_name: Option<&str>, edition: Edition) - crates, everything_else, already_has_extern_crate, + ignore: false, + crate_name, + name, } } @@ -987,6 +1089,7 @@ pub(crate) struct IndividualTestOptions { target: TargetTriple, test_id: String, maybe_sysroot: Option, + no_run: bool, } impl IndividualTestOptions { @@ -1020,10 +1123,19 @@ impl IndividualTestOptions { target: options.target.clone(), test_id, maybe_sysroot: options.maybe_sysroot.clone(), + no_run: options.no_run, } } } +fn ignore_to_bool(ignore: &Ignore, target_str: &str) -> bool { + match ignore { + Ignore::All => true, + Ignore::None => false, + Ignore::Some(ref ignores) => ignores.iter().any(|s| target_str.contains(s)), + } +} + pub(crate) trait Tester { fn add_test(&mut self, test: String, config: LangString, line: usize); fn get_line(&self) -> usize { @@ -1032,8 +1144,65 @@ pub(crate) trait Tester { fn register_header(&mut self, _name: &str, _level: u32) {} } +#[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_test( + &mut self, + doctest: DocTest, + config: LangString, + target_str: &str, + line: usize, + rustdoc_test_options: IndividualTestOptions, + opts: &GlobalTestOptions, + edition: Edition, + path: PathBuf, + unused_externs: Arc>>, + ) { + if config.compile_fail || config.test_harness { + self.standalone.push(doctest.generate_test_desc_and_fn( + config, + target_str, + line, + rustdoc_test_options, + opts, + edition, + path, + unused_externs, + )); + } else { + self.others.entry(edition).or_default().push(doctest); + } + } + + pub(crate) fn run_tests(self, mut test_args: Vec, nocapture: bool) { + test_args.insert(0, "rustdoctest".to_string()); + if nocapture { + test_args.push("--nocapture".to_string()); + } + let Self { mut standalone, others } = self; + standalone.sort_by(|a, b| a.desc.name.as_slice().cmp(&b.desc.name.as_slice())); + test::test_main(&test_args, standalone, None); + + // FIXME: generate code for others + // for (edition, doctests) in others { + // let mut id = 0; + // let mut output = String::new(); + + // for doctest in doctests { + // } + // } + drop(others); + } +} + pub(crate) struct Collector { - pub(crate) tests: Vec, + pub(crate) tests: DocTestKinds, // The name of the test displayed to the user, separated by `::`. // @@ -1083,7 +1252,7 @@ impl Collector { arg_file: PathBuf, ) -> Collector { Collector { - tests: Vec::new(), + tests: Default::default(), names: Vec::new(), rustdoc_options, use_headers, @@ -1136,12 +1305,11 @@ 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; + // let no_run = config.no_run || self.rustdoc_options.no_run; if !config.compile_fail { self.compiling_test_count.fetch_add(1, Ordering::SeqCst); } @@ -1180,97 +1348,19 @@ impl Tester for Collector { 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 mut doctest = make_test(test, Some(crate_name), edition, name); + doctest.ignore = ignore_to_bool(&config.ignore, &target_str); + self.tests.add_test( + doctest, + config, + &target_str, + line, + rustdoc_test_options, + &opts, + edition, + path, + self.unused_extern_reports.clone(), + ); } fn get_line(&self) -> usize { diff --git a/src/librustdoc/doctest/tests.rs b/src/librustdoc/doctest/tests.rs index e2d5c6dac66a8..af2d2b45b5eb8 100644 --- a/src/librustdoc/doctest/tests.rs +++ b/src/librustdoc/doctest/tests.rs @@ -1,5 +1,6 @@ use super::{make_test, GlobalTestOptions}; use rustc_span::edition::DEFAULT_EDITION; +use std::borrow::Cow; #[test] fn make_test_basic() { @@ -12,8 +13,8 @@ assert_eq!(2+2, 4); }" .to_string(); let krate = None; - let (output, len) = - make_test(input, krate, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); + let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) + .generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -28,9 +29,9 @@ fn main() { assert_eq!(2+2, 4); }" .to_string(); - let krate = Some("asdf"); - let (output, len) = - make_test(input, krate, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); + let krate = Some(Cow::Borrowed("asdf")); + let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) + .generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -50,9 +51,9 @@ use asdf::qwop; assert_eq!(2+2, 4); }" .to_string(); - let krate = Some("asdf"); - let (output, len) = - make_test(input, krate, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); + let krate = Some(Cow::Borrowed("asdf")); + let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) + .generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 3)); } @@ -71,9 +72,9 @@ use asdf::qwop; assert_eq!(2+2, 4); }" .to_string(); - let krate = Some("asdf"); - let (output, len) = - make_test(input, krate, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); + let krate = Some(Cow::Borrowed("asdf")); + let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) + .generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -92,9 +93,9 @@ use std::*; assert_eq!(2+2, 4); }" .to_string(); - let krate = Some("std"); - let (output, len) = - make_test(input, krate, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); + let krate = Some(Cow::Borrowed("std")); + let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) + .generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -114,9 +115,9 @@ use asdf::qwop; assert_eq!(2+2, 4); }" .to_string(); - let krate = Some("asdf"); - let (output, len) = - make_test(input, krate, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); + let krate = Some(Cow::Borrowed("asdf")); + let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) + .generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -134,9 +135,9 @@ use asdf::qwop; assert_eq!(2+2, 4); }" .to_string(); - let krate = Some("asdf"); - let (output, len) = - make_test(input, krate, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); + let krate = Some(Cow::Borrowed("asdf")); + let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) + .generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -157,9 +158,9 @@ use asdf::qwop; assert_eq!(2+2, 4); }" .to_string(); - let krate = Some("asdf"); - let (output, len) = make_test(input.clone(), krate, DEFAULT_EDITION) - .generate_unique_doctest(krate, false, &opts, None); + let krate = Some(Cow::Borrowed("asdf")); + let (output, len) = make_test(input.clone(), krate.clone(), DEFAULT_EDITION, String::new()) + .generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 3)); // Adding more will also bump the returned line offset. @@ -173,8 +174,8 @@ use asdf::qwop; assert_eq!(2+2, 4); }" .to_string(); - let (output, len) = - make_test(input, krate, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); + let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) + .generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 4)); } @@ -193,8 +194,8 @@ assert_eq!(2+2, 4); }" .to_string(); let krate = None; - let (output, len) = - make_test(input, krate, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); + let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) + .generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -212,8 +213,8 @@ fn main() { }" .to_string(); let krate = None; - let (output, len) = - make_test(input, krate, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); + let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) + .generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 1)); } @@ -231,8 +232,8 @@ assert_eq!(2+2, 4); }" .to_string(); let krate = None; - let (output, len) = - make_test(input, krate, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); + let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) + .generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -248,8 +249,8 @@ assert_eq!(2+2, 4);" assert_eq!(2+2, 4);" .to_string(); let krate = None; - let (output, len) = - make_test(input, krate, DEFAULT_EDITION).generate_unique_doctest(krate, true, &opts, None); + let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) + .generate_unique_doctest(true, &opts, None); assert_eq!((output, len), (expected, 1)); } @@ -269,8 +270,8 @@ assert_eq!(2+2, 4); .to_string(); let krate = None; - let (output, len) = - make_test(input, krate, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); + let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) + .generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); let input = "extern crate hella_qwop; @@ -286,9 +287,9 @@ assert_eq!(asdf::foo, 4); }" .to_string(); - let krate = Some("asdf"); - let (output, len) = - make_test(input, krate, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); + let krate = Some(Cow::Borrowed("asdf")); + let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) + .generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 3)); } @@ -307,9 +308,9 @@ test_wrapper! { }" .to_string(); - let krate = Some("my_crate"); - let (output, len) = - make_test(input, krate, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); + let krate = Some(Cow::Borrowed("my_crate")); + let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) + .generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 1)); } @@ -331,8 +332,8 @@ Ok::<(), io:Error>(()) } _inner().unwrap() }" .to_string(); let krate = None; - let (output, len) = - make_test(input, krate, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); + let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) + .generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -347,12 +348,8 @@ assert_eq!(2+2, 4); } _doctest_main__some_unique_name() }" .to_string(); let krate = None; - let (output, len) = make_test(input, krate, DEFAULT_EDITION).generate_unique_doctest( - krate, - false, - &opts, - Some("_some_unique_name"), - ); + let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) + .generate_unique_doctest(false, &opts, Some("_some_unique_name")); assert_eq!((output, len), (expected, 2)); } @@ -374,8 +371,8 @@ fn main() { }" .to_string(); let krate = None; - let (output, len) = - make_test(input, krate, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); + let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) + .generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -398,7 +395,7 @@ fn main() { }" .to_string(); let krate = None; - let (output, len) = - make_test(input, krate, DEFAULT_EDITION).generate_unique_doctest(krate, false, &opts, None); + let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) + .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 09478af28a9c4..25f035470318d 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -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,12 +301,12 @@ 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, edition) - .generate_unique_doctest(krate, false, &opts, None); + let (test, _) = doctest::make_test(test, krate, edition, String::new()) + .generate_unique_doctest(false, &opts, None); let channel = if test.contains("#![feature(") { "&version=nightly" } else { "" }; let test_escaped = small_url_encode(test); diff --git a/src/librustdoc/markdown.rs b/src/librustdoc/markdown.rs index 7289ed56dc7a2..ebe0e75c0ccbc 100644 --- a/src/librustdoc/markdown.rs +++ b/src/librustdoc/markdown.rs @@ -183,6 +183,6 @@ pub(crate) fn test(options: Options) -> Result<(), String> { false, ); - crate::doctest::run_tests(options.test_args, options.nocapture, collector.tests); + collector.tests.run_tests(options.test_args, options.nocapture); Ok(()) } From 78171c699bb3b44ed494d2485e7908f1f6e0f99c Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Sun, 14 Apr 2024 01:21:28 +0200 Subject: [PATCH 04/24] Compile all (compatible) doctests as one --- src/librustdoc/doctest.rs | 394 ++++++++++++++++++++++---------- src/librustdoc/doctest/tests.rs | 99 ++++---- src/librustdoc/html/markdown.rs | 36 ++- src/librustdoc/markdown.rs | 4 +- 4 files changed, 350 insertions(+), 183 deletions(-) diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index 512b7d969b3c6..e5ee8b6d5b96f 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -23,6 +23,7 @@ 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; @@ -172,7 +173,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| { @@ -215,11 +216,11 @@ 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)) }) })?; - tests.run_tests(test_args, nocapture); + tests.run_tests(test_args, nocapture, opts); // Collect and warn about unused externs, but only if we've gotten // reports for each doctest @@ -361,11 +362,13 @@ fn wrapped_rustc_command(rustc_wrappers: &[PathBuf], rustc_binary: &Path) -> Com } fn run_test( - doctest: DocTest, test: String, + supports_color: bool, line_offset: usize, line: usize, - rustdoc_options: IndividualTestOptions, + rustdoc_options: Arc, + is_multiple_tests: bool, + outdir: Arc, mut lang_string: LangString, edition: Edition, path: PathBuf, @@ -373,7 +376,7 @@ fn run_test( ) -> Result<(), TestFailure> { // 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); + let output_file = outdir.path().join(rust_out); let rustc_binary = rustdoc_options .test_builder @@ -401,16 +404,14 @@ fn run_test( compiler.arg("-Z").arg("unstable-options"); } - if rustdoc_options.no_run - && !lang_string.compile_fail - && rustdoc_options.should_persist_doctests + if lang_string.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 { @@ -428,11 +429,7 @@ fn run_test( compiler.arg("--color").arg("always"); } ColorConfig::Auto => { - compiler.arg("--color").arg(if doctest.supports_color { - "always" - } else { - "never" - }); + compiler.arg("--color").arg(if supports_color { "always" } else { "never" }); } } } @@ -448,7 +445,12 @@ fn run_test( 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"); + let output = if is_multiple_tests { + let status = child.wait().expect("Failed to wait"); + process::Output { status, stdout: Vec::new(), stderr: Vec::new() } + } else { + child.wait_with_output().expect("Failed to read stdout") + }; struct Bomb<'a>(&'a str); impl Drop for Bomb<'_> { @@ -499,7 +501,7 @@ fn run_test( } } - if rustdoc_options.no_run { + if lang_string.no_run { return Ok(()); } @@ -507,19 +509,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(), @@ -561,14 +563,19 @@ pub(crate) struct DocTest { supports_color: bool, already_has_extern_crate: bool, main_fn_span: Option, - #[allow(dead_code)] - extern_crates: Vec, crate_attrs: String, crates: String, everything_else: String, ignore: bool, crate_name: Option, name: String, + lang_string: LangString, + line: usize, + file: String, + failed_ast: bool, + test_id: String, + outdir: Arc, + rustdoc_test_options: Arc, } impl DocTest { @@ -584,20 +591,7 @@ impl DocTest { self.test_code.len() + self.crate_attrs.len() + self.crates.len(), ); - 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; - } - - // 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; - } + Self::push_attrs(&mut prog, opts, &mut line_offset); // Now push any outer attributes from the example, assuming they // are intended to be crate attributes. @@ -678,26 +672,37 @@ impl DocTest { (prog, line_offset) } + 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; + } + + // 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; + } + } + fn generate_test_desc_and_fn( mut self, - config: LangString, - target_str: &str, - line: usize, - rustdoc_test_options: IndividualTestOptions, opts: &GlobalTestOptions, edition: Edition, path: PathBuf, unused_externs: Arc>>, ) -> TestDescAndFn { - let (code, line_offset) = self.generate_unique_doctest( - config.test_harness, - opts, - Some(&rustdoc_test_options.test_id), - ); + 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, .. } = self; TestDescAndFn { desc: test::TestDesc { name: test::DynTestName(std::mem::replace(&mut self.name, String::new())), - ignore: ignore_to_bool(&config.ignore, target_str), + ignore: self.ignore, ignore_message: None, source_file: "", start_line: 0, @@ -706,8 +711,8 @@ impl DocTest { end_col: 0, // compiler failures are test failures should_panic: test::ShouldPanic::No, - compile_fail: config.compile_fail, - no_run: rustdoc_test_options.no_run, + compile_fail: lang_string.compile_fail, + no_run: lang_string.no_run, test_type: test::TestType::DocTest, }, testfn: test::DynTestFn(Box::new(move || { @@ -715,12 +720,14 @@ impl DocTest { unused_externs.lock().unwrap().push(uext); }; let res = run_test( - self, code, + supports_color, line_offset, - line, + self.line, rustdoc_test_options, - config, + false, + outdir, + lang_string, edition, path, report_unused_externs, @@ -781,6 +788,67 @@ impl DocTest { })), } } + + fn generate_test_desc(&self, id: usize, output: &mut String) -> String { + let test_id = format!("__doctest_{id}"); + + 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: false, + 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, + should_panic = if 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.lang_string.no_run { "Ok::<(), String>(())" } else { "self::main()" }, + ) + .unwrap(); + test_id + } } /// Transforms a test into code that can be compiled into a Rust binary, and returns the number of @@ -790,7 +858,26 @@ pub(crate) fn make_test( crate_name: Option>, edition: Edition, name: String, + lang_string: LangString, + line: usize, + file: String, + rustdoc_test_options: Arc, + test_id: String, ) -> DocTest { + let outdir = Arc::new(if let Some(ref path) = rustdoc_test_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")) + }); + let (crate_attrs, everything_else, crates) = partition_source(&s, edition); let mut supports_color = false; let crate_name = crate_name.map(|c| c.into_owned()); @@ -827,13 +914,12 @@ pub(crate) fn make_test( let mut found_main = None; let mut found_extern_crate = crate_name.is_none(); let mut found_macro = false; - let mut extern_crates = Vec::new(); 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, Vec::new()); + return (found_main, found_extern_crate, found_macro); } }; @@ -849,12 +935,11 @@ pub(crate) fn make_test( if let ast::ItemKind::ExternCrate(original) = item.kind { if !found_extern_crate && let Some(ref crate_name) = crate_name { - match original { - Some(name) => found_extern_crate = name.as_str() == crate_name, - None => found_extern_crate = item.ident.as_str() == crate_name, - } + found_extern_crate = match original { + Some(name) => name.as_str() == crate_name, + None => item.ident.as_str() == crate_name, + }; } - extern_crates.push(item.span); } if !found_macro && let ast::ItemKind::MacCall(..) = item.kind { @@ -883,18 +968,16 @@ pub(crate) fn make_test( // drop. psess.dcx.reset_err_count(); - (found_main, found_extern_crate, found_macro, extern_crates) + (found_main, found_extern_crate, found_macro) }) }); - let Ok((mut main_fn_span, already_has_extern_crate, found_macro, extern_crates)) = result - else { + let Ok((mut main_fn_span, 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 DocTest { test_code: s, supports_color: false, main_fn_span: None, - extern_crates: Vec::new(), crate_attrs, crates, everything_else, @@ -902,6 +985,13 @@ pub(crate) fn make_test( ignore: false, crate_name, name, + lang_string, + line, + file, + failed_ast: true, + rustdoc_test_options, + outdir, + test_id, }; }; @@ -925,7 +1015,6 @@ pub(crate) fn make_test( test_code: s, supports_color, main_fn_span, - extern_crates, crate_attrs, crates, everything_else, @@ -933,6 +1022,13 @@ pub(crate) fn make_test( ignore: false, crate_name, name, + lang_string, + line, + file, + failed_ast: false, + rustdoc_test_options, + outdir, + test_id, } } @@ -1074,56 +1170,54 @@ fn partition_source(s: &str, edition: Edition) -> (String, String, String) { (before, after.trim().to_owned(), crates) } +#[derive(Clone)] 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, - no_run: bool, } 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(), - no_run: options.no_run, + } + } + + 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(), } } } @@ -1152,24 +1246,21 @@ pub(crate) struct DocTestKinds { } impl DocTestKinds { - pub(crate) fn add_test( + pub(crate) fn add_doctest( &mut self, doctest: DocTest, - config: LangString, - target_str: &str, - line: usize, - rustdoc_test_options: IndividualTestOptions, opts: &GlobalTestOptions, edition: Edition, path: PathBuf, unused_externs: Arc>>, ) { - if config.compile_fail || config.test_harness { + if doctest.failed_ast + || doctest.lang_string.compile_fail + || doctest.lang_string.test_harness + || doctest.ignore + || doctest.crate_attrs.contains("#![no_std]") + { self.standalone.push(doctest.generate_test_desc_and_fn( - config, - target_str, - line, - rustdoc_test_options, opts, edition, path, @@ -1180,24 +1271,73 @@ impl DocTestKinds { } } - pub(crate) fn run_tests(self, mut test_args: Vec, nocapture: bool) { + pub(crate) fn run_tests( + self, + mut test_args: Vec, + nocapture: bool, + opts: GlobalTestOptions, + ) { test_args.insert(0, "rustdoctest".to_string()); if nocapture { test_args.push("--nocapture".to_string()); } let Self { mut standalone, others } = self; - standalone.sort_by(|a, b| a.desc.name.as_slice().cmp(&b.desc.name.as_slice())); - test::test_main(&test_args, standalone, None); - // FIXME: generate code for others - // for (edition, doctests) in others { - // let mut id = 0; - // let mut output = String::new(); + for (edition, mut doctests) in others { + doctests.sort_by(|a, b| a.name.cmp(&b.name)); + let mut ids = String::new(); + let mut output = "\ +#![allow(unused_extern_crates)] +#![allow(internal_features)] +#![feature(test)] +#![feature(rustc_attrs)] +#![feature(coverage_attribute)]\n" + .to_string(); + + DocTest::push_attrs(&mut output, &opts, &mut 0); + output.push_str("extern crate test;\n"); - // for doctest in doctests { - // } - // } - drop(others); + let rustdoc_test_options = Arc::clone(&doctests[0].rustdoc_test_options); + let outdir = Arc::clone(&doctests[0].outdir); + + let mut supports_color = true; + for (pos, doctest) in doctests.into_iter().enumerate() { + if !ids.is_empty() { + ids.push(','); + } + ids.push_str(&format!("&{}::TEST", doctest.generate_test_desc(pos, &mut output))); + supports_color &= doctest.supports_color; + } + write!( + output, + "\ +#[rustc_main] +#[coverage(off)] +fn main() {{ + test::test_main_static(&[{ids}]); + panic!(\"fuck\"); +}}", + ) + .unwrap(); + if let Err(TestFailure::CompileError) = run_test( + output, + supports_color, + 0, + 0, + rustdoc_test_options, + true, + outdir, + LangString::empty_for_test(), + edition, + PathBuf::from(format!("doctest_edition_{edition}.rs")), + |_: UnusedExterns| {}, + ) { + // FIXME: run all tests one by one. + } + } + + standalone.sort_by(|a, b| a.desc.name.as_slice().cmp(&b.desc.name.as_slice())); + test::test_main(&test_args, standalone, None); } } @@ -1237,7 +1377,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 { @@ -1251,6 +1391,7 @@ 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: Default::default(), names: Vec::new(), @@ -1265,7 +1406,7 @@ impl Collector { visited_tests: FxHashMap::default(), unused_extern_reports: Default::default(), compiling_test_count: AtomicUsize::new(0), - arg_file, + rustdoc_test_options, } } @@ -1323,7 +1464,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` @@ -1335,8 +1476,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. @@ -1344,22 +1483,25 @@ impl Tester for Collector { }, ); - let rustdoc_test_options = - IndividualTestOptions::new(&self.rustdoc_options, &self.arg_file, test_id); - debug!("creating test {name}: {test}"); - let mut doctest = make_test(test, Some(crate_name), edition, name); - doctest.ignore = ignore_to_bool(&config.ignore, &target_str); - self.tests.add_test( - doctest, + let mut doctest = make_test( + test, + Some(crate_name), + edition, + name, config, - &target_str, line, - rustdoc_test_options, + file, + Arc::clone(&self.rustdoc_test_options), + test_id, + ); + doctest.ignore = ignore_to_bool(&doctest.lang_string.ignore, &target_str); + self.tests.add_doctest( + doctest, &opts, edition, path, - self.unused_extern_reports.clone(), + Arc::clone(&self.unused_extern_reports), ); } diff --git a/src/librustdoc/doctest/tests.rs b/src/librustdoc/doctest/tests.rs index af2d2b45b5eb8..1e8d9a79798bb 100644 --- a/src/librustdoc/doctest/tests.rs +++ b/src/librustdoc/doctest/tests.rs @@ -1,6 +1,21 @@ -use super::{make_test, GlobalTestOptions}; +use super::{DocTest, GlobalTestOptions, IndividualTestOptions}; +use crate::html::markdown::LangString; use rustc_span::edition::DEFAULT_EDITION; -use std::borrow::Cow; +use std::sync::Arc; + +fn make_test(input: String, krate: Option<&str>) -> DocTest { + super::make_test( + input, + krate.map(|k| k.into()), + DEFAULT_EDITION, + String::new(), // test name + LangString::empty_for_test(), + 0, // line + String::new(), // file name + Arc::new(IndividualTestOptions::empty()), + String::new(), // test id + ) +} #[test] fn make_test_basic() { @@ -13,8 +28,7 @@ assert_eq!(2+2, 4); }" .to_string(); let krate = None; - let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) - .generate_unique_doctest(false, &opts, None); + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -29,9 +43,8 @@ fn main() { assert_eq!(2+2, 4); }" .to_string(); - let krate = Some(Cow::Borrowed("asdf")); - let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) - .generate_unique_doctest(false, &opts, None); + let krate = Some("asdf"); + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -51,9 +64,8 @@ use asdf::qwop; assert_eq!(2+2, 4); }" .to_string(); - let krate = Some(Cow::Borrowed("asdf")); - let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) - .generate_unique_doctest(false, &opts, None); + let krate = Some("asdf"); + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 3)); } @@ -72,9 +84,8 @@ use asdf::qwop; assert_eq!(2+2, 4); }" .to_string(); - let krate = Some(Cow::Borrowed("asdf")); - let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) - .generate_unique_doctest(false, &opts, None); + let krate = Some("asdf"); + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -93,9 +104,8 @@ use std::*; assert_eq!(2+2, 4); }" .to_string(); - let krate = Some(Cow::Borrowed("std")); - let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) - .generate_unique_doctest(false, &opts, None); + let krate = Some("std"); + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -115,9 +125,8 @@ use asdf::qwop; assert_eq!(2+2, 4); }" .to_string(); - let krate = Some(Cow::Borrowed("asdf")); - let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) - .generate_unique_doctest(false, &opts, None); + let krate = Some("asdf"); + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -135,9 +144,8 @@ use asdf::qwop; assert_eq!(2+2, 4); }" .to_string(); - let krate = Some(Cow::Borrowed("asdf")); - let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) - .generate_unique_doctest(false, &opts, None); + let krate = Some("asdf"); + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -158,9 +166,9 @@ use asdf::qwop; assert_eq!(2+2, 4); }" .to_string(); - let krate = Some(Cow::Borrowed("asdf")); - let (output, len) = make_test(input.clone(), krate.clone(), DEFAULT_EDITION, String::new()) - .generate_unique_doctest(false, &opts, 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. @@ -174,8 +182,7 @@ use asdf::qwop; assert_eq!(2+2, 4); }" .to_string(); - let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) - .generate_unique_doctest(false, &opts, None); + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 4)); } @@ -194,8 +201,7 @@ assert_eq!(2+2, 4); }" .to_string(); let krate = None; - let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) - .generate_unique_doctest(false, &opts, None); + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -213,8 +219,7 @@ fn main() { }" .to_string(); let krate = None; - let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) - .generate_unique_doctest(false, &opts, None); + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 1)); } @@ -232,8 +237,7 @@ assert_eq!(2+2, 4); }" .to_string(); let krate = None; - let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) - .generate_unique_doctest(false, &opts, None); + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -249,8 +253,7 @@ assert_eq!(2+2, 4);" assert_eq!(2+2, 4);" .to_string(); let krate = None; - let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) - .generate_unique_doctest(true, &opts, None); + let (output, len) = make_test(input, krate).generate_unique_doctest(true, &opts, None); assert_eq!((output, len), (expected, 1)); } @@ -270,8 +273,7 @@ assert_eq!(2+2, 4); .to_string(); let krate = None; - let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) - .generate_unique_doctest(false, &opts, 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; @@ -287,9 +289,8 @@ assert_eq!(asdf::foo, 4); }" .to_string(); - let krate = Some(Cow::Borrowed("asdf")); - let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) - .generate_unique_doctest(false, &opts, None); + let krate = Some("asdf"); + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 3)); } @@ -308,9 +309,8 @@ test_wrapper! { }" .to_string(); - let krate = Some(Cow::Borrowed("my_crate")); - let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) - .generate_unique_doctest(false, &opts, 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)); } @@ -332,8 +332,7 @@ Ok::<(), io:Error>(()) } _inner().unwrap() }" .to_string(); let krate = None; - let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) - .generate_unique_doctest(false, &opts, None); + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -348,8 +347,8 @@ assert_eq!(2+2, 4); } _doctest_main__some_unique_name() }" .to_string(); let krate = None; - let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) - .generate_unique_doctest(false, &opts, Some("_some_unique_name")); + let (output, len) = + make_test(input, krate).generate_unique_doctest(false, &opts, Some("_some_unique_name")); assert_eq!((output, len), (expected, 2)); } @@ -371,8 +370,7 @@ fn main() { }" .to_string(); let krate = None; - let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) - .generate_unique_doctest(false, &opts, None); + let (output, len) = make_test(input, krate).generate_unique_doctest(false, &opts, None); assert_eq!((output, len), (expected, 2)); } @@ -395,7 +393,6 @@ fn main() { }" .to_string(); let krate = None; - let (output, len) = make_test(input, krate, DEFAULT_EDITION, String::new()) - .generate_unique_doctest(false, &opts, 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 25f035470318d..9bc1852960166 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; @@ -305,8 +305,18 @@ impl<'a, I: Iterator>> Iterator for CodeBlocks<'_, 'a, I> { let mut opts: GlobalTestOptions = Default::default(); opts.insert_indent_space = true; - let (test, _) = doctest::make_test(test, krate, edition, String::new()) - .generate_unique_doctest(false, &opts, None); + let (test, _) = doctest::make_test( + test, + krate, + edition, + String::new(), + LangString::empty_for_test(), + 0, + String::new(), + Arc::new(IndividualTestOptions::empty()), + String::new(), + ) + .generate_unique_doctest(false, &opts, None); let channel = if test.contains("#![feature(") { "&version=nightly" } else { "" }; let test_escaped = small_url_encode(test); @@ -872,6 +882,24 @@ pub(crate) struct LangString { 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: true, + compile_fail: false, + error_codes: Vec::new(), + edition: None, + added_classes: Vec::new(), + unknown: Vec::new(), + } + } +} + #[derive(Eq, PartialEq, Clone, Debug)] pub(crate) enum Ignore { All, diff --git a/src/librustdoc/markdown.rs b/src/librustdoc/markdown.rs index ebe0e75c0ccbc..0ef29434f199c 100644 --- a/src/librustdoc/markdown.rs +++ b/src/librustdoc/markdown.rs @@ -164,7 +164,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 +183,6 @@ pub(crate) fn test(options: Options) -> Result<(), String> { false, ); - collector.tests.run_tests(options.test_args, options.nocapture); + collector.tests.run_tests(options.test_args, options.nocapture, opts); Ok(()) } From e0223873c8d49e7d94781c2bf46f652b1910a123 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 15 Apr 2024 11:49:39 +0200 Subject: [PATCH 05/24] Remove `ignore_to_bool` function --- src/librustdoc/doctest.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index e5ee8b6d5b96f..10ee305a0d55c 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -1222,14 +1222,6 @@ impl IndividualTestOptions { } } -fn ignore_to_bool(ignore: &Ignore, target_str: &str) -> bool { - match ignore { - Ignore::All => true, - Ignore::None => false, - Ignore::Some(ref ignores) => ignores.iter().any(|s| target_str.contains(s)), - } -} - pub(crate) trait Tester { fn add_test(&mut self, test: String, config: LangString, line: usize); fn get_line(&self) -> usize { @@ -1495,7 +1487,11 @@ impl Tester for Collector { Arc::clone(&self.rustdoc_test_options), test_id, ); - doctest.ignore = ignore_to_bool(&doctest.lang_string.ignore, &target_str); + doctest.ignore = match doctest.lang_string.ignore { + Ignore::All => true, + Ignore::None => false, + Ignore::Some(ref ignores) => ignores.iter().any(|s| target_str.contains(s)), + }; self.tests.add_doctest( doctest, &opts, From cce04bb3da50159452a29768d8ffd03080c6ebea Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 15 Apr 2024 11:59:56 +0200 Subject: [PATCH 06/24] Set `ignore` value directly into `make_test` --- src/librustdoc/doctest.rs | 19 +++++++++++-------- src/librustdoc/doctest/tests.rs | 1 + src/librustdoc/html/markdown.rs | 1 + 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index 10ee305a0d55c..acd238f617e1d 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -863,6 +863,7 @@ pub(crate) fn make_test( file: String, rustdoc_test_options: Arc, test_id: String, + target_str: &str, ) -> DocTest { let outdir = Arc::new(if let Some(ref path) = rustdoc_test_options.persist_doctests { let mut path = path.clone(); @@ -971,6 +972,12 @@ pub(crate) fn make_test( (found_main, found_extern_crate, found_macro) }) }); + + 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 Ok((mut main_fn_span, 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. @@ -982,7 +989,7 @@ pub(crate) fn make_test( crates, everything_else, already_has_extern_crate: false, - ignore: false, + ignore, crate_name, name, lang_string, @@ -1019,7 +1026,7 @@ pub(crate) fn make_test( crates, everything_else, already_has_extern_crate, - ignore: false, + ignore, crate_name, name, lang_string, @@ -1476,7 +1483,7 @@ impl Tester for Collector { ); debug!("creating test {name}: {test}"); - let mut doctest = make_test( + let doctest = make_test( test, Some(crate_name), edition, @@ -1486,12 +1493,8 @@ impl Tester for Collector { file, Arc::clone(&self.rustdoc_test_options), test_id, + &target_str, ); - doctest.ignore = match doctest.lang_string.ignore { - Ignore::All => true, - Ignore::None => false, - Ignore::Some(ref ignores) => ignores.iter().any(|s| target_str.contains(s)), - }; self.tests.add_doctest( doctest, &opts, diff --git a/src/librustdoc/doctest/tests.rs b/src/librustdoc/doctest/tests.rs index 1e8d9a79798bb..bd236834dc5e6 100644 --- a/src/librustdoc/doctest/tests.rs +++ b/src/librustdoc/doctest/tests.rs @@ -14,6 +14,7 @@ fn make_test(input: String, krate: Option<&str>) -> DocTest { String::new(), // file name Arc::new(IndividualTestOptions::empty()), String::new(), // test id + "", // target_str ) } diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index 9bc1852960166..e6ee2e882fdae 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -315,6 +315,7 @@ impl<'a, I: Iterator>> Iterator for CodeBlocks<'_, 'a, I> { String::new(), Arc::new(IndividualTestOptions::empty()), String::new(), + "", ) .generate_unique_doctest(false, &opts, None); let channel = if test.contains("#![feature(") { "&version=nightly" } else { "" }; From b8f0f2c3e1d7b0ac85f2da49da63463569593ca8 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 15 Apr 2024 12:14:54 +0200 Subject: [PATCH 07/24] Add fallback in case the "all in one" doctests failed to compile --- src/librustdoc/doctest.rs | 29 +++++++++++++++++------------ src/librustdoc/markdown.rs | 4 +++- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index acd238f617e1d..95b7471dbb863 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -220,7 +220,7 @@ pub(crate) fn run( }) })?; - tests.run_tests(test_args, nocapture, opts); + tests.run_tests(test_args, nocapture, opts, &unused_extern_reports); // Collect and warn about unused externs, but only if we've gotten // reports for each doctest @@ -1251,7 +1251,7 @@ impl DocTestKinds { opts: &GlobalTestOptions, edition: Edition, path: PathBuf, - unused_externs: Arc>>, + unused_externs: &Arc>>, ) { if doctest.failed_ast || doctest.lang_string.compile_fail @@ -1263,7 +1263,7 @@ impl DocTestKinds { opts, edition, path, - unused_externs, + Arc::clone(unused_externs), )); } else { self.others.entry(edition).or_default().push(doctest); @@ -1275,6 +1275,7 @@ impl DocTestKinds { mut test_args: Vec, nocapture: bool, opts: GlobalTestOptions, + unused_externs: &Arc>>, ) { test_args.insert(0, "rustdoctest".to_string()); if nocapture { @@ -1300,7 +1301,7 @@ impl DocTestKinds { let outdir = Arc::clone(&doctests[0].outdir); let mut supports_color = true; - for (pos, doctest) in doctests.into_iter().enumerate() { + for (pos, doctest) in doctests.iter().enumerate() { if !ids.is_empty() { ids.push(','); } @@ -1331,7 +1332,17 @@ fn main() {{ PathBuf::from(format!("doctest_edition_{edition}.rs")), |_: UnusedExterns| {}, ) { - // FIXME: run all tests one by one. + // 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 {edition} all at once"); + for (pos, doctest) in doctests.into_iter().enumerate() { + standalone.push(doctest.generate_test_desc_and_fn( + &opts, + edition, + format!("doctest_{edition}_{pos}").into(), + Arc::clone(unused_externs), + )); + } } } @@ -1495,13 +1506,7 @@ impl Tester for Collector { test_id, &target_str, ); - self.tests.add_doctest( - doctest, - &opts, - edition, - path, - Arc::clone(&self.unused_extern_reports), - ); + self.tests.add_doctest(doctest, &opts, edition, path, &self.unused_extern_reports); } fn get_line(&self) -> usize { diff --git a/src/librustdoc/markdown.rs b/src/librustdoc/markdown.rs index 0ef29434f199c..239fb422f75eb 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; @@ -183,6 +184,7 @@ pub(crate) fn test(options: Options) -> Result<(), String> { false, ); - collector.tests.run_tests(options.test_args, options.nocapture, opts); + let unused_externs = Arc::new(Mutex::new(Vec::new())); + collector.tests.run_tests(options.test_args, options.nocapture, opts, &unused_externs); Ok(()) } From 56acb5dc2fb6b1778daff12f451f90096009afd8 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 15 Apr 2024 16:52:37 +0200 Subject: [PATCH 08/24] Create `DocTestInfo` to improve API of `run_test` --- src/librustdoc/doctest.rs | 58 ++++++++++++++++++++------------- src/librustdoc/doctest/tests.rs | 7 ++-- src/librustdoc/html/markdown.rs | 2 ++ 3 files changed, 43 insertions(+), 24 deletions(-) diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index 95b7471dbb863..5e012c6834c37 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -361,17 +361,21 @@ fn wrapped_rustc_command(rustc_wrappers: &[PathBuf], rustc_binary: &Path) -> Com command } +pub(crate) struct DocTestInfo { + line_offset: usize, + line: usize, + path: PathBuf, +} + fn run_test( test: String, supports_color: bool, - line_offset: usize, - line: usize, + test_info: Option, rustdoc_options: Arc, is_multiple_tests: bool, outdir: Arc, mut lang_string: LangString, edition: Edition, - path: PathBuf, report_unused_externs: impl Fn(UnusedExterns), ) -> Result<(), TestFailure> { // Make sure we emit well-formed executable names for our target. @@ -391,8 +395,13 @@ 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 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"); @@ -571,11 +580,14 @@ pub(crate) struct DocTest { 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, } impl DocTest { @@ -693,12 +705,11 @@ impl DocTest { mut self, opts: &GlobalTestOptions, edition: Edition, - path: PathBuf, 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, .. } = self; + let Self { supports_color, rustdoc_test_options, lang_string, outdir, path, .. } = self; TestDescAndFn { desc: test::TestDesc { name: test::DynTestName(std::mem::replace(&mut self.name, String::new())), @@ -722,14 +733,12 @@ impl DocTest { let res = run_test( code, supports_color, - line_offset, - self.line, + Some(DocTestInfo { line_offset, line: self.line, path }), rustdoc_test_options, false, outdir, lang_string, edition, - path, report_unused_externs, ); @@ -844,7 +853,7 @@ pub const TEST: test::TestDescAndFn = test::TestDescAndFn {{ should_panic = if 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.lang_string.no_run { "Ok::<(), String>(())" } else { "self::main()" }, + runner = if self.no_run { "Ok::<(), String>(())" } else { "self::main()" }, ) .unwrap(); test_id @@ -864,6 +873,8 @@ pub(crate) fn make_test( rustdoc_test_options: Arc, test_id: String, target_str: &str, + path: PathBuf, + no_run: bool, ) -> DocTest { let outdir = Arc::new(if let Some(ref path) = rustdoc_test_options.persist_doctests { let mut path = path.clone(); @@ -999,6 +1010,8 @@ pub(crate) fn make_test( rustdoc_test_options, outdir, test_id, + path, + no_run, }; }; @@ -1036,6 +1049,8 @@ pub(crate) fn make_test( rustdoc_test_options, outdir, test_id, + path, + no_run, } } @@ -1250,7 +1265,6 @@ impl DocTestKinds { doctest: DocTest, opts: &GlobalTestOptions, edition: Edition, - path: PathBuf, unused_externs: &Arc>>, ) { if doctest.failed_ast @@ -1262,7 +1276,6 @@ impl DocTestKinds { self.standalone.push(doctest.generate_test_desc_and_fn( opts, edition, - path, Arc::clone(unused_externs), )); } else { @@ -1322,32 +1335,31 @@ fn main() {{ if let Err(TestFailure::CompileError) = run_test( output, supports_color, - 0, - 0, + None, rustdoc_test_options, true, outdir, LangString::empty_for_test(), edition, - PathBuf::from(format!("doctest_edition_{edition}.rs")), |_: UnusedExterns| {}, ) { // 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 {edition} all at once"); - for (pos, doctest) in doctests.into_iter().enumerate() { + for doctest in doctests { standalone.push(doctest.generate_test_desc_and_fn( &opts, edition, - format!("doctest_{edition}_{pos}").into(), Arc::clone(unused_externs), )); } } } - standalone.sort_by(|a, b| a.desc.name.as_slice().cmp(&b.desc.name.as_slice())); - test::test_main(&test_args, standalone, None); + if !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); + } } } @@ -1460,7 +1472,7 @@ impl Tester for Collector { 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 no_run = config.no_run || self.rustdoc_options.no_run; + let no_run = config.no_run || self.rustdoc_options.no_run; if !config.compile_fail { self.compiling_test_count.fetch_add(1, Ordering::SeqCst); } @@ -1505,8 +1517,10 @@ impl Tester for Collector { Arc::clone(&self.rustdoc_test_options), test_id, &target_str, + path, + no_run, ); - self.tests.add_doctest(doctest, &opts, edition, path, &self.unused_extern_reports); + self.tests.add_doctest(doctest, &opts, edition, &self.unused_extern_reports); } fn get_line(&self) -> usize { diff --git a/src/librustdoc/doctest/tests.rs b/src/librustdoc/doctest/tests.rs index bd236834dc5e6..b7b5564044424 100644 --- a/src/librustdoc/doctest/tests.rs +++ b/src/librustdoc/doctest/tests.rs @@ -1,6 +1,7 @@ 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 { @@ -13,8 +14,10 @@ fn make_test(input: String, krate: Option<&str>) -> DocTest { 0, // line String::new(), // file name Arc::new(IndividualTestOptions::empty()), - String::new(), // test id - "", // target_str + String::new(), // test id + "", // target_str + PathBuf::new(), // path + true, // no_run ) } diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index e6ee2e882fdae..b7fce48a68d66 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -316,6 +316,8 @@ impl<'a, I: Iterator>> Iterator for CodeBlocks<'_, 'a, I> { Arc::new(IndividualTestOptions::empty()), String::new(), "", + "doctest.rs".into(), + true, ) .generate_unique_doctest(false, &opts, None); let channel = if test.contains("#![feature(") { "&version=nightly" } else { "" }; From 882d119a0473c08ce4db093cdbda727c241de397 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 15 Apr 2024 17:38:56 +0200 Subject: [PATCH 09/24] Correctly handle test args and move `ignore`d doctests into the one file --- src/librustdoc/doctest.rs | 50 ++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index 5e012c6834c37..1ff1793133f19 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -377,6 +377,7 @@ fn run_test( mut lang_string: LangString, edition: Edition, report_unused_externs: impl Fn(UnusedExterns), + no_run: bool, ) -> Result<(), TestFailure> { // Make sure we emit well-formed executable names for our target. let rust_out = add_exe_suffix("rust_out".to_owned(), &rustdoc_options.target); @@ -413,8 +414,7 @@ fn run_test( compiler.arg("-Z").arg("unstable-options"); } - if lang_string.no_run && !lang_string.compile_fail && rustdoc_options.persist_doctests.is_none() - { + 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 { @@ -454,7 +454,7 @@ fn run_test( 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 = if is_multiple_tests { + let output = if !is_multiple_tests { let status = child.wait().expect("Failed to wait"); process::Output { status, stdout: Vec::new(), stderr: Vec::new() } } else { @@ -510,7 +510,7 @@ fn run_test( } } - if lang_string.no_run { + if no_run { return Ok(()); } @@ -709,7 +709,9 @@ impl DocTest { ) -> 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, .. } = self; + let Self { + supports_color, rustdoc_test_options, lang_string, outdir, path, no_run, .. + } = self; TestDescAndFn { desc: test::TestDesc { name: test::DynTestName(std::mem::replace(&mut self.name, String::new())), @@ -723,7 +725,7 @@ impl DocTest { // compiler failures are test failures should_panic: test::ShouldPanic::No, compile_fail: lang_string.compile_fail, - no_run: lang_string.no_run, + no_run, test_type: test::TestType::DocTest, }, testfn: test::DynTestFn(Box::new(move || { @@ -740,6 +742,7 @@ impl DocTest { lang_string, edition, report_unused_externs, + no_run, ); if let Err(err) = res { @@ -803,7 +806,9 @@ impl DocTest { writeln!(output, "mod {test_id} {{\n{}", self.crates).unwrap(); - if self.main_fn_span.is_some() { + if self.ignore { + // We generate nothing. + } else 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("(())") { @@ -850,10 +855,11 @@ pub const TEST: test::TestDescAndFn = test::TestDescAndFn {{ ignore = self.ignore, file = self.file, line = self.line, - should_panic = if self.lang_string.should_panic { "Yes" } else { "No" }, + 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 { "Ok::<(), String>(())" } else { "self::main()" }, + runner = + if self.no_run || self.ignore { "Ok::<(), String>(())" } else { "self::main()" }, ) .unwrap(); test_id @@ -931,10 +937,11 @@ pub(crate) fn make_test( Ok(p) => p, Err(errs) => { errs.into_iter().for_each(|err| err.cancel()); - return (found_main, found_extern_crate, found_macro); + return (found_main, found_extern_crate, found_macro, true); } }; + let mut has_errors = false; loop { match parser.parse_item(ForceCollect::No) { Ok(Some(item)) => { @@ -965,6 +972,7 @@ pub(crate) fn make_test( Ok(None) => break, Err(e) => { e.cancel(); + has_errors = true; break; } } @@ -974,13 +982,14 @@ pub(crate) fn make_test( parser.maybe_consume_incorrect_semicolon(None); } + has_errors = has_errors || psess.dcx.has_errors_or_delayed_bugs().is_some(); // 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(); - (found_main, found_extern_crate, found_macro) + (found_main, found_extern_crate, found_macro, has_errors) }) }); @@ -989,7 +998,7 @@ pub(crate) fn make_test( Ignore::None => false, Ignore::Some(ref ignores) => ignores.iter().any(|s| target_str.contains(s)), }; - let Ok((mut main_fn_span, already_has_extern_crate, found_macro)) = result else { + let Ok((mut main_fn_span, already_has_extern_crate, found_macro, has_errors)) = 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 DocTest { @@ -1045,7 +1054,7 @@ pub(crate) fn make_test( lang_string, line, file, - failed_ast: false, + failed_ast: has_errors, rustdoc_test_options, outdir, test_id, @@ -1192,7 +1201,6 @@ fn partition_source(s: &str, edition: Edition) -> (String, String, String) { (before, after.trim().to_owned(), crates) } -#[derive(Clone)] pub(crate) struct IndividualTestOptions { test_builder: Option, test_builder_wrappers: Vec, @@ -1270,7 +1278,6 @@ impl DocTestKinds { if doctest.failed_ast || doctest.lang_string.compile_fail || doctest.lang_string.test_harness - || doctest.ignore || doctest.crate_attrs.contains("#![no_std]") { self.standalone.push(doctest.generate_test_desc_and_fn( @@ -1295,6 +1302,7 @@ impl DocTestKinds { test_args.push("--nocapture".to_string()); } let Self { mut standalone, others } = self; + let mut ran_edition_tests = 0; for (edition, mut doctests) in others { doctests.sort_by(|a, b| a.name.cmp(&b.name)); @@ -1318,17 +1326,18 @@ impl DocTestKinds { if !ids.is_empty() { ids.push(','); } - ids.push_str(&format!("&{}::TEST", doctest.generate_test_desc(pos, &mut output))); + ids.push_str(&format!("{}::TEST", doctest.generate_test_desc(pos, &mut output))); supports_color &= doctest.supports_color; } + let test_args = + test_args.iter().map(|arg| format!("{arg:?}.to_string(),")).collect::(); write!( output, "\ #[rustc_main] #[coverage(off)] fn main() {{ - test::test_main_static(&[{ids}]); - panic!(\"fuck\"); + test::test_main(&[{test_args}], vec![{ids}], None); }}", ) .unwrap(); @@ -1342,6 +1351,7 @@ fn main() {{ LangString::empty_for_test(), edition, |_: UnusedExterns| {}, + false, ) { // We failed to compile all compatible tests as one so we push them into the // "standalone" doctests. @@ -1353,10 +1363,12 @@ fn main() {{ Arc::clone(unused_externs), )); } + } else { + ran_edition_tests += 1; } } - if !standalone.is_empty() { + 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); } From 45b075f0dd6b0e09baf9c2f41e19ad0ba15a45a9 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 15 Apr 2024 19:07:36 +0200 Subject: [PATCH 10/24] Fix case where AST failed to parse to give better errors --- src/librustdoc/doctest.rs | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index 1ff1793133f19..ff1ca7be752df 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -454,12 +454,7 @@ fn run_test( 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 = if !is_multiple_tests { - let status = child.wait().expect("Failed to wait"); - process::Output { status, stdout: Vec::new(), stderr: Vec::new() } - } else { - child.wait_with_output().expect("Failed to read stdout") - }; + let output = child.wait_with_output().expect("Failed to read stdout"); struct Bomb<'a>(&'a str); impl Drop for Bomb<'_> { @@ -598,6 +593,11 @@ impl DocTest { // 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(), @@ -841,7 +841,7 @@ pub const TEST: test::TestDescAndFn = test::TestDescAndFn {{ end_line: 0, end_col: 0, compile_fail: false, - no_run: false, + no_run: {no_run}, should_panic: test::ShouldPanic::{should_panic}, test_type: test::TestType::UnitTest, }}, @@ -855,6 +855,7 @@ pub const TEST: test::TestDescAndFn = test::TestDescAndFn {{ 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. @@ -937,11 +938,10 @@ pub(crate) fn make_test( Ok(p) => p, Err(errs) => { errs.into_iter().for_each(|err| err.cancel()); - return (found_main, found_extern_crate, found_macro, true); + return (found_main, found_extern_crate, found_macro); } }; - let mut has_errors = false; loop { match parser.parse_item(ForceCollect::No) { Ok(Some(item)) => { @@ -972,7 +972,6 @@ pub(crate) fn make_test( Ok(None) => break, Err(e) => { e.cancel(); - has_errors = true; break; } } @@ -982,14 +981,13 @@ pub(crate) fn make_test( parser.maybe_consume_incorrect_semicolon(None); } - has_errors = has_errors || psess.dcx.has_errors_or_delayed_bugs().is_some(); // 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(); - (found_main, found_extern_crate, found_macro, has_errors) + (found_main, found_extern_crate, found_macro) }) }); @@ -998,7 +996,7 @@ pub(crate) fn make_test( Ignore::None => false, Ignore::Some(ref ignores) => ignores.iter().any(|s| target_str.contains(s)), }; - let Ok((mut main_fn_span, already_has_extern_crate, found_macro, has_errors)) = result else { + let Ok((mut main_fn_span, 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 DocTest { @@ -1054,7 +1052,7 @@ pub(crate) fn make_test( lang_string, line, file, - failed_ast: has_errors, + failed_ast: false, rustdoc_test_options, outdir, test_id, @@ -1315,6 +1313,10 @@ impl DocTestKinds { #![feature(coverage_attribute)]\n" .to_string(); + for doctest in &doctests { + output.push_str(&doctest.crate_attrs); + } + DocTest::push_attrs(&mut output, &opts, &mut 0); output.push_str("extern crate test;\n"); From cdb59a1f6286ab16b83b23f304da51eb26562b69 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 16 Apr 2024 00:39:42 +0200 Subject: [PATCH 11/24] Correctly handle exit status --- src/librustdoc/doctest.rs | 41 ++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index ff1ca7be752df..96203ee0d1dd9 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -454,7 +454,12 @@ fn run_test( 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"); + let output = if is_multiple_tests { + let status = child.wait().expect("Failed to wait"); + process::Output { status, stdout: Vec::new(), stderr: Vec::new() } + } else { + child.wait_with_output().expect("Failed to read stdout") + }; struct Bomb<'a>(&'a str); impl Drop for Bomb<'_> { @@ -535,17 +540,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 @@ -1271,11 +1276,14 @@ impl DocTestKinds { 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 + || 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( @@ -1301,6 +1309,7 @@ impl DocTestKinds { } let Self { mut standalone, others } = self; let mut ran_edition_tests = 0; + let mut nb_errors = 0; for (edition, mut doctests) in others { doctests.sort_by(|a, b| a.name.cmp(&b.name)); @@ -1343,7 +1352,7 @@ fn main() {{ }}", ) .unwrap(); - if let Err(TestFailure::CompileError) = run_test( + let ret = run_test( output, supports_color, None, @@ -1354,7 +1363,8 @@ fn main() {{ edition, |_: UnusedExterns| {}, false, - ) { + ); + if let Err(TestFailure::CompileError) = ret { // 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 {edition} all at once"); @@ -1367,6 +1377,9 @@ fn main() {{ } } else { ran_edition_tests += 1; + if ret.is_err() { + nb_errors += 1; + } } } @@ -1374,6 +1387,10 @@ fn main() {{ 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); + } } } @@ -1534,7 +1551,13 @@ impl Tester for Collector { path, no_run, ); - self.tests.add_doctest(doctest, &opts, edition, &self.unused_extern_reports); + self.tests.add_doctest( + doctest, + &opts, + edition, + &self.rustdoc_options, + &self.unused_extern_reports, + ); } fn get_line(&self) -> usize { From e6f38c59666b53a1df3f79df0b8f6bc93c0326e2 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 16 Apr 2024 00:39:50 +0200 Subject: [PATCH 12/24] Bless rustdoc ui tests --- tests/rustdoc-ui/doctest/cfg-test.stdout | 2 +- .../rustdoc-ui/doctest/doctest-output.stdout | 2 +- .../failed-doctest-should-panic.stdout | 5 +++-- tests/rustdoc-ui/doctest/no-run-flag.stdout | 19 ++++++++++++------- tests/rustdoc-ui/doctest/test-type.stdout | 15 ++++++++++----- 5 files changed, 27 insertions(+), 16 deletions(-) diff --git a/tests/rustdoc-ui/doctest/cfg-test.stdout b/tests/rustdoc-ui/doctest/cfg-test.stdout index 2960ff8d3b473..43da2419ff79a 100644 --- a/tests/rustdoc-ui/doctest/cfg-test.stdout +++ b/tests/rustdoc-ui/doctest/cfg-test.stdout @@ -1,7 +1,7 @@ running 2 tests -test $DIR/cfg-test.rs - Bar (line 27) ... ok test $DIR/cfg-test.rs - Foo (line 19) ... ok +test $DIR/cfg-test.rs - Bar (line 27) ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME diff --git a/tests/rustdoc-ui/doctest/doctest-output.stdout b/tests/rustdoc-ui/doctest/doctest-output.stdout index 35b0e366fb5cc..adcafc547e03f 100644 --- a/tests/rustdoc-ui/doctest/doctest-output.stdout +++ b/tests/rustdoc-ui/doctest/doctest-output.stdout @@ -1,8 +1,8 @@ running 3 tests test $DIR/doctest-output.rs - (line 8) ... ok -test $DIR/doctest-output.rs - ExpandedStruct (line 24) ... ok test $DIR/doctest-output.rs - foo::bar (line 18) ... ok +test $DIR/doctest-output.rs - ExpandedStruct (line 24) ... ok test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME 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.stdout b/tests/rustdoc-ui/doctest/no-run-flag.stdout index 02f28aaf60da0..538d3f7b6819e 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 -test $DIR/no-run-flag.rs - f (line 11) - compile ... ok +running 6 tests 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 11) ... ok +test $DIR/no-run-flag.rs - f (line 17) ... ok +test $DIR/no-run-flag.rs - f (line 28) ... ok +test $DIR/no-run-flag.rs - f (line 32) ... ok +test $DIR/no-run-flag.rs - f (line 8) ... ok + +test result: ok. 5 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in $TIME + + +running 1 test 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. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME diff --git a/tests/rustdoc-ui/doctest/test-type.stdout b/tests/rustdoc-ui/doctest/test-type.stdout index a66fd240d34c4..8109b09412c31 100644 --- a/tests/rustdoc-ui/doctest/test-type.stdout +++ b/tests/rustdoc-ui/doctest/test-type.stdout @@ -1,10 +1,15 @@ -running 5 tests +running 4 tests test $DIR/test-type.rs - f (line 12) ... ignored -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 15) ... 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; 1 ignored; 0 measured; 0 filtered out; finished in $TIME + + +running 1 test +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; 0 ignored; 0 measured; 0 filtered out; finished in $TIME From 2c27d9c010830893d144ea529fa96a95d85b517c Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 16 Apr 2024 21:54:47 +0200 Subject: [PATCH 13/24] Run doctests by batches instead of running them all at once when there are too many --- src/librustdoc/doctest.rs | 235 ++++++++++++++++++++++++++++---------- 1 file changed, 173 insertions(+), 62 deletions(-) diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index 96203ee0d1dd9..59da294cdb746 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -1263,6 +1263,166 @@ pub(crate) trait Tester { fn register_header(&mut self, _name: &str, _level: u32) {} } +/// If there are too many doctests than can be compiled at once, we need to limit the size +/// of the generated test to prevent everything to break down. +/// +/// We add all doctests one by one through [`DocTestRunner::add_test`] and when it reaches +/// `TEST_BATCH_SIZE` size or is dropped, it runs all stored doctests at once. +struct DocTestRunner<'a> { + nb_errors: &'a mut usize, + ran_edition_tests: &'a mut usize, + standalone: &'a mut Vec, + crate_attrs: FxHashSet, + edition: Edition, + ids: String, + output: String, + supports_color: bool, + rustdoc_test_options: &'a Arc, + outdir: &'a Arc, + nb_tests: usize, + doctests: Vec, + opts: &'a GlobalTestOptions, + test_args: &'a [String], + unused_externs: &'a Arc>>, +} + +impl<'a> DocTestRunner<'a> { + const TEST_BATCH_SIZE: usize = 250; + + fn new( + nb_errors: &'a mut usize, + ran_edition_tests: &'a mut usize, + standalone: &'a mut Vec, + edition: Edition, + rustdoc_test_options: &'a Arc, + outdir: &'a Arc, + opts: &'a GlobalTestOptions, + test_args: &'a [String], + unused_externs: &'a Arc>>, + ) -> Self { + Self { + nb_errors, + ran_edition_tests, + standalone, + edition, + crate_attrs: FxHashSet::default(), + ids: String::new(), + output: String::new(), + supports_color: true, + rustdoc_test_options, + outdir, + nb_tests: 0, + doctests: Vec::with_capacity(Self::TEST_BATCH_SIZE), + opts, + test_args, + unused_externs, + } + } + + fn add_test(&mut self, doctest: DocTest) { + 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); + + if self.nb_tests >= Self::TEST_BATCH_SIZE { + self.run_tests(); + } + } + + fn run_tests(&mut self) { + if self.nb_tests == 0 { + return; + } + 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, &self.opts, &mut 0); + code.push_str("extern crate test;\n"); + + let test_args = + self.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 ret = run_test( + code, + self.supports_color, + None, + Arc::clone(self.rustdoc_test_options), + true, + Arc::clone(self.outdir), + LangString::empty_for_test(), + self.edition, + |_: UnusedExterns| {}, + false, + ); + if let Err(TestFailure::CompileError) = ret { + // 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", + self.edition + ); + for doctest in self.doctests.drain(..) { + self.standalone.push(doctest.generate_test_desc_and_fn( + &self.opts, + self.edition, + Arc::clone(self.unused_externs), + )); + } + } else { + *self.ran_edition_tests += 1; + if ret.is_err() { + *self.nb_errors += 1; + } + } + + // We reset values. + self.supports_color = true; + self.ids.clear(); + self.output.clear(); + self.crate_attrs.clear(); + self.nb_tests = 0; + self.doctests.clear(); + } +} + +impl<'a> Drop for DocTestRunner<'a> { + fn drop(&mut self) { + self.run_tests(); + } +} + #[derive(Default)] pub(crate) struct DocTestKinds { /// Tests that cannot be run together with the rest (`compile_fail` and `test_harness`). @@ -1313,73 +1473,24 @@ impl DocTestKinds { for (edition, mut doctests) in others { doctests.sort_by(|a, b| a.name.cmp(&b.name)); - let mut ids = String::new(); - let mut output = "\ -#![allow(unused_extern_crates)] -#![allow(internal_features)] -#![feature(test)] -#![feature(rustc_attrs)] -#![feature(coverage_attribute)]\n" - .to_string(); - - for doctest in &doctests { - output.push_str(&doctest.crate_attrs); - } - - DocTest::push_attrs(&mut output, &opts, &mut 0); - output.push_str("extern crate test;\n"); - let rustdoc_test_options = Arc::clone(&doctests[0].rustdoc_test_options); let outdir = Arc::clone(&doctests[0].outdir); - let mut supports_color = true; - for (pos, doctest) in doctests.iter().enumerate() { - if !ids.is_empty() { - ids.push(','); - } - ids.push_str(&format!("{}::TEST", doctest.generate_test_desc(pos, &mut output))); - supports_color &= doctest.supports_color; - } - let test_args = - test_args.iter().map(|arg| format!("{arg:?}.to_string(),")).collect::(); - write!( - output, - "\ -#[rustc_main] -#[coverage(off)] -fn main() {{ - test::test_main(&[{test_args}], vec![{ids}], None); -}}", - ) - .unwrap(); - let ret = run_test( - output, - supports_color, - None, - rustdoc_test_options, - true, - outdir, - LangString::empty_for_test(), + // When `DocTestRunner` is dropped, it'll run all pending doctests it didn't already + // run, so no need to worry about it. + let mut tests_runner = DocTestRunner::new( + &mut nb_errors, + &mut ran_edition_tests, + &mut standalone, edition, - |_: UnusedExterns| {}, - false, + &rustdoc_test_options, + &outdir, + &opts, + &test_args, + unused_externs, ); - if let Err(TestFailure::CompileError) = ret { - // 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 {edition} all at once"); - for doctest in doctests { - standalone.push(doctest.generate_test_desc_and_fn( - &opts, - edition, - Arc::clone(unused_externs), - )); - } - } else { - ran_edition_tests += 1; - if ret.is_err() { - nb_errors += 1; - } + for doctest in doctests { + tests_runner.add_test(doctest); } } From 76f70fa11becd1363d17f1771f2afd00e7074dae Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 16 Apr 2024 22:35:03 +0200 Subject: [PATCH 14/24] Don't set `test_harness` to `true` by default as it would generate invalid code for combined doctests --- src/librustdoc/html/markdown.rs | 2 +- tests/rustdoc-ui/doctest/cfg-test.stdout | 2 +- tests/rustdoc-ui/doctest/doctest-output.stdout | 2 +- tests/rustdoc-ui/doctest/no-run-flag.stdout | 10 +++++----- tests/rustdoc-ui/doctest/test-type.stdout | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index b7fce48a68d66..f4018ed0591f0 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -893,7 +893,7 @@ impl LangString { no_run: false, ignore: Ignore::None, rust: true, - test_harness: true, + test_harness: false, compile_fail: false, error_codes: Vec::new(), edition: None, diff --git a/tests/rustdoc-ui/doctest/cfg-test.stdout b/tests/rustdoc-ui/doctest/cfg-test.stdout index 43da2419ff79a..2960ff8d3b473 100644 --- a/tests/rustdoc-ui/doctest/cfg-test.stdout +++ b/tests/rustdoc-ui/doctest/cfg-test.stdout @@ -1,7 +1,7 @@ running 2 tests -test $DIR/cfg-test.rs - Foo (line 19) ... ok test $DIR/cfg-test.rs - Bar (line 27) ... ok +test $DIR/cfg-test.rs - Foo (line 19) ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME diff --git a/tests/rustdoc-ui/doctest/doctest-output.stdout b/tests/rustdoc-ui/doctest/doctest-output.stdout index adcafc547e03f..35b0e366fb5cc 100644 --- a/tests/rustdoc-ui/doctest/doctest-output.stdout +++ b/tests/rustdoc-ui/doctest/doctest-output.stdout @@ -1,8 +1,8 @@ running 3 tests test $DIR/doctest-output.rs - (line 8) ... ok -test $DIR/doctest-output.rs - foo::bar (line 18) ... ok test $DIR/doctest-output.rs - ExpandedStruct (line 24) ... ok +test $DIR/doctest-output.rs - foo::bar (line 18) ... ok test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME diff --git a/tests/rustdoc-ui/doctest/no-run-flag.stdout b/tests/rustdoc-ui/doctest/no-run-flag.stdout index 538d3f7b6819e..1860146661b99 100644 --- a/tests/rustdoc-ui/doctest/no-run-flag.stdout +++ b/tests/rustdoc-ui/doctest/no-run-flag.stdout @@ -1,11 +1,11 @@ running 6 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 11) ... ok -test $DIR/no-run-flag.rs - f (line 17) ... ok -test $DIR/no-run-flag.rs - f (line 28) ... ok -test $DIR/no-run-flag.rs - f (line 32) ... ok -test $DIR/no-run-flag.rs - f (line 8) ... ok +test $DIR/no-run-flag.rs - f (line 17) - compile ... 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. 5 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in $TIME diff --git a/tests/rustdoc-ui/doctest/test-type.stdout b/tests/rustdoc-ui/doctest/test-type.stdout index 8109b09412c31..34d8e0c60e0c1 100644 --- a/tests/rustdoc-ui/doctest/test-type.stdout +++ b/tests/rustdoc-ui/doctest/test-type.stdout @@ -1,7 +1,7 @@ running 4 tests test $DIR/test-type.rs - f (line 12) ... ignored -test $DIR/test-type.rs - f (line 15) ... ok +test $DIR/test-type.rs - f (line 15) - compile ... ok test $DIR/test-type.rs - f (line 6) ... ok test $DIR/test-type.rs - f (line 9) - should panic ... ok From f235cbf9ce0b3904ed765435611b1b65946100dd Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Wed, 17 Apr 2024 00:20:45 +0200 Subject: [PATCH 15/24] Ignore test using link which creates segfault --- .../unstable-book/src/language-features/link-arg-attribute.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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")] From b5ca844ad3c9a386b47f83715ec0dbb72f3a65e4 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Wed, 17 Apr 2024 16:03:55 +0200 Subject: [PATCH 16/24] Improve handling of `--persist-doctests` with combined doctests and update associated test to check both combined and non-combined doctests --- src/librustdoc/doctest.rs | 48 ++++++++++++++----- .../run-make/doctests-keep-binaries/Makefile | 33 +++++++++++++ .../run-make/doctests-keep-binaries/rmake.rs | 10 +++- tests/run-make/doctests-keep-binaries/t.rs | 8 ++++ 4 files changed, 86 insertions(+), 13 deletions(-) create mode 100644 tests/run-make/doctests-keep-binaries/Makefile diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index 59da294cdb746..69f313d4a1879 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -313,6 +313,7 @@ enum TestFailure { UnexpectedRunPass, } +#[derive(Debug)] enum DirState { Temp(tempfile::TempDir), Perm(PathBuf), @@ -367,21 +368,41 @@ pub(crate) struct DocTestInfo { path: PathBuf, } +fn build_test_dir(outdir: &Arc, is_multiple_tests: bool) -> 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 out_dir = if is_multiple_tests && is_perm_dir { + // If this a "multiple tests" case and we generate it into a non temporary directory, we + // want to put it into the parent instead. + out_dir.parent().unwrap_or(out_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() +} + fn run_test( test: String, supports_color: bool, test_info: Option, rustdoc_options: Arc, is_multiple_tests: bool, - outdir: Arc, mut lang_string: LangString, edition: Edition, report_unused_externs: impl Fn(UnusedExterns), no_run: bool, + // Used to prevent overwriting a binary in case `--persist-doctests` is used. + binary_extra: Option<&str>, + out_dir: PathBuf, ) -> Result<(), TestFailure> { - // 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 = outdir.path().join(rust_out); + 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 @@ -717,6 +738,7 @@ impl DocTest { let Self { supports_color, rustdoc_test_options, lang_string, outdir, path, no_run, .. } = self; + let out_dir = build_test_dir(&outdir, false); TestDescAndFn { desc: test::TestDesc { name: test::DynTestName(std::mem::replace(&mut self.name, String::new())), @@ -743,12 +765,18 @@ impl DocTest { Some(DocTestInfo { line_offset, line: self.line, path }), rustdoc_test_options, false, - outdir, lang_string, edition, report_unused_externs, no_run, + None, + out_dir, ); + // 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); if let Err(err) = res { match err { @@ -892,11 +920,6 @@ pub(crate) fn make_test( 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")) @@ -1374,17 +1397,20 @@ test::test_main(&[{test_args}], vec![{ids}], None); ids = self.ids, ) .expect("failed to generate test code"); + let out_dir = build_test_dir(self.outdir, true); let ret = run_test( code, self.supports_color, None, Arc::clone(self.rustdoc_test_options), true, - Arc::clone(self.outdir), LangString::empty_for_test(), self.edition, |_: UnusedExterns| {}, false, + // To prevent writing over an existing doctest + Some(&format!("_{}_{}", self.edition, *self.ran_edition_tests)), + out_dir, ); if let Err(TestFailure::CompileError) = ret { // We failed to compile all compatible tests as one so we push them into the diff --git a/tests/run-make/doctests-keep-binaries/Makefile b/tests/run-make/doctests-keep-binaries/Makefile new file mode 100644 index 0000000000000..e346ab7b79020 --- /dev/null +++ b/tests/run-make/doctests-keep-binaries/Makefile @@ -0,0 +1,33 @@ +# ignore-cross-compile +include ../tools.mk + +# Check that valid binaries are persisted by running them, regardless of whether the --run or --no-run option is used. + +MY_SRC_DIR := ${CURDIR} + +all: run no_run test_run_directory + +run: + mkdir -p $(TMPDIR)/doctests + $(RUSTC) --crate-type rlib t.rs + $(RUSTDOC) -Zunstable-options --test --persist-doctests $(TMPDIR)/doctests --extern t=$(TMPDIR)/libt.rlib t.rs + $(TMPDIR)/doctests/t_rs_12_0/rust_out + $(TMPDIR)/doctests/rust_out_2015_0 + rm -rf $(TMPDIR)/doctests + +no_run: + mkdir -p $(TMPDIR)/doctests + $(RUSTC) --crate-type rlib t.rs + $(RUSTDOC) -Zunstable-options --test --persist-doctests $(TMPDIR)/doctests --extern t=$(TMPDIR)/libt.rlib t.rs --no-run + $(TMPDIR)/doctests/t_rs_12_0/rust_out + $(TMPDIR)/doctests/rust_out_2015_0 + rm -rf $(TMPDIR)/doctests + +# Behavior with --test-run-directory with relative paths. +test_run_directory: + mkdir -p $(TMPDIR)/doctests + mkdir -p $(TMPDIR)/rundir + $(RUSTC) --crate-type rlib t.rs + ( cd $(TMPDIR); \ + $(RUSTDOC) -Zunstable-options --test --persist-doctests doctests --test-run-directory rundir --extern t=libt.rlib $(MY_SRC_DIR)/t.rs ) + rm -rf $(TMPDIR)/doctests $(TMPDIR)/rundir 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() {} From e4b74f425be2a596c38be230b8789dd75f0684fa Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 29 Apr 2024 17:44:25 +0200 Subject: [PATCH 17/24] Run merged doctests as one instead of using batches --- library/std/src/keyword_docs.rs | 2 +- src/librustdoc/doctest.rs | 224 +++++++++++++++----------------- 2 files changed, 103 insertions(+), 123 deletions(-) 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/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index 69f313d4a1879..5965222542368 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -417,6 +417,10 @@ fn run_test( } compiler.arg("--edition").arg(&edition.to_string()); + 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( @@ -464,21 +468,30 @@ 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).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 stdin = child.stdin.as_mut().expect("Failed to open stdin"); - stdin.write_all(test.as_bytes()).expect("could write out test sources"); - } 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"); child.wait_with_output().expect("Failed to read stdout") }; @@ -837,27 +850,29 @@ impl DocTest { fn generate_test_desc(&self, id: usize, output: &mut String) -> String { let test_id = format!("__doctest_{id}"); - writeln!(output, "mod {test_id} {{\n{}", self.crates).unwrap(); - if self.ignore { - // We generate nothing. - } else if self.main_fn_span.is_some() { - output.push_str(&self.everything_else); + // We generate nothing else. + writeln!(output, "mod {test_id} {{\n").unwrap(); } else { - let returns_result = if self.everything_else.trim_end().ends_with("(())") { - "-> Result<(), impl core::fmt::Debug>" + writeln!(output, "mod {test_id} {{\n{}", self.crates).unwrap(); + if self.main_fn_span.is_some() { + output.push_str(&self.everything_else); } else { - "" - }; - write!( - output, - "\ -fn main() {returns_result} {{ - {} -}}", - self.everything_else - ) - .unwrap(); + 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, @@ -925,6 +940,9 @@ pub(crate) fn make_test( 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(&s, edition); let mut supports_color = false; let crate_name = crate_name.map(|c| c.into_owned()); @@ -1286,65 +1304,33 @@ pub(crate) trait Tester { fn register_header(&mut self, _name: &str, _level: u32) {} } -/// If there are too many doctests than can be compiled at once, we need to limit the size -/// of the generated test to prevent everything to break down. -/// -/// We add all doctests one by one through [`DocTestRunner::add_test`] and when it reaches -/// `TEST_BATCH_SIZE` size or is dropped, it runs all stored doctests at once. -struct DocTestRunner<'a> { - nb_errors: &'a mut usize, - ran_edition_tests: &'a mut usize, - standalone: &'a mut Vec, +/// Convenient type to merge compatible doctests into one. +struct DocTestRunner { crate_attrs: FxHashSet, - edition: Edition, ids: String, output: String, supports_color: bool, - rustdoc_test_options: &'a Arc, - outdir: &'a Arc, nb_tests: usize, doctests: Vec, - opts: &'a GlobalTestOptions, - test_args: &'a [String], - unused_externs: &'a Arc>>, } -impl<'a> DocTestRunner<'a> { - const TEST_BATCH_SIZE: usize = 250; - - fn new( - nb_errors: &'a mut usize, - ran_edition_tests: &'a mut usize, - standalone: &'a mut Vec, - edition: Edition, - rustdoc_test_options: &'a Arc, - outdir: &'a Arc, - opts: &'a GlobalTestOptions, - test_args: &'a [String], - unused_externs: &'a Arc>>, - ) -> Self { +impl DocTestRunner { + fn new() -> Self { Self { - nb_errors, - ran_edition_tests, - standalone, - edition, crate_attrs: FxHashSet::default(), ids: String::new(), output: String::new(), supports_color: true, - rustdoc_test_options, - outdir, nb_tests: 0, - doctests: Vec::with_capacity(Self::TEST_BATCH_SIZE), - opts, - test_args, - unused_externs, + doctests: Vec::with_capacity(10), } } fn add_test(&mut self, doctest: DocTest) { - for line in doctest.crate_attrs.split('\n') { - self.crate_attrs.insert(line.to_string()); + 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(','); @@ -1356,16 +1342,16 @@ impl<'a> DocTestRunner<'a> { self.supports_color &= doctest.supports_color; self.nb_tests += 1; self.doctests.push(doctest); - - if self.nb_tests >= Self::TEST_BATCH_SIZE { - self.run_tests(); - } } - fn run_tests(&mut self) { - if self.nb_tests == 0 { - return; - } + 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)] @@ -1379,11 +1365,11 @@ impl<'a> DocTestRunner<'a> { code.push('\n'); } - DocTest::push_attrs(&mut code, &self.opts, &mut 0); + DocTest::push_attrs(&mut code, opts, &mut 0); code.push_str("extern crate test;\n"); let test_args = - self.test_args.iter().map(|arg| format!("{arg:?}.to_string(),")).collect::(); + test_args.iter().map(|arg| format!("{arg:?}.to_string(),")).collect::(); write!( code, "\ @@ -1397,55 +1383,26 @@ test::test_main(&[{test_args}], vec![{ids}], None); ids = self.ids, ) .expect("failed to generate test code"); - let out_dir = build_test_dir(self.outdir, true); + let out_dir = build_test_dir(outdir, true); let ret = run_test( code, self.supports_color, None, - Arc::clone(self.rustdoc_test_options), + rustdoc_test_options, true, LangString::empty_for_test(), - self.edition, + edition, |_: UnusedExterns| {}, false, // To prevent writing over an existing doctest - Some(&format!("_{}_{}", self.edition, *self.ran_edition_tests)), + Some(&format!("_{}", edition)), out_dir, ); if let Err(TestFailure::CompileError) = ret { - // 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", - self.edition - ); - for doctest in self.doctests.drain(..) { - self.standalone.push(doctest.generate_test_desc_and_fn( - &self.opts, - self.edition, - Arc::clone(self.unused_externs), - )); - } + Err(()) } else { - *self.ran_edition_tests += 1; - if ret.is_err() { - *self.nb_errors += 1; - } + Ok(ret.is_ok()) } - - // We reset values. - self.supports_color = true; - self.ids.clear(); - self.output.clear(); - self.crate_attrs.clear(); - self.nb_tests = 0; - self.doctests.clear(); - } -} - -impl<'a> Drop for DocTestRunner<'a> { - fn drop(&mut self) { - self.run_tests(); } } @@ -1498,25 +1455,48 @@ impl DocTestKinds { let mut nb_errors = 0; for (edition, mut doctests) in others { + if doctests.is_empty() { + continue; + } doctests.sort_by(|a, b| a.name.cmp(&b.name)); - let rustdoc_test_options = Arc::clone(&doctests[0].rustdoc_test_options); let outdir = Arc::clone(&doctests[0].outdir); // When `DocTestRunner` is dropped, it'll run all pending doctests it didn't already // run, so no need to worry about it. - let mut tests_runner = DocTestRunner::new( - &mut nb_errors, - &mut ran_edition_tests, - &mut standalone, + 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); + } + match tests_runner.run_tests( + rustdoc_test_options, edition, - &rustdoc_test_options, - &outdir, &opts, &test_args, - unused_externs, - ); - for doctest in doctests { - tests_runner.add_test(doctest); + &outdir, + ) { + Ok(success) => { + ran_edition_tests += 1; + if !success { + nb_errors += 1; + } + } + Err(()) => { + // 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 + ); + for doctest in tests_runner.doctests.drain(..) { + standalone.push(doctest.generate_test_desc_and_fn( + &opts, + edition, + Arc::clone(unused_externs), + )); + } + } } } From 7ea8909cd2c48114038cbb7bb5ce6ac1e0373e7f Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 29 Apr 2024 23:49:44 +0200 Subject: [PATCH 18/24] Allow to merge doctests only starting the 2024 edition --- src/librustdoc/doctest.rs | 88 ++++++++++++------- src/librustdoc/markdown.rs | 2 +- .../run-make/doctests-keep-binaries/Makefile | 10 +-- .../doctest/failed-doctest-should-panic.rs | 2 +- tests/rustdoc-ui/doctest/no-run-flag.rs | 2 +- tests/rustdoc-ui/doctest/test-type.rs | 2 +- 6 files changed, 64 insertions(+), 42 deletions(-) diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index 5965222542368..50cdb523db26f 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -161,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; @@ -220,7 +221,13 @@ pub(crate) fn run( }) })?; - tests.run_tests(test_args, nocapture, opts, &unused_extern_reports); + 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 @@ -473,7 +480,7 @@ fn run_test( if std::fs::write(&out_source, &test).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) + return Err(TestFailure::CompileError); } compiler.arg(out_source); compiler.stderr(Stdio::null()); @@ -1398,11 +1405,23 @@ test::test_main(&[{test_args}], vec![{ids}], None); Some(&format!("_{}", edition)), out_dir, ); - if let Err(TestFailure::CompileError) = ret { - Err(()) - } else { - Ok(ret.is_ok()) - } + 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), + )); } } @@ -1444,6 +1463,7 @@ impl DocTestKinds { mut test_args: Vec, nocapture: bool, opts: GlobalTestOptions, + can_merge_doctests: bool, unused_externs: &Arc>>, ) { test_args.insert(0, "rustdoctest".to_string()); @@ -1458,45 +1478,47 @@ impl DocTestKinds { if doctests.is_empty() { continue; } - doctests.sort_by(|a, b| a.name.cmp(&b.name)); - let outdir = Arc::clone(&doctests[0].outdir); + if can_merge_doctests { + doctests.sort_by(|a, b| a.name.cmp(&b.name)); + let outdir = Arc::clone(&doctests[0].outdir); - // When `DocTestRunner` is dropped, it'll run all pending doctests it didn't already - // run, so no need to worry about it. - let mut tests_runner = DocTestRunner::new(); - let rustdoc_test_options = Arc::clone(&doctests[0].rustdoc_test_options); + // When `DocTestRunner` is dropped, it'll run all pending doctests it didn't already + // run, so no need to worry about it. + 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); - } - match tests_runner.run_tests( - rustdoc_test_options, - edition, - &opts, - &test_args, - &outdir, - ) { - Ok(success) => { + 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; } - } - Err(()) => { + 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 ); - for doctest in tests_runner.doctests.drain(..) { - standalone.push(doctest.generate_test_desc_and_fn( - &opts, - edition, - Arc::clone(unused_externs), - )); - } + add_standalone_tests( + &mut standalone, + tests_runner.doctests, + &opts, + edition, + unused_externs, + ); } + } else { + add_standalone_tests(&mut standalone, doctests, &opts, edition, unused_externs); } } diff --git a/src/librustdoc/markdown.rs b/src/librustdoc/markdown.rs index 239fb422f75eb..f49915a76db2a 100644 --- a/src/librustdoc/markdown.rs +++ b/src/librustdoc/markdown.rs @@ -185,6 +185,6 @@ pub(crate) fn test(options: Options) -> Result<(), String> { ); let unused_externs = Arc::new(Mutex::new(Vec::new())); - collector.tests.run_tests(options.test_args, options.nocapture, opts, &unused_externs); + collector.tests.run_tests(options.test_args, options.nocapture, opts, false, &unused_externs); Ok(()) } diff --git a/tests/run-make/doctests-keep-binaries/Makefile b/tests/run-make/doctests-keep-binaries/Makefile index e346ab7b79020..f71ce2d4faabe 100644 --- a/tests/run-make/doctests-keep-binaries/Makefile +++ b/tests/run-make/doctests-keep-binaries/Makefile @@ -10,17 +10,17 @@ all: run no_run test_run_directory run: mkdir -p $(TMPDIR)/doctests $(RUSTC) --crate-type rlib t.rs - $(RUSTDOC) -Zunstable-options --test --persist-doctests $(TMPDIR)/doctests --extern t=$(TMPDIR)/libt.rlib t.rs + $(RUSTDOC) -Zunstable-options --test --persist-doctests $(TMPDIR)/doctests --extern t=$(TMPDIR)/libt.rlib t.rs --edition 2024 $(TMPDIR)/doctests/t_rs_12_0/rust_out - $(TMPDIR)/doctests/rust_out_2015_0 + $(TMPDIR)/doctests/rust_out_2024 rm -rf $(TMPDIR)/doctests no_run: mkdir -p $(TMPDIR)/doctests $(RUSTC) --crate-type rlib t.rs - $(RUSTDOC) -Zunstable-options --test --persist-doctests $(TMPDIR)/doctests --extern t=$(TMPDIR)/libt.rlib t.rs --no-run + $(RUSTDOC) -Zunstable-options --test --persist-doctests $(TMPDIR)/doctests --extern t=$(TMPDIR)/libt.rlib t.rs --no-run --edition 2024 $(TMPDIR)/doctests/t_rs_12_0/rust_out - $(TMPDIR)/doctests/rust_out_2015_0 + $(TMPDIR)/doctests/rust_out_2024 rm -rf $(TMPDIR)/doctests # Behavior with --test-run-directory with relative paths. @@ -29,5 +29,5 @@ test_run_directory: mkdir -p $(TMPDIR)/rundir $(RUSTC) --crate-type rlib t.rs ( cd $(TMPDIR); \ - $(RUSTDOC) -Zunstable-options --test --persist-doctests doctests --test-run-directory rundir --extern t=libt.rlib $(MY_SRC_DIR)/t.rs ) + $(RUSTDOC) -Zunstable-options --test --persist-doctests doctests --test-run-directory rundir --extern t=libt.rlib $(MY_SRC_DIR)/t.rs --edition 2024 ) rm -rf $(TMPDIR)/doctests $(TMPDIR)/rundir 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/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/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" From 5a3d239afb77912c1efe123fa5922510ed4cae3e Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 30 Apr 2024 17:12:08 +0200 Subject: [PATCH 19/24] Add standalone doctest attribute --- src/librustdoc/doctest.rs | 1 + src/librustdoc/html/markdown.rs | 7 +++ .../run-make/doctests-keep-binaries/Makefile | 33 ------------- .../doctests-merge/doctest-failure.stdout | 26 ++++++++++ .../doctests-merge/doctest-standalone.rs | 18 +++++++ .../doctests-merge/doctest-standalone.stdout | 7 +++ .../doctests-merge/doctest-success.stdout | 7 +++ tests/run-make/doctests-merge/doctest.rs | 18 +++++++ tests/run-make/doctests-merge/rmake.rs | 47 +++++++++++++++++++ 9 files changed, 131 insertions(+), 33 deletions(-) delete mode 100644 tests/run-make/doctests-keep-binaries/Makefile create mode 100644 tests/run-make/doctests-merge/doctest-failure.stdout create mode 100644 tests/run-make/doctests-merge/doctest-standalone.rs create mode 100644 tests/run-make/doctests-merge/doctest-standalone.stdout create mode 100644 tests/run-make/doctests-merge/doctest-success.stdout create mode 100644 tests/run-make/doctests-merge/doctest.rs create mode 100644 tests/run-make/doctests-merge/rmake.rs diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index 50cdb523db26f..8a153494d8b93 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -1444,6 +1444,7 @@ impl DocTestKinds { 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]") diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index f4018ed0591f0..d348e7312034c 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -879,6 +879,7 @@ 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, @@ -895,6 +896,7 @@ impl LangString { rust: true, test_harness: false, compile_fail: false, + standalone: false, error_codes: Vec::new(), edition: None, added_classes: Vec::new(), @@ -1231,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(), @@ -1312,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/tests/run-make/doctests-keep-binaries/Makefile b/tests/run-make/doctests-keep-binaries/Makefile deleted file mode 100644 index f71ce2d4faabe..0000000000000 --- a/tests/run-make/doctests-keep-binaries/Makefile +++ /dev/null @@ -1,33 +0,0 @@ -# ignore-cross-compile -include ../tools.mk - -# Check that valid binaries are persisted by running them, regardless of whether the --run or --no-run option is used. - -MY_SRC_DIR := ${CURDIR} - -all: run no_run test_run_directory - -run: - mkdir -p $(TMPDIR)/doctests - $(RUSTC) --crate-type rlib t.rs - $(RUSTDOC) -Zunstable-options --test --persist-doctests $(TMPDIR)/doctests --extern t=$(TMPDIR)/libt.rlib t.rs --edition 2024 - $(TMPDIR)/doctests/t_rs_12_0/rust_out - $(TMPDIR)/doctests/rust_out_2024 - rm -rf $(TMPDIR)/doctests - -no_run: - mkdir -p $(TMPDIR)/doctests - $(RUSTC) --crate-type rlib t.rs - $(RUSTDOC) -Zunstable-options --test --persist-doctests $(TMPDIR)/doctests --extern t=$(TMPDIR)/libt.rlib t.rs --no-run --edition 2024 - $(TMPDIR)/doctests/t_rs_12_0/rust_out - $(TMPDIR)/doctests/rust_out_2024 - rm -rf $(TMPDIR)/doctests - -# Behavior with --test-run-directory with relative paths. -test_run_directory: - mkdir -p $(TMPDIR)/doctests - mkdir -p $(TMPDIR)/rundir - $(RUSTC) --crate-type rlib t.rs - ( cd $(TMPDIR); \ - $(RUSTDOC) -Zunstable-options --test --persist-doctests doctests --test-run-directory rundir --extern t=libt.rlib $(MY_SRC_DIR)/t.rs --edition 2024 ) - rm -rf $(TMPDIR)/doctests $(TMPDIR)/rundir 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); +} From 847664a44a28e5f121b5c269d842423d9a3b1882 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 30 Apr 2024 17:45:09 +0200 Subject: [PATCH 20/24] Add documentation for the doctest `standalone` attribute --- .../documentation-tests.md | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/doc/rustdoc/src/write-documentation/documentation-tests.md b/src/doc/rustdoc/src/write-documentation/documentation-tests.md index a7d3186fb78b7..dfa3dfcb658d3 100644 --- a/src/doc/rustdoc/src/write-documentation/documentation-tests.md +++ b/src/doc/rustdoc/src/write-documentation/documentation-tests.md @@ -376,6 +376,55 @@ that the code sample should be compiled using the respective edition of Rust. # fn foo() {} ``` +Starting the 2024 edition, 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. + +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: ittells `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 From 24a23e13c98390d026cd17bce9a2d7d912b02f26 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Fri, 3 May 2024 12:38:04 +0200 Subject: [PATCH 21/24] Improve code --- .../documentation-tests.md | 2 +- src/librustdoc/doctest.rs | 69 ++++++++++++------- src/librustdoc/doctest/tests.rs | 28 ++++---- src/librustdoc/html/markdown.rs | 26 +++---- 4 files changed, 71 insertions(+), 54 deletions(-) diff --git a/src/doc/rustdoc/src/write-documentation/documentation-tests.md b/src/doc/rustdoc/src/write-documentation/documentation-tests.md index dfa3dfcb658d3..97e0209405327 100644 --- a/src/doc/rustdoc/src/write-documentation/documentation-tests.md +++ b/src/doc/rustdoc/src/write-documentation/documentation-tests.md @@ -403,7 +403,7 @@ pub fn init() { 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: ittells `rustdoc` that a doctest +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: diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index 8a153494d8b93..5c55d570782cd 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -922,22 +922,38 @@ pub const TEST: test::TestDescAndFn = test::TestDescAndFn {{ } } +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, +} + /// 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: String, - crate_name: Option>, - edition: Edition, - name: String, - lang_string: LangString, - line: usize, - file: String, - rustdoc_test_options: Arc, - test_id: String, - target_str: &str, - path: PathBuf, - no_run: bool, -) -> DocTest { +pub(crate) fn make_test(test_args: MakeTestArgs<'_, '_>) -> DocTest { + let MakeTestArgs { + source_code, + crate_name, + edition, + name, + 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 { let mut path = path.clone(); path.push(&test_id); @@ -950,7 +966,7 @@ pub(crate) fn make_test( // FIXME: This partition source is pretty bad. Something like // would be // a much better approach. - let (crate_attrs, everything_else, crates) = partition_source(&s, edition); + 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()); @@ -963,7 +979,7 @@ pub(crate) fn make_test( use rustc_parse::parser::ForceCollect; use rustc_span::source_map::FilePathMapping; - let filename = FileName::anon_source_code(&s); + let filename = FileName::anon_source_code(&source_code); let source = format!("{crates}{everything_else}"); // Any errors in parsing should also appear when the doctest is compiled for real, so just @@ -1053,7 +1069,7 @@ pub(crate) fn make_test( // 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: s, + test_code: source_code, supports_color: false, main_fn_span: None, crate_attrs, @@ -1081,7 +1097,8 @@ pub(crate) fn make_test( // https://github.com/rust-lang/rust/issues/56898 if found_macro && main_fn_span.is_none() - && s.lines() + && source_code + .lines() .map(|line| { let comment = line.find("//"); if let Some(comment_begins) = comment { &line[0..comment_begins] } else { line } @@ -1092,7 +1109,7 @@ pub(crate) fn make_test( } DocTest { - test_code: s, + test_code: source_code, supports_color, main_fn_span, crate_attrs, @@ -1677,20 +1694,20 @@ impl Tester for Collector { ); debug!("creating test {name}: {test}"); - let doctest = make_test( - test, - Some(crate_name), + let doctest = make_test(MakeTestArgs { + source_code: test, + crate_name: Some(crate_name), edition, name, - config, + lang_string: config, line, file, - Arc::clone(&self.rustdoc_test_options), + rustdoc_test_options: Arc::clone(&self.rustdoc_test_options), test_id, - &target_str, + target_str: &target_str, path, no_run, - ); + }); self.tests.add_doctest( doctest, &opts, diff --git a/src/librustdoc/doctest/tests.rs b/src/librustdoc/doctest/tests.rs index b7b5564044424..37306274b4c90 100644 --- a/src/librustdoc/doctest/tests.rs +++ b/src/librustdoc/doctest/tests.rs @@ -5,20 +5,20 @@ use std::path::PathBuf; use std::sync::Arc; fn make_test(input: String, krate: Option<&str>) -> DocTest { - super::make_test( - input, - krate.map(|k| k.into()), - DEFAULT_EDITION, - String::new(), // test name - LangString::empty_for_test(), - 0, // line - String::new(), // file name - Arc::new(IndividualTestOptions::empty()), - String::new(), // test id - "", // target_str - PathBuf::new(), // path - true, // no_run - ) + 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] diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index d348e7312034c..f4bee1d0c8969 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -305,20 +305,20 @@ impl<'a, I: Iterator>> Iterator for CodeBlocks<'_, 'a, I> { let mut opts: GlobalTestOptions = Default::default(); opts.insert_indent_space = true; - let (test, _) = doctest::make_test( - test, - krate, + let (test, _) = doctest::make_test(crate::doctest::MakeTestArgs { + source_code: test, + crate_name: krate, edition, - String::new(), - LangString::empty_for_test(), - 0, - String::new(), - Arc::new(IndividualTestOptions::empty()), - String::new(), - "", - "doctest.rs".into(), - true, - ) + 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 { "" }; From 88cbb780d341e98046f846d2500e624d92242680 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Sun, 12 May 2024 12:10:38 +0200 Subject: [PATCH 22/24] Improve code and rustdoc book --- .../documentation-tests.md | 9 +- src/librustdoc/doctest.rs | 109 ++++++++++-------- 2 files changed, 68 insertions(+), 50 deletions(-) diff --git a/src/doc/rustdoc/src/write-documentation/documentation-tests.md b/src/doc/rustdoc/src/write-documentation/documentation-tests.md index 97e0209405327..225d777a85502 100644 --- a/src/doc/rustdoc/src/write-documentation/documentation-tests.md +++ b/src/doc/rustdoc/src/write-documentation/documentation-tests.md @@ -376,9 +376,12 @@ that the code sample should be compiled using the respective edition of Rust. # fn foo() {} ``` -Starting the 2024 edition, 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. +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: diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index 5c55d570782cd..cc3af92a4b3ce 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -375,14 +375,14 @@ pub(crate) struct DocTestInfo { path: PathBuf, } -fn build_test_dir(outdir: &Arc, is_multiple_tests: bool) -> 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 out_dir = if is_multiple_tests && is_perm_dir { - // If this a "multiple tests" case and we generate it into a non temporary directory, we - // want to put it into the parent instead. - out_dir.parent().unwrap_or(out_dir) + 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 }; @@ -393,20 +393,33 @@ fn build_test_dir(outdir: &Arc, is_multiple_tests: bool) -> PathBuf { out_dir.into() } -fn run_test( - test: String, +struct RunTestInfo { + test_code: String, supports_color: bool, - test_info: Option, - rustdoc_options: Arc, is_multiple_tests: bool, - mut lang_string: LangString, edition: Edition, - report_unused_externs: impl Fn(UnusedExterns), 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>, - out_dir: PathBuf, ) -> Result<(), TestFailure> { + let RunTestInfo { + test_code, + supports_color, + is_multiple_tests, + edition, + 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); @@ -477,7 +490,7 @@ fn run_test( 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).is_err() { + 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); @@ -498,7 +511,7 @@ fn run_test( 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"); + stdin.write_all(test_code.as_bytes()).expect("could write out test sources"); child.wait_with_output().expect("Failed to read stdout") }; @@ -758,7 +771,7 @@ impl DocTest { let Self { supports_color, rustdoc_test_options, lang_string, outdir, path, no_run, .. } = self; - let out_dir = build_test_dir(&outdir, false); + 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())), @@ -780,17 +793,19 @@ impl DocTest { unused_externs.lock().unwrap().push(uext); }; let res = run_test( - code, - supports_color, + 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, - false, - lang_string, - edition, report_unused_externs, - no_run, None, - out_dir, ); // We need to move `outdir` into the closure to ensure the `TempDir` struct won't // be dropped before all tests have been run. @@ -955,10 +970,7 @@ pub(crate) fn make_test(test_args: MakeTestArgs<'_, '_>) -> DocTest { no_run, } = test_args; let outdir = Arc::new(if let Some(ref path) = rustdoc_test_options.persist_doctests { - let mut path = path.clone(); - path.push(&test_id); - - DirState::Perm(path) + DirState::Perm(path.clone()) } else { DirState::Temp(get_doctest_dir().expect("rustdoc needs a tempdir")) }); @@ -999,7 +1011,7 @@ pub(crate) fn make_test(test_args: MakeTestArgs<'_, '_>) -> DocTest { let dcx = DiagCtxt::new(Box::new(emitter)).disable_warnings(); let psess = ParseSess::with_dcx(dcx, sm); - let mut found_main = None; + let mut found_main_span = None; let mut found_extern_crate = crate_name.is_none(); let mut found_macro = false; @@ -1007,34 +1019,35 @@ pub(crate) fn make_test(test_args: MakeTestArgs<'_, '_>) -> DocTest { Ok(p) => p, Err(errs) => { errs.into_iter().for_each(|err| err.cancel()); - return (found_main, found_extern_crate, found_macro); + return (found_main_span, found_extern_crate, found_macro); } }; loop { match parser.parse_item(ForceCollect::No) { Ok(Some(item)) => { - if found_main.is_none() + if found_main_span.is_none() && let ast::ItemKind::Fn(..) = item.kind && item.ident.name == sym::main { - found_main = Some(item.span); + found_main_span = Some(item.span); } - if let ast::ItemKind::ExternCrate(original) = item.kind { - 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, - }; - } + if let ast::ItemKind::ExternCrate(original) = item.kind + && !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, + }; } if !found_macro && let ast::ItemKind::MacCall(..) = item.kind { found_macro = true; } - if found_main.is_some() && found_extern_crate { + if found_main_span.is_some() && found_extern_crate { break; } } @@ -1056,7 +1069,7 @@ pub(crate) fn make_test(test_args: MakeTestArgs<'_, '_>) -> DocTest { // drop. psess.dcx.reset_err_count(); - (found_main, found_extern_crate, found_macro) + (found_main_span, found_extern_crate, found_macro) }) }); @@ -1407,20 +1420,22 @@ test::test_main(&[{test_args}], vec![{ids}], None); ids = self.ids, ) .expect("failed to generate test code"); - let out_dir = build_test_dir(outdir, true); + let out_dir = build_test_dir(outdir, true, ""); let ret = run_test( - code, - self.supports_color, + 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, - true, - LangString::empty_for_test(), - edition, |_: UnusedExterns| {}, - false, // To prevent writing over an existing doctest Some(&format!("_{}", edition)), - out_dir, ); if let Err(TestFailure::CompileError) = ret { Err(()) } else { Ok(ret.is_ok()) } } From 222f7dca2964f4c5c7dcd722b776f81adb0a233e Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Sun, 12 May 2024 12:12:57 +0200 Subject: [PATCH 23/24] Add equivalent of rustdoc-ui test `failed-doctest-should-panic` for the 2021 edition --- .../doctest/failed-doctest-should-panic-2021.rs | 12 ++++++++++++ .../failed-doctest-should-panic-2021.stdout | 14 ++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 tests/rustdoc-ui/doctest/failed-doctest-should-panic-2021.rs create mode 100644 tests/rustdoc-ui/doctest/failed-doctest-should-panic-2021.stdout 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 + From 2e3c2aa20c079c00253d4c24f03a9ec523765ec1 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Sun, 12 May 2024 16:08:38 +0200 Subject: [PATCH 24/24] If there is any AST error with a doctest, we make it a standalone test To do so, AST error detection was improved in order to not filter out too many doctests. --- src/librustdoc/doctest.rs | 298 +++++++++++++------- tests/rustdoc-ui/doctest/no-run-flag.stdout | 10 +- tests/rustdoc-ui/doctest/test-type.stdout | 10 +- tests/rustdoc-ui/doctest/wrong-ast.rs | 20 ++ tests/rustdoc-ui/doctest/wrong-ast.stdout | 36 +++ 5 files changed, 259 insertions(+), 115 deletions(-) create mode 100644 tests/rustdoc-ui/doctest/wrong-ast.rs create mode 100644 tests/rustdoc-ui/doctest/wrong-ast.stdout diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index cc3af92a4b3ce..16db255f3db25 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -952,6 +952,132 @@ pub(crate) struct MakeTestArgs<'a, 'b> { 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(); +} + +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, + _ => {} + } + } + } + } + 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, + _ => {} + } + } + + 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 { @@ -960,7 +1086,7 @@ pub(crate) fn make_test(test_args: MakeTestArgs<'_, '_>) -> DocTest { crate_name, edition, name, - lang_string, + mut lang_string, line, file, rustdoc_test_options, @@ -986,90 +1112,44 @@ pub(crate) fn make_test(test_args: MakeTestArgs<'_, '_>) -> DocTest { // 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; - - let filename = FileName::anon_source_code(&source_code); - let source = format!("{crates}{everything_else}"); - - // 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 found_main_span = None; let mut found_extern_crate = crate_name.is_none(); let mut found_macro = false; - 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_span, found_extern_crate, found_macro); - } - }; - - loop { - match parser.parse_item(ForceCollect::No) { - Ok(Some(item)) => { - if found_main_span.is_none() - && let ast::ItemKind::Fn(..) = item.kind - && item.ident.name == sym::main - { - found_main_span = Some(item.span); - } - - if let ast::ItemKind::ExternCrate(original) = item.kind - && !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, - }; - } - - if !found_macro && let ast::ItemKind::MacCall(..) = item.kind { - found_macro = true; - } - - if found_main_span.is_some() && found_extern_crate { - break; - } - } - Ok(None) => break, - Err(e) => { - e.cancel(); - break; - } - } - - // The supplied item is only used for diagnostics, - // which are swallowed here anyway. - parser.maybe_consume_incorrect_semicolon(None); + 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, + ); } - // 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(); - - (found_main_span, found_extern_crate, found_macro) + (found_main_span, found_extern_crate, found_macro, parsing_result) }) }); @@ -1078,30 +1158,35 @@ pub(crate) fn make_test(test_args: MakeTestArgs<'_, '_>) -> DocTest { Ignore::None => false, Ignore::Some(ref ignores) => ignores.iter().any(|s| target_str.contains(s)), }; - let Ok((mut main_fn_span, 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 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, - }; + 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 @@ -1121,6 +1206,11 @@ pub(crate) fn make_test(test_args: MakeTestArgs<'_, '_>) -> DocTest { main_fn_span = Some(DUMMY_SP); } + 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, @@ -1515,8 +1605,6 @@ impl DocTestKinds { doctests.sort_by(|a, b| a.name.cmp(&b.name)); let outdir = Arc::clone(&doctests[0].outdir); - // When `DocTestRunner` is dropped, it'll run all pending doctests it didn't already - // run, so no need to worry about it. let mut tests_runner = DocTestRunner::new(); let rustdoc_test_options = Arc::clone(&doctests[0].rustdoc_test_options); diff --git a/tests/rustdoc-ui/doctest/no-run-flag.stdout b/tests/rustdoc-ui/doctest/no-run-flag.stdout index 1860146661b99..6838be8e6c435 100644 --- a/tests/rustdoc-ui/doctest/no-run-flag.stdout +++ b/tests/rustdoc-ui/doctest/no-run-flag.stdout @@ -1,17 +1,17 @@ -running 6 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 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. 5 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 1 test +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; 0 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/test-type.stdout b/tests/rustdoc-ui/doctest/test-type.stdout index 34d8e0c60e0c1..4a6c5463833f1 100644 --- a/tests/rustdoc-ui/doctest/test-type.stdout +++ b/tests/rustdoc-ui/doctest/test-type.stdout @@ -1,15 +1,15 @@ -running 4 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 6) ... ok test $DIR/test-type.rs - f (line 9) - should panic ... ok -test result: ok. 3 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in $TIME +test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME -running 1 test +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. 1 passed; 0 failed; 0 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 +