1use crate::{
26 config::elements::{FlakyResult, SlowTimeoutResult},
27 errors::{DisplayErrorChain, FormatVersionError, FormatVersionErrorInner, WriteEventError},
28 list::{RustTestSuite, TestList},
29 output_spec::{LiveSpec, OutputSpec},
30 reporter::events::{
31 ChildExecutionOutputDescription, ChildOutputDescription, ExecutionDescription,
32 ExecutionResultDescription, ExecutionStatuses, StressIndex, TestEvent, TestEventKind,
33 },
34 test_output::ChildSingleOutput,
35};
36use bstr::ByteSlice;
37use iddqd::{IdOrdItem, IdOrdMap, id_ord_map, id_upcast};
38use nextest_metadata::{MismatchReason, RustBinaryId, TestCaseName};
39use std::fmt::Write as _;
40
41#[derive(Copy, Clone)]
44#[repr(u8)]
45enum FormatMinorVersion {
46 First = 1,
62 #[doc(hidden)]
63 _Max,
64}
65
66#[derive(Copy, Clone)]
70#[repr(u8)]
71enum FormatMajorVersion {
72 Unstable = 0,
74 #[doc(hidden)]
75 _Max,
76}
77
78struct LibtestSuite<'cfg> {
80 failed: usize,
82 succeeded: usize,
84 ignored: usize,
86 filtered: usize,
88 running: usize,
90
91 stress_index: Option<StressIndex>,
92 meta: &'cfg RustTestSuite<'cfg>,
93 total: std::time::Duration,
95 ignore_block: Option<bytes::BytesMut>,
99 output_block: bytes::BytesMut,
105}
106
107impl IdOrdItem for LibtestSuite<'_> {
108 type Key<'a>
109 = &'a RustBinaryId
110 where
111 Self: 'a;
112
113 fn key(&self) -> Self::Key<'_> {
114 &self.meta.binary_id
115 }
116
117 id_upcast!();
118}
119
120#[derive(Copy, Clone, Debug)]
123pub enum EmitNextestObject {
124 Yes,
126 No,
128}
129
130const KIND_TEST: &str = "test";
131const KIND_SUITE: &str = "suite";
132
133const EVENT_STARTED: &str = "started";
134const EVENT_IGNORED: &str = "ignored";
135const EVENT_OK: &str = "ok";
136const EVENT_FAILED: &str = "failed";
137
138#[inline]
139fn fmt_err(err: std::fmt::Error) -> WriteEventError {
140 WriteEventError::Io(std::io::Error::new(std::io::ErrorKind::OutOfMemory, err))
141}
142
143pub struct LibtestReporter<'cfg> {
146 _minor: FormatMinorVersion,
147 _major: FormatMajorVersion,
148 test_list: Option<&'cfg TestList<'cfg>>,
149 test_suites: IdOrdMap<LibtestSuite<'cfg>>,
150 emit_nextest_obj: bool,
153}
154
155impl<'cfg> LibtestReporter<'cfg> {
156 pub fn new(
167 version: Option<&str>,
168 emit_nextest_obj: EmitNextestObject,
169 ) -> Result<Self, FormatVersionError> {
170 let emit_nextest_obj = matches!(emit_nextest_obj, EmitNextestObject::Yes);
171
172 let Some(version) = version else {
173 return Ok(Self {
174 _minor: FormatMinorVersion::First,
175 _major: FormatMajorVersion::Unstable,
176 test_list: None,
177 test_suites: IdOrdMap::new(),
178 emit_nextest_obj,
179 });
180 };
181 let Some((major, minor)) = version.split_once('.') else {
182 return Err(FormatVersionError {
183 input: version.into(),
184 error: FormatVersionErrorInner::InvalidFormat {
185 expected: "<major>.<minor>",
186 },
187 });
188 };
189
190 let major: u8 = major.parse().map_err(|err| FormatVersionError {
191 input: version.into(),
192 error: FormatVersionErrorInner::InvalidInteger {
193 which: "major",
194 err,
195 },
196 })?;
197
198 let minor: u8 = minor.parse().map_err(|err| FormatVersionError {
199 input: version.into(),
200 error: FormatVersionErrorInner::InvalidInteger {
201 which: "minor",
202 err,
203 },
204 })?;
205
206 let major = match major {
207 0 => FormatMajorVersion::Unstable,
208 o => {
209 return Err(FormatVersionError {
210 input: version.into(),
211 error: FormatVersionErrorInner::InvalidValue {
212 which: "major",
213 value: o,
214 range: (FormatMajorVersion::Unstable as u8)
215 ..(FormatMajorVersion::_Max as u8),
216 },
217 });
218 }
219 };
220
221 let minor = match minor {
222 1 => FormatMinorVersion::First,
223 o => {
224 return Err(FormatVersionError {
225 input: version.into(),
226 error: FormatVersionErrorInner::InvalidValue {
227 which: "minor",
228 value: o,
229 range: (FormatMinorVersion::First as u8)..(FormatMinorVersion::_Max as u8),
230 },
231 });
232 }
233 };
234
235 Ok(Self {
236 _major: major,
237 _minor: minor,
238 test_list: None,
239 test_suites: IdOrdMap::new(),
240 emit_nextest_obj,
241 })
242 }
243
244 pub(crate) fn write_event(&mut self, event: &TestEvent<'cfg>) -> Result<(), WriteEventError> {
245 let mut retries = None;
246
247 let (kind, eve, stress_index, test_instance) = match &event.kind {
249 TestEventKind::TestStarted {
250 stress_index,
251 test_instance,
252 ..
253 } => (KIND_TEST, EVENT_STARTED, stress_index, test_instance),
254 TestEventKind::TestSkipped {
255 stress_index,
256 test_instance,
257 reason: MismatchReason::Ignored,
258 } => {
259 (KIND_TEST, EVENT_STARTED, stress_index, test_instance)
263 }
264 TestEventKind::TestFinished {
265 stress_index,
266 test_instance,
267 run_statuses,
268 ..
269 } => {
270 if run_statuses.len() > 1 {
271 retries = Some(run_statuses.len());
272 }
273
274 (
275 KIND_TEST,
276 event_for_finished_test(run_statuses),
277 stress_index,
278 test_instance,
279 )
280 }
281 TestEventKind::RunStarted { test_list, .. } => {
282 self.test_list = Some(*test_list);
283 return Ok(());
284 }
285 TestEventKind::StressSubRunFinished { .. } | TestEventKind::RunFinished { .. } => {
286 for test_suite in std::mem::take(&mut self.test_suites) {
287 self.finalize(test_suite)?;
288 }
289
290 return Ok(());
291 }
292 _ => return Ok(()),
293 };
294
295 let test_list = self
297 .test_list
298 .expect("test_list should be set by RunStarted before any test events");
299 let suite_info = test_list
300 .get_suite(test_instance.binary_id)
301 .expect("suite should exist in test list");
302 let crate_name = suite_info.package.name();
303 let binary_name = &suite_info.binary_name;
304
305 let mut test_suite = match self.test_suites.entry(&suite_info.binary_id) {
307 id_ord_map::Entry::Vacant(e) => {
308 let (running, ignored, filtered) =
309 suite_info.status.test_cases().fold((0, 0, 0), |acc, case| {
310 if case.test_info.ignored {
311 (acc.0, acc.1 + 1, acc.2)
312 } else if case.test_info.filter_match.is_match() {
313 (acc.0 + 1, acc.1, acc.2)
314 } else {
315 (acc.0, acc.1, acc.2 + 1)
316 }
317 });
318
319 let mut out = bytes::BytesMut::with_capacity(1024);
320 write!(
321 &mut out,
322 r#"{{"type":"{KIND_SUITE}","event":"{EVENT_STARTED}","test_count":{}"#,
323 running + ignored,
324 )
325 .map_err(fmt_err)?;
326
327 if self.emit_nextest_obj {
328 write!(
329 out,
330 r#","nextest":{{"crate":"{crate_name}","test_binary":"{binary_name}","kind":"{}""#,
331 suite_info.kind,
332 )
333 .map_err(fmt_err)?;
334
335 if let Some(stress_index) = stress_index {
336 write!(out, r#","stress_index":{}"#, stress_index.current)
337 .map_err(fmt_err)?;
338 if let Some(total) = stress_index.total {
339 write!(out, r#","stress_total":{total}"#).map_err(fmt_err)?;
340 }
341 }
342
343 write!(out, "}}").map_err(fmt_err)?;
344 }
345
346 out.extend_from_slice(b"}\n");
347
348 e.insert(LibtestSuite {
349 running,
350 failed: 0,
351 succeeded: 0,
352 ignored,
353 filtered,
354 stress_index: *stress_index,
355 meta: suite_info,
356 total: std::time::Duration::new(0, 0),
357 ignore_block: None,
358 output_block: out,
359 })
360 }
361 id_ord_map::Entry::Occupied(e) => e.into_mut(),
362 };
363
364 let test_suite_mut = &mut *test_suite;
365 let out = &mut test_suite_mut.output_block;
366
367 if matches!(event.kind, TestEventKind::TestFinished { .. })
370 && let Some(ib) = test_suite_mut.ignore_block.take()
371 {
372 out.extend_from_slice(&ib);
373 }
374
375 write!(
384 out,
385 r#"{{"type":"{kind}","event":"{eve}","name":"{}::{}"#,
386 suite_info.package.name(),
387 suite_info.binary_name,
388 )
389 .map_err(fmt_err)?;
390
391 if let Some(stress_index) = stress_index {
392 write!(out, "@stress-{}", stress_index.current).map_err(fmt_err)?;
393 }
394 write!(out, "${}", test_instance.test_name).map_err(fmt_err)?;
395 if let Some(retry_count) = retries {
396 write!(out, "#{retry_count}\"").map_err(fmt_err)?;
397 } else {
398 out.extend_from_slice(b"\"");
399 }
400
401 match &event.kind {
402 TestEventKind::TestFinished { run_statuses, .. } => {
403 let last_status = run_statuses.last_status();
404
405 test_suite_mut.total += last_status.time_taken;
406 test_suite_mut.running -= 1;
407
408 write!(
413 out,
414 r#","exec_time":{}"#,
415 last_status.time_taken.as_secs_f64()
416 )
417 .map_err(fmt_err)?;
418
419 let is_flaky_fail = matches!(
422 run_statuses.describe(),
423 ExecutionDescription::Flaky {
424 result: FlakyResult::Fail,
425 ..
426 }
427 );
428
429 if is_flaky_fail {
430 test_suite_mut.failed += 1;
431 out.extend_from_slice(br#","reason":"flaky test treated as failure""#);
432 } else {
433 match &last_status.result {
434 ExecutionResultDescription::Fail { .. }
435 | ExecutionResultDescription::ExecFail => {
436 test_suite_mut.failed += 1;
437
438 write!(out, r#","stdout":""#).map_err(fmt_err)?;
441
442 strip_human_output_from_failed_test(
443 &last_status.output,
444 out,
445 test_instance.test_name,
446 )?;
447 out.extend_from_slice(b"\"");
448 }
449 ExecutionResultDescription::Timeout {
450 result: SlowTimeoutResult::Fail,
451 } => {
452 test_suite_mut.failed += 1;
453 out.extend_from_slice(br#","reason":"time limit exceeded""#);
454 }
455 _ => {
456 test_suite_mut.succeeded += 1;
457 }
458 }
459 }
460 }
461 TestEventKind::TestSkipped { .. } => {
462 test_suite_mut.running -= 1;
463
464 if test_suite_mut.ignore_block.is_none() {
465 test_suite_mut.ignore_block = Some(bytes::BytesMut::with_capacity(1024));
466 }
467
468 let ib = test_suite_mut
469 .ignore_block
470 .get_or_insert_with(|| bytes::BytesMut::with_capacity(1024));
471
472 writeln!(
473 ib,
474 r#"{{"type":"{kind}","event":"{EVENT_IGNORED}","name":"{}::{}${}"}}"#,
475 suite_info.package.name(),
476 suite_info.binary_name,
477 test_instance.test_name,
478 )
479 .map_err(fmt_err)?;
480 }
481 _ => {}
482 };
483
484 out.extend_from_slice(b"}\n");
485
486 if self.emit_nextest_obj {
487 {
488 use std::io::Write as _;
489
490 let mut stdout = std::io::stdout().lock();
491 stdout.write_all(out).map_err(WriteEventError::Io)?;
492 stdout.flush().map_err(WriteEventError::Io)?;
493 out.clear();
494 }
495
496 if test_suite_mut.running == 0 {
497 std::mem::drop(test_suite);
498
499 if let Some(test_suite) = self.test_suites.remove(&suite_info.binary_id) {
500 self.finalize(test_suite)?;
501 }
502 }
503 } else {
504 if test_suite_mut.running > 0 {
507 return Ok(());
508 }
509
510 std::mem::drop(test_suite);
511
512 if let Some(test_suite) = self.test_suites.remove(&suite_info.binary_id) {
513 self.finalize(test_suite)?;
514 }
515 }
516
517 Ok(())
518 }
519
520 fn finalize(&self, mut test_suite: LibtestSuite) -> Result<(), WriteEventError> {
521 let event = if test_suite.failed > 0 {
522 EVENT_FAILED
523 } else {
524 EVENT_OK
525 };
526
527 let out = &mut test_suite.output_block;
528 let suite_info = test_suite.meta;
529
530 if test_suite.running > 0 {
534 test_suite.filtered += test_suite.running;
535 }
536
537 write!(
538 out,
539 r#"{{"type":"{KIND_SUITE}","event":"{event}","passed":{},"failed":{},"ignored":{},"measured":0,"filtered_out":{},"exec_time":{}"#,
540 test_suite.succeeded,
541 test_suite.failed,
542 test_suite.ignored,
543 test_suite.filtered,
544 test_suite.total.as_secs_f64(),
545 )
546 .map_err(fmt_err)?;
547
548 if self.emit_nextest_obj {
549 let crate_name = suite_info.package.name();
550 let binary_name = &suite_info.binary_name;
551 write!(
552 out,
553 r#","nextest":{{"crate":"{crate_name}","test_binary":"{binary_name}","kind":"{}""#,
554 suite_info.kind,
555 )
556 .map_err(fmt_err)?;
557
558 if let Some(stress_index) = test_suite.stress_index {
559 write!(out, r#","stress_index":{}"#, stress_index.current).map_err(fmt_err)?;
560 if let Some(total) = stress_index.total {
561 write!(out, r#","stress_total":{total}"#).map_err(fmt_err)?;
562 }
563 }
564
565 write!(out, "}}").map_err(fmt_err)?;
566 }
567
568 out.extend_from_slice(b"}\n");
569
570 {
571 use std::io::Write as _;
572
573 let mut stdout = std::io::stdout().lock();
574 stdout.write_all(out).map_err(WriteEventError::Io)?;
575 stdout.flush().map_err(WriteEventError::Io)?;
576 }
577
578 Ok(())
579 }
580}
581
582fn event_for_finished_test<S: OutputSpec>(run_statuses: &ExecutionStatuses<S>) -> &'static str {
587 match run_statuses.describe() {
588 ExecutionDescription::Success { .. }
589 | ExecutionDescription::Flaky {
590 result: FlakyResult::Pass,
591 ..
592 } => EVENT_OK,
593 ExecutionDescription::Flaky {
594 result: FlakyResult::Fail,
595 ..
596 }
597 | ExecutionDescription::Failure { .. } => EVENT_FAILED,
598 }
599}
600
601fn strip_human_output_from_failed_test(
608 output: &ChildExecutionOutputDescription<LiveSpec>,
609 out: &mut bytes::BytesMut,
610 test_name: &TestCaseName,
611) -> Result<(), WriteEventError> {
612 match output {
613 ChildExecutionOutputDescription::Output {
614 result: _,
615 output,
616 errors,
617 } => {
618 match output {
619 ChildOutputDescription::Combined { output } => {
620 strip_human_stdout_or_combined(output, out, test_name)?;
621 }
622 ChildOutputDescription::Split { stdout, stderr } => {
623 #[cfg(not(test))]
627 {
628 debug_assert!(false, "libtest output requires CaptureStrategy::Combined");
629 }
630 if let Some(stdout) = stdout {
631 if !stdout.is_empty() {
632 write!(out, "--- STDOUT ---\\n").map_err(fmt_err)?;
633 strip_human_stdout_or_combined(stdout, out, test_name)?;
634 }
635 } else {
636 write!(out, "(stdout not captured)").map_err(fmt_err)?;
637 }
638 if let Some(stderr) = stderr {
640 if !stderr.is_empty() {
641 write!(out, "\\n--- STDERR ---\\n").map_err(fmt_err)?;
642 write!(out, "{}", EscapedString(stderr.as_str_lossy()))
643 .map_err(fmt_err)?;
644 }
645 } else {
646 writeln!(out, "\\n(stderr not captured)").map_err(fmt_err)?;
647 }
648 }
649 ChildOutputDescription::NotLoaded => {
650 unreachable!(
651 "attempted to strip output from output that was not loaded \
652 (the libtest reporter is not used during replay, where NotLoaded \
653 is produced)"
654 );
655 }
656 }
657
658 if let Some(errors) = errors {
659 write!(out, "\\n--- EXECUTION ERRORS ---\\n").map_err(fmt_err)?;
660 write!(
661 out,
662 "{}",
663 EscapedString(&DisplayErrorChain::new(errors).to_string())
664 )
665 .map_err(fmt_err)?;
666 }
667 }
668 ChildExecutionOutputDescription::StartError(error) => {
669 write!(out, "--- EXECUTION ERROR ---\\n").map_err(fmt_err)?;
670 write!(
671 out,
672 "{}",
673 EscapedString(&DisplayErrorChain::new(error).to_string())
674 )
675 .map_err(fmt_err)?;
676 }
677 }
678 Ok(())
679}
680
681fn strip_human_stdout_or_combined(
682 output: &ChildSingleOutput,
683 out: &mut bytes::BytesMut,
684 test_name: &TestCaseName,
685) -> Result<(), WriteEventError> {
686 if output.buf().contains_str("running 1 test\n") {
687 let lines = output
689 .lines()
690 .skip_while(|line| line != b"running 1 test")
691 .skip(1)
692 .take_while(|line| {
693 if let Some(name) = line
694 .strip_prefix(b"test ")
695 .and_then(|np| np.strip_suffix(b" ... FAILED"))
696 && test_name.as_bytes() == name
697 {
698 return false;
699 }
700
701 true
702 })
703 .map(|line| line.to_str_lossy());
704
705 for line in lines {
706 write!(out, "{}\\n", EscapedString(&line)).map_err(fmt_err)?;
708 }
709 } else {
710 write!(out, "{}", EscapedString(output.as_str_lossy())).map_err(fmt_err)?;
713 }
714
715 Ok(())
716}
717
718struct EscapedString<'s>(&'s str);
722
723impl std::fmt::Display for EscapedString<'_> {
724 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> ::std::fmt::Result {
725 let mut start = 0;
726 let s = self.0;
727
728 for (i, byte) in s.bytes().enumerate() {
729 let escaped = match byte {
730 b'"' => "\\\"",
731 b'\\' => "\\\\",
732 b'\x00' => "\\u0000",
733 b'\x01' => "\\u0001",
734 b'\x02' => "\\u0002",
735 b'\x03' => "\\u0003",
736 b'\x04' => "\\u0004",
737 b'\x05' => "\\u0005",
738 b'\x06' => "\\u0006",
739 b'\x07' => "\\u0007",
740 b'\x08' => "\\b",
741 b'\t' => "\\t",
742 b'\n' => "\\n",
743 b'\x0b' => "\\u000b",
744 b'\x0c' => "\\f",
745 b'\r' => "\\r",
746 b'\x0e' => "\\u000e",
747 b'\x0f' => "\\u000f",
748 b'\x10' => "\\u0010",
749 b'\x11' => "\\u0011",
750 b'\x12' => "\\u0012",
751 b'\x13' => "\\u0013",
752 b'\x14' => "\\u0014",
753 b'\x15' => "\\u0015",
754 b'\x16' => "\\u0016",
755 b'\x17' => "\\u0017",
756 b'\x18' => "\\u0018",
757 b'\x19' => "\\u0019",
758 b'\x1a' => "\\u001a",
759 b'\x1b' => "\\u001b",
760 b'\x1c' => "\\u001c",
761 b'\x1d' => "\\u001d",
762 b'\x1e' => "\\u001e",
763 b'\x1f' => "\\u001f",
764 b'\x7f' => "\\u007f",
765 _ => {
766 continue;
767 }
768 };
769
770 if start < i {
771 f.write_str(&s[start..i])?;
772 }
773
774 f.write_str(escaped)?;
775
776 start = i + 1;
777 }
778
779 if start != self.0.len() {
780 f.write_str(&s[start..])?;
781 }
782
783 Ok(())
784 }
785}
786
787#[cfg(test)]
788mod test {
789 use crate::{
790 config::elements::{FlakyResult, LeakTimeoutResult, SlowTimeoutResult},
791 errors::ChildStartError,
792 output_spec::LiveSpec,
793 reporter::{
794 events::{
795 ChildExecutionOutputDescription, ExecuteStatus, ExecutionResult,
796 ExecutionResultDescription, ExecutionStatuses, FailureDescription, FailureStatus,
797 RetryData,
798 },
799 structured::libtest::{
800 EVENT_FAILED, EVENT_OK, event_for_finished_test,
801 strip_human_output_from_failed_test,
802 },
803 },
804 test_output::{ChildExecutionOutput, ChildOutput, ChildSplitOutput},
805 };
806 use bytes::{Bytes, BytesMut};
807 use chrono::Local;
808 use color_eyre::eyre::eyre;
809 use nextest_metadata::TestCaseName;
810 use std::{io, sync::Arc, time::Duration};
811
812 #[test]
816 fn strips_human_output() {
817 const TEST_OUTPUT: &[&str] = &[
818 "\n",
819 "running 1 test\n",
820 "[src/index.rs:185] \"boop\" = \"boop\"\n",
821 "this is stdout\n",
822 "this i stderr\nok?\n",
823 "thread 'index::test::download_url_crates_io'",
824 r" panicked at src/index.rs:206:9:
825oh no
826stack backtrace:
827 0: rust_begin_unwind
828 at /rustc/a28077b28a02b92985b3a3faecf92813155f1ea1/library/std/src/panicking.rs:597:5
829 1: core::panicking::panic_fmt
830 at /rustc/a28077b28a02b92985b3a3faecf92813155f1ea1/library/core/src/panicking.rs:72:14
831 2: tame_index::index::test::download_url_crates_io
832 at ./src/index.rs:206:9
833 3: tame_index::index::test::download_url_crates_io::{{closure}}
834 at ./src/index.rs:179:33
835 4: core::ops::function::FnOnce::call_once
836 at /rustc/a28077b28a02b92985b3a3faecf92813155f1ea1/library/core/src/ops/function.rs:250:5
837 5: core::ops::function::FnOnce::call_once
838 at /rustc/a28077b28a02b92985b3a3faecf92813155f1ea1/library/core/src/ops/function.rs:250:5
839note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
840",
841 "test index::test::download_url_crates_io ... FAILED\n",
842 "\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",
843 ];
844
845 let output = {
846 let mut acc = BytesMut::new();
847 for line in TEST_OUTPUT {
848 acc.extend_from_slice(line.as_bytes());
849 }
850
851 ChildOutput::Combined {
852 output: acc.freeze().into(),
853 }
854 };
855
856 let mut actual = bytes::BytesMut::new();
857 let output_desc: ChildExecutionOutputDescription<_> = ChildExecutionOutput::Output {
858 result: None,
859 output,
860 errors: None,
861 }
862 .into();
863 strip_human_output_from_failed_test(
864 &output_desc,
865 &mut actual,
866 &TestCaseName::new("index::test::download_url_crates_io"),
867 )
868 .unwrap();
869
870 insta::assert_snapshot!(std::str::from_utf8(&actual).unwrap());
871 }
872
873 #[test]
874 fn strips_human_output_custom_test_harness() {
875 const TEST_OUTPUT: &[&str] = &["\n", "this is a custom test harness!!!\n", "1 test passed"];
877
878 let output = {
879 let mut acc = BytesMut::new();
880 for line in TEST_OUTPUT {
881 acc.extend_from_slice(line.as_bytes());
882 }
883
884 ChildOutput::Combined {
885 output: acc.freeze().into(),
886 }
887 };
888
889 let mut actual = bytes::BytesMut::new();
890 let output_desc: ChildExecutionOutputDescription<_> = ChildExecutionOutput::Output {
891 result: None,
892 output,
893 errors: None,
894 }
895 .into();
896 strip_human_output_from_failed_test(
897 &output_desc,
898 &mut actual,
899 &TestCaseName::new("non-existent"),
900 )
901 .unwrap();
902
903 insta::assert_snapshot!(std::str::from_utf8(&actual).unwrap());
904 }
905
906 #[test]
907 fn strips_human_output_start_error() {
908 let inner_error = eyre!("inner error");
909 let error = io::Error::other(inner_error);
910
911 let output: ChildExecutionOutputDescription<_> =
912 ChildExecutionOutput::StartError(ChildStartError::Spawn(Arc::new(error))).into();
913
914 let mut actual = bytes::BytesMut::new();
915 strip_human_output_from_failed_test(
916 &output,
917 &mut actual,
918 &TestCaseName::new("non-existent"),
919 )
920 .unwrap();
921
922 insta::assert_snapshot!(std::str::from_utf8(&actual).unwrap());
923 }
924
925 #[test]
926 fn strips_human_output_none() {
927 let mut actual = bytes::BytesMut::new();
928 let output_desc: ChildExecutionOutputDescription<_> = ChildExecutionOutput::Output {
929 result: None,
930 output: ChildOutput::Split(ChildSplitOutput {
931 stdout: None,
932 stderr: None,
933 }),
934 errors: None,
935 }
936 .into();
937 strip_human_output_from_failed_test(
938 &output_desc,
939 &mut actual,
940 &TestCaseName::new("non-existent"),
941 )
942 .unwrap();
943
944 insta::assert_snapshot!(std::str::from_utf8(&actual).unwrap());
945 }
946
947 fn make_test_output(result: ExecutionResult) -> ChildExecutionOutputDescription<LiveSpec> {
948 ChildExecutionOutput::Output {
949 result: Some(result),
950 output: ChildOutput::Split(ChildSplitOutput {
951 stdout: Some(Bytes::new().into()),
952 stderr: Some(Bytes::new().into()),
953 }),
954 errors: None,
955 }
956 .into()
957 }
958
959 fn make_passing_status(attempt: u32, total_attempts: u32) -> ExecuteStatus<LiveSpec> {
960 ExecuteStatus {
961 retry_data: RetryData {
962 attempt,
963 total_attempts,
964 },
965 output: make_test_output(ExecutionResult::Pass),
966 result: ExecutionResultDescription::Pass,
967 start_time: Local::now().fixed_offset(),
968 time_taken: Duration::from_secs(1),
969 is_slow: false,
970 delay_before_start: Duration::ZERO,
971 error_summary: None,
972 output_error_slice: None,
973 }
974 }
975
976 fn make_failing_status(attempt: u32, total_attempts: u32) -> ExecuteStatus<LiveSpec> {
977 ExecuteStatus {
978 retry_data: RetryData {
979 attempt,
980 total_attempts,
981 },
982 output: make_test_output(ExecutionResult::Fail {
983 failure_status: FailureStatus::ExitCode(1),
984 leaked: false,
985 }),
986 result: ExecutionResultDescription::Fail {
987 failure: FailureDescription::ExitCode { code: 1 },
988 leaked: false,
989 },
990 start_time: Local::now().fixed_offset(),
991 time_taken: Duration::from_secs(1),
992 is_slow: false,
993 delay_before_start: Duration::ZERO,
994 error_summary: None,
995 output_error_slice: None,
996 }
997 }
998
999 #[test]
1000 fn event_for_finished_test_variants() {
1001 let statuses =
1003 ExecutionStatuses::new(vec![make_passing_status(1, 1)], FlakyResult::default());
1004 assert_eq!(event_for_finished_test(&statuses), EVENT_OK, "single pass");
1005
1006 let statuses =
1008 ExecutionStatuses::new(vec![make_failing_status(1, 1)], FlakyResult::default());
1009 assert_eq!(
1010 event_for_finished_test(&statuses),
1011 EVENT_FAILED,
1012 "single failure"
1013 );
1014
1015 let statuses = ExecutionStatuses::new(
1017 vec![make_failing_status(1, 2), make_passing_status(2, 2)],
1018 FlakyResult::Pass,
1019 );
1020 assert_eq!(
1021 event_for_finished_test(&statuses),
1022 EVENT_OK,
1023 "flaky with result = pass"
1024 );
1025
1026 let statuses = ExecutionStatuses::new(
1028 vec![make_failing_status(1, 2), make_passing_status(2, 2)],
1029 FlakyResult::Fail,
1030 );
1031 assert_eq!(
1032 event_for_finished_test(&statuses),
1033 EVENT_FAILED,
1034 "flaky with result = fail"
1035 );
1036
1037 let statuses = ExecutionStatuses::new(
1039 vec![make_failing_status(1, 2), make_failing_status(2, 2)],
1040 FlakyResult::Pass,
1041 );
1042 assert_eq!(
1043 event_for_finished_test(&statuses),
1044 EVENT_FAILED,
1045 "all retries failed"
1046 );
1047
1048 let mut leak_pass = make_passing_status(1, 1);
1050 leak_pass.result = ExecutionResultDescription::Leak {
1051 result: LeakTimeoutResult::Pass,
1052 };
1053 let statuses = ExecutionStatuses::new(vec![leak_pass], FlakyResult::default());
1054 assert_eq!(
1055 event_for_finished_test(&statuses),
1056 EVENT_OK,
1057 "leak with result = pass"
1058 );
1059
1060 let mut leak_fail = make_passing_status(1, 1);
1062 leak_fail.result = ExecutionResultDescription::Leak {
1063 result: LeakTimeoutResult::Fail,
1064 };
1065 let statuses = ExecutionStatuses::new(vec![leak_fail], FlakyResult::default());
1066 assert_eq!(
1067 event_for_finished_test(&statuses),
1068 EVENT_FAILED,
1069 "leak with result = fail"
1070 );
1071
1072 let mut timeout_pass = make_passing_status(1, 1);
1074 timeout_pass.result = ExecutionResultDescription::Timeout {
1075 result: SlowTimeoutResult::Pass,
1076 };
1077 let statuses = ExecutionStatuses::new(vec![timeout_pass], FlakyResult::default());
1078 assert_eq!(
1079 event_for_finished_test(&statuses),
1080 EVENT_OK,
1081 "timeout with result = pass"
1082 );
1083
1084 let mut timeout_fail = make_passing_status(1, 1);
1086 timeout_fail.result = ExecutionResultDescription::Timeout {
1087 result: SlowTimeoutResult::Fail,
1088 };
1089 let statuses = ExecutionStatuses::new(vec![timeout_fail], FlakyResult::default());
1090 assert_eq!(
1091 event_for_finished_test(&statuses),
1092 EVENT_FAILED,
1093 "timeout with result = fail"
1094 );
1095
1096 let mut exec_fail = make_passing_status(1, 1);
1098 exec_fail.result = ExecutionResultDescription::ExecFail;
1099 let statuses = ExecutionStatuses::new(vec![exec_fail], FlakyResult::default());
1100 assert_eq!(
1101 event_for_finished_test(&statuses),
1102 EVENT_FAILED,
1103 "exec fail"
1104 );
1105 }
1106}