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,
nocapture,
opts,
rustdoc_options,
&rustdoc_options,
&unused_extern_reports,
standalone_tests,
mergeable_tests,
@ -259,10 +259,10 @@ pub(crate) fn run_tests(
mut test_args: Vec<String>,
nocapture: bool,
opts: GlobalTestOptions,
rustdoc_options: RustdocOptions,
rustdoc_options: &Arc<RustdocOptions>,
unused_extern_reports: &Arc<Mutex<Vec<UnusedExterns>>>,
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());
if nocapture {
@ -270,28 +270,32 @@ pub(crate) fn run_tests(
}
let mut nb_errors = 0;
let target_str = rustdoc_options.target.to_string();
for (edition, mut doctests) in mergeable_tests {
if doctests.is_empty() {
continue;
}
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 rustdoc_test_options = IndividualTestOptions::new(
&rustdoc_options,
format!("merged_doctest"),
PathBuf::from(r"doctest.rs"),
&format!("merged_doctest_{edition}"),
PathBuf::from(format!("doctest_{edition}.rs")),
);
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) =
tests_runner.run_tests(rustdoc_test_options, edition, &opts, &test_args, &outdir)
{
if let Ok(success) = tests_runner.run_tests(
rustdoc_test_options,
edition,
&opts,
&test_args,
rustdoc_options,
) {
if !success {
nb_errors += 1;
}
@ -311,7 +315,7 @@ pub(crate) fn run_tests(
doctest,
scraped_test,
opts.clone(),
rustdoc_test_options.clone(),
Arc::clone(&rustdoc_options),
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
// 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.
@ -642,12 +646,11 @@ fn make_maybe_absolute_path(path: PathBuf) -> PathBuf {
}
struct IndividualTestOptions {
outdir: DirState,
test_id: String,
path: PathBuf,
}
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 mut path = path.clone();
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"))
};
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.
struct ScrapedDoctest {
pub(crate) struct ScrapedDoctest {
filename: FileName,
line: usize,
logical_path: Vec<String>,
langstr: LangString,
text: String,
name: String,
@ -692,7 +694,7 @@ fn new(
let name =
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 {
self.langstr.edition.unwrap_or(opts.edition)
@ -701,6 +703,19 @@ fn edition(&self, opts: &RustdocOptions) -> Edition {
fn no_run(&self, opts: &RustdocOptions) -> bool {
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 {
@ -757,7 +772,7 @@ fn add_test(&mut self, scraped_test: ScrapedDoctest) {
let edition = scraped_test.edition(&self.rustdoc_options);
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
|| scraped_test.langstr.test_harness
|| self.rustdoc_options.nocapture
@ -784,7 +799,7 @@ fn generate_test_desc_and_fn(
test,
scraped_test,
self.opts.clone(),
self.rustdoc_options.clone(),
Arc::clone(&self.rustdoc_options),
self.unused_extern_reports.clone(),
)
}
@ -794,32 +809,20 @@ fn generate_test_desc_and_fn(
test: DocTest,
scraped_test: ScrapedDoctest,
opts: GlobalTestOptions,
rustdoc_options: IndividualTestOptions,
rustdoc_options: Arc<RustdocOptions>,
unused_externs: Arc<Mutex<Vec<UnusedExterns>>>,
) -> test::TestDescAndFn {
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 {
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);
debug!("creating test {}: {}", scraped_test.name, scraped_test.text);
test::TestDescAndFn {
desc: test::TestDesc {
name: test::DynTestName(name),
name: test::DynTestName(scraped_test.name.clone()),
ignore: match scraped_test.langstr.ignore {
Ignore::All => true,
Ignore::None => false,

View File

@ -1,6 +1,7 @@
//! Doctest functionality used only for doctests in `.md` Markdown files.
use std::fs::read_to_string;
use std::sync::{Arc, Mutex};
use rustc_span::FileName;
use tempfile::tempdir;
@ -114,6 +115,16 @@ pub(crate) fn test(options: Options) -> Result<(), String> {
let mut collector = CreateRunnableDoctests::new(options.clone(), opts);
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(())
}

View File

@ -2,13 +2,12 @@
use rustc_span::edition::Edition;
use std::fmt::Write;
use std::sync::{Arc, Mutex};
use crate::doctest::{
run_test, DirState, DocTest, GlobalTestOptions, IndividualTestOptions, RunnableDoctest,
RustdocOptions, ScrapedDoctest, TestFailure, UnusedExterns,
run_test, DocTest, GlobalTestOptions, IndividualTestOptions, RunnableDoctest, RustdocOptions,
ScrapedDoctest, TestFailure, UnusedExterns,
};
use crate::html::markdown::LangString;
use crate::html::markdown::{Ignore, LangString};
/// Convenient type to merge compatible doctests into one.
pub(crate) struct DocTestRunner {
@ -17,7 +16,6 @@ pub(crate) struct DocTestRunner {
output: String,
supports_color: bool,
nb_tests: usize,
doctests: Vec<DocTest>,
}
impl DocTestRunner {
@ -28,12 +26,21 @@ pub(crate) fn new() -> Self {
output: String::new(),
supports_color: true,
nb_tests: 0,
doctests: Vec::with_capacity(10),
}
}
pub(crate) fn add_test(&mut self, doctest: &DocTest, scraped_test: &ScrapedDoctest) {
if !doctest.ignore {
pub(crate) fn add_test(
&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') {
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!(
"{}::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.nb_tests += 1;
self.doctests.push(doctest);
}
pub(crate) fn run_tests(
@ -56,9 +68,7 @@ pub(crate) fn run_tests(
edition: Edition,
opts: &GlobalTestOptions,
test_args: &[String],
outdir: &Arc<DirState>,
rustdoc_options: &RustdocOptions,
unused_externs: Arc<Mutex<Vec<UnusedExterns>>>,
) -> Result<bool, ()> {
let mut code = "\
#![allow(unused_extern_crates)]
@ -73,7 +83,19 @@ pub(crate) fn run_tests(
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");
let test_args =
@ -91,7 +113,6 @@ fn main() {{
ids = self.ids,
)
.expect("failed to generate test code");
// let out_dir = build_test_dir(outdir, true, "");
let runnable_test = RunnableDoctest {
full_test_code: code,
full_test_line_offset: 0,
@ -102,7 +123,8 @@ fn main() {{
edition,
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()) }
}
}
@ -111,12 +133,13 @@ fn main() {{
fn generate_mergeable_doctest(
doctest: &DocTest,
scraped_test: &ScrapedDoctest,
ignore: bool,
id: usize,
output: &mut String,
) -> String {
let test_id = format!("__doctest_{id}");
if doctest.ignore {
if ignore {
// We generate nothing else.
writeln!(output, "mod {test_id} {{\n").unwrap();
} else {
@ -166,8 +189,7 @@ fn main() {returns_result} {{
}};
}}",
test_name = scraped_test.name,
ignore = scraped_test.langstr.ignore,
file = scraped_test.file,
file = scraped_test.path(),
line = scraped_test.line,
no_run = scraped_test.langstr.no_run,
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
// 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>(())"
} else {
"self::main()"

View File

@ -10,9 +10,10 @@ fn make_test(
opts: &GlobalTestOptions,
test_id: Option<&str>,
) -> (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) =
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)
}