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