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