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::{FlakyResult, SlowTimeoutResult},
27    errors::{DisplayErrorChain, FormatVersionError, FormatVersionErrorInner, WriteEventError},
28    list::{RustTestSuite, TestList},
29    output_spec::{LiveSpec, OutputSpec},
30    reporter::events::{
31        ChildExecutionOutputDescription, ChildOutputDescription, ExecutionDescription,
32        ExecutionResultDescription, ExecutionStatuses, StressIndex, TestEvent, TestEventKind,
33    },
34    test_output::ChildSingleOutput,
35};
36use bstr::ByteSlice;
37use iddqd::{IdOrdItem, IdOrdMap, id_ord_map, id_upcast};
38use nextest_metadata::{MismatchReason, RustBinaryId, TestCaseName};
39use std::fmt::Write as _;
40
41/// To support pinning the version of the output, we just use this simple enum
42/// to document changes as libtest output changes
43#[derive(Copy, Clone)]
44#[repr(u8)]
45enum FormatMinorVersion {
46    /// The libtest output as of `rustc 1.75.0-nightly (aa1a71e9e 2023-10-26)` with `--format json --report-time`
47    ///
48    /// * `{ "type": "suite", "event": "started", "test_count": <u32> }` - Start of a test binary run, always printed
49    ///   * `{ "type": "test", "event": "started", "name": "<name>" }` - Start of a single test, always printed
50    ///   * `{ "type": "test", "name": "<name>", "event": "ignored" }` - Printed if a test is ignored
51    ///     * Will have an additional `"message" = "<message>"` field if the there is a message in the ignore attribute eg. `#[ignore = "not yet implemented"]`
52    ///   * `{ "type": "test", "name": "<name>", "event": "ok", "exec_time": <f32> }` - Printed if a test runs successfully
53    ///   * `{ "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
54    ///     * If `--ensure-time` is passed, libtest will add `"reason": "time limit exceeded"` if the test passes, but exceeds the time limit.
55    ///     * 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
56    /// * `{ "type": "suite", "event": "<overall_status>", "passed": <u32>, "failed": <u32>, "ignored": <u32>, "measured": <u32>, "filtered_out": <u32>, "exec_time": <f32> }`
57    ///   * `event` will be `"ok"` if no failures occurred, or `"failed"` if `"failed" > 0`
58    ///   * `ignored` will be > 0 if there are `#[ignore]` tests and `--ignored` was not passed
59    ///   * `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
60    ///   * `measured` is only > 0 if running benchmarks
61    First = 1,
62    #[doc(hidden)]
63    _Max,
64}
65
66/// If libtest output is ever stabilized, this would most likely become the single
67/// version and we could get rid of the minor version, but who knows if that
68/// will ever happen
69#[derive(Copy, Clone)]
70#[repr(u8)]
71enum FormatMajorVersion {
72    /// The libtest output is unstable
73    Unstable = 0,
74    #[doc(hidden)]
75    _Max,
76}
77
78/// The accumulated stats for a single test binary
79struct LibtestSuite<'cfg> {
80    /// The number of tests that failed
81    failed: usize,
82    /// The number of tests that succeeded
83    succeeded: usize,
84    /// The number of tests that were ignored
85    ignored: usize,
86    /// The number of tests that were not executed due to filters
87    filtered: usize,
88    /// The number of tests in this suite that are still running
89    running: usize,
90
91    stress_index: Option<StressIndex>,
92    meta: &'cfg RustTestSuite<'cfg>,
93    /// The accumulated duration of every test that has been executed
94    total: std::time::Duration,
95    /// Libtest outputs outputs a `started` event for every test that isn't
96    /// filtered, including ignored tests, then outputs `ignored` events after
97    /// all the started events, so we just mimic that with a temporary buffer
98    ignore_block: Option<bytes::BytesMut>,
99    /// The single block of output accumulated for all tests executed in the binary,
100    /// this needs to be emitted as a single block to emulate how cargo test works,
101    /// executing each test binary serially and outputting a json line for each
102    /// event, as otherwise consumers would not be able to associate a single test
103    /// with its parent suite
104    output_block: bytes::BytesMut,
105}
106
107impl IdOrdItem for LibtestSuite<'_> {
108    type Key<'a>
109        = &'a RustBinaryId
110    where
111        Self: 'a;
112
113    fn key(&self) -> Self::Key<'_> {
114        &self.meta.binary_id
115    }
116
117    id_upcast!();
118}
119
120/// Determines whether the `nextest` subobject is added with additional metadata
121/// to events
122#[derive(Copy, Clone, Debug)]
123pub enum EmitNextestObject {
124    /// The `nextest` subobject is added
125    Yes,
126    /// The `nextest` subobject is not added
127    No,
128}
129
130const KIND_TEST: &str = "test";
131const KIND_SUITE: &str = "suite";
132
133const EVENT_STARTED: &str = "started";
134const EVENT_IGNORED: &str = "ignored";
135const EVENT_OK: &str = "ok";
136const EVENT_FAILED: &str = "failed";
137
138#[inline]
139fn fmt_err(err: std::fmt::Error) -> WriteEventError {
140    WriteEventError::Io(std::io::Error::new(std::io::ErrorKind::OutOfMemory, err))
141}
142
143/// A reporter that reports test runs in the same line-by-line JSON format as
144/// libtest itself
145pub struct LibtestReporter<'cfg> {
146    _minor: FormatMinorVersion,
147    _major: FormatMajorVersion,
148    test_list: Option<&'cfg TestList<'cfg>>,
149    test_suites: IdOrdMap<LibtestSuite<'cfg>>,
150    /// If true, we emit a `nextest` subobject with additional metadata in it
151    /// that consumers can use for easier integration if they wish
152    emit_nextest_obj: bool,
153}
154
155impl<'cfg> LibtestReporter<'cfg> {
156    /// Creates a new libtest reporter
157    ///
158    /// The version string is used to allow the reporter to evolve along with
159    /// libtest, but still be able to output a stable format for consumers. If
160    /// it is not specified the latest version of the format will be produced.
161    ///
162    /// If [`EmitNextestObject::Yes`] is passed, an additional `nextest` subobject
163    /// will be added to some events that includes additional metadata not produced
164    /// by libtest, but most consumers should still be able to consume them as
165    /// the base format itself is not changed
166    pub fn new(
167        version: Option<&str>,
168        emit_nextest_obj: EmitNextestObject,
169    ) -> Result<Self, FormatVersionError> {
170        let emit_nextest_obj = matches!(emit_nextest_obj, EmitNextestObject::Yes);
171
172        let Some(version) = version else {
173            return Ok(Self {
174                _minor: FormatMinorVersion::First,
175                _major: FormatMajorVersion::Unstable,
176                test_list: None,
177                test_suites: IdOrdMap::new(),
178                emit_nextest_obj,
179            });
180        };
181        let Some((major, minor)) = version.split_once('.') else {
182            return Err(FormatVersionError {
183                input: version.into(),
184                error: FormatVersionErrorInner::InvalidFormat {
185                    expected: "<major>.<minor>",
186                },
187            });
188        };
189
190        let major: u8 = major.parse().map_err(|err| FormatVersionError {
191            input: version.into(),
192            error: FormatVersionErrorInner::InvalidInteger {
193                which: "major",
194                err,
195            },
196        })?;
197
198        let minor: u8 = minor.parse().map_err(|err| FormatVersionError {
199            input: version.into(),
200            error: FormatVersionErrorInner::InvalidInteger {
201                which: "minor",
202                err,
203            },
204        })?;
205
206        let major = match major {
207            0 => FormatMajorVersion::Unstable,
208            o => {
209                return Err(FormatVersionError {
210                    input: version.into(),
211                    error: FormatVersionErrorInner::InvalidValue {
212                        which: "major",
213                        value: o,
214                        range: (FormatMajorVersion::Unstable as u8)
215                            ..(FormatMajorVersion::_Max as u8),
216                    },
217                });
218            }
219        };
220
221        let minor = match minor {
222            1 => FormatMinorVersion::First,
223            o => {
224                return Err(FormatVersionError {
225                    input: version.into(),
226                    error: FormatVersionErrorInner::InvalidValue {
227                        which: "minor",
228                        value: o,
229                        range: (FormatMinorVersion::First as u8)..(FormatMinorVersion::_Max as u8),
230                    },
231                });
232            }
233        };
234
235        Ok(Self {
236            _major: major,
237            _minor: minor,
238            test_list: None,
239            test_suites: IdOrdMap::new(),
240            emit_nextest_obj,
241        })
242    }
243
244    pub(crate) fn write_event(&mut self, event: &TestEvent<'cfg>) -> Result<(), WriteEventError> {
245        let mut retries = None;
246
247        // Write the pieces of data that are the same across all events
248        let (kind, eve, stress_index, test_instance) = match &event.kind {
249            TestEventKind::TestStarted {
250                stress_index,
251                test_instance,
252                ..
253            } => (KIND_TEST, EVENT_STARTED, stress_index, test_instance),
254            TestEventKind::TestSkipped {
255                stress_index,
256                test_instance,
257                reason: MismatchReason::Ignored,
258            } => {
259                // Note: unfortunately, libtest does not expose the message test in `#[ignore = "<message>"]`
260                // so we can't replicate the behavior of libtest exactly by emitting
261                // that message as additional metadata
262                (KIND_TEST, EVENT_STARTED, stress_index, test_instance)
263            }
264            TestEventKind::TestFinished {
265                stress_index,
266                test_instance,
267                run_statuses,
268                ..
269            } => {
270                if run_statuses.len() > 1 {
271                    retries = Some(run_statuses.len());
272                }
273
274                (
275                    KIND_TEST,
276                    event_for_finished_test(run_statuses),
277                    stress_index,
278                    test_instance,
279                )
280            }
281            TestEventKind::RunStarted { test_list, .. } => {
282                self.test_list = Some(*test_list);
283                return Ok(());
284            }
285            TestEventKind::StressSubRunFinished { .. } | TestEventKind::RunFinished { .. } => {
286                for test_suite in std::mem::take(&mut self.test_suites) {
287                    self.finalize(test_suite)?;
288                }
289
290                return Ok(());
291            }
292            _ => return Ok(()),
293        };
294
295        // Look up the suite info from the test list.
296        let test_list = self
297            .test_list
298            .expect("test_list should be set by RunStarted before any test events");
299        let suite_info = test_list
300            .get_suite(test_instance.binary_id)
301            .expect("suite should exist in test list");
302        let crate_name = suite_info.package.name();
303        let binary_name = &suite_info.binary_name;
304
305        // Emit the suite start if this is the first test of the suite
306        let mut test_suite = match self.test_suites.entry(&suite_info.binary_id) {
307            id_ord_map::Entry::Vacant(e) => {
308                let (running, ignored, filtered) =
309                    suite_info.status.test_cases().fold((0, 0, 0), |acc, case| {
310                        if case.test_info.ignored {
311                            (acc.0, acc.1 + 1, acc.2)
312                        } else if case.test_info.filter_match.is_match() {
313                            (acc.0 + 1, acc.1, acc.2)
314                        } else {
315                            (acc.0, acc.1, acc.2 + 1)
316                        }
317                    });
318
319                let mut out = bytes::BytesMut::with_capacity(1024);
320                write!(
321                    &mut out,
322                    r#"{{"type":"{KIND_SUITE}","event":"{EVENT_STARTED}","test_count":{}"#,
323                    running + ignored,
324                )
325                .map_err(fmt_err)?;
326
327                if self.emit_nextest_obj {
328                    write!(
329                        out,
330                        r#","nextest":{{"crate":"{crate_name}","test_binary":"{binary_name}","kind":"{}""#,
331                        suite_info.kind,
332                    )
333                    .map_err(fmt_err)?;
334
335                    if let Some(stress_index) = stress_index {
336                        write!(out, r#","stress_index":{}"#, stress_index.current)
337                            .map_err(fmt_err)?;
338                        if let Some(total) = stress_index.total {
339                            write!(out, r#","stress_total":{total}"#).map_err(fmt_err)?;
340                        }
341                    }
342
343                    write!(out, "}}").map_err(fmt_err)?;
344                }
345
346                out.extend_from_slice(b"}\n");
347
348                e.insert(LibtestSuite {
349                    running,
350                    failed: 0,
351                    succeeded: 0,
352                    ignored,
353                    filtered,
354                    stress_index: *stress_index,
355                    meta: suite_info,
356                    total: std::time::Duration::new(0, 0),
357                    ignore_block: None,
358                    output_block: out,
359                })
360            }
361            id_ord_map::Entry::Occupied(e) => e.into_mut(),
362        };
363
364        let test_suite_mut = &mut *test_suite;
365        let out = &mut test_suite_mut.output_block;
366
367        // After all the tests have been started or ignored, put the block of
368        // tests that were ignored just as libtest does
369        if matches!(event.kind, TestEventKind::TestFinished { .. })
370            && let Some(ib) = test_suite_mut.ignore_block.take()
371        {
372            out.extend_from_slice(&ib);
373        }
374
375        // This is one place where we deviate from the behavior of libtest, by
376        // always prefixing the test name with both the crate and the binary name,
377        // as this information is quite important to distinguish tests from each
378        // other when testing inside a large workspace with hundreds or thousands
379        // of tests
380        //
381        // Additionally, a `#<n>` is used as a suffix if the test was retried,
382        // as libtest does not support that functionality
383        write!(
384            out,
385            r#"{{"type":"{kind}","event":"{eve}","name":"{}::{}"#,
386            suite_info.package.name(),
387            suite_info.binary_name,
388        )
389        .map_err(fmt_err)?;
390
391        if let Some(stress_index) = stress_index {
392            write!(out, "@stress-{}", stress_index.current).map_err(fmt_err)?;
393        }
394        write!(out, "${}", test_instance.test_name).map_err(fmt_err)?;
395        if let Some(retry_count) = retries {
396            write!(out, "#{retry_count}\"").map_err(fmt_err)?;
397        } else {
398            out.extend_from_slice(b"\"");
399        }
400
401        match &event.kind {
402            TestEventKind::TestFinished { run_statuses, .. } => {
403                let last_status = run_statuses.last_status();
404
405                test_suite_mut.total += last_status.time_taken;
406                test_suite_mut.running -= 1;
407
408                // libtest actually requires an additional `--report-time` flag to be
409                // passed for the exec_time information to be written. This doesn't
410                // really make sense when outputting structured output so we emit it
411                // unconditionally
412                write!(
413                    out,
414                    r#","exec_time":{}"#,
415                    last_status.time_taken.as_secs_f64()
416                )
417                .map_err(fmt_err)?;
418
419                // Check for flaky-fail: a test that passed on retry but is
420                // configured to be treated as a failure.
421                let is_flaky_fail = matches!(
422                    run_statuses.describe(),
423                    ExecutionDescription::Flaky {
424                        result: FlakyResult::Fail,
425                        ..
426                    }
427                );
428
429                if is_flaky_fail {
430                    test_suite_mut.failed += 1;
431                    out.extend_from_slice(br#","reason":"flaky test treated as failure""#);
432                } else {
433                    match &last_status.result {
434                        ExecutionResultDescription::Fail { .. }
435                        | ExecutionResultDescription::ExecFail => {
436                            test_suite_mut.failed += 1;
437
438                            // Write the output from the test into the `stdout` (even
439                            // though it could contain stderr output as well).
440                            write!(out, r#","stdout":""#).map_err(fmt_err)?;
441
442                            strip_human_output_from_failed_test(
443                                &last_status.output,
444                                out,
445                                test_instance.test_name,
446                            )?;
447                            out.extend_from_slice(b"\"");
448                        }
449                        ExecutionResultDescription::Timeout {
450                            result: SlowTimeoutResult::Fail,
451                        } => {
452                            test_suite_mut.failed += 1;
453                            out.extend_from_slice(br#","reason":"time limit exceeded""#);
454                        }
455                        _ => {
456                            test_suite_mut.succeeded += 1;
457                        }
458                    }
459                }
460            }
461            TestEventKind::TestSkipped { .. } => {
462                test_suite_mut.running -= 1;
463
464                if test_suite_mut.ignore_block.is_none() {
465                    test_suite_mut.ignore_block = Some(bytes::BytesMut::with_capacity(1024));
466                }
467
468                let ib = test_suite_mut
469                    .ignore_block
470                    .get_or_insert_with(|| bytes::BytesMut::with_capacity(1024));
471
472                writeln!(
473                    ib,
474                    r#"{{"type":"{kind}","event":"{EVENT_IGNORED}","name":"{}::{}${}"}}"#,
475                    suite_info.package.name(),
476                    suite_info.binary_name,
477                    test_instance.test_name,
478                )
479                .map_err(fmt_err)?;
480            }
481            _ => {}
482        };
483
484        out.extend_from_slice(b"}\n");
485
486        if self.emit_nextest_obj {
487            {
488                use std::io::Write as _;
489
490                let mut stdout = std::io::stdout().lock();
491                stdout.write_all(out).map_err(WriteEventError::Io)?;
492                stdout.flush().map_err(WriteEventError::Io)?;
493                out.clear();
494            }
495
496            if test_suite_mut.running == 0 {
497                std::mem::drop(test_suite);
498
499                if let Some(test_suite) = self.test_suites.remove(&suite_info.binary_id) {
500                    self.finalize(test_suite)?;
501                }
502            }
503        } else {
504            // If this is the last test of the suite, emit the test suite summary
505            // before emitting the entire block
506            if test_suite_mut.running > 0 {
507                return Ok(());
508            }
509
510            std::mem::drop(test_suite);
511
512            if let Some(test_suite) = self.test_suites.remove(&suite_info.binary_id) {
513                self.finalize(test_suite)?;
514            }
515        }
516
517        Ok(())
518    }
519
520    fn finalize(&self, mut test_suite: LibtestSuite) -> Result<(), WriteEventError> {
521        let event = if test_suite.failed > 0 {
522            EVENT_FAILED
523        } else {
524            EVENT_OK
525        };
526
527        let out = &mut test_suite.output_block;
528        let suite_info = test_suite.meta;
529
530        // It's possible that a test failure etc has cancelled the run, in which
531        // case we might still have tests that are "running", even ones that are
532        // actually skipped, so we just add those to the filtered list
533        if test_suite.running > 0 {
534            test_suite.filtered += test_suite.running;
535        }
536
537        write!(
538            out,
539            r#"{{"type":"{KIND_SUITE}","event":"{event}","passed":{},"failed":{},"ignored":{},"measured":0,"filtered_out":{},"exec_time":{}"#,
540            test_suite.succeeded,
541            test_suite.failed,
542            test_suite.ignored,
543            test_suite.filtered,
544            test_suite.total.as_secs_f64(),
545        )
546        .map_err(fmt_err)?;
547
548        if self.emit_nextest_obj {
549            let crate_name = suite_info.package.name();
550            let binary_name = &suite_info.binary_name;
551            write!(
552                out,
553                r#","nextest":{{"crate":"{crate_name}","test_binary":"{binary_name}","kind":"{}""#,
554                suite_info.kind,
555            )
556            .map_err(fmt_err)?;
557
558            if let Some(stress_index) = test_suite.stress_index {
559                write!(out, r#","stress_index":{}"#, stress_index.current).map_err(fmt_err)?;
560                if let Some(total) = stress_index.total {
561                    write!(out, r#","stress_total":{total}"#).map_err(fmt_err)?;
562                }
563            }
564
565            write!(out, "}}").map_err(fmt_err)?;
566        }
567
568        out.extend_from_slice(b"}\n");
569
570        {
571            use std::io::Write as _;
572
573            let mut stdout = std::io::stdout().lock();
574            stdout.write_all(out).map_err(WriteEventError::Io)?;
575            stdout.flush().map_err(WriteEventError::Io)?;
576        }
577
578        Ok(())
579    }
580}
581
582/// Returns the libtest JSON event string for a finished test.
583///
584/// Uses `ExecutionDescription` to determine the overall outcome, which
585/// correctly accounts for flaky tests configured with `flaky-result = "fail"`.
586fn event_for_finished_test<S: OutputSpec>(run_statuses: &ExecutionStatuses<S>) -> &'static str {
587    match run_statuses.describe() {
588        ExecutionDescription::Success { .. }
589        | ExecutionDescription::Flaky {
590            result: FlakyResult::Pass,
591            ..
592        } => EVENT_OK,
593        ExecutionDescription::Flaky {
594            result: FlakyResult::Fail,
595            ..
596        }
597        | ExecutionDescription::Failure { .. } => EVENT_FAILED,
598    }
599}
600
601/// Unfortunately, to replicate the libtest json output, we need to do our own
602/// filtering of the output to strip out the data emitted by libtest in the
603/// human format.
604///
605/// This function relies on the fact that nextest runs every individual test in
606/// isolation.
607fn strip_human_output_from_failed_test(
608    output: &ChildExecutionOutputDescription<LiveSpec>,
609    out: &mut bytes::BytesMut,
610    test_name: &TestCaseName,
611) -> Result<(), WriteEventError> {
612    match output {
613        ChildExecutionOutputDescription::Output {
614            result: _,
615            output,
616            errors,
617        } => {
618            match output {
619                ChildOutputDescription::Combined { output } => {
620                    strip_human_stdout_or_combined(output, out, test_name)?;
621                }
622                ChildOutputDescription::Split { stdout, stderr } => {
623                    // This is not a case that we hit because we always set CaptureStrategy to Combined. But
624                    // handle it in a reasonable fashion. (We do have a unit test for this case, so gate the
625                    // assertion with cfg(not(test)).)
626                    #[cfg(not(test))]
627                    {
628                        debug_assert!(false, "libtest output requires CaptureStrategy::Combined");
629                    }
630                    if let Some(stdout) = stdout {
631                        if !stdout.is_empty() {
632                            write!(out, "--- STDOUT ---\\n").map_err(fmt_err)?;
633                            strip_human_stdout_or_combined(stdout, out, test_name)?;
634                        }
635                    } else {
636                        write!(out, "(stdout not captured)").map_err(fmt_err)?;
637                    }
638                    // If stderr is not empty, just write all of it in.
639                    if let Some(stderr) = stderr {
640                        if !stderr.is_empty() {
641                            write!(out, "\\n--- STDERR ---\\n").map_err(fmt_err)?;
642                            write!(out, "{}", EscapedString(stderr.as_str_lossy()))
643                                .map_err(fmt_err)?;
644                        }
645                    } else {
646                        writeln!(out, "\\n(stderr not captured)").map_err(fmt_err)?;
647                    }
648                }
649                ChildOutputDescription::NotLoaded => {
650                    unreachable!(
651                        "attempted to strip output from output that was not loaded \
652                         (the libtest reporter is not used during replay, where NotLoaded \
653                         is produced)"
654                    );
655                }
656            }
657
658            if let Some(errors) = errors {
659                write!(out, "\\n--- EXECUTION ERRORS ---\\n").map_err(fmt_err)?;
660                write!(
661                    out,
662                    "{}",
663                    EscapedString(&DisplayErrorChain::new(errors).to_string())
664                )
665                .map_err(fmt_err)?;
666            }
667        }
668        ChildExecutionOutputDescription::StartError(error) => {
669            write!(out, "--- EXECUTION ERROR ---\\n").map_err(fmt_err)?;
670            write!(
671                out,
672                "{}",
673                EscapedString(&DisplayErrorChain::new(error).to_string())
674            )
675            .map_err(fmt_err)?;
676        }
677    }
678    Ok(())
679}
680
681fn strip_human_stdout_or_combined(
682    output: &ChildSingleOutput,
683    out: &mut bytes::BytesMut,
684    test_name: &TestCaseName,
685) -> Result<(), WriteEventError> {
686    if output.buf().contains_str("running 1 test\n") {
687        // This is most likely the default test harness.
688        let lines = output
689            .lines()
690            .skip_while(|line| line != b"running 1 test")
691            .skip(1)
692            .take_while(|line| {
693                if let Some(name) = line
694                    .strip_prefix(b"test ")
695                    .and_then(|np| np.strip_suffix(b" ... FAILED"))
696                    && test_name.as_bytes() == name
697                {
698                    return false;
699                }
700
701                true
702            })
703            .map(|line| line.to_str_lossy());
704
705        for line in lines {
706            // This will never fail unless we are OOM
707            write!(out, "{}\\n", EscapedString(&line)).map_err(fmt_err)?;
708        }
709    } else {
710        // This is most likely a custom test harness. Just write out the entire
711        // output.
712        write!(out, "{}", EscapedString(output.as_str_lossy())).map_err(fmt_err)?;
713    }
714
715    Ok(())
716}
717
718/// Copy of the same string escaper used in libtest
719///
720/// <https://github.com/rust-lang/rust/blob/f440b5f0ea042cb2087a36631b20878f9847ee28/library/test/src/formatters/json.rs#L222-L285>
721struct EscapedString<'s>(&'s str);
722
723impl std::fmt::Display for EscapedString<'_> {
724    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> ::std::fmt::Result {
725        let mut start = 0;
726        let s = self.0;
727
728        for (i, byte) in s.bytes().enumerate() {
729            let escaped = match byte {
730                b'"' => "\\\"",
731                b'\\' => "\\\\",
732                b'\x00' => "\\u0000",
733                b'\x01' => "\\u0001",
734                b'\x02' => "\\u0002",
735                b'\x03' => "\\u0003",
736                b'\x04' => "\\u0004",
737                b'\x05' => "\\u0005",
738                b'\x06' => "\\u0006",
739                b'\x07' => "\\u0007",
740                b'\x08' => "\\b",
741                b'\t' => "\\t",
742                b'\n' => "\\n",
743                b'\x0b' => "\\u000b",
744                b'\x0c' => "\\f",
745                b'\r' => "\\r",
746                b'\x0e' => "\\u000e",
747                b'\x0f' => "\\u000f",
748                b'\x10' => "\\u0010",
749                b'\x11' => "\\u0011",
750                b'\x12' => "\\u0012",
751                b'\x13' => "\\u0013",
752                b'\x14' => "\\u0014",
753                b'\x15' => "\\u0015",
754                b'\x16' => "\\u0016",
755                b'\x17' => "\\u0017",
756                b'\x18' => "\\u0018",
757                b'\x19' => "\\u0019",
758                b'\x1a' => "\\u001a",
759                b'\x1b' => "\\u001b",
760                b'\x1c' => "\\u001c",
761                b'\x1d' => "\\u001d",
762                b'\x1e' => "\\u001e",
763                b'\x1f' => "\\u001f",
764                b'\x7f' => "\\u007f",
765                _ => {
766                    continue;
767                }
768            };
769
770            if start < i {
771                f.write_str(&s[start..i])?;
772            }
773
774            f.write_str(escaped)?;
775
776            start = i + 1;
777        }
778
779        if start != self.0.len() {
780            f.write_str(&s[start..])?;
781        }
782
783        Ok(())
784    }
785}
786
787#[cfg(test)]
788mod test {
789    use crate::{
790        config::elements::{FlakyResult, LeakTimeoutResult, SlowTimeoutResult},
791        errors::ChildStartError,
792        output_spec::LiveSpec,
793        reporter::{
794            events::{
795                ChildExecutionOutputDescription, ExecuteStatus, ExecutionResult,
796                ExecutionResultDescription, ExecutionStatuses, FailureDescription, FailureStatus,
797                RetryData,
798            },
799            structured::libtest::{
800                EVENT_FAILED, EVENT_OK, event_for_finished_test,
801                strip_human_output_from_failed_test,
802            },
803        },
804        test_output::{ChildExecutionOutput, ChildOutput, ChildSplitOutput},
805    };
806    use bytes::{Bytes, BytesMut};
807    use chrono::Local;
808    use color_eyre::eyre::eyre;
809    use nextest_metadata::TestCaseName;
810    use std::{io, sync::Arc, time::Duration};
811
812    /// Validates that the human output portion from a failed test is stripped
813    /// out when writing a JSON string, as it is not part of the output when
814    /// libtest itself outputs the JSON, so we have 100% identical output to libtest
815    #[test]
816    fn strips_human_output() {
817        const TEST_OUTPUT: &[&str] = &[
818            "\n",
819            "running 1 test\n",
820            "[src/index.rs:185] \"boop\" = \"boop\"\n",
821            "this is stdout\n",
822            "this i stderr\nok?\n",
823            "thread 'index::test::download_url_crates_io'",
824            r" panicked at src/index.rs:206:9:
825oh no
826stack backtrace:
827    0: rust_begin_unwind
828                at /rustc/a28077b28a02b92985b3a3faecf92813155f1ea1/library/std/src/panicking.rs:597:5
829    1: core::panicking::panic_fmt
830                at /rustc/a28077b28a02b92985b3a3faecf92813155f1ea1/library/core/src/panicking.rs:72:14
831    2: tame_index::index::test::download_url_crates_io
832                at ./src/index.rs:206:9
833    3: tame_index::index::test::download_url_crates_io::{{closure}}
834                at ./src/index.rs:179:33
835    4: core::ops::function::FnOnce::call_once
836                at /rustc/a28077b28a02b92985b3a3faecf92813155f1ea1/library/core/src/ops/function.rs:250:5
837    5: core::ops::function::FnOnce::call_once
838                at /rustc/a28077b28a02b92985b3a3faecf92813155f1ea1/library/core/src/ops/function.rs:250:5
839note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
840",
841            "test index::test::download_url_crates_io ... FAILED\n",
842            "\n\nfailures:\n\nfailures:\n    index::test::download_url_crates_io\n\ntest result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 13 filtered out; finished in 0.01s\n",
843        ];
844
845        let output = {
846            let mut acc = BytesMut::new();
847            for line in TEST_OUTPUT {
848                acc.extend_from_slice(line.as_bytes());
849            }
850
851            ChildOutput::Combined {
852                output: acc.freeze().into(),
853            }
854        };
855
856        let mut actual = bytes::BytesMut::new();
857        let output_desc: ChildExecutionOutputDescription<_> = ChildExecutionOutput::Output {
858            result: None,
859            output,
860            errors: None,
861        }
862        .into();
863        strip_human_output_from_failed_test(
864            &output_desc,
865            &mut actual,
866            &TestCaseName::new("index::test::download_url_crates_io"),
867        )
868        .unwrap();
869
870        insta::assert_snapshot!(std::str::from_utf8(&actual).unwrap());
871    }
872
873    #[test]
874    fn strips_human_output_custom_test_harness() {
875        // For a custom test harness, we don't strip the human output at all.
876        const TEST_OUTPUT: &[&str] = &["\n", "this is a custom test harness!!!\n", "1 test passed"];
877
878        let output = {
879            let mut acc = BytesMut::new();
880            for line in TEST_OUTPUT {
881                acc.extend_from_slice(line.as_bytes());
882            }
883
884            ChildOutput::Combined {
885                output: acc.freeze().into(),
886            }
887        };
888
889        let mut actual = bytes::BytesMut::new();
890        let output_desc: ChildExecutionOutputDescription<_> = ChildExecutionOutput::Output {
891            result: None,
892            output,
893            errors: None,
894        }
895        .into();
896        strip_human_output_from_failed_test(
897            &output_desc,
898            &mut actual,
899            &TestCaseName::new("non-existent"),
900        )
901        .unwrap();
902
903        insta::assert_snapshot!(std::str::from_utf8(&actual).unwrap());
904    }
905
906    #[test]
907    fn strips_human_output_start_error() {
908        let inner_error = eyre!("inner error");
909        let error = io::Error::other(inner_error);
910
911        let output: ChildExecutionOutputDescription<_> =
912            ChildExecutionOutput::StartError(ChildStartError::Spawn(Arc::new(error))).into();
913
914        let mut actual = bytes::BytesMut::new();
915        strip_human_output_from_failed_test(
916            &output,
917            &mut actual,
918            &TestCaseName::new("non-existent"),
919        )
920        .unwrap();
921
922        insta::assert_snapshot!(std::str::from_utf8(&actual).unwrap());
923    }
924
925    #[test]
926    fn strips_human_output_none() {
927        let mut actual = bytes::BytesMut::new();
928        let output_desc: ChildExecutionOutputDescription<_> = ChildExecutionOutput::Output {
929            result: None,
930            output: ChildOutput::Split(ChildSplitOutput {
931                stdout: None,
932                stderr: None,
933            }),
934            errors: None,
935        }
936        .into();
937        strip_human_output_from_failed_test(
938            &output_desc,
939            &mut actual,
940            &TestCaseName::new("non-existent"),
941        )
942        .unwrap();
943
944        insta::assert_snapshot!(std::str::from_utf8(&actual).unwrap());
945    }
946
947    fn make_test_output(result: ExecutionResult) -> ChildExecutionOutputDescription<LiveSpec> {
948        ChildExecutionOutput::Output {
949            result: Some(result),
950            output: ChildOutput::Split(ChildSplitOutput {
951                stdout: Some(Bytes::new().into()),
952                stderr: Some(Bytes::new().into()),
953            }),
954            errors: None,
955        }
956        .into()
957    }
958
959    fn make_passing_status(attempt: u32, total_attempts: u32) -> ExecuteStatus<LiveSpec> {
960        ExecuteStatus {
961            retry_data: RetryData {
962                attempt,
963                total_attempts,
964            },
965            output: make_test_output(ExecutionResult::Pass),
966            result: ExecutionResultDescription::Pass,
967            start_time: Local::now().fixed_offset(),
968            time_taken: Duration::from_secs(1),
969            is_slow: false,
970            delay_before_start: Duration::ZERO,
971            error_summary: None,
972            output_error_slice: None,
973        }
974    }
975
976    fn make_failing_status(attempt: u32, total_attempts: u32) -> ExecuteStatus<LiveSpec> {
977        ExecuteStatus {
978            retry_data: RetryData {
979                attempt,
980                total_attempts,
981            },
982            output: make_test_output(ExecutionResult::Fail {
983                failure_status: FailureStatus::ExitCode(1),
984                leaked: false,
985            }),
986            result: ExecutionResultDescription::Fail {
987                failure: FailureDescription::ExitCode { code: 1 },
988                leaked: false,
989            },
990            start_time: Local::now().fixed_offset(),
991            time_taken: Duration::from_secs(1),
992            is_slow: false,
993            delay_before_start: Duration::ZERO,
994            error_summary: None,
995            output_error_slice: None,
996        }
997    }
998
999    #[test]
1000    fn event_for_finished_test_variants() {
1001        // Single pass.
1002        let statuses =
1003            ExecutionStatuses::new(vec![make_passing_status(1, 1)], FlakyResult::default());
1004        assert_eq!(event_for_finished_test(&statuses), EVENT_OK, "single pass");
1005
1006        // Single failure.
1007        let statuses =
1008            ExecutionStatuses::new(vec![make_failing_status(1, 1)], FlakyResult::default());
1009        assert_eq!(
1010            event_for_finished_test(&statuses),
1011            EVENT_FAILED,
1012            "single failure"
1013        );
1014
1015        // Flaky pass: fail then pass, default result (pass).
1016        let statuses = ExecutionStatuses::new(
1017            vec![make_failing_status(1, 2), make_passing_status(2, 2)],
1018            FlakyResult::Pass,
1019        );
1020        assert_eq!(
1021            event_for_finished_test(&statuses),
1022            EVENT_OK,
1023            "flaky with result = pass"
1024        );
1025
1026        // Flaky fail: fail then pass, result = fail.
1027        let statuses = ExecutionStatuses::new(
1028            vec![make_failing_status(1, 2), make_passing_status(2, 2)],
1029            FlakyResult::Fail,
1030        );
1031        assert_eq!(
1032            event_for_finished_test(&statuses),
1033            EVENT_FAILED,
1034            "flaky with result = fail"
1035        );
1036
1037        // All retries failed.
1038        let statuses = ExecutionStatuses::new(
1039            vec![make_failing_status(1, 2), make_failing_status(2, 2)],
1040            FlakyResult::Pass,
1041        );
1042        assert_eq!(
1043            event_for_finished_test(&statuses),
1044            EVENT_FAILED,
1045            "all retries failed"
1046        );
1047
1048        // Leak { Pass } → success.
1049        let mut leak_pass = make_passing_status(1, 1);
1050        leak_pass.result = ExecutionResultDescription::Leak {
1051            result: LeakTimeoutResult::Pass,
1052        };
1053        let statuses = ExecutionStatuses::new(vec![leak_pass], FlakyResult::default());
1054        assert_eq!(
1055            event_for_finished_test(&statuses),
1056            EVENT_OK,
1057            "leak with result = pass"
1058        );
1059
1060        // Leak { Fail } → failure.
1061        let mut leak_fail = make_passing_status(1, 1);
1062        leak_fail.result = ExecutionResultDescription::Leak {
1063            result: LeakTimeoutResult::Fail,
1064        };
1065        let statuses = ExecutionStatuses::new(vec![leak_fail], FlakyResult::default());
1066        assert_eq!(
1067            event_for_finished_test(&statuses),
1068            EVENT_FAILED,
1069            "leak with result = fail"
1070        );
1071
1072        // Timeout { Pass } → success.
1073        let mut timeout_pass = make_passing_status(1, 1);
1074        timeout_pass.result = ExecutionResultDescription::Timeout {
1075            result: SlowTimeoutResult::Pass,
1076        };
1077        let statuses = ExecutionStatuses::new(vec![timeout_pass], FlakyResult::default());
1078        assert_eq!(
1079            event_for_finished_test(&statuses),
1080            EVENT_OK,
1081            "timeout with result = pass"
1082        );
1083
1084        // Timeout { Fail } → failure.
1085        let mut timeout_fail = make_passing_status(1, 1);
1086        timeout_fail.result = ExecutionResultDescription::Timeout {
1087            result: SlowTimeoutResult::Fail,
1088        };
1089        let statuses = ExecutionStatuses::new(vec![timeout_fail], FlakyResult::default());
1090        assert_eq!(
1091            event_for_finished_test(&statuses),
1092            EVENT_FAILED,
1093            "timeout with result = fail"
1094        );
1095
1096        // ExecFail → failure.
1097        let mut exec_fail = make_passing_status(1, 1);
1098        exec_fail.result = ExecutionResultDescription::ExecFail;
1099        let statuses = ExecutionStatuses::new(vec![exec_fail], FlakyResult::default());
1100        assert_eq!(
1101            event_for_finished_test(&statuses),
1102            EVENT_FAILED,
1103            "exec fail"
1104        );
1105    }
1106}