cli_testing_specialist/utils/
resource_limits.rs1use crate::error::{CliTestError, Result};
2use std::time::Duration;
3
4#[derive(Debug, Clone)]
32pub struct ResourceLimits {
33 pub max_memory_bytes: u64,
35
36 pub max_file_descriptors: u64,
38
39 pub max_processes: u64,
41
42 pub execution_timeout: Duration,
44}
45
46impl Default for ResourceLimits {
47 fn default() -> Self {
48 Self {
49 max_memory_bytes: 500 * 1024 * 1024, max_file_descriptors: 1024,
51 max_processes: 100,
52 execution_timeout: Duration::from_secs(300), }
54 }
55}
56
57impl ResourceLimits {
58 pub fn new(
60 max_memory_bytes: u64,
61 max_file_descriptors: u64,
62 max_processes: u64,
63 execution_timeout: Duration,
64 ) -> Self {
65 Self {
66 max_memory_bytes,
67 max_file_descriptors,
68 max_processes,
69 execution_timeout,
70 }
71 }
72
73 #[cfg(unix)]
78 pub fn apply(&self) -> Result<()> {
79 use libc::{rlimit, setrlimit, RLIMIT_AS, RLIMIT_NOFILE, RLIMIT_NPROC};
80
81 let mem_limit = rlimit {
83 rlim_cur: self.max_memory_bytes,
84 rlim_max: self.max_memory_bytes,
85 };
86
87 unsafe {
88 if setrlimit(RLIMIT_AS, &mem_limit) != 0 {
89 return Err(CliTestError::ExecutionFailed(
90 "Failed to set memory limit".to_string(),
91 ));
92 }
93 }
94
95 let fd_limit = rlimit {
97 rlim_cur: self.max_file_descriptors,
98 rlim_max: self.max_file_descriptors,
99 };
100
101 unsafe {
102 if setrlimit(RLIMIT_NOFILE, &fd_limit) != 0 {
103 return Err(CliTestError::ExecutionFailed(
104 "Failed to set file descriptor limit".to_string(),
105 ));
106 }
107 }
108
109 let proc_limit = rlimit {
111 rlim_cur: self.max_processes,
112 rlim_max: self.max_processes,
113 };
114
115 unsafe {
116 if setrlimit(RLIMIT_NPROC, &proc_limit) != 0 {
117 return Err(CliTestError::ExecutionFailed(
118 "Failed to set process limit".to_string(),
119 ));
120 }
121 }
122
123 Ok(())
124 }
125
126 #[cfg(windows)]
131 pub fn apply(&self) -> Result<()> {
132 use windows::Win32::Foundation::{CloseHandle, HANDLE};
133 use windows::Win32::System::JobObjects::{
134 AssignProcessToJobObject, CreateJobObjectW, JobObjectExtendedLimitInformation,
135 SetInformationJobObject, JOBOBJECT_BASIC_LIMIT_INFORMATION,
136 JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_ACTIVE_PROCESS,
137 JOB_OBJECT_LIMIT_JOB_MEMORY, JOB_OBJECT_LIMIT_PROCESS_MEMORY,
138 };
139 use windows::Win32::System::Threading::GetCurrentProcess;
140
141 unsafe {
142 let job = CreateJobObjectW(None, None).map_err(|e| {
144 CliTestError::ExecutionFailed(format!("Failed to create job object: {}", e))
145 })?;
146
147 let mut limits = JOBOBJECT_EXTENDED_LIMIT_INFORMATION {
149 BasicLimitInformation: JOBOBJECT_BASIC_LIMIT_INFORMATION {
150 LimitFlags: JOB_OBJECT_LIMIT_ACTIVE_PROCESS
151 | JOB_OBJECT_LIMIT_PROCESS_MEMORY
152 | JOB_OBJECT_LIMIT_JOB_MEMORY,
153 ActiveProcessLimit: self.max_processes as u32,
154 ..Default::default()
155 },
156 ProcessMemoryLimit: self.max_memory_bytes as usize,
157 JobMemoryLimit: self.max_memory_bytes as usize,
158 ..Default::default()
159 };
160
161 SetInformationJobObject(
163 job,
164 JobObjectExtendedLimitInformation,
165 &mut limits as *mut _ as *mut _,
166 std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
167 )
168 .map_err(|e| {
169 CloseHandle(job);
170 CliTestError::ExecutionFailed(format!("Failed to set job limits: {}", e))
171 })?;
172
173 let current_process = GetCurrentProcess();
175 AssignProcessToJobObject(job, current_process).map_err(|e| {
176 CloseHandle(job);
177 CliTestError::ExecutionFailed(format!("Failed to assign process to job: {}", e))
178 })?;
179
180 log::debug!("Resource limits applied via Job Object");
184 }
185
186 Ok(())
187 }
188
189 #[cfg(not(any(unix, windows)))]
191 pub fn apply(&self) -> Result<()> {
192 log::warn!("Resource limits not supported on this platform");
193 Ok(())
194 }
195
196 pub fn timeout(&self) -> Duration {
198 self.execution_timeout
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn test_default_limits() {
208 let limits = ResourceLimits::default();
209
210 assert_eq!(limits.max_memory_bytes, 500 * 1024 * 1024);
211 assert_eq!(limits.max_file_descriptors, 1024);
212 assert_eq!(limits.max_processes, 100);
213 assert_eq!(limits.execution_timeout, Duration::from_secs(300));
214 }
215
216 #[test]
217 fn test_custom_limits() {
218 let limits = ResourceLimits::new(100 * 1024 * 1024, 512, 50, Duration::from_secs(60));
219
220 assert_eq!(limits.max_memory_bytes, 100 * 1024 * 1024);
221 assert_eq!(limits.max_file_descriptors, 512);
222 assert_eq!(limits.max_processes, 50);
223 assert_eq!(limits.execution_timeout, Duration::from_secs(60));
224 }
225
226 #[test]
227 fn test_timeout_accessor() {
228 let limits = ResourceLimits::default();
229 assert_eq!(limits.timeout(), Duration::from_secs(300));
230 }
231
232 #[cfg(unix)]
233 #[test]
234 #[cfg_attr(
235 all(target_os = "linux", not(target_env = "musl")),
236 ignore = "Actual setrlimit calls affect process limits in CI"
237 )]
238 fn test_apply_limits_unix() {
239 let limits = ResourceLimits::new(100 * 1024 * 1024, 512, 50, Duration::from_secs(60));
242
243 let _ = limits.apply();
245 }
246
247 #[cfg(unix)]
250 #[test]
251 #[cfg_attr(
252 all(target_os = "linux", not(target_env = "musl")),
253 ignore = "Actual setrlimit calls affect process limits in CI"
254 )]
255 fn test_unix_limit_verification_with_getrlimit() {
256 use libc::{getrlimit, rlimit, RLIMIT_AS, RLIMIT_NOFILE, RLIMIT_NPROC};
257
258 let target_memory = 100 * 1024 * 1024; let target_fd = 256; let target_proc = 50; let limits = ResourceLimits::new(
265 target_memory,
266 target_fd,
267 target_proc,
268 Duration::from_secs(60),
269 );
270
271 let mut mem_before = rlimit {
273 rlim_cur: 0,
274 rlim_max: 0,
275 };
276 unsafe {
277 getrlimit(RLIMIT_AS, &mut mem_before);
278 }
279
280 let apply_result = limits.apply();
282
283 let mut mem_after = rlimit {
286 rlim_cur: 0,
287 rlim_max: 0,
288 };
289 unsafe {
290 getrlimit(RLIMIT_AS, &mut mem_after);
291 }
292
293 if apply_result.is_ok() {
296 assert!(
299 mem_after.rlim_cur > 0,
300 "After successful apply, memory limit should be set"
301 );
302
303 let mut fd_after = rlimit {
305 rlim_cur: 0,
306 rlim_max: 0,
307 };
308 unsafe {
309 getrlimit(RLIMIT_NOFILE, &mut fd_after);
310 assert!(
311 fd_after.rlim_cur > 0,
312 "After successful apply, FD limit should be set"
313 );
314 }
315
316 let mut proc_after = rlimit {
318 rlim_cur: 0,
319 rlim_max: 0,
320 };
321 unsafe {
322 getrlimit(RLIMIT_NPROC, &mut proc_after);
323 assert!(
324 proc_after.rlim_cur > 0,
325 "After successful apply, process limit should be set"
326 );
327 }
328 }
329 }
330
331 #[cfg(unix)]
332 #[test]
333 #[cfg_attr(
334 all(target_os = "linux", not(target_env = "musl")),
335 ignore = "Actual setrlimit calls affect process limits in CI"
336 )]
337 fn test_unix_apply_returns_result_not_ok() {
338 let limits = ResourceLimits::new(100 * 1024 * 1024, 256, 50, Duration::from_secs(60));
342
343 let result = limits.apply();
345
346 match result {
349 Ok(()) => {
350 }
353 Err(_) => {
354 eprintln!("Apply failed (expected in restricted environments)");
356 }
357 }
358 }
359
360 #[cfg(not(any(unix, windows)))]
363 #[test]
364 fn test_other_platform_apply_executes() {
365 let limits = ResourceLimits::default();
367
368 let result = limits.apply();
371 assert!(
372 result.is_ok(),
373 "Other platforms should succeed with warning"
374 );
375 }
376
377 #[cfg(unix)]
380 #[test]
381 #[cfg_attr(
382 all(target_os = "linux", not(target_env = "musl")),
383 ignore = "Actual setrlimit calls affect process limits in CI"
384 )]
385 fn test_unix_setrlimit_error_detection() {
386 use libc::{getrlimit, rlimit, RLIMIT_AS};
387
388 let limits = ResourceLimits::new(
390 u64::MAX, u64::MAX, u64::MAX, Duration::from_secs(60),
394 );
395
396 let result = limits.apply();
399
400 match result {
403 Ok(()) => {
404 let mut limit = rlimit {
406 rlim_cur: 0,
407 rlim_max: 0,
408 };
409 unsafe {
410 getrlimit(RLIMIT_AS, &mut limit);
411 assert!(limit.rlim_cur > 0, "Limit should be set");
412 }
413 }
414 Err(e) => {
415 eprintln!("Setrlimit failed (expected): {}", e);
417 }
418 }
419 }
420
421 #[cfg(unix)]
424 #[test]
425 #[cfg_attr(
426 all(target_os = "linux", not(target_env = "musl")),
427 ignore = "Actual setrlimit calls affect process limits in CI"
428 )]
429 fn test_unix_all_three_setrlimit_calls_must_succeed() {
430 use libc::{getrlimit, rlimit, RLIMIT_AS, RLIMIT_NOFILE, RLIMIT_NPROC};
431
432 let limits = ResourceLimits::new(
435 100 * 1024 * 1024, 256, 50, Duration::from_secs(60),
439 );
440
441 let result = limits.apply();
443
444 if result.is_ok() {
447 let mut mem_limit = rlimit {
449 rlim_cur: 0,
450 rlim_max: 0,
451 };
452 let mut fd_limit = rlimit {
453 rlim_cur: 0,
454 rlim_max: 0,
455 };
456 let mut proc_limit = rlimit {
457 rlim_cur: 0,
458 rlim_max: 0,
459 };
460
461 unsafe {
462 assert_eq!(getrlimit(RLIMIT_AS, &mut mem_limit), 0);
464 assert_eq!(getrlimit(RLIMIT_NOFILE, &mut fd_limit), 0);
465 assert_eq!(getrlimit(RLIMIT_NPROC, &mut proc_limit), 0);
466
467 assert!(mem_limit.rlim_cur > 0, "Memory limit should be set");
469 assert!(fd_limit.rlim_cur > 0, "FD limit should be set");
470 assert!(proc_limit.rlim_cur > 0, "Process limit should be set");
471 }
472 } else {
473 eprintln!("Apply failed: {:?}", result);
476 }
477 }
478}