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