1use std::collections::{BTreeMap, BTreeSet};
23use std::ffi::OsString;
24use std::io::{self, Read, Write};
25use std::path::{Path, PathBuf};
26use std::process::{Command, ExitStatus, Stdio};
27use std::sync::mpsc;
28use std::thread;
29use std::time::{Duration, Instant};
30
31use cabin_build::{BuildGraph, Dialect};
32use cabin_core::TargetKind;
33use cabin_workspace::{PackageGraph, WorkspacePackage};
34use thiserror::Error;
35
36#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct TestExecutable {
39 pub package: String,
42 pub target: String,
44 pub executable: PathBuf,
46 pub working_dir: PathBuf,
50 pub env: BTreeMap<String, OsString>,
57}
58
59#[derive(Debug, Clone, Default)]
66pub struct TestPlan {
67 executables: Vec<TestExecutable>,
68}
69
70impl<'a> IntoIterator for &'a TestPlan {
71 type Item = &'a TestExecutable;
72 type IntoIter = std::slice::Iter<'a, TestExecutable>;
73
74 fn into_iter(self) -> Self::IntoIter {
75 self.executables.iter()
76 }
77}
78
79impl TestPlan {
80 pub fn iter(&self) -> std::slice::Iter<'_, TestExecutable> {
82 self.executables.iter()
83 }
84
85 pub fn len(&self) -> usize {
87 self.executables.len()
88 }
89
90 pub fn is_empty(&self) -> bool {
92 self.executables.is_empty()
93 }
94
95 pub fn for_each_executable_mut(&mut self, mut f: impl FnMut(&mut TestExecutable)) {
100 for exe in &mut self.executables {
101 f(exe);
102 }
103 }
104}
105
106pub fn plan_tests(
126 package_graph: &PackageGraph,
127 build_graph: &BuildGraph,
128 selected_packages: Option<&[usize]>,
129) -> TestPlan {
130 let outputs: BTreeSet<&Path> = build_graph
133 .default_outputs
134 .iter()
135 .map(|p| p.as_std_path())
136 .collect();
137
138 let pkg_indices: Vec<usize> = match selected_packages {
139 Some(s) => s.to_vec(),
140 None => package_graph.primary_packages.clone(),
141 };
142
143 let mut entries: Vec<TestExecutable> = Vec::new();
144 for idx in pkg_indices {
145 let package = &package_graph.packages[idx];
146 for target in &package.package.targets {
147 if target.kind != TargetKind::Test {
148 continue;
149 }
150 let Some(exe) =
155 expected_executable(package, target.name.as_str(), build_graph.dialect, &outputs)
156 else {
157 continue;
158 };
159 entries.push(TestExecutable {
160 package: package.package.name.as_str().to_owned(),
161 target: target.name.as_str().to_owned(),
162 executable: exe.to_path_buf(),
163 working_dir: package.manifest_dir.clone(),
164 env: BTreeMap::new(),
165 });
166 }
167 }
168
169 entries.sort_by(|a, b| {
170 a.package
171 .cmp(&b.package)
172 .then_with(|| a.target.cmp(&b.target))
173 });
174 TestPlan {
175 executables: entries,
176 }
177}
178
179fn expected_executable<'a>(
180 package: &WorkspacePackage,
181 target_name: &str,
182 dialect: Dialect,
183 outputs: &BTreeSet<&'a Path>,
184) -> Option<&'a Path> {
185 let exe_name = dialect.executable_name(target_name);
195 let needle_tail: PathBuf = ["packages", package.package.name.as_str(), &exe_name]
196 .iter()
197 .collect();
198 outputs.iter().copied().find(|p| p.ends_with(&needle_tail))
199}
200
201#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct TestRunResult {
204 pub executable: TestExecutable,
206 pub status: TestRunStatus,
208 pub stdout: Vec<u8>,
210 pub stderr: Vec<u8>,
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq)]
216pub enum TestRunStatus {
217 Passed,
219 Failed { code: Option<i32> },
222}
223
224impl TestRunStatus {
225 pub const fn is_success(self) -> bool {
227 matches!(self, TestRunStatus::Passed)
228 }
229
230 fn from_status(status: ExitStatus) -> Self {
231 if status.success() {
232 Self::Passed
233 } else {
234 Self::Failed {
235 code: status.code(),
236 }
237 }
238 }
239}
240
241#[derive(Debug, Clone, PartialEq, Eq)]
243pub struct TestSummary {
244 pub results: Vec<TestRunResult>,
246 pub elapsed: Duration,
249}
250
251impl TestSummary {
252 pub fn total(&self) -> usize {
254 self.results.len()
255 }
256
257 pub fn passed(&self) -> usize {
259 self.results
260 .iter()
261 .filter(|r| r.status.is_success())
262 .count()
263 }
264
265 pub fn failed(&self) -> usize {
267 self.results
268 .iter()
269 .filter(|r| !r.status.is_success())
270 .count()
271 }
272
273 pub fn all_passed(&self) -> bool {
275 self.results.iter().all(|r| r.status.is_success())
276 }
277}
278
279pub trait TestOutputSink {
285 fn write_stdout(&mut self, executable: &TestExecutable, bytes: &[u8]) -> io::Result<()>;
291 fn write_stderr(&mut self, executable: &TestExecutable, bytes: &[u8]) -> io::Result<()>;
297
298 fn test_finished(&mut self, result: &TestRunResult) -> io::Result<()> {
308 let _ = result;
309 Ok(())
310 }
311}
312
313impl TestOutputSink for () {
314 fn write_stdout(&mut self, _executable: &TestExecutable, _bytes: &[u8]) -> io::Result<()> {
315 Ok(())
316 }
317 fn write_stderr(&mut self, _executable: &TestExecutable, _bytes: &[u8]) -> io::Result<()> {
318 Ok(())
319 }
320}
321
322pub fn null_sink() -> impl TestOutputSink {}
325
326pub struct StreamingSink<W1, W2> {
330 pub stdout: W1,
333 pub stderr: W2,
336}
337
338fn write_labeled<W: Write>(
342 writer: &mut W,
343 executable: &TestExecutable,
344 bytes: &[u8],
345 label: &str,
346) -> io::Result<()> {
347 if !bytes.is_empty() {
348 writeln!(
349 writer,
350 "---- {label}: {}:{} ----",
351 executable.package, executable.target
352 )?;
353 writer.write_all(bytes)?;
354 if !bytes.ends_with(b"\n") {
355 writer.write_all(b"\n")?;
356 }
357 }
358 Ok(())
359}
360
361impl<W1: Write, W2: Write> TestOutputSink for StreamingSink<W1, W2> {
362 fn write_stdout(&mut self, executable: &TestExecutable, bytes: &[u8]) -> io::Result<()> {
363 write_labeled(&mut self.stdout, executable, bytes, "stdout")
364 }
365 fn write_stderr(&mut self, executable: &TestExecutable, bytes: &[u8]) -> io::Result<()> {
366 write_labeled(&mut self.stderr, executable, bytes, "stderr")
367 }
368 fn test_finished(&mut self, result: &TestRunResult) -> io::Result<()> {
369 writeln!(self.stdout, "{}", render_result_line(result))
370 }
371}
372
373pub fn run_tests<S: TestOutputSink>(
397 plan: &TestPlan,
398 sink: &mut S,
399) -> Result<TestSummary, TestRunError> {
400 let started = Instant::now();
401 let mut results: Vec<TestRunResult> = Vec::with_capacity(plan.executables.len());
402 for executable in &plan.executables {
403 let mut command = Command::new(&executable.executable);
404 command.current_dir(&executable.working_dir);
405 command.stdout(Stdio::piped()).stderr(Stdio::piped());
406 for (key, value) in &executable.env {
412 command.env(key, value);
413 }
414 let mut child = retry_on_etxtbsy(SPAWN_RETRY_ATTEMPTS, SPAWN_RETRY_BASE_DELAY, || {
420 command.spawn()
421 })
422 .map_err(|source| TestRunError::Spawn {
423 package: executable.package.clone(),
424 target: executable.target.clone(),
425 executable: executable.executable.clone(),
426 source,
427 })?;
428
429 let stdout = child
430 .stdout
431 .take()
432 .expect("stdout is piped before child spawn");
433 let stderr = child
434 .stderr
435 .take()
436 .expect("stderr is piped before child spawn");
437 let (tx, rx) = mpsc::channel();
438 let stdout_thread = spawn_output_reader(OutputStream::Stdout, stdout, tx.clone());
439 let stderr_thread = spawn_output_reader(OutputStream::Stderr, stderr, tx);
440
441 let mut stdout = Vec::new();
442 let mut stderr = Vec::new();
443 let output_result = forward_output_events(executable, sink, rx, &mut stdout, &mut stderr);
444 if let Err(err) = output_result {
445 let _ = child.kill();
446 let _ = child.wait();
447 let _ = stdout_thread.join();
448 let _ = stderr_thread.join();
449 return Err(err);
450 }
451 let status = child.wait().map_err(|source| TestRunError::Wait {
452 package: executable.package.clone(),
453 target: executable.target.clone(),
454 executable: executable.executable.clone(),
455 source,
456 })?;
457 let _ = stdout_thread.join();
458 let _ = stderr_thread.join();
459
460 let result = TestRunResult {
461 executable: executable.clone(),
462 status: TestRunStatus::from_status(status),
463 stdout,
464 stderr,
465 };
466 sink.test_finished(&result).map_err(TestRunError::SinkIo)?;
467 results.push(result);
468 }
469 Ok(TestSummary {
470 results,
471 elapsed: started.elapsed(),
472 })
473}
474
475const SPAWN_RETRY_ATTEMPTS: u32 = 8;
477const SPAWN_RETRY_BASE_DELAY: Duration = Duration::from_millis(1);
480
481fn retry_on_etxtbsy<T>(
487 max_attempts: u32,
488 base_delay: Duration,
489 mut attempt: impl FnMut() -> io::Result<T>,
490) -> io::Result<T> {
491 let mut delay = base_delay;
492 let mut result = attempt();
493 for _ in 1..max_attempts {
494 match &result {
495 Err(err) if err.kind() == io::ErrorKind::ExecutableFileBusy => {}
496 _ => return result,
497 }
498 thread::sleep(delay);
499 delay = delay.saturating_mul(2);
500 result = attempt();
501 }
502 result
503}
504
505#[derive(Debug, Clone, Copy)]
506enum OutputStream {
507 Stdout,
508 Stderr,
509}
510
511struct OutputEvent {
512 stream: OutputStream,
513 bytes: Vec<u8>,
514}
515
516fn spawn_output_reader<R: Read + Send + 'static>(
517 stream: OutputStream,
518 mut reader: R,
519 tx: mpsc::Sender<Result<OutputEvent, io::Error>>,
520) -> thread::JoinHandle<()> {
521 thread::spawn(move || {
522 let mut buf = [0_u8; 8192];
523 loop {
524 match reader.read(&mut buf) {
525 Ok(0) => break,
526 Ok(n) => {
527 if tx
528 .send(Ok(OutputEvent {
529 stream,
530 bytes: buf[..n].to_vec(),
531 }))
532 .is_err()
533 {
534 break;
535 }
536 }
537 Err(source) => {
538 let _ = tx.send(Err(source));
539 break;
540 }
541 }
542 }
543 })
544}
545
546fn forward_output_events<S: TestOutputSink>(
547 executable: &TestExecutable,
548 sink: &mut S,
549 rx: mpsc::Receiver<Result<OutputEvent, io::Error>>,
550 stdout: &mut Vec<u8>,
551 stderr: &mut Vec<u8>,
552) -> Result<(), TestRunError> {
553 for event in rx {
554 let event = event.map_err(TestRunError::OutputIo)?;
555 match event.stream {
556 OutputStream::Stdout => {
557 sink.write_stdout(executable, &event.bytes)
558 .map_err(TestRunError::SinkIo)?;
559 stdout.extend_from_slice(&event.bytes);
560 }
561 OutputStream::Stderr => {
562 sink.write_stderr(executable, &event.bytes)
563 .map_err(TestRunError::SinkIo)?;
564 stderr.extend_from_slice(&event.bytes);
565 }
566 }
567 }
568 Ok(())
569}
570
571pub fn render_summary_line(summary: &TestSummary, filtered_out: usize) -> String {
582 let passed = summary.passed();
583 let failed = summary.failed();
584 let outcome = if failed == 0 { "ok" } else { "FAILED" };
585 let elapsed = summary.elapsed.as_secs_f64();
586 format!(
587 "test result: {outcome}. {passed} passed; {failed} failed; 0 ignored; 0 measured; {filtered_out} filtered out; finished in {elapsed:.2}s"
588 )
589}
590
591pub fn render_epilogue(summary: &TestSummary, filtered_out: usize) -> String {
597 use std::fmt::Write as _;
600
601 let mut out = String::new();
602 let failed: Vec<&TestRunResult> = summary
603 .results
604 .iter()
605 .filter(|r| !r.status.is_success())
606 .collect();
607 if !failed.is_empty() {
608 out.push_str("\nfailures:\n");
609 for result in failed {
610 let _ = writeln!(
611 out,
612 " {}:{}",
613 result.executable.package, result.executable.target
614 );
615 }
616 }
617 out.push('\n');
618 out.push_str(&render_summary_line(summary, filtered_out));
619 out
620}
621
622pub fn render_result_line(result: &TestRunResult) -> String {
625 let label = match result.status {
626 TestRunStatus::Passed => "ok".to_owned(),
627 TestRunStatus::Failed { code: Some(c) } => format!("FAILED (exit {c})"),
628 TestRunStatus::Failed { code: None } => "FAILED (terminated by signal)".to_owned(),
629 };
630 format!(
631 "test {}:{} ... {label}",
632 result.executable.package, result.executable.target
633 )
634}
635
636#[derive(Debug, Error)]
638pub enum TestRunError {
639 #[error("failed to start test target `{package}:{target}` ({}): {source}", .executable.display())]
641 Spawn {
642 package: String,
643 target: String,
644 executable: PathBuf,
645 #[source]
646 source: io::Error,
647 },
648 #[error("failed to wait for test target `{package}:{target}` ({}): {source}", .executable.display())]
651 Wait {
652 package: String,
653 target: String,
654 executable: PathBuf,
655 #[source]
656 source: io::Error,
657 },
658 #[error("failed to read captured test output: {0}")]
660 OutputIo(#[source] io::Error),
661 #[error("failed to write captured test output: {0}")]
665 SinkIo(#[source] io::Error),
666}
667
668#[cfg(test)]
669mod tests {
670 use super::*;
671 #[cfg(unix)]
673 use assert_fs::TempDir;
674 #[cfg(unix)]
675 use assert_fs::prelude::*;
676 #[cfg(unix)]
677 use std::os::unix::fs::PermissionsExt;
678
679 #[cfg(unix)]
687 fn write_executable(file: &assert_fs::fixture::ChildPath, body: &str) {
688 file.write_str(body).unwrap();
689 let mut perms = std::fs::metadata(file.path()).unwrap().permissions();
690 perms.set_mode(0o755);
691 std::fs::set_permissions(file.path(), perms).unwrap();
692 }
693
694 #[test]
695 fn plan_orders_executables_by_package_then_target() {
696 let plan = TestPlan {
697 executables: vec![
698 TestExecutable {
699 package: "alpha".into(),
700 target: "z_test".into(),
701 executable: PathBuf::from("/tmp/x"),
702 working_dir: PathBuf::from("/tmp"),
703 env: BTreeMap::new(),
704 },
705 TestExecutable {
706 package: "alpha".into(),
707 target: "a_test".into(),
708 executable: PathBuf::from("/tmp/x"),
709 working_dir: PathBuf::from("/tmp"),
710 env: BTreeMap::new(),
711 },
712 ],
713 };
714 let summary = TestSummary {
718 results: plan
719 .executables
720 .iter()
721 .map(|e| TestRunResult {
722 executable: e.clone(),
723 status: TestRunStatus::Passed,
724 stdout: Vec::new(),
725 stderr: Vec::new(),
726 })
727 .collect(),
728 elapsed: Duration::ZERO,
729 };
730 assert_eq!(summary.total(), 2);
731 assert_eq!(summary.passed(), 2);
732 assert!(summary.all_passed());
733 assert_eq!(
734 render_summary_line(&summary, 0),
735 "test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s"
736 );
737 }
738
739 fn result_for(target: &str, status: TestRunStatus) -> TestRunResult {
740 TestRunResult {
741 executable: TestExecutable {
742 package: "demo".into(),
743 target: target.into(),
744 executable: PathBuf::from("/tmp/x"),
745 working_dir: PathBuf::from("/tmp"),
746 env: BTreeMap::new(),
747 },
748 status,
749 stdout: Vec::new(),
750 stderr: Vec::new(),
751 }
752 }
753
754 #[test]
755 fn epilogue_is_blank_line_plus_summary_when_everything_passes() {
756 let summary = TestSummary {
757 results: vec![result_for("pass_test", TestRunStatus::Passed)],
758 elapsed: Duration::ZERO,
759 };
760 assert_eq!(
761 render_epilogue(&summary, 2),
762 "\ntest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s"
763 );
764 }
765
766 #[test]
767 fn epilogue_recaps_failed_test_names_before_the_summary() {
768 let summary = TestSummary {
769 results: vec![
770 result_for("fail_test", TestRunStatus::Failed { code: Some(1) }),
771 result_for("pass_test", TestRunStatus::Passed),
772 ],
773 elapsed: Duration::ZERO,
774 };
775 assert_eq!(
776 render_epilogue(&summary, 0),
777 "\nfailures:\n demo:fail_test\n\ntest result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s"
778 );
779 }
780
781 #[test]
782 #[cfg(unix)]
783 fn run_tests_reports_pass_and_fail_in_summary() {
784 struct RecordingSink {
785 finished: Vec<String>,
786 }
787 impl TestOutputSink for RecordingSink {
788 fn write_stdout(&mut self, _e: &TestExecutable, _b: &[u8]) -> io::Result<()> {
789 Ok(())
790 }
791 fn write_stderr(&mut self, _e: &TestExecutable, _b: &[u8]) -> io::Result<()> {
792 Ok(())
793 }
794 fn test_finished(&mut self, result: &TestRunResult) -> io::Result<()> {
795 self.finished.push(result.executable.target.clone());
796 Ok(())
797 }
798 }
799
800 let dir = TempDir::new().unwrap();
801 let pass = dir.child("pass_test");
802 let fail = dir.child("fail_test");
803 write_executable(&pass, "#!/bin/sh\nexit 0\n");
804 write_executable(&fail, "#!/bin/sh\nexit 1\n");
805 let plan = TestPlan {
806 executables: vec![
807 TestExecutable {
808 package: "demo".into(),
809 target: "fail_test".into(),
810 executable: fail.to_path_buf(),
811 working_dir: dir.path().to_path_buf(),
812 env: BTreeMap::new(),
813 },
814 TestExecutable {
815 package: "demo".into(),
816 target: "pass_test".into(),
817 executable: pass.to_path_buf(),
818 working_dir: dir.path().to_path_buf(),
819 env: BTreeMap::new(),
820 },
821 ],
822 };
823 let mut sink = RecordingSink {
824 finished: Vec::new(),
825 };
826 let summary = run_tests(&plan, &mut sink).unwrap();
827 assert_eq!(summary.total(), 2);
828 assert_eq!(summary.passed(), 1);
829 assert_eq!(summary.failed(), 1);
830 assert!(!summary.all_passed());
831 assert_eq!(summary.results[0].executable.target, "fail_test");
834 assert!(matches!(
835 summary.results[0].status,
836 TestRunStatus::Failed { code: Some(1) }
837 ));
838 assert_eq!(summary.results[1].executable.target, "pass_test");
839 assert!(summary.results[1].status.is_success());
840 assert_eq!(sink.finished, vec!["fail_test", "pass_test"]);
842 }
843
844 #[test]
845 #[cfg(unix)]
846 fn run_tests_forwards_output_before_process_exits() {
847 struct MarkerSink {
848 marker: PathBuf,
849 }
850
851 impl TestOutputSink for MarkerSink {
852 fn write_stdout(
853 &mut self,
854 _executable: &TestExecutable,
855 bytes: &[u8],
856 ) -> io::Result<()> {
857 if bytes
858 .windows("ready".len())
859 .any(|window| window == b"ready")
860 {
861 std::fs::write(&self.marker, b"seen")?;
862 }
863 Ok(())
864 }
865
866 fn write_stderr(
867 &mut self,
868 _executable: &TestExecutable,
869 _bytes: &[u8],
870 ) -> io::Result<()> {
871 Ok(())
872 }
873 }
874
875 let dir = TempDir::new().unwrap();
876 let marker = dir.child("sink-saw-output");
877 let script = dir.child("streaming_test");
878 write_executable(
879 &script,
880 r#"#!/bin/sh
881printf 'ready\n'
882i=0
883while [ "$i" -lt 40 ]; do
884 if [ -f "$MARKER" ]; then
885 exit 0
886 fi
887 i=$((i + 1))
888 sleep 0.05
889done
890exit 42
891"#,
892 );
893 let plan = TestPlan {
894 executables: vec![TestExecutable {
895 package: "demo".into(),
896 target: "streaming_test".into(),
897 executable: script.to_path_buf(),
898 working_dir: dir.path().to_path_buf(),
899 env: BTreeMap::from([("MARKER".to_owned(), marker.path().as_os_str().to_owned())]),
900 }],
901 };
902 let mut sink = MarkerSink {
903 marker: marker.to_path_buf(),
904 };
905 let summary = run_tests(&plan, &mut sink).unwrap();
906
907 assert!(summary.all_passed(), "{summary:?}");
908 assert_eq!(summary.results[0].stdout, b"ready\n");
909 }
910
911 #[test]
912 fn render_result_line_includes_exit_code_for_failures() {
913 let exe = TestExecutable {
914 package: "demo".into(),
915 target: "fail_test".into(),
916 executable: PathBuf::from("/tmp/x"),
917 working_dir: PathBuf::from("/tmp"),
918 env: BTreeMap::new(),
919 };
920 let result = TestRunResult {
921 executable: exe.clone(),
922 status: TestRunStatus::Failed { code: Some(42) },
923 stdout: Vec::new(),
924 stderr: Vec::new(),
925 };
926 assert_eq!(
927 render_result_line(&result),
928 "test demo:fail_test ... FAILED (exit 42)"
929 );
930 let result = TestRunResult {
931 executable: exe,
932 status: TestRunStatus::Passed,
933 stdout: Vec::new(),
934 stderr: Vec::new(),
935 };
936 assert_eq!(render_result_line(&result), "test demo:fail_test ... ok");
937 }
938
939 #[test]
940 fn streaming_sink_skips_empty_output() {
941 let mut sink = StreamingSink {
942 stdout: Vec::<u8>::new(),
943 stderr: Vec::<u8>::new(),
944 };
945 let exe = TestExecutable {
946 package: "demo".into(),
947 target: "x".into(),
948 executable: PathBuf::from("/tmp/x"),
949 working_dir: PathBuf::from("/tmp"),
950 env: BTreeMap::new(),
951 };
952 sink.write_stdout(&exe, &[]).unwrap();
953 sink.write_stderr(&exe, &[]).unwrap();
954 assert!(sink.stdout.is_empty());
955 assert!(sink.stderr.is_empty());
956 sink.write_stdout(&exe, b"hello").unwrap();
957 sink.test_finished(&TestRunResult {
958 executable: exe,
959 status: TestRunStatus::Passed,
960 stdout: Vec::new(),
961 stderr: Vec::new(),
962 })
963 .unwrap();
964 let out = String::from_utf8(sink.stdout).unwrap();
965 assert!(out.contains("---- stdout: demo:x ----"));
966 assert!(out.contains("hello"));
967 assert!(out.contains("test demo:x ... ok\n"));
968 assert!(out.ends_with('\n'));
969 }
970
971 #[test]
972 fn retry_on_etxtbsy_retries_until_spawn_succeeds() {
973 let mut calls = 0;
974 let result = retry_on_etxtbsy(8, Duration::ZERO, || {
975 calls += 1;
976 if calls < 3 {
977 Err(io::Error::from(io::ErrorKind::ExecutableFileBusy))
978 } else {
979 Ok(99)
980 }
981 });
982 assert_eq!(result.unwrap(), 99);
983 assert_eq!(calls, 3);
984 }
985
986 #[test]
987 fn retry_on_etxtbsy_gives_up_after_max_attempts() {
988 let mut calls = 0;
989 let result: io::Result<()> = retry_on_etxtbsy(4, Duration::ZERO, || {
990 calls += 1;
991 Err(io::Error::from(io::ErrorKind::ExecutableFileBusy))
992 });
993 assert_eq!(
994 result.unwrap_err().kind(),
995 io::ErrorKind::ExecutableFileBusy
996 );
997 assert_eq!(calls, 4);
998 }
999
1000 #[test]
1001 fn retry_on_etxtbsy_does_not_retry_other_errors() {
1002 let mut calls = 0;
1003 let result: io::Result<()> = retry_on_etxtbsy(8, Duration::ZERO, || {
1004 calls += 1;
1005 Err(io::Error::from(io::ErrorKind::PermissionDenied))
1006 });
1007 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::PermissionDenied);
1008 assert_eq!(calls, 1);
1009 }
1010}