Split standalone and mergeable doctests
This commit is contained in:
parent
39f029a852
commit
96051f20e2
@ -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);
|
||||
|
@ -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}() }}"),
|
||||
|
@ -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) {
|
||||
|
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 {
|
||||
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) {}
|
||||
|
@ -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(") { "&version=nightly" } else { "" };
|
||||
|
||||
let test_escaped = small_url_encode(test);
|
||||
|
Loading…
Reference in New Issue
Block a user