Skip to main content

bootstrap/utils/
render_tests.rs

1//! This module renders the JSON output of libtest into a human-readable form, trying to be as
2//! similar to libtest's native output as possible.
3//!
4//! This is needed because we need to use libtest in JSON mode to extract granular information
5//! about the executed tests. Doing so suppresses the human-readable output, and (compared to Cargo
6//! and rustc) libtest doesn't include the rendered human-readable output as a JSON field. We had
7//! to reimplement all the rendering logic in this module because of that.
8
9use std::fs::File;
10use std::io::{BufRead, BufReader, Read, Write};
11use std::process::ChildStdout;
12use std::time::Duration;
13
14use termcolor::{Color, ColorSpec, WriteColor};
15
16use crate::core::build_steps::test::failed_tests::RecordFailedTests;
17use crate::core::builder::Builder;
18use crate::utils::exec::BootstrapCommand;
19use crate::utils::helpers;
20
21const TERSE_TESTS_PER_LINE: usize = 88;
22
23pub(crate) fn add_flags_and_try_run_tests(
24    builder: &Builder<'_>,
25    cmd: &mut BootstrapCommand,
26    record_failed_tests: RecordFailedTests,
27) -> bool {
28    if !cmd.get_args().any(|arg| arg == "--") {
29        cmd.arg("--");
30    }
31    cmd.args(["-Z", "unstable-options", "--format", "json"]);
32
33    try_run_tests(builder, cmd, false, record_failed_tests)
34}
35
36pub(crate) fn try_run_tests(
37    builder: &Builder<'_>,
38    cmd: &mut BootstrapCommand,
39    stream: bool,
40    record_failed_tests: RecordFailedTests,
41) -> bool {
42    if run_tests(builder, cmd, stream, record_failed_tests) {
43        return true;
44    }
45
46    if builder.fail_fast {
47        helpers::exit_process(1);
48    }
49
50    builder.config.exec_ctx().add_to_delay_failure(format!("{cmd:?}"));
51
52    false
53}
54
55fn run_tests(
56    builder: &Builder<'_>,
57    cmd: &mut BootstrapCommand,
58    stream: bool,
59    record_failed_tests: RecordFailedTests,
60) -> bool {
61    builder.do_if_verbose(|| println!("running: {cmd:?}"));
62
63    let Some(mut streaming_command) = cmd.stream_capture_stdout(&builder.config.exec_ctx) else {
64        return true;
65    };
66
67    // This runs until the stdout of the child is closed, which means the child exited. We don't
68    // run this on another thread since the builder is not Sync.
69    let renderer =
70        Renderer::new(streaming_command.stdout.take().unwrap(), builder, record_failed_tests);
71    if stream {
72        renderer.stream_all();
73    } else {
74        renderer.render_all();
75    }
76
77    let status = streaming_command.wait(&builder.config.exec_ctx).unwrap();
78    if !status.success() && builder.is_verbose() {
79        println!(
80            "\n\ncommand did not execute successfully: {cmd:?}\n\
81             expected success, got: {status}",
82        );
83    }
84
85    status.success()
86}
87
88struct Renderer<'a> {
89    stdout: BufReader<ChildStdout>,
90    failures: Vec<TestOutcome>,
91    benches: Vec<BenchOutcome>,
92    builder: &'a Builder<'a>,
93    tests_count: Option<usize>,
94    executed_tests: usize,
95    /// Number of tests that were skipped due to already being up-to-date
96    /// (i.e. no relevant changes occurred since they last ran).
97    up_to_date_tests: usize,
98    ignored_tests: usize,
99    terse_tests_in_line: usize,
100    ci_latest_logged_percentage: f64,
101
102    failed_tests: Option<File>,
103}
104
105impl<'a> Renderer<'a> {
106    fn new(
107        stdout: ChildStdout,
108        builder: &'a Builder<'a>,
109        record_failed_tests: RecordFailedTests,
110    ) -> Self {
111        let failed_tests = record_failed_tests.path().and_then(|path| {
112            // create the file (overwriting any previous) to get ready to record new failed tests
113            match File::options().create(true).append(true).truncate(false).open(path) {
114                Ok(f) => Some(f),
115                Err(e) => {
116                    println!(
117                        "Couldn't open file {} to write test failures to: {e}. (attempted because `--record` was passed). Test failures will not be recorded.",
118                        path.display()
119                    );
120                    None
121                }
122            }
123        });
124
125        Self {
126            stdout: BufReader::new(stdout),
127            benches: Vec::new(),
128            failures: Vec::new(),
129            builder,
130            tests_count: None,
131            executed_tests: 0,
132            up_to_date_tests: 0,
133            ignored_tests: 0,
134            terse_tests_in_line: 0,
135            ci_latest_logged_percentage: 0.0,
136            failed_tests,
137        }
138    }
139
140    fn render_all(mut self) {
141        let mut line = Vec::new();
142        loop {
143            line.clear();
144            match self.stdout.read_until(b'\n', &mut line) {
145                Ok(_) => {}
146                Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => break,
147                Err(err) => panic!("failed to read output of test runner: {err}"),
148            }
149            if line.is_empty() {
150                break;
151            }
152
153            match serde_json::from_slice(&line) {
154                Ok(parsed) => self.render_message(parsed),
155                Err(_err) => {
156                    // Handle non-JSON output, for example when --nocapture is passed.
157                    let mut stdout = std::io::stdout();
158                    stdout.write_all(&line).unwrap();
159                    let _ = stdout.flush();
160                }
161            }
162        }
163
164        if self.up_to_date_tests > 0 {
165            let n = self.up_to_date_tests;
166            let s = if n > 1 { "s" } else { "" };
167            println!("help: ignored {n} up-to-date test{s}; use `--force-rerun` to prevent this\n");
168        }
169    }
170
171    /// Renders the stdout characters one by one
172    fn stream_all(mut self) {
173        let mut buffer = [0; 1];
174        loop {
175            match self.stdout.read(&mut buffer) {
176                Ok(0) => break,
177                Ok(_) => {
178                    let mut stdout = std::io::stdout();
179                    stdout.write_all(&buffer).unwrap();
180                    let _ = stdout.flush();
181                }
182                Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => break,
183                Err(err) => panic!("failed to read output of test runner: {err}"),
184            }
185        }
186    }
187
188    fn render_test_outcome(&mut self, outcome: Outcome<'_>, test: &TestOutcome) {
189        self.executed_tests += 1;
190
191        if let Outcome::Ignored { reason } = outcome {
192            self.ignored_tests += 1;
193            // Keep this in sync with the "up-to-date" ignore message inserted by compiletest.
194            if reason == Some("up-to-date") {
195                self.up_to_date_tests += 1;
196            }
197        }
198
199        #[cfg(feature = "build-metrics")]
200        self.builder.metrics.record_test(
201            &test.name,
202            match outcome {
203                Outcome::Ok | Outcome::BenchOk => build_helper::metrics::TestOutcome::Passed,
204                Outcome::Failed => build_helper::metrics::TestOutcome::Failed,
205                Outcome::Ignored { reason } => build_helper::metrics::TestOutcome::Ignored {
206                    ignore_reason: reason.map(|s| s.to_string()),
207                },
208            },
209            self.builder,
210        );
211
212        if self.builder.config.verbose_tests {
213            self.render_test_outcome_verbose(outcome, test);
214        } else if self.builder.config.is_running_on_ci() {
215            self.render_test_outcome_ci(outcome, test);
216        } else {
217            self.render_test_outcome_terse(outcome, test);
218        }
219    }
220
221    fn render_test_outcome_verbose(&self, outcome: Outcome<'_>, test: &TestOutcome) {
222        print!("test {} ... ", test.name);
223        self.builder.colored_stdout(|stdout| outcome.write_long(stdout)).unwrap();
224        if let Some(exec_time) = test.exec_time {
225            print!(" ({exec_time:.2?})");
226        }
227        println!();
228    }
229
230    fn render_test_outcome_terse(&mut self, outcome: Outcome<'_>, test: &TestOutcome) {
231        if self.terse_tests_in_line != 0
232            && self.terse_tests_in_line.is_multiple_of(TERSE_TESTS_PER_LINE)
233        {
234            if let Some(total) = self.tests_count {
235                let total = total.to_string();
236                let executed = format!("{:>width$}", self.executed_tests - 1, width = total.len());
237                print!(" {executed}/{total}");
238            }
239            println!();
240            self.terse_tests_in_line = 0;
241        }
242
243        self.terse_tests_in_line += 1;
244        self.builder.colored_stdout(|stdout| outcome.write_short(stdout, &test.name)).unwrap();
245        let _ = std::io::stdout().flush();
246    }
247
248    fn render_test_outcome_ci(&mut self, outcome: Outcome<'_>, test: &TestOutcome) {
249        if let Some(total) = self.tests_count {
250            let percent = self.executed_tests as f64 / total as f64;
251
252            if self.ci_latest_logged_percentage + 0.10 < percent {
253                let total = total.to_string();
254                let executed = format!("{:>width$}", self.executed_tests, width = total.len());
255                let pretty_percent = format!("{:.0}%", percent * 100.0);
256                let passed_tests = self.executed_tests - (self.failures.len() + self.ignored_tests);
257                println!(
258                    "{:<4} -- {executed}/{total}, {:>total_indent$} passed, {} failed, {} ignored",
259                    pretty_percent,
260                    passed_tests,
261                    self.failures.len(),
262                    self.ignored_tests,
263                    total_indent = total.len()
264                );
265                self.ci_latest_logged_percentage += 0.10;
266            }
267        }
268
269        self.builder.colored_stdout(|stdout| outcome.write_ci(stdout, &test.name)).unwrap();
270        let _ = std::io::stdout().flush();
271    }
272
273    fn render_suite_outcome(&self, outcome: Outcome<'_>, suite: &SuiteOutcome) {
274        // The terse output doesn't end with a newline, so we need to add it ourselves.
275        if !self.builder.config.verbose_tests {
276            println!();
277        }
278
279        if !self.failures.is_empty() {
280            println!("\nfailures:\n");
281            for failure in &self.failures {
282                if failure.stdout.is_some() || failure.message.is_some() {
283                    println!("---- {} stdout ----", failure.name);
284                    if let Some(stdout) = &failure.stdout {
285                        // Captured test output normally ends with a newline,
286                        // so only use `println!` if it doesn't.
287                        print!("{stdout}");
288                        if !stdout.ends_with('\n') {
289                            println!("\n\\ (no newline at end of output)");
290                        }
291                    }
292                    println!("---- {} stdout end ----", failure.name);
293                    if let Some(message) = &failure.message {
294                        println!("NOTE: {message}");
295                    }
296                }
297            }
298
299            println!("\nfailures:");
300            for failure in &self.failures {
301                println!("    {}", failure.name);
302            }
303
304            if self.failed_tests.is_some() {
305                println!(
306                    "This list of test failures was recorded.\nUse `x test --rerun` to retry just these {} failed tests.",
307                    self.failures.len(),
308                )
309            }
310        }
311
312        if !self.benches.is_empty() {
313            println!("\nbenchmarks:");
314
315            let mut rows = Vec::new();
316            for bench in &self.benches {
317                rows.push((
318                    &bench.name,
319                    format!("{:.2?}ns/iter", bench.median),
320                    format!("+/- {:.2?}", bench.deviation),
321                ));
322            }
323
324            let max_0 = rows.iter().map(|r| r.0.len()).max().unwrap_or(0);
325            let max_1 = rows.iter().map(|r| r.1.len()).max().unwrap_or(0);
326            let max_2 = rows.iter().map(|r| r.2.len()).max().unwrap_or(0);
327            for row in &rows {
328                println!("    {:<max_0$} {:>max_1$} {:>max_2$}", row.0, row.1, row.2);
329            }
330        }
331
332        print!("\ntest result: ");
333        self.builder.colored_stdout(|stdout| outcome.write_long(stdout)).unwrap();
334        println!(
335            ". {} passed; {} failed; {} ignored; {} measured; {} filtered out{time}\n",
336            suite.passed,
337            suite.failed,
338            suite.ignored,
339            suite.measured,
340            suite.filtered_out,
341            time = match suite.exec_time {
342                Some(t) => format!("; finished in {:.2?}", Duration::from_secs_f64(t)),
343                None => String::new(),
344            }
345        );
346    }
347
348    fn render_report(&self, report: &Report) {
349        let &Report { total_time, compilation_time } = report;
350        // Should match `write_merged_doctest_times` in `library/test/src/formatters/pretty.rs`.
351        println!(
352            "all doctests ran in {total_time:.2}s; merged doctests compilation took {compilation_time:.2}s"
353        );
354    }
355
356    fn render_message(&mut self, message: Message) {
357        match message {
358            Message::Suite(SuiteMessage::Started { test_count }) => {
359                println!("\nrunning {test_count} tests");
360                self.benches = vec![];
361                self.failures = vec![];
362                self.ignored_tests = 0;
363                self.executed_tests = 0;
364                self.terse_tests_in_line = 0;
365                self.tests_count = Some(test_count);
366            }
367            Message::Suite(SuiteMessage::Ok(outcome)) => {
368                self.render_suite_outcome(Outcome::Ok, &outcome);
369            }
370            Message::Suite(SuiteMessage::Failed(outcome)) => {
371                self.render_suite_outcome(Outcome::Failed, &outcome);
372            }
373            Message::Report(report) => {
374                self.render_report(&report);
375            }
376            Message::Bench(outcome) => {
377                // The formatting for benchmarks doesn't replicate 1:1 the formatting libtest
378                // outputs, mostly because libtest's formatting is broken in terse mode, which is
379                // the default used by our monorepo. We use a different formatting instead:
380                // successful benchmarks are just showed as "benchmarked"/"b", and the details are
381                // outputted at the bottom like failures.
382                let fake_test_outcome = TestOutcome {
383                    name: outcome.name.clone(),
384                    exec_time: None,
385                    stdout: None,
386                    message: None,
387                };
388                self.render_test_outcome(Outcome::BenchOk, &fake_test_outcome);
389                self.benches.push(outcome);
390            }
391            Message::Test(TestMessage::Ok(outcome)) => {
392                self.render_test_outcome(Outcome::Ok, &outcome);
393            }
394            Message::Test(TestMessage::Ignored(outcome)) => {
395                self.render_test_outcome(
396                    Outcome::Ignored { reason: outcome.message.as_deref() },
397                    &outcome,
398                );
399            }
400            Message::Test(TestMessage::Failed(outcome)) => {
401                self.render_test_outcome(Outcome::Failed, &outcome);
402                if let Some(failed_tests) = &mut self.failed_tests
403                    && let Err(e) = writeln!(failed_tests, "{}", outcome.name)
404                {
405                    eprintln!(
406                        "failed to write test failure to file: {e} (attempted because `--record` was passed)"
407                    );
408                }
409                self.failures.push(outcome);
410            }
411            Message::Test(TestMessage::Timeout { name }) => {
412                println!("test {name} has been running for a long time");
413            }
414            Message::Test(TestMessage::Started) => {} // Not useful
415        }
416    }
417}
418
419enum Outcome<'a> {
420    Ok,
421    BenchOk,
422    Failed,
423    Ignored { reason: Option<&'a str> },
424}
425
426impl Outcome<'_> {
427    fn write_short(&self, writer: &mut dyn WriteColor, name: &str) -> Result<(), std::io::Error> {
428        match self {
429            Outcome::Ok => {
430                writer.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?;
431                write!(writer, ".")?;
432            }
433            Outcome::BenchOk => {
434                writer.set_color(ColorSpec::new().set_fg(Some(Color::Cyan)))?;
435                write!(writer, "b")?;
436            }
437            Outcome::Failed => {
438                // Put failed tests on their own line and include the test name, so that it's faster
439                // to see which test failed without having to wait for them all to run.
440                writeln!(writer)?;
441                writer.set_color(ColorSpec::new().set_fg(Some(Color::Red)))?;
442                writeln!(writer, "{name} ... F")?;
443            }
444            Outcome::Ignored { .. } => {
445                writer.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))?;
446                write!(writer, "i")?;
447            }
448        }
449        writer.reset()
450    }
451
452    fn write_long(&self, writer: &mut dyn WriteColor) -> Result<(), std::io::Error> {
453        match self {
454            Outcome::Ok => {
455                writer.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?;
456                write!(writer, "ok")?;
457            }
458            Outcome::BenchOk => {
459                writer.set_color(ColorSpec::new().set_fg(Some(Color::Cyan)))?;
460                write!(writer, "benchmarked")?;
461            }
462            Outcome::Failed => {
463                writer.set_color(ColorSpec::new().set_fg(Some(Color::Red)))?;
464                write!(writer, "FAILED")?;
465            }
466            Outcome::Ignored { reason } => {
467                writer.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))?;
468                write!(writer, "ignored")?;
469                if let Some(reason) = reason {
470                    write!(writer, ", {reason}")?;
471                }
472            }
473        }
474        writer.reset()
475    }
476
477    fn write_ci(&self, writer: &mut dyn WriteColor, name: &str) -> Result<(), std::io::Error> {
478        match self {
479            Outcome::Ok | Outcome::BenchOk | Outcome::Ignored { .. } => {}
480            Outcome::Failed => {
481                writer.set_color(ColorSpec::new().set_fg(Some(Color::Red)))?;
482                writeln!(writer, "   {name} ... FAILED")?;
483            }
484        }
485        writer.reset()
486    }
487}
488
489#[derive(serde_derive::Deserialize)]
490#[serde(tag = "type", rename_all = "snake_case")]
491enum Message {
492    Suite(SuiteMessage),
493    Test(TestMessage),
494    Bench(BenchOutcome),
495    Report(Report),
496}
497
498#[derive(serde_derive::Deserialize)]
499#[serde(tag = "event", rename_all = "snake_case")]
500enum SuiteMessage {
501    Ok(SuiteOutcome),
502    Failed(SuiteOutcome),
503    Started { test_count: usize },
504}
505
506#[derive(serde_derive::Deserialize)]
507struct SuiteOutcome {
508    passed: usize,
509    failed: usize,
510    ignored: usize,
511    measured: usize,
512    filtered_out: usize,
513    /// The time it took to execute this test suite, or `None` if time measurement was not possible
514    /// (e.g. due to running on wasm).
515    exec_time: Option<f64>,
516}
517
518#[derive(serde_derive::Deserialize)]
519#[serde(tag = "event", rename_all = "snake_case")]
520enum TestMessage {
521    Ok(TestOutcome),
522    Failed(TestOutcome),
523    Ignored(TestOutcome),
524    Timeout { name: String },
525    Started,
526}
527
528#[derive(serde_derive::Deserialize)]
529struct BenchOutcome {
530    name: String,
531    median: f64,
532    deviation: f64,
533}
534
535#[derive(serde_derive::Deserialize)]
536struct TestOutcome {
537    name: String,
538    exec_time: Option<f64>,
539    stdout: Option<String>,
540    message: Option<String>,
541}
542
543/// Emitted when running doctests.
544#[derive(serde_derive::Deserialize)]
545struct Report {
546    total_time: f64,
547    compilation_time: f64,
548}