diff --git a/library/test/src/console.rs b/library/test/src/console.rs index 8f29f1dada528..13b2b3d502c81 100644 --- a/library/test/src/console.rs +++ b/library/test/src/console.rs @@ -281,23 +281,15 @@ fn on_test_event( Ok(()) } -/// A simple console test runner. -/// Runs provided tests reporting process and results to the stdout. -pub fn run_tests_console(opts: &TestOpts, tests: Vec) -> io::Result { +pub(crate) fn get_formatter(opts: &TestOpts, max_name_len: usize) -> Box { let output = match term::stdout() { None => OutputLocation::Raw(io::stdout()), Some(t) => OutputLocation::Pretty(t), }; - let max_name_len = tests - .iter() - .max_by_key(|t| len_if_padded(t)) - .map(|t| t.desc.name.as_slice().len()) - .unwrap_or(0); - let is_multithreaded = opts.test_threads.unwrap_or_else(get_concurrency) > 1; - let mut out: Box = match opts.format { + match opts.format { OutputFormat::Pretty => Box::new(PrettyFormatter::new( output, opts.use_color(), @@ -310,7 +302,19 @@ pub fn run_tests_console(opts: &TestOpts, tests: Vec) -> io::Resu } OutputFormat::Json => Box::new(JsonFormatter::new(output)), OutputFormat::Junit => Box::new(JunitFormatter::new(output)), - }; + } +} + +/// A simple console test runner. +/// Runs provided tests reporting process and results to the stdout. +pub fn run_tests_console(opts: &TestOpts, tests: Vec) -> io::Result { + let max_name_len = tests + .iter() + .max_by_key(|t| len_if_padded(t)) + .map(|t| t.desc.name.as_slice().len()) + .unwrap_or(0); + + let mut out = get_formatter(opts, max_name_len); let mut st = ConsoleTestState::new(opts)?; // Prevent the usage of `Instant` in some cases: diff --git a/library/test/src/formatters/json.rs b/library/test/src/formatters/json.rs index 92c1c0716f1f2..4a101f00d74b6 100644 --- a/library/test/src/formatters/json.rs +++ b/library/test/src/formatters/json.rs @@ -215,6 +215,17 @@ impl OutputFormatter for JsonFormatter { Ok(state.failed == 0) } + + fn write_merged_doctests_times( + &mut self, + total_time: f64, + compilation_time: f64, + ) -> io::Result<()> { + let newline = "\n"; + self.writeln_message(&format!( + r#"{{ "type": "report", "total_time": {total_time}, "compilation_time": {compilation_time} }}{newline}"#, + )) + } } /// A formatting utility used to print strings with characters in need of escaping. diff --git a/library/test/src/formatters/junit.rs b/library/test/src/formatters/junit.rs index 84153a9d05b59..1566f1cb1dac6 100644 --- a/library/test/src/formatters/junit.rs +++ b/library/test/src/formatters/junit.rs @@ -182,6 +182,16 @@ impl OutputFormatter for JunitFormatter { Ok(state.failed == 0) } + + fn write_merged_doctests_times( + &mut self, + total_time: f64, + compilation_time: f64, + ) -> io::Result<()> { + self.write_message(&format!( + "\n", + )) + } } fn parse_class_name(desc: &TestDesc) -> (String, String) { diff --git a/library/test/src/formatters/mod.rs b/library/test/src/formatters/mod.rs index f1225fecfef1a..c97cdb16a5079 100644 --- a/library/test/src/formatters/mod.rs +++ b/library/test/src/formatters/mod.rs @@ -33,6 +33,11 @@ pub(crate) trait OutputFormatter { state: &ConsoleTestState, ) -> io::Result<()>; fn write_run_finish(&mut self, state: &ConsoleTestState) -> io::Result; + fn write_merged_doctests_times( + &mut self, + total_time: f64, + compilation_time: f64, + ) -> io::Result<()>; } pub(crate) fn write_stderr_delimiter(test_output: &mut Vec, test_name: &TestName) { diff --git a/library/test/src/formatters/pretty.rs b/library/test/src/formatters/pretty.rs index bf3fc40db4117..5836138644aa4 100644 --- a/library/test/src/formatters/pretty.rs +++ b/library/test/src/formatters/pretty.rs @@ -303,4 +303,14 @@ impl OutputFormatter for PrettyFormatter { Ok(success) } + + fn write_merged_doctests_times( + &mut self, + total_time: f64, + compilation_time: f64, + ) -> io::Result<()> { + self.write_plain(format!( + "all doctests ran in {total_time:.2}s; merged doctests compilation took {compilation_time:.2}s\n", + )) + } } diff --git a/library/test/src/formatters/terse.rs b/library/test/src/formatters/terse.rs index b28120ab56e69..0720f06e174fc 100644 --- a/library/test/src/formatters/terse.rs +++ b/library/test/src/formatters/terse.rs @@ -295,4 +295,14 @@ impl OutputFormatter for TerseFormatter { Ok(success) } + + fn write_merged_doctests_times( + &mut self, + total_time: f64, + compilation_time: f64, + ) -> io::Result<()> { + self.write_plain(format!( + "all doctests ran in {total_time:.2}s; merged doctests compilation took {compilation_time:.2}s\n", + )) + } } diff --git a/library/test/src/lib.rs b/library/test/src/lib.rs index 1190bb56b97a0..d554807bbde70 100644 --- a/library/test/src/lib.rs +++ b/library/test/src/lib.rs @@ -244,6 +244,21 @@ fn make_owned_test(test: &&TestDescAndFn) -> TestDescAndFn { } } +/// Public API used by rustdoc to display the `total` and `compilation` times in the expected +/// format. +pub fn print_merged_doctests_times(args: &[String], total_time: f64, compilation_time: f64) { + let opts = match cli::parse_opts(args) { + Some(Ok(o)) => o, + Some(Err(msg)) => { + eprintln!("error: {msg}"); + process::exit(ERROR_EXIT_CODE); + } + None => return, + }; + let mut formatter = console::get_formatter(&opts, 0); + formatter.write_merged_doctests_times(total_time, compilation_time).unwrap(); +} + /// Invoked when unit tests terminate. Returns `Result::Err` if the test is /// considered a failure. By default, invokes `report()` and checks for a `0` /// result. diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs index 35ace6566381b..7d37668ba0129 100644 --- a/src/librustdoc/doctest.rs +++ b/src/librustdoc/doctest.rs @@ -12,7 +12,7 @@ use std::process::{self, Command, Stdio}; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; -use std::{fmt, panic, str}; +use std::{panic, str}; pub(crate) use make::{BuildDocTestBuilder, DocTestBuilder}; pub(crate) use markdown::test as test_markdown; @@ -60,24 +60,15 @@ impl MergedDoctestTimes { self.added_compilation_times += 1; } - fn display_times(&self) { + /// 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 { - println!("{self}"); + if self.added_compilation_times == 0 { + return None; } - } -} - -impl fmt::Display for MergedDoctestTimes { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "all doctests ran in {:.2}s; merged doctests compilation took {:.2}s", - self.total_time.elapsed().as_secs_f64(), - self.compilation_time.as_secs_f64(), - ) + Some((self.total_time.elapsed().as_secs_f64(), self.compilation_time.as_secs_f64())) } } @@ -400,15 +391,21 @@ pub(crate) fn run_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()); - times.display_times(); + if let Some((total_time, compilation_time)) = times { + test::print_merged_doctests_times(&test_args, total_time, compilation_time); + } }); } + let times = times.times_in_secs(); + // We ensure temp dir destructor is called. + std::mem::drop(temp_dir); + if let Some((total_time, compilation_time)) = times { + test::print_merged_doctests_times(&test_args, total_time, compilation_time); + } if nb_errors != 0 { - // We ensure temp dir destructor is called. - std::mem::drop(temp_dir); - times.display_times(); // FIXME(GuillaumeGomez): Uncomment the next line once #144297 has been merged. // std::process::exit(test::ERROR_EXIT_CODE); std::process::exit(101); diff --git a/tests/run-make/rustdoc-doctest-output-format/file.rs b/tests/run-make/rustdoc-doctest-output-format/file.rs new file mode 100644 index 0000000000000..51d17849fd718 --- /dev/null +++ b/tests/run-make/rustdoc-doctest-output-format/file.rs @@ -0,0 +1,3 @@ +//! ``` +//! let x = 12; +//! ``` diff --git a/tests/run-make/rustdoc-doctest-output-format/rmake.rs b/tests/run-make/rustdoc-doctest-output-format/rmake.rs new file mode 100644 index 0000000000000..a54d62114f1e6 --- /dev/null +++ b/tests/run-make/rustdoc-doctest-output-format/rmake.rs @@ -0,0 +1,79 @@ +//! Regression test to ensure that the output format is respected for doctests. +//! +//! Regression test for . + +use run_make_support::{rustdoc, serde_json}; + +fn run_test(edition: &str, format: Option<&str>) -> String { + let mut r = rustdoc(); + r.input("file.rs").edition(edition).arg("--test"); + if let Some(format) = format { + r.args(&["--test-args", "-Zunstable-options"]).args(&[ + "--test-args", + "--format", + "--test-args", + format, + ]); + } + r.run().stdout_utf8() +} + +fn check_json_output(edition: &str, expected_reports: usize) { + let out = run_test(edition, Some("json")); + let mut found_report = 0; + for (line_nb, line) in out.lines().enumerate() { + match serde_json::from_str::(&line) { + Ok(value) => { + if value.get("type") == Some(&serde_json::json!("report")) { + found_report += 1; + } + } + Err(error) => panic!( + "failed for {edition} edition (json format) at line {}: non-JSON value: {error}\n\ + ====== output ======\n{out}", + line_nb + 1, + ), + } + } + if found_report != expected_reports { + panic!( + "failed for {edition} edition (json format): expected {expected_reports} doctest \ + time `report`, found {found_report}\n====== output ======\n{out}", + ); + } +} + +fn check_non_json_output(edition: &str, expected_reports: usize) { + let out = run_test(edition, None); + let mut found_report = 0; + for (line_nb, line) in out.lines().enumerate() { + if line.starts_with('{') && serde_json::from_str::(&line).is_ok() { + panic!( + "failed for {edition} edition: unexpected json at line {}: `{line}`\n\ + ====== output ======\n{out}", + line_nb + 1 + ); + } + if line.starts_with("all doctests ran in") + && line.contains("; merged doctests compilation took ") + { + found_report += 1; + } + } + if found_report != expected_reports { + panic!( + "failed for {edition} edition: expected {expected_reports} doctest time `report`, \ + found {found_report}\n====== output ======\n{out}", + ); + } +} + +fn main() { + // Only the merged doctests generate the "times report". + check_json_output("2021", 0); + check_json_output("2024", 1); + + // Only the merged doctests generate the "times report". + check_non_json_output("2021", 0); + check_non_json_output("2024", 1); +}