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    /// The failure regexes are ignored by `wait_with_output`.
348    ///
349    /// To never match any log lines, use `RegexSet::empty()`.
350    ///
351    /// This method is a [`TestChild::with_failure_regexes`] wrapper for
352    /// strings, `Regex`es, and [`RegexSet`]s.
353    ///
354    /// # Panics
355    ///
356    /// - adds a panic to any method that reads output,
357    ///   if any stdout or stderr lines match any failure regex
358    pub fn with_failure_regex_set<F, X>(self, failure_regexes: F, ignore_regexes: X) -> Self
359    where
360        F: ToRegexSet,
361        X: ToRegexSet,
362    {
363        let failure_regexes = failure_regexes
364            .to_regex_set()
365            .expect("failure regexes must be valid");
366
367        let ignore_regexes = ignore_regexes
368            .to_regex_set()
369            .expect("ignore regexes must be valid");
370
371        self.with_failure_regexes(failure_regexes, ignore_regexes)
372    }
373
374    /// Sets up command output so each line is checked against a failure regex set,
375    /// unless it matches any of the ignore regexes.
376    ///
377    /// The failure regexes are ignored by `wait_with_output`.
378    ///
379    /// To never match any log lines, use [`NO_MATCHES_REGEX_ITER`].
380    ///
381    /// This method is a [`TestChild::with_failure_regexes`] wrapper for
382    /// regular expression iterators.
383    ///
384    /// # Panics
385    ///
386    /// - adds a panic to any method that reads output,
387    ///   if any stdout or stderr lines match any failure regex
388    pub fn with_failure_regex_iter<F, X>(self, failure_regexes: F, ignore_regexes: X) -> Self
389    where
390        F: CollectRegexSet,
391        X: CollectRegexSet,
392    {
393        let failure_regexes = failure_regexes
394            .collect_regex_set()
395            .expect("failure regexes must be valid");
396
397        let ignore_regexes = ignore_regexes
398            .collect_regex_set()
399            .expect("ignore regexes must be valid");
400
401        self.with_failure_regexes(failure_regexes, ignore_regexes)
402    }
403
404    /// Sets up command output so each line is checked against a failure regex set,
405    /// unless it matches any of the ignore regexes.
406    ///
407    /// The failure regexes are ignored by `wait_with_output`.
408    ///
409    /// # Panics
410    ///
411    /// - adds a panic to any method that reads output,
412    ///   if any stdout or stderr lines match any failure regex
413    pub fn with_failure_regexes(
414        mut self,
415        failure_regexes: RegexSet,
416        ignore_regexes: impl Into<Option<RegexSet>>,
417    ) -> Self {
418        self.failure_regexes = failure_regexes;
419        self.ignore_regexes = ignore_regexes.into().unwrap_or_else(RegexSet::empty);
420
421        self.apply_failure_regexes_to_outputs();
422
423        self
424    }
425
426    /// Applies the failure and ignore regex sets to command output.
427    ///
428    /// The failure regexes are ignored by `wait_with_output`.
429    ///
430    /// # Panics
431    ///
432    /// - adds a panic to any method that reads output,
433    ///   if any stdout or stderr lines match any failure regex
434    pub fn apply_failure_regexes_to_outputs(&mut self) {
435        if self.stdout.is_none() {
436            self.stdout = self
437                .child
438                .as_mut()
439                .and_then(|child| child.stdout.take())
440                .map(|output| self.map_into_string_lines(output))
441        }
442
443        if self.stderr.is_none() {
444            self.stderr = self
445                .child
446                .as_mut()
447                .and_then(|child| child.stderr.take())
448                .map(|output| self.map_into_string_lines(output))
449        }
450    }
451
452    /// Maps a reader into a string line iterator,
453    /// and applies the failure and ignore regex sets to it.
454    fn map_into_string_lines<R>(
455        &self,
456        reader: R,
457    ) -> Box<dyn IteratorDebug<Item = std::io::Result<String>> + Send>
458    where
459        R: Read + Debug + Send + 'static,
460    {
461        let failure_regexes = self.failure_regexes.clone();
462        let ignore_regexes = self.ignore_regexes.clone();
463        let cmd = self.cmd.clone();
464        let bypass_test_capture = self.bypass_test_capture;
465
466        let reader = BufReader::new(reader);
467        let lines = BufRead::lines(reader).map(move |line| {
468            check_failure_regexes(
469                line,
470                &failure_regexes,
471                &ignore_regexes,
472                &cmd,
473                bypass_test_capture,
474            )
475        });
476
477        Box::new(lines) as _
478    }
479
480    /// Kill the child process.
481    ///
482    /// If `ignore_exited` is `true`, log "can't kill an exited process" errors,
483    /// rather than returning them.
484    ///
485    /// Returns the result of the kill.
486    ///
487    /// ## BUGS
488    ///
489    /// On Windows (and possibly macOS), this function can return `Ok` for
490    /// processes that have panicked. See #1781.
491    #[spandoc::spandoc]
492    pub fn kill(&mut self, ignore_exited: bool) -> Result<()> {
493        let child = match self.child.as_mut() {
494            Some(child) => child,
495            None if ignore_exited => {
496                Self::write_to_test_logs(
497                    "test child was already taken\n\
498                     ignoring kill because ignore_exited is true",
499                    self.bypass_test_capture,
500                );
501                return Ok(());
502            }
503            None => {
504                return Err(eyre!(
505                    "test child was already taken\n\
506                     call kill() once for each child process, or set ignore_exited to true"
507                ))
508                .context_from(self.as_mut())
509            }
510        };
511
512        /// SPANDOC: Killing child process
513        let kill_result = child.kill().or_else(|error| {
514            if ignore_exited && error.kind() == ErrorKind::InvalidInput {
515                Ok(())
516            } else {
517                Err(error)
518            }
519        });
520
521        kill_result.context_from(self.as_mut())?;
522
523        Ok(())
524    }
525
526    /// Kill the process, and consume all its remaining output.
527    ///
528    /// If `ignore_exited` is `true`, log "can't kill an exited process" errors,
529    /// rather than returning them.
530    ///
531    /// Returns the result of the kill.
532    pub fn kill_and_consume_output(&mut self, ignore_exited: bool) -> Result<()> {
533        self.apply_failure_regexes_to_outputs();
534
535        // Prevent a hang when consuming output,
536        // by making sure the child's output actually finishes.
537        let kill_result = self.kill(ignore_exited);
538
539        // Read unread child output.
540        //
541        // This checks for failure logs, and prevents some test hangs and deadlocks.
542        //
543        // TODO: this could block if stderr is full and stdout is waiting for stderr to be read.
544        if self.stdout.is_some() {
545            let wrote_lines =
546                self.wait_for_stdout_line(format!("\n{} Child Stdout:", self.command_path));
547
548            while self.wait_for_stdout_line(None) {}
549
550            if wrote_lines {
551                // Write an empty line, to make output more readable
552                Self::write_to_test_logs("", self.bypass_test_capture);
553            }
554        }
555
556        if self.stderr.is_some() {
557            let wrote_lines =
558                self.wait_for_stderr_line(format!("\n{} Child Stderr:", self.command_path));
559
560            while self.wait_for_stderr_line(None) {}
561
562            if wrote_lines {
563                Self::write_to_test_logs("", self.bypass_test_capture);
564            }
565        }
566
567        kill_result
568    }
569
570    /// Kill the process, and return all its remaining standard output and standard error output.
571    ///
572    /// If `ignore_exited` is `true`, log "can't kill an exited process" errors,
573    /// rather than returning them.
574    ///
575    /// Returns `Ok(output)`, or an error if the kill failed.
576    pub fn kill_and_return_output(&mut self, ignore_exited: bool) -> Result<String> {
577        self.apply_failure_regexes_to_outputs();
578
579        // Prevent a hang when consuming output,
580        // by making sure the child's output actually finishes.
581        let kill_result = self.kill(ignore_exited);
582
583        // Read unread child output.
584        let mut stdout_buf = String::new();
585        let mut stderr_buf = String::new();
586
587        // This also checks for failure logs, and prevents some test hangs and deadlocks.
588        loop {
589            let mut remaining_output = false;
590
591            if let Some(stdout) = self.stdout.as_mut() {
592                if let Some(line) =
593                    Self::wait_and_return_output_line(stdout, self.bypass_test_capture)
594                {
595                    stdout_buf.push_str(&line);
596                    remaining_output = true;
597                }
598            }
599
600            if let Some(stderr) = self.stderr.as_mut() {
601                if let Some(line) =
602                    Self::wait_and_return_output_line(stderr, self.bypass_test_capture)
603                {
604                    stderr_buf.push_str(&line);
605                    remaining_output = true;
606                }
607            }
608
609            if !remaining_output {
610                break;
611            }
612        }
613
614        let mut output = stdout_buf;
615        output.push_str(&stderr_buf);
616
617        kill_result.map(|()| output)
618    }
619
620    /// Waits until a line of standard output is available, then consumes it.
621    ///
622    /// If there is a line, and `write_context` is `Some`, writes the context to the test logs.
623    /// Always writes the line to the test logs.
624    ///
625    /// Returns `true` if a line was available,
626    /// or `false` if the standard output has finished.
627    pub fn wait_for_stdout_line<OptS>(&mut self, write_context: OptS) -> bool
628    where
629        OptS: Into<Option<String>>,
630    {
631        self.apply_failure_regexes_to_outputs();
632
633        if let Some(line_result) = self.stdout.as_mut().and_then(|iter| iter.next()) {
634            let bypass_test_capture = self.bypass_test_capture;
635
636            if let Some(write_context) = write_context.into() {
637                Self::write_to_test_logs(write_context, bypass_test_capture);
638            }
639
640            Self::write_to_test_logs(
641                line_result
642                    .context_from(self)
643                    .expect("failure reading test process logs"),
644                bypass_test_capture,
645            );
646
647            return true;
648        }
649
650        false
651    }
652
653    /// Waits until a line of standard error is available, then consumes it.
654    ///
655    /// If there is a line, and `write_context` is `Some`, writes the context to the test logs.
656    /// Always writes the line to the test logs.
657    ///
658    /// Returns `true` if a line was available,
659    /// or `false` if the standard error has finished.
660    pub fn wait_for_stderr_line<OptS>(&mut self, write_context: OptS) -> bool
661    where
662        OptS: Into<Option<String>>,
663    {
664        self.apply_failure_regexes_to_outputs();
665
666        if let Some(line_result) = self.stderr.as_mut().and_then(|iter| iter.next()) {
667            let bypass_test_capture = self.bypass_test_capture;
668
669            if let Some(write_context) = write_context.into() {
670                Self::write_to_test_logs(write_context, bypass_test_capture);
671            }
672
673            Self::write_to_test_logs(
674                line_result
675                    .context_from(self)
676                    .expect("failure reading test process logs"),
677                bypass_test_capture,
678            );
679
680            return true;
681        }
682
683        false
684    }
685
686    /// Waits until a line of `output` is available, then returns it.
687    ///
688    /// If there is a line, and `write_context` is `Some`, writes the context to the test logs.
689    /// Always writes the line to the test logs.
690    ///
691    /// Returns `true` if a line was available,
692    /// or `false` if the standard output has finished.
693    #[allow(clippy::unwrap_in_result)]
694    fn wait_and_return_output_line(
695        mut output: impl Iterator<Item = std::io::Result<String>>,
696        bypass_test_capture: bool,
697    ) -> Option<String> {
698        if let Some(line_result) = output.next() {
699            let line_result = line_result.expect("failure reading test process logs");
700
701            Self::write_to_test_logs(&line_result, bypass_test_capture);
702
703            return Some(line_result);
704        }
705
706        None
707    }
708
709    /// Waits for the child process to exit, then returns its output.
710    ///
711    /// # Correctness
712    ///
713    /// The other test child output methods take one or both outputs,
714    /// making them unavailable to this method.
715    ///
716    /// Ignores any configured timeouts.
717    ///
718    /// Returns an error if the child has already been taken.
719    /// TODO: return an error if both outputs have already been taken.
720    #[spandoc::spandoc]
721    pub fn wait_with_output(mut self) -> Result<TestOutput<T>> {
722        let child = match self.child.take() {
723            Some(child) => child,
724
725            // Also checks the taken child output for failure regexes,
726            // either in `context_from`, or on drop.
727            None => {
728                return Err(eyre!(
729                    "test child was already taken\n\
730                     wait_with_output can only be called once for each child process",
731                ))
732                .context_from(self.as_mut())
733            }
734        };
735
736        // TODO: fix the usage in the zebrad acceptance tests, or fix the bugs in TestChild,
737        //       then re-enable this check
738        /*
739        if child.stdout.is_none() && child.stderr.is_none() {
740            // Also checks the taken child output for failure regexes,
741            // either in `context_from`, or on drop.
742            return Err(eyre!(
743                "child stdout and stderr were already taken.\n\
744                 Hint: choose one of these alternatives:\n\
745                 1. use wait_with_output once on each child process, or\n\
746                 2. replace wait_with_output with the other TestChild output methods"
747            ))
748            .context_from(self.as_mut());
749        };
750         */
751
752        /// SPANDOC: waiting for command to exit
753        let output = child.wait_with_output().with_section({
754            let cmd = self.cmd.clone();
755            || cmd.header("Command:")
756        })?;
757
758        Ok(TestOutput {
759            output,
760            cmd: self.cmd.clone(),
761            dir: self.dir.take(),
762        })
763    }
764
765    /// Set a timeout for `expect_stdout_line_matches` or `expect_stderr_line_matches`.
766    ///
767    /// Does not apply to `wait_with_output`.
768    pub fn with_timeout(mut self, timeout: Duration) -> Self {
769        self.timeout = Some(timeout);
770        self.deadline = Some(Instant::now() + timeout);
771
772        self
773    }
774
775    /// Configures this test runner to forward stdout and stderr to the true stdout,
776    /// rather than the fakestdout used by cargo tests.
777    pub fn bypass_test_capture(mut self, cond: bool) -> Self {
778        self.bypass_test_capture = cond;
779        self
780    }
781
782    /// Checks each line of the child's stdout against any regex in `success_regex`,
783    /// and returns the first matching line. Prints all stdout lines.
784    ///
785    /// Kills the child on error, or after the configured timeout has elapsed.
786    /// See [`Self::expect_line_matching_regex_set`] for details.
787    //
788    // TODO: these methods could block if stderr is full and stdout is waiting for stderr to be read
789    #[instrument(skip(self))]
790    #[allow(clippy::unwrap_in_result)]
791    pub fn expect_stdout_line_matches<R>(&mut self, success_regex: R) -> Result<String>
792    where
793        R: ToRegexSet + Debug,
794    {
795        self.apply_failure_regexes_to_outputs();
796
797        let mut lines = self
798            .stdout
799            .take()
800            .expect("child must capture stdout to call expect_stdout_line_matches, and it can't be called again after an error");
801
802        match self.expect_line_matching_regex_set(&mut lines, success_regex, "stdout", true) {
803            Ok(line) => {
804                // Replace the log lines for the next check
805                self.stdout = Some(lines);
806                Ok(line)
807            }
808            Err(report) => {
809                // Read all the log lines for error context
810                self.stdout = Some(lines);
811                Err(report).context_from(self)
812            }
813        }
814    }
815
816    /// Checks each line of the child's stderr against any regex in `success_regex`,
817    /// and returns the first matching line. Prints all stderr lines to stdout.
818    ///
819    /// Kills the child on error, or after the configured timeout has elapsed.
820    /// See [`Self::expect_line_matching_regex_set`] for details.
821    #[instrument(skip(self))]
822    #[allow(clippy::unwrap_in_result)]
823    pub fn expect_stderr_line_matches<R>(&mut self, success_regex: R) -> Result<String>
824    where
825        R: ToRegexSet + Debug,
826    {
827        self.apply_failure_regexes_to_outputs();
828
829        let mut lines = self
830            .stderr
831            .take()
832            .expect("child must capture stderr to call expect_stderr_line_matches, and it can't be called again after an error");
833
834        match self.expect_line_matching_regex_set(&mut lines, success_regex, "stderr", true) {
835            Ok(line) => {
836                // Replace the log lines for the next check
837                self.stderr = Some(lines);
838                Ok(line)
839            }
840            Err(report) => {
841                // Read all the log lines for error context
842                self.stderr = Some(lines);
843                Err(report).context_from(self)
844            }
845        }
846    }
847
848    /// Checks each line of the child's stdout, until it finds every regex in `unordered_regexes`,
849    /// and returns all lines matched by any regex, until each regex has been matched at least once.
850    /// If the output finishes or the command times out before all regexes are matched, returns an error with
851    /// a list of unmatched regexes. Prints all stdout lines.
852    ///
853    /// Kills the child on error, or after the configured timeout has elapsed.
854    /// See [`Self::expect_line_matching_regex_set`] for details.
855    //
856    // TODO: these methods could block if stderr is full and stdout is waiting for stderr to be read
857    #[instrument(skip(self))]
858    #[allow(clippy::unwrap_in_result)]
859    pub fn expect_stdout_line_matches_all_unordered<RegexList>(
860        &mut self,
861        unordered_regexes: RegexList,
862    ) -> Result<Vec<String>>
863    where
864        RegexList: IntoIterator + Debug,
865        RegexList::Item: ToRegexSet,
866    {
867        let regex_list = unordered_regexes.collect_regex_set()?;
868
869        let mut unmatched_indexes: HashSet<usize> = (0..regex_list.len()).collect();
870        let mut matched_lines = Vec::new();
871
872        while !unmatched_indexes.is_empty() {
873            let line = self
874                .expect_stdout_line_matches(regex_list.clone())
875                .map_err(|err| {
876                    let unmatched_regexes = regex_list.patterns_for_indexes(&unmatched_indexes);
877
878                    err.with_section(|| {
879                        format!("{unmatched_regexes:#?}").header("Unmatched regexes:")
880                    })
881                    .with_section(|| format!("{matched_lines:#?}").header("Matched lines:"))
882                })?;
883
884            let matched_indices: HashSet<usize> = regex_list.matches(&line).iter().collect();
885            unmatched_indexes = &unmatched_indexes - &matched_indices;
886
887            matched_lines.push(line);
888        }
889
890        Ok(matched_lines)
891    }
892
893    /// Checks each line of the child's stderr, until it finds every regex in `unordered_regexes`,
894    /// and returns all lines matched by any regex, until each regex has been matched at least once.
895    /// If the output finishes or the command times out before all regexes are matched, returns an error with
896    /// a list of unmatched regexes. Prints all stderr lines.
897    ///
898    /// Kills the child on error, or after the configured timeout has elapsed.
899    /// See [`Self::expect_line_matching_regex_set`] for details.
900    //
901    // TODO: these methods could block if stdout is full and stderr is waiting for stdout to be read
902    #[instrument(skip(self))]
903    #[allow(clippy::unwrap_in_result)]
904    pub fn expect_stderr_line_matches_all_unordered<RegexList>(
905        &mut self,
906        unordered_regexes: RegexList,
907    ) -> Result<Vec<String>>
908    where
909        RegexList: IntoIterator + Debug,
910        RegexList::Item: ToRegexSet,
911    {
912        let regex_list = unordered_regexes.collect_regex_set()?;
913
914        let mut unmatched_indexes: HashSet<usize> = (0..regex_list.len()).collect();
915        let mut matched_lines = Vec::new();
916
917        while !unmatched_indexes.is_empty() {
918            let line = self
919                .expect_stderr_line_matches(regex_list.clone())
920                .map_err(|err| {
921                    let unmatched_regexes = regex_list.patterns_for_indexes(&unmatched_indexes);
922
923                    err.with_section(|| {
924                        format!("{unmatched_regexes:#?}").header("Unmatched regexes:")
925                    })
926                    .with_section(|| format!("{matched_lines:#?}").header("Matched lines:"))
927                })?;
928
929            let matched_indices: HashSet<usize> = regex_list.matches(&line).iter().collect();
930            unmatched_indexes = &unmatched_indexes - &matched_indices;
931
932            matched_lines.push(line);
933        }
934
935        Ok(matched_lines)
936    }
937
938    /// Checks each line of the child's stdout against `success_regex`,
939    /// and returns the first matching line. Does not print any output.
940    ///
941    /// Kills the child on error, or after the configured timeout has elapsed.
942    /// See [`Self::expect_line_matching_regex_set`] for details.
943    #[instrument(skip(self))]
944    #[allow(clippy::unwrap_in_result)]
945    pub fn expect_stdout_line_matches_silent<R>(&mut self, success_regex: R) -> Result<String>
946    where
947        R: ToRegexSet + Debug,
948    {
949        self.apply_failure_regexes_to_outputs();
950
951        let mut lines = self
952            .stdout
953            .take()
954            .expect("child must capture stdout to call expect_stdout_line_matches, and it can't be called again after an error");
955
956        match self.expect_line_matching_regex_set(&mut lines, success_regex, "stdout", false) {
957            Ok(line) => {
958                // Replace the log lines for the next check
959                self.stdout = Some(lines);
960                Ok(line)
961            }
962            Err(report) => {
963                // Read all the log lines for error context
964                self.stdout = Some(lines);
965                Err(report).context_from(self)
966            }
967        }
968    }
969
970    /// Checks each line of the child's stderr against `success_regex`,
971    /// and returns the first matching line. Does not print any output.
972    ///
973    /// Kills the child on error, or after the configured timeout has elapsed.
974    /// See [`Self::expect_line_matching_regex_set`] for details.
975    #[instrument(skip(self))]
976    #[allow(clippy::unwrap_in_result)]
977    pub fn expect_stderr_line_matches_silent<R>(&mut self, success_regex: R) -> Result<String>
978    where
979        R: ToRegexSet + Debug,
980    {
981        self.apply_failure_regexes_to_outputs();
982
983        let mut lines = self
984            .stderr
985            .take()
986            .expect("child must capture stderr to call expect_stderr_line_matches, and it can't be called again after an error");
987
988        match self.expect_line_matching_regex_set(&mut lines, success_regex, "stderr", false) {
989            Ok(line) => {
990                // Replace the log lines for the next check
991                self.stderr = Some(lines);
992                Ok(line)
993            }
994            Err(report) => {
995                // Read all the log lines for error context
996                self.stderr = Some(lines);
997                Err(report).context_from(self)
998            }
999        }
1000    }
1001
1002    /// Checks each line in `lines` against a regex set, and returns Ok if a line matches.
1003    ///
1004    /// [`Self::expect_line_matching_regexes`] wrapper for strings,
1005    /// [`Regex`](regex::Regex)es, and [`RegexSet`]s.
1006    #[allow(clippy::unwrap_in_result)]
1007    pub fn expect_line_matching_regex_set<L, R>(
1008        &mut self,
1009        lines: &mut L,
1010        success_regexes: R,
1011        stream_name: &str,
1012        write_to_logs: bool,
1013    ) -> Result<String>
1014    where
1015        L: Iterator<Item = std::io::Result<String>>,
1016        R: ToRegexSet,
1017    {
1018        let success_regexes = success_regexes
1019            .to_regex_set()
1020            .expect("regexes must be valid");
1021
1022        self.expect_line_matching_regexes(lines, success_regexes, stream_name, write_to_logs)
1023    }
1024
1025    /// Checks each line in `lines` against a regex set, and returns Ok if a line matches.
1026    ///
1027    /// [`Self::expect_line_matching_regexes`] wrapper for regular expression iterators.
1028    #[allow(clippy::unwrap_in_result)]
1029    pub fn expect_line_matching_regex_iter<L, I>(
1030        &mut self,
1031        lines: &mut L,
1032        success_regexes: I,
1033        stream_name: &str,
1034        write_to_logs: bool,
1035    ) -> Result<String>
1036    where
1037        L: Iterator<Item = std::io::Result<String>>,
1038        I: CollectRegexSet,
1039    {
1040        let success_regexes = success_regexes
1041            .collect_regex_set()
1042            .expect("regexes must be valid");
1043
1044        self.expect_line_matching_regexes(lines, success_regexes, stream_name, write_to_logs)
1045    }
1046
1047    /// Checks each line in `lines` against `success_regexes`, and returns Ok if a line
1048    /// matches. Uses `stream_name` as the name for `lines` in error reports.
1049    ///
1050    /// Kills the child on error, or after the configured timeout has elapsed.
1051    ///
1052    /// Note: the timeout is only checked after each full line is received from
1053    /// the child (#1140).
1054    #[instrument(skip(self, lines))]
1055    #[allow(clippy::unwrap_in_result)]
1056    pub fn expect_line_matching_regexes<L>(
1057        &mut self,
1058        lines: &mut L,
1059        success_regexes: RegexSet,
1060        stream_name: &str,
1061        write_to_logs: bool,
1062    ) -> Result<String>
1063    where
1064        L: Iterator<Item = std::io::Result<String>>,
1065    {
1066        // We don't check `is_running` here,
1067        // because we want to read to the end of the buffered output,
1068        // even if the child process has exited.
1069        while !self.past_deadline() {
1070            let line = if let Some(line) = lines.next() {
1071                line?
1072            } else {
1073                // When the child process closes its output,
1074                // and we've read all of the buffered output,
1075                // stop checking for any more output.
1076                break;
1077            };
1078
1079            if write_to_logs {
1080                // Since we're about to discard this line write it to stdout.
1081                Self::write_to_test_logs(&line, self.bypass_test_capture);
1082            }
1083
1084            if success_regexes.is_match(&line) {
1085                return Ok(line);
1086            }
1087        }
1088
1089        if self.is_running() {
1090            // If the process exits between is_running and kill, we will see
1091            // spurious errors here. So we want to ignore "no such process"
1092            // errors from kill.
1093            self.kill(true)?;
1094        }
1095
1096        let timeout = self
1097            .timeout
1098            .map(|timeout| humantime::format_duration(timeout).to_string())
1099            .unwrap_or_else(|| "unlimited".to_string());
1100
1101        let report = eyre!(
1102            "{stream_name} of command did not log any matches for the given regex,\n\
1103             within the {timeout} command timeout",
1104        )
1105        .with_section(|| format!("{:#?}", success_regexes.patterns()).header("Match Regex:"));
1106
1107        Err(report)
1108    }
1109
1110    /// Write `line` to stdout, so it can be seen in the test logs.
1111    ///
1112    /// Use [bypass_test_capture(true)](TestChild::bypass_test_capture) or
1113    /// `cargo test -- --nocapture` to see this output.
1114    ///
1115    /// May cause weird reordering for stdout / stderr.
1116    /// Uses stdout even if the original lines were from stderr.
1117    #[allow(clippy::print_stdout)]
1118    fn write_to_test_logs<S>(line: S, bypass_test_capture: bool)
1119    where
1120        S: AsRef<str>,
1121    {
1122        write_to_test_logs(line, bypass_test_capture);
1123    }
1124
1125    /// Kill `child`, wait for its output, and use that output as the context for
1126    /// an error report based on `error`.
1127    #[instrument(skip(self, result))]
1128    pub fn kill_on_error<V, E>(mut self, result: Result<V, E>) -> Result<(V, Self), Report>
1129    where
1130        E: Into<Report> + Send + Sync + 'static,
1131    {
1132        let mut error: Report = match result {
1133            Ok(success) => return Ok((success, self)),
1134            Err(error) => error.into(),
1135        };
1136
1137        if self.is_running() {
1138            let kill_res = self.kill(true);
1139            if let Err(kill_err) = kill_res {
1140                error = error.wrap_err(kill_err);
1141            }
1142        }
1143
1144        let output_res = self.wait_with_output();
1145        let error = match output_res {
1146            Err(output_err) => error.wrap_err(output_err),
1147            Ok(output) => error.context_from(&output),
1148        };
1149
1150        Err(error)
1151    }
1152
1153    fn past_deadline(&self) -> bool {
1154        self.deadline
1155            .map(|deadline| Instant::now() > deadline)
1156            .unwrap_or(false)
1157    }
1158
1159    /// Is this process currently running?
1160    ///
1161    /// ## Bugs
1162    ///
1163    /// On Windows and macOS, this function can return `true` for processes that
1164    /// have panicked. See #1781.
1165    ///
1166    /// ## Panics
1167    ///
1168    /// If the child process was already been taken using wait_with_output.
1169    pub fn is_running(&mut self) -> bool {
1170        matches!(
1171            self.child
1172                .as_mut()
1173                .expect("child has not been taken")
1174                .try_wait(),
1175            Ok(None),
1176        )
1177    }
1178}
1179
1180impl<T> AsRef<TestChild<T>> for TestChild<T> {
1181    fn as_ref(&self) -> &Self {
1182        self
1183    }
1184}
1185
1186impl<T> AsMut<TestChild<T>> for TestChild<T> {
1187    fn as_mut(&mut self) -> &mut Self {
1188        self
1189    }
1190}
1191
1192impl<T> Drop for TestChild<T> {
1193    fn drop(&mut self) {
1194        // Clean up child processes when the test finishes,
1195        // and check for failure logs.
1196        self.kill_and_consume_output(true)
1197            .expect("failure reading test process logs")
1198    }
1199}
1200
1201/// Test command output logs.
1202// TODO: split this struct into its own module
1203#[derive(Debug)]
1204pub struct TestOutput<T> {
1205    /// The test directory for this test output.
1206    ///
1207    /// Keeps the test dir around from `TestChild`,
1208    /// so it doesn't get deleted during `wait_with_output`.
1209    #[allow(dead_code)]
1210    pub dir: Option<T>,
1211
1212    /// The test command for this test output.
1213    pub cmd: String,
1214
1215    /// The test exit status, standard out, and standard error.
1216    pub output: Output,
1217}
1218
1219impl<T> TestOutput<T> {
1220    pub fn assert_success(self) -> Result<Self> {
1221        if !self.output.status.success() {
1222            Err(eyre!("command exited unsuccessfully")).context_from(&self)?;
1223        }
1224
1225        Ok(self)
1226    }
1227
1228    pub fn assert_failure(self) -> Result<Self> {
1229        if self.output.status.success() {
1230            Err(eyre!("command unexpectedly exited successfully")).context_from(&self)?;
1231        }
1232
1233        Ok(self)
1234    }
1235
1236    /// Checks the output of a command, using a closure to determine if the
1237    /// output is valid.
1238    ///
1239    /// If the closure returns `true`, the check returns `Ok(self)`.
1240    /// If the closure returns `false`, the check returns an error containing
1241    /// `output_name` and `err_msg`, with context from the command.
1242    ///
1243    /// `output` is typically `self.output.stdout` or `self.output.stderr`.
1244    #[instrument(skip(self, output_predicate, output))]
1245    pub fn output_check<P>(
1246        &self,
1247        output_predicate: P,
1248        output: &[u8],
1249        output_name: impl ToString + fmt::Debug,
1250        err_msg: impl ToString + fmt::Debug,
1251    ) -> Result<&Self>
1252    where
1253        P: FnOnce(&str) -> bool,
1254    {
1255        let output = String::from_utf8_lossy(output);
1256
1257        if output_predicate(&output) {
1258            Ok(self)
1259        } else {
1260            Err(eyre!(
1261                "{} of command did not {}",
1262                output_name.to_string(),
1263                err_msg.to_string()
1264            ))
1265            .context_from(self)
1266        }
1267    }
1268
1269    /// Checks each line in the output of a command, using a closure to determine
1270    /// if the line is valid.
1271    ///
1272    /// See [`Self::output_check`] for details.
1273    #[instrument(skip(self, line_predicate, output))]
1274    pub fn any_output_line<P>(
1275        &self,
1276        mut line_predicate: P,
1277        output: &[u8],
1278        output_name: impl ToString + fmt::Debug,
1279        err_msg: impl ToString + fmt::Debug,
1280    ) -> Result<&Self>
1281    where
1282        P: FnMut(&str) -> bool,
1283    {
1284        let output_predicate = |stdout: &str| {
1285            for line in stdout.lines() {
1286                if line_predicate(line) {
1287                    return true;
1288                }
1289            }
1290            false
1291        };
1292
1293        self.output_check(
1294            output_predicate,
1295            output,
1296            output_name,
1297            format!("have any lines that {}", err_msg.to_string()),
1298        )
1299    }
1300
1301    /// Tests if any lines in the output of a command contain `s`.
1302    ///
1303    /// See [`Self::any_output_line`] for details.
1304    #[instrument(skip(self, output))]
1305    pub fn any_output_line_contains(
1306        &self,
1307        s: &str,
1308        output: &[u8],
1309        output_name: impl ToString + fmt::Debug,
1310        err_msg: impl ToString + fmt::Debug,
1311    ) -> Result<&Self> {
1312        self.any_output_line(
1313            |line| line.contains(s),
1314            output,
1315            output_name,
1316            format!("contain {}", err_msg.to_string()),
1317        )
1318        .with_section(|| format!("{s:?}").header("Match String:"))
1319    }
1320
1321    /// Tests if standard output contains `s`.
1322    #[instrument(skip(self))]
1323    pub fn stdout_contains(&self, s: &str) -> Result<&Self> {
1324        self.output_check(
1325            |stdout| stdout.contains(s),
1326            &self.output.stdout,
1327            "stdout",
1328            "contain the given string",
1329        )
1330        .with_section(|| format!("{s:?}").header("Match String:"))
1331    }
1332
1333    /// Tests if standard output matches `regex`.
1334    #[instrument(skip(self))]
1335    #[allow(clippy::unwrap_in_result)]
1336    pub fn stdout_matches<R>(&self, regex: R) -> Result<&Self>
1337    where
1338        R: ToRegexSet + Debug,
1339    {
1340        let re = regex.to_regex_set().expect("regex must be valid");
1341
1342        self.output_check(
1343            |stdout| re.is_match(stdout),
1344            &self.output.stdout,
1345            "stdout",
1346            "matched the given regex",
1347        )
1348        .with_section(|| format!("{regex:?}").header("Match Regex:"))
1349    }
1350
1351    /// Tests if any lines in standard output contain `s`.
1352    #[instrument(skip(self))]
1353    pub fn stdout_line_contains(&self, s: &str) -> Result<&Self> {
1354        self.any_output_line_contains(s, &self.output.stdout, "stdout", "the given string")
1355    }
1356
1357    /// Tests if any lines in standard output match `regex`.
1358    #[instrument(skip(self))]
1359    #[allow(clippy::unwrap_in_result)]
1360    pub fn stdout_line_matches<R>(&self, regex: R) -> Result<&Self>
1361    where
1362        R: ToRegexSet + Debug,
1363    {
1364        let re = regex.to_regex_set().expect("regex must be valid");
1365
1366        self.any_output_line(
1367            |line| re.is_match(line),
1368            &self.output.stdout,
1369            "stdout",
1370            "matched the given regex",
1371        )
1372        .with_section(|| format!("{regex:?}").header("Line Match Regex:"))
1373    }
1374
1375    /// Tests if standard error contains `s`.
1376    #[instrument(skip(self))]
1377    pub fn stderr_contains(&self, s: &str) -> Result<&Self> {
1378        self.output_check(
1379            |stderr| stderr.contains(s),
1380            &self.output.stderr,
1381            "stderr",
1382            "contain the given string",
1383        )
1384        .with_section(|| format!("{s:?}").header("Match String:"))
1385    }
1386
1387    /// Tests if standard error matches `regex`.
1388    #[instrument(skip(self))]
1389    #[allow(clippy::unwrap_in_result)]
1390    pub fn stderr_matches<R>(&self, regex: R) -> Result<&Self>
1391    where
1392        R: ToRegexSet + Debug,
1393    {
1394        let re = regex.to_regex_set().expect("regex must be valid");
1395
1396        self.output_check(
1397            |stderr| re.is_match(stderr),
1398            &self.output.stderr,
1399            "stderr",
1400            "matched the given regex",
1401        )
1402        .with_section(|| format!("{regex:?}").header("Match Regex:"))
1403    }
1404
1405    /// Tests if any lines in standard error contain `s`.
1406    #[instrument(skip(self))]
1407    pub fn stderr_line_contains(&self, s: &str) -> Result<&Self> {
1408        self.any_output_line_contains(s, &self.output.stderr, "stderr", "the given string")
1409    }
1410
1411    /// Tests if any lines in standard error match `regex`.
1412    #[instrument(skip(self))]
1413    #[allow(clippy::unwrap_in_result)]
1414    pub fn stderr_line_matches<R>(&self, regex: R) -> Result<&Self>
1415    where
1416        R: ToRegexSet + Debug,
1417    {
1418        let re = regex.to_regex_set().expect("regex must be valid");
1419
1420        self.any_output_line(
1421            |line| re.is_match(line),
1422            &self.output.stderr,
1423            "stderr",
1424            "matched the given regex",
1425        )
1426        .with_section(|| format!("{regex:?}").header("Line Match Regex:"))
1427    }
1428
1429    /// Returns Ok if the program was killed, Err(Report) if exit was by another
1430    /// reason.
1431    pub fn assert_was_killed(&self) -> Result<()> {
1432        if self.was_killed() {
1433            Ok(())
1434        } else {
1435            Err(eyre!(
1436                "command exited without a kill, but the test expected kill exit"
1437            ))
1438            .context_from(self)?
1439        }
1440    }
1441
1442    /// Returns Ok if the program was not killed, Err(Report) if exit was by
1443    /// another reason.
1444    pub fn assert_was_not_killed(&self) -> Result<()> {
1445        if self.was_killed() {
1446            Err(eyre!(
1447                "command was killed, but the test expected an exit without a kill"
1448            ))
1449            .context_from(self)?
1450        } else {
1451            Ok(())
1452        }
1453    }
1454
1455    #[cfg(not(unix))]
1456    fn was_killed(&self) -> bool {
1457        self.output.status.code() == Some(1)
1458    }
1459
1460    #[cfg(unix)]
1461    fn was_killed(&self) -> bool {
1462        self.output.status.signal() == Some(9)
1463    }
1464
1465    /// Takes the generic `dir` parameter out of this `TestOutput`.
1466    pub fn take_dir(&mut self) -> Option<T> {
1467        self.dir.take()
1468    }
1469}
1470
1471/// Add context to an error report
1472// TODO: split this trait into its own module
1473pub trait ContextFrom<S> {
1474    type Return;
1475
1476    fn context_from(self, source: S) -> Self::Return;
1477}
1478
1479impl<C, T, E> ContextFrom<C> for Result<T, E>
1480where
1481    E: Into<Report>,
1482    Report: ContextFrom<C, Return = Report>,
1483{
1484    type Return = Result<T, Report>;
1485
1486    fn context_from(self, source: C) -> Self::Return {
1487        self.map_err(|e| e.into())
1488            .map_err(|report| report.context_from(source))
1489    }
1490}
1491
1492impl ContextFrom<&TestStatus> for Report {
1493    type Return = Report;
1494
1495    fn context_from(self, source: &TestStatus) -> Self::Return {
1496        let command = || source.cmd.clone().header("Command:");
1497
1498        self.with_section(command).context_from(&source.status)
1499    }
1500}
1501
1502impl<T> ContextFrom<&mut TestChild<T>> for Report {
1503    type Return = Report;
1504
1505    #[allow(clippy::print_stdout)]
1506    fn context_from(mut self, source: &mut TestChild<T>) -> Self::Return {
1507        self = self.section(source.cmd.clone().header("Command:"));
1508
1509        if let Some(child) = &mut source.child {
1510            if let Ok(Some(status)) = child.try_wait() {
1511                self = self.context_from(&status);
1512            }
1513        }
1514
1515        // Reading test child process output could hang if the child process is still running,
1516        // so kill it first.
1517        if let Some(child) = source.child.as_mut() {
1518            let _ = child.kill();
1519        }
1520
1521        let mut stdout_buf = String::new();
1522        let mut stderr_buf = String::new();
1523
1524        if let Some(stdout) = &mut source.stdout {
1525            for line in stdout {
1526                let line = line.unwrap_or_else(|error| {
1527                    format!("failure reading test process logs: {error:?}")
1528                });
1529                let _ = writeln!(&mut stdout_buf, "{line}");
1530            }
1531        } else if let Some(child) = &mut source.child {
1532            if let Some(stdout) = &mut child.stdout {
1533                let _ = stdout.read_to_string(&mut stdout_buf);
1534            }
1535        }
1536
1537        if let Some(stderr) = &mut source.stderr {
1538            for line in stderr {
1539                let line = line.unwrap_or_else(|error| {
1540                    format!("failure reading test process logs: {error:?}")
1541                });
1542                let _ = writeln!(&mut stderr_buf, "{line}");
1543            }
1544        } else if let Some(child) = &mut source.child {
1545            if let Some(stderr) = &mut child.stderr {
1546                let _ = stderr.read_to_string(&mut stderr_buf);
1547            }
1548        }
1549
1550        self.section(stdout_buf.header(format!("{} Unread Stdout:", source.command_path)))
1551            .section(stderr_buf.header(format!("{} Unread Stderr:", source.command_path)))
1552    }
1553}
1554
1555impl<T> ContextFrom<&TestOutput<T>> for Report {
1556    type Return = Report;
1557
1558    fn context_from(self, source: &TestOutput<T>) -> Self::Return {
1559        self.with_section(|| source.cmd.clone().header("Command:"))
1560            .context_from(&source.output)
1561    }
1562}
1563
1564impl ContextFrom<&Output> for Report {
1565    type Return = Report;
1566
1567    fn context_from(self, source: &Output) -> Self::Return {
1568        // TODO: add TestChild.command_path before Stdout and Stderr header names
1569        let stdout = || {
1570            String::from_utf8_lossy(&source.stdout)
1571                .into_owned()
1572                .header("Stdout:")
1573        };
1574        let stderr = || {
1575            String::from_utf8_lossy(&source.stderr)
1576                .into_owned()
1577                .header("Stderr:")
1578        };
1579
1580        self.context_from(&source.status)
1581            .with_section(stdout)
1582            .with_section(stderr)
1583    }
1584}
1585
1586impl ContextFrom<&ExitStatus> for Report {
1587    type Return = Report;
1588
1589    fn context_from(self, source: &ExitStatus) -> Self::Return {
1590        let how = if source.success() {
1591            "successfully"
1592        } else {
1593            "unsuccessfully"
1594        };
1595
1596        if let Some(code) = source.code() {
1597            return self.with_section(|| {
1598                format!("command exited {how} with status code {code}").header("Exit Status:")
1599            });
1600        }
1601
1602        #[cfg(unix)]
1603        if let Some(signal) = source.signal() {
1604            self.with_section(|| {
1605                format!("command terminated {how} by signal {signal}").header("Exit Status:")
1606            })
1607        } else {
1608            unreachable!("on unix all processes either terminate via signal or with an exit code");
1609        }
1610
1611        #[cfg(not(unix))]
1612        self.with_section(|| {
1613            format!("command exited {} without a status code or signal", how).header("Exit Status:")
1614        })
1615    }
1616}