1#![feature(exit_status_error)]
6
7use core::str::FromStr;
8use std::process::Command;
9use std::process::ExitStatusError;
10use std::time::Duration;
11use std::time::Instant;
12
13use rootcause::prelude::ResultExt;
14use rootcause::report;
15use ytil_sys::cli::Args;
16
17#[ytil_sys::main]
19fn main() -> rootcause::Result<()> {
20 let args = ytil_sys::cli::get();
21
22 if args.has_help() {
23 println!(include_str!("../help.txt"));
24 return Ok(());
25 }
26
27 let Some((cooldown_secs, args)) = args.split_first() else {
28 eprintln!("{}", include_str!("../help.txt"));
29 return Err(report!("missing cooldown arg")).attach_with(|| format!("args={args:#?}"));
30 };
31 let cooldown = Duration::from_secs(
32 cooldown_secs
33 .parse()
34 .context("invalid cooldown secs")
35 .attach_with(|| format!("value={cooldown_secs}"))?,
36 );
37
38 let Some((exit_cond, args)) = args.split_first() else {
39 eprintln!("{}", include_str!("../help.txt"));
40 return Err(report!("missing exit condition arg")).attach_with(|| format!("args={args:#?}"));
41 };
42 let exit_cond = ExitCond::from_str(exit_cond)
43 .context("invalid exit condition")
44 .attach_with(|| format!("args={args:#?}"))?;
45
46 let Some((program, program_args)) = args.split_first() else {
47 eprintln!("{}", include_str!("../help.txt"));
48 return Err(report!("missing command arg")).attach_with(|| format!("args={args:#?}"));
49 };
50
51 let mut tries = vec![];
52 loop {
53 let now = Instant::now();
54 let output = Command::new(program)
55 .args(program_args)
56 .output()
57 .context("error running cmd")
58 .attach_with(|| format!("program={program:?} args={program_args:?}"))?;
59 tries.push(now.elapsed());
60
61 let terminal_output = if output.status.success() {
62 output.stdout
63 } else {
64 output.stderr
65 };
66 println!("{}", String::from_utf8_lossy(&terminal_output));
67
68 if exit_cond.should_break(output.status.exit_ok()) {
69 break;
70 }
71 std::thread::sleep(cooldown);
72 }
73
74 let tries_count = u32::try_from(tries.len())
75 .context("cannot convert tries len to u32")
76 .attach_with(|| format!("len={}", tries.len()))?;
77 let total_time = tries.iter().fold(Duration::ZERO, |acc, &d| acc.saturating_add(d));
78 let avg_runs_time = if tries_count > 0 {
79 total_time.checked_div(tries_count).unwrap_or(Duration::ZERO)
80 } else {
81 Duration::ZERO
82 };
83 println!("Summary:\n - tries {tries_count}\n - avg time {avg_runs_time:#?}");
84
85 Ok(())
86}
87
88#[cfg_attr(test, derive(Debug))]
90enum ExitCond {
91 Ok,
93 Ko,
95}
96
97impl ExitCond {
98 pub const fn should_break(&self, cmd_res: Result<(), ExitStatusError>) -> bool {
100 matches!((self, cmd_res), (Self::Ok, Ok(())) | (Self::Ko, Err(_)))
101 }
102}
103
104impl FromStr for ExitCond {
106 type Err = rootcause::Report;
107
108 fn from_str(s: &str) -> Result<Self, Self::Err> {
109 Ok(match s {
110 "ok" => Self::Ok,
111 "ko" => Self::Ko,
112 unexpected => Err(report!("unexpected exit condition")).attach_with(|| format!("value={unexpected}"))?,
113 })
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use core::str::FromStr;
120
121 use test_that::prelude::*;
122
123 use super::*;
124
125 #[test]
126 fn test_exit_cond_from_str_when_ok_returns_ok_variant() {
127 assert_that!(ExitCond::from_str("ok"), ok(matches_pattern!(ExitCond::Ok)));
128 }
129
130 #[test]
131 fn test_exit_cond_from_str_when_ko_returns_ko_variant() {
132 assert_that!(ExitCond::from_str("ko"), ok(matches_pattern!(ExitCond::Ko)));
133 }
134
135 #[test]
136 fn test_exit_cond_from_str_when_invalid_returns_error() {
137 assert_that!(
138 (ExitCond::from_str("invalid")).map(|_| ()),
139 err(displays_as(contains_substring("unexpected exit condition")))
140 );
141 }
142
143 #[test]
144 fn test_should_break_ok_cond_with_success_result_returns_true() {
145 assert_that!(ExitCond::Ok.should_break(Ok(())), eq(true));
146 }
147
148 #[test]
149 fn test_should_break_ok_cond_with_failure_result_returns_false() {
150 let err_result: Result<(), ExitStatusError> = Command::new("false").status().unwrap().exit_ok();
151 assert_that!(ExitCond::Ok.should_break(err_result), eq(false));
152 }
153
154 #[test]
155 fn test_should_break_ko_cond_with_failure_result_returns_true() {
156 let err_result: Result<(), ExitStatusError> = Command::new("false").status().unwrap().exit_ok();
157 assert_that!(ExitCond::Ko.should_break(err_result), eq(true));
158 }
159
160 #[test]
161 fn test_should_break_ko_cond_with_success_result_returns_false() {
162 assert_that!(ExitCond::Ko.should_break(Ok(())), eq(false));
163 }
164}