Skip to content

Commit 24dd889

Browse files
feat: add stop-on-exit-code option
This change adds the `stop-on-exit-code` optional argument. When supplied `retry` will exit "success" if the command passed to `retry` exits with that status code.
1 parent 5914f95 commit 24dd889

File tree

2 files changed

+68
-15
lines changed

2 files changed

+68
-15
lines changed

src/cli.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,20 @@ pub(crate) struct Cli {
8282
#[arg(long, value_parser = duration_from_str, help = "Timeout to enforce on each attempt")]
8383
pub task_timeout: Option<Duration>,
8484

85+
/// Exit code to stop on.
86+
///
87+
/// Any exit code that matches this value will stop retrying.
88+
///
89+
/// Accepted format is:
90+
/// [0-9]+
91+
///
92+
/// Examples:
93+
/// ```
94+
/// retry --stop-on-exit-code 2 -- bash -c "exit 2"
95+
/// ```
96+
#[arg(long, help = "Exit code to stop on")]
97+
pub stop_on_exit_code: Option<i32>,
98+
8599
// TODO: enforce this is a non-empty array
86100
/// The command to run.
87101
///

src/main.rs

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ async fn eval(command: &[String]) -> Result<ExitStatus> {
3636
Ok(child.wait().await?)
3737
}
3838

39-
async fn run_task(command: &[String], task_timeout: Option<Duration>) -> Result<TaskOutcome> {
39+
async fn run_task(
40+
command: &[String],
41+
task_timeout: Option<Duration>,
42+
stop_on_exit_code: Option<i32>,
43+
) -> Result<TaskOutcome> {
4044
let status_code = match task_timeout {
4145
None => eval(command).await?.code(),
4246
Some(task_timeout) => {
@@ -58,15 +62,25 @@ async fn run_task(command: &[String], task_timeout: Option<Duration>) -> Result<
5862
if status_code == 0 {
5963
return Ok(TaskOutcome::Success);
6064
}
65+
66+
if let Some(exit_on_status_code) = stop_on_exit_code {
67+
if status_code == exit_on_status_code {
68+
return Ok(TaskOutcome::Success);
69+
}
70+
}
6171
}
6272

6373
Ok(TaskOutcome::Timeout)
6474
}
6575

66-
async fn loop_task(command: &[String], task_timeout: Option<Duration>) -> Result<TaskOutcome> {
76+
async fn loop_task(
77+
command: &[String],
78+
task_timeout: Option<Duration>,
79+
stop_on_exit_code: Option<i32>,
80+
) -> Result<TaskOutcome> {
6781
loop {
68-
let status_code = run_task(command, task_timeout).await?;
69-
if status_code == TaskOutcome::Success {
82+
let task_outcome = run_task(command, task_timeout, stop_on_exit_code).await?;
83+
if task_outcome == TaskOutcome::Success {
7084
return Ok(TaskOutcome::Success);
7185
}
7286
}
@@ -76,18 +90,22 @@ async fn run_tasks(
7690
command: Vec<String>,
7791
up_to: Retry,
7892
task_timeout: Option<Duration>,
93+
stop_on_exit_code: Option<i32>,
7994
) -> Result<()> {
8095
match up_to {
8196
Retry::ForDuration(duration) => {
82-
let task_outcome =
83-
tokio::time::timeout(duration, loop_task(&command, task_timeout)).await;
97+
let task_outcome = tokio::time::timeout(
98+
duration,
99+
loop_task(&command, task_timeout, stop_on_exit_code),
100+
)
101+
.await;
84102
if let Ok(Ok(TaskOutcome::Success)) = task_outcome {
85103
return Ok(());
86104
}
87105
}
88106
Retry::NumberOfTimes(num_times) => {
89107
for _ in 0..num_times {
90-
let task_outcome = run_task(&command, task_timeout).await?;
108+
let task_outcome = run_task(&command, task_timeout, stop_on_exit_code).await?;
91109
if task_outcome == TaskOutcome::Success {
92110
return Ok(());
93111
}
@@ -101,7 +119,13 @@ async fn run_tasks(
101119
#[tokio::main]
102120
async fn main() -> Result<()> {
103121
let args: Cli = Cli::parse();
104-
run_tasks(args.command, args.up_to, args.task_timeout).await
122+
run_tasks(
123+
args.command,
124+
args.up_to,
125+
args.task_timeout,
126+
args.stop_on_exit_code,
127+
)
128+
.await
105129
}
106130
#[cfg(test)]
107131
mod tests {
@@ -124,30 +148,30 @@ mod tests {
124148
#[tokio::test]
125149
async fn test_run_task_success() {
126150
let command = vec!["true".to_owned()];
127-
let task_outcome = run_task(&command, None).await.unwrap();
151+
let task_outcome = run_task(&command, None, None).await.unwrap();
128152
assert_eq!(task_outcome, TaskOutcome::Success);
129153
}
130154

131155
#[tokio::test]
132156
async fn test_run_task_failure() {
133157
let command = vec!["false".to_owned()];
134-
let task_outcome = run_task(&command, None).await.unwrap();
158+
let task_outcome = run_task(&command, None, None).await.unwrap();
135159
assert_eq!(task_outcome, TaskOutcome::Timeout);
136160
}
137161

138162
#[tokio::test]
139163
async fn test_run_task_timeout() {
140164
let command = vec!["sleep".to_owned(), "10".to_owned()];
141165
let task_timeout = Some(Duration::from_secs(1));
142-
let task_outcome = run_task(&command, task_timeout).await.unwrap();
166+
let task_outcome = run_task(&command, task_timeout, None).await.unwrap();
143167
assert_eq!(task_outcome, TaskOutcome::Timeout);
144168
}
145169

146170
#[tokio::test]
147171
async fn test_loop_task_success() {
148172
let command = vec!["true".to_owned()];
149173
let task_timeout = Some(Duration::from_secs(5));
150-
let task_outcome = loop_task(&command, task_timeout).await.unwrap();
174+
let task_outcome = loop_task(&command, task_timeout, None).await.unwrap();
151175
assert_eq!(task_outcome, TaskOutcome::Success);
152176
}
153177

@@ -156,7 +180,7 @@ mod tests {
156180
let command = vec!["true".to_owned()];
157181
let up_to = Retry::NumberOfTimes(3);
158182
let task_timeout = Some(Duration::from_secs(5));
159-
let result = run_tasks(command, up_to, task_timeout).await;
183+
let result = run_tasks(command, up_to, task_timeout, None).await;
160184
assert_eq!(result.is_ok(), true);
161185
}
162186

@@ -165,7 +189,7 @@ mod tests {
165189
let command = vec!["false".to_owned()];
166190
let up_to = Retry::NumberOfTimes(3);
167191
let task_timeout = Some(Duration::from_secs(5));
168-
let result = run_tasks(command, up_to, task_timeout).await;
192+
let result = run_tasks(command, up_to, task_timeout, None).await;
169193
assert_eq!(result.is_err(), true);
170194
}
171195

@@ -174,7 +198,22 @@ mod tests {
174198
let command = vec!["sleep".to_owned(), "10".to_owned()];
175199
let up_to = Retry::ForDuration(Duration::from_secs(5));
176200
let task_timeout = Some(Duration::from_secs(1));
177-
let result = run_tasks(command, up_to, task_timeout).await;
201+
let result = run_tasks(command, up_to, task_timeout, None).await;
178202
assert_eq!(result.is_err(), true);
179203
}
204+
205+
#[tokio::test]
206+
async fn test_run_tasks_stop_on_exit_code() {
207+
let command = vec![
208+
"bash".to_string(),
209+
"-c".to_string(),
210+
"\"exit\"".to_string(),
211+
"\"2\"".to_string(),
212+
];
213+
let up_to = Retry::NumberOfTimes(5);
214+
let task_timeout = Some(Duration::from_secs(5));
215+
let stop_on_exit_code = Some(2);
216+
let result = run_tasks(command, up_to, task_timeout, stop_on_exit_code).await;
217+
assert_eq!(result.is_ok(), true);
218+
}
180219
}

0 commit comments

Comments
 (0)