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