From 5b0e21810b2f2e6c737ffac231c03e2aad21795a Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Thu, 22 May 2025 17:22:44 +0200 Subject: [PATCH 1/3] Give more information into extracted doctest information --- src/librustdoc/doctest.rs | 4 +- src/librustdoc/doctest/extracted.rs | 34 +++++- src/librustdoc/doctest/make.rs | 133 ++++++++++++++++------- src/librustdoc/doctest/tests.rs | 4 +- src/librustdoc/html/markdown.rs | 3 +- tests/rustdoc-ui/extract-doctests.stdout | 2 +- 6 files changed, 132 insertions(+), 48 deletions(-) diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index b2fe24db0a2eb..bbfebb4e2353d 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -1044,14 +1044,14 @@ fn doctest_run_fn( let report_unused_externs = |uext| { unused_externs.lock().unwrap().push(uext); }; - let (full_test_code, full_test_line_offset) = doctest.generate_unique_doctest( + let (wrapper, full_test_line_offset) = doctest.generate_unique_doctest( &scraped_test.text, scraped_test.langstr.test_harness, &global_opts, Some(&global_opts.crate_name), ); let runnable_test = RunnableDocTest { - full_test_code, + full_test_code: wrapper.to_string(), full_test_line_offset, test_opts, global_opts, diff --git a/src/librustdoc/doctest/extracted.rs b/src/librustdoc/doctest/extracted.rs index 3b17ccc78c712..d6a7f943650a2 100644 --- a/src/librustdoc/doctest/extracted.rs +++ b/src/librustdoc/doctest/extracted.rs @@ -5,6 +5,7 @@ use serde::Serialize; +use super::make::DocTestWrapResult; use super::{BuildDocTestBuilder, ScrapedDocTest}; use crate::config::Options as RustdocOptions; use crate::html::markdown; @@ -14,7 +15,7 @@ use crate::html::markdown; /// This integer is incremented with every breaking change to the API, /// and is returned along with the JSON blob into the `format_version` root field. /// Consuming code should assert that this value matches the format version(s) that it supports. -const FORMAT_VERSION: u32 = 1; +const FORMAT_VERSION: u32 = 2; #[derive(Serialize)] pub(crate) struct ExtractedDocTests { @@ -42,7 +43,7 @@ impl ExtractedDocTests { .edition(edition) .lang_str(&langstr) .build(None); - let (full_test_code, size) = doctest.generate_unique_doctest( + let (wrapper, _size) = doctest.generate_unique_doctest( &text, langstr.test_harness, opts, @@ -52,13 +53,38 @@ impl ExtractedDocTests { file: filename.prefer_remapped_unconditionaly().to_string(), line, doctest_attributes: langstr.into(), - doctest_code: if size != 0 { Some(full_test_code) } else { None }, + doctest_code: match wrapper { + DocTestWrapResult::Valid { crate_level_code, wrapper, code } => Some(DocTest { + crate_level: crate_level_code, + code, + wrapper: wrapper.map( + |super::make::WrapperInfo { before, after, returns_result, .. }| { + WrapperInfo { before, after, returns_result } + }, + ), + }), + DocTestWrapResult::SyntaxError { .. } => None, + }, original_code: text, name, }); } } +#[derive(Serialize)] +pub(crate) struct WrapperInfo { + before: String, + after: String, + returns_result: bool, +} + +#[derive(Serialize)] +pub(crate) struct DocTest { + crate_level: String, + code: String, + wrapper: Option, +} + #[derive(Serialize)] pub(crate) struct ExtractedDocTest { file: String, @@ -66,7 +92,7 @@ pub(crate) struct ExtractedDocTest { doctest_attributes: LangString, original_code: String, /// `None` if the code syntax is invalid. - doctest_code: Option, + doctest_code: Option, name: String, } diff --git a/src/librustdoc/doctest/make.rs b/src/librustdoc/doctest/make.rs index 66647b880186d..1c837d9457eaa 100644 --- a/src/librustdoc/doctest/make.rs +++ b/src/librustdoc/doctest/make.rs @@ -184,6 +184,73 @@ pub(crate) struct DocTestBuilder { pub(crate) can_be_merged: bool, } +/// Contains needed information for doctest to be correctly generated with expected "wrapping". +pub(crate) struct WrapperInfo { + pub(crate) before: String, + pub(crate) after: String, + pub(crate) returns_result: bool, + insert_indent_space: bool, +} + +impl WrapperInfo { + fn len(&self) -> usize { + self.before.len() + self.after.len() + } +} + +/// Contains a doctest information. Can be converted into code with the `to_string()` method. +pub(crate) enum DocTestWrapResult { + Valid { + crate_level_code: String, + wrapper: Option, + code: String, + }, + /// Contains the original source code. + SyntaxError(String), +} + +impl std::string::ToString for DocTestWrapResult { + fn to_string(&self) -> String { + match self { + Self::SyntaxError(s) => s.clone(), + Self::Valid { crate_level_code, wrapper, code } => { + let mut prog_len = code.len() + crate_level_code.len(); + if let Some(wrapper) = wrapper { + prog_len += wrapper.len(); + if wrapper.insert_indent_space { + prog_len += code.lines().count() * 4; + } + } + let mut prog = String::with_capacity(prog_len); + + prog.push_str(crate_level_code); + if let Some(wrapper) = wrapper { + prog.push_str(&wrapper.before); + + // add extra 4 spaces for each line to offset the code block + if wrapper.insert_indent_space { + write!( + prog, + "{}", + fmt::from_fn(|f| code + .lines() + .map(|line| fmt::from_fn(move |f| write!(f, " {line}"))) + .joined("\n", f)) + ) + .unwrap(); + } else { + prog.push_str(code); + } + prog.push_str(&wrapper.after); + } else { + prog.push_str(code); + } + prog + } + } + } +} + impl DocTestBuilder { fn invalid( crate_attrs: String, @@ -214,49 +281,49 @@ impl DocTestBuilder { dont_insert_main: bool, opts: &GlobalTestOptions, crate_name: Option<&str>, - ) -> (String, usize) { + ) -> (DocTestWrapResult, usize) { if self.invalid_ast { // If the AST failed to compile, no need to go generate a complete doctest, the error // will be better this way. debug!("invalid AST:\n{test_code}"); - return (test_code.to_string(), 0); + return (DocTestWrapResult::SyntaxError(test_code.to_string()), 0); } let mut line_offset = 0; - let mut prog = String::new(); - let everything_else = self.everything_else.trim(); + let mut crate_level_code = String::new(); + let code = self.everything_else.trim().to_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"); + crate_level_code.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")); + crate_level_code.push_str(&format!("#![{attr}]\n")); line_offset += 1; } // Now push any outer attributes from the example, assuming they // are intended to be crate attributes. if !self.crate_attrs.is_empty() { - prog.push_str(&self.crate_attrs); + crate_level_code.push_str(&self.crate_attrs); if !self.crate_attrs.ends_with('\n') { - prog.push('\n'); + crate_level_code.push('\n'); } } if !self.maybe_crate_attrs.is_empty() { - prog.push_str(&self.maybe_crate_attrs); + crate_level_code.push_str(&self.maybe_crate_attrs); if !self.maybe_crate_attrs.ends_with('\n') { - prog.push('\n'); + crate_level_code.push('\n'); } } if !self.crates.is_empty() { - prog.push_str(&self.crates); + crate_level_code.push_str(&self.crates); if !self.crates.ends_with('\n') { - prog.push('\n'); + crate_level_code.push('\n'); } } @@ -274,17 +341,20 @@ impl DocTestBuilder { { // 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"); + crate_level_code.push_str("#[allow(unused_extern_crates)]\n"); - prog.push_str(&format!("extern crate r#{crate_name};\n")); + crate_level_code.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.has_main_fn || prog.contains("![no_std]") { - prog.push_str(everything_else); + let wrapper = if dont_insert_main + || self.has_main_fn + || crate_level_code.contains("![no_std]") + { + None } else { - let returns_result = everything_else.ends_with("(())"); + let returns_result = code.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(ref test_id) = self.test_id { @@ -318,28 +388,15 @@ impl DocTestBuilder { // /// ``` <- end of the inner main line_offset += 1; - prog.push_str(&main_pre); - - // add extra 4 spaces for each line to offset the code block - if opts.insert_indent_space { - write!( - prog, - "{}", - fmt::from_fn(|f| everything_else - .lines() - .map(|line| fmt::from_fn(move |f| write!(f, " {line}"))) - .joined("\n", f)) - ) - .unwrap(); - } else { - prog.push_str(everything_else); - }; - prog.push_str(&main_post); - } - - debug!("final doctest:\n{prog}"); + Some(WrapperInfo { + before: main_pre, + after: main_post, + returns_result, + insert_indent_space: opts.insert_indent_space, + }) + }; - (prog, line_offset) + (DocTestWrapResult::Valid { code, wrapper, crate_level_code }, line_offset) } } diff --git a/src/librustdoc/doctest/tests.rs b/src/librustdoc/doctest/tests.rs index 08248fdf39bbe..47bd7daf485cb 100644 --- a/src/librustdoc/doctest/tests.rs +++ b/src/librustdoc/doctest/tests.rs @@ -17,9 +17,9 @@ fn make_test( builder = builder.test_id(test_id.to_string()); } let doctest = builder.build(None); - let (code, line_offset) = + let (wrapper, line_offset) = doctest.generate_unique_doctest(test_code, dont_insert_main, opts, crate_name); - (code, line_offset) + (wrapper.to_string(), line_offset) } /// Default [`GlobalTestOptions`] for these unit tests. diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index ad7dfafd90c7d..a38935f5da38c 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -308,7 +308,8 @@ impl<'a, I: Iterator>> Iterator for CodeBlocks<'_, 'a, I> { builder = builder.crate_name(krate); } let doctest = builder.build(None); - let (test, _) = doctest.generate_unique_doctest(&test, false, &opts, krate); + let (wrapped, _) = doctest.generate_unique_doctest(&test, false, &opts, krate); + let test = wrapped.to_string(); let channel = if test.contains("#![feature(") { "&version=nightly" } else { "" }; let test_escaped = small_url_encode(test); diff --git a/tests/rustdoc-ui/extract-doctests.stdout b/tests/rustdoc-ui/extract-doctests.stdout index b11531b844ee4..796ecd82f1c93 100644 --- a/tests/rustdoc-ui/extract-doctests.stdout +++ b/tests/rustdoc-ui/extract-doctests.stdout @@ -1 +1 @@ -{"format_version":1,"doctests":[{"file":"$DIR/extract-doctests.rs","line":8,"doctest_attributes":{"original":"ignore (checking attributes)","should_panic":false,"no_run":false,"ignore":"All","rust":true,"test_harness":false,"compile_fail":false,"standalone_crate":false,"error_codes":[],"edition":null,"added_css_classes":[],"unknown":[]},"original_code":"let x = 12;\nlet y = 14;","doctest_code":"#![allow(unused)]\nfn main() {\nlet x = 12;\nlet y = 14;\n}","name":"$DIR/extract-doctests.rs - (line 8)"},{"file":"$DIR/extract-doctests.rs","line":13,"doctest_attributes":{"original":"edition2018,compile_fail","should_panic":false,"no_run":true,"ignore":"None","rust":true,"test_harness":false,"compile_fail":true,"standalone_crate":false,"error_codes":[],"edition":"2018","added_css_classes":[],"unknown":[]},"original_code":"let","doctest_code":null,"name":"$DIR/extract-doctests.rs - (line 13)"}]} \ No newline at end of file +{"format_version":2,"doctests":[{"file":"$DIR/extract-doctests.rs","line":8,"doctest_attributes":{"original":"ignore (checking attributes)","should_panic":false,"no_run":false,"ignore":"All","rust":true,"test_harness":false,"compile_fail":false,"standalone_crate":false,"error_codes":[],"edition":null,"added_css_classes":[],"unknown":[]},"original_code":"let x = 12;\nlet y = 14;","doctest_code":{"crate_level":"#![allow(unused)]\n","code":"let x = 12;\nlet y = 14;","wrapper":{"before":"fn main() {\n","after":"\n}","returns_result":false}},"name":"$DIR/extract-doctests.rs - (line 8)"},{"file":"$DIR/extract-doctests.rs","line":13,"doctest_attributes":{"original":"edition2018,compile_fail","should_panic":false,"no_run":true,"ignore":"None","rust":true,"test_harness":false,"compile_fail":true,"standalone_crate":false,"error_codes":[],"edition":"2018","added_css_classes":[],"unknown":[]},"original_code":"let","doctest_code":null,"name":"$DIR/extract-doctests.rs - (line 13)"}]} \ No newline at end of file From 9888eb4562c09ef12f9182bcc54e0cb5017ad9e5 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Thu, 22 May 2025 17:23:36 +0200 Subject: [PATCH 2/3] Add new extracted option doctest ui test for result returning doctest --- tests/rustdoc-ui/extract-doctests-result.rs | 11 +++++++++++ tests/rustdoc-ui/extract-doctests-result.stdout | 1 + 2 files changed, 12 insertions(+) create mode 100644 tests/rustdoc-ui/extract-doctests-result.rs create mode 100644 tests/rustdoc-ui/extract-doctests-result.stdout diff --git a/tests/rustdoc-ui/extract-doctests-result.rs b/tests/rustdoc-ui/extract-doctests-result.rs new file mode 100644 index 0000000000000..88affb6d33316 --- /dev/null +++ b/tests/rustdoc-ui/extract-doctests-result.rs @@ -0,0 +1,11 @@ +// Test to ensure that it generates expected output for `--output-format=doctest` command-line +// flag. + +//@ compile-flags:-Z unstable-options --output-format=doctest +//@ normalize-stdout: "tests/rustdoc-ui" -> "$$DIR" +//@ check-pass + +//! ``` +//! let x = 12; +//! Ok(()) +//! ``` diff --git a/tests/rustdoc-ui/extract-doctests-result.stdout b/tests/rustdoc-ui/extract-doctests-result.stdout new file mode 100644 index 0000000000000..44e6d33c66268 --- /dev/null +++ b/tests/rustdoc-ui/extract-doctests-result.stdout @@ -0,0 +1 @@ +{"format_version":2,"doctests":[{"file":"$DIR/extract-doctests-result.rs","line":8,"doctest_attributes":{"original":"","should_panic":false,"no_run":false,"ignore":"None","rust":true,"test_harness":false,"compile_fail":false,"standalone_crate":false,"error_codes":[],"edition":null,"added_css_classes":[],"unknown":[]},"original_code":"let x = 12;\nOk(())","doctest_code":{"crate_level":"#![allow(unused)]\n","code":"let x = 12;\nOk(())","wrapper":{"before":"fn main() { fn _inner() -> core::result::Result<(), impl core::fmt::Debug> {\n","after":"\n} _inner().unwrap() }","returns_result":true}},"name":"$DIR/extract-doctests-result.rs - (line 8)"}]} \ No newline at end of file From 7447a702bd8362b1a16fb51d53816bd0502f1688 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Fri, 23 May 2025 14:32:25 +0200 Subject: [PATCH 3/3] Improve naming of variables in `DocTestBuilder::generate_unique_doctest` --- src/librustdoc/doctest/make.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/librustdoc/doctest/make.rs b/src/librustdoc/doctest/make.rs index 1c837d9457eaa..fa3863276fe30 100644 --- a/src/librustdoc/doctest/make.rs +++ b/src/librustdoc/doctest/make.rs @@ -277,7 +277,7 @@ impl DocTestBuilder { /// lines before the test code begins. pub(crate) fn generate_unique_doctest( &self, - test_code: &str, + doctest_code: &str, dont_insert_main: bool, opts: &GlobalTestOptions, crate_name: Option<&str>, @@ -285,12 +285,12 @@ impl DocTestBuilder { if self.invalid_ast { // If the AST failed to compile, no need to go generate a complete doctest, the error // will be better this way. - debug!("invalid AST:\n{test_code}"); - return (DocTestWrapResult::SyntaxError(test_code.to_string()), 0); + debug!("invalid AST:\n{doctest_code}"); + return (DocTestWrapResult::SyntaxError(doctest_code.to_string()), 0); } let mut line_offset = 0; let mut crate_level_code = String::new(); - let code = self.everything_else.trim().to_string(); + let processed_code = self.everything_else.trim().to_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 @@ -337,7 +337,7 @@ impl DocTestBuilder { // NOTE: this is terribly inaccurate because it doesn't actually // parse the source, but only has false positives, not false // negatives. - test_code.contains(crate_name) + doctest_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. @@ -354,7 +354,7 @@ impl DocTestBuilder { { None } else { - let returns_result = code.ends_with("(())"); + let returns_result = processed_code.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(ref test_id) = self.test_id { @@ -396,7 +396,7 @@ impl DocTestBuilder { }) }; - (DocTestWrapResult::Valid { code, wrapper, crate_level_code }, line_offset) + (DocTestWrapResult::Valid { code: processed_code, wrapper, crate_level_code }, line_offset) } }