cli_testing_specialist/utils/
validator.rs1use crate::error::{CliTestError, Result};
2use std::path::{Path, PathBuf};
3use std::process::{Command, Stdio};
4use std::time::Duration;
5
6pub fn validate_binary_path(path: &Path) -> Result<PathBuf> {
13 if !path.exists() {
15 return Err(CliTestError::BinaryNotFound(path.to_path_buf()));
16 }
17
18 if !path.is_file() {
20 return Err(CliTestError::BinaryNotFound(path.to_path_buf()));
21 }
22
23 #[cfg(unix)]
25 {
26 use std::os::unix::fs::PermissionsExt;
27 let metadata = path.metadata()?;
28 let permissions = metadata.permissions();
29
30 if permissions.mode() & 0o111 == 0 {
32 return Err(CliTestError::BinaryNotExecutable(path.to_path_buf()));
33 }
34 }
35
36 let canonical = path.canonicalize()?;
38
39 Ok(canonical)
40}
41
42pub fn execute_with_timeout(binary: &Path, args: &[&str], timeout: Duration) -> Result<String> {
49 use std::io::Read;
50
51 log::debug!(
52 "Executing: {} {} (timeout: {:?})",
53 binary.display(),
54 args.join(" "),
55 timeout
56 );
57
58 let mut child = Command::new(binary)
60 .args(args)
61 .stdout(Stdio::piped())
62 .stderr(Stdio::piped())
63 .spawn()?;
64
65 let start = std::time::Instant::now();
67
68 loop {
69 match child.try_wait()? {
71 Some(_status) => {
72 let mut stdout = String::new();
74 if let Some(mut pipe) = child.stdout.take() {
75 pipe.read_to_string(&mut stdout)?;
76 }
77
78 let mut stderr = String::new();
80 if let Some(mut pipe) = child.stderr.take() {
81 pipe.read_to_string(&mut stderr)?;
82 }
83
84 let output = if !stdout.is_empty() { stdout } else { stderr };
86
87 log::debug!("Execution completed in {:?}", start.elapsed());
88 return Ok(output);
89 }
90 None => {
91 if start.elapsed() >= timeout {
93 log::warn!("Execution timeout exceeded, killing process");
95 child.kill()?;
96 child.wait()?;
97
98 return Err(CliTestError::ExecutionFailed(format!(
99 "Timeout after {:?}",
100 timeout
101 )));
102 }
103
104 std::thread::sleep(Duration::from_millis(50));
106 }
107 }
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use std::fs::File;
115 use tempfile::TempDir;
116
117 #[test]
118 fn test_validate_nonexistent_binary() {
119 let path = Path::new("/nonexistent/binary");
120 let result = validate_binary_path(path);
121
122 assert!(result.is_err());
123 assert!(matches!(
124 result.unwrap_err(),
125 CliTestError::BinaryNotFound(_)
126 ));
127 }
128
129 #[test]
130 fn test_validate_directory() {
131 let temp_dir = TempDir::new().unwrap();
132 let result = validate_binary_path(temp_dir.path());
133
134 assert!(result.is_err());
135 assert!(matches!(
136 result.unwrap_err(),
137 CliTestError::BinaryNotFound(_)
138 ));
139 }
140
141 #[cfg(unix)]
142 #[test]
143 fn test_validate_non_executable_file() {
144 use std::os::unix::fs::PermissionsExt;
145
146 let temp_dir = TempDir::new().unwrap();
147 let file_path = temp_dir.path().join("non_executable");
148
149 File::create(&file_path).unwrap();
151 let mut perms = std::fs::metadata(&file_path).unwrap().permissions();
152 perms.set_mode(0o644); std::fs::set_permissions(&file_path, perms).unwrap();
154
155 let result = validate_binary_path(&file_path);
156
157 assert!(result.is_err());
158 assert!(matches!(
159 result.unwrap_err(),
160 CliTestError::BinaryNotExecutable(_)
161 ));
162 }
163
164 #[test]
165 fn test_execute_with_timeout_echo() {
166 #[cfg(unix)]
168 {
169 let echo_path = Path::new("/bin/echo");
170 if echo_path.exists() {
171 let result =
172 execute_with_timeout(echo_path, &["hello", "world"], Duration::from_secs(5));
173
174 assert!(result.is_ok());
175 let output = result.unwrap();
176 assert!(output.contains("hello"));
177 }
178 }
179 }
180
181 #[test]
182 fn test_execute_with_timeout_sleep() {
183 #[cfg(unix)]
185 {
186 let sleep_path = Path::new("/bin/sleep");
187 if sleep_path.exists() {
188 let result = execute_with_timeout(
189 sleep_path,
190 &["10"], Duration::from_millis(500), );
193
194 assert!(result.is_err());
195 if let Err(CliTestError::ExecutionFailed(msg)) = result {
196 assert!(msg.contains("Timeout"));
197 }
198 }
199 }
200 }
201
202 #[test]
203 fn test_canonicalization() {
204 #[cfg(unix)]
206 {
207 let ls_path = Path::new("/bin/ls");
208 if ls_path.exists() {
209 let result = validate_binary_path(ls_path);
210 assert!(result.is_ok());
211
212 let canonical = result.unwrap();
213 assert!(canonical.is_absolute());
214 }
215 }
216 }
217}