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