1use crate::{
26    config::elements::LeakTimeoutResult,
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::Leak {
272                            result: LeakTimeoutResult::Pass,
273                        } => EVENT_OK,
274                        ExecutionResult::Leak {
275                            result: LeakTimeoutResult::Fail,
276                        }
277                        | ExecutionResult::Fail { .. }
278                        | ExecutionResult::ExecFail
279                        | ExecutionResult::Timeout => EVENT_FAILED,
280                    },
281                    stress_index,
282                    test_instance,
283                )
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 suite_info = test_instance.suite_info;
296        let crate_name = suite_info.package.name();
297        let binary_name = &suite_info.binary_name;
298
299        let mut test_suite = match self.test_suites.entry(&suite_info.binary_id) {
301            id_ord_map::Entry::Vacant(e) => {
302                let (running, ignored, filtered) =
303                    suite_info.status.test_cases().fold((0, 0, 0), |acc, tc| {
304                        if tc.1.ignored {
305                            (acc.0, acc.1 + 1, acc.2)
306                        } else if tc.1.filter_match.is_match() {
307                            (acc.0 + 1, acc.1, acc.2)
308                        } else {
309                            (acc.0, acc.1, acc.2 + 1)
310                        }
311                    });
312
313                let mut out = bytes::BytesMut::with_capacity(1024);
314                write!(
315                    &mut out,
316                    r#"{{"type":"{KIND_SUITE}","event":"{EVENT_STARTED}","test_count":{}"#,
317                    running + ignored,
318                )
319                .map_err(fmt_err)?;
320
321                if self.emit_nextest_obj {
322                    write!(
323                        out,
324                        r#","nextest":{{"crate":"{crate_name}","test_binary":"{binary_name}","kind":"{}""#,
325                        suite_info.kind,
326                    )
327                    .map_err(fmt_err)?;
328
329                    if let Some(stress_index) = stress_index {
330                        write!(out, r#","stress_index":{}"#, stress_index.current)
331                            .map_err(fmt_err)?;
332                        if let Some(total) = stress_index.total {
333                            write!(out, r#","stress_total":{total}"#).map_err(fmt_err)?;
334                        }
335                    }
336
337                    write!(out, "}}").map_err(fmt_err)?;
338                }
339
340                out.extend_from_slice(b"}\n");
341
342                e.insert(LibtestSuite {
343                    running,
344                    failed: 0,
345                    succeeded: 0,
346                    ignored,
347                    filtered,
348                    stress_index: *stress_index,
349                    meta: test_instance.suite_info,
350                    total: std::time::Duration::new(0, 0),
351                    ignore_block: None,
352                    output_block: out,
353                })
354            }
355            id_ord_map::Entry::Occupied(e) => e.into_mut(),
356        };
357
358        let test_suite_mut = &mut *test_suite;
359        let out = &mut test_suite_mut.output_block;
360
361        if matches!(event.kind, TestEventKind::TestFinished { .. })
364            && let Some(ib) = test_suite_mut.ignore_block.take()
365        {
366            out.extend_from_slice(&ib);
367        }
368
369        write!(
378            out,
379            r#"{{"type":"{kind}","event":"{eve}","name":"{}::{}"#,
380            suite_info.package.name(),
381            suite_info.binary_name,
382        )
383        .map_err(fmt_err)?;
384
385        if let Some(stress_index) = stress_index {
386            write!(out, "@stress-{}", stress_index.current).map_err(fmt_err)?;
387        }
388        write!(out, "${}", test_instance.name).map_err(fmt_err)?;
389        if let Some(retry_count) = retries {
390            write!(out, "#{retry_count}\"").map_err(fmt_err)?;
391        } else {
392            out.extend_from_slice(b"\"");
393        }
394
395        match &event.kind {
396            TestEventKind::TestFinished { run_statuses, .. } => {
397                let last_status = run_statuses.last_status();
398
399                test_suite_mut.total += last_status.time_taken;
400                test_suite_mut.running -= 1;
401
402                write!(
407                    out,
408                    r#","exec_time":{}"#,
409                    last_status.time_taken.as_secs_f64()
410                )
411                .map_err(fmt_err)?;
412
413                match last_status.result {
414                    ExecutionResult::Fail { .. } | ExecutionResult::ExecFail => {
415                        test_suite_mut.failed += 1;
416
417                        write!(out, r#","stdout":""#).map_err(fmt_err)?;
420
421                        strip_human_output_from_failed_test(
422                            &last_status.output,
423                            out,
424                            test_instance.name,
425                        )?;
426                        out.extend_from_slice(b"\"");
427                    }
428                    ExecutionResult::Timeout => {
429                        test_suite_mut.failed += 1;
430                        out.extend_from_slice(br#","reason":"time limit exceeded""#);
431                    }
432                    _ => {
433                        test_suite_mut.succeeded += 1;
434                    }
435                }
436            }
437            TestEventKind::TestSkipped { .. } => {
438                test_suite_mut.running -= 1;
439
440                if test_suite_mut.ignore_block.is_none() {
441                    test_suite_mut.ignore_block = Some(bytes::BytesMut::with_capacity(1024));
442                }
443
444                let ib = test_suite_mut
445                    .ignore_block
446                    .get_or_insert_with(|| bytes::BytesMut::with_capacity(1024));
447
448                writeln!(
449                    ib,
450                    r#"{{"type":"{kind}","event":"{EVENT_IGNORED}","name":"{}::{}${}"}}"#,
451                    suite_info.package.name(),
452                    suite_info.binary_name,
453                    test_instance.name,
454                )
455                .map_err(fmt_err)?;
456            }
457            _ => {}
458        };
459
460        out.extend_from_slice(b"}\n");
461
462        if self.emit_nextest_obj {
463            {
464                use std::io::Write as _;
465
466                let mut stdout = std::io::stdout().lock();
467                stdout.write_all(out).map_err(WriteEventError::Io)?;
468                stdout.flush().map_err(WriteEventError::Io)?;
469                out.clear();
470            }
471
472            if test_suite_mut.running == 0 {
473                std::mem::drop(test_suite);
474
475                if let Some(test_suite) = self.test_suites.remove(&suite_info.binary_id) {
476                    self.finalize(test_suite)?;
477                }
478            }
479        } else {
480            if test_suite_mut.running > 0 {
483                return Ok(());
484            }
485
486            std::mem::drop(test_suite);
487
488            if let Some(test_suite) = self.test_suites.remove(&suite_info.binary_id) {
489                self.finalize(test_suite)?;
490            }
491        }
492
493        Ok(())
494    }
495
496    fn finalize(&self, mut test_suite: LibtestSuite) -> Result<(), WriteEventError> {
497        let event = if test_suite.failed > 0 {
498            EVENT_FAILED
499        } else {
500            EVENT_OK
501        };
502
503        let out = &mut test_suite.output_block;
504        let suite_info = test_suite.meta;
505
506        if test_suite.running > 0 {
510            test_suite.filtered += test_suite.running;
511        }
512
513        write!(
514            out,
515            r#"{{"type":"{KIND_SUITE}","event":"{event}","passed":{},"failed":{},"ignored":{},"measured":0,"filtered_out":{},"exec_time":{}"#,
516            test_suite.succeeded,
517            test_suite.failed,
518            test_suite.ignored,
519            test_suite.filtered,
520            test_suite.total.as_secs_f64(),
521        )
522        .map_err(fmt_err)?;
523
524        if self.emit_nextest_obj {
525            let crate_name = suite_info.package.name();
526            let binary_name = &suite_info.binary_name;
527            write!(
528                out,
529                r#","nextest":{{"crate":"{crate_name}","test_binary":"{binary_name}","kind":"{}""#,
530                suite_info.kind,
531            )
532            .map_err(fmt_err)?;
533
534            if let Some(stress_index) = test_suite.stress_index {
535                write!(out, r#","stress_index":{}"#, stress_index.current).map_err(fmt_err)?;
536                if let Some(total) = stress_index.total {
537                    write!(out, r#","stress_total":{total}"#).map_err(fmt_err)?;
538                }
539            }
540
541            write!(out, "}}").map_err(fmt_err)?;
542        }
543
544        out.extend_from_slice(b"}\n");
545
546        {
547            use std::io::Write as _;
548
549            let mut stdout = std::io::stdout().lock();
550            stdout.write_all(out).map_err(WriteEventError::Io)?;
551            stdout.flush().map_err(WriteEventError::Io)?;
552        }
553
554        Ok(())
555    }
556}
557
558fn strip_human_output_from_failed_test(
565    output: &ChildExecutionOutput,
566    out: &mut bytes::BytesMut,
567    test_name: &str,
568) -> Result<(), WriteEventError> {
569    match output {
570        ChildExecutionOutput::Output {
571            result: _,
572            output,
573            errors,
574        } => {
575            match output {
576                ChildOutput::Combined { output } => {
577                    strip_human_stdout_or_combined(output, out, test_name)?;
578                }
579                ChildOutput::Split(split) => {
580                    #[cfg(not(test))]
584                    {
585                        debug_assert!(false, "libtest output requires CaptureStrategy::Combined");
586                    }
587                    if let Some(stdout) = &split.stdout {
588                        if !stdout.is_empty() {
589                            write!(out, "--- STDOUT ---\\n").map_err(fmt_err)?;
590                            strip_human_stdout_or_combined(stdout, out, test_name)?;
591                        }
592                    } else {
593                        write!(out, "(stdout not captured)").map_err(fmt_err)?;
594                    }
595                    if let Some(stderr) = &split.stderr {
597                        if !stderr.is_empty() {
598                            write!(out, "\\n--- STDERR ---\\n").map_err(fmt_err)?;
599                            write!(out, "{}", EscapedString(stderr.as_str_lossy()))
600                                .map_err(fmt_err)?;
601                        }
602                    } else {
603                        writeln!(out, "\\n(stderr not captured)").map_err(fmt_err)?;
604                    }
605                }
606            }
607
608            if let Some(errors) = errors {
609                write!(out, "\\n--- EXECUTION ERRORS ---\\n").map_err(fmt_err)?;
610                write!(
611                    out,
612                    "{}",
613                    EscapedString(&DisplayErrorChain::new(errors).to_string())
614                )
615                .map_err(fmt_err)?;
616            }
617        }
618        ChildExecutionOutput::StartError(error) => {
619            write!(out, "--- EXECUTION ERROR ---\\n").map_err(fmt_err)?;
620            write!(
621                out,
622                "{}",
623                EscapedString(&DisplayErrorChain::new(error).to_string())
624            )
625            .map_err(fmt_err)?;
626        }
627    }
628    Ok(())
629}
630
631fn strip_human_stdout_or_combined(
632    output: &ChildSingleOutput,
633    out: &mut bytes::BytesMut,
634    test_name: &str,
635) -> Result<(), WriteEventError> {
636    if output.buf.contains_str("running 1 test\n") {
637        let lines = output
639            .lines()
640            .skip_while(|line| line != b"running 1 test")
641            .skip(1)
642            .take_while(|line| {
643                if let Some(name) = line
644                    .strip_prefix(b"test ")
645                    .and_then(|np| np.strip_suffix(b" ... FAILED"))
646                    && test_name.as_bytes() == name
647                {
648                    return false;
649                }
650
651                true
652            })
653            .map(|line| line.to_str_lossy());
654
655        for line in lines {
656            write!(out, "{}\\n", EscapedString(&line)).map_err(fmt_err)?;
658        }
659    } else {
660        write!(out, "{}", EscapedString(output.as_str_lossy())).map_err(fmt_err)?;
663    }
664
665    Ok(())
666}
667
668struct EscapedString<'s>(&'s str);
672
673impl std::fmt::Display for EscapedString<'_> {
674    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> ::std::fmt::Result {
675        let mut start = 0;
676        let s = self.0;
677
678        for (i, byte) in s.bytes().enumerate() {
679            let escaped = match byte {
680                b'"' => "\\\"",
681                b'\\' => "\\\\",
682                b'\x00' => "\\u0000",
683                b'\x01' => "\\u0001",
684                b'\x02' => "\\u0002",
685                b'\x03' => "\\u0003",
686                b'\x04' => "\\u0004",
687                b'\x05' => "\\u0005",
688                b'\x06' => "\\u0006",
689                b'\x07' => "\\u0007",
690                b'\x08' => "\\b",
691                b'\t' => "\\t",
692                b'\n' => "\\n",
693                b'\x0b' => "\\u000b",
694                b'\x0c' => "\\f",
695                b'\r' => "\\r",
696                b'\x0e' => "\\u000e",
697                b'\x0f' => "\\u000f",
698                b'\x10' => "\\u0010",
699                b'\x11' => "\\u0011",
700                b'\x12' => "\\u0012",
701                b'\x13' => "\\u0013",
702                b'\x14' => "\\u0014",
703                b'\x15' => "\\u0015",
704                b'\x16' => "\\u0016",
705                b'\x17' => "\\u0017",
706                b'\x18' => "\\u0018",
707                b'\x19' => "\\u0019",
708                b'\x1a' => "\\u001a",
709                b'\x1b' => "\\u001b",
710                b'\x1c' => "\\u001c",
711                b'\x1d' => "\\u001d",
712                b'\x1e' => "\\u001e",
713                b'\x1f' => "\\u001f",
714                b'\x7f' => "\\u007f",
715                _ => {
716                    continue;
717                }
718            };
719
720            if start < i {
721                f.write_str(&s[start..i])?;
722            }
723
724            f.write_str(escaped)?;
725
726            start = i + 1;
727        }
728
729        if start != self.0.len() {
730            f.write_str(&s[start..])?;
731        }
732
733        Ok(())
734    }
735}
736
737#[cfg(test)]
738mod test {
739    use crate::{
740        errors::ChildStartError,
741        reporter::structured::libtest::strip_human_output_from_failed_test,
742        test_output::{ChildExecutionOutput, ChildOutput, ChildSplitOutput},
743    };
744    use bytes::BytesMut;
745    use color_eyre::eyre::eyre;
746    use std::{io, sync::Arc};
747
748    #[test]
752    fn strips_human_output() {
753        const TEST_OUTPUT: &[&str] = &[
754            "\n",
755            "running 1 test\n",
756            "[src/index.rs:185] \"boop\" = \"boop\"\n",
757            "this is stdout\n",
758            "this i stderr\nok?\n",
759            "thread 'index::test::download_url_crates_io'",
760            r" panicked at src/index.rs:206:9:
761oh no
762stack backtrace:
763    0: rust_begin_unwind
764                at /rustc/a28077b28a02b92985b3a3faecf92813155f1ea1/library/std/src/panicking.rs:597:5
765    1: core::panicking::panic_fmt
766                at /rustc/a28077b28a02b92985b3a3faecf92813155f1ea1/library/core/src/panicking.rs:72:14
767    2: tame_index::index::test::download_url_crates_io
768                at ./src/index.rs:206:9
769    3: tame_index::index::test::download_url_crates_io::{{closure}}
770                at ./src/index.rs:179:33
771    4: core::ops::function::FnOnce::call_once
772                at /rustc/a28077b28a02b92985b3a3faecf92813155f1ea1/library/core/src/ops/function.rs:250:5
773    5: core::ops::function::FnOnce::call_once
774                at /rustc/a28077b28a02b92985b3a3faecf92813155f1ea1/library/core/src/ops/function.rs:250:5
775note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
776",
777            "test index::test::download_url_crates_io ... FAILED\n",
778            "\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",
779        ];
780
781        let output = {
782            let mut acc = BytesMut::new();
783            for line in TEST_OUTPUT {
784                acc.extend_from_slice(line.as_bytes());
785            }
786
787            ChildOutput::Combined {
788                output: acc.freeze().into(),
789            }
790        };
791
792        let mut actual = bytes::BytesMut::new();
793        strip_human_output_from_failed_test(
794            &ChildExecutionOutput::Output {
795                result: None,
796                output,
797                errors: None,
798            },
799            &mut actual,
800            "index::test::download_url_crates_io",
801        )
802        .unwrap();
803
804        insta::assert_snapshot!(std::str::from_utf8(&actual).unwrap());
805    }
806
807    #[test]
808    fn strips_human_output_custom_test_harness() {
809        const TEST_OUTPUT: &[&str] = &["\n", "this is a custom test harness!!!\n", "1 test passed"];
811
812        let output = {
813            let mut acc = BytesMut::new();
814            for line in TEST_OUTPUT {
815                acc.extend_from_slice(line.as_bytes());
816            }
817
818            ChildOutput::Combined {
819                output: acc.freeze().into(),
820            }
821        };
822
823        let mut actual = bytes::BytesMut::new();
824        strip_human_output_from_failed_test(
825            &ChildExecutionOutput::Output {
826                result: None,
827                output,
828                errors: None,
829            },
830            &mut actual,
831            "non-existent",
832        )
833        .unwrap();
834
835        insta::assert_snapshot!(std::str::from_utf8(&actual).unwrap());
836    }
837
838    #[test]
839    fn strips_human_output_start_error() {
840        let inner_error = eyre!("inner error");
841        let error = io::Error::other(inner_error);
842
843        let output = ChildExecutionOutput::StartError(ChildStartError::Spawn(Arc::new(error)));
844
845        let mut actual = bytes::BytesMut::new();
846        strip_human_output_from_failed_test(&output, &mut actual, "non-existent").unwrap();
847
848        insta::assert_snapshot!(std::str::from_utf8(&actual).unwrap());
849    }
850
851    #[test]
852    fn strips_human_output_none() {
853        let mut actual = bytes::BytesMut::new();
854        strip_human_output_from_failed_test(
855            &ChildExecutionOutput::Output {
856                result: None,
857                output: ChildOutput::Split(ChildSplitOutput {
858                    stdout: None,
859                    stderr: None,
860                }),
861                errors: None,
862            },
863            &mut actual,
864            "non-existent",
865        )
866        .unwrap();
867
868        insta::assert_snapshot!(std::str::from_utf8(&actual).unwrap());
869    }
870}