1use 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
31pub trait IteratorDebug: Iterator + Debug {}
33
34impl<T> IteratorDebug for T where T: Iterator + Debug {}
35
36pub 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
44pub trait CommandExt {
48 fn status2(&mut self) -> Result<TestStatus, Report>;
51
52 fn output2(&mut self) -> Result<TestOutput<NoDir>, Report>;
55
56 fn spawn2<T>(&mut self, dir: T, command_path: impl ToString) -> Result<TestChild<T>, Report>;
59}
60
61impl CommandExt for Command {
62 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 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 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
119pub trait TestDirExt
125where
126 Self: AsRef<Path> + Sized,
127{
128 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#[derive(Debug)]
157pub struct TestStatus {
158 pub cmd: String,
160
161 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#[derive(Debug)]
186pub struct TestChild<T> {
187 pub dir: Option<T>,
192
193 pub cmd: String,
195
196 pub command_path: String,
198
199 pub child: Option<Child>,
204
205 pub stdout: Option<Box<dyn IteratorDebug<Item = std::io::Result<String>> + Send>>,
209
210 pub stderr: Option<Box<dyn IteratorDebug<Item = std::io::Result<String>> + Send>>,
212
213 failure_regexes: RegexSet,
220
221 ignore_regexes: RegexSet,
229
230 pub deadline: Option<Instant>,
234
235 pub timeout: Option<Duration>,
239
240 bypass_test_capture: bool,
243}
244
245pub 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 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 !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 failure_matches.is_empty() {
291 return Ok(line);
292 }
293
294 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#[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 #[allow(clippy::explicit_write)]
327 writeln!(std::io::stdout(), "{line}").unwrap();
328 } else {
329 println!("{line}");
332 }
333
334 let _ = std::io::stdout().lock().flush();
336}
337
338pub const NO_MATCHES_REGEX_ITER: &[&str] = &[];
342
343impl<T> TestChild<T> {
344 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 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 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 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 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 #[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 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 pub fn kill_and_consume_output(&mut self, ignore_exited: bool) -> Result<()> {
533 self.apply_failure_regexes_to_outputs();
534
535 let kill_result = self.kill(ignore_exited);
538
539 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 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 pub fn kill_and_return_output(&mut self, ignore_exited: bool) -> Result<String> {
577 self.apply_failure_regexes_to_outputs();
578
579 let kill_result = self.kill(ignore_exited);
582
583 let mut stdout_buf = String::new();
585 let mut stderr_buf = String::new();
586
587 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 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 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 #[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 #[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 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 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 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 pub fn bypass_test_capture(mut self, cond: bool) -> Self {
778 self.bypass_test_capture = cond;
779 self
780 }
781
782 #[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 self.stdout = Some(lines);
806 Ok(line)
807 }
808 Err(report) => {
809 self.stdout = Some(lines);
811 Err(report).context_from(self)
812 }
813 }
814 }
815
816 #[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 self.stderr = Some(lines);
838 Ok(line)
839 }
840 Err(report) => {
841 self.stderr = Some(lines);
843 Err(report).context_from(self)
844 }
845 }
846 }
847
848 #[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 #[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 #[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 self.stdout = Some(lines);
960 Ok(line)
961 }
962 Err(report) => {
963 self.stdout = Some(lines);
965 Err(report).context_from(self)
966 }
967 }
968 }
969
970 #[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 self.stderr = Some(lines);
992 Ok(line)
993 }
994 Err(report) => {
995 self.stderr = Some(lines);
997 Err(report).context_from(self)
998 }
999 }
1000 }
1001
1002 #[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 #[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 #[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 while !self.past_deadline() {
1070 let line = if let Some(line) = lines.next() {
1071 line?
1072 } else {
1073 break;
1077 };
1078
1079 if write_to_logs {
1080 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 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 #[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 #[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 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 self.kill_and_consume_output(true)
1197 .expect("failure reading test process logs")
1198 }
1199}
1200
1201#[derive(Debug)]
1204pub struct TestOutput<T> {
1205 #[allow(dead_code)]
1210 pub dir: Option<T>,
1211
1212 pub cmd: String,
1214
1215 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 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 pub fn take_dir(&mut self) -> Option<T> {
1467 self.dir.take()
1468 }
1469}
1470
1471pub 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 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 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}