Add DocTest type

This commit is contained in:
Guillaume Gomez 2024-06-08 16:31:53 +02:00
parent 05fbfde17d
commit 5e244370fe
4 changed files with 192 additions and 140 deletions

View File

@ -10,7 +10,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::{panic, str};
pub(crate) use make::make_test;
pub(crate) use make::DocTest;
pub(crate) use markdown::test as test_markdown;
use rustc_ast as ast;
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
@ -732,13 +732,12 @@ fn doctest_run_fn(
unused_externs.lock().unwrap().push(uext);
};
let edition = scraped_test.edition(&rustdoc_options);
let (full_test_code, full_test_line_offset, supports_color) = make_test(
&scraped_test.text,
Some(&global_opts.crate_name),
let doctest = DocTest::new(&scraped_test.text, Some(&global_opts.crate_name), edition);
let (full_test_code, full_test_line_offset) = doctest.generate_unique_doctest(
scraped_test.langstr.test_harness,
&global_opts,
edition,
Some(&test_opts.test_id),
Some(&global_opts.crate_name),
);
let runnable_test = RunnableDoctest {
full_test_code,
@ -747,7 +746,8 @@ fn doctest_run_fn(
global_opts,
scraped_test,
};
let res = run_test(runnable_test, &rustdoc_options, supports_color, report_unused_externs);
let res =
run_test(runnable_test, &rustdoc_options, doctest.supports_color, report_unused_externs);
if let Err(err) = res {
match err {

View File

@ -13,27 +13,66 @@ use rustc_session::parse::ParseSess;
use rustc_span::edition::Edition;
use rustc_span::source_map::SourceMap;
use rustc_span::symbol::sym;
use rustc_span::FileName;
use rustc_span::{FileName, Span, DUMMY_SP};
use super::GlobalTestOptions;
/// 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();
pub(crate) struct DocTest {
pub(crate) test_code: String,
pub(crate) supports_color: bool,
pub(crate) already_has_extern_crate: bool,
pub(crate) main_fn_span: Option<Span>,
pub(crate) crate_attrs: String,
pub(crate) crates: String,
pub(crate) everything_else: String,
}
impl DocTest {
pub(crate) fn new(source: &str, crate_name: Option<&str>, edition: Edition) -> Self {
let (crate_attrs, everything_else, crates) = partition_source(source, 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.
let Ok((main_fn_span, already_has_extern_crate)) =
check_for_main_and_extern_crate(crate_name, source, edition, &mut supports_color)
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.to_string(),
supports_color: false,
main_fn_span: None,
crate_attrs,
crates,
everything_else,
already_has_extern_crate: false,
};
};
Self {
test_code: source.to_string(),
supports_color,
main_fn_span,
crate_attrs,
crates,
everything_else,
already_has_extern_crate,
}
}
/// Transforms a test into code that can be compiled into a Rust binary, and returns the number of
/// lines before the test code begins.
pub(crate) fn generate_unique_doctest(
&self,
dont_insert_main: bool,
opts: &GlobalTestOptions,
// If `test_id` is `None`, it means we're generating code for a code example "run" link.
test_id: Option<&str>,
crate_name: Option<&str>,
) -> (String, usize) {
let mut line_offset = 0;
let mut prog = String::new();
let everything_else = self.everything_else.trim();
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
@ -51,22 +90,12 @@ pub(crate) fn make_test(
// 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);
// Uses librustc_ast to parse the doctest and find if there's a main fn and the extern
// crate already is included.
let Ok((already_has_main, already_has_extern_crate)) =
check_for_main_and_extern_crate(crate_name, s.to_owned(), edition, &mut supports_color)
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);
};
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 !already_has_extern_crate &&
if !self.already_has_extern_crate &&
!opts.no_crate_inject &&
let Some(crate_name) = crate_name &&
crate_name != "std" &&
@ -74,7 +103,7 @@ pub(crate) fn make_test(
// 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)
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.
@ -85,10 +114,10 @@ pub(crate) fn make_test(
}
// FIXME: This code cannot yet handle no_std test cases yet
if dont_insert_main || already_has_main || prog.contains("![no_std]") {
if dont_insert_main || self.main_fn_span.is_some() || prog.contains("![no_std]") {
prog.push_str(everything_else);
} else {
let returns_result = everything_else.trim_end().ends_with("(())");
let returns_result = everything_else.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 {
@ -122,30 +151,35 @@ pub(crate) fn make_test(
// /// ``` <- 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
let content = if opts.insert_indent_space {
everything_else
if opts.insert_indent_space {
prog.push_str(
&everything_else
.lines()
.map(|line| format!(" {}", line))
.collect::<Vec<String>>()
.join("\n")
.join("\n"),
);
} else {
everything_else.to_string()
prog.push_str(everything_else);
};
prog.extend([&main_pre, content.as_str(), &main_post].iter().cloned());
prog.push_str(&main_post);
}
debug!("final doctest:\n{prog}");
(prog, line_offset, supports_color)
(prog, line_offset)
}
}
fn check_for_main_and_extern_crate(
crate_name: Option<&str>,
source: String,
source: &str,
edition: Edition,
supports_color: &mut bool,
) -> Result<(bool, bool), FatalError> {
) -> Result<(Option<Span>, bool), FatalError> {
let result = rustc_driver::catch_fatal_errors(|| {
rustc_span::create_session_if_not_set_then(edition, |_| {
use rustc_errors::emitter::{Emitter, HumanEmitter};
@ -153,7 +187,7 @@ fn check_for_main_and_extern_crate(
use rustc_parse::parser::ForceCollect;
use rustc_span::source_map::FilePathMapping;
let filename = FileName::anon_source_code(&source);
let filename = FileName::anon_source_code(source);
// 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.
@ -172,11 +206,11 @@ fn check_for_main_and_extern_crate(
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 parser = match new_parser_from_source_str(&psess, filename, source.clone()) {
let mut parser = match new_parser_from_source_str(&psess, filename, source.to_owned()) {
Ok(p) => p,
Err(errs) => {
errs.into_iter().for_each(|err| err.cancel());
@ -187,11 +221,11 @@ fn check_for_main_and_extern_crate(
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
@ -211,7 +245,7 @@ fn check_for_main_and_extern_crate(
found_macro = true;
}
if found_main && found_extern_crate {
if found_main.is_some() && found_extern_crate {
break;
}
}
@ -236,23 +270,26 @@ fn check_for_main_and_extern_crate(
(found_main, found_extern_crate, found_macro)
})
});
let (mut already_has_main, already_has_extern_crate, found_macro) = result?;
let (mut main_fn_span, already_has_extern_crate, found_macro) = result?;
// If a doctest's `fn main` is being masked by a wrapper macro, the parsing loop above won't
// see it. In that case, run the old text-based scan to see if they at least have a main
// function written inside a macro invocation. See
// https://github.com/rust-lang/rust/issues/56898
if found_macro && !already_has_main {
already_has_main = source
if found_macro
&& main_fn_span.is_none()
&& source
.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"));
};
.any(|code| code.contains("fn main"))
{
main_fn_span = Some(DUMMY_SP);
}
Ok((already_has_main, already_has_extern_crate))
Ok((main_fn_span, already_has_extern_crate))
}
fn check_if_attr_is_complete(source: &str, edition: Edition) -> bool {

View File

@ -1,8 +1,22 @@
use std::path::PathBuf;
use rustc_span::edition::DEFAULT_EDITION;
use super::{DocTest, GlobalTestOptions};
use rustc_span::edition::{Edition, DEFAULT_EDITION};
use super::{make_test, GlobalTestOptions};
// FIXME: remove the last element of the returned tuple and simplify arguments of this helper.
fn make_test(
test_code: &str,
crate_name: Option<&str>,
dont_insert_main: bool,
opts: &GlobalTestOptions,
edition: Edition,
test_id: Option<&str>,
) -> (String, usize, ()) {
let doctest = DocTest::new(test_code, crate_name, edition);
let (code, line_offset) =
doctest.generate_unique_doctest(dont_insert_main, opts, test_id, crate_name);
(code, line_offset, ())
}
/// Default [`GlobalTestOptions`] for these unit tests.
fn default_global_opts(crate_name: impl Into<String>) -> GlobalTestOptions {

View File

@ -297,10 +297,11 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for CodeBlocks<'_, 'a, I> {
attrs: vec![],
args_file: PathBuf::new(),
};
let (test, _, _) = doctest::make_test(&test, krate, false, &opts, edition, None);
let doctest = doctest::DocTest::new(&test, krate, edition);
let (test, _) = doctest.generate_unique_doctest(false, &opts, None, krate);
let channel = if test.contains("#![feature(") { "&amp;version=nightly" } else { "" };
let test_escaped = small_url_encode(test);
let test_escaped = small_url_encode(doctest.test_code);
Some(format!(
"<a class=\"test-arrow\" \
target=\"_blank\" \