update_kit/utils/
process.rs1use async_trait::async_trait;
2use tokio::process::Command;
3
4use crate::errors::UpdateKitError;
5
6#[derive(Debug, Clone)]
8pub struct CommandOutput {
9 pub exit_code: Option<i32>,
10 pub stdout: String,
11 pub stderr: String,
12}
13
14impl CommandOutput {
15 pub fn success(&self) -> bool {
17 self.exit_code == Some(0)
18 }
19}
20
21#[async_trait]
23pub trait CommandRunner: Send + Sync {
24 async fn run(&self, program: &str, args: &[&str]) -> Result<CommandOutput, UpdateKitError>;
25}
26
27#[derive(Debug, Default)]
29pub struct TokioCommandRunner;
30
31#[async_trait]
32impl CommandRunner for TokioCommandRunner {
33 async fn run(&self, program: &str, args: &[&str]) -> Result<CommandOutput, UpdateKitError> {
34 let output = Command::new(program)
35 .args(args)
36 .stdout(std::process::Stdio::piped())
37 .stderr(std::process::Stdio::piped())
38 .spawn()
39 .map_err(|e| {
40 UpdateKitError::CommandSpawnFailed(format!(
41 "failed to spawn '{program}': {e}"
42 ))
43 })?
44 .wait_with_output()
45 .await
46 .map_err(|e| {
47 UpdateKitError::CommandSpawnFailed(format!(
48 "failed to collect output from '{program}': {e}"
49 ))
50 })?;
51
52 Ok(CommandOutput {
53 exit_code: output.status.code(),
54 stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
55 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
56 })
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 #[test]
65 fn command_output_success_true() {
66 let output = CommandOutput {
67 exit_code: Some(0),
68 stdout: String::new(),
69 stderr: String::new(),
70 };
71 assert!(output.success());
72 }
73
74 #[test]
75 fn command_output_success_false_nonzero() {
76 let output = CommandOutput {
77 exit_code: Some(1),
78 stdout: String::new(),
79 stderr: String::new(),
80 };
81 assert!(!output.success());
82 }
83
84 #[test]
85 fn command_output_success_false_none() {
86 let output = CommandOutput {
87 exit_code: None,
88 stdout: String::new(),
89 stderr: String::new(),
90 };
91 assert!(!output.success());
92 }
93
94 #[tokio::test]
95 async fn tokio_runner_runs_echo() {
96 let runner = TokioCommandRunner;
97 let result = runner.run("echo", &["hello"]).await.unwrap();
98 assert!(result.success());
99 assert!(result.stdout.contains("hello"));
100 }
101}