Split standalone and mergeable doctests
This commit is contained in:
parent
39f029a852
commit
96051f20e2
@ -1,5 +1,6 @@
|
|||||||
mod make;
|
mod make;
|
||||||
mod markdown;
|
mod markdown;
|
||||||
|
mod runner;
|
||||||
mod rust;
|
mod rust;
|
||||||
|
|
||||||
use std::fs::File;
|
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");
|
let args_path = temp_dir.path().join("rustdoc-cfgs");
|
||||||
crate::wrap_return(dcx, generate_args_file(&args_path, &options))?;
|
crate::wrap_return(dcx, generate_args_file(&args_path, &options))?;
|
||||||
|
|
||||||
// FIXME: use mergeable tests!
|
let CreateRunnableDoctests {
|
||||||
let (standalone_tests, unused_extern_reports, compiling_test_count) =
|
standalone_tests,
|
||||||
interface::run_compiler(config, |compiler| {
|
mergeable_tests,
|
||||||
compiler.enter(|queries| {
|
rustdoc_options,
|
||||||
let collector = queries.global_ctxt()?.enter(|tcx| {
|
opts,
|
||||||
let crate_name = tcx.crate_name(LOCAL_CRATE).to_string();
|
unused_extern_reports,
|
||||||
let crate_attrs = tcx.hir().attrs(CRATE_HIR_ID);
|
compiling_test_count,
|
||||||
let opts = scrape_test_config(crate_name, crate_attrs, args_path);
|
..
|
||||||
let enable_per_target_ignores = options.enable_per_target_ignores;
|
} = 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 mut collector = CreateRunnableDoctests::new(options, opts);
|
||||||
let hir_collector = HirCollector::new(
|
let hir_collector = HirCollector::new(
|
||||||
&compiler.sess,
|
&compiler.sess,
|
||||||
tcx.hir(),
|
tcx.hir(),
|
||||||
ErrorCodes::from(compiler.sess.opts.unstable_features.is_nightly_build()),
|
ErrorCodes::from(compiler.sess.opts.unstable_features.is_nightly_build()),
|
||||||
enable_per_target_ignores,
|
enable_per_target_ignores,
|
||||||
tcx,
|
tcx,
|
||||||
);
|
);
|
||||||
let tests = hir_collector.collect_crate();
|
let tests = hir_collector.collect_crate();
|
||||||
tests.into_iter().for_each(|t| collector.add_test(t));
|
tests.into_iter().for_each(|t| collector.add_test(t));
|
||||||
|
|
||||||
collector
|
collector
|
||||||
});
|
});
|
||||||
if compiler.sess.dcx().has_errors().is_some() {
|
if compiler.sess.dcx().has_errors().is_some() {
|
||||||
FatalError.raise();
|
FatalError.raise();
|
||||||
}
|
}
|
||||||
|
|
||||||
let unused_extern_reports = collector.unused_extern_reports.clone();
|
Ok(collector)
|
||||||
let compiling_test_count = collector.compiling_test_count.load(Ordering::SeqCst);
|
})
|
||||||
Ok((collector.standalone_tests, unused_extern_reports, compiling_test_count))
|
})?;
|
||||||
})
|
|
||||||
})?;
|
|
||||||
|
|
||||||
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
|
// Collect and warn about unused externs, but only if we've gotten
|
||||||
// reports for each doctest
|
// reports for each doctest
|
||||||
@ -243,14 +258,74 @@ pub(crate) fn run(dcx: DiagCtxtHandle<'_>, options: RustdocOptions) -> Result<()
|
|||||||
pub(crate) fn run_tests(
|
pub(crate) fn run_tests(
|
||||||
mut test_args: Vec<String>,
|
mut test_args: Vec<String>,
|
||||||
nocapture: bool,
|
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());
|
test_args.insert(0, "rustdoctest".to_string());
|
||||||
if nocapture {
|
if nocapture {
|
||||||
test_args.push("--nocapture".to_string());
|
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.
|
// 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,
|
full_test_line_offset: usize,
|
||||||
test_opts: IndividualTestOptions,
|
test_opts: IndividualTestOptions,
|
||||||
global_opts: GlobalTestOptions,
|
global_opts: GlobalTestOptions,
|
||||||
scraped_test: ScrapedDoctest,
|
langstr: LangString,
|
||||||
|
line: usize,
|
||||||
|
edition: Edition,
|
||||||
|
no_run: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_test(
|
fn run_test(
|
||||||
@ -374,8 +452,7 @@ fn run_test(
|
|||||||
supports_color: bool,
|
supports_color: bool,
|
||||||
report_unused_externs: impl Fn(UnusedExterns),
|
report_unused_externs: impl Fn(UnusedExterns),
|
||||||
) -> Result<(), TestFailure> {
|
) -> Result<(), TestFailure> {
|
||||||
let scraped_test = &doctest.scraped_test;
|
let langstr = &doctest.langstr;
|
||||||
let langstr = &scraped_test.langstr;
|
|
||||||
// Make sure we emit well-formed executable names for our target.
|
// 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 rust_out = add_exe_suffix("rust_out".to_owned(), &rustdoc_options.target);
|
||||||
let output_file = doctest.test_opts.outdir.path().join(rust_out);
|
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(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_PATH", &doctest.test_opts.path);
|
||||||
compiler.env(
|
compiler.env(
|
||||||
"UNSTABLE_RUSTDOC_TEST_LINE",
|
"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);
|
compiler.arg("-o").arg(&output_file);
|
||||||
if langstr.test_harness {
|
if langstr.test_harness {
|
||||||
@ -409,10 +486,7 @@ fn run_test(
|
|||||||
compiler.arg("-Z").arg("unstable-options");
|
compiler.arg("-Z").arg("unstable-options");
|
||||||
}
|
}
|
||||||
|
|
||||||
if scraped_test.no_run(rustdoc_options)
|
if doctest.no_run && !langstr.compile_fail && rustdoc_options.persist_doctests.is_none() {
|
||||||
&& !langstr.compile_fail
|
|
||||||
&& rustdoc_options.persist_doctests.is_none()
|
|
||||||
{
|
|
||||||
// FIXME: why does this code check if it *shouldn't* persist doctests
|
// FIXME: why does this code check if it *shouldn't* persist doctests
|
||||||
// -- shouldn't it be the negation?
|
// -- shouldn't it be the negation?
|
||||||
compiler.arg("--emit=metadata");
|
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
|
// 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
|
// colored output, we can't anymore because of the color escape characters before
|
||||||
// the ":".
|
// the ":".
|
||||||
let missing_codes: Vec<String> = scraped_test
|
let missing_codes: Vec<String> = langstr
|
||||||
.langstr
|
|
||||||
.error_codes
|
.error_codes
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|err| !out.contains(&format!("error[{err}]")))
|
.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(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -600,9 +673,27 @@ struct ScrapedDoctest {
|
|||||||
logical_path: Vec<String>,
|
logical_path: Vec<String>,
|
||||||
langstr: LangString,
|
langstr: LangString,
|
||||||
text: String,
|
text: String,
|
||||||
|
name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScrapedDoctest {
|
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 {
|
fn edition(&self, opts: &RustdocOptions) -> Edition {
|
||||||
self.langstr.edition.unwrap_or(opts.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) {
|
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`
|
// For example `module/file.rs` would become `module_file_rs`
|
||||||
let file = scraped_test
|
let file = scraped_test
|
||||||
.filename
|
.filename
|
||||||
@ -717,42 +755,99 @@ fn generate_test_desc_and_fn(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let rustdoc_options = self.rustdoc_options.clone();
|
let edition = scraped_test.edition(&self.rustdoc_options);
|
||||||
let rustdoc_test_options = IndividualTestOptions::new(&self.rustdoc_options, test_id, path);
|
let doctest =
|
||||||
|
DocTest::new(&scraped_test.text, Some(&self.opts.crate_name), edition, test_id);
|
||||||
debug!("creating test {name}: {}", scraped_test.text);
|
let is_standalone = scraped_test.langstr.compile_fail
|
||||||
test::TestDescAndFn {
|
|| scraped_test.langstr.test_harness
|
||||||
desc: test::TestDesc {
|
|| self.rustdoc_options.nocapture
|
||||||
name: test::DynTestName(name),
|
|| self.rustdoc_options.test_args.iter().any(|arg| arg == "--show-output")
|
||||||
ignore: match scraped_test.langstr.ignore {
|
|| doctest.crate_attrs.contains("#![no_std]");
|
||||||
Ignore::All => true,
|
if is_standalone {
|
||||||
Ignore::None => false,
|
let test_desc = self.generate_test_desc_and_fn(doctest, scraped_test);
|
||||||
Ignore::Some(ref ignores) => ignores.iter().any(|s| target_str.contains(s)),
|
self.standalone_tests.push(test_desc);
|
||||||
},
|
} else {
|
||||||
ignore_message: None,
|
self.mergeable_tests.entry(edition).or_default().push((doctest, scraped_test));
|
||||||
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 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(
|
fn doctest_run_fn(
|
||||||
@ -770,7 +865,6 @@ fn doctest_run_fn(
|
|||||||
&scraped_test.text,
|
&scraped_test.text,
|
||||||
scraped_test.langstr.test_harness,
|
scraped_test.langstr.test_harness,
|
||||||
&global_opts,
|
&global_opts,
|
||||||
Some(&test_opts.test_id),
|
|
||||||
Some(&global_opts.crate_name),
|
Some(&global_opts.crate_name),
|
||||||
);
|
);
|
||||||
let runnable_test = RunnableDoctest {
|
let runnable_test = RunnableDoctest {
|
||||||
@ -778,7 +872,10 @@ fn doctest_run_fn(
|
|||||||
full_test_line_offset,
|
full_test_line_offset,
|
||||||
test_opts,
|
test_opts,
|
||||||
global_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 =
|
let res =
|
||||||
run_test(runnable_test, &rustdoc_options, doctest.supports_color, report_unused_externs);
|
run_test(runnable_test, &rustdoc_options, doctest.supports_color, report_unused_externs);
|
||||||
|
@ -24,10 +24,17 @@ pub(crate) struct DocTest {
|
|||||||
pub(crate) crate_attrs: String,
|
pub(crate) crate_attrs: String,
|
||||||
pub(crate) crates: String,
|
pub(crate) crates: String,
|
||||||
pub(crate) everything_else: String,
|
pub(crate) everything_else: String,
|
||||||
|
pub(crate) test_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DocTest {
|
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 (crate_attrs, everything_else, crates) = partition_source(source, edition);
|
||||||
let mut supports_color = false;
|
let mut supports_color = false;
|
||||||
|
|
||||||
@ -45,6 +52,7 @@ pub(crate) fn new(source: &str, crate_name: Option<&str>, edition: Edition) -> S
|
|||||||
crates,
|
crates,
|
||||||
everything_else,
|
everything_else,
|
||||||
already_has_extern_crate: false,
|
already_has_extern_crate: false,
|
||||||
|
test_id,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
Self {
|
Self {
|
||||||
@ -54,6 +62,7 @@ pub(crate) fn new(source: &str, crate_name: Option<&str>, edition: Edition) -> S
|
|||||||
crates,
|
crates,
|
||||||
everything_else,
|
everything_else,
|
||||||
already_has_extern_crate,
|
already_has_extern_crate,
|
||||||
|
test_id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,8 +73,6 @@ pub(crate) fn generate_unique_doctest(
|
|||||||
test_code: &str,
|
test_code: &str,
|
||||||
dont_insert_main: bool,
|
dont_insert_main: bool,
|
||||||
opts: &GlobalTestOptions,
|
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>,
|
crate_name: Option<&str>,
|
||||||
) -> (String, usize) {
|
) -> (String, usize) {
|
||||||
let mut line_offset = 0;
|
let mut line_offset = 0;
|
||||||
@ -118,12 +125,12 @@ pub(crate) fn generate_unique_doctest(
|
|||||||
let returns_result = everything_else.ends_with("(())");
|
let returns_result = everything_else.ends_with("(())");
|
||||||
// Give each doctest main function a unique name.
|
// Give each doctest main function a unique name.
|
||||||
// This is for example needed for the tooling around `-C instrument-coverage`.
|
// 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}")
|
format!("_doctest_main_{test_id}")
|
||||||
} else {
|
} else {
|
||||||
"_inner".into()
|
"_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 {
|
let (main_pre, main_post) = if returns_result {
|
||||||
(
|
(
|
||||||
format!(
|
format!(
|
||||||
@ -131,7 +138,7 @@ pub(crate) fn generate_unique_doctest(
|
|||||||
),
|
),
|
||||||
format!("\n}} {inner_fn_name}().unwrap() }}"),
|
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!("fn main() {{ {inner_attr}fn {inner_fn_name}() {{\n",),
|
||||||
format!("\n}} {inner_fn_name}() }}"),
|
format!("\n}} {inner_fn_name}() }}"),
|
||||||
|
@ -22,13 +22,7 @@ fn visit_test(&mut self, test: String, config: LangString, rel_line: MdRelLine)
|
|||||||
let filename = self.filename.clone();
|
let filename = self.filename.clone();
|
||||||
// First line of Markdown is line 1.
|
// First line of Markdown is line 1.
|
||||||
let line = 1 + rel_line.offset();
|
let line = 1 + rel_line.offset();
|
||||||
self.tests.push(ScrapedDoctest {
|
self.tests.push(ScrapedDoctest::new(filename, line, self.cur_path.clone(), config, test));
|
||||||
filename,
|
|
||||||
line,
|
|
||||||
logical_path: self.cur_path.clone(),
|
|
||||||
langstr: config,
|
|
||||||
text: test,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visit_header(&mut self, name: &str, level: u32) {
|
fn visit_header(&mut self, name: &str, level: u32) {
|
||||||
|
188
src/librustdoc/doctest/runner.rs
Normal file
188
src/librustdoc/doctest/runner.rs
Normal 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
|
||||||
|
}
|
@ -51,13 +51,13 @@ fn get_base_line(&self) -> usize {
|
|||||||
impl DoctestVisitor for RustCollector {
|
impl DoctestVisitor for RustCollector {
|
||||||
fn visit_test(&mut self, test: String, config: LangString, rel_line: MdRelLine) {
|
fn visit_test(&mut self, test: String, config: LangString, rel_line: MdRelLine) {
|
||||||
let line = self.get_base_line() + rel_line.offset();
|
let line = self.get_base_line() + rel_line.offset();
|
||||||
self.tests.push(ScrapedDoctest {
|
self.tests.push(ScrapedDoctest::new(
|
||||||
filename: self.get_filename(),
|
self.get_filename(),
|
||||||
line,
|
line,
|
||||||
logical_path: self.cur_path.clone(),
|
self.cur_path.clone(),
|
||||||
langstr: config,
|
config,
|
||||||
text: test,
|
test,
|
||||||
});
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visit_header(&mut self, _name: &str, _level: u32) {}
|
fn visit_header(&mut self, _name: &str, _level: u32) {}
|
||||||
|
@ -297,8 +297,8 @@ fn next(&mut self) -> Option<Self::Item> {
|
|||||||
attrs: vec![],
|
attrs: vec![],
|
||||||
args_file: PathBuf::new(),
|
args_file: PathBuf::new(),
|
||||||
};
|
};
|
||||||
let doctest = doctest::DocTest::new(&test, krate, edition);
|
let doctest = doctest::DocTest::new(&test, krate, edition, None);
|
||||||
let (test, _) = doctest.generate_unique_doctest(&test, false, &opts, None, krate);
|
let (test, _) = doctest.generate_unique_doctest(&test, false, &opts, krate);
|
||||||
let channel = if test.contains("#![feature(") { "&version=nightly" } else { "" };
|
let channel = if test.contains("#![feature(") { "&version=nightly" } else { "" };
|
||||||
|
|
||||||
let test_escaped = small_url_encode(test);
|
let test_escaped = small_url_encode(test);
|
||||||
|
Loading…
Reference in New Issue
Block a user