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