Split doctests into two categories: mergeable ones and standalone ones

This commit is contained in:
Guillaume Gomez 2024-06-10 15:31:19 +02:00
parent 96051f20e2
commit 6ae3524835
4 changed files with 99 additions and 62 deletions

View File

@ -206,7 +206,7 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, options: RustdocOptions) -> Result<()
test_args, test_args,
nocapture, nocapture,
opts, opts,
rustdoc_options, &rustdoc_options,
&unused_extern_reports, &unused_extern_reports,
standalone_tests, standalone_tests,
mergeable_tests, mergeable_tests,
@ -259,10 +259,10 @@ pub(crate) fn run_tests(
mut test_args: Vec<String>, mut test_args: Vec<String>,
nocapture: bool, nocapture: bool,
opts: GlobalTestOptions, opts: GlobalTestOptions,
rustdoc_options: RustdocOptions, rustdoc_options: &Arc<RustdocOptions>,
unused_extern_reports: &Arc<Mutex<Vec<UnusedExterns>>>, unused_extern_reports: &Arc<Mutex<Vec<UnusedExterns>>>,
mut standalone_tests: Vec<test::TestDescAndFn>, mut standalone_tests: Vec<test::TestDescAndFn>,
mut mergeable_tests: FxHashMap<Edition, Vec<(DocTest, ScrapedDoctest)>>, mergeable_tests: FxHashMap<Edition, Vec<(DocTest, ScrapedDoctest)>>,
) { ) {
test_args.insert(0, "rustdoctest".to_string()); test_args.insert(0, "rustdoctest".to_string());
if nocapture { if nocapture {
@ -270,28 +270,32 @@ pub(crate) fn run_tests(
} }
let mut nb_errors = 0; let mut nb_errors = 0;
let target_str = rustdoc_options.target.to_string();
for (edition, mut doctests) in mergeable_tests { for (edition, mut doctests) in mergeable_tests {
if doctests.is_empty() { if doctests.is_empty() {
continue; continue;
} }
doctests.sort_by(|(_, a), (_, b)| a.name.cmp(&b.name)); doctests.sort_by(|(_, a), (_, b)| a.name.cmp(&b.name));
let outdir = Arc::clone(&doctests[0].outdir);
let mut tests_runner = runner::DocTestRunner::new(); let mut tests_runner = runner::DocTestRunner::new();
let rustdoc_test_options = IndividualTestOptions::new( let rustdoc_test_options = IndividualTestOptions::new(
&rustdoc_options, &rustdoc_options,
format!("merged_doctest"), &format!("merged_doctest_{edition}"),
PathBuf::from(r"doctest.rs"), PathBuf::from(format!("doctest_{edition}.rs")),
); );
for (doctest, scraped_test) in &doctests { for (doctest, scraped_test) in &doctests {
tests_runner.add_test(doctest, scraped_test); tests_runner.add_test(doctest, scraped_test, &target_str);
} }
if let Ok(success) = if let Ok(success) = tests_runner.run_tests(
tests_runner.run_tests(rustdoc_test_options, edition, &opts, &test_args, &outdir) rustdoc_test_options,
{ edition,
&opts,
&test_args,
rustdoc_options,
) {
if !success { if !success {
nb_errors += 1; nb_errors += 1;
} }
@ -311,7 +315,7 @@ pub(crate) fn run_tests(
doctest, doctest,
scraped_test, scraped_test,
opts.clone(), opts.clone(),
rustdoc_test_options.clone(), Arc::clone(&rustdoc_options),
unused_extern_reports.clone(), unused_extern_reports.clone(),
)); ));
} }
@ -406,7 +410,7 @@ fn path(&self) -> &std::path::Path {
// We could unify this struct the one in rustc but they have different // We could unify this struct the one in rustc but they have different
// ownership semantics, so doing so would create wasteful allocations. // ownership semantics, so doing so would create wasteful allocations.
#[derive(serde::Serialize, serde::Deserialize)] #[derive(serde::Serialize, serde::Deserialize)]
struct UnusedExterns { pub(crate) struct UnusedExterns {
/// Lint level of the unused_crate_dependencies lint /// Lint level of the unused_crate_dependencies lint
lint_level: String, lint_level: String,
/// List of unused externs by their names. /// List of unused externs by their names.
@ -642,12 +646,11 @@ fn make_maybe_absolute_path(path: PathBuf) -> PathBuf {
} }
struct IndividualTestOptions { struct IndividualTestOptions {
outdir: DirState, outdir: DirState,
test_id: String,
path: PathBuf, path: PathBuf,
} }
impl IndividualTestOptions { impl IndividualTestOptions {
fn new(options: &RustdocOptions, test_id: String, test_path: PathBuf) -> Self { fn new(options: &RustdocOptions, test_id: &str, test_path: PathBuf) -> Self {
let outdir = if let Some(ref path) = options.persist_doctests { let outdir = if let Some(ref path) = options.persist_doctests {
let mut path = path.clone(); let mut path = path.clone();
path.push(&test_id); path.push(&test_id);
@ -662,15 +665,14 @@ fn new(options: &RustdocOptions, test_id: String, test_path: PathBuf) -> Self {
DirState::Temp(get_doctest_dir().expect("rustdoc needs a tempdir")) DirState::Temp(get_doctest_dir().expect("rustdoc needs a tempdir"))
}; };
Self { outdir, test_id, path: test_path } Self { outdir, path: test_path }
} }
} }
/// A doctest scraped from the code, ready to be turned into a runnable test. /// A doctest scraped from the code, ready to be turned into a runnable test.
struct ScrapedDoctest { pub(crate) struct ScrapedDoctest {
filename: FileName, filename: FileName,
line: usize, line: usize,
logical_path: Vec<String>,
langstr: LangString, langstr: LangString,
text: String, text: String,
name: String, name: String,
@ -692,7 +694,7 @@ fn new(
let name = let name =
format!("{} - {item_path}(line {line})", filename.prefer_remapped_unconditionaly()); format!("{} - {item_path}(line {line})", filename.prefer_remapped_unconditionaly());
Self { filename, line, logical_path, langstr, text, name } Self { filename, line, langstr, text, name }
} }
fn edition(&self, opts: &RustdocOptions) -> Edition { fn edition(&self, opts: &RustdocOptions) -> Edition {
self.langstr.edition.unwrap_or(opts.edition) self.langstr.edition.unwrap_or(opts.edition)
@ -701,6 +703,19 @@ fn edition(&self, opts: &RustdocOptions) -> Edition {
fn no_run(&self, opts: &RustdocOptions) -> bool { fn no_run(&self, opts: &RustdocOptions) -> bool {
self.langstr.no_run || opts.no_run self.langstr.no_run || opts.no_run
} }
fn path(&self) -> PathBuf {
match &self.filename {
FileName::Real(path) => {
if let Some(local_path) = path.local_path() {
local_path.to_path_buf()
} else {
// Somehow we got the filename from the metadata of another crate, should never happen
unreachable!("doctest from a different crate");
}
}
_ => PathBuf::from(r"doctest.rs"),
}
}
} }
pub(crate) trait DoctestVisitor { pub(crate) trait DoctestVisitor {
@ -757,7 +772,7 @@ fn add_test(&mut self, scraped_test: ScrapedDoctest) {
let edition = scraped_test.edition(&self.rustdoc_options); let edition = scraped_test.edition(&self.rustdoc_options);
let doctest = let doctest =
DocTest::new(&scraped_test.text, Some(&self.opts.crate_name), edition, test_id); DocTest::new(&scraped_test.text, Some(&self.opts.crate_name), edition, Some(test_id));
let is_standalone = scraped_test.langstr.compile_fail let is_standalone = scraped_test.langstr.compile_fail
|| scraped_test.langstr.test_harness || scraped_test.langstr.test_harness
|| self.rustdoc_options.nocapture || self.rustdoc_options.nocapture
@ -784,7 +799,7 @@ fn generate_test_desc_and_fn(
test, test,
scraped_test, scraped_test,
self.opts.clone(), self.opts.clone(),
self.rustdoc_options.clone(), Arc::clone(&self.rustdoc_options),
self.unused_extern_reports.clone(), self.unused_extern_reports.clone(),
) )
} }
@ -794,32 +809,20 @@ fn generate_test_desc_and_fn(
test: DocTest, test: DocTest,
scraped_test: ScrapedDoctest, scraped_test: ScrapedDoctest,
opts: GlobalTestOptions, opts: GlobalTestOptions,
rustdoc_options: IndividualTestOptions, rustdoc_options: Arc<RustdocOptions>,
unused_externs: Arc<Mutex<Vec<UnusedExterns>>>, unused_externs: Arc<Mutex<Vec<UnusedExterns>>>,
) -> test::TestDescAndFn { ) -> test::TestDescAndFn {
let target_str = rustdoc_options.target.to_string(); let target_str = rustdoc_options.target.to_string();
let rustdoc_test_options = IndividualTestOptions::new(
&rustdoc_options,
test.test_id.as_deref().unwrap_or_else(|| "<doctest>"),
scraped_test.path(),
);
let path = match &scraped_test.filename { debug!("creating test {}: {}", scraped_test.name, scraped_test.text);
FileName::Real(path) => {
if let Some(local_path) = path.local_path() {
local_path.to_path_buf()
} else {
// Somehow we got the filename from the metadata of another crate, should never happen
unreachable!("doctest from a different crate");
}
}
_ => PathBuf::from(r"doctest.rs"),
};
let name = &test.name;
let rustdoc_test_options =
IndividualTestOptions::new(&rustdoc_options, test.test_id.clone(), path);
// let rustdoc_options_clone = rustdoc_options.clone();
debug!("creating test {name}: {}", scraped_test.text);
test::TestDescAndFn { test::TestDescAndFn {
desc: test::TestDesc { desc: test::TestDesc {
name: test::DynTestName(name), name: test::DynTestName(scraped_test.name.clone()),
ignore: match scraped_test.langstr.ignore { ignore: match scraped_test.langstr.ignore {
Ignore::All => true, Ignore::All => true,
Ignore::None => false, Ignore::None => false,

View File

@ -1,6 +1,7 @@
//! Doctest functionality used only for doctests in `.md` Markdown files. //! Doctest functionality used only for doctests in `.md` Markdown files.
use std::fs::read_to_string; use std::fs::read_to_string;
use std::sync::{Arc, Mutex};
use rustc_span::FileName; use rustc_span::FileName;
use tempfile::tempdir; use tempfile::tempdir;
@ -114,6 +115,16 @@ pub(crate) fn test(options: Options) -> Result<(), String> {
let mut collector = CreateRunnableDoctests::new(options.clone(), opts); let mut collector = CreateRunnableDoctests::new(options.clone(), opts);
md_collector.tests.into_iter().for_each(|t| collector.add_test(t)); md_collector.tests.into_iter().for_each(|t| collector.add_test(t));
crate::doctest::run_tests(options.test_args, options.nocapture, collector.standalone_tests); let CreateRunnableDoctests { opts, rustdoc_options, standalone_tests, mergeable_tests, .. } =
collector;
crate::doctest::run_tests(
options.test_args,
options.nocapture,
opts,
&rustdoc_options,
&Arc::new(Mutex::new(Vec::new())),
standalone_tests,
mergeable_tests,
);
Ok(()) Ok(())
} }

View File

@ -2,13 +2,12 @@
use rustc_span::edition::Edition; use rustc_span::edition::Edition;
use std::fmt::Write; use std::fmt::Write;
use std::sync::{Arc, Mutex};
use crate::doctest::{ use crate::doctest::{
run_test, DirState, DocTest, GlobalTestOptions, IndividualTestOptions, RunnableDoctest, run_test, DocTest, GlobalTestOptions, IndividualTestOptions, RunnableDoctest, RustdocOptions,
RustdocOptions, ScrapedDoctest, TestFailure, UnusedExterns, ScrapedDoctest, TestFailure, UnusedExterns,
}; };
use crate::html::markdown::LangString; use crate::html::markdown::{Ignore, LangString};
/// Convenient type to merge compatible doctests into one. /// Convenient type to merge compatible doctests into one.
pub(crate) struct DocTestRunner { pub(crate) struct DocTestRunner {
@ -17,7 +16,6 @@ pub(crate) struct DocTestRunner {
output: String, output: String,
supports_color: bool, supports_color: bool,
nb_tests: usize, nb_tests: usize,
doctests: Vec<DocTest>,
} }
impl DocTestRunner { impl DocTestRunner {
@ -28,12 +26,21 @@ pub(crate) fn new() -> Self {
output: String::new(), output: String::new(),
supports_color: true, supports_color: true,
nb_tests: 0, nb_tests: 0,
doctests: Vec::with_capacity(10),
} }
} }
pub(crate) fn add_test(&mut self, doctest: &DocTest, scraped_test: &ScrapedDoctest) { pub(crate) fn add_test(
if !doctest.ignore { &mut self,
doctest: &DocTest,
scraped_test: &ScrapedDoctest,
target_str: &str,
) {
let ignore = match scraped_test.langstr.ignore {
Ignore::All => true,
Ignore::None => false,
Ignore::Some(ref ignores) => ignores.iter().any(|s| target_str.contains(s)),
};
if !ignore {
for line in doctest.crate_attrs.split('\n') { for line in doctest.crate_attrs.split('\n') {
self.crate_attrs.insert(line.to_string()); self.crate_attrs.insert(line.to_string());
} }
@ -43,11 +50,16 @@ pub(crate) fn add_test(&mut self, doctest: &DocTest, scraped_test: &ScrapedDocte
} }
self.ids.push_str(&format!( self.ids.push_str(&format!(
"{}::TEST", "{}::TEST",
generate_mergeable_doctest(doctest, scraped_test, self.nb_tests, &mut self.output), generate_mergeable_doctest(
doctest,
scraped_test,
ignore,
self.nb_tests,
&mut self.output
),
)); ));
self.supports_color &= doctest.supports_color; self.supports_color &= doctest.supports_color;
self.nb_tests += 1; self.nb_tests += 1;
self.doctests.push(doctest);
} }
pub(crate) fn run_tests( pub(crate) fn run_tests(
@ -56,9 +68,7 @@ pub(crate) fn run_tests(
edition: Edition, edition: Edition,
opts: &GlobalTestOptions, opts: &GlobalTestOptions,
test_args: &[String], test_args: &[String],
outdir: &Arc<DirState>,
rustdoc_options: &RustdocOptions, rustdoc_options: &RustdocOptions,
unused_externs: Arc<Mutex<Vec<UnusedExterns>>>,
) -> Result<bool, ()> { ) -> Result<bool, ()> {
let mut code = "\ let mut code = "\
#![allow(unused_extern_crates)] #![allow(unused_extern_crates)]
@ -73,7 +83,19 @@ pub(crate) fn run_tests(
code.push('\n'); code.push('\n');
} }
DocTest::push_attrs(&mut code, opts, &mut 0); 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.
code.push_str("#![allow(unused)]\n");
}
// Next, any attributes that came from the crate root via #![doc(test(attr(...)))].
for attr in &opts.attrs {
code.push_str(&format!("#![{attr}]\n"));
}
code.push_str("extern crate test;\n"); code.push_str("extern crate test;\n");
let test_args = let test_args =
@ -91,7 +113,6 @@ fn main() {{
ids = self.ids, ids = self.ids,
) )
.expect("failed to generate test code"); .expect("failed to generate test code");
// let out_dir = build_test_dir(outdir, true, "");
let runnable_test = RunnableDoctest { let runnable_test = RunnableDoctest {
full_test_code: code, full_test_code: code,
full_test_line_offset: 0, full_test_line_offset: 0,
@ -102,7 +123,8 @@ fn main() {{
edition, edition,
no_run: false, no_run: false,
}; };
let ret = run_test(runnable_test, rustdoc_options, self.supports_color, unused_externs); let ret =
run_test(runnable_test, rustdoc_options, self.supports_color, |_: UnusedExterns| {});
if let Err(TestFailure::CompileError) = ret { Err(()) } else { Ok(ret.is_ok()) } if let Err(TestFailure::CompileError) = ret { Err(()) } else { Ok(ret.is_ok()) }
} }
} }
@ -111,12 +133,13 @@ fn main() {{
fn generate_mergeable_doctest( fn generate_mergeable_doctest(
doctest: &DocTest, doctest: &DocTest,
scraped_test: &ScrapedDoctest, scraped_test: &ScrapedDoctest,
ignore: bool,
id: usize, id: usize,
output: &mut String, output: &mut String,
) -> String { ) -> String {
let test_id = format!("__doctest_{id}"); let test_id = format!("__doctest_{id}");
if doctest.ignore { if ignore {
// We generate nothing else. // We generate nothing else.
writeln!(output, "mod {test_id} {{\n").unwrap(); writeln!(output, "mod {test_id} {{\n").unwrap();
} else { } else {
@ -166,8 +189,7 @@ fn main() {returns_result} {{
}}; }};
}}", }}",
test_name = scraped_test.name, test_name = scraped_test.name,
ignore = scraped_test.langstr.ignore, file = scraped_test.path(),
file = scraped_test.file,
line = scraped_test.line, line = scraped_test.line,
no_run = scraped_test.langstr.no_run, no_run = scraped_test.langstr.no_run,
should_panic = if !scraped_test.langstr.no_run && scraped_test.langstr.should_panic { should_panic = if !scraped_test.langstr.no_run && scraped_test.langstr.should_panic {
@ -177,7 +199,7 @@ fn main() {returns_result} {{
}, },
// Setting `no_run` to `true` in `TestDesc` still makes the test run, so we simply // Setting `no_run` to `true` in `TestDesc` still makes the test run, so we simply
// don't give it the function to run. // don't give it the function to run.
runner = if scraped_test.langstr.no_run || scraped_test.langstr.ignore { runner = if ignore || scraped_test.langstr.no_run {
"Ok::<(), String>(())" "Ok::<(), String>(())"
} else { } else {
"self::main()" "self::main()"

View File

@ -10,9 +10,10 @@ fn make_test(
opts: &GlobalTestOptions, opts: &GlobalTestOptions,
test_id: Option<&str>, test_id: Option<&str>,
) -> (String, usize) { ) -> (String, usize) {
let doctest = DocTest::new(test_code, crate_name, DEFAULT_EDITION); let doctest =
DocTest::new(test_code, crate_name, DEFAULT_EDITION, test_id.map(|s| s.to_string()));
let (code, line_offset) = let (code, line_offset) =
doctest.generate_unique_doctest(test_code, dont_insert_main, opts, test_id, crate_name); doctest.generate_unique_doctest(test_code, dont_insert_main, opts, crate_name);
(code, line_offset) (code, line_offset)
} }