use crate::error::{CliTestError, Result};
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct ResourceLimits {
pub max_memory_bytes: u64,
pub max_file_descriptors: u64,
pub max_processes: u64,
pub execution_timeout: Duration,
}
impl Default for ResourceLimits {
fn default() -> Self {
Self {
max_memory_bytes: 500 * 1024 * 1024, max_file_descriptors: 1024,
max_processes: 100,
execution_timeout: Duration::from_secs(300), }
}
}
impl ResourceLimits {
pub fn new(
max_memory_bytes: u64,
max_file_descriptors: u64,
max_processes: u64,
execution_timeout: Duration,
) -> Self {
Self {
max_memory_bytes,
max_file_descriptors,
max_processes,
execution_timeout,
}
}
#[cfg(unix)]
pub fn apply(&self) -> Result<()> {
use libc::{rlimit, setrlimit, RLIMIT_AS, RLIMIT_NOFILE, RLIMIT_NPROC};
let mem_limit = rlimit {
rlim_cur: self.max_memory_bytes,
rlim_max: self.max_memory_bytes,
};
unsafe {
if setrlimit(RLIMIT_AS, &mem_limit) != 0 {
return Err(CliTestError::ExecutionFailed(
"Failed to set memory limit".to_string(),
));
}
}
let fd_limit = rlimit {
rlim_cur: self.max_file_descriptors,
rlim_max: self.max_file_descriptors,
};
unsafe {
if setrlimit(RLIMIT_NOFILE, &fd_limit) != 0 {
return Err(CliTestError::ExecutionFailed(
"Failed to set file descriptor limit".to_string(),
));
}
}
let proc_limit = rlimit {
rlim_cur: self.max_processes,
rlim_max: self.max_processes,
};
unsafe {
if setrlimit(RLIMIT_NPROC, &proc_limit) != 0 {
return Err(CliTestError::ExecutionFailed(
"Failed to set process limit".to_string(),
));
}
}
Ok(())
}
#[cfg(windows)]
pub fn apply(&self) -> Result<()> {
use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::System::JobObjects::{
AssignProcessToJobObject, CreateJobObjectW, JobObjectExtendedLimitInformation,
SetInformationJobObject, JOBOBJECT_BASIC_LIMIT_INFORMATION,
JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_ACTIVE_PROCESS,
JOB_OBJECT_LIMIT_JOB_MEMORY, JOB_OBJECT_LIMIT_PROCESS_MEMORY,
};
use windows::Win32::System::Threading::GetCurrentProcess;
unsafe {
let job = CreateJobObjectW(None, None).map_err(|e| {
CliTestError::ExecutionFailed(format!("Failed to create job object: {}", e))
})?;
let mut limits = JOBOBJECT_EXTENDED_LIMIT_INFORMATION {
BasicLimitInformation: JOBOBJECT_BASIC_LIMIT_INFORMATION {
LimitFlags: JOB_OBJECT_LIMIT_ACTIVE_PROCESS
| JOB_OBJECT_LIMIT_PROCESS_MEMORY
| JOB_OBJECT_LIMIT_JOB_MEMORY,
ActiveProcessLimit: self.max_processes as u32,
..Default::default()
},
ProcessMemoryLimit: self.max_memory_bytes as usize,
JobMemoryLimit: self.max_memory_bytes as usize,
..Default::default()
};
SetInformationJobObject(
job,
JobObjectExtendedLimitInformation,
&mut limits as *mut _ as *mut _,
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
)
.map_err(|e| {
CloseHandle(job);
CliTestError::ExecutionFailed(format!("Failed to set job limits: {}", e))
})?;
let current_process = GetCurrentProcess();
AssignProcessToJobObject(job, current_process).map_err(|e| {
CloseHandle(job);
CliTestError::ExecutionFailed(format!("Failed to assign process to job: {}", e))
})?;
log::debug!("Resource limits applied via Job Object");
}
Ok(())
}
#[cfg(not(any(unix, windows)))]
pub fn apply(&self) -> Result<()> {
log::warn!("Resource limits not supported on this platform");
Ok(())
}
pub fn timeout(&self) -> Duration {
self.execution_timeout
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_limits() {
let limits = ResourceLimits::default();
assert_eq!(limits.max_memory_bytes, 500 * 1024 * 1024);
assert_eq!(limits.max_file_descriptors, 1024);
assert_eq!(limits.max_processes, 100);
assert_eq!(limits.execution_timeout, Duration::from_secs(300));
}
#[test]
fn test_custom_limits() {
let limits = ResourceLimits::new(100 * 1024 * 1024, 512, 50, Duration::from_secs(60));
assert_eq!(limits.max_memory_bytes, 100 * 1024 * 1024);
assert_eq!(limits.max_file_descriptors, 512);
assert_eq!(limits.max_processes, 50);
assert_eq!(limits.execution_timeout, Duration::from_secs(60));
}
#[test]
fn test_timeout_accessor() {
let limits = ResourceLimits::default();
assert_eq!(limits.timeout(), Duration::from_secs(300));
}
#[cfg(unix)]
#[test]
#[cfg_attr(
all(target_os = "linux", not(target_env = "musl")),
ignore = "Actual setrlimit calls affect process limits in CI"
)]
fn test_apply_limits_unix() {
let limits = ResourceLimits::new(100 * 1024 * 1024, 512, 50, Duration::from_secs(60));
let _ = limits.apply();
}
#[cfg(unix)]
#[test]
#[cfg_attr(
all(target_os = "linux", not(target_env = "musl")),
ignore = "Actual setrlimit calls affect process limits in CI"
)]
fn test_unix_limit_verification_with_getrlimit() {
use libc::{getrlimit, rlimit, RLIMIT_AS, RLIMIT_NOFILE, RLIMIT_NPROC};
let target_memory = 100 * 1024 * 1024; let target_fd = 256; let target_proc = 50;
let limits = ResourceLimits::new(
target_memory,
target_fd,
target_proc,
Duration::from_secs(60),
);
let mut mem_before = rlimit {
rlim_cur: 0,
rlim_max: 0,
};
unsafe {
getrlimit(RLIMIT_AS, &mut mem_before);
}
let apply_result = limits.apply();
let mut mem_after = rlimit {
rlim_cur: 0,
rlim_max: 0,
};
unsafe {
getrlimit(RLIMIT_AS, &mut mem_after);
}
if apply_result.is_ok() {
assert!(
mem_after.rlim_cur > 0,
"After successful apply, memory limit should be set"
);
let mut fd_after = rlimit {
rlim_cur: 0,
rlim_max: 0,
};
unsafe {
getrlimit(RLIMIT_NOFILE, &mut fd_after);
assert!(
fd_after.rlim_cur > 0,
"After successful apply, FD limit should be set"
);
}
let mut proc_after = rlimit {
rlim_cur: 0,
rlim_max: 0,
};
unsafe {
getrlimit(RLIMIT_NPROC, &mut proc_after);
assert!(
proc_after.rlim_cur > 0,
"After successful apply, process limit should be set"
);
}
}
}
#[cfg(unix)]
#[test]
#[cfg_attr(
all(target_os = "linux", not(target_env = "musl")),
ignore = "Actual setrlimit calls affect process limits in CI"
)]
fn test_unix_apply_returns_result_not_ok() {
let limits = ResourceLimits::new(100 * 1024 * 1024, 256, 50, Duration::from_secs(60));
let result = limits.apply();
match result {
Ok(()) => {
}
Err(_) => {
eprintln!("Apply failed (expected in restricted environments)");
}
}
}
#[cfg(not(any(unix, windows)))]
#[test]
fn test_other_platform_apply_executes() {
let limits = ResourceLimits::default();
let result = limits.apply();
assert!(
result.is_ok(),
"Other platforms should succeed with warning"
);
}
#[cfg(unix)]
#[test]
#[cfg_attr(
all(target_os = "linux", not(target_env = "musl")),
ignore = "Actual setrlimit calls affect process limits in CI"
)]
fn test_unix_setrlimit_error_detection() {
use libc::{getrlimit, rlimit, RLIMIT_AS};
let limits = ResourceLimits::new(
u64::MAX, u64::MAX, u64::MAX, Duration::from_secs(60),
);
let result = limits.apply();
match result {
Ok(()) => {
let mut limit = rlimit {
rlim_cur: 0,
rlim_max: 0,
};
unsafe {
getrlimit(RLIMIT_AS, &mut limit);
assert!(limit.rlim_cur > 0, "Limit should be set");
}
}
Err(e) => {
eprintln!("Setrlimit failed (expected): {}", e);
}
}
}
#[cfg(unix)]
#[test]
#[cfg_attr(
all(target_os = "linux", not(target_env = "musl")),
ignore = "Actual setrlimit calls affect process limits in CI"
)]
fn test_unix_all_three_setrlimit_calls_must_succeed() {
use libc::{getrlimit, rlimit, RLIMIT_AS, RLIMIT_NOFILE, RLIMIT_NPROC};
let limits = ResourceLimits::new(
100 * 1024 * 1024, 256, 50, Duration::from_secs(60),
);
let result = limits.apply();
if result.is_ok() {
let mut mem_limit = rlimit {
rlim_cur: 0,
rlim_max: 0,
};
let mut fd_limit = rlimit {
rlim_cur: 0,
rlim_max: 0,
};
let mut proc_limit = rlimit {
rlim_cur: 0,
rlim_max: 0,
};
unsafe {
assert_eq!(getrlimit(RLIMIT_AS, &mut mem_limit), 0);
assert_eq!(getrlimit(RLIMIT_NOFILE, &mut fd_limit), 0);
assert_eq!(getrlimit(RLIMIT_NPROC, &mut proc_limit), 0);
assert!(mem_limit.rlim_cur > 0, "Memory limit should be set");
assert!(fd_limit.rlim_cur > 0, "FD limit should be set");
assert!(proc_limit.rlim_cur > 0, "Process limit should be set");
}
} else {
eprintln!("Apply failed: {:?}", result);
}
}
}