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::elements::{LeakTimeoutResult, SlowTimeoutResult},
27    errors::{DisplayErrorChain, FormatVersionError, FormatVersionErrorInner, WriteEventError},
28    list::RustTestSuite,
29    reporter::events::{ExecutionResult, StressIndex, TestEvent, TestEventKind},
30    test_output::{ChildExecutionOutput, ChildOutput, ChildSingleOutput},
31};
32use bstr::ByteSlice;
33use iddqd::{IdOrdItem, IdOrdMap, id_ord_map, id_upcast};
34use nextest_metadata::{MismatchReason, RustBinaryId};
35use std::fmt::Write as _;
36
37/// To support pinning the version of the output, we just use this simple enum
38/// to document changes as libtest output changes
39#[derive(Copy, Clone)]
40#[repr(u8)]
41enum FormatMinorVersion {
42    /// The libtest output as of `rustc 1.75.0-nightly (aa1a71e9e 2023-10-26)` with `--format json --report-time`
43    ///
44    /// * `{ "type": "suite", "event": "started", "test_count": <u32> }` - Start of a test binary run, always printed
45    ///   * `{ "type": "test", "event": "started", "name": "<name>" }` - Start of a single test, always printed
46    ///   * `{ "type": "test", "name": "<name>", "event": "ignored" }` - Printed if a test is ignored
47    ///     * Will have an additional `"message" = "<message>"` field if the there is a message in the ignore attribute eg. `#[ignore = "not yet implemented"]`
48    ///   * `{ "type": "test", "name": "<name>", "event": "ok", "exec_time": <f32> }` - Printed if a test runs successfully
49    ///   * `{ "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
50    ///     * If `--ensure-time` is passed, libtest will add `"reason": "time limit exceeded"` if the test passes, but exceeds the time limit.
51    ///     * 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
52    /// * `{ "type": "suite", "event": "<overall_status>", "passed": <u32>, "failed": <u32>, "ignored": <u32>, "measured": <u32>, "filtered_out": <u32>, "exec_time": <f32> }`
53    ///   * `event` will be `"ok"` if no failures occurred, or `"failed"` if `"failed" > 0`
54    ///   * `ignored` will be > 0 if there are `#[ignore]` tests and `--ignored` was not passed
55    ///   * `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
56    ///   * `measured` is only > 0 if running benchmarks
57    First = 1,
58    #[doc(hidden)]
59    _Max,
60}
61
62/// If libtest output is ever stabilized, this would most likely become the single
63/// version and we could get rid of the minor version, but who knows if that
64/// will ever happen
65#[derive(Copy, Clone)]
66#[repr(u8)]
67enum FormatMajorVersion {
68    /// The libtest output is unstable
69    Unstable = 0,
70    #[doc(hidden)]
71    _Max,
72}
73
74/// The accumulated stats for a single test binary
75struct LibtestSuite<'cfg> {
76    /// The number of tests that failed
77    failed: usize,
78    /// The number of tests that succeeded
79    succeeded: usize,
80    /// The number of tests that were ignored
81    ignored: usize,
82    /// The number of tests that were not executed due to filters
83    filtered: usize,
84    /// The number of tests in this suite that are still running
85    running: usize,
86
87    stress_index: Option<StressIndex>,
88    meta: &'cfg RustTestSuite<'cfg>,
89    /// The accumulated duration of every test that has been executed
90    total: std::time::Duration,
91    /// Libtest outputs outputs a `started` event for every test that isn't
92    /// filtered, including ignored tests, then outputs `ignored` events after
93    /// all the started events, so we just mimic that with a temporary buffer
94    ignore_block: Option<bytes::BytesMut>,
95    /// The single block of output accumulated for all tests executed in the binary,
96    /// this needs to be emitted as a single block to emulate how cargo test works,
97    /// executing each test binary serially and outputting a json line for each
98    /// event, as otherwise consumers would not be able to associate a single test
99    /// with its parent suite
100    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/// Determines whether the `nextest` subobject is added with additional metadata
117/// to events
118#[derive(Copy, Clone, Debug)]
119pub enum EmitNextestObject {
120    /// The `nextest` subobject is added
121    Yes,
122    /// The `nextest` subobject is not added
123    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
139/// A reporter that reports test runs in the same line-by-line JSON format as
140/// libtest itself
141pub struct LibtestReporter<'cfg> {
142    _minor: FormatMinorVersion,
143    _major: FormatMajorVersion,
144    test_suites: IdOrdMap<LibtestSuite<'cfg>>,
145    /// If true, we emit a `nextest` subobject with additional metadata in it
146    /// that consumers can use for easier integration if they wish
147    emit_nextest_obj: bool,
148}
149
150impl<'cfg> LibtestReporter<'cfg> {
151    /// Creates a new libtest reporter
152    ///
153    /// The version string is used to allow the reporter to evolve along with
154    /// libtest, but still be able to output a stable format for consumers. If
155    /// it is not specified the latest version of the format will be produced.
156    ///
157    /// If [`EmitNextestObject::Yes`] is passed, an additional `nextest` subobject
158    /// will be added to some events that includes additional metadata not produced
159    /// by libtest, but most consumers should still be able to consume them as
160    /// the base format itself is not changed
161    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        // Write the pieces of data that are the same across all events
241        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                // Note: unfortunately, libtest does not expose the message test in `#[ignore = "<message>"]`
253                // so we can't replicate the behavior of libtest exactly by emitting
254                // that message as additional metadata
255                (KIND_TEST, EVENT_STARTED, stress_index, test_instance)
256            }
257            TestEventKind::TestFinished {
258                stress_index,
259                test_instance,
260                run_statuses,
261                ..
262            } => {
263                if run_statuses.len() > 1 {
264                    retries = Some(run_statuses.len());
265                }
266
267                (
268                    KIND_TEST,
269                    match run_statuses.last_status().result {
270                        ExecutionResult::Pass
271                        | ExecutionResult::Timeout {
272                            result: SlowTimeoutResult::Pass,
273                        }
274                        | ExecutionResult::Leak {
275                            result: LeakTimeoutResult::Pass,
276                        } => EVENT_OK,
277                        ExecutionResult::Leak {
278                            result: LeakTimeoutResult::Fail,
279                        }
280                        | ExecutionResult::Fail { .. }
281                        | ExecutionResult::ExecFail
282                        | ExecutionResult::Timeout {
283                            result: SlowTimeoutResult::Fail,
284                        } => EVENT_FAILED,
285                    },
286                    stress_index,
287                    test_instance,
288                )
289            }
290            TestEventKind::StressSubRunFinished { .. } | TestEventKind::RunFinished { .. } => {
291                for test_suite in std::mem::take(&mut self.test_suites) {
292                    self.finalize(test_suite)?;
293                }
294
295                return Ok(());
296            }
297            _ => return Ok(()),
298        };
299
300        let suite_info = test_instance.suite_info;
301        let crate_name = suite_info.package.name();
302        let binary_name = &suite_info.binary_name;
303
304        // Emit the suite start if this is the first test of the suite
305        let mut test_suite = match self.test_suites.entry(&suite_info.binary_id) {
306            id_ord_map::Entry::Vacant(e) => {
307                let (running, ignored, filtered) =
308                    suite_info.status.test_cases().fold((0, 0, 0), |acc, case| {
309                        if case.test_info.ignored {
310                            (acc.0, acc.1 + 1, acc.2)
311                        } else if case.test_info.filter_match.is_match() {
312                            (acc.0 + 1, acc.1, acc.2)
313                        } else {
314                            (acc.0, acc.1, acc.2 + 1)
315                        }
316                    });
317
318                let mut out = bytes::BytesMut::with_capacity(1024);
319                write!(
320                    &mut out,
321                    r#"{{"type":"{KIND_SUITE}","event":"{EVENT_STARTED}","test_count":{}"#,
322                    running + ignored,
323                )
324                .map_err(fmt_err)?;
325
326                if self.emit_nextest_obj {
327                    write!(
328                        out,
329                        r#","nextest":{{"crate":"{crate_name}","test_binary":"{binary_name}","kind":"{}""#,
330                        suite_info.kind,
331                    )
332                    .map_err(fmt_err)?;
333
334                    if let Some(stress_index) = stress_index {
335                        write!(out, r#","stress_index":{}"#, stress_index.current)
336                            .map_err(fmt_err)?;
337                        if let Some(total) = stress_index.total {
338                            write!(out, r#","stress_total":{total}"#).map_err(fmt_err)?;
339                        }
340                    }
341
342                    write!(out, "}}").map_err(fmt_err)?;
343                }
344
345                out.extend_from_slice(b"}\n");
346
347                e.insert(LibtestSuite {
348                    running,
349                    failed: 0,
350                    succeeded: 0,
351                    ignored,
352                    filtered,
353                    stress_index: *stress_index,
354                    meta: test_instance.suite_info,
355                    total: std::time::Duration::new(0, 0),
356                    ignore_block: None,
357                    output_block: out,
358                })
359            }
360            id_ord_map::Entry::Occupied(e) => e.into_mut(),
361        };
362
363        let test_suite_mut = &mut *test_suite;
364        let out = &mut test_suite_mut.output_block;
365
366        // After all the tests have been started or ignored, put the block of
367        // tests that were ignored just as libtest does
368        if matches!(event.kind, TestEventKind::TestFinished { .. })
369            && let Some(ib) = test_suite_mut.ignore_block.take()
370        {
371            out.extend_from_slice(&ib);
372        }
373
374        // This is one place where we deviate from the behavior of libtest, by
375        // always prefixing the test name with both the crate and the binary name,
376        // as this information is quite important to distinguish tests from each
377        // other when testing inside a large workspace with hundreds or thousands
378        // of tests
379        //
380        // Additionally, a `#<n>` is used as a suffix if the test was retried,
381        // as libtest does not support that functionality
382        write!(
383            out,
384            r#"{{"type":"{kind}","event":"{eve}","name":"{}::{}"#,
385            suite_info.package.name(),
386            suite_info.binary_name,
387        )
388        .map_err(fmt_err)?;
389
390        if let Some(stress_index) = stress_index {
391            write!(out, "@stress-{}", stress_index.current).map_err(fmt_err)?;
392        }
393        write!(out, "${}", test_instance.name).map_err(fmt_err)?;
394        if let Some(retry_count) = retries {
395            write!(out, "#{retry_count}\"").map_err(fmt_err)?;
396        } else {
397            out.extend_from_slice(b"\"");
398        }
399
400        match &event.kind {
401            TestEventKind::TestFinished { run_statuses, .. } => {
402                let last_status = run_statuses.last_status();
403
404                test_suite_mut.total += last_status.time_taken;
405                test_suite_mut.running -= 1;
406
407                // libtest actually requires an additional `--report-time` flag to be
408                // passed for the exec_time information to be written. This doesn't
409                // really make sense when outputting structured output so we emit it
410                // unconditionally
411                write!(
412                    out,
413                    r#","exec_time":{}"#,
414                    last_status.time_taken.as_secs_f64()
415                )
416                .map_err(fmt_err)?;
417
418                match last_status.result {
419                    ExecutionResult::Fail { .. } | ExecutionResult::ExecFail => {
420                        test_suite_mut.failed += 1;
421
422                        // Write the output from the test into the `stdout` (even
423                        // though it could contain stderr output as well).
424                        write!(out, r#","stdout":""#).map_err(fmt_err)?;
425
426                        strip_human_output_from_failed_test(
427                            &last_status.output,
428                            out,
429                            test_instance.name,
430                        )?;
431                        out.extend_from_slice(b"\"");
432                    }
433                    ExecutionResult::Timeout {
434                        result: SlowTimeoutResult::Fail,
435                    } => {
436                        test_suite_mut.failed += 1;
437                        out.extend_from_slice(br#","reason":"time limit exceeded""#);
438                    }
439                    _ => {
440                        test_suite_mut.succeeded += 1;
441                    }
442                }
443            }
444            TestEventKind::TestSkipped { .. } => {
445                test_suite_mut.running -= 1;
446
447                if test_suite_mut.ignore_block.is_none() {
448                    test_suite_mut.ignore_block = Some(bytes::BytesMut::with_capacity(1024));
449                }
450
451                let ib = test_suite_mut
452                    .ignore_block
453                    .get_or_insert_with(|| bytes::BytesMut::with_capacity(1024));
454
455                writeln!(
456                    ib,
457                    r#"{{"type":"{kind}","event":"{EVENT_IGNORED}","name":"{}::{}${}"}}"#,
458                    suite_info.package.name(),
459                    suite_info.binary_name,
460                    test_instance.name,
461                )
462                .map_err(fmt_err)?;
463            }
464            _ => {}
465        };
466
467        out.extend_from_slice(b"}\n");
468
469        if self.emit_nextest_obj {
470            {
471                use std::io::Write as _;
472
473                let mut stdout = std::io::stdout().lock();
474                stdout.write_all(out).map_err(WriteEventError::Io)?;
475                stdout.flush().map_err(WriteEventError::Io)?;
476                out.clear();
477            }
478
479            if test_suite_mut.running == 0 {
480                std::mem::drop(test_suite);
481
482                if let Some(test_suite) = self.test_suites.remove(&suite_info.binary_id) {
483                    self.finalize(test_suite)?;
484                }
485            }
486        } else {
487            // If this is the last test of the suite, emit the test suite summary
488            // before emitting the entire block
489            if test_suite_mut.running > 0 {
490                return Ok(());
491            }
492
493            std::mem::drop(test_suite);
494
495            if let Some(test_suite) = self.test_suites.remove(&suite_info.binary_id) {
496                self.finalize(test_suite)?;
497            }
498        }
499
500        Ok(())
501    }
502
503    fn finalize(&self, mut test_suite: LibtestSuite) -> Result<(), WriteEventError> {
504        let event = if test_suite.failed > 0 {
505            EVENT_FAILED
506        } else {
507            EVENT_OK
508        };
509
510        let out = &mut test_suite.output_block;
511        let suite_info = test_suite.meta;
512
513        // It's possible that a test failure etc has cancelled the run, in which
514        // case we might still have tests that are "running", even ones that are
515        // actually skipped, so we just add those to the filtered list
516        if test_suite.running > 0 {
517            test_suite.filtered += test_suite.running;
518        }
519
520        write!(
521            out,
522            r#"{{"type":"{KIND_SUITE}","event":"{event}","passed":{},"failed":{},"ignored":{},"measured":0,"filtered_out":{},"exec_time":{}"#,
523            test_suite.succeeded,
524            test_suite.failed,
525            test_suite.ignored,
526            test_suite.filtered,
527            test_suite.total.as_secs_f64(),
528        )
529        .map_err(fmt_err)?;
530
531        if self.emit_nextest_obj {
532            let crate_name = suite_info.package.name();
533            let binary_name = &suite_info.binary_name;
534            write!(
535                out,
536                r#","nextest":{{"crate":"{crate_name}","test_binary":"{binary_name}","kind":"{}""#,
537                suite_info.kind,
538            )
539            .map_err(fmt_err)?;
540
541            if let Some(stress_index) = test_suite.stress_index {
542                write!(out, r#","stress_index":{}"#, stress_index.current).map_err(fmt_err)?;
543                if let Some(total) = stress_index.total {
544                    write!(out, r#","stress_total":{total}"#).map_err(fmt_err)?;
545                }
546            }
547
548            write!(out, "}}").map_err(fmt_err)?;
549        }
550
551        out.extend_from_slice(b"}\n");
552
553        {
554            use std::io::Write as _;
555
556            let mut stdout = std::io::stdout().lock();
557            stdout.write_all(out).map_err(WriteEventError::Io)?;
558            stdout.flush().map_err(WriteEventError::Io)?;
559        }
560
561        Ok(())
562    }
563}
564
565/// Unfortunately, to replicate the libtest json output, we need to do our own
566/// filtering of the output to strip out the data emitted by libtest in the
567/// human format.
568///
569/// This function relies on the fact that nextest runs every individual test in
570/// isolation.
571fn strip_human_output_from_failed_test(
572    output: &ChildExecutionOutput,
573    out: &mut bytes::BytesMut,
574    test_name: &str,
575) -> Result<(), WriteEventError> {
576    match output {
577        ChildExecutionOutput::Output {
578            result: _,
579            output,
580            errors,
581        } => {
582            match output {
583                ChildOutput::Combined { output } => {
584                    strip_human_stdout_or_combined(output, out, test_name)?;
585                }
586                ChildOutput::Split(split) => {
587                    // This is not a case that we hit because we always set CaptureStrategy to Combined. But
588                    // handle it in a reasonable fashion. (We do have a unit test for this case, so gate the
589                    // assertion with cfg(not(test)).)
590                    #[cfg(not(test))]
591                    {
592                        debug_assert!(false, "libtest output requires CaptureStrategy::Combined");
593                    }
594                    if let Some(stdout) = &split.stdout {
595                        if !stdout.is_empty() {
596                            write!(out, "--- STDOUT ---\\n").map_err(fmt_err)?;
597                            strip_human_stdout_or_combined(stdout, out, test_name)?;
598                        }
599                    } else {
600                        write!(out, "(stdout not captured)").map_err(fmt_err)?;
601                    }
602                    // If stderr is not empty, just write all of it in.
603                    if let Some(stderr) = &split.stderr {
604                        if !stderr.is_empty() {
605                            write!(out, "\\n--- STDERR ---\\n").map_err(fmt_err)?;
606                            write!(out, "{}", EscapedString(stderr.as_str_lossy()))
607                                .map_err(fmt_err)?;
608                        }
609                    } else {
610                        writeln!(out, "\\n(stderr not captured)").map_err(fmt_err)?;
611                    }
612                }
613            }
614
615            if let Some(errors) = errors {
616                write!(out, "\\n--- EXECUTION ERRORS ---\\n").map_err(fmt_err)?;
617                write!(
618                    out,
619                    "{}",
620                    EscapedString(&DisplayErrorChain::new(errors).to_string())
621                )
622                .map_err(fmt_err)?;
623            }
624        }
625        ChildExecutionOutput::StartError(error) => {
626            write!(out, "--- EXECUTION ERROR ---\\n").map_err(fmt_err)?;
627            write!(
628                out,
629                "{}",
630                EscapedString(&DisplayErrorChain::new(error).to_string())
631            )
632            .map_err(fmt_err)?;
633        }
634    }
635    Ok(())
636}
637
638fn strip_human_stdout_or_combined(
639    output: &ChildSingleOutput,
640    out: &mut bytes::BytesMut,
641    test_name: &str,
642) -> Result<(), WriteEventError> {
643    if output.buf.contains_str("running 1 test\n") {
644        // This is most likely the default test harness.
645        let lines = output
646            .lines()
647            .skip_while(|line| line != b"running 1 test")
648            .skip(1)
649            .take_while(|line| {
650                if let Some(name) = line
651                    .strip_prefix(b"test ")
652                    .and_then(|np| np.strip_suffix(b" ... FAILED"))
653                    && test_name.as_bytes() == name
654                {
655                    return false;
656                }
657
658                true
659            })
660            .map(|line| line.to_str_lossy());
661
662        for line in lines {
663            // This will never fail unless we are OOM
664            write!(out, "{}\\n", EscapedString(&line)).map_err(fmt_err)?;
665        }
666    } else {
667        // This is most likely a custom test harness. Just write out the entire
668        // output.
669        write!(out, "{}", EscapedString(output.as_str_lossy())).map_err(fmt_err)?;
670    }
671
672    Ok(())
673}
674
675/// Copy of the same string escaper used in libtest
676///
677/// <https://github.com/rust-lang/rust/blob/f440b5f0ea042cb2087a36631b20878f9847ee28/library/test/src/formatters/json.rs#L222-L285>
678struct EscapedString<'s>(&'s str);
679
680impl std::fmt::Display for EscapedString<'_> {
681    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> ::std::fmt::Result {
682        let mut start = 0;
683        let s = self.0;
684
685        for (i, byte) in s.bytes().enumerate() {
686            let escaped = match byte {
687                b'"' => "\\\"",
688                b'\\' => "\\\\",
689                b'\x00' => "\\u0000",
690                b'\x01' => "\\u0001",
691                b'\x02' => "\\u0002",
692                b'\x03' => "\\u0003",
693                b'\x04' => "\\u0004",
694                b'\x05' => "\\u0005",
695                b'\x06' => "\\u0006",
696                b'\x07' => "\\u0007",
697                b'\x08' => "\\b",
698                b'\t' => "\\t",
699                b'\n' => "\\n",
700                b'\x0b' => "\\u000b",
701                b'\x0c' => "\\f",
702                b'\r' => "\\r",
703                b'\x0e' => "\\u000e",
704                b'\x0f' => "\\u000f",
705                b'\x10' => "\\u0010",
706                b'\x11' => "\\u0011",
707                b'\x12' => "\\u0012",
708                b'\x13' => "\\u0013",
709                b'\x14' => "\\u0014",
710                b'\x15' => "\\u0015",
711                b'\x16' => "\\u0016",
712                b'\x17' => "\\u0017",
713                b'\x18' => "\\u0018",
714                b'\x19' => "\\u0019",
715                b'\x1a' => "\\u001a",
716                b'\x1b' => "\\u001b",
717                b'\x1c' => "\\u001c",
718                b'\x1d' => "\\u001d",
719                b'\x1e' => "\\u001e",
720                b'\x1f' => "\\u001f",
721                b'\x7f' => "\\u007f",
722                _ => {
723                    continue;
724                }
725            };
726
727            if start < i {
728                f.write_str(&s[start..i])?;
729            }
730
731            f.write_str(escaped)?;
732
733            start = i + 1;
734        }
735
736        if start != self.0.len() {
737            f.write_str(&s[start..])?;
738        }
739
740        Ok(())
741    }
742}
743
744#[cfg(test)]
745mod test {
746    use crate::{
747        errors::ChildStartError,
748        reporter::structured::libtest::strip_human_output_from_failed_test,
749        test_output::{ChildExecutionOutput, ChildOutput, ChildSplitOutput},
750    };
751    use bytes::BytesMut;
752    use color_eyre::eyre::eyre;
753    use std::{io, sync::Arc};
754
755    /// Validates that the human output portion from a failed test is stripped
756    /// out when writing a JSON string, as it is not part of the output when
757    /// libtest itself outputs the JSON, so we have 100% identical output to libtest
758    #[test]
759    fn strips_human_output() {
760        const TEST_OUTPUT: &[&str] = &[
761            "\n",
762            "running 1 test\n",
763            "[src/index.rs:185] \"boop\" = \"boop\"\n",
764            "this is stdout\n",
765            "this i stderr\nok?\n",
766            "thread 'index::test::download_url_crates_io'",
767            r" panicked at src/index.rs:206:9:
768oh no
769stack backtrace:
770    0: rust_begin_unwind
771                at /rustc/a28077b28a02b92985b3a3faecf92813155f1ea1/library/std/src/panicking.rs:597:5
772    1: core::panicking::panic_fmt
773                at /rustc/a28077b28a02b92985b3a3faecf92813155f1ea1/library/core/src/panicking.rs:72:14
774    2: tame_index::index::test::download_url_crates_io
775                at ./src/index.rs:206:9
776    3: tame_index::index::test::download_url_crates_io::{{closure}}
777                at ./src/index.rs:179:33
778    4: core::ops::function::FnOnce::call_once
779                at /rustc/a28077b28a02b92985b3a3faecf92813155f1ea1/library/core/src/ops/function.rs:250:5
780    5: core::ops::function::FnOnce::call_once
781                at /rustc/a28077b28a02b92985b3a3faecf92813155f1ea1/library/core/src/ops/function.rs:250:5
782note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
783",
784            "test index::test::download_url_crates_io ... FAILED\n",
785            "\n\nfailures:\n\nfailures:\n    index::test::download_url_crates_io\n\ntest result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 13 filtered out; finished in 0.01s\n",
786        ];
787
788        let output = {
789            let mut acc = BytesMut::new();
790            for line in TEST_OUTPUT {
791                acc.extend_from_slice(line.as_bytes());
792            }
793
794            ChildOutput::Combined {
795                output: acc.freeze().into(),
796            }
797        };
798
799        let mut actual = bytes::BytesMut::new();
800        strip_human_output_from_failed_test(
801            &ChildExecutionOutput::Output {
802                result: None,
803                output,
804                errors: None,
805            },
806            &mut actual,
807            "index::test::download_url_crates_io",
808        )
809        .unwrap();
810
811        insta::assert_snapshot!(std::str::from_utf8(&actual).unwrap());
812    }
813
814    #[test]
815    fn strips_human_output_custom_test_harness() {
816        // For a custom test harness, we don't strip the human output at all.
817        const TEST_OUTPUT: &[&str] = &["\n", "this is a custom test harness!!!\n", "1 test passed"];
818
819        let output = {
820            let mut acc = BytesMut::new();
821            for line in TEST_OUTPUT {
822                acc.extend_from_slice(line.as_bytes());
823            }
824
825            ChildOutput::Combined {
826                output: acc.freeze().into(),
827            }
828        };
829
830        let mut actual = bytes::BytesMut::new();
831        strip_human_output_from_failed_test(
832            &ChildExecutionOutput::Output {
833                result: None,
834                output,
835                errors: None,
836            },
837            &mut actual,
838            "non-existent",
839        )
840        .unwrap();
841
842        insta::assert_snapshot!(std::str::from_utf8(&actual).unwrap());
843    }
844
845    #[test]
846    fn strips_human_output_start_error() {
847        let inner_error = eyre!("inner error");
848        let error = io::Error::other(inner_error);
849
850        let output = ChildExecutionOutput::StartError(ChildStartError::Spawn(Arc::new(error)));
851
852        let mut actual = bytes::BytesMut::new();
853        strip_human_output_from_failed_test(&output, &mut actual, "non-existent").unwrap();
854
855        insta::assert_snapshot!(std::str::from_utf8(&actual).unwrap());
856    }
857
858    #[test]
859    fn strips_human_output_none() {
860        let mut actual = bytes::BytesMut::new();
861        strip_human_output_from_failed_test(
862            &ChildExecutionOutput::Output {
863                result: None,
864                output: ChildOutput::Split(ChildSplitOutput {
865                    stdout: None,
866                    stderr: None,
867                }),
868                errors: None,
869            },
870            &mut actual,
871            "non-existent",
872        )
873        .unwrap();
874
875        insta::assert_snapshot!(std::str::from_utf8(&actual).unwrap());
876    }
877}