| use std::fmt::Write; |
| use std::time::Duration; |
| |
| use rustc_data_structures::fx::FxIndexSet; |
| 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: FxIndexSet<String>, |
| global_crate_attrs: FxIndexSet<String>, |
| ids: String, |
| output: String, |
| output_merged_tests: String, |
| supports_color: bool, |
| nb_tests: usize, |
| } |
| |
| impl DocTestRunner { |
| pub(crate) fn new() -> Self { |
| Self { |
| crate_attrs: FxIndexSet::default(), |
| global_crate_attrs: FxIndexSet::default(), |
| ids: String::new(), |
| output: String::new(), |
| output_merged_tests: 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()); |
| } |
| for line in &doctest.global_crate_attrs { |
| self.global_crate_attrs.insert(line.to_string()); |
| } |
| } |
| self.ids.push_str(&format!( |
| "tests.push({}::TEST);\n", |
| generate_mergeable_doctest( |
| doctest, |
| scraped_test, |
| ignore, |
| self.nb_tests, |
| &mut self.output, |
| &mut self.output_merged_tests, |
| ), |
| )); |
| self.supports_color &= doctest.supports_color; |
| self.nb_tests += 1; |
| } |
| |
| /// Returns a tuple containing the `Duration` of the compilation and the `Result` of the test. |
| /// |
| /// If compilation failed, it will return `Err`, otherwise it will return `Ok` containing if |
| /// the test ran successfully. |
| pub(crate) fn run_merged_tests( |
| &mut self, |
| test_options: IndividualTestOptions, |
| edition: Edition, |
| opts: &GlobalTestOptions, |
| test_args: &[String], |
| rustdoc_options: &RustdocOptions, |
| ) -> (Duration, Result<bool, ()>) { |
| let mut code = "\ |
| #![allow(unused_extern_crates)] |
| #![allow(internal_features)] |
| #![feature(test)] |
| #![feature(rustc_attrs)] |
| " |
| .to_string(); |
| |
| let mut code_prefix = String::new(); |
| |
| for crate_attr in &self.crate_attrs { |
| code_prefix.push_str(crate_attr); |
| code_prefix.push('\n'); |
| } |
| |
| if self.global_crate_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_prefix.push_str("#![allow(unused)]\n"); |
| } |
| |
| // Next, any attributes that came from #![doc(test(attr(...)))]. |
| for attr in &self.global_crate_attrs { |
| code_prefix.push_str(&format!("#![{attr}]\n")); |
| } |
| |
| code.push_str("extern crate test;\n"); |
| writeln!(code, "extern crate doctest_bundle_{edition} as doctest_bundle;").unwrap(); |
| |
| 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; |
| use std::process::ExitCode; |
| |
| pub static BINARY_PATH: OnceLock<PathBuf> = OnceLock::new(); |
| pub const RUN_OPTION: &str = \"RUSTDOC_DOCTEST_RUN_NB_TEST\"; |
| |
| #[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) -> ExitCode {{ |
| let out = std::process::Command::new(bin) |
| .env(self::RUN_OPTION, test_nb.to_string()) |
| .args(std::env::args().skip(1).collect::<Vec<_>>()) |
| .output() |
| .expect(\"failed to run command\"); |
| if !out.status.success() {{ |
| if let Some(code) = out.status.code() {{ |
| eprintln!(\"Test executable failed (exit status: {{code}}).\"); |
| }} else {{ |
| eprintln!(\"Test executable failed (terminated by signal).\"); |
| }} |
| if !out.stdout.is_empty() || !out.stderr.is_empty() {{ |
| eprintln!(); |
| }} |
| if !out.stdout.is_empty() {{ |
| eprintln!(\"stdout:\"); |
| eprintln!(\"{{}}\", String::from_utf8_lossy(&out.stdout)); |
| }} |
| if !out.stderr.is_empty() {{ |
| eprintln!(\"stderr:\"); |
| eprintln!(\"{{}}\", String::from_utf8_lossy(&out.stderr)); |
| }} |
| ExitCode::FAILURE |
| }} else {{ |
| ExitCode::SUCCESS |
| }} |
| }} |
| }} |
| |
| #[rustc_main] |
| fn main() -> std::process::ExitCode {{ |
| let tests = {{ |
| let mut tests = Vec::with_capacity({nb_tests}); |
| {ids} |
| tests |
| }}; |
| let test_args = &[{test_args}]; |
| const ENV_BIN: &'static str = \"RUSTDOC_DOCTEST_BIN_PATH\"; |
| |
| if let Ok(binary) = std::env::var(ENV_BIN) {{ |
| let _ = crate::__doctest_mod::BINARY_PATH.set(binary.into()); |
| unsafe {{ std::env::remove_var(ENV_BIN); }} |
| return std::process::Termination::report(test::test_main(test_args, tests, None)); |
| }} else if let Ok(nb_test) = std::env::var(__doctest_mod::RUN_OPTION) {{ |
| if let Ok(nb_test) = nb_test.parse::<usize>() {{ |
| if let Some(test) = tests.get(nb_test) {{ |
| if let test::StaticTestFn(f) = &test.testfn {{ |
| return std::process::Termination::report(f()); |
| }} |
| }} |
| }} |
| panic!(\"Unexpected value for `{{}}`\", __doctest_mod::RUN_OPTION); |
| }} |
| |
| eprintln!(\"WARNING: No rustdoc doctest environment variable provided so doctests will be run in \ |
| the same process\"); |
| std::process::Termination::report(test::test_main(test_args, tests, None)) |
| }}", |
| nb_tests = self.nb_tests, |
| output = self.output_merged_tests, |
| ids = self.ids, |
| ) |
| .expect("failed to generate test code"); |
| let runnable_test = RunnableDocTest { |
| full_test_code: format!("{code_prefix}{code}", code = self.output), |
| full_test_line_offset: 0, |
| test_opts: test_options, |
| global_opts: opts.clone(), |
| langstr: LangString::default(), |
| line: 0, |
| edition, |
| no_run: false, |
| merged_test_code: Some(code), |
| }; |
| let (duration, ret) = |
| run_test(runnable_test, rustdoc_options, self.supports_color, |_: UnusedExterns| {}); |
| (duration, 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, |
| output_merged_tests: &mut String, |
| ) -> String { |
| let test_id = format!("__doctest_{id}"); |
| |
| if ignore { |
| // We generate nothing else. |
| writeln!(output, "pub mod {test_id} {{}}\n").unwrap(); |
| } else { |
| writeln!(output, "pub mod {test_id} {{\n{}{}", doctest.crates, doctest.maybe_crate_attrs) |
| .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(); |
| } |
| writeln!( |
| output, |
| "\npub fn __main_fn() -> impl std::process::Termination {{ main() }} \n}}\n" |
| ) |
| .unwrap(); |
| } |
| let not_running = ignore || scraped_test.langstr.no_run; |
| writeln!( |
| output_merged_tests, |
| " |
| mod {test_id} {{ |
| 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(doctest_bundle::{test_id}::__main_fn()) |
| }} |
| ", |
| ) |
| }, |
| ) |
| .unwrap(); |
| test_id |
| } |