blob: d5bc2a93fa80786c0172ea5c7dbb4d892734bc5d [file] [log] [blame] [edit]
mod make;
mod markdown;
mod runner;
mod rust;
use std::fs::File;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::{self, Command, Stdio};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::{panic, str};
pub(crate) use make::DocTestBuilder;
pub(crate) use markdown::test as test_markdown;
use rustc_ast as ast;
use rustc_data_structures::fx::{FxHashMap, FxIndexMap, FxIndexSet};
use rustc_errors::{ColorConfig, DiagCtxtHandle, ErrorGuaranteed, FatalError};
use rustc_hir::CRATE_HIR_ID;
use rustc_hir::def_id::LOCAL_CRATE;
use rustc_interface::interface;
use rustc_session::config::{self, CrateType, ErrorOutputType, Input};
use rustc_session::lint;
use rustc_span::FileName;
use rustc_span::edition::Edition;
use rustc_span::symbol::sym;
use rustc_target::spec::{Target, TargetTriple};
use tempfile::{Builder as TempFileBuilder, TempDir};
use tracing::debug;
use self::rust::HirCollector;
use crate::config::Options as RustdocOptions;
use crate::html::markdown::{ErrorCodes, Ignore, LangString, MdRelLine};
use crate::lint::init_lints;
/// Options that apply to all doctests in a crate or Markdown file (for `rustdoc foo.md`).
#[derive(Clone)]
pub(crate) struct GlobalTestOptions {
/// Name of the crate (for regular `rustdoc`) or Markdown file (for `rustdoc foo.md`).
pub(crate) crate_name: String,
/// Whether to disable the default `extern crate my_crate;` when creating doctests.
pub(crate) no_crate_inject: bool,
/// Whether inserting extra indent spaces in code block,
/// default is `false`, only `true` for generating code link of Rust playground
pub(crate) insert_indent_space: bool,
/// Additional crate-level attributes to add to doctests.
pub(crate) attrs: Vec<String>,
/// Path to file containing arguments for the invocation of rustc.
pub(crate) args_file: PathBuf,
}
pub(crate) fn generate_args_file(file_path: &Path, options: &RustdocOptions) -> Result<(), String> {
let mut file = File::create(file_path)
.map_err(|error| format!("failed to create args file: {error:?}"))?;
// We now put the common arguments into the file we created.
let mut content = vec!["--crate-type=bin".to_string()];
for cfg in &options.cfgs {
content.push(format!("--cfg={cfg}"));
}
for check_cfg in &options.check_cfgs {
content.push(format!("--check-cfg={check_cfg}"));
}
for lib_str in &options.lib_strs {
content.push(format!("-L{lib_str}"));
}
for extern_str in &options.extern_strs {
content.push(format!("--extern={extern_str}"));
}
content.push("-Ccodegen-units=1".to_string());
for codegen_options_str in &options.codegen_options_strs {
content.push(format!("-C{codegen_options_str}"));
}
for unstable_option_str in &options.unstable_opts_strs {
content.push(format!("-Z{unstable_option_str}"));
}
let content = content.join("\n");
file.write_all(content.as_bytes())
.map_err(|error| format!("failed to write arguments to temporary file: {error:?}"))?;
Ok(())
}
fn get_doctest_dir() -> io::Result<TempDir> {
TempFileBuilder::new().prefix("rustdoctest").tempdir()
}
pub(crate) fn run(
dcx: DiagCtxtHandle<'_>,
input: Input,
options: RustdocOptions,
) -> Result<(), ErrorGuaranteed> {
let invalid_codeblock_attributes_name = crate::lint::INVALID_CODEBLOCK_ATTRIBUTES.name;
// See core::create_config for what's going on here.
let allowed_lints = vec![
invalid_codeblock_attributes_name.to_owned(),
lint::builtin::UNKNOWN_LINTS.name.to_owned(),
lint::builtin::RENAMED_AND_REMOVED_LINTS.name.to_owned(),
];
let (lint_opts, lint_caps) = init_lints(allowed_lints, options.lint_opts.clone(), |lint| {
if lint.name == invalid_codeblock_attributes_name {
None
} else {
Some((lint.name_lower(), lint::Allow))
}
});
debug!(?lint_opts);
let crate_types =
if options.proc_macro_crate { vec![CrateType::ProcMacro] } else { vec![CrateType::Rlib] };
let sessopts = config::Options {
maybe_sysroot: options.maybe_sysroot.clone(),
search_paths: options.libs.clone(),
crate_types,
lint_opts,
lint_cap: Some(options.lint_cap.unwrap_or(lint::Forbid)),
cg: options.codegen_options.clone(),
externs: options.externs.clone(),
unstable_features: options.unstable_features,
actually_rustdoc: true,
edition: options.edition,
target_triple: options.target.clone(),
crate_name: options.crate_name.clone(),
remap_path_prefix: options.remap_path_prefix.clone(),
..config::Options::default()
};
let mut cfgs = options.cfgs.clone();
cfgs.push("doc".to_owned());
cfgs.push("doctest".to_owned());
let config = interface::Config {
opts: sessopts,
crate_cfg: cfgs,
crate_check_cfg: options.check_cfgs.clone(),
input: input.clone(),
output_file: None,
output_dir: None,
file_loader: None,
locale_resources: rustc_driver::DEFAULT_LOCALE_RESOURCES.to_vec(),
lint_caps,
psess_created: None,
hash_untracked_state: None,
register_lints: Some(Box::new(crate::lint::register_lints)),
override_queries: None,
make_codegen_backend: None,
registry: rustc_driver::diagnostics_registry(),
ice_file: None,
using_internal_features: Arc::default(),
expanded_args: options.expanded_args.clone(),
};
let externs = options.externs.clone();
let json_unused_externs = options.json_unused_externs;
let temp_dir = match get_doctest_dir()
.map_err(|error| format!("failed to create temporary directory: {error:?}"))
{
Ok(temp_dir) => temp_dir,
Err(error) => return crate::wrap_return(dcx, Err(error)),
};
let args_path = temp_dir.path().join("rustdoc-cfgs");
crate::wrap_return(dcx, generate_args_file(&args_path, &options))?;
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(
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();
}
Ok(collector)
})
})?;
run_tests(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
if json_unused_externs.is_enabled() {
let unused_extern_reports: Vec<_> =
std::mem::take(&mut unused_extern_reports.lock().unwrap());
if unused_extern_reports.len() == compiling_test_count {
let extern_names =
externs.iter().map(|(name, _)| name).collect::<FxIndexSet<&String>>();
let mut unused_extern_names = unused_extern_reports
.iter()
.map(|uexts| uexts.unused_extern_names.iter().collect::<FxIndexSet<&String>>())
.fold(extern_names, |uextsa, uextsb| {
uextsa.intersection(&uextsb).copied().collect::<FxIndexSet<&String>>()
})
.iter()
.map(|v| (*v).clone())
.collect::<Vec<String>>();
unused_extern_names.sort();
// Take the most severe lint level
let lint_level = unused_extern_reports
.iter()
.map(|uexts| uexts.lint_level.as_str())
.max_by_key(|v| match *v {
"warn" => 1,
"deny" => 2,
"forbid" => 3,
// The allow lint level is not expected,
// as if allow is specified, no message
// is to be emitted.
v => unreachable!("Invalid lint level '{v}'"),
})
.unwrap_or("warn")
.to_string();
let uext = UnusedExterns { lint_level, unused_extern_names };
let unused_extern_json = serde_json::to_string(&uext).unwrap();
eprintln!("{unused_extern_json}");
}
}
Ok(())
}
pub(crate) fn run_tests(
opts: GlobalTestOptions,
rustdoc_options: &Arc<RustdocOptions>,
unused_extern_reports: &Arc<Mutex<Vec<UnusedExterns>>>,
mut standalone_tests: Vec<test::TestDescAndFn>,
mergeable_tests: FxIndexMap<Edition, Vec<(DocTestBuilder, ScrapedDocTest)>>,
) {
let mut test_args = Vec::with_capacity(rustdoc_options.test_args.len() + 1);
test_args.insert(0, "rustdoctest".to_string());
test_args.extend_from_slice(&rustdoc_options.test_args);
if rustdoc_options.nocapture {
test_args.push("--nocapture".to_string());
}
let mut nb_errors = 0;
let mut ran_edition_tests = 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 mut tests_runner = runner::DocTestRunner::new();
let rustdoc_test_options = IndividualTestOptions::new(
rustdoc_options,
&Some(format!("merged_doctest_{edition}")),
PathBuf::from(format!("doctest_{edition}.rs")),
);
for (doctest, scraped_test) in &doctests {
tests_runner.add_test(doctest, scraped_test, &target_str);
}
if let Ok(success) = tests_runner.run_merged_tests(
rustdoc_test_options,
edition,
&opts,
&test_args,
rustdoc_options,
) {
ran_edition_tests += 1;
if !success {
nb_errors += 1;
}
continue;
}
// 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(),
Arc::clone(rustdoc_options),
unused_extern_reports.clone(),
));
}
}
// We need to call `test_main` even if there is no doctest to run to get the output
// `running 0 tests...`.
if ran_edition_tests == 0 || !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.
fn scrape_test_config(
crate_name: String,
attrs: &[ast::Attribute],
args_file: PathBuf,
) -> GlobalTestOptions {
use rustc_ast_pretty::pprust;
let mut opts = GlobalTestOptions {
crate_name,
no_crate_inject: false,
attrs: Vec::new(),
insert_indent_space: false,
args_file,
};
let test_attrs: Vec<_> = attrs
.iter()
.filter(|a| a.has_name(sym::doc))
.flat_map(|a| a.meta_item_list().unwrap_or_default())
.filter(|a| a.has_name(sym::test))
.collect();
let attrs = test_attrs.iter().flat_map(|a| a.meta_item_list().unwrap_or(&[]));
for attr in attrs {
if attr.has_name(sym::no_crate_inject) {
opts.no_crate_inject = true;
}
if attr.has_name(sym::attr)
&& let Some(l) = attr.meta_item_list()
{
for item in l {
opts.attrs.push(pprust::meta_list_item_to_string(item));
}
}
}
opts
}
/// Documentation test failure modes.
enum TestFailure {
/// The test failed to compile.
CompileError,
/// The test is marked `compile_fail` but compiled successfully.
UnexpectedCompilePass,
/// The test failed to compile (as expected) but the compiler output did not contain all
/// expected error codes.
MissingErrorCodes(Vec<String>),
/// The test binary was unable to be executed.
ExecutionError(io::Error),
/// The test binary exited with a non-zero exit code.
///
/// This typically means an assertion in the test failed or another form of panic occurred.
ExecutionFailure(process::Output),
/// The test is marked `should_panic` but the test binary executed successfully.
UnexpectedRunPass,
}
enum DirState {
Temp(tempfile::TempDir),
Perm(PathBuf),
}
impl DirState {
fn path(&self) -> &std::path::Path {
match self {
DirState::Temp(t) => t.path(),
DirState::Perm(p) => p.as_path(),
}
}
}
// NOTE: Keep this in sync with the equivalent structs in rustc
// and cargo.
// 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)]
pub(crate) struct UnusedExterns {
/// Lint level of the unused_crate_dependencies lint
lint_level: String,
/// List of unused externs by their names.
unused_extern_names: Vec<String>,
}
fn add_exe_suffix(input: String, target: &TargetTriple) -> String {
let exe_suffix = match target {
TargetTriple::TargetTriple(_) => Target::expect_builtin(target).options.exe_suffix,
TargetTriple::TargetJson { contents, .. } => {
Target::from_json(contents.parse().unwrap()).unwrap().0.options.exe_suffix
}
};
input + &exe_suffix
}
fn wrapped_rustc_command(rustc_wrappers: &[PathBuf], rustc_binary: &Path) -> Command {
let mut args = rustc_wrappers.iter().map(PathBuf::as_path).chain([rustc_binary]);
let exe = args.next().expect("unable to create rustc command");
let mut command = Command::new(exe);
for arg in args {
command.arg(arg);
}
command
}
/// Information needed for running a bundle of doctests.
///
/// This data structure contains the "full" test code, including the wrappers
/// (if multiple doctests are merged), `main` function,
/// and everything needed to calculate the compiler's command-line arguments.
/// The `# ` prefix on boring lines has also been stripped.
pub(crate) struct RunnableDocTest {
full_test_code: String,
full_test_line_offset: usize,
test_opts: IndividualTestOptions,
global_opts: GlobalTestOptions,
langstr: LangString,
line: usize,
edition: Edition,
no_run: bool,
is_multiple_tests: bool,
}
impl RunnableDocTest {
fn path_for_merged_doctest(&self) -> PathBuf {
self.test_opts.outdir.path().join(format!("doctest_{}.rs", self.edition))
}
}
/// Execute a `RunnableDoctest`.
///
/// This is the function that calculates the compiler command line, invokes the compiler, then
/// invokes the test or tests in a separate executable (if applicable).
fn run_test(
doctest: RunnableDocTest,
rustdoc_options: &RustdocOptions,
supports_color: bool,
report_unused_externs: impl Fn(UnusedExterns),
) -> Result<(), TestFailure> {
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);
let rustc_binary = rustdoc_options
.test_builder
.as_deref()
.unwrap_or_else(|| rustc_interface::util::rustc_path().expect("found rustc"));
let mut compiler = wrapped_rustc_command(&rustdoc_options.test_builder_wrappers, rustc_binary);
compiler.arg(format!("@{}", doctest.global_opts.args_file.display()));
if let Some(sysroot) = &rustdoc_options.maybe_sysroot {
compiler.arg(format!("--sysroot={}", sysroot.display()));
}
compiler.arg("--edition").arg(doctest.edition.to_string());
if !doctest.is_multiple_tests {
// Setting these environment variables is unneeded if this is a merged doctest.
compiler.env("UNSTABLE_RUSTDOC_TEST_PATH", &doctest.test_opts.path);
compiler.env(
"UNSTABLE_RUSTDOC_TEST_LINE",
format!("{}", doctest.line as isize - doctest.full_test_line_offset as isize),
);
}
compiler.arg("-o").arg(&output_file);
if langstr.test_harness {
compiler.arg("--test");
}
if rustdoc_options.json_unused_externs.is_enabled() && !langstr.compile_fail {
compiler.arg("--error-format=json");
compiler.arg("--json").arg("unused-externs");
compiler.arg("-W").arg("unused_crate_dependencies");
compiler.arg("-Z").arg("unstable-options");
}
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");
}
compiler.arg("--target").arg(match &rustdoc_options.target {
TargetTriple::TargetTriple(s) => s,
TargetTriple::TargetJson { path_for_rustdoc, .. } => {
path_for_rustdoc.to_str().expect("target path must be valid unicode")
}
});
if let ErrorOutputType::HumanReadable(kind, color_config) = rustdoc_options.error_format {
let short = kind.short();
if short {
compiler.arg("--error-format").arg("short");
}
match color_config {
ColorConfig::Never => {
compiler.arg("--color").arg("never");
}
ColorConfig::Always => {
compiler.arg("--color").arg("always");
}
ColorConfig::Auto => {
compiler.arg("--color").arg(if supports_color { "always" } else { "never" });
}
}
}
// If this is a merged doctest, we need to write it into a file instead of using stdin
// because if the size of the merged doctests is too big, it'll simply break stdin.
if doctest.is_multiple_tests {
// It makes the compilation failure much faster if it is for a combined doctest.
compiler.arg("--error-format=short");
let input_file = doctest.path_for_merged_doctest();
if std::fs::write(&input_file, &doctest.full_test_code).is_err() {
// If we cannot write this file for any reason, we leave. All combined tests will be
// tested as standalone tests.
return Err(TestFailure::CompileError);
}
compiler.arg(input_file);
if !rustdoc_options.nocapture {
// If `nocapture` is disabled, then we don't display rustc's output when compiling
// the merged doctests.
compiler.stderr(Stdio::null());
}
} else {
compiler.arg("-");
compiler.stdin(Stdio::piped());
compiler.stderr(Stdio::piped());
}
debug!("compiler invocation for doctest: {compiler:?}");
let mut child = compiler.spawn().expect("Failed to spawn rustc process");
let output = if doctest.is_multiple_tests {
let status = child.wait().expect("Failed to wait");
process::Output { status, stdout: Vec::new(), stderr: Vec::new() }
} else {
let stdin = child.stdin.as_mut().expect("Failed to open stdin");
stdin.write_all(doctest.full_test_code.as_bytes()).expect("could write out test sources");
child.wait_with_output().expect("Failed to read stdout")
};
struct Bomb<'a>(&'a str);
impl Drop for Bomb<'_> {
fn drop(&mut self) {
eprint!("{}", self.0);
}
}
let mut out = str::from_utf8(&output.stderr)
.unwrap()
.lines()
.filter(|l| {
if let Ok(uext) = serde_json::from_str::<UnusedExterns>(l) {
report_unused_externs(uext);
false
} else {
true
}
})
.intersperse_with(|| "\n")
.collect::<String>();
// Add a \n to the end to properly terminate the last line,
// but only if there was output to be printed
if !out.is_empty() {
out.push('\n');
}
let _bomb = Bomb(&out);
match (output.status.success(), langstr.compile_fail) {
(true, true) => {
return Err(TestFailure::UnexpectedCompilePass);
}
(true, false) => {}
(false, true) => {
if !langstr.error_codes.is_empty() {
// 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> = langstr
.error_codes
.iter()
.filter(|err| !out.contains(&format!("error[{err}]")))
.cloned()
.collect();
if !missing_codes.is_empty() {
return Err(TestFailure::MissingErrorCodes(missing_codes));
}
}
}
(false, false) => {
return Err(TestFailure::CompileError);
}
}
if doctest.no_run {
return Ok(());
}
// Run the code!
let mut cmd;
let output_file = make_maybe_absolute_path(output_file);
if let Some(tool) = &rustdoc_options.runtool {
let tool = make_maybe_absolute_path(tool.into());
cmd = Command::new(tool);
cmd.args(&rustdoc_options.runtool_args);
cmd.arg(&output_file);
} else {
cmd = Command::new(&output_file);
if doctest.is_multiple_tests {
cmd.arg("*doctest-bin-path");
cmd.arg(&output_file);
}
}
if let Some(run_directory) = &rustdoc_options.test_run_directory {
cmd.current_dir(run_directory);
}
let result = if doctest.is_multiple_tests || rustdoc_options.nocapture {
cmd.status().map(|status| process::Output {
status,
stdout: Vec::new(),
stderr: Vec::new(),
})
} else {
cmd.output()
};
match result {
Err(e) => return Err(TestFailure::ExecutionError(e)),
Ok(out) => {
if langstr.should_panic && out.status.success() {
return Err(TestFailure::UnexpectedRunPass);
} else if !langstr.should_panic && !out.status.success() {
return Err(TestFailure::ExecutionFailure(out));
}
}
}
Ok(())
}
/// Converts a path intended to use as a command to absolute if it is
/// relative, and not a single component.
///
/// This is needed to deal with relative paths interacting with
/// `Command::current_dir` in a platform-specific way.
fn make_maybe_absolute_path(path: PathBuf) -> PathBuf {
if path.components().count() == 1 {
// Look up process via PATH.
path
} else {
std::env::current_dir().map(|c| c.join(&path)).unwrap_or_else(|_| path)
}
}
struct IndividualTestOptions {
outdir: DirState,
path: PathBuf,
}
impl IndividualTestOptions {
fn new(options: &RustdocOptions, test_id: &Option<String>, test_path: PathBuf) -> Self {
let outdir = if let Some(ref path) = options.persist_doctests {
let mut path = path.clone();
path.push(&test_id.as_deref().unwrap_or("<doctest>"));
if let Err(err) = std::fs::create_dir_all(&path) {
eprintln!("Couldn't create directory for doctest executables: {err}");
panic::resume_unwind(Box::new(()));
}
DirState::Perm(path)
} else {
DirState::Temp(get_doctest_dir().expect("rustdoc needs a tempdir"))
};
Self { outdir, path: test_path }
}
}
/// A doctest scraped from the code, ready to be turned into a runnable test.
///
/// The pipeline goes: [`clean`] AST -> `ScrapedDoctest` -> `RunnableDoctest`.
/// [`run_merged_tests`] converts a bunch of scraped doctests to a single runnable doctest,
/// while [`generate_unique_doctest`] does the standalones.
///
/// [`clean`]: crate::clean
/// [`run_merged_tests`]: crate::doctest::runner::DocTestRunner::run_merged_tests
/// [`generate_unique_doctest`]: crate::doctest::make::DocTestBuilder::generate_unique_doctest
pub(crate) struct ScrapedDocTest {
filename: FileName,
line: usize,
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, langstr, text, name }
}
fn edition(&self, opts: &RustdocOptions) -> Edition {
self.langstr.edition.unwrap_or(opts.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 {
fn visit_test(&mut self, test: String, config: LangString, rel_line: MdRelLine);
fn visit_header(&mut self, _name: &str, _level: u32) {}
}
struct CreateRunnableDocTests {
standalone_tests: Vec<test::TestDescAndFn>,
mergeable_tests: FxIndexMap<Edition, Vec<(DocTestBuilder, ScrapedDocTest)>>,
rustdoc_options: Arc<RustdocOptions>,
opts: GlobalTestOptions,
visited_tests: FxHashMap<(String, usize), usize>,
unused_extern_reports: Arc<Mutex<Vec<UnusedExterns>>>,
compiling_test_count: AtomicUsize,
can_merge_doctests: bool,
}
impl CreateRunnableDocTests {
fn new(rustdoc_options: RustdocOptions, opts: GlobalTestOptions) -> CreateRunnableDocTests {
let can_merge_doctests = rustdoc_options.edition >= Edition::Edition2024;
CreateRunnableDocTests {
standalone_tests: Vec::new(),
mergeable_tests: FxIndexMap::default(),
rustdoc_options: Arc::new(rustdoc_options),
opts,
visited_tests: FxHashMap::default(),
unused_extern_reports: Default::default(),
compiling_test_count: AtomicUsize::new(0),
can_merge_doctests,
}
}
fn add_test(&mut self, scraped_test: ScrapedDocTest) {
// For example `module/file.rs` would become `module_file_rs`
let file = scraped_test
.filename
.prefer_local()
.to_string_lossy()
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
.collect::<String>();
let test_id = format!(
"{file}_{line}_{number}",
file = file,
line = scraped_test.line,
number = {
// Increases the current test number, if this file already
// exists or it creates a new entry with a test number of 0.
self.visited_tests
.entry((file.clone(), scraped_test.line))
.and_modify(|v| *v += 1)
.or_insert(0)
},
);
let edition = scraped_test.edition(&self.rustdoc_options);
let doctest = DocTestBuilder::new(
&scraped_test.text,
Some(&self.opts.crate_name),
edition,
self.can_merge_doctests,
Some(test_id),
Some(&scraped_test.langstr),
);
let is_standalone = !doctest.can_be_merged
|| scraped_test.langstr.compile_fail
|| scraped_test.langstr.test_harness
|| scraped_test.langstr.standalone_crate
|| self.rustdoc_options.nocapture
|| self.rustdoc_options.test_args.iter().any(|arg| arg == "--show-output");
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: DocTestBuilder,
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(),
Arc::clone(&self.rustdoc_options),
self.unused_extern_reports.clone(),
)
}
}
fn generate_test_desc_and_fn(
test: DocTestBuilder,
scraped_test: ScrapedDocTest,
opts: GlobalTestOptions,
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, scraped_test.path());
debug!("creating test {}: {}", scraped_test.name, scraped_test.text);
test::TestDescAndFn {
desc: test::TestDesc {
name: test::DynTestName(scraped_test.name.clone()),
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(
test_opts: IndividualTestOptions,
global_opts: GlobalTestOptions,
doctest: DocTestBuilder,
scraped_test: ScrapedDocTest,
rustdoc_options: Arc<RustdocOptions>,
unused_externs: Arc<Mutex<Vec<UnusedExterns>>>,
) -> Result<(), String> {
let report_unused_externs = |uext| {
unused_externs.lock().unwrap().push(uext);
};
let (full_test_code, full_test_line_offset) = doctest.generate_unique_doctest(
&scraped_test.text,
scraped_test.langstr.test_harness,
&global_opts,
Some(&global_opts.crate_name),
);
let runnable_test = RunnableDocTest {
full_test_code,
full_test_line_offset,
test_opts,
global_opts,
langstr: scraped_test.langstr.clone(),
line: scraped_test.line,
edition: scraped_test.edition(&rustdoc_options),
no_run: scraped_test.no_run(&rustdoc_options),
is_multiple_tests: false,
};
let res =
run_test(runnable_test, &rustdoc_options, doctest.supports_color, report_unused_externs);
if let Err(err) = res {
match err {
TestFailure::CompileError => {
eprint!("Couldn't compile the test.");
}
TestFailure::UnexpectedCompilePass => {
eprint!("Test compiled successfully, but it's marked `compile_fail`.");
}
TestFailure::UnexpectedRunPass => {
eprint!("Test executable succeeded, but it's marked `should_panic`.");
}
TestFailure::MissingErrorCodes(codes) => {
eprint!("Some expected error codes were not found: {codes:?}");
}
TestFailure::ExecutionError(err) => {
eprint!("Couldn't run the test: {err}");
if err.kind() == io::ErrorKind::PermissionDenied {
eprint!(" - maybe your tempdir is mounted with noexec?");
}
}
TestFailure::ExecutionFailure(out) => {
eprintln!("Test executable failed ({reason}).", reason = out.status);
// FIXME(#12309): An unfortunate side-effect of capturing the test
// executable's output is that the relative ordering between the test's
// stdout and stderr is lost. However, this is better than the
// alternative: if the test executable inherited the parent's I/O
// handles the output wouldn't be captured at all, even on success.
//
// The ordering could be preserved if the test process' stderr was
// redirected to stdout, but that functionality does not exist in the
// standard library, so it may not be portable enough.
let stdout = str::from_utf8(&out.stdout).unwrap_or_default();
let stderr = str::from_utf8(&out.stderr).unwrap_or_default();
if !stdout.is_empty() || !stderr.is_empty() {
eprintln!();
if !stdout.is_empty() {
eprintln!("stdout:\n{stdout}");
}
if !stderr.is_empty() {
eprintln!("stderr:\n{stderr}");
}
}
}
}
panic::resume_unwind(Box::new(()));
}
Ok(())
}
#[cfg(test)] // used in tests
impl DocTestVisitor for Vec<usize> {
fn visit_test(&mut self, _test: String, _config: LangString, rel_line: MdRelLine) {
self.push(1 + rel_line.offset());
}
}
#[cfg(test)]
mod tests;