nextest_runner/reporter/structured/
libtest.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! libtest compatible output support
5//!
6//! Before 1.70.0 it was possible to send `--format json` to test executables and
7//! they would print out a JSON line to stdout for various events. This format
8//! was however not intended to be stabilized, so 1.70.0 made it nightly only as
9//! intended. However, machine readable output is immensely useful to other
10//! tooling that can much more easily consume it than parsing the output meant
11//! for humans.
12//!
13//! Since there already existed tooling using the libtest output format, this
14//! event aggregator replicates that format so that projects can seamlessly
15//! integrate cargo-nextest into their project, as well as get the benefit of
16//! running their tests on stable instead of being forced to use nightly.
17//!
18//! This implementation will attempt to follow the libtest format as it changes,
19//! but the rate of changes is quite low (see <https://github.com/rust-lang/rust/blob/master/library/test/src/formatters/json.rs>)
20//! so this should not be a big issue to users, however, if the format is changed,
21//! the changes will be replicated in this file with a new minor version allowing
22//! users to move to the new format or stick to the format version(s) they were
23//! using before
24
25use 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/// To support pinning the version of the output, we just use this simple enum
37/// to document changes as libtest output changes
38#[derive(Copy, Clone)]
39#[repr(u8)]
40enum FormatMinorVersion {
41    /// The libtest output as of `rustc 1.75.0-nightly (aa1a71e9e 2023-10-26)` with `--format json --report-time`
42    ///
43    /// * `{ "type": "suite", "event": "started", "test_count": <u32> }` - Start of a test binary run, always printed
44    ///   * `{ "type": "test", "event": "started", "name": "<name>" }` - Start of a single test, always printed
45    ///   * `{ "type": "test", "name": "<name>", "event": "ignored" }` - Printed if a test is ignored
46    ///     * Will have an additional `"message" = "<message>"` field if the there is a message in the ignore attribute eg. `#[ignore = "not yet implemented"]`
47    ///   * `{ "type": "test", "name": "<name>", "event": "ok", "exec_time": <f32> }` - Printed if a test runs successfully
48    ///   * `{ "type": "test", "name": "<name>", "event": "failed", "exec_time": <f32>, "stdout": "<escaped output collected during test execution>" }` - Printed if a test fails, note the stdout field actually contains both stdout and stderr despite the name
49    ///     * If `--ensure-time` is passed, libtest will add `"reason": "time limit exceeded"` if the test passes, but exceeds the time limit.
50    ///     * If `#[should_panic = "<expected message>"]` is used and message doesn't match, an additional `"message": "panic did not contain expected string\n<panic message>"` field is added
51    /// * `{ "type": "suite", "event": "<overall_status>", "passed": <u32>, "failed": <u32>, "ignored": <u32>, "measured": <u32>, "filtered_out": <u32>, "exec_time": <f32> }`
52    ///   * `event` will be `"ok"` if no failures occurred, or `"failed"` if `"failed" > 0`
53    ///   * `ignored` will be > 0 if there are `#[ignore]` tests and `--ignored` was not passed
54    ///   * `filtered_out` with be > 0 if there were tests not marked `#[ignore]` and `--ignored` was passed OR a test filter was passed and 1 or more tests were not executed
55    ///   * `measured` is only > 0 if running benchmarks
56    First = 1,
57    #[doc(hidden)]
58    _Max,
59}
60
61/// If libtest output is ever stabilized, this would most likely become the single
62/// version and we could get rid of the minor version, but who knows if that
63/// will ever happen
64#[derive(Copy, Clone)]
65#[repr(u8)]
66enum FormatMajorVersion {
67    /// The libtest output is unstable
68    Unstable = 0,
69    #[doc(hidden)]
70    _Max,
71}
72
73/// The accumulated stats for a single test binary
74struct LibtestSuite<'cfg> {
75    /// The number of tests that failed
76    failed: usize,
77    /// The number of tests that succeeded
78    succeeded: usize,
79    /// The number of tests that were ignored
80    ignored: usize,
81    /// The number of tests that were not executed due to filters
82    filtered: usize,
83    /// The number of tests in this suite that are still running
84    running: usize,
85    meta: &'cfg RustTestSuite<'cfg>,
86    /// The accumulated duration of every test that has been executed
87    total: std::time::Duration,
88    /// Libtest outputs outputs a `started` event for every test that isn't
89    /// filtered, including ignored tests, then outputs `ignored` events after
90    /// all the started events, so we just mimic that with a temporary buffer
91    ignore_block: Option<bytes::BytesMut>,
92    /// The single block of output accumulated for all tests executed in the binary,
93    /// this needs to be emitted as a single block to emulate how cargo test works,
94    /// executing each test binary serially and outputting a json line for each
95    /// event, as otherwise consumers would not be able to associate a single test
96    /// with its parent suite
97    output_block: bytes::BytesMut,
98}
99
100/// Determines whether the `nextest` subobject is added with additional metadata
101/// to events
102#[derive(Copy, Clone, Debug)]
103pub enum EmitNextestObject {
104    /// The `nextest` subobject is added
105    Yes,
106    /// The `nextest` subobject is not added
107    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
123/// A reporter that reports test runs in the same line-by-line JSON format as
124/// libtest itself
125pub struct LibtestReporter<'cfg> {
126    _minor: FormatMinorVersion,
127    _major: FormatMajorVersion,
128    test_suites: BTreeMap<&'cfg str, LibtestSuite<'cfg>>,
129    /// If true, we emit a `nextest` subobject with additional metadata in it
130    /// that consumers can use for easier integration if they wish
131    emit_nextest_obj: bool,
132}
133
134impl<'cfg> LibtestReporter<'cfg> {
135    /// Creates a new libtest reporter
136    ///
137    /// The version string is used to allow the reporter to evolve along with
138    /// libtest, but still be able to output a stable format for consumers. If
139    /// it is not specified the latest version of the format will be produced.
140    ///
141    /// If [`EmitNextestObject::Yes`] is passed, an additional `nextest` subobject
142    /// will be added to some events that includes additional metadata not produced
143    /// by libtest, but most consumers should still be able to consume them as
144    /// the base format itself is not changed
145    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        // Write the pieces of data that are the same across all events
225        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                // Note: unfortunately, libtest does not expose the message test in `#[ignore = "<message>"]`
234                // so we can't replicate the behavior of libtest exactly by emitting
235                // that message as additional metadata
236                (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        // Emit the suite start if this is the first test of the suite
279        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        // After all the tests have been started or ignored, put the block of
329        // tests that were ignored just as libtest does
330        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        // This is one place where we deviate from the behavior of libtest, by
337        // always prefixing the test name with both the crate and the binary name,
338        // as this information is quite important to distinguish tests from each
339        // other when testing inside a large workspace with hundreds or thousands
340        // of tests
341        //
342        // Additionally, a `#<n>` is used as a suffix if the test was retried,
343        // as libtest does not support that functionality
344        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                // libtest actually requires an additional `--report-time` flag to be
367                // passed for the exec_time information to be written. This doesn't
368                // really make sense when outputting structured output so we emit it
369                // unconditionally
370                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 the output from the test into the `stdout` (even
382                        // though it could contain stderr output as well).
383                        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 this is the last test of the suite, emit the test suite summary
427        // before emitting the entire block
428        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        // It's possible that a test failure etc has cancelled the run, in which
450        // case we might still have tests that are "running", even ones that are
451        // actually skipped, so we just add those to the filtered list
452        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
492/// Unfortunately, to replicate the libtest json output, we need to do our own
493/// filtering of the output to strip out the data emitted by libtest in the
494/// human format.
495///
496/// This function relies on the fact that nextest runs every individual test in
497/// isolation.
498fn 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                    // This is not a case that we hit because we always set CaptureStrategy to Combined. But
515                    // handle it in a reasonable fashion. (We do have a unit test for this case, so gate the
516                    // assertion with cfg(not(test)).)
517                    #[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 stderr is not empty, just write all of it in.
530                    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        // This is most likely the default test harness.
572        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            // This will never fail unless we are OOM
592            write!(out, "{}\\n", EscapedString(&line)).map_err(fmt_err)?;
593        }
594    } else {
595        // This is most likely a custom test harness. Just write out the entire
596        // output.
597        write!(out, "{}", EscapedString(output.as_str_lossy())).map_err(fmt_err)?;
598    }
599
600    Ok(())
601}
602
603/// Copy of the same string escaper used in libtest
604///
605/// <https://github.com/rust-lang/rust/blob/f440b5f0ea042cb2087a36631b20878f9847ee28/library/test/src/formatters/json.rs#L222-L285>
606struct 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    /// Validates that the human output portion from a failed test is stripped
684    /// out when writing a JSON string, as it is not part of the output when
685    /// libtest itself outputs the JSON, so we have 100% identical output to libtest
686    #[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        // For a custom test harness, we don't strip the human output at all.
745        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}