| mod extracted; | |
| mod make; | |
| mod markdown; | |
| mod runner; | |
| mod rust; | |
| use std::fs::File; | |
| use std::hash::{Hash, Hasher}; | |
| use std::io::{self, Write}; | |
| use std::path::{Path, PathBuf}; | |
| use std::process::{self, Command, Stdio}; | |
| use std::str::FromStr; | |
| use std::sync::atomic::{AtomicUsize, Ordering}; | |
| use std::sync::{Arc, Mutex}; | |
| use std::time::{Duration, Instant}; | |
| use std::{panic, str}; | |
| pub(crate) use make::{BuildDocTestBuilder, DocTestBuilder}; | |
| pub(crate) use markdown::test as test_markdown; | |
| use proc_macro2::{TokenStream, TokenTree}; | |
| use rustc_data_structures::fx::{FxHashMap, FxHashSet, FxHasher, FxIndexMap, FxIndexSet}; | |
| use rustc_errors::emitter::HumanReadableErrorType; | |
| use rustc_errors::{ColorConfig, DiagCtxtHandle}; | |
| use rustc_hir::attrs::AttributeKind; | |
| use rustc_hir::def_id::LOCAL_CRATE; | |
| use rustc_hir::{Attribute, CRATE_HIR_ID}; | |
| use rustc_interface::interface; | |
| use rustc_middle::ty::TyCtxt; | |
| use rustc_session::config::{self, CrateType, ErrorOutputType, Input}; | |
| use rustc_session::lint; | |
| use rustc_span::edition::Edition; | |
| use rustc_span::{FileName, RemapPathScopeComponents, Span}; | |
| use rustc_target::spec::{Target, TargetTuple}; | |
| use tempfile::{Builder as TempFileBuilder, TempDir}; | |
| use tracing::{debug, info}; | |
| use self::rust::HirCollector; | |
| use crate::config::{MergeDoctests, Options as RustdocOptions, OutputFormat}; | |
| use crate::html::markdown::{CodeLineMapping, ErrorCodes, Ignore, LangString, MdRelLine}; | |
| use crate::lint::init_lints; | |
| /// Type used to display times (compilation and total) information for merged doctests. | |
| struct MergedDoctestTimes { | |
| total_time: Instant, | |
| /// Total time spent compiling all merged doctests. | |
| compilation_time: Duration, | |
| /// This field is used to keep track of how many merged doctests we (tried to) compile. | |
| added_compilation_times: usize, | |
| } | |
| impl MergedDoctestTimes { | |
| fn new() -> Self { | |
| Self { | |
| total_time: Instant::now(), | |
| compilation_time: Duration::default(), | |
| added_compilation_times: 0, | |
| } | |
| } | |
| fn add_compilation_time(&mut self, duration: Duration) { | |
| self.compilation_time += duration; | |
| self.added_compilation_times += 1; | |
| } | |
| /// Returns `(total_time, compilation_time)`. | |
| fn times_in_secs(&self) -> Option<(f64, f64)> { | |
| // If no merged doctest was compiled, then there is nothing to display since the numbers | |
| // displayed by `libtest` for standalone tests are already accurate (they include both | |
| // compilation and runtime). | |
| if self.added_compilation_times == 0 { | |
| return None; | |
| } | |
| Some((self.total_time.elapsed().as_secs_f64(), self.compilation_time.as_secs_f64())) | |
| } | |
| } | |
| /// 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, | |
| /// 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![]; | |
| 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}")); | |
| } | |
| content.extend(options.doctest_build_args.clone()); | |
| 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(opts: &RustdocOptions) -> io::Result<TempDir> { | |
| let mut builder = TempFileBuilder::new(); | |
| builder.prefix("rustdoctest"); | |
| if opts.codegen_options.save_temps { | |
| builder.disable_cleanup(true); | |
| } | |
| builder.tempdir() | |
| } | |
| pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions) { | |
| 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 { | |
| sysroot: options.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(), | |
| remap_path_scope: options.remap_path_scope.clone(), | |
| unstable_opts: options.unstable_opts.clone(), | |
| error_format: options.error_format.clone(), | |
| target_modifiers: options.target_modifiers.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, | |
| lint_caps, | |
| psess_created: None, | |
| track_state: None, | |
| register_lints: Some(Box::new(crate::lint::register_lints)), | |
| override_queries: None, | |
| extra_symbols: Vec::new(), | |
| make_codegen_backend: None, | |
| ice_file: None, | |
| using_internal_features: &rustc_driver::USING_INTERNAL_FEATURES, | |
| }; | |
| let externs = options.externs.clone(); | |
| let json_unused_externs = options.json_unused_externs; | |
| let temp_dir = match get_doctest_dir(&options) | |
| .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 extract_doctests = options.output_format == OutputFormat::Doctest; | |
| let save_temps = options.codegen_options.save_temps; | |
| let result = interface::run_compiler(config, |compiler| { | |
| let krate = rustc_interface::passes::parse(&compiler.sess); | |
| let (collector, _incr_comp_session) = | |
| rustc_interface::create_and_enter_global_ctxt(compiler, krate, |tcx| { | |
| let crate_name = tcx.crate_name(LOCAL_CRATE).to_string(); | |
| let opts = scrape_test_config(tcx, crate_name, args_path); | |
| let hir_collector = HirCollector::new( | |
| ErrorCodes::from(compiler.sess.opts.unstable_features.is_nightly_build()), | |
| tcx, | |
| ); | |
| let tests = hir_collector.collect_crate(); | |
| if extract_doctests { | |
| let mut collector = extracted::ExtractedDocTests::new(); | |
| tests.into_iter().for_each(|t| collector.add_test(t, &opts, &options)); | |
| let stdout = std::io::stdout(); | |
| let mut stdout = stdout.lock(); | |
| if let Err(error) = serde_json::ser::to_writer(&mut stdout, &collector) { | |
| eprintln!(); | |
| Err(format!("Failed to generate JSON output for doctests: {error:?}")) | |
| } else { | |
| Ok(None) | |
| } | |
| } else { | |
| let mut collector = CreateRunnableDocTests::new(options, opts); | |
| tests | |
| .into_iter() | |
| .for_each(|t| collector.add_test(t, Some(compiler.sess.dcx()))); | |
| Ok(Some(collector)) | |
| } | |
| }); | |
| compiler.sess.dcx().abort_if_errors(); | |
| collector | |
| }); | |
| let CreateRunnableDocTests { | |
| standalone_tests, | |
| mergeable_tests, | |
| rustdoc_options, | |
| opts, | |
| unused_extern_reports, | |
| compiling_test_count, | |
| .. | |
| } = match result { | |
| Ok(Some(collector)) => collector, | |
| Ok(None) => return, | |
| Err(error) => { | |
| eprintln!("{error}"); | |
| // Since some files in the temporary folder are still owned and alive, we need | |
| // to manually remove the folder. | |
| if !save_temps { | |
| let _ = std::fs::remove_dir_all(temp_dir.path()); | |
| } | |
| std::process::exit(1); | |
| } | |
| }; | |
| run_tests( | |
| dcx, | |
| opts, | |
| &rustdoc_options, | |
| &unused_extern_reports, | |
| standalone_tests, | |
| mergeable_tests, | |
| Some(temp_dir), | |
| ); | |
| 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}"); | |
| } | |
| } | |
| } | |
| pub(crate) fn run_tests( | |
| dcx: DiagCtxtHandle<'_>, | |
| opts: GlobalTestOptions, | |
| rustdoc_options: &Arc<RustdocOptions>, | |
| unused_extern_reports: &Arc<Mutex<Vec<UnusedExterns>>>, | |
| mut standalone_tests: Vec<test::TestDescAndFn>, | |
| mergeable_tests: FxIndexMap<MergeableTestKey, Vec<(DocTestBuilder, ScrapedDocTest)>>, | |
| // We pass this argument so we can drop it manually before using `exit`. | |
| mut temp_dir: Option<TempDir>, | |
| ) { | |
| 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.no_capture { | |
| test_args.push("--no-capture".to_string()); | |
| } | |
| let mut nb_errors = 0; | |
| let mut ran_edition_tests = 0; | |
| let mut times = MergedDoctestTimes::new(); | |
| let target_str = rustdoc_options.target.to_string(); | |
| for (MergeableTestKey { edition, global_crate_attrs_hash }, 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}_{global_crate_attrs_hash}")), | |
| PathBuf::from(format!("doctest_{edition}_{global_crate_attrs_hash}.rs")), | |
| ); | |
| for (doctest, scraped_test) in &doctests { | |
| tests_runner.add_test(doctest, scraped_test, &target_str); | |
| } | |
| let (duration, ret) = tests_runner.run_merged_tests( | |
| rustdoc_test_options, | |
| edition, | |
| &opts, | |
| &test_args, | |
| rustdoc_options, | |
| ); | |
| times.add_compilation_time(duration); | |
| if let Ok(success) = ret { | |
| ran_edition_tests += 1; | |
| if !success { | |
| nb_errors += 1; | |
| } | |
| continue; | |
| } | |
| if rustdoc_options.merge_doctests == MergeDoctests::Always { | |
| let mut diag = dcx.struct_fatal("failed to merge doctests"); | |
| diag.note("requested explicitly on the command line with `--merge-doctests=yes`"); | |
| diag.emit(); | |
| } | |
| // 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_with_exit_callback(&test_args, standalone_tests, None, || { | |
| let times = times.times_in_secs(); | |
| // We ensure temp dir destructor is called. | |
| std::mem::drop(temp_dir.take()); | |
| if let Some((total_time, compilation_time)) = times { | |
| test::print_merged_doctests_times(&test_args, total_time, compilation_time); | |
| } | |
| }); | |
| } else { | |
| // If the first condition branch exited successfully, `test_main_with_exit_callback` will | |
| // not exit the process. So to prevent displaying the times twice, we put it behind an | |
| // `else` condition. | |
| if let Some((total_time, compilation_time)) = times.times_in_secs() { | |
| test::print_merged_doctests_times(&test_args, total_time, compilation_time); | |
| } | |
| } | |
| // We ensure temp dir destructor is called. | |
| std::mem::drop(temp_dir); | |
| if nb_errors != 0 { | |
| std::process::exit(test::ERROR_EXIT_CODE); | |
| } | |
| } | |
| // Look for `#![doc(test(no_crate_inject))]`, used by crates in the std facade. | |
| fn scrape_test_config( | |
| tcx: TyCtxt<'_>, | |
| crate_name: String, | |
| args_file: PathBuf, | |
| ) -> GlobalTestOptions { | |
| let mut opts = GlobalTestOptions { | |
| crate_name, | |
| no_crate_inject: false, | |
| insert_indent_space: false, | |
| args_file, | |
| }; | |
| let source_map = tcx.sess.source_map(); | |
| 'main: for attr in tcx.hir_attrs(CRATE_HIR_ID) { | |
| let Attribute::Parsed(AttributeKind::Doc(d)) = attr else { continue }; | |
| for attr_span in &d.test_attrs { | |
| // FIXME: This is ugly, remove when `test_attrs` has been ported to new attribute API. | |
| if let Ok(snippet) = source_map.span_to_snippet(*attr_span) | |
| && let Ok(stream) = TokenStream::from_str(&snippet) | |
| { | |
| // NOTE: `test(attr(..))` is handled when discovering the individual tests | |
| if stream.into_iter().any(|token| { | |
| matches!( | |
| token, | |
| TokenTree::Ident(i) if i.to_string() == "no_crate_inject", | |
| ) | |
| }) { | |
| opts.no_crate_inject = true; | |
| break 'main; | |
| } | |
| } | |
| } | |
| } | |
| 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(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: &TargetTuple) -> String { | |
| let exe_suffix = match target { | |
| TargetTuple::TargetTuple(_) => Target::expect_builtin(target).options.exe_suffix, | |
| TargetTuple::TargetJson { contents, .. } => { | |
| Target::from_json(contents).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, | |
| merged_test_code: Option<String>, | |
| } | |
| impl RunnableDocTest { | |
| fn path_for_merged_doctest_bundle(&self) -> PathBuf { | |
| self.test_opts.outdir.path().join(format!("doctest_bundle_{}.rs", self.edition)) | |
| } | |
| fn path_for_merged_doctest_runner(&self) -> PathBuf { | |
| self.test_opts.outdir.path().join(format!("doctest_runner_{}.rs", self.edition)) | |
| } | |
| fn is_multiple_tests(&self) -> bool { | |
| self.merged_test_code.is_some() | |
| } | |
| } | |
| /// 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). | |
| /// | |
| /// Returns a tuple containing the `Duration` of the compilation and the `Result` of the test. | |
| fn run_test( | |
| doctest: RunnableDocTest, | |
| rustdoc_options: &RustdocOptions, | |
| supports_color: bool, | |
| report_unused_externs: impl Fn(UnusedExterns), | |
| ) -> (Duration, 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 instant = Instant::now(); | |
| // Common arguments used for compiling the doctest runner. | |
| // On merged doctests, the compiler is invoked twice: once for the test code itself, | |
| // and once for the runner wrapper (which needs to use `#![feature]` on stable). | |
| let mut compiler_args = vec![]; | |
| compiler_args.push(format!("@{}", doctest.global_opts.args_file.display())); | |
| let sysroot = &rustdoc_options.sysroot; | |
| if let Some(explicit_sysroot) = &sysroot.explicit { | |
| compiler_args.push(format!("--sysroot={}", explicit_sysroot.display())); | |
| } | |
| compiler_args.extend_from_slice(&["--edition".to_owned(), doctest.edition.to_string()]); | |
| if langstr.test_harness { | |
| compiler_args.push("--test".to_owned()); | |
| } | |
| if rustdoc_options.json_unused_externs.is_enabled() && !langstr.compile_fail { | |
| compiler_args.push("--error-format=json".to_owned()); | |
| compiler_args.extend_from_slice(&["--json".to_owned(), "unused-externs".to_owned()]); | |
| compiler_args.extend_from_slice(&["-W".to_owned(), "unused_crate_dependencies".to_owned()]); | |
| compiler_args.extend_from_slice(&["-Z".to_owned(), "unstable-options".to_owned()]); | |
| } | |
| 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_args.push("--emit=metadata".to_owned()); | |
| } | |
| compiler_args.extend_from_slice(&[ | |
| "--target".to_owned(), | |
| match &rustdoc_options.target { | |
| TargetTuple::TargetTuple(s) => s.clone(), | |
| TargetTuple::TargetJson { path_for_rustdoc, .. } => { | |
| path_for_rustdoc.to_str().expect("target path must be valid unicode").to_owned() | |
| } | |
| }, | |
| ]); | |
| if let ErrorOutputType::HumanReadable { kind, color_config } = rustdoc_options.error_format { | |
| let short = kind.short(); | |
| let unicode = kind == HumanReadableErrorType { unicode: true, short }; | |
| if short { | |
| compiler_args.extend_from_slice(&["--error-format".to_owned(), "short".to_owned()]); | |
| } | |
| if unicode { | |
| compiler_args | |
| .extend_from_slice(&["--error-format".to_owned(), "human-unicode".to_owned()]); | |
| } | |
| match color_config { | |
| ColorConfig::Never => { | |
| compiler_args.extend_from_slice(&["--color".to_owned(), "never".to_owned()]); | |
| } | |
| ColorConfig::Always => { | |
| compiler_args.extend_from_slice(&["--color".to_owned(), "always".to_owned()]); | |
| } | |
| ColorConfig::Auto => { | |
| compiler_args.extend_from_slice(&[ | |
| "--color".to_owned(), | |
| if supports_color { "always" } else { "never" }.to_owned(), | |
| ]); | |
| } | |
| } | |
| } | |
| let rustc_binary = rustdoc_options | |
| .test_builder | |
| .as_deref() | |
| .unwrap_or_else(|| rustc_interface::util::rustc_path(sysroot).expect("found rustc")); | |
| let mut compiler = wrapped_rustc_command(&rustdoc_options.test_builder_wrappers, rustc_binary); | |
| compiler.args(&compiler_args); | |
| // 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_bundle(); | |
| 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 (Duration::default(), Err(TestFailure::CompileError)); | |
| } | |
| if !rustdoc_options.no_capture && rustdoc_options.merge_doctests == MergeDoctests::Auto { | |
| // If `no_capture` is disabled, and we might fallback to standalone tests, then we don't | |
| // display rustc's output when compiling the merged doctests. | |
| compiler.stderr(Stdio::null()); | |
| } | |
| // bundled tests are an rlib, loaded by a separate runner executable | |
| compiler | |
| .arg("--crate-type=lib") | |
| .arg("--out-dir") | |
| .arg(doctest.test_opts.outdir.path()) | |
| .arg(input_file); | |
| } else { | |
| compiler.arg("--crate-type=bin").arg("-o").arg(&output_file); | |
| // 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("-"); | |
| compiler.stdin(Stdio::piped()); | |
| compiler.stderr(Stdio::piped()); | |
| } | |
| info!("compiler invocation for doctest: {compiler:?}"); | |
| let mut child = match compiler.spawn() { | |
| Ok(child) => child, | |
| Err(error) => { | |
| eprintln!("Failed to spawn {:?}: {error:?}", compiler.get_program()); | |
| return (Duration::default(), Err(TestFailure::CompileError)); | |
| } | |
| }; | |
| let output = if let Some(merged_test_code) = &doctest.merged_test_code { | |
| // compile-fail tests never get merged, so this should always pass | |
| let status = child.wait().expect("Failed to wait"); | |
| // the actual test runner is a separate component, built with nightly-only features; | |
| // build it now | |
| let runner_input_file = doctest.path_for_merged_doctest_runner(); | |
| let mut runner_compiler = | |
| wrapped_rustc_command(&rustdoc_options.test_builder_wrappers, rustc_binary); | |
| // the test runner does not contain any user-written code, so this doesn't allow | |
| // the user to exploit nightly-only features on stable | |
| runner_compiler.env("RUSTC_BOOTSTRAP", "1"); | |
| runner_compiler.args(compiler_args); | |
| runner_compiler.args(["--crate-type=bin", "-o"]).arg(&output_file); | |
| let mut extern_path = std::ffi::OsString::from(format!( | |
| "--extern=doctest_bundle_{edition}=", | |
| edition = doctest.edition | |
| )); | |
| // Deduplicate passed -L directory paths, since usually all dependencies will be in the | |
| // same directory (e.g. target/debug/deps from Cargo). | |
| let mut seen_search_dirs = FxHashSet::default(); | |
| for extern_str in &rustdoc_options.extern_strs { | |
| if let Some((_cratename, path)) = extern_str.split_once('=') { | |
| // Direct dependencies of the tests themselves are | |
| // indirect dependencies of the test runner. | |
| // They need to be in the library search path. | |
| let dir = Path::new(path) | |
| .parent() | |
| .filter(|x| x.components().count() > 0) | |
| .unwrap_or(Path::new(".")); | |
| if seen_search_dirs.insert(dir) { | |
| runner_compiler.arg("-L").arg(dir); | |
| } | |
| } | |
| } | |
| let output_bundle_file = doctest | |
| .test_opts | |
| .outdir | |
| .path() | |
| .join(format!("libdoctest_bundle_{edition}.rlib", edition = doctest.edition)); | |
| extern_path.push(&output_bundle_file); | |
| runner_compiler.arg(extern_path); | |
| runner_compiler.arg(&runner_input_file); | |
| if std::fs::write(&runner_input_file, merged_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 (instant.elapsed(), Err(TestFailure::CompileError)); | |
| } | |
| if !rustdoc_options.no_capture && rustdoc_options.merge_doctests == MergeDoctests::Auto { | |
| // If `no_capture` is disabled and we're autodetecting whether to merge, | |
| // we don't display rustc's output when compiling the merged doctests. | |
| runner_compiler.stderr(Stdio::null()); | |
| } else { | |
| runner_compiler.stderr(Stdio::inherit()); | |
| } | |
| runner_compiler.arg("--error-format=short"); | |
| info!("compiler invocation for doctest runner: {runner_compiler:?}"); | |
| let status = if !status.success() { | |
| status | |
| } else { | |
| let mut child_runner = match runner_compiler.spawn() { | |
| Ok(child) => child, | |
| Err(error) => { | |
| eprintln!("Failed to spawn {:?}: {error:?}", runner_compiler.get_program()); | |
| return (Duration::default(), Err(TestFailure::CompileError)); | |
| } | |
| }; | |
| child_runner.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 (instant.elapsed(), 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 (instant.elapsed(), Err(TestFailure::MissingErrorCodes(missing_codes))); | |
| } | |
| } | |
| } | |
| (false, false) => { | |
| return (instant.elapsed(), Err(TestFailure::CompileError)); | |
| } | |
| } | |
| let duration = instant.elapsed(); | |
| if doctest.no_run { | |
| return (duration, Ok(())); | |
| } | |
| // Run the code! | |
| let mut cmd; | |
| let output_file = make_maybe_absolute_path(output_file); | |
| if let Some(tool) = &rustdoc_options.test_runtool { | |
| let tool = make_maybe_absolute_path(tool.into()); | |
| cmd = Command::new(tool); | |
| cmd.args(&rustdoc_options.test_runtool_args); | |
| cmd.arg(&output_file); | |
| } else { | |
| cmd = Command::new(&output_file); | |
| if doctest.is_multiple_tests() { | |
| cmd.env("RUSTDOC_DOCTEST_BIN_PATH", &output_file); | |
| } | |
| } | |
| if let Some(run_directory) = &rustdoc_options.test_run_directory { | |
| cmd.current_dir(run_directory); | |
| } | |
| info!("running doctest executable: {cmd:?}"); | |
| let result = if doctest.is_multiple_tests() || rustdoc_options.no_capture { | |
| cmd.status().map(|status| process::Output { | |
| status, | |
| stdout: Vec::new(), | |
| stderr: Vec::new(), | |
| }) | |
| } else { | |
| cmd.output() | |
| }; | |
| match result { | |
| Err(e) => return (duration, Err(TestFailure::ExecutionError(e))), | |
| Ok(out) => { | |
| if langstr.should_panic && out.status.success() { | |
| return (duration, Err(TestFailure::UnexpectedRunPass)); | |
| } else if !langstr.should_panic && !out.status.success() { | |
| return (duration, Err(TestFailure::ExecutionFailure(out))); | |
| } | |
| } | |
| } | |
| (duration, 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(options).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 | |
| #[derive(Debug)] | |
| pub(crate) struct ScrapedDocTest { | |
| filename: FileName, | |
| line: usize, | |
| langstr: LangString, | |
| text: String, | |
| name: String, | |
| span: Span, | |
| code_mappings: Vec<CodeLineMapping>, | |
| global_crate_attrs: Vec<String>, | |
| } | |
| impl ScrapedDocTest { | |
| fn new( | |
| filename: FileName, | |
| line: usize, | |
| logical_path: Vec<String>, | |
| langstr: LangString, | |
| text: String, | |
| span: Span, | |
| code_mappings: Vec<CodeLineMapping>, | |
| global_crate_attrs: Vec<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.display(RemapPathScopeComponents::DOCUMENTATION) | |
| ); | |
| Self { filename, line, langstr, text, name, span, code_mappings, global_crate_attrs } | |
| } | |
| 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(name) => { | |
| name.path(RemapPathScopeComponents::DOCUMENTATION).to_path_buf() | |
| } | |
| _ => PathBuf::from(r"doctest.rs"), | |
| } | |
| } | |
| } | |
| pub(crate) trait DocTestVisitor { | |
| fn visit_test( | |
| &mut self, | |
| test: String, | |
| config: LangString, | |
| rel_line: MdRelLine, | |
| code_mappings: Vec<CodeLineMapping>, | |
| ); | |
| fn visit_header(&mut self, _name: &str, _level: u32) {} | |
| } | |
| #[derive(Clone, Debug, Hash, Eq, PartialEq)] | |
| pub(crate) struct MergeableTestKey { | |
| edition: Edition, | |
| global_crate_attrs_hash: u64, | |
| } | |
| struct CreateRunnableDocTests { | |
| standalone_tests: Vec<test::TestDescAndFn>, | |
| mergeable_tests: FxIndexMap<MergeableTestKey, 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: MergeDoctests, | |
| } | |
| impl CreateRunnableDocTests { | |
| fn new(rustdoc_options: RustdocOptions, opts: GlobalTestOptions) -> CreateRunnableDocTests { | |
| CreateRunnableDocTests { | |
| standalone_tests: Vec::new(), | |
| mergeable_tests: FxIndexMap::default(), | |
| opts, | |
| visited_tests: FxHashMap::default(), | |
| unused_extern_reports: Default::default(), | |
| compiling_test_count: AtomicUsize::new(0), | |
| can_merge_doctests: rustdoc_options.merge_doctests, | |
| rustdoc_options: Arc::new(rustdoc_options), | |
| } | |
| } | |
| fn add_test(&mut self, scraped_test: ScrapedDocTest, dcx: Option<DiagCtxtHandle<'_>>) { | |
| // For example `module/file.rs` would become `module_file_rs` | |
| // | |
| // Note that we are kind-of extending the definition of the MACRO scope here, but | |
| // after all `#[doc]` is kind-of a macro. | |
| let file = scraped_test | |
| .filename | |
| .display(RemapPathScopeComponents::MACRO) | |
| .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 = BuildDocTestBuilder::new(&scraped_test.text) | |
| .crate_name(&self.opts.crate_name) | |
| .global_crate_attrs(scraped_test.global_crate_attrs.clone()) | |
| .edition(edition) | |
| .can_merge_doctests(self.can_merge_doctests) | |
| .test_id(test_id) | |
| .lang_str(&scraped_test.langstr) | |
| .span(scraped_test.span) | |
| .code_mappings(&scraped_test.code_mappings) | |
| .build(dcx); | |
| let is_standalone = !doctest.can_be_merged | |
| || self.rustdoc_options.no_capture | |
| || 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(MergeableTestKey { | |
| edition, | |
| global_crate_attrs_hash: { | |
| let mut hasher = FxHasher::default(); | |
| scraped_test.global_crate_attrs.hash(&mut hasher); | |
| hasher.finish() | |
| }, | |
| }) | |
| .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 (wrapped, 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: wrapped.to_string(), | |
| 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), | |
| merged_test_code: None, | |
| }; | |
| 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, | |
| _code_mappings: Vec<CodeLineMapping>, | |
| ) { | |
| self.push(1 + rel_line.offset()); | |
| } | |
| } | |
| #[cfg(test)] | |
| mod tests; |