1use crate::{
26 config::LeakTimeoutResult,
27 errors::{DisplayErrorChain, FormatVersionError, FormatVersionErrorInner, WriteEventError},
28 list::RustTestSuite,
29 reporter::events::{ExecutionResult, TestEvent, TestEventKind},
30 test_output::{ChildExecutionOutput, ChildOutput, ChildSingleOutput},
31};
32use bstr::ByteSlice;
33use nextest_metadata::MismatchReason;
34use std::{collections::BTreeMap, fmt::Write as _};
35
36#[derive(Copy, Clone)]
39#[repr(u8)]
40enum FormatMinorVersion {
41 First = 1,
57 #[doc(hidden)]
58 _Max,
59}
60
61#[derive(Copy, Clone)]
65#[repr(u8)]
66enum FormatMajorVersion {
67 Unstable = 0,
69 #[doc(hidden)]
70 _Max,
71}
72
73struct LibtestSuite<'cfg> {
75 failed: usize,
77 succeeded: usize,
79 ignored: usize,
81 filtered: usize,
83 running: usize,
85 meta: &'cfg RustTestSuite<'cfg>,
86 total: std::time::Duration,
88 ignore_block: Option<bytes::BytesMut>,
92 output_block: bytes::BytesMut,
98}
99
100#[derive(Copy, Clone, Debug)]
103pub enum EmitNextestObject {
104 Yes,
106 No,
108}
109
110const KIND_TEST: &str = "test";
111const KIND_SUITE: &str = "suite";
112
113const EVENT_STARTED: &str = "started";
114const EVENT_IGNORED: &str = "ignored";
115const EVENT_OK: &str = "ok";
116const EVENT_FAILED: &str = "failed";
117
118#[inline]
119fn fmt_err(err: std::fmt::Error) -> WriteEventError {
120 WriteEventError::Io(std::io::Error::new(std::io::ErrorKind::OutOfMemory, err))
121}
122
123pub struct LibtestReporter<'cfg> {
126 _minor: FormatMinorVersion,
127 _major: FormatMajorVersion,
128 test_suites: BTreeMap<&'cfg str, LibtestSuite<'cfg>>,
129 emit_nextest_obj: bool,
132}
133
134impl<'cfg> LibtestReporter<'cfg> {
135 pub fn new(
146 version: Option<&str>,
147 emit_nextest_obj: EmitNextestObject,
148 ) -> Result<Self, FormatVersionError> {
149 let emit_nextest_obj = matches!(emit_nextest_obj, EmitNextestObject::Yes);
150
151 let Some(version) = version else {
152 return Ok(Self {
153 _minor: FormatMinorVersion::First,
154 _major: FormatMajorVersion::Unstable,
155 test_suites: BTreeMap::new(),
156 emit_nextest_obj,
157 });
158 };
159 let Some((major, minor)) = version.split_once('.') else {
160 return Err(FormatVersionError {
161 input: version.into(),
162 error: FormatVersionErrorInner::InvalidFormat {
163 expected: "<major>.<minor>",
164 },
165 });
166 };
167
168 let major: u8 = major.parse().map_err(|err| FormatVersionError {
169 input: version.into(),
170 error: FormatVersionErrorInner::InvalidInteger {
171 which: "major",
172 err,
173 },
174 })?;
175
176 let minor: u8 = minor.parse().map_err(|err| FormatVersionError {
177 input: version.into(),
178 error: FormatVersionErrorInner::InvalidInteger {
179 which: "minor",
180 err,
181 },
182 })?;
183
184 let major = match major {
185 0 => FormatMajorVersion::Unstable,
186 o => {
187 return Err(FormatVersionError {
188 input: version.into(),
189 error: FormatVersionErrorInner::InvalidValue {
190 which: "major",
191 value: o,
192 range: (FormatMajorVersion::Unstable as u8)
193 ..(FormatMajorVersion::_Max as u8),
194 },
195 });
196 }
197 };
198
199 let minor = match minor {
200 1 => FormatMinorVersion::First,
201 o => {
202 return Err(FormatVersionError {
203 input: version.into(),
204 error: FormatVersionErrorInner::InvalidValue {
205 which: "minor",
206 value: o,
207 range: (FormatMinorVersion::First as u8)..(FormatMinorVersion::_Max as u8),
208 },
209 });
210 }
211 };
212
213 Ok(Self {
214 _major: major,
215 _minor: minor,
216 test_suites: BTreeMap::new(),
217 emit_nextest_obj,
218 })
219 }
220
221 pub(crate) fn write_event(&mut self, event: &TestEvent<'cfg>) -> Result<(), WriteEventError> {
222 let mut retries = None;
223
224 let (kind, eve, test_instance) = match &event.kind {
226 TestEventKind::TestStarted { test_instance, .. } => {
227 (KIND_TEST, EVENT_STARTED, test_instance)
228 }
229 TestEventKind::TestSkipped {
230 test_instance,
231 reason: MismatchReason::Ignored,
232 } => {
233 (KIND_TEST, EVENT_STARTED, test_instance)
237 }
238 TestEventKind::TestFinished {
239 test_instance,
240 run_statuses,
241 ..
242 } => {
243 if run_statuses.len() > 1 {
244 retries = Some(run_statuses.len());
245 }
246
247 (
248 KIND_TEST,
249 match run_statuses.last_status().result {
250 ExecutionResult::Pass
251 | ExecutionResult::Leak {
252 result: LeakTimeoutResult::Pass,
253 } => EVENT_OK,
254 ExecutionResult::Leak {
255 result: LeakTimeoutResult::Fail,
256 }
257 | ExecutionResult::Fail { .. }
258 | ExecutionResult::ExecFail
259 | ExecutionResult::Timeout => EVENT_FAILED,
260 },
261 test_instance,
262 )
263 }
264 TestEventKind::RunFinished { .. } => {
265 for test_suite in std::mem::take(&mut self.test_suites).into_values() {
266 self.finalize(test_suite)?;
267 }
268
269 return Ok(());
270 }
271 _ => return Ok(()),
272 };
273
274 let suite_info = test_instance.suite_info;
275 let crate_name = suite_info.package.name();
276 let binary_name = &suite_info.binary_name;
277
278 let test_suite = match self.test_suites.entry(suite_info.binary_id.as_str()) {
280 std::collections::btree_map::Entry::Vacant(e) => {
281 let (running, ignored, filtered) =
282 suite_info.status.test_cases().fold((0, 0, 0), |acc, tc| {
283 if tc.1.ignored {
284 (acc.0, acc.1 + 1, acc.2)
285 } else if tc.1.filter_match.is_match() {
286 (acc.0 + 1, acc.1, acc.2)
287 } else {
288 (acc.0, acc.1, acc.2 + 1)
289 }
290 });
291
292 let mut out = bytes::BytesMut::with_capacity(1024);
293 write!(
294 &mut out,
295 r#"{{"type":"{KIND_SUITE}","event":"{EVENT_STARTED}","test_count":{}"#,
296 running + ignored,
297 )
298 .map_err(fmt_err)?;
299
300 if self.emit_nextest_obj {
301 write!(
302 &mut out,
303 r#","nextest":{{"crate":"{crate_name}","test_binary":"{binary_name}","kind":"{}"}}"#,
304 suite_info.kind,
305 )
306 .map_err(fmt_err)?;
307 }
308
309 out.extend_from_slice(b"}\n");
310
311 e.insert(LibtestSuite {
312 running,
313 failed: 0,
314 succeeded: 0,
315 ignored,
316 filtered,
317 meta: test_instance.suite_info,
318 total: std::time::Duration::new(0, 0),
319 ignore_block: None,
320 output_block: out,
321 })
322 }
323 std::collections::btree_map::Entry::Occupied(e) => e.into_mut(),
324 };
325
326 let out = &mut test_suite.output_block;
327
328 if matches!(event.kind, TestEventKind::TestFinished { .. }) {
331 if let Some(ib) = test_suite.ignore_block.take() {
332 out.extend_from_slice(&ib);
333 }
334 }
335
336 write!(
345 out,
346 r#"{{"type":"{kind}","event":"{eve}","name":"{}::{}${}"#,
347 suite_info.package.name(),
348 suite_info.binary_name,
349 test_instance.name,
350 )
351 .map_err(fmt_err)?;
352
353 if let Some(retry_count) = retries {
354 write!(out, "#{retry_count}\"").map_err(fmt_err)?;
355 } else {
356 out.extend_from_slice(b"\"");
357 }
358
359 match &event.kind {
360 TestEventKind::TestFinished { run_statuses, .. } => {
361 let last_status = run_statuses.last_status();
362
363 test_suite.total += last_status.time_taken;
364 test_suite.running -= 1;
365
366 write!(
371 out,
372 r#","exec_time":{}"#,
373 last_status.time_taken.as_secs_f64()
374 )
375 .map_err(fmt_err)?;
376
377 match last_status.result {
378 ExecutionResult::Fail { .. } | ExecutionResult::ExecFail => {
379 test_suite.failed += 1;
380
381 write!(out, r#","stdout":""#).map_err(fmt_err)?;
384
385 strip_human_output_from_failed_test(
386 &last_status.output,
387 out,
388 test_instance.name,
389 )?;
390 out.extend_from_slice(b"\"");
391 }
392 ExecutionResult::Timeout => {
393 test_suite.failed += 1;
394 out.extend_from_slice(br#","reason":"time limit exceeded""#);
395 }
396 _ => {
397 test_suite.succeeded += 1;
398 }
399 }
400 }
401 TestEventKind::TestSkipped { .. } => {
402 test_suite.running -= 1;
403
404 if test_suite.ignore_block.is_none() {
405 test_suite.ignore_block = Some(bytes::BytesMut::with_capacity(1024));
406 }
407
408 let ib = test_suite
409 .ignore_block
410 .get_or_insert_with(|| bytes::BytesMut::with_capacity(1024));
411
412 writeln!(
413 ib,
414 r#"{{"type":"{kind}","event":"{EVENT_IGNORED}","name":"{}::{}${}"}}"#,
415 suite_info.package.name(),
416 suite_info.binary_name,
417 test_instance.name,
418 )
419 .map_err(fmt_err)?;
420 }
421 _ => {}
422 };
423
424 out.extend_from_slice(b"}\n");
425
426 if test_suite.running > 0 {
429 return Ok(());
430 }
431
432 if let Some(test_suite) = self.test_suites.remove(suite_info.binary_id.as_str()) {
433 self.finalize(test_suite)?;
434 }
435
436 Ok(())
437 }
438
439 fn finalize(&self, mut test_suite: LibtestSuite) -> Result<(), WriteEventError> {
440 let event = if test_suite.failed > 0 {
441 EVENT_FAILED
442 } else {
443 EVENT_OK
444 };
445
446 let out = &mut test_suite.output_block;
447 let suite_info = test_suite.meta;
448
449 if test_suite.running > 0 {
453 test_suite.filtered += test_suite.running;
454 }
455
456 write!(
457 out,
458 r#"{{"type":"{KIND_SUITE}","event":"{event}","passed":{},"failed":{},"ignored":{},"measured":0,"filtered_out":{},"exec_time":{}"#,
459 test_suite.succeeded,
460 test_suite.failed,
461 test_suite.ignored,
462 test_suite.filtered,
463 test_suite.total.as_secs_f64(),
464 )
465 .map_err(fmt_err)?;
466
467 if self.emit_nextest_obj {
468 let crate_name = suite_info.package.name();
469 let binary_name = &suite_info.binary_name;
470 write!(
471 out,
472 r#","nextest":{{"crate":"{crate_name}","test_binary":"{binary_name}","kind":"{}"}}"#,
473 suite_info.kind,
474 )
475 .map_err(fmt_err)?;
476 }
477
478 out.extend_from_slice(b"}\n");
479
480 {
481 use std::io::Write as _;
482
483 let mut stdout = std::io::stdout().lock();
484 stdout.write_all(out).map_err(WriteEventError::Io)?;
485 stdout.flush().map_err(WriteEventError::Io)?;
486 }
487
488 Ok(())
489 }
490}
491
492fn strip_human_output_from_failed_test(
499 output: &ChildExecutionOutput,
500 out: &mut bytes::BytesMut,
501 test_name: &str,
502) -> Result<(), WriteEventError> {
503 match output {
504 ChildExecutionOutput::Output {
505 result: _,
506 output,
507 errors,
508 } => {
509 match output {
510 ChildOutput::Combined { output } => {
511 strip_human_stdout_or_combined(output, out, test_name)?;
512 }
513 ChildOutput::Split(split) => {
514 #[cfg(not(test))]
518 {
519 debug_assert!(false, "libtest output requires CaptureStrategy::Combined");
520 }
521 if let Some(stdout) = &split.stdout {
522 if !stdout.is_empty() {
523 write!(out, "--- STDOUT ---\\n").map_err(fmt_err)?;
524 strip_human_stdout_or_combined(stdout, out, test_name)?;
525 }
526 } else {
527 write!(out, "(stdout not captured)").map_err(fmt_err)?;
528 }
529 if let Some(stderr) = &split.stderr {
531 if !stderr.is_empty() {
532 write!(out, "\\n--- STDERR ---\\n").map_err(fmt_err)?;
533 write!(out, "{}", EscapedString(stderr.as_str_lossy()))
534 .map_err(fmt_err)?;
535 }
536 } else {
537 writeln!(out, "\\n(stderr not captured)").map_err(fmt_err)?;
538 }
539 }
540 }
541
542 if let Some(errors) = errors {
543 write!(out, "\\n--- EXECUTION ERRORS ---\\n").map_err(fmt_err)?;
544 write!(
545 out,
546 "{}",
547 EscapedString(&DisplayErrorChain::new(errors).to_string())
548 )
549 .map_err(fmt_err)?;
550 }
551 }
552 ChildExecutionOutput::StartError(error) => {
553 write!(out, "--- EXECUTION ERROR ---\\n").map_err(fmt_err)?;
554 write!(
555 out,
556 "{}",
557 EscapedString(&DisplayErrorChain::new(error).to_string())
558 )
559 .map_err(fmt_err)?;
560 }
561 }
562 Ok(())
563}
564
565fn strip_human_stdout_or_combined(
566 output: &ChildSingleOutput,
567 out: &mut bytes::BytesMut,
568 test_name: &str,
569) -> Result<(), WriteEventError> {
570 if output.buf.contains_str("running 1 test\n") {
571 let lines = output
573 .lines()
574 .skip_while(|line| line != b"running 1 test")
575 .skip(1)
576 .take_while(|line| {
577 if let Some(name) = line
578 .strip_prefix(b"test ")
579 .and_then(|np| np.strip_suffix(b" ... FAILED"))
580 {
581 if test_name.as_bytes() == name {
582 return false;
583 }
584 }
585
586 true
587 })
588 .map(|line| line.to_str_lossy());
589
590 for line in lines {
591 write!(out, "{}\\n", EscapedString(&line)).map_err(fmt_err)?;
593 }
594 } else {
595 write!(out, "{}", EscapedString(output.as_str_lossy())).map_err(fmt_err)?;
598 }
599
600 Ok(())
601}
602
603struct EscapedString<'s>(&'s str);
607
608impl std::fmt::Display for EscapedString<'_> {
609 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> ::std::fmt::Result {
610 let mut start = 0;
611 let s = self.0;
612
613 for (i, byte) in s.bytes().enumerate() {
614 let escaped = match byte {
615 b'"' => "\\\"",
616 b'\\' => "\\\\",
617 b'\x00' => "\\u0000",
618 b'\x01' => "\\u0001",
619 b'\x02' => "\\u0002",
620 b'\x03' => "\\u0003",
621 b'\x04' => "\\u0004",
622 b'\x05' => "\\u0005",
623 b'\x06' => "\\u0006",
624 b'\x07' => "\\u0007",
625 b'\x08' => "\\b",
626 b'\t' => "\\t",
627 b'\n' => "\\n",
628 b'\x0b' => "\\u000b",
629 b'\x0c' => "\\f",
630 b'\r' => "\\r",
631 b'\x0e' => "\\u000e",
632 b'\x0f' => "\\u000f",
633 b'\x10' => "\\u0010",
634 b'\x11' => "\\u0011",
635 b'\x12' => "\\u0012",
636 b'\x13' => "\\u0013",
637 b'\x14' => "\\u0014",
638 b'\x15' => "\\u0015",
639 b'\x16' => "\\u0016",
640 b'\x17' => "\\u0017",
641 b'\x18' => "\\u0018",
642 b'\x19' => "\\u0019",
643 b'\x1a' => "\\u001a",
644 b'\x1b' => "\\u001b",
645 b'\x1c' => "\\u001c",
646 b'\x1d' => "\\u001d",
647 b'\x1e' => "\\u001e",
648 b'\x1f' => "\\u001f",
649 b'\x7f' => "\\u007f",
650 _ => {
651 continue;
652 }
653 };
654
655 if start < i {
656 f.write_str(&s[start..i])?;
657 }
658
659 f.write_str(escaped)?;
660
661 start = i + 1;
662 }
663
664 if start != self.0.len() {
665 f.write_str(&s[start..])?;
666 }
667
668 Ok(())
669 }
670}
671
672#[cfg(test)]
673mod test {
674 use crate::{
675 errors::ChildStartError,
676 reporter::structured::libtest::strip_human_output_from_failed_test,
677 test_output::{ChildExecutionOutput, ChildOutput, ChildSplitOutput},
678 };
679 use bytes::BytesMut;
680 use color_eyre::eyre::eyre;
681 use std::{io, sync::Arc};
682
683 #[test]
687 fn strips_human_output() {
688 const TEST_OUTPUT: &[&str] = &[
689 "\n",
690 "running 1 test\n",
691 "[src/index.rs:185] \"boop\" = \"boop\"\n",
692 "this is stdout\n",
693 "this i stderr\nok?\n",
694 "thread 'index::test::download_url_crates_io'",
695 r" panicked at src/index.rs:206:9:
696oh no
697stack backtrace:
698 0: rust_begin_unwind
699 at /rustc/a28077b28a02b92985b3a3faecf92813155f1ea1/library/std/src/panicking.rs:597:5
700 1: core::panicking::panic_fmt
701 at /rustc/a28077b28a02b92985b3a3faecf92813155f1ea1/library/core/src/panicking.rs:72:14
702 2: tame_index::index::test::download_url_crates_io
703 at ./src/index.rs:206:9
704 3: tame_index::index::test::download_url_crates_io::{{closure}}
705 at ./src/index.rs:179:33
706 4: core::ops::function::FnOnce::call_once
707 at /rustc/a28077b28a02b92985b3a3faecf92813155f1ea1/library/core/src/ops/function.rs:250:5
708 5: core::ops::function::FnOnce::call_once
709 at /rustc/a28077b28a02b92985b3a3faecf92813155f1ea1/library/core/src/ops/function.rs:250:5
710note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
711",
712 "test index::test::download_url_crates_io ... FAILED\n",
713 "\n\nfailures:\n\nfailures:\n index::test::download_url_crates_io\n\ntest result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 13 filtered out; finished in 0.01s\n",
714 ];
715
716 let output = {
717 let mut acc = BytesMut::new();
718 for line in TEST_OUTPUT {
719 acc.extend_from_slice(line.as_bytes());
720 }
721
722 ChildOutput::Combined {
723 output: acc.freeze().into(),
724 }
725 };
726
727 let mut actual = bytes::BytesMut::new();
728 strip_human_output_from_failed_test(
729 &ChildExecutionOutput::Output {
730 result: None,
731 output,
732 errors: None,
733 },
734 &mut actual,
735 "index::test::download_url_crates_io",
736 )
737 .unwrap();
738
739 insta::assert_snapshot!(std::str::from_utf8(&actual).unwrap());
740 }
741
742 #[test]
743 fn strips_human_output_custom_test_harness() {
744 const TEST_OUTPUT: &[&str] = &["\n", "this is a custom test harness!!!\n", "1 test passed"];
746
747 let output = {
748 let mut acc = BytesMut::new();
749 for line in TEST_OUTPUT {
750 acc.extend_from_slice(line.as_bytes());
751 }
752
753 ChildOutput::Combined {
754 output: acc.freeze().into(),
755 }
756 };
757
758 let mut actual = bytes::BytesMut::new();
759 strip_human_output_from_failed_test(
760 &ChildExecutionOutput::Output {
761 result: None,
762 output,
763 errors: None,
764 },
765 &mut actual,
766 "non-existent",
767 )
768 .unwrap();
769
770 insta::assert_snapshot!(std::str::from_utf8(&actual).unwrap());
771 }
772
773 #[test]
774 fn strips_human_output_start_error() {
775 let inner_error = eyre!("inner error");
776 let error = io::Error::new(io::ErrorKind::Other, inner_error);
777
778 let output = ChildExecutionOutput::StartError(ChildStartError::Spawn(Arc::new(error)));
779
780 let mut actual = bytes::BytesMut::new();
781 strip_human_output_from_failed_test(&output, &mut actual, "non-existent").unwrap();
782
783 insta::assert_snapshot!(std::str::from_utf8(&actual).unwrap());
784 }
785
786 #[test]
787 fn strips_human_output_none() {
788 let mut actual = bytes::BytesMut::new();
789 strip_human_output_from_failed_test(
790 &ChildExecutionOutput::Output {
791 result: None,
792 output: ChildOutput::Split(ChildSplitOutput {
793 stdout: None,
794 stderr: None,
795 }),
796 errors: None,
797 },
798 &mut actual,
799 "non-existent",
800 )
801 .unwrap();
802
803 insta::assert_snapshot!(std::str::from_utf8(&actual).unwrap());
804 }
805}