Split standalone and mergeable doctests

This commit is contained in:
Guillaume Gomez 2024-06-08 22:55:52 +02:00
parent 39f029a852
commit 96051f20e2
6 changed files with 441 additions and 155 deletions

View File

@ -1,5 +1,6 @@
mod make;
mod markdown;
mod runner;
mod rust;
use std::fs::File;
@ -164,40 +165,54 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, options: RustdocOptions) -> Result<()
let args_path = temp_dir.path().join("rustdoc-cfgs");
crate::wrap_return(dcx, generate_args_file(&args_path, &options))?;
// FIXME: use mergeable tests!
let (standalone_tests, unused_extern_reports, compiling_test_count) =
interface::run_compiler(config, |compiler| {
compiler.enter(|queries| {
let collector = queries.global_ctxt()?.enter(|tcx| {
let crate_name = tcx.crate_name(LOCAL_CRATE).to_string();
let crate_attrs = tcx.hir().attrs(CRATE_HIR_ID);
let opts = scrape_test_config(crate_name, crate_attrs, args_path);
let enable_per_target_ignores = options.enable_per_target_ignores;
let CreateRunnableDoctests {
standalone_tests,
mergeable_tests,
rustdoc_options,
opts,
unused_extern_reports,
compiling_test_count,
..
} = interface::run_compiler(config, |compiler| {
compiler.enter(|queries| {
let collector = queries.global_ctxt()?.enter(|tcx| {
let crate_name = tcx.crate_name(LOCAL_CRATE).to_string();
let crate_attrs = tcx.hir().attrs(CRATE_HIR_ID);
let opts = scrape_test_config(crate_name, crate_attrs, args_path);
let enable_per_target_ignores = options.enable_per_target_ignores;
let mut collector = CreateRunnableDoctests::new(options, opts);
let hir_collector = HirCollector::new(
&compiler.sess,
tcx.hir(),
ErrorCodes::from(compiler.sess.opts.unstable_features.is_nightly_build()),
enable_per_target_ignores,
tcx,
);
let tests = hir_collector.collect_crate();
tests.into_iter().for_each(|t| collector.add_test(t));
let mut collector = CreateRunnableDoctests::new(options, opts);
let hir_collector = HirCollector::new(
&compiler.sess,
tcx.hir(),
ErrorCodes::from(compiler.sess.opts.unstable_features.is_nightly_build()),
enable_per_target_ignores,
tcx,
);
let tests = hir_collector.collect_crate();
tests.into_iter().for_each(|t| collector.add_test(t));
collector
});
if compiler.sess.dcx().has_errors().is_some() {
FatalError.raise();
}
collector
});
if compiler.sess.dcx().has_errors().is_some() {
FatalError.raise();
}
let unused_extern_reports = collector.unused_extern_reports.clone();
let compiling_test_count = collector.compiling_test_count.load(Ordering::SeqCst);
Ok((collector.standalone_tests, unused_extern_reports, compiling_test_count))
})
})?;
Ok(collector)
})
})?;
run_tests(test_args, nocapture, standalone_tests);
run_tests(
test_args,
nocapture,
opts,
rustdoc_options,
&unused_extern_reports,
standalone_tests,
mergeable_tests,
);
let compiling_test_count = compiling_test_count.load(Ordering::SeqCst);
// Collect and warn about unused externs, but only if we've gotten
// reports for each doctest
@ -243,14 +258,74 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, options: RustdocOptions) -> Result<()
pub(crate) fn run_tests(
mut test_args: Vec<String>,
nocapture: bool,
mut tests: Vec<test::TestDescAndFn>,
opts: GlobalTestOptions,
rustdoc_options: RustdocOptions,
unused_extern_reports: &Arc<Mutex<Vec<UnusedExterns>>>,
mut standalone_tests: Vec<test::TestDescAndFn>,
mut mergeable_tests: FxHashMap<Edition, Vec<(DocTest, ScrapedDoctest)>>,
) {
test_args.insert(0, "rustdoctest".to_string());
if nocapture {
test_args.push("--nocapture".to_string());
}
tests.sort_by(|a, b| a.desc.name.as_slice().cmp(&b.desc.name.as_slice()));
test::test_main(&test_args, tests, None);
let mut nb_errors = 0;
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"),
);
for (doctest, scraped_test) in &doctests {
tests_runner.add_test(doctest, scraped_test);
}
if let Ok(success) =
tests_runner.run_tests(rustdoc_test_options, edition, &opts, &test_args, &outdir)
{
if !success {
nb_errors += 1;
}
continue;
} else {
// We failed to compile all compatible tests as one so we push them into the
// `standalone_tests` doctests.
debug!("Failed to compile compatible doctests for edition {} all at once", edition);
for (doctest, scraped_test) in doctests {
doctest.generate_unique_doctest(
&scraped_test.text,
scraped_test.langstr.test_harness,
&opts,
Some(&opts.crate_name),
);
standalone_tests.push(generate_test_desc_and_fn(
doctest,
scraped_test,
opts.clone(),
rustdoc_test_options.clone(),
unused_extern_reports.clone(),
));
}
}
}
if !standalone_tests.is_empty() {
standalone_tests.sort_by(|a, b| a.desc.name.as_slice().cmp(&b.desc.name.as_slice()));
test::test_main(&test_args, standalone_tests, None);
}
if nb_errors != 0 {
// libtest::ERROR_EXIT_CODE is not public but it's the same value.
std::process::exit(101);
}
}
// Look for `#![doc(test(no_crate_inject))]`, used by crates in the std facade.
@ -365,7 +440,10 @@ struct RunnableDoctest {
full_test_line_offset: usize,
test_opts: IndividualTestOptions,
global_opts: GlobalTestOptions,
scraped_test: ScrapedDoctest,
langstr: LangString,
line: usize,
edition: Edition,
no_run: bool,
}
fn run_test(
@ -374,8 +452,7 @@ fn run_test(
supports_color: bool,
report_unused_externs: impl Fn(UnusedExterns),
) -> Result<(), TestFailure> {
let scraped_test = &doctest.scraped_test;
let langstr = &scraped_test.langstr;
let langstr = &doctest.langstr;
// Make sure we emit well-formed executable names for our target.
let rust_out = add_exe_suffix("rust_out".to_owned(), &rustdoc_options.target);
let output_file = doctest.test_opts.outdir.path().join(rust_out);
@ -392,11 +469,11 @@ fn run_test(
compiler.arg(format!("--sysroot={}", sysroot.display()));
}
compiler.arg("--edition").arg(&scraped_test.edition(rustdoc_options).to_string());
compiler.arg("--edition").arg(&doctest.edition.to_string());
compiler.env("UNSTABLE_RUSTDOC_TEST_PATH", &doctest.test_opts.path);
compiler.env(
"UNSTABLE_RUSTDOC_TEST_LINE",
format!("{}", scraped_test.line as isize - doctest.full_test_line_offset as isize),
format!("{}", doctest.line as isize - doctest.full_test_line_offset as isize),
);
compiler.arg("-o").arg(&output_file);
if langstr.test_harness {
@ -409,10 +486,7 @@ fn run_test(
compiler.arg("-Z").arg("unstable-options");
}
if scraped_test.no_run(rustdoc_options)
&& !langstr.compile_fail
&& rustdoc_options.persist_doctests.is_none()
{
if doctest.no_run && !langstr.compile_fail && rustdoc_options.persist_doctests.is_none() {
// FIXME: why does this code check if it *shouldn't* persist doctests
// -- shouldn't it be the negation?
compiler.arg("--emit=metadata");
@ -493,8 +567,7 @@ fn drop(&mut self) {
// We used to check if the output contained "error[{}]: " but since we added the
// colored output, we can't anymore because of the color escape characters before
// the ":".
let missing_codes: Vec<String> = scraped_test
.langstr
let missing_codes: Vec<String> = langstr
.error_codes
.iter()
.filter(|err| !out.contains(&format!("error[{err}]")))
@ -511,7 +584,7 @@ fn drop(&mut self) {
}
}
if scraped_test.no_run(rustdoc_options) {
if doctest.no_run {
return Ok(());
}
@ -600,9 +673,27 @@ struct ScrapedDoctest {
logical_path: Vec<String>,
langstr: LangString,
text: String,
name: String,
}
impl ScrapedDoctest {
fn new(
filename: FileName,
line: usize,
logical_path: Vec<String>,
langstr: LangString,
text: String,
) -> Self {
let mut item_path = logical_path.join("::");
item_path.retain(|c| c != ' ');
if !item_path.is_empty() {
item_path.push(' ');
}
let name =
format!("{} - {item_path}(line {line})", filename.prefer_remapped_unconditionaly());
Self { filename, line, logical_path, langstr, text, name }
}
fn edition(&self, opts: &RustdocOptions) -> Edition {
self.langstr.edition.unwrap_or(opts.edition)
}
@ -641,60 +732,7 @@ fn new(rustdoc_options: RustdocOptions, opts: GlobalTestOptions) -> CreateRunnab
}
}
fn generate_name(&self, filename: &FileName, line: usize, logical_path: &[String]) -> String {
let mut item_path = logical_path.join("::");
item_path.retain(|c| c != ' ');
if !item_path.is_empty() {
item_path.push(' ');
}
format!("{} - {item_path}(line {line})", filename.prefer_remapped_unconditionaly())
}
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);
let is_standalone = scraped_test.langstr.compile_fail
|| scraped_test.langstr.test_harness
|| self.rustdoc_options.nocapture
|| self.rustdoc_options.test_args.iter().any(|arg| arg == "--show-output")
|| doctest.crate_attrs.contains("#![no_std]");
if is_standalone {
let test_desc = self.generate_test_desc_and_fn(doctest, scraped_test);
self.standalone_tests.push(test_desc);
} else {
self.mergeable_tests.entry(edition).or_default().push((doctest, scraped_test));
}
}
fn generate_test_desc_and_fn(
&mut self,
test: DocTest,
scraped_test: ScrapedDoctest,
) -> test::TestDescAndFn {
let name = self.generate_name(
&scraped_test.filename,
scraped_test.line,
&scraped_test.logical_path,
);
let opts = self.opts.clone();
let target_str = self.rustdoc_options.target.to_string();
let unused_externs = self.unused_extern_reports.clone();
if !scraped_test.langstr.compile_fail {
self.compiling_test_count.fetch_add(1, Ordering::SeqCst);
}
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"),
};
// For example `module/file.rs` would become `module_file_rs`
let file = scraped_test
.filename
@ -717,42 +755,99 @@ fn generate_test_desc_and_fn(
},
);
let rustdoc_options = self.rustdoc_options.clone();
let rustdoc_test_options = IndividualTestOptions::new(&self.rustdoc_options, test_id, path);
debug!("creating test {name}: {}", scraped_test.text);
test::TestDescAndFn {
desc: test::TestDesc {
name: test::DynTestName(name),
ignore: match scraped_test.langstr.ignore {
Ignore::All => true,
Ignore::None => false,
Ignore::Some(ref ignores) => ignores.iter().any(|s| target_str.contains(s)),
},
ignore_message: None,
source_file: "",
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 0,
// compiler failures are test failures
should_panic: test::ShouldPanic::No,
compile_fail: scraped_test.langstr.compile_fail,
no_run: scraped_test.no_run(&rustdoc_options),
test_type: test::TestType::DocTest,
},
testfn: test::DynTestFn(Box::new(move || {
doctest_run_fn(
rustdoc_test_options,
opts,
test,
scraped_test,
rustdoc_options,
unused_externs,
)
})),
let edition = scraped_test.edition(&self.rustdoc_options);
let doctest =
DocTest::new(&scraped_test.text, Some(&self.opts.crate_name), edition, test_id);
let is_standalone = scraped_test.langstr.compile_fail
|| scraped_test.langstr.test_harness
|| self.rustdoc_options.nocapture
|| self.rustdoc_options.test_args.iter().any(|arg| arg == "--show-output")
|| doctest.crate_attrs.contains("#![no_std]");
if is_standalone {
let test_desc = self.generate_test_desc_and_fn(doctest, scraped_test);
self.standalone_tests.push(test_desc);
} else {
self.mergeable_tests.entry(edition).or_default().push((doctest, scraped_test));
}
}
fn generate_test_desc_and_fn(
&mut self,
test: DocTest,
scraped_test: ScrapedDoctest,
) -> test::TestDescAndFn {
if !scraped_test.langstr.compile_fail {
self.compiling_test_count.fetch_add(1, Ordering::SeqCst);
}
generate_test_desc_and_fn(
test,
scraped_test,
self.opts.clone(),
self.rustdoc_options.clone(),
self.unused_extern_reports.clone(),
)
}
}
fn generate_test_desc_and_fn(
test: DocTest,
scraped_test: ScrapedDoctest,
opts: GlobalTestOptions,
rustdoc_options: IndividualTestOptions,
unused_externs: Arc<Mutex<Vec<UnusedExterns>>>,
) -> test::TestDescAndFn {
let target_str = rustdoc_options.target.to_string();
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);
test::TestDescAndFn {
desc: test::TestDesc {
name: test::DynTestName(name),
ignore: match scraped_test.langstr.ignore {
Ignore::All => true,
Ignore::None => false,
Ignore::Some(ref ignores) => ignores.iter().any(|s| target_str.contains(s)),
},
ignore_message: None,
source_file: "",
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 0,
// compiler failures are test failures
should_panic: test::ShouldPanic::No,
compile_fail: scraped_test.langstr.compile_fail,
no_run: scraped_test.no_run(&rustdoc_options),
test_type: test::TestType::DocTest,
},
testfn: test::DynTestFn(Box::new(move || {
doctest_run_fn(
rustdoc_test_options,
opts,
test,
scraped_test,
rustdoc_options,
unused_externs,
)
})),
}
}
fn doctest_run_fn(
@ -770,7 +865,6 @@ fn doctest_run_fn(
&scraped_test.text,
scraped_test.langstr.test_harness,
&global_opts,
Some(&test_opts.test_id),
Some(&global_opts.crate_name),
);
let runnable_test = RunnableDoctest {
@ -778,7 +872,10 @@ fn doctest_run_fn(
full_test_line_offset,
test_opts,
global_opts,
scraped_test,
langstr: scraped_test.langstr.clone(),
line: scraped_test.line,
edition: scraped_test.edition(&rustdoc_options),
no_run: scraped_test.no_run(&rustdoc_options),
};
let res =
run_test(runnable_test, &rustdoc_options, doctest.supports_color, report_unused_externs);

View File

@ -24,10 +24,17 @@ pub(crate) struct DocTest {
pub(crate) crate_attrs: String,
pub(crate) crates: String,
pub(crate) everything_else: String,
pub(crate) test_id: Option<String>,
}
impl DocTest {
pub(crate) fn new(source: &str, crate_name: Option<&str>, edition: Edition) -> Self {
pub(crate) fn new(
source: &str,
crate_name: Option<&str>,
edition: Edition,
// If `test_id` is `None`, it means we're generating code for a code example "run" link.
test_id: Option<String>,
) -> Self {
let (crate_attrs, everything_else, crates) = partition_source(source, edition);
let mut supports_color = false;
@ -45,6 +52,7 @@ pub(crate) fn new(source: &str, crate_name: Option<&str>, edition: Edition) -> S
crates,
everything_else,
already_has_extern_crate: false,
test_id,
};
};
Self {
@ -54,6 +62,7 @@ pub(crate) fn new(source: &str, crate_name: Option<&str>, edition: Edition) -> S
crates,
everything_else,
already_has_extern_crate,
test_id,
}
}
@ -64,8 +73,6 @@ pub(crate) fn generate_unique_doctest(
test_code: &str,
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;
@ -118,12 +125,12 @@ pub(crate) fn generate_unique_doctest(
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 {
let inner_fn_name = if let Some(ref test_id) = self.test_id {
format!("_doctest_main_{test_id}")
} else {
"_inner".into()
};
let inner_attr = if test_id.is_some() { "#[allow(non_snake_case)] " } else { "" };
let inner_attr = if self.test_id.is_some() { "#[allow(non_snake_case)] " } else { "" };
let (main_pre, main_post) = if returns_result {
(
format!(
@ -131,7 +138,7 @@ pub(crate) fn generate_unique_doctest(
),
format!("\n}} {inner_fn_name}().unwrap() }}"),
)
} else if test_id.is_some() {
} else if self.test_id.is_some() {
(
format!("fn main() {{ {inner_attr}fn {inner_fn_name}() {{\n",),
format!("\n}} {inner_fn_name}() }}"),

View File

@ -22,13 +22,7 @@ fn visit_test(&mut self, test: String, config: LangString, rel_line: MdRelLine)
let filename = self.filename.clone();
// First line of Markdown is line 1.
let line = 1 + rel_line.offset();
self.tests.push(ScrapedDoctest {
filename,
line,
logical_path: self.cur_path.clone(),
langstr: config,
text: test,
});
self.tests.push(ScrapedDoctest::new(filename, line, self.cur_path.clone(), config, test));
}
fn visit_header(&mut self, name: &str, level: u32) {

View File

@ -0,0 +1,188 @@
use rustc_data_structures::fx::FxHashSet;
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,
};
use crate::html::markdown::LangString;
/// Convenient type to merge compatible doctests into one.
pub(crate) struct DocTestRunner {
crate_attrs: FxHashSet<String>,
ids: String,
output: String,
supports_color: bool,
nb_tests: usize,
doctests: Vec<DocTest>,
}
impl DocTestRunner {
pub(crate) fn new() -> Self {
Self {
crate_attrs: FxHashSet::default(),
ids: String::new(),
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 {
for line in doctest.crate_attrs.split('\n') {
self.crate_attrs.insert(line.to_string());
}
}
if !self.ids.is_empty() {
self.ids.push(',');
}
self.ids.push_str(&format!(
"{}::TEST",
generate_mergeable_doctest(doctest, scraped_test, 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(
&mut self,
test_options: IndividualTestOptions,
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)]
#![allow(internal_features)]
#![feature(test)]
#![feature(rustc_attrs)]
#![feature(coverage_attribute)]\n"
.to_string();
for crate_attr in &self.crate_attrs {
code.push_str(crate_attr);
code.push('\n');
}
DocTest::push_attrs(&mut code, opts, &mut 0);
code.push_str("extern crate test;\n");
let test_args =
test_args.iter().map(|arg| format!("{arg:?}.to_string(),")).collect::<String>();
write!(
code,
"\
{output}
#[rustc_main]
#[coverage(off)]
fn main() {{
test::test_main(&[{test_args}], vec![{ids}], None);
}}",
output = self.output,
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,
test_opts: test_options,
global_opts: opts.clone(),
langstr: LangString::default(),
line: 0,
edition,
no_run: false,
};
let ret = run_test(runnable_test, rustdoc_options, self.supports_color, unused_externs);
if let Err(TestFailure::CompileError) = ret { Err(()) } else { Ok(ret.is_ok()) }
}
}
/// Push new doctest content into `output`. Returns the test ID for this doctest.
fn generate_mergeable_doctest(
doctest: &DocTest,
scraped_test: &ScrapedDoctest,
id: usize,
output: &mut String,
) -> String {
let test_id = format!("__doctest_{id}");
if doctest.ignore {
// We generate nothing else.
writeln!(output, "mod {test_id} {{\n").unwrap();
} else {
writeln!(output, "mod {test_id} {{\n{}", doctest.crates).unwrap();
if doctest.main_fn_span.is_some() {
output.push_str(&doctest.everything_else);
} else {
let returns_result = if doctest.everything_else.trim_end().ends_with("(())") {
"-> Result<(), impl core::fmt::Debug>"
} else {
""
};
write!(
output,
"\
fn main() {returns_result} {{
{}
}}",
doctest.everything_else
)
.unwrap();
}
}
writeln!(
output,
"
#[rustc_test_marker = {test_name:?}]
pub const TEST: test::TestDescAndFn = test::TestDescAndFn {{
desc: test::TestDesc {{
name: test::StaticTestName({test_name:?}),
ignore: {ignore},
ignore_message: None,
source_file: {file:?},
start_line: {line},
start_col: 0,
end_line: 0,
end_col: 0,
compile_fail: false,
no_run: {no_run},
should_panic: test::ShouldPanic::{should_panic},
test_type: test::TestType::UnitTest,
}},
testfn: test::StaticTestFn(
#[coverage(off)]
|| test::assert_test_result({runner}),
)
}};
}}",
test_name = scraped_test.name,
ignore = scraped_test.langstr.ignore,
file = scraped_test.file,
line = scraped_test.line,
no_run = scraped_test.langstr.no_run,
should_panic = if !scraped_test.langstr.no_run && scraped_test.langstr.should_panic {
"Yes"
} else {
"No"
},
// 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 {
"Ok::<(), String>(())"
} else {
"self::main()"
},
)
.unwrap();
test_id
}

View File

@ -51,13 +51,13 @@ fn get_base_line(&self) -> usize {
impl DoctestVisitor for RustCollector {
fn visit_test(&mut self, test: String, config: LangString, rel_line: MdRelLine) {
let line = self.get_base_line() + rel_line.offset();
self.tests.push(ScrapedDoctest {
filename: self.get_filename(),
self.tests.push(ScrapedDoctest::new(
self.get_filename(),
line,
logical_path: self.cur_path.clone(),
langstr: config,
text: test,
});
self.cur_path.clone(),
config,
test,
));
}
fn visit_header(&mut self, _name: &str, _level: u32) {}

View File

@ -297,8 +297,8 @@ fn next(&mut self) -> Option<Self::Item> {
attrs: vec![],
args_file: PathBuf::new(),
};
let doctest = doctest::DocTest::new(&test, krate, edition);
let (test, _) = doctest.generate_unique_doctest(&test, false, &opts, None, krate);
let doctest = doctest::DocTest::new(&test, krate, edition, None);
let (test, _) = doctest.generate_unique_doctest(&test, false, &opts, krate);
let channel = if test.contains("#![feature(") { "&amp;version=nightly" } else { "" };
let test_escaped = small_url_encode(test);