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> {
25 if !path.exists() {
27 return Err(CliTestError::BinaryNotFound(path.to_path_buf()));
28 }
29
30 if !path.is_file() {
32 return Err(CliTestError::BinaryNotFound(path.to_path_buf()));
33 }
34
35 #[cfg(unix)]
37 {
38 use std::os::unix::fs::PermissionsExt;
39 let metadata = path.metadata()?;
40 let permissions = metadata.permissions();
41
42 if permissions.mode() & 0o111 == 0 {
44 return Err(CliTestError::BinaryNotExecutable(path.to_path_buf()));
45 }
46 }
47
48 let canonical = path.canonicalize()?;
50
51 Ok(canonical)
52}
53
54pub fn execute_with_timeout(binary: &Path, args: &[&str], timeout: Duration) -> Result<String> {
79 execute_with_timeout_and_limits(
80 binary,
81 args,
82 timeout,
83 Some(&crate::utils::ResourceLimits::default()),
84 )
85}
86
87pub fn execute_with_timeout_and_limits(
92 binary: &Path,
93 args: &[&str],
94 timeout: Duration,
95 limits: Option<&crate::utils::ResourceLimits>,
96) -> Result<String> {
97 use std::io::Read;
98
99 log::debug!(
100 "Executing: {} {} (timeout: {:?})",
101 binary.display(),
102 args.join(" "),
103 timeout
104 );
105
106 let mut command = Command::new(binary);
108 command
109 .args(args)
110 .stdout(Stdio::piped())
111 .stderr(Stdio::piped());
112
113 #[cfg(unix)]
115 if let Some(resource_limits) = limits {
116 use std::os::unix::process::CommandExt;
117
118 let max_memory = resource_limits.max_memory_bytes;
120 let max_fds = resource_limits.max_file_descriptors;
121 let max_procs = resource_limits.max_processes;
122
123 unsafe {
124 command.pre_exec(move || {
125 use libc::{getrlimit, rlimit, setrlimit, RLIMIT_AS, RLIMIT_NOFILE, RLIMIT_NPROC};
126
127 let mut current_limit = rlimit {
129 rlim_cur: 0,
130 rlim_max: 0,
131 };
132
133 if getrlimit(RLIMIT_AS, &mut current_limit) == 0 {
135 if current_limit.rlim_max == libc::RLIM_INFINITY
137 || current_limit.rlim_max > max_memory
138 {
139 let mem_limit = rlimit {
140 rlim_cur: max_memory,
141 rlim_max: max_memory,
142 };
143 let _ = setrlimit(RLIMIT_AS, &mem_limit);
145 }
146 }
147
148 if getrlimit(RLIMIT_NOFILE, &mut current_limit) == 0
150 && (current_limit.rlim_max == libc::RLIM_INFINITY
151 || current_limit.rlim_max > max_fds)
152 {
153 let fd_limit = rlimit {
154 rlim_cur: max_fds,
155 rlim_max: max_fds,
156 };
157 let _ = setrlimit(RLIMIT_NOFILE, &fd_limit);
158 }
159
160 if getrlimit(RLIMIT_NPROC, &mut current_limit) == 0
162 && (current_limit.rlim_max == libc::RLIM_INFINITY
163 || current_limit.rlim_max > max_procs)
164 {
165 let proc_limit = rlimit {
166 rlim_cur: max_procs,
167 rlim_max: max_procs,
168 };
169 let _ = setrlimit(RLIMIT_NPROC, &proc_limit);
170 }
171
172 Ok(())
173 });
174 }
175 }
176
177 let mut child = command.spawn()?;
179
180 #[cfg(windows)]
182 if let Some(resource_limits) = limits {
183 apply_windows_job_limits(&child, resource_limits)?;
184 }
185
186 let start = std::time::Instant::now();
188
189 loop {
190 match child.try_wait()? {
192 Some(_status) => {
193 let mut stdout = String::new();
195 if let Some(mut pipe) = child.stdout.take() {
196 pipe.read_to_string(&mut stdout)?;
197 }
198
199 let mut stderr = String::new();
201 if let Some(mut pipe) = child.stderr.take() {
202 pipe.read_to_string(&mut stderr)?;
203 }
204
205 let output = if !stdout.is_empty() { stdout } else { stderr };
207
208 log::debug!("Execution completed in {:?}", start.elapsed());
209 return Ok(output);
210 }
211 None => {
212 if start.elapsed() >= timeout {
214 log::warn!("Execution timeout exceeded, killing process");
216 child.kill()?;
217 child.wait()?;
218
219 return Err(CliTestError::ExecutionFailed(format!(
220 "Timeout after {:?}",
221 timeout
222 )));
223 }
224
225 std::thread::sleep(Duration::from_millis(50));
227 }
228 }
229 }
230}
231
232#[cfg(windows)]
234fn apply_windows_job_limits(
235 child: &std::process::Child,
236 limits: &crate::utils::ResourceLimits,
237) -> Result<()> {
238 use std::os::windows::process::CommandExt;
239 use windows::Win32::Foundation::{CloseHandle, HANDLE};
240 use windows::Win32::System::JobObjects::{
241 AssignProcessToJobObject, CreateJobObjectW, JobObjectExtendedLimitInformation,
242 SetInformationJobObject, JOBOBJECT_BASIC_LIMIT_INFORMATION,
243 JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_ACTIVE_PROCESS,
244 JOB_OBJECT_LIMIT_JOB_MEMORY, JOB_OBJECT_LIMIT_PROCESS_MEMORY,
245 };
246
247 unsafe {
248 let job = CreateJobObjectW(None, None).map_err(|e| {
250 CliTestError::ExecutionFailed(format!("Failed to create job object: {}", e))
251 })?;
252
253 let mut job_limits = JOBOBJECT_EXTENDED_LIMIT_INFORMATION {
255 BasicLimitInformation: JOBOBJECT_BASIC_LIMIT_INFORMATION {
256 LimitFlags: JOB_OBJECT_LIMIT_ACTIVE_PROCESS
257 | JOB_OBJECT_LIMIT_PROCESS_MEMORY
258 | JOB_OBJECT_LIMIT_JOB_MEMORY,
259 ActiveProcessLimit: limits.max_processes as u32,
260 ..Default::default()
261 },
262 ProcessMemoryLimit: limits.max_memory_bytes as usize,
263 JobMemoryLimit: limits.max_memory_bytes as usize,
264 ..Default::default()
265 };
266
267 SetInformationJobObject(
269 job,
270 JobObjectExtendedLimitInformation,
271 &mut job_limits as *mut _ as *mut _,
272 std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
273 )
274 .map_err(|e| {
275 CloseHandle(job);
276 CliTestError::ExecutionFailed(format!("Failed to set job limits: {}", e))
277 })?;
278
279 let child_handle = HANDLE(child.id() as isize);
281 AssignProcessToJobObject(job, child_handle).map_err(|e| {
282 CloseHandle(job);
283 CliTestError::ExecutionFailed(format!("Failed to assign process to job: {}", e))
284 })?;
285
286 log::debug!("Resource limits applied to child process via Job Object");
289 }
290
291 Ok(())
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297 use std::fs::File;
298 use tempfile::TempDir;
299
300 #[test]
301 fn test_validate_nonexistent_binary() {
302 let path = Path::new("/nonexistent/binary");
303 let result = validate_binary_path(path);
304
305 assert!(result.is_err());
306 assert!(matches!(
307 result.unwrap_err(),
308 CliTestError::BinaryNotFound(_)
309 ));
310 }
311
312 #[test]
313 fn test_validate_directory() {
314 let temp_dir = TempDir::new().unwrap();
315 let result = validate_binary_path(temp_dir.path());
316
317 assert!(result.is_err());
318 assert!(matches!(
319 result.unwrap_err(),
320 CliTestError::BinaryNotFound(_)
321 ));
322 }
323
324 #[cfg(unix)]
325 #[test]
326 fn test_validate_non_executable_file() {
327 use std::os::unix::fs::PermissionsExt;
328
329 let temp_dir = TempDir::new().unwrap();
330 let file_path = temp_dir.path().join("non_executable");
331
332 File::create(&file_path).unwrap();
334 let mut perms = std::fs::metadata(&file_path).unwrap().permissions();
335 perms.set_mode(0o644); std::fs::set_permissions(&file_path, perms).unwrap();
337
338 let result = validate_binary_path(&file_path);
339
340 assert!(result.is_err());
341 assert!(matches!(
342 result.unwrap_err(),
343 CliTestError::BinaryNotExecutable(_)
344 ));
345 }
346
347 #[test]
348 fn test_execute_with_timeout_echo() {
349 #[cfg(unix)]
351 {
352 let echo_path = Path::new("/bin/echo");
353 if echo_path.exists() {
354 let result =
355 execute_with_timeout(echo_path, &["hello", "world"], Duration::from_secs(5));
356
357 assert!(result.is_ok());
358 let output = result.unwrap();
359 assert!(output.contains("hello"));
360 }
361 }
362 }
363
364 #[test]
365 fn test_execute_with_timeout_sleep() {
366 #[cfg(unix)]
368 {
369 let sleep_path = Path::new("/bin/sleep");
370 if sleep_path.exists() {
371 let result = execute_with_timeout(
372 sleep_path,
373 &["10"], Duration::from_millis(500), );
376
377 assert!(result.is_err());
378 if let Err(CliTestError::ExecutionFailed(msg)) = result {
379 assert!(msg.contains("Timeout"));
380 }
381 }
382 }
383 }
384
385 #[test]
386 fn test_canonicalization() {
387 #[cfg(unix)]
389 {
390 let ls_path = Path::new("/bin/ls");
391 if ls_path.exists() {
392 let result = validate_binary_path(ls_path);
393 assert!(result.is_ok());
394
395 let canonical = result.unwrap();
396 assert!(canonical.is_absolute());
397 }
398 }
399 }
400}