1use 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 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 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 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 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 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 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 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 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 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 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) => {} }
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 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 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#[derive(serde_derive::Deserialize)]
544struct Report {
545 total_time: f64,
546 compilation_time: f64,
547}