Add DocTest
type
This commit is contained in:
parent
05fbfde17d
commit
5e244370fe
@ -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 {
|
||||
|
@ -13,139 +13,173 @@ 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();
|
||||
let mut supports_color = false;
|
||||
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,
|
||||
}
|
||||
|
||||
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 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;
|
||||
|
||||
// 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(&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);
|
||||
};
|
||||
|
||||
// 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)
|
||||
{
|
||||
// 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 || 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()
|
||||
// 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,
|
||||
};
|
||||
};
|
||||
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::<Vec<String>>()
|
||||
.join("\n")
|
||||
} else {
|
||||
everything_else.to_string()
|
||||
};
|
||||
prog.extend([&main_pre, content.as_str(), &main_post].iter().cloned());
|
||||
Self {
|
||||
test_code: source.to_string(),
|
||||
supports_color,
|
||||
main_fn_span,
|
||||
crate_attrs,
|
||||
crates,
|
||||
everything_else,
|
||||
already_has_extern_crate,
|
||||
}
|
||||
}
|
||||
|
||||
debug!("final doctest:\n{prog}");
|
||||
/// 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
|
||||
// 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;
|
||||
}
|
||||
|
||||
(prog, line_offset, supports_color)
|
||||
// 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(everything_else);
|
||||
} else {
|
||||
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 {
|
||||
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;
|
||||
|
||||
prog.push_str(&main_pre);
|
||||
|
||||
// add extra 4 spaces for each line to offset the code block
|
||||
if opts.insert_indent_space {
|
||||
prog.push_str(
|
||||
&everything_else
|
||||
.lines()
|
||||
.map(|line| format!(" {}", line))
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n"),
|
||||
);
|
||||
} else {
|
||||
prog.push_str(everything_else);
|
||||
};
|
||||
prog.push_str(&main_post);
|
||||
}
|
||||
|
||||
debug!("final doctest:\n{prog}");
|
||||
|
||||
(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 {
|
||||
|
@ -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 {
|
||||
|
@ -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(") { "&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\" \
|
||||
|
Loading…
x
Reference in New Issue
Block a user