lightweight_command_runner/
command_runner.rs

1// ---------------- [ File: lightweight-command-runner/src/command_runner.rs ]
2crate::ix!();
3
4pub trait CommandRunner: Send + Sync {
5
6    fn run_command(&self, cmd: tokio::process::Command) 
7        -> tokio::task::JoinHandle<Result<std::process::Output, io::Error>>;
8}
9
10pub struct DefaultCommandRunner;
11
12impl CommandRunner for DefaultCommandRunner {
13
14    fn run_command(&self, cmd: tokio::process::Command) 
15        -> tokio::task::JoinHandle<Result<std::process::Output, io::Error>> 
16    {
17        tokio::spawn(async move {
18            let mut cmd = cmd;
19            cmd.output().await
20        })
21    }
22}
23
24#[cfg(test)]
25mod test_command_runner {
26    use super::*;
27    
28    use tokio::process::Command;
29    use tokio::runtime::Runtime;
30
31    // We'll write multiple tests covering different scenarios:
32    // 1) A successful command (like `echo "Hello"`) that should return exit code 0.
33    // 2) A failing command (like `thisShouldNotExist`) that fails to launch or returns a non-zero exit code.
34    // 3) A test specifically for verifying stdout/stderr content, if feasible.
35    // 4) Tests for `make_exit_status` using Unix or Windows raw codes, ensuring they produce the correct exit status.
36
37    /// Creates a new `DefaultCommandRunner` for testing.
38    fn create_command_runner() -> DefaultCommandRunner {
39        DefaultCommandRunner
40    }
41
42    /// Test that a simple command completes successfully with exit code 0.
43    /// We'll attempt a cross-platform approach using "echo" to print something.
44    #[test]
45    fn test_run_command_successfully() {
46        let rt = Runtime::new().expect("Failed to create tokio runtime");
47        rt.block_on(async {
48            let runner = create_command_runner();
49
50            let cmd = if cfg!(windows) {
51                let mut c = Command::new("cmd");
52                c.arg("/C").arg("echo hello"); 
53                c
54            } else {
55                let mut c = Command::new("echo");
56                c.arg("hello");
57                c
58            };
59
60            let handle = runner.run_command(cmd);
61            let output_result = handle.await.expect("JoinHandle panicked");
62            assert!(
63                output_result.is_ok(),
64                "Expected successful result from echo command"
65            );
66            let output = output_result.unwrap();
67            assert!(
68                output.status.success(),
69                "Expected exit code 0 from echo command"
70            );
71        });
72    }
73
74    /// Test that running a non-existent command produces an error, or at least a non-zero exit code.
75    #[test]
76    fn test_run_command_non_existent() {
77        let rt = Runtime::new().expect("Failed to create tokio runtime");
78        rt.block_on(async {
79            let runner = create_command_runner();
80
81            // We'll try a made-up command name that hopefully doesn't exist
82            let cmd = if cfg!(windows) {
83                // Windows might say "not recognized as an internal or external command"
84                Command::new("thisCommandDefinitelyShouldNotExistOnWindows")
85            } else {
86                // Linux or Mac will typically say "No such file or directory"
87                Command::new("thisCommandDefinitelyShouldNotExistOnUnix")
88            };
89
90            let handle = runner.run_command(cmd);
91            let output_result = handle.await.expect("JoinHandle panicked");
92            assert!(output_result.is_err() || !output_result.as_ref().unwrap().status.success(),
93                "Expected an error or a failing exit code for non-existent command"
94            );
95        });
96    }
97
98    /// Test that we can capture stdout from a command that writes to stdout.
99    #[test]
100    fn test_run_command_stdout_capture() {
101        let rt = Runtime::new().expect("Failed to create tokio runtime");
102        rt.block_on(async {
103            let runner = create_command_runner();
104
105            let cmd = if cfg!(windows) {
106                let mut c = Command::new("cmd");
107                c.arg("/C").arg("echo capture_this_stdout");
108                c
109            } else {
110                let mut c = Command::new("echo");
111                c.arg("capture_this_stdout");
112                c
113            };
114
115            let handle = runner.run_command(cmd);
116            let output_result = handle.await.expect("JoinHandle panicked");
117            let output = match output_result {
118                Ok(o) => o,
119                Err(e) => panic!("Failed to run echo command: {e}"),
120            };
121            assert!(
122                output.status.success(),
123                "Expected exit code 0 from echo command"
124            );
125
126            // Convert stdout to string and see if it contains "capture_this_stdout"
127            let stdout_str = String::from_utf8_lossy(&output.stdout);
128            assert!(
129                stdout_str.contains("capture_this_stdout"),
130                "Expected stdout to contain 'capture_this_stdout', got: {stdout_str}"
131            );
132        });
133    }
134
135    /// Test that we can capture stderr from a command that writes to stderr.
136    /// We'll intentionally run something that fails, so it prints to stderr.
137    #[test]
138    fn test_run_command_stderr_capture() {
139        let rt = Runtime::new().expect("Failed to create tokio runtime");
140        rt.block_on(async {
141            let runner = create_command_runner();
142
143            // On Unix, `ls` a non-existent file typically prints to stderr.
144            // On Windows, `dir` of a non-existent file also prints to stderr.
145            let cmd = if cfg!(windows) {
146                let mut c = Command::new("cmd");
147                c.arg("/C").arg("dir thisDirectoryDoesNotExist");
148                c
149            } else {
150                let mut c = Command::new("ls");
151                c.arg("thisDirectoryDoesNotExist");
152                c
153            };
154
155            let handle = runner.run_command(cmd);
156            let output_result = handle.await.expect("JoinHandle panicked");
157            let output = match output_result {
158                Ok(o) => o,
159                Err(e) => panic!("Failed to run 'ls/dir' command: {e}"),
160            };
161
162            assert!(
163                !output.status.success(),
164                "Expected a non-zero exit code for listing a non-existent directory"
165            );
166            let stderr_str = String::from_utf8_lossy(&output.stderr);
167            assert!(
168                !stderr_str.is_empty(),
169                "Expected non-empty stderr for listing a non-existent directory"
170            );
171        });
172    }
173
174    /// Test that we can construct and interpret exit status codes for Unix systems.
175    #[cfg(unix)]
176    #[test]
177    fn test_make_exit_status_unix() {
178        // raw=0 => success
179        let status_success = make_exit_status(0);
180        assert!(status_success.success());
181
182        // raw=256 => means "exit code 1" on many Linux/BSD systems
183        let status_error = make_exit_status(256);
184        assert!(!status_error.success());
185        assert_eq!(status_error.code(), Some(1), "Expected exit code 1");
186    }
187}