use std::fmt::Write; use rustc_data_structures::fx::FxHashSet; use rustc_span::edition::Edition; use crate::doctest::{ DocTestBuilder, GlobalTestOptions, IndividualTestOptions, RunnableDocTest, RustdocOptions, ScrapedDocTest, TestFailure, UnusedExterns, run_test, }; use crate::html::markdown::{Ignore, LangString}; /// Convenient type to merge compatible doctests into one. pub(crate) struct DocTestRunner { crate_attrs: FxHashSet, ids: String, output: String, supports_color: bool, nb_tests: usize, } impl DocTestRunner { pub(crate) fn new() -> Self { Self { crate_attrs: FxHashSet::default(), ids: String::new(), output: String::new(), supports_color: true, nb_tests: 0, } } pub(crate) fn add_test( &mut self, doctest: &DocTestBuilder, scraped_test: &ScrapedDocTest, target_str: &str, ) { let ignore = match scraped_test.langstr.ignore { Ignore::All => true, Ignore::None => false, Ignore::Some(ref ignores) => ignores.iter().any(|s| target_str.contains(s)), }; if !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, ignore, self.nb_tests, &mut self.output ), )); self.supports_color &= doctest.supports_color; self.nb_tests += 1; } pub(crate) fn run_merged_tests( &mut self, test_options: IndividualTestOptions, edition: Edition, opts: &GlobalTestOptions, test_args: &[String], rustdoc_options: &RustdocOptions, ) -> Result { let mut code = "\ #![allow(unused_extern_crates)] #![allow(internal_features)] #![feature(test)] #![feature(rustc_attrs)] " .to_string(); for crate_attr in &self.crate_attrs { code.push_str(crate_attr); code.push('\n'); } if opts.attrs.is_empty() { // If there aren't any attributes supplied by #![doc(test(attr(...)))], then allow some // lints that are commonly triggered in doctests. The crate-level test attributes are // commonly used to make tests fail in case they trigger warnings, so having this there in // that case may cause some tests to pass when they shouldn't have. code.push_str("#![allow(unused)]\n"); } // Next, any attributes that came from the crate root via #![doc(test(attr(...)))]. for attr in &opts.attrs { code.push_str(&format!("#![{attr}]\n")); } code.push_str("extern crate test;\n"); let test_args = test_args.iter().fold(String::new(), |mut x, arg| { write!(x, "{arg:?}.to_string(),").unwrap(); x }); write!( code, "\ {output} mod __doctest_mod {{ use std::sync::OnceLock; use std::path::PathBuf; pub static BINARY_PATH: OnceLock = OnceLock::new(); pub const RUN_OPTION: &str = \"*doctest-inner-test\"; pub const BIN_OPTION: &str = \"*doctest-bin-path\"; #[allow(unused)] pub fn doctest_path() -> Option<&'static PathBuf> {{ self::BINARY_PATH.get() }} #[allow(unused)] pub fn doctest_runner(bin: &std::path::Path, test_nb: usize) -> Result<(), String> {{ let out = std::process::Command::new(bin) .arg(self::RUN_OPTION) .arg(test_nb.to_string()) .output() .expect(\"failed to run command\"); if !out.status.success() {{ Err(String::from_utf8_lossy(&out.stderr).to_string()) }} else {{ Ok(()) }} }} }} #[rustc_main] fn main() -> std::process::ExitCode {{ const TESTS: [test::TestDescAndFn; {nb_tests}] = [{ids}]; let bin_marker = std::ffi::OsStr::new(__doctest_mod::BIN_OPTION); let test_marker = std::ffi::OsStr::new(__doctest_mod::RUN_OPTION); let test_args = &[{test_args}]; let mut args = std::env::args_os().skip(1); while let Some(arg) = args.next() {{ if arg == bin_marker {{ let Some(binary) = args.next() else {{ panic!(\"missing argument after `{{}}`\", __doctest_mod::BIN_OPTION); }}; if crate::__doctest_mod::BINARY_PATH.set(binary.into()).is_err() {{ panic!(\"`{{}}` option was used more than once\", bin_marker.to_string_lossy()); }} return std::process::Termination::report(test::test_main(test_args, Vec::from(TESTS), None)); }} else if arg == test_marker {{ let Some(nb_test) = args.next() else {{ panic!(\"missing argument after `{{}}`\", __doctest_mod::RUN_OPTION); }}; if let Some(nb_test) = nb_test.to_str().and_then(|nb| nb.parse::().ok()) {{ if let Some(test) = TESTS.get(nb_test) {{ if let test::StaticTestFn(f) = test.testfn {{ return std::process::Termination::report(f()); }} }} }} panic!(\"Unexpected value after `{{}}`\", __doctest_mod::RUN_OPTION); }} }} eprintln!(\"WARNING: No argument provided so doctests will be run in the same process\"); std::process::Termination::report(test::test_main(test_args, Vec::from(TESTS), None)) }}", nb_tests = self.nb_tests, output = self.output, ids = self.ids, ) .expect("failed to generate test code"); 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, is_multiple_tests: true, }; let ret = run_test(runnable_test, rustdoc_options, self.supports_color, |_: UnusedExterns| {}); 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: &DocTestBuilder, scraped_test: &ScrapedDocTest, ignore: bool, id: usize, output: &mut String, ) -> String { let test_id = format!("__doctest_{id}"); if ignore { // We generate nothing else. writeln!(output, "mod {test_id} {{\n").unwrap(); } else { writeln!(output, "mod {test_id} {{\n{}{}", doctest.crates, doctest.maybe_crate_attrs) .unwrap(); if scraped_test.langstr.no_run { // To prevent having warnings about unused items since they're not called. writeln!(output, "#![allow(unused)]").unwrap(); } if doctest.has_main_fn { 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(); } } let not_running = ignore || scraped_test.langstr.no_run; writeln!( output, " pub const TEST: test::TestDescAndFn = test::TestDescAndFn::new_doctest( {test_name:?}, {ignore}, {file:?}, {line}, {no_run}, {should_panic}, test::StaticTestFn( || {{{runner}}}, )); }}", test_name = scraped_test.name, file = scraped_test.path(), line = scraped_test.line, no_run = scraped_test.langstr.no_run, should_panic = !scraped_test.langstr.no_run && scraped_test.langstr.should_panic, // 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 not_running { "test::assert_test_result(Ok::<(), String>(()))".to_string() } else { format!( " if let Some(bin_path) = crate::__doctest_mod::doctest_path() {{ test::assert_test_result(crate::__doctest_mod::doctest_runner(bin_path, {id})) }} else {{ test::assert_test_result(self::main()) }} ", ) }, ) .unwrap(); test_id }