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
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 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 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 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 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 #[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 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 pub fn kill_and_consume_output(&mut self, ignore_exited: bool) -> Result<()> {
525 self.apply_failure_regexes_to_outputs();
526
527 let kill_result = self.kill(ignore_exited);
530
531 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 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 pub fn kill_and_return_output(&mut self, ignore_exited: bool) -> Result<String> {
569 self.apply_failure_regexes_to_outputs();
570
571 let kill_result = self.kill(ignore_exited);
574
575 let mut stdout_buf = String::new();
577 let mut stderr_buf = String::new();
578
579 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 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 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 #[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 #[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 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 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 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 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 pub fn bypass_test_capture(mut self, cond: bool) -> Self {
781 self.bypass_test_capture = cond;
782 self
783 }
784
785 #[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 self.stdout = Some(lines);
809 Ok(line)
810 }
811 Err(report) => {
812 self.stdout = Some(lines);
814 Err(report).context_from(self)
815 }
816 }
817 }
818
819 #[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 self.stderr = Some(lines);
841 Ok(line)
842 }
843 Err(report) => {
844 self.stderr = Some(lines);
846 Err(report).context_from(self)
847 }
848 }
849 }
850
851 #[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 #[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 #[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 self.stdout = Some(lines);
963 Ok(line)
964 }
965 Err(report) => {
966 self.stdout = Some(lines);
968 Err(report).context_from(self)
969 }
970 }
971 }
972
973 #[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 self.stderr = Some(lines);
995 Ok(line)
996 }
997 Err(report) => {
998 self.stderr = Some(lines);
1000 Err(report).context_from(self)
1001 }
1002 }
1003 }
1004
1005 #[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 #[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 #[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 while !self.past_deadline() {
1073 let line = if let Some(line) = lines.next() {
1074 line?
1075 } else {
1076 break;
1080 };
1081
1082 if write_to_logs {
1083 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 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 #[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 #[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 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 self.kill_and_consume_output(true)
1200 .expect("failure reading test process logs")
1201 }
1202}
1203
1204#[derive(Debug)]
1207pub struct TestOutput<T> {
1208 #[allow(dead_code)]
1213 pub dir: Option<T>,
1214
1215 pub cmd: String,
1217
1218 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 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 pub fn take_dir(&mut self) -> Option<T> {
1470 self.dir.take()
1471 }
1472}
1473
1474pub 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 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 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}