Skip to main content

zebra_test/
command.rs

1//! Launching test commands for Zebra integration and acceptance tests.
2
3use std::{
4    collections::HashSet,
5    convert::Infallible as NoDir,
6    fmt::{self, Debug, Write as _},
7    io::{BufRead, BufReader, ErrorKind, Read, Write as _},
8    path::Path,
9    process::{Child, Command, ExitStatus, Output, Stdio},
10    time::{Duration, Instant},
11};
12
13#[cfg(unix)]
14use std::os::unix::process::ExitStatusExt;
15
16use color_eyre::{
17    eyre::{eyre, Context, Report, Result},
18    Help, SectionExt,
19};
20use regex::RegexSet;
21use tracing::instrument;
22
23#[macro_use]
24mod arguments;
25
26pub mod to_regex;
27
28pub use self::arguments::Arguments;
29use self::to_regex::{CollectRegexSet, RegexSetExt, ToRegexSet};
30
31/// A super-trait for [`Iterator`] + [`Debug`].
32pub trait IteratorDebug: Iterator + Debug {}
33
34impl<T> IteratorDebug for T where T: Iterator + Debug {}
35
36/// Runs a command
37pub fn test_cmd(command_path: &str, tempdir: &Path) -> Result<Command> {
38    let mut cmd = Command::new(command_path);
39    cmd.current_dir(tempdir);
40
41    Ok(cmd)
42}
43
44// TODO: split these extensions into their own module
45
46/// Wrappers for `Command` methods to integrate with [`zebra_test`](crate).
47pub trait CommandExt {
48    /// wrapper for `status` fn on `Command` that constructs informative error
49    /// reports
50    fn status2(&mut self) -> Result<TestStatus, Report>;
51
52    /// wrapper for `output` fn on `Command` that constructs informative error
53    /// reports
54    fn output2(&mut self) -> Result<TestOutput<NoDir>, Report>;
55
56    /// wrapper for `spawn` fn on `Command` that constructs informative error
57    /// reports using the original `command_path`
58    fn spawn2<T>(&mut self, dir: T, command_path: impl ToString) -> Result<TestChild<T>, Report>;
59}
60
61impl CommandExt for Command {
62    /// wrapper for `status` fn on `Command` that constructs informative error
63    /// reports
64    fn status2(&mut self) -> Result<TestStatus, Report> {
65        let cmd = format!("{self:?}");
66        let status = self.status();
67
68        let command = || cmd.clone().header("Command:");
69
70        let status = status
71            .wrap_err("failed to execute process")
72            .with_section(command)?;
73
74        Ok(TestStatus { cmd, status })
75    }
76
77    /// wrapper for `output` fn on `Command` that constructs informative error
78    /// reports
79    fn output2(&mut self) -> Result<TestOutput<NoDir>, Report> {
80        let output = self.output();
81
82        let output = output
83            .wrap_err("failed to execute process")
84            .with_section(|| format!("{self:?}").header("Command:"))?;
85
86        Ok(TestOutput {
87            dir: None,
88            output,
89            cmd: format!("{self:?}"),
90        })
91    }
92
93    /// wrapper for `spawn` fn on `Command` that constructs informative error
94    /// reports using the original `command_path`
95    fn spawn2<T>(&mut self, dir: T, command_path: impl ToString) -> Result<TestChild<T>, Report> {
96        let command_and_args = format!("{self:?}");
97        let child = self.spawn();
98
99        let child = child
100            .wrap_err("failed to execute process")
101            .with_section(|| command_and_args.clone().header("Command:"))?;
102
103        Ok(TestChild {
104            dir: Some(dir),
105            cmd: command_and_args,
106            command_path: command_path.to_string(),
107            child: Some(child),
108            stdout: None,
109            stderr: None,
110            failure_regexes: RegexSet::empty(),
111            ignore_regexes: RegexSet::empty(),
112            deadline: None,
113            timeout: None,
114            bypass_test_capture: false,
115        })
116    }
117}
118
119/// Extension trait for methods on `tempdir::TempDir` for using it as a test
120/// directory with an arbitrary command.
121///
122/// This trait is separate from `ZebradTestDirExt`, so that we can test
123/// `zebra_test::command` without running `zebrad`.
124pub trait TestDirExt
125where
126    Self: AsRef<Path> + Sized,
127{
128    /// Spawn `cmd` with `args` as a child process in this test directory,
129    /// potentially taking ownership of the tempdir for the duration of the
130    /// child process.
131    fn spawn_child_with_command(self, cmd: &str, args: Arguments) -> Result<TestChild<Self>>;
132}
133
134impl<T> TestDirExt for T
135where
136    Self: AsRef<Path> + Sized,
137{
138    #[allow(clippy::unwrap_in_result)]
139    fn spawn_child_with_command(
140        self,
141        command_path: &str,
142        args: Arguments,
143    ) -> Result<TestChild<Self>> {
144        let mut cmd = test_cmd(command_path, self.as_ref())?;
145
146        Ok(cmd
147            .args(args.into_arguments())
148            .stdout(Stdio::piped())
149            .stderr(Stdio::piped())
150            .spawn2(self, command_path)
151            .unwrap())
152    }
153}
154
155/// Test command exit status information.
156#[derive(Debug)]
157pub struct TestStatus {
158    /// The original command string.
159    pub cmd: String,
160
161    /// The exit status of the command.
162    pub status: ExitStatus,
163}
164
165impl TestStatus {
166    pub fn assert_success(self) -> Result<Self> {
167        if !self.status.success() {
168            Err(eyre!("command exited unsuccessfully")).context_from(&self)?;
169        }
170
171        Ok(self)
172    }
173
174    pub fn assert_failure(self) -> Result<Self> {
175        if self.status.success() {
176            Err(eyre!("command unexpectedly exited successfully")).context_from(&self)?;
177        }
178
179        Ok(self)
180    }
181}
182
183/// A test command child process.
184// TODO: split this struct into its own module (or multiple modules)
185#[derive(Debug)]
186pub struct TestChild<T> {
187    /// The working directory of the command.
188    ///
189    /// `None` when the command has been waited on,
190    /// and its output has been taken.
191    pub dir: Option<T>,
192
193    /// The full command string, including arguments and working directory.
194    pub cmd: String,
195
196    /// The path of the command, as passed to spawn2().
197    pub command_path: String,
198
199    /// The child process itself.
200    ///
201    /// `None` when the command has been waited on,
202    /// and its output has been taken.
203    pub child: Option<Child>,
204
205    /// The standard output stream of the child process.
206    ///
207    /// TODO: replace with `Option<ChildOutput { stdout, stderr }>.
208    pub stdout: Option<Box<dyn IteratorDebug<Item = std::io::Result<String>> + Send>>,
209
210    /// The standard error stream of the child process.
211    pub stderr: Option<Box<dyn IteratorDebug<Item = std::io::Result<String>> + Send>>,
212
213    /// Command outputs which indicate test failure.
214    ///
215    /// This list of regexes is matches against `stdout` or `stderr`,
216    /// in every method that reads command output.
217    ///
218    /// If any line matches any failure regex, the test fails.
219    failure_regexes: RegexSet,
220
221    /// Command outputs which are ignored when checking for test failure.
222    /// These regexes override `failure_regexes`.
223    ///
224    /// This list of regexes is matches against `stdout` or `stderr`,
225    /// in every method that reads command output.
226    ///
227    /// If a line matches any ignore regex, the failure regex check is skipped for that line.
228    ignore_regexes: RegexSet,
229
230    /// The deadline for this command to finish.
231    ///
232    /// Only checked when the command outputs each new line (#1140).
233    pub deadline: Option<Instant>,
234
235    /// The timeout for this command to finish.
236    ///
237    /// Only used for debugging output.
238    pub timeout: Option<Duration>,
239
240    /// If true, write child output directly to standard output,
241    /// bypassing the Rust test harness output capture.
242    bypass_test_capture: bool,
243}
244
245/// Checks command output log `line` from `cmd` against a `failure_regexes` regex set,
246/// and returns an error if any regex matches. The line is skipped if it matches `ignore_regexes`.
247///
248/// Passes through errors from the underlying reader.
249pub fn check_failure_regexes(
250    line: std::io::Result<String>,
251    failure_regexes: &RegexSet,
252    ignore_regexes: &RegexSet,
253    cmd: &str,
254    bypass_test_capture: bool,
255) -> std::io::Result<String> {
256    let line = line?;
257
258    // Check if the line matches any patterns
259    let ignore_matches = ignore_regexes.matches(&line);
260    let ignore_matches: Vec<&str> = ignore_matches
261        .iter()
262        .map(|index| ignore_regexes.patterns()[index].as_str())
263        .collect();
264
265    let failure_matches = failure_regexes.matches(&line);
266    let failure_matches: Vec<&str> = failure_matches
267        .iter()
268        .map(|index| failure_regexes.patterns()[index].as_str())
269        .collect();
270
271    // If we match an ignore pattern, ignore any failure matches
272    if !ignore_matches.is_empty() {
273        let ignore_matches = ignore_matches.join(",");
274
275        let ignore_msg = if failure_matches.is_empty() {
276            format!("Log matched ignore regexes: {ignore_matches:?}, but no failure regexes",)
277        } else {
278            let failure_matches = failure_matches.join(",");
279            format!(
280                "Ignoring failure regexes: {failure_matches:?}, because log matched ignore regexes: {ignore_matches:?}",
281            )
282        };
283
284        write_to_test_logs(ignore_msg, bypass_test_capture);
285
286        return Ok(line);
287    }
288
289    // If there were no failures, pass the log line through
290    if failure_matches.is_empty() {
291        return Ok(line);
292    }
293
294    // Otherwise, if the process logged a failure message, return an error
295    let error = std::io::Error::other(format!(
296        "test command:\n\
297             {cmd}\n\n\
298             Logged a failure message:\n\
299             {line}\n\n\
300             Matching failure regex: \
301             {failure_matches:#?}\n\n\
302             All Failure regexes: \
303             {:#?}\n",
304        failure_regexes.patterns(),
305    ));
306
307    Err(error)
308}
309
310/// Write `line` to stdout, so it can be seen in the test logs.
311///
312/// Set `bypass_test_capture` to `true` or
313/// use `cargo test -- --nocapture` to see this output.
314///
315/// May cause weird reordering for stdout / stderr.
316/// Uses stdout even if the original lines were from stderr.
317#[allow(clippy::print_stdout)]
318fn write_to_test_logs<S>(line: S, bypass_test_capture: bool)
319where
320    S: AsRef<str>,
321{
322    let line = line.as_ref();
323
324    if bypass_test_capture {
325        // Send lines directly to the terminal (or process stdout file redirect).
326        #[allow(clippy::explicit_write)]
327        writeln!(std::io::stdout(), "{line}").unwrap();
328    } else {
329        // If the test fails, the test runner captures and displays this output.
330        // To show this output unconditionally, use `cargo test -- --nocapture`.
331        println!("{line}");
332    }
333
334    // Some OSes require a flush to send all output to the terminal.
335    let _ = std::io::stdout().lock().flush();
336}
337
338/// A [`CollectRegexSet`] iterator that never matches anything.
339///
340/// Used to work around type inference issues in [`TestChild::with_failure_regex_iter`].
341pub const NO_MATCHES_REGEX_ITER: &[&str] = &[];
342
343impl<T> TestChild<T> {
344    /// Sets up command output so each line is checked against a failure regex set,
345    /// unless it matches any of the ignore regexes.
346    ///
347    /// To never match any log lines, use `RegexSet::empty()`.
348    ///
349    /// This method is a [`TestChild::with_failure_regexes`] wrapper for
350    /// strings, `Regex`es, and [`RegexSet`]s.
351    ///
352    /// # Panics
353    ///
354    /// - adds a panic to any method that reads output,
355    ///   if any stdout or stderr lines match any failure regex
356    pub fn with_failure_regex_set<F, X>(self, failure_regexes: F, ignore_regexes: X) -> Self
357    where
358        F: ToRegexSet,
359        X: ToRegexSet,
360    {
361        let failure_regexes = failure_regexes
362            .to_regex_set()
363            .expect("failure regexes must be valid");
364
365        let ignore_regexes = ignore_regexes
366            .to_regex_set()
367            .expect("ignore regexes must be valid");
368
369        self.with_failure_regexes(failure_regexes, ignore_regexes)
370    }
371
372    /// Sets up command output so each line is checked against a failure regex set,
373    /// unless it matches any of the ignore regexes.
374    ///
375    /// To never match any log lines, use [`NO_MATCHES_REGEX_ITER`].
376    ///
377    /// This method is a [`TestChild::with_failure_regexes`] wrapper for
378    /// regular expression iterators.
379    ///
380    /// # Panics
381    ///
382    /// - adds a panic to any method that reads output,
383    ///   if any stdout or stderr lines match any failure regex
384    pub fn with_failure_regex_iter<F, X>(self, failure_regexes: F, ignore_regexes: X) -> Self
385    where
386        F: CollectRegexSet,
387        X: CollectRegexSet,
388    {
389        let failure_regexes = failure_regexes
390            .collect_regex_set()
391            .expect("failure regexes must be valid");
392
393        let ignore_regexes = ignore_regexes
394            .collect_regex_set()
395            .expect("ignore regexes must be valid");
396
397        self.with_failure_regexes(failure_regexes, ignore_regexes)
398    }
399
400    /// Sets up command output so each line is checked against a failure regex set,
401    /// unless it matches any of the ignore regexes.
402    ///
403    /// # Panics
404    ///
405    /// - adds a panic to any method that reads output,
406    ///   if any stdout or stderr lines match any failure regex
407    pub fn with_failure_regexes(
408        mut self,
409        failure_regexes: RegexSet,
410        ignore_regexes: impl Into<Option<RegexSet>>,
411    ) -> Self {
412        self.failure_regexes = failure_regexes;
413        self.ignore_regexes = ignore_regexes.into().unwrap_or_else(RegexSet::empty);
414
415        self.apply_failure_regexes_to_outputs();
416
417        self
418    }
419
420    /// Applies the failure and ignore regex sets to command output.
421    ///
422    /// # Panics
423    ///
424    /// - adds a panic to any method that reads output,
425    ///   if any stdout or stderr lines match any failure regex
426    pub fn apply_failure_regexes_to_outputs(&mut self) {
427        if self.stdout.is_none() {
428            self.stdout = self
429                .child
430                .as_mut()
431                .and_then(|child| child.stdout.take())
432                .map(|output| self.map_into_string_lines(output))
433        }
434
435        if self.stderr.is_none() {
436            self.stderr = self
437                .child
438                .as_mut()
439                .and_then(|child| child.stderr.take())
440                .map(|output| self.map_into_string_lines(output))
441        }
442    }
443
444    /// Maps a reader into a string line iterator,
445    /// and applies the failure and ignore regex sets to it.
446    fn map_into_string_lines<R>(
447        &self,
448        reader: R,
449    ) -> Box<dyn IteratorDebug<Item = std::io::Result<String>> + Send>
450    where
451        R: Read + Debug + Send + 'static,
452    {
453        let failure_regexes = self.failure_regexes.clone();
454        let ignore_regexes = self.ignore_regexes.clone();
455        let cmd = self.cmd.clone();
456        let bypass_test_capture = self.bypass_test_capture;
457
458        let reader = BufReader::new(reader);
459        let lines = BufRead::lines(reader).map(move |line| {
460            check_failure_regexes(
461                line,
462                &failure_regexes,
463                &ignore_regexes,
464                &cmd,
465                bypass_test_capture,
466            )
467        });
468
469        Box::new(lines) as _
470    }
471
472    /// Kill the child process.
473    ///
474    /// If `ignore_exited` is `true`, log "can't kill an exited process" errors,
475    /// rather than returning them.
476    ///
477    /// Returns the result of the kill.
478    ///
479    /// ## BUGS
480    ///
481    /// On Windows (and possibly macOS), this function can return `Ok` for
482    /// processes that have panicked. See #1781.
483    #[spandoc::spandoc]
484    pub fn kill(&mut self, ignore_exited: bool) -> Result<()> {
485        let child = match self.child.as_mut() {
486            Some(child) => child,
487            None if ignore_exited => {
488                Self::write_to_test_logs(
489                    "test child was already taken\n\
490                     ignoring kill because ignore_exited is true",
491                    self.bypass_test_capture,
492                );
493                return Ok(());
494            }
495            None => {
496                return Err(eyre!(
497                    "test child was already taken\n\
498                     call kill() once for each child process, or set ignore_exited to true"
499                ))
500                .context_from(self.as_mut())
501            }
502        };
503
504        /// SPANDOC: Killing child process
505        let kill_result = child.kill().or_else(|error| {
506            if ignore_exited && error.kind() == ErrorKind::InvalidInput {
507                Ok(())
508            } else {
509                Err(error)
510            }
511        });
512
513        kill_result.context_from(self.as_mut())?;
514
515        Ok(())
516    }
517
518    /// Kill the process, and consume all its remaining output.
519    ///
520    /// If `ignore_exited` is `true`, log "can't kill an exited process" errors,
521    /// rather than returning them.
522    ///
523    /// Returns the result of the kill.
524    pub fn kill_and_consume_output(&mut self, ignore_exited: bool) -> Result<()> {
525        self.apply_failure_regexes_to_outputs();
526
527        // Prevent a hang when consuming output,
528        // by making sure the child's output actually finishes.
529        let kill_result = self.kill(ignore_exited);
530
531        // Read unread child output.
532        //
533        // This checks for failure logs, and prevents some test hangs and deadlocks.
534        //
535        // TODO: this could block if stderr is full and stdout is waiting for stderr to be read.
536        if self.stdout.is_some() {
537            let wrote_lines =
538                self.wait_for_stdout_line(format!("\n{} Child Stdout:", self.command_path));
539
540            while self.wait_for_stdout_line(None) {}
541
542            if wrote_lines {
543                // Write an empty line, to make output more readable
544                Self::write_to_test_logs("", self.bypass_test_capture);
545            }
546        }
547
548        if self.stderr.is_some() {
549            let wrote_lines =
550                self.wait_for_stderr_line(format!("\n{} Child Stderr:", self.command_path));
551
552            while self.wait_for_stderr_line(None) {}
553
554            if wrote_lines {
555                Self::write_to_test_logs("", self.bypass_test_capture);
556            }
557        }
558
559        kill_result
560    }
561
562    /// Kill the process, and return all its remaining standard output and standard error output.
563    ///
564    /// If `ignore_exited` is `true`, log "can't kill an exited process" errors,
565    /// rather than returning them.
566    ///
567    /// Returns `Ok(output)`, or an error if the kill failed.
568    pub fn kill_and_return_output(&mut self, ignore_exited: bool) -> Result<String> {
569        self.apply_failure_regexes_to_outputs();
570
571        // Prevent a hang when consuming output,
572        // by making sure the child's output actually finishes.
573        let kill_result = self.kill(ignore_exited);
574
575        // Read unread child output.
576        let mut stdout_buf = String::new();
577        let mut stderr_buf = String::new();
578
579        // This also checks for failure logs, and prevents some test hangs and deadlocks.
580        loop {
581            let mut remaining_output = false;
582
583            if let Some(stdout) = self.stdout.as_mut() {
584                if let Some(line) =
585                    Self::wait_and_return_output_line(stdout, self.bypass_test_capture)
586                {
587                    stdout_buf.push_str(&line);
588                    remaining_output = true;
589                }
590            }
591
592            if let Some(stderr) = self.stderr.as_mut() {
593                if let Some(line) =
594                    Self::wait_and_return_output_line(stderr, self.bypass_test_capture)
595                {
596                    stderr_buf.push_str(&line);
597                    remaining_output = true;
598                }
599            }
600
601            if !remaining_output {
602                break;
603            }
604        }
605
606        let mut output = stdout_buf;
607        output.push_str(&stderr_buf);
608
609        kill_result.map(|()| output)
610    }
611
612    /// Waits until a line of standard output is available, then consumes it.
613    ///
614    /// If there is a line, and `write_context` is `Some`, writes the context to the test logs.
615    /// Always writes the line to the test logs.
616    ///
617    /// Returns `true` if a line was available,
618    /// or `false` if the standard output has finished.
619    pub fn wait_for_stdout_line<OptS>(&mut self, write_context: OptS) -> bool
620    where
621        OptS: Into<Option<String>>,
622    {
623        self.apply_failure_regexes_to_outputs();
624
625        if let Some(line_result) = self.stdout.as_mut().and_then(|iter| iter.next()) {
626            let bypass_test_capture = self.bypass_test_capture;
627
628            if let Some(write_context) = write_context.into() {
629                Self::write_to_test_logs(write_context, bypass_test_capture);
630            }
631
632            Self::write_to_test_logs(
633                line_result
634                    .context_from(self)
635                    .expect("failure reading test process logs"),
636                bypass_test_capture,
637            );
638
639            return true;
640        }
641
642        false
643    }
644
645    /// Waits until a line of standard error is available, then consumes it.
646    ///
647    /// If there is a line, and `write_context` is `Some`, writes the context to the test logs.
648    /// Always writes the line to the test logs.
649    ///
650    /// Returns `true` if a line was available,
651    /// or `false` if the standard error has finished.
652    pub fn wait_for_stderr_line<OptS>(&mut self, write_context: OptS) -> bool
653    where
654        OptS: Into<Option<String>>,
655    {
656        self.apply_failure_regexes_to_outputs();
657
658        if let Some(line_result) = self.stderr.as_mut().and_then(|iter| iter.next()) {
659            let bypass_test_capture = self.bypass_test_capture;
660
661            if let Some(write_context) = write_context.into() {
662                Self::write_to_test_logs(write_context, bypass_test_capture);
663            }
664
665            Self::write_to_test_logs(
666                line_result
667                    .context_from(self)
668                    .expect("failure reading test process logs"),
669                bypass_test_capture,
670            );
671
672            return true;
673        }
674
675        false
676    }
677
678    /// Waits until a line of `output` is available, then returns it.
679    ///
680    /// If there is a line, and `write_context` is `Some`, writes the context to the test logs.
681    /// Always writes the line to the test logs.
682    ///
683    /// Returns `true` if a line was available,
684    /// or `false` if the standard output has finished.
685    #[allow(clippy::unwrap_in_result)]
686    fn wait_and_return_output_line(
687        mut output: impl Iterator<Item = std::io::Result<String>>,
688        bypass_test_capture: bool,
689    ) -> Option<String> {
690        if let Some(line_result) = output.next() {
691            let line_result = line_result.expect("failure reading test process logs");
692
693            Self::write_to_test_logs(&line_result, bypass_test_capture);
694
695            return Some(line_result);
696        }
697
698        None
699    }
700
701    /// Waits for the child process to exit, then returns its output.
702    ///
703    /// # Correctness
704    ///
705    /// Outputs already taken by other test child output methods are drained concurrently,
706    /// checking failure regexes, but are not included in the returned output.
707    ///
708    /// Ignores any configured timeouts.
709    ///
710    /// Returns an error if the child has already been taken.
711    #[spandoc::spandoc]
712    #[allow(clippy::unwrap_in_result)]
713    pub fn wait_with_output(mut self) -> Result<TestOutput<T>> {
714        let child = match self.child.take() {
715            Some(child) => child,
716
717            // Also checks the taken child output for failure regexes,
718            // either in `context_from`, or on drop.
719            None => {
720                return Err(eyre!(
721                    "test child was already taken\n\
722                     wait_with_output can only be called once for each child process",
723                ))
724                .context_from(self.as_mut())
725            }
726        };
727
728        let bypass_test_capture = self.bypass_test_capture;
729        let output = std::thread::scope(|scope| {
730            let readers = [self.stdout.take(), self.stderr.take()].map(|lines| {
731                lines.map(|lines| {
732                    scope.spawn(move || {
733                        // Keep draining after an error, so the child can always exit.
734                        let mut result = Ok(());
735                        for line in lines {
736                            result =
737                                result.and(line.map(|line| {
738                                    Self::write_to_test_logs(line, bypass_test_capture)
739                                }));
740                        }
741                        result
742                    })
743                })
744            });
745
746            /// SPANDOC: waiting for command to exit
747            let output = child.wait_with_output();
748            for reader in readers.into_iter().flatten() {
749                match reader.join() {
750                    Ok(result) => result.expect("failure reading test process logs"),
751                    Err(panic) => std::panic::resume_unwind(panic),
752                }
753            }
754            output
755        })
756        .with_section({
757            let cmd = self.cmd.clone();
758            || cmd.header("Command:")
759        })?;
760
761        Ok(TestOutput {
762            output,
763            cmd: self.cmd.clone(),
764            dir: self.dir.take(),
765        })
766    }
767
768    /// Set a timeout for `expect_stdout_line_matches` or `expect_stderr_line_matches`.
769    ///
770    /// Does not apply to `wait_with_output`.
771    pub fn with_timeout(mut self, timeout: Duration) -> Self {
772        self.timeout = Some(timeout);
773        self.deadline = Some(Instant::now() + timeout);
774
775        self
776    }
777
778    /// Configures this test runner to forward stdout and stderr to the true stdout,
779    /// rather than the fakestdout used by cargo tests.
780    pub fn bypass_test_capture(mut self, cond: bool) -> Self {
781        self.bypass_test_capture = cond;
782        self
783    }
784
785    /// Checks each line of the child's stdout against any regex in `success_regex`,
786    /// and returns the first matching line. Prints all stdout lines.
787    ///
788    /// Kills the child on error, or after the configured timeout has elapsed.
789    /// See [`Self::expect_line_matching_regex_set`] for details.
790    //
791    // TODO: these methods could block if stderr is full and stdout is waiting for stderr to be read
792    #[instrument(skip(self))]
793    #[allow(clippy::unwrap_in_result)]
794    pub fn expect_stdout_line_matches<R>(&mut self, success_regex: R) -> Result<String>
795    where
796        R: ToRegexSet + Debug,
797    {
798        self.apply_failure_regexes_to_outputs();
799
800        let mut lines = self
801            .stdout
802            .take()
803            .expect("child must capture stdout to call expect_stdout_line_matches, and it can't be called again after an error");
804
805        match self.expect_line_matching_regex_set(&mut lines, success_regex, "stdout", true) {
806            Ok(line) => {
807                // Replace the log lines for the next check
808                self.stdout = Some(lines);
809                Ok(line)
810            }
811            Err(report) => {
812                // Read all the log lines for error context
813                self.stdout = Some(lines);
814                Err(report).context_from(self)
815            }
816        }
817    }
818
819    /// Checks each line of the child's stderr against any regex in `success_regex`,
820    /// and returns the first matching line. Prints all stderr lines to stdout.
821    ///
822    /// Kills the child on error, or after the configured timeout has elapsed.
823    /// See [`Self::expect_line_matching_regex_set`] for details.
824    #[instrument(skip(self))]
825    #[allow(clippy::unwrap_in_result)]
826    pub fn expect_stderr_line_matches<R>(&mut self, success_regex: R) -> Result<String>
827    where
828        R: ToRegexSet + Debug,
829    {
830        self.apply_failure_regexes_to_outputs();
831
832        let mut lines = self
833            .stderr
834            .take()
835            .expect("child must capture stderr to call expect_stderr_line_matches, and it can't be called again after an error");
836
837        match self.expect_line_matching_regex_set(&mut lines, success_regex, "stderr", true) {
838            Ok(line) => {
839                // Replace the log lines for the next check
840                self.stderr = Some(lines);
841                Ok(line)
842            }
843            Err(report) => {
844                // Read all the log lines for error context
845                self.stderr = Some(lines);
846                Err(report).context_from(self)
847            }
848        }
849    }
850
851    /// Checks each line of the child's stdout, until it finds every regex in `unordered_regexes`,
852    /// and returns all lines matched by any regex, until each regex has been matched at least once.
853    /// If the output finishes or the command times out before all regexes are matched, returns an error with
854    /// a list of unmatched regexes. Prints all stdout lines.
855    ///
856    /// Kills the child on error, or after the configured timeout has elapsed.
857    /// See [`Self::expect_line_matching_regex_set`] for details.
858    //
859    // TODO: these methods could block if stderr is full and stdout is waiting for stderr to be read
860    #[instrument(skip(self))]
861    #[allow(clippy::unwrap_in_result)]
862    pub fn expect_stdout_line_matches_all_unordered<RegexList>(
863        &mut self,
864        unordered_regexes: RegexList,
865    ) -> Result<Vec<String>>
866    where
867        RegexList: IntoIterator + Debug,
868        RegexList::Item: ToRegexSet,
869    {
870        let regex_list = unordered_regexes.collect_regex_set()?;
871
872        let mut unmatched_indexes: HashSet<usize> = (0..regex_list.len()).collect();
873        let mut matched_lines = Vec::new();
874
875        while !unmatched_indexes.is_empty() {
876            let line = self
877                .expect_stdout_line_matches(regex_list.clone())
878                .map_err(|err| {
879                    let unmatched_regexes = regex_list.patterns_for_indexes(&unmatched_indexes);
880
881                    err.with_section(|| {
882                        format!("{unmatched_regexes:#?}").header("Unmatched regexes:")
883                    })
884                    .with_section(|| format!("{matched_lines:#?}").header("Matched lines:"))
885                })?;
886
887            let matched_indices: HashSet<usize> = regex_list.matches(&line).iter().collect();
888            unmatched_indexes = &unmatched_indexes - &matched_indices;
889
890            matched_lines.push(line);
891        }
892
893        Ok(matched_lines)
894    }
895
896    /// Checks each line of the child's stderr, until it finds every regex in `unordered_regexes`,
897    /// and returns all lines matched by any regex, until each regex has been matched at least once.
898    /// If the output finishes or the command times out before all regexes are matched, returns an error with
899    /// a list of unmatched regexes. Prints all stderr lines.
900    ///
901    /// Kills the child on error, or after the configured timeout has elapsed.
902    /// See [`Self::expect_line_matching_regex_set`] for details.
903    //
904    // TODO: these methods could block if stdout is full and stderr is waiting for stdout to be read
905    #[instrument(skip(self))]
906    #[allow(clippy::unwrap_in_result)]
907    pub fn expect_stderr_line_matches_all_unordered<RegexList>(
908        &mut self,
909        unordered_regexes: RegexList,
910    ) -> Result<Vec<String>>
911    where
912        RegexList: IntoIterator + Debug,
913        RegexList::Item: ToRegexSet,
914    {
915        let regex_list = unordered_regexes.collect_regex_set()?;
916
917        let mut unmatched_indexes: HashSet<usize> = (0..regex_list.len()).collect();
918        let mut matched_lines = Vec::new();
919
920        while !unmatched_indexes.is_empty() {
921            let line = self
922                .expect_stderr_line_matches(regex_list.clone())
923                .map_err(|err| {
924                    let unmatched_regexes = regex_list.patterns_for_indexes(&unmatched_indexes);
925
926                    err.with_section(|| {
927                        format!("{unmatched_regexes:#?}").header("Unmatched regexes:")
928                    })
929                    .with_section(|| format!("{matched_lines:#?}").header("Matched lines:"))
930                })?;
931
932            let matched_indices: HashSet<usize> = regex_list.matches(&line).iter().collect();
933            unmatched_indexes = &unmatched_indexes - &matched_indices;
934
935            matched_lines.push(line);
936        }
937
938        Ok(matched_lines)
939    }
940
941    /// Checks each line of the child's stdout against `success_regex`,
942    /// and returns the first matching line. Does not print any output.
943    ///
944    /// Kills the child on error, or after the configured timeout has elapsed.
945    /// See [`Self::expect_line_matching_regex_set`] for details.
946    #[instrument(skip(self))]
947    #[allow(clippy::unwrap_in_result)]
948    pub fn expect_stdout_line_matches_silent<R>(&mut self, success_regex: R) -> Result<String>
949    where
950        R: ToRegexSet + Debug,
951    {
952        self.apply_failure_regexes_to_outputs();
953
954        let mut lines = self
955            .stdout
956            .take()
957            .expect("child must capture stdout to call expect_stdout_line_matches, and it can't be called again after an error");
958
959        match self.expect_line_matching_regex_set(&mut lines, success_regex, "stdout", false) {
960            Ok(line) => {
961                // Replace the log lines for the next check
962                self.stdout = Some(lines);
963                Ok(line)
964            }
965            Err(report) => {
966                // Read all the log lines for error context
967                self.stdout = Some(lines);
968                Err(report).context_from(self)
969            }
970        }
971    }
972
973    /// Checks each line of the child's stderr against `success_regex`,
974    /// and returns the first matching line. Does not print any output.
975    ///
976    /// Kills the child on error, or after the configured timeout has elapsed.
977    /// See [`Self::expect_line_matching_regex_set`] for details.
978    #[instrument(skip(self))]
979    #[allow(clippy::unwrap_in_result)]
980    pub fn expect_stderr_line_matches_silent<R>(&mut self, success_regex: R) -> Result<String>
981    where
982        R: ToRegexSet + Debug,
983    {
984        self.apply_failure_regexes_to_outputs();
985
986        let mut lines = self
987            .stderr
988            .take()
989            .expect("child must capture stderr to call expect_stderr_line_matches, and it can't be called again after an error");
990
991        match self.expect_line_matching_regex_set(&mut lines, success_regex, "stderr", false) {
992            Ok(line) => {
993                // Replace the log lines for the next check
994                self.stderr = Some(lines);
995                Ok(line)
996            }
997            Err(report) => {
998                // Read all the log lines for error context
999                self.stderr = Some(lines);
1000                Err(report).context_from(self)
1001            }
1002        }
1003    }
1004
1005    /// Checks each line in `lines` against a regex set, and returns Ok if a line matches.
1006    ///
1007    /// [`Self::expect_line_matching_regexes`] wrapper for strings,
1008    /// [`Regex`](regex::Regex)es, and [`RegexSet`]s.
1009    #[allow(clippy::unwrap_in_result)]
1010    pub fn expect_line_matching_regex_set<L, R>(
1011        &mut self,
1012        lines: &mut L,
1013        success_regexes: R,
1014        stream_name: &str,
1015        write_to_logs: bool,
1016    ) -> Result<String>
1017    where
1018        L: Iterator<Item = std::io::Result<String>>,
1019        R: ToRegexSet,
1020    {
1021        let success_regexes = success_regexes
1022            .to_regex_set()
1023            .expect("regexes must be valid");
1024
1025        self.expect_line_matching_regexes(lines, success_regexes, stream_name, write_to_logs)
1026    }
1027
1028    /// Checks each line in `lines` against a regex set, and returns Ok if a line matches.
1029    ///
1030    /// [`Self::expect_line_matching_regexes`] wrapper for regular expression iterators.
1031    #[allow(clippy::unwrap_in_result)]
1032    pub fn expect_line_matching_regex_iter<L, I>(
1033        &mut self,
1034        lines: &mut L,
1035        success_regexes: I,
1036        stream_name: &str,
1037        write_to_logs: bool,
1038    ) -> Result<String>
1039    where
1040        L: Iterator<Item = std::io::Result<String>>,
1041        I: CollectRegexSet,
1042    {
1043        let success_regexes = success_regexes
1044            .collect_regex_set()
1045            .expect("regexes must be valid");
1046
1047        self.expect_line_matching_regexes(lines, success_regexes, stream_name, write_to_logs)
1048    }
1049
1050    /// Checks each line in `lines` against `success_regexes`, and returns Ok if a line
1051    /// matches. Uses `stream_name` as the name for `lines` in error reports.
1052    ///
1053    /// Kills the child on error, or after the configured timeout has elapsed.
1054    ///
1055    /// Note: the timeout is only checked after each full line is received from
1056    /// the child (#1140).
1057    #[instrument(skip(self, lines))]
1058    #[allow(clippy::unwrap_in_result)]
1059    pub fn expect_line_matching_regexes<L>(
1060        &mut self,
1061        lines: &mut L,
1062        success_regexes: RegexSet,
1063        stream_name: &str,
1064        write_to_logs: bool,
1065    ) -> Result<String>
1066    where
1067        L: Iterator<Item = std::io::Result<String>>,
1068    {
1069        // We don't check `is_running` here,
1070        // because we want to read to the end of the buffered output,
1071        // even if the child process has exited.
1072        while !self.past_deadline() {
1073            let line = if let Some(line) = lines.next() {
1074                line?
1075            } else {
1076                // When the child process closes its output,
1077                // and we've read all of the buffered output,
1078                // stop checking for any more output.
1079                break;
1080            };
1081
1082            if write_to_logs {
1083                // Since we're about to discard this line write it to stdout.
1084                Self::write_to_test_logs(&line, self.bypass_test_capture);
1085            }
1086
1087            if success_regexes.is_match(&line) {
1088                return Ok(line);
1089            }
1090        }
1091
1092        if self.is_running() {
1093            // If the process exits between is_running and kill, we will see
1094            // spurious errors here. So we want to ignore "no such process"
1095            // errors from kill.
1096            self.kill(true)?;
1097        }
1098
1099        let timeout = self
1100            .timeout
1101            .map(|timeout| humantime::format_duration(timeout).to_string())
1102            .unwrap_or_else(|| "unlimited".to_string());
1103
1104        let report = eyre!(
1105            "{stream_name} of command did not log any matches for the given regex,\n\
1106             within the {timeout} command timeout",
1107        )
1108        .with_section(|| format!("{:#?}", success_regexes.patterns()).header("Match Regex:"));
1109
1110        Err(report)
1111    }
1112
1113    /// Write `line` to stdout, so it can be seen in the test logs.
1114    ///
1115    /// Use [bypass_test_capture(true)](TestChild::bypass_test_capture) or
1116    /// `cargo test -- --nocapture` to see this output.
1117    ///
1118    /// May cause weird reordering for stdout / stderr.
1119    /// Uses stdout even if the original lines were from stderr.
1120    #[allow(clippy::print_stdout)]
1121    fn write_to_test_logs<S>(line: S, bypass_test_capture: bool)
1122    where
1123        S: AsRef<str>,
1124    {
1125        write_to_test_logs(line, bypass_test_capture);
1126    }
1127
1128    /// Kill `child`, wait for its output, and use that output as the context for
1129    /// an error report based on `error`.
1130    #[instrument(skip(self, result))]
1131    pub fn kill_on_error<V, E>(mut self, result: Result<V, E>) -> Result<(V, Self), Report>
1132    where
1133        E: Into<Report> + Send + Sync + 'static,
1134    {
1135        let mut error: Report = match result {
1136            Ok(success) => return Ok((success, self)),
1137            Err(error) => error.into(),
1138        };
1139
1140        if self.is_running() {
1141            let kill_res = self.kill(true);
1142            if let Err(kill_err) = kill_res {
1143                error = error.wrap_err(kill_err);
1144            }
1145        }
1146
1147        let output_res = self.wait_with_output();
1148        let error = match output_res {
1149            Err(output_err) => error.wrap_err(output_err),
1150            Ok(output) => error.context_from(&output),
1151        };
1152
1153        Err(error)
1154    }
1155
1156    fn past_deadline(&self) -> bool {
1157        self.deadline
1158            .map(|deadline| Instant::now() > deadline)
1159            .unwrap_or(false)
1160    }
1161
1162    /// Is this process currently running?
1163    ///
1164    /// ## Bugs
1165    ///
1166    /// On Windows and macOS, this function can return `true` for processes that
1167    /// have panicked. See #1781.
1168    ///
1169    /// ## Panics
1170    ///
1171    /// If the child process was already been taken using wait_with_output.
1172    pub fn is_running(&mut self) -> bool {
1173        matches!(
1174            self.child
1175                .as_mut()
1176                .expect("child has not been taken")
1177                .try_wait(),
1178            Ok(None),
1179        )
1180    }
1181}
1182
1183impl<T> AsRef<TestChild<T>> for TestChild<T> {
1184    fn as_ref(&self) -> &Self {
1185        self
1186    }
1187}
1188
1189impl<T> AsMut<TestChild<T>> for TestChild<T> {
1190    fn as_mut(&mut self) -> &mut Self {
1191        self
1192    }
1193}
1194
1195impl<T> Drop for TestChild<T> {
1196    fn drop(&mut self) {
1197        // Clean up child processes when the test finishes,
1198        // and check for failure logs.
1199        self.kill_and_consume_output(true)
1200            .expect("failure reading test process logs")
1201    }
1202}
1203
1204/// Test command output logs.
1205// TODO: split this struct into its own module
1206#[derive(Debug)]
1207pub struct TestOutput<T> {
1208    /// The test directory for this test output.
1209    ///
1210    /// Keeps the test dir around from `TestChild`,
1211    /// so it doesn't get deleted during `wait_with_output`.
1212    #[allow(dead_code)]
1213    pub dir: Option<T>,
1214
1215    /// The test command for this test output.
1216    pub cmd: String,
1217
1218    /// The test exit status, standard out, and standard error.
1219    pub output: Output,
1220}
1221
1222impl<T> TestOutput<T> {
1223    pub fn assert_success(self) -> Result<Self> {
1224        if !self.output.status.success() {
1225            Err(eyre!("command exited unsuccessfully")).context_from(&self)?;
1226        }
1227
1228        Ok(self)
1229    }
1230
1231    pub fn assert_failure(self) -> Result<Self> {
1232        if self.output.status.success() {
1233            Err(eyre!("command unexpectedly exited successfully")).context_from(&self)?;
1234        }
1235
1236        Ok(self)
1237    }
1238
1239    /// Checks the output of a command, using a closure to determine if the
1240    /// output is valid.
1241    ///
1242    /// If the closure returns `true`, the check returns `Ok(self)`.
1243    /// If the closure returns `false`, the check returns an error containing
1244    /// `output_name` and `err_msg`, with context from the command.
1245    ///
1246    /// `output` is typically `self.output.stdout` or `self.output.stderr`.
1247    #[instrument(skip(self, output_predicate, output))]
1248    pub fn output_check<P>(
1249        &self,
1250        output_predicate: P,
1251        output: &[u8],
1252        output_name: impl ToString + fmt::Debug,
1253        err_msg: impl ToString + fmt::Debug,
1254    ) -> Result<&Self>
1255    where
1256        P: FnOnce(&str) -> bool,
1257    {
1258        let output = String::from_utf8_lossy(output);
1259
1260        if output_predicate(&output) {
1261            Ok(self)
1262        } else {
1263            Err(eyre!(
1264                "{} of command did not {}",
1265                output_name.to_string(),
1266                err_msg.to_string()
1267            ))
1268            .context_from(self)
1269        }
1270    }
1271
1272    /// Checks each line in the output of a command, using a closure to determine
1273    /// if the line is valid.
1274    ///
1275    /// See [`Self::output_check`] for details.
1276    #[instrument(skip(self, line_predicate, output))]
1277    pub fn any_output_line<P>(
1278        &self,
1279        mut line_predicate: P,
1280        output: &[u8],
1281        output_name: impl ToString + fmt::Debug,
1282        err_msg: impl ToString + fmt::Debug,
1283    ) -> Result<&Self>
1284    where
1285        P: FnMut(&str) -> bool,
1286    {
1287        let output_predicate = |stdout: &str| {
1288            for line in stdout.lines() {
1289                if line_predicate(line) {
1290                    return true;
1291                }
1292            }
1293            false
1294        };
1295
1296        self.output_check(
1297            output_predicate,
1298            output,
1299            output_name,
1300            format!("have any lines that {}", err_msg.to_string()),
1301        )
1302    }
1303
1304    /// Tests if any lines in the output of a command contain `s`.
1305    ///
1306    /// See [`Self::any_output_line`] for details.
1307    #[instrument(skip(self, output))]
1308    pub fn any_output_line_contains(
1309        &self,
1310        s: &str,
1311        output: &[u8],
1312        output_name: impl ToString + fmt::Debug,
1313        err_msg: impl ToString + fmt::Debug,
1314    ) -> Result<&Self> {
1315        self.any_output_line(
1316            |line| line.contains(s),
1317            output,
1318            output_name,
1319            format!("contain {}", err_msg.to_string()),
1320        )
1321        .with_section(|| format!("{s:?}").header("Match String:"))
1322    }
1323
1324    /// Tests if standard output contains `s`.
1325    #[instrument(skip(self))]
1326    pub fn stdout_contains(&self, s: &str) -> Result<&Self> {
1327        self.output_check(
1328            |stdout| stdout.contains(s),
1329            &self.output.stdout,
1330            "stdout",
1331            "contain the given string",
1332        )
1333        .with_section(|| format!("{s:?}").header("Match String:"))
1334    }
1335
1336    /// Tests if standard output matches `regex`.
1337    #[instrument(skip(self))]
1338    #[allow(clippy::unwrap_in_result)]
1339    pub fn stdout_matches<R>(&self, regex: R) -> Result<&Self>
1340    where
1341        R: ToRegexSet + Debug,
1342    {
1343        let re = regex.to_regex_set().expect("regex must be valid");
1344
1345        self.output_check(
1346            |stdout| re.is_match(stdout),
1347            &self.output.stdout,
1348            "stdout",
1349            "matched the given regex",
1350        )
1351        .with_section(|| format!("{regex:?}").header("Match Regex:"))
1352    }
1353
1354    /// Tests if any lines in standard output contain `s`.
1355    #[instrument(skip(self))]
1356    pub fn stdout_line_contains(&self, s: &str) -> Result<&Self> {
1357        self.any_output_line_contains(s, &self.output.stdout, "stdout", "the given string")
1358    }
1359
1360    /// Tests if any lines in standard output match `regex`.
1361    #[instrument(skip(self))]
1362    #[allow(clippy::unwrap_in_result)]
1363    pub fn stdout_line_matches<R>(&self, regex: R) -> Result<&Self>
1364    where
1365        R: ToRegexSet + Debug,
1366    {
1367        let re = regex.to_regex_set().expect("regex must be valid");
1368
1369        self.any_output_line(
1370            |line| re.is_match(line),
1371            &self.output.stdout,
1372            "stdout",
1373            "matched the given regex",
1374        )
1375        .with_section(|| format!("{regex:?}").header("Line Match Regex:"))
1376    }
1377
1378    /// Tests if standard error contains `s`.
1379    #[instrument(skip(self))]
1380    pub fn stderr_contains(&self, s: &str) -> Result<&Self> {
1381        self.output_check(
1382            |stderr| stderr.contains(s),
1383            &self.output.stderr,
1384            "stderr",
1385            "contain the given string",
1386        )
1387        .with_section(|| format!("{s:?}").header("Match String:"))
1388    }
1389
1390    /// Tests if standard error matches `regex`.
1391    #[instrument(skip(self))]
1392    #[allow(clippy::unwrap_in_result)]
1393    pub fn stderr_matches<R>(&self, regex: R) -> Result<&Self>
1394    where
1395        R: ToRegexSet + Debug,
1396    {
1397        let re = regex.to_regex_set().expect("regex must be valid");
1398
1399        self.output_check(
1400            |stderr| re.is_match(stderr),
1401            &self.output.stderr,
1402            "stderr",
1403            "matched the given regex",
1404        )
1405        .with_section(|| format!("{regex:?}").header("Match Regex:"))
1406    }
1407
1408    /// Tests if any lines in standard error contain `s`.
1409    #[instrument(skip(self))]
1410    pub fn stderr_line_contains(&self, s: &str) -> Result<&Self> {
1411        self.any_output_line_contains(s, &self.output.stderr, "stderr", "the given string")
1412    }
1413
1414    /// Tests if any lines in standard error match `regex`.
1415    #[instrument(skip(self))]
1416    #[allow(clippy::unwrap_in_result)]
1417    pub fn stderr_line_matches<R>(&self, regex: R) -> Result<&Self>
1418    where
1419        R: ToRegexSet + Debug,
1420    {
1421        let re = regex.to_regex_set().expect("regex must be valid");
1422
1423        self.any_output_line(
1424            |line| re.is_match(line),
1425            &self.output.stderr,
1426            "stderr",
1427            "matched the given regex",
1428        )
1429        .with_section(|| format!("{regex:?}").header("Line Match Regex:"))
1430    }
1431
1432    /// Returns Ok if the program was killed, Err(Report) if exit was by another
1433    /// reason.
1434    pub fn assert_was_killed(&self) -> Result<()> {
1435        if self.was_killed() {
1436            Ok(())
1437        } else {
1438            Err(eyre!(
1439                "command exited without a kill, but the test expected kill exit"
1440            ))
1441            .context_from(self)?
1442        }
1443    }
1444
1445    /// Returns Ok if the program was not killed, Err(Report) if exit was by
1446    /// another reason.
1447    pub fn assert_was_not_killed(&self) -> Result<()> {
1448        if self.was_killed() {
1449            Err(eyre!(
1450                "command was killed, but the test expected an exit without a kill"
1451            ))
1452            .context_from(self)?
1453        } else {
1454            Ok(())
1455        }
1456    }
1457
1458    #[cfg(not(unix))]
1459    fn was_killed(&self) -> bool {
1460        self.output.status.code() == Some(1)
1461    }
1462
1463    #[cfg(unix)]
1464    fn was_killed(&self) -> bool {
1465        self.output.status.signal() == Some(9)
1466    }
1467
1468    /// Takes the generic `dir` parameter out of this `TestOutput`.
1469    pub fn take_dir(&mut self) -> Option<T> {
1470        self.dir.take()
1471    }
1472}
1473
1474/// Add context to an error report
1475// TODO: split this trait into its own module
1476pub trait ContextFrom<S> {
1477    type Return;
1478
1479    fn context_from(self, source: S) -> Self::Return;
1480}
1481
1482impl<C, T, E> ContextFrom<C> for Result<T, E>
1483where
1484    E: Into<Report>,
1485    Report: ContextFrom<C, Return = Report>,
1486{
1487    type Return = Result<T, Report>;
1488
1489    fn context_from(self, source: C) -> Self::Return {
1490        self.map_err(|e| e.into())
1491            .map_err(|report| report.context_from(source))
1492    }
1493}
1494
1495impl ContextFrom<&TestStatus> for Report {
1496    type Return = Report;
1497
1498    fn context_from(self, source: &TestStatus) -> Self::Return {
1499        let command = || source.cmd.clone().header("Command:");
1500
1501        self.with_section(command).context_from(&source.status)
1502    }
1503}
1504
1505impl<T> ContextFrom<&mut TestChild<T>> for Report {
1506    type Return = Report;
1507
1508    #[allow(clippy::print_stdout)]
1509    fn context_from(mut self, source: &mut TestChild<T>) -> Self::Return {
1510        self = self.section(source.cmd.clone().header("Command:"));
1511
1512        if let Some(child) = &mut source.child {
1513            if let Ok(Some(status)) = child.try_wait() {
1514                self = self.context_from(&status);
1515            }
1516        }
1517
1518        // Reading test child process output could hang if the child process is still running,
1519        // so kill it first.
1520        if let Some(child) = source.child.as_mut() {
1521            let _ = child.kill();
1522        }
1523
1524        let mut stdout_buf = String::new();
1525        let mut stderr_buf = String::new();
1526
1527        if let Some(stdout) = &mut source.stdout {
1528            for line in stdout {
1529                let line = line.unwrap_or_else(|error| {
1530                    format!("failure reading test process logs: {error:?}")
1531                });
1532                let _ = writeln!(&mut stdout_buf, "{line}");
1533            }
1534        } else if let Some(child) = &mut source.child {
1535            if let Some(stdout) = &mut child.stdout {
1536                let _ = stdout.read_to_string(&mut stdout_buf);
1537            }
1538        }
1539
1540        if let Some(stderr) = &mut source.stderr {
1541            for line in stderr {
1542                let line = line.unwrap_or_else(|error| {
1543                    format!("failure reading test process logs: {error:?}")
1544                });
1545                let _ = writeln!(&mut stderr_buf, "{line}");
1546            }
1547        } else if let Some(child) = &mut source.child {
1548            if let Some(stderr) = &mut child.stderr {
1549                let _ = stderr.read_to_string(&mut stderr_buf);
1550            }
1551        }
1552
1553        self.section(stdout_buf.header(format!("{} Unread Stdout:", source.command_path)))
1554            .section(stderr_buf.header(format!("{} Unread Stderr:", source.command_path)))
1555    }
1556}
1557
1558impl<T> ContextFrom<&TestOutput<T>> for Report {
1559    type Return = Report;
1560
1561    fn context_from(self, source: &TestOutput<T>) -> Self::Return {
1562        self.with_section(|| source.cmd.clone().header("Command:"))
1563            .context_from(&source.output)
1564    }
1565}
1566
1567impl ContextFrom<&Output> for Report {
1568    type Return = Report;
1569
1570    fn context_from(self, source: &Output) -> Self::Return {
1571        // TODO: add TestChild.command_path before Stdout and Stderr header names
1572        let stdout = || {
1573            String::from_utf8_lossy(&source.stdout)
1574                .into_owned()
1575                .header("Stdout:")
1576        };
1577        let stderr = || {
1578            String::from_utf8_lossy(&source.stderr)
1579                .into_owned()
1580                .header("Stderr:")
1581        };
1582
1583        self.context_from(&source.status)
1584            .with_section(stdout)
1585            .with_section(stderr)
1586    }
1587}
1588
1589impl ContextFrom<&ExitStatus> for Report {
1590    type Return = Report;
1591
1592    fn context_from(self, source: &ExitStatus) -> Self::Return {
1593        let how = if source.success() {
1594            "successfully"
1595        } else {
1596            "unsuccessfully"
1597        };
1598
1599        if let Some(code) = source.code() {
1600            return self.with_section(|| {
1601                format!("command exited {how} with status code {code}").header("Exit Status:")
1602            });
1603        }
1604
1605        #[cfg(unix)]
1606        if let Some(signal) = source.signal() {
1607            self.with_section(|| {
1608                format!("command terminated {how} by signal {signal}").header("Exit Status:")
1609            })
1610        } else {
1611            unreachable!("on unix all processes either terminate via signal or with an exit code");
1612        }
1613
1614        #[cfg(not(unix))]
1615        self.with_section(|| {
1616            format!("command exited {} without a status code or signal", how).header("Exit Status:")
1617        })
1618    }
1619}