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