use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceLimits {
#[serde(default)]
pub time_limit: Option<f64>,
#[serde(default)]
pub wall_time_limit: Option<f64>,
#[serde(default)]
pub memory_limit: Option<u64>,
#[serde(default)]
pub stack_limit: Option<u64>,
#[serde(default)]
pub max_processes: Option<u32>,
#[serde(default)]
pub max_output: Option<u64>,
#[serde(default)]
pub max_open_files: Option<u32>,
#[serde(default)]
pub extra_time: Option<f64>,
}
impl ResourceLimits {
pub const KB: u64 = 1;
pub const MB: u64 = 1024;
pub const GB: u64 = 1024 * 1024;
pub fn new() -> Self {
Self::default()
}
pub fn with_time_limit(mut self, seconds: f64) -> Self {
self.time_limit = Some(seconds);
self
}
pub fn with_wall_time_limit(mut self, seconds: f64) -> Self {
self.wall_time_limit = Some(seconds);
self
}
pub fn with_memory_limit(mut self, kb: u64) -> Self {
self.memory_limit = Some(kb);
self
}
pub fn with_stack_limit(mut self, kb: u64) -> Self {
self.stack_limit = Some(kb);
self
}
pub fn with_max_processes(mut self, count: u32) -> Self {
self.max_processes = Some(count);
self
}
pub fn with_max_output(mut self, kb: u64) -> Self {
self.max_output = Some(kb);
self
}
pub fn with_overrides(&self, overrides: &ResourceLimits) -> ResourceLimits {
ResourceLimits {
time_limit: overrides.time_limit.or(self.time_limit),
wall_time_limit: overrides.wall_time_limit.or(self.wall_time_limit),
memory_limit: overrides.memory_limit.or(self.memory_limit),
stack_limit: overrides.stack_limit.or(self.stack_limit),
max_processes: overrides.max_processes.or(self.max_processes),
max_output: overrides.max_output.or(self.max_output),
max_open_files: overrides.max_open_files.or(self.max_open_files),
extra_time: overrides.extra_time.or(self.extra_time),
}
}
}
impl Default for ResourceLimits {
fn default() -> Self {
Self {
time_limit: Some(2.0),
wall_time_limit: Some(5.0),
memory_limit: Some(262144), stack_limit: Some(262144), max_processes: Some(1),
max_output: Some(65536), max_open_files: Some(64),
extra_time: Some(0.5),
}
}
}
#[derive(Debug, Clone)]
pub struct ExecutionResult {
pub status: ExecutionStatus,
pub limit_exceeded: LimitExceeded,
pub time: f64,
pub wall_time: f64,
pub memory: u64,
pub cg_memory: Option<u64>,
pub max_rss: Option<u64>,
pub exit_code: Option<i32>,
pub signal: Option<i32>,
pub message: Option<String>,
pub stdout: Option<Vec<u8>>,
pub stderr: Option<Vec<u8>>,
}
impl ExecutionResult {
#[must_use]
pub fn is_success(&self) -> bool {
matches!(self.status, ExecutionStatus::Ok) && self.exit_code == Some(0)
}
pub fn detect_memory_limit(&mut self, memory_limit: u64) {
if self.limit_exceeded.is_exceeded() {
return;
}
if matches!(
self.status,
ExecutionStatus::Signaled | ExecutionStatus::RuntimeError
) && let Some(cg_mem) = self.cg_memory
&& cg_mem >= memory_limit
{
self.limit_exceeded = LimitExceeded::Memory;
}
}
}
impl Default for ExecutionResult {
fn default() -> Self {
Self {
status: ExecutionStatus::Ok,
limit_exceeded: LimitExceeded::NotExceeded,
time: 0.0,
wall_time: 0.0,
memory: 0,
cg_memory: None,
max_rss: None,
exit_code: None,
signal: None,
message: None,
stdout: None,
stderr: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ExecutionStatus {
#[serde(rename = "OK")]
Ok,
#[serde(rename = "RE")]
RuntimeError,
#[serde(rename = "TO")]
TimeLimitExceeded,
#[serde(rename = "SG")]
Signaled,
#[serde(rename = "XX")]
InternalError,
}
impl ExecutionStatus {
pub fn from_isolate_status(status: &str) -> Self {
match status {
"OK" => ExecutionStatus::Ok,
"RE" => ExecutionStatus::RuntimeError,
"TO" => ExecutionStatus::TimeLimitExceeded,
"SG" => ExecutionStatus::Signaled,
"XX" => ExecutionStatus::InternalError,
_ => ExecutionStatus::InternalError,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum LimitExceeded {
#[default]
#[serde(rename = "none")]
NotExceeded,
#[serde(rename = "time")]
Time,
#[serde(rename = "wall_time")]
WallTime,
#[serde(rename = "memory")]
Memory,
#[serde(rename = "output")]
Output,
}
impl LimitExceeded {
pub fn from_message(message: Option<&str>) -> Self {
let Some(msg) = message else {
return LimitExceeded::NotExceeded;
};
let msg_lower = msg.to_lowercase();
if msg_lower.contains("time limit") {
if msg_lower.contains("wall") {
LimitExceeded::WallTime
} else {
LimitExceeded::Time
}
} else if msg_lower.contains("memory") || msg_lower.contains("out of memory") {
LimitExceeded::Memory
} else if msg_lower.contains("output") {
LimitExceeded::Output
} else {
LimitExceeded::NotExceeded
}
}
#[must_use]
pub fn is_exceeded(&self) -> bool {
!matches!(self, LimitExceeded::NotExceeded)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MountConfig {
pub source: String,
pub target: String,
#[serde(default)]
pub writable: bool,
#[serde(default)]
pub optional: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resource_limits_default_has_all_fields() {
let limits = ResourceLimits::default();
assert!(limits.time_limit.is_some());
assert!(limits.wall_time_limit.is_some());
assert!(limits.memory_limit.is_some());
assert!(limits.stack_limit.is_some());
assert!(limits.max_processes.is_some());
assert!(limits.max_output.is_some());
assert!(limits.max_open_files.is_some());
assert!(limits.extra_time.is_some());
}
#[test]
fn resource_limits_new_equals_default() {
let new = ResourceLimits::new();
let default = ResourceLimits::default();
assert_eq!(new.time_limit, default.time_limit);
assert_eq!(new.memory_limit, default.memory_limit);
}
#[test]
fn resource_limits_builder_methods() {
let limits = ResourceLimits::new()
.with_time_limit(5.0)
.with_wall_time_limit(10.0)
.with_memory_limit(1024)
.with_stack_limit(512)
.with_max_processes(4)
.with_max_output(2048);
assert_eq!(limits.time_limit, Some(5.0));
assert_eq!(limits.wall_time_limit, Some(10.0));
assert_eq!(limits.memory_limit, Some(1024));
assert_eq!(limits.stack_limit, Some(512));
assert_eq!(limits.max_processes, Some(4));
assert_eq!(limits.max_output, Some(2048));
}
#[test]
fn with_overrides_empty_preserves_base() {
let base = ResourceLimits::default();
let empty = ResourceLimits {
time_limit: None,
wall_time_limit: None,
memory_limit: None,
stack_limit: None,
max_processes: None,
max_output: None,
max_open_files: None,
extra_time: None,
};
let result = base.with_overrides(&empty);
assert_eq!(result.time_limit, base.time_limit);
assert_eq!(result.wall_time_limit, base.wall_time_limit);
assert_eq!(result.memory_limit, base.memory_limit);
assert_eq!(result.stack_limit, base.stack_limit);
assert_eq!(result.max_processes, base.max_processes);
assert_eq!(result.max_output, base.max_output);
assert_eq!(result.max_open_files, base.max_open_files);
assert_eq!(result.extra_time, base.extra_time);
}
#[test]
fn with_overrides_replaces_values() {
let base = ResourceLimits::default();
let overrides = ResourceLimits {
time_limit: Some(10.0),
memory_limit: Some(512 * ResourceLimits::MB),
..Default::default()
};
let result = base.with_overrides(&overrides);
assert_eq!(result.time_limit, Some(10.0));
assert_eq!(result.memory_limit, Some(512 * ResourceLimits::MB));
assert_eq!(result.wall_time_limit, base.wall_time_limit);
}
#[test]
fn with_overrides_partial_override() {
let base = ResourceLimits {
time_limit: Some(2.0),
memory_limit: Some(256 * ResourceLimits::MB),
max_processes: None,
..Default::default()
};
let overrides = ResourceLimits {
time_limit: Some(5.0),
max_processes: Some(4),
..Default::default()
};
let result = base.with_overrides(&overrides);
assert_eq!(result.time_limit, Some(5.0)); assert_eq!(result.memory_limit, Some(256 * ResourceLimits::MB)); assert_eq!(result.max_processes, Some(4)); }
#[test]
fn execution_status_from_isolate_status_ok() {
assert_eq!(
ExecutionStatus::from_isolate_status("OK"),
ExecutionStatus::Ok
);
}
#[test]
fn execution_status_from_isolate_status_re() {
assert_eq!(
ExecutionStatus::from_isolate_status("RE"),
ExecutionStatus::RuntimeError
);
}
#[test]
fn execution_status_from_isolate_status_to() {
assert_eq!(
ExecutionStatus::from_isolate_status("TO"),
ExecutionStatus::TimeLimitExceeded
);
}
#[test]
fn execution_status_from_isolate_status_sg() {
assert_eq!(
ExecutionStatus::from_isolate_status("SG"),
ExecutionStatus::Signaled
);
}
#[test]
fn execution_status_from_isolate_status_xx() {
assert_eq!(
ExecutionStatus::from_isolate_status("XX"),
ExecutionStatus::InternalError
);
}
#[test]
fn execution_status_from_isolate_status_unknown_defaults_to_internal_error() {
assert_eq!(
ExecutionStatus::from_isolate_status("UNKNOWN"),
ExecutionStatus::InternalError
);
assert_eq!(
ExecutionStatus::from_isolate_status(""),
ExecutionStatus::InternalError
);
assert_eq!(
ExecutionStatus::from_isolate_status("ok"),
ExecutionStatus::InternalError
);
}
#[test]
fn limit_exceeded_from_message_none() {
assert_eq!(
LimitExceeded::from_message(None),
LimitExceeded::NotExceeded
);
}
#[test]
fn limit_exceeded_from_message_time_limit() {
assert_eq!(
LimitExceeded::from_message(Some("Time limit exceeded")),
LimitExceeded::Time
);
assert_eq!(
LimitExceeded::from_message(Some("time limit exceeded")),
LimitExceeded::Time
);
assert_eq!(
LimitExceeded::from_message(Some("TIME LIMIT EXCEEDED")),
LimitExceeded::Time
);
}
#[test]
fn limit_exceeded_from_message_wall_time() {
assert_eq!(
LimitExceeded::from_message(Some("Wall time limit exceeded")),
LimitExceeded::WallTime
);
assert_eq!(
LimitExceeded::from_message(Some("wall time limit exceeded")),
LimitExceeded::WallTime
);
}
#[test]
fn limit_exceeded_from_message_memory() {
assert_eq!(
LimitExceeded::from_message(Some("Memory limit exceeded")),
LimitExceeded::Memory
);
assert_eq!(
LimitExceeded::from_message(Some("Out of memory")),
LimitExceeded::Memory
);
assert_eq!(
LimitExceeded::from_message(Some("out of memory")),
LimitExceeded::Memory
);
}
#[test]
fn limit_exceeded_from_message_output() {
assert_eq!(
LimitExceeded::from_message(Some("Output limit exceeded")),
LimitExceeded::Output
);
assert_eq!(
LimitExceeded::from_message(Some("output limit")),
LimitExceeded::Output
);
}
#[test]
fn limit_exceeded_from_message_unknown() {
assert_eq!(
LimitExceeded::from_message(Some("Some other error")),
LimitExceeded::NotExceeded
);
assert_eq!(
LimitExceeded::from_message(Some("")),
LimitExceeded::NotExceeded
);
}
#[test]
fn limit_exceeded_is_exceeded() {
assert!(!LimitExceeded::NotExceeded.is_exceeded());
assert!(LimitExceeded::Time.is_exceeded());
assert!(LimitExceeded::WallTime.is_exceeded());
assert!(LimitExceeded::Memory.is_exceeded());
assert!(LimitExceeded::Output.is_exceeded());
}
#[test]
fn execution_result_is_success_true() {
let result = ExecutionResult {
status: ExecutionStatus::Ok,
exit_code: Some(0),
..Default::default()
};
assert!(result.is_success());
}
#[test]
fn execution_result_is_success_false_non_zero_exit() {
let result = ExecutionResult {
status: ExecutionStatus::Ok,
exit_code: Some(1),
..Default::default()
};
assert!(!result.is_success());
}
#[test]
fn execution_result_is_success_false_bad_status() {
let result = ExecutionResult {
status: ExecutionStatus::RuntimeError,
exit_code: Some(0),
..Default::default()
};
assert!(!result.is_success());
}
#[test]
fn execution_result_is_success_false_no_exit_code() {
let result = ExecutionResult {
status: ExecutionStatus::Ok,
exit_code: None,
..Default::default()
};
assert!(!result.is_success());
}
#[test]
fn execution_result_default() {
let result = ExecutionResult::default();
assert_eq!(result.status, ExecutionStatus::Ok);
assert_eq!(result.limit_exceeded, LimitExceeded::NotExceeded);
assert_eq!(result.time, 0.0);
assert_eq!(result.wall_time, 0.0);
assert_eq!(result.memory, 0);
assert!(result.exit_code.is_none());
assert!(result.signal.is_none());
assert!(result.message.is_none());
assert!(result.stdout.is_none());
assert!(result.stderr.is_none());
}
#[test]
fn detect_memory_limit_sets_memory_when_cg_mem_at_limit() {
let mut result = ExecutionResult {
status: ExecutionStatus::Signaled,
signal: Some(9),
cg_memory: Some(262144),
..Default::default()
};
result.detect_memory_limit(262144);
assert_eq!(result.limit_exceeded, LimitExceeded::Memory);
}
#[test]
fn detect_memory_limit_noop_when_already_exceeded() {
let mut result = ExecutionResult {
status: ExecutionStatus::Signaled,
signal: Some(9),
limit_exceeded: LimitExceeded::Time,
cg_memory: Some(262144),
..Default::default()
};
result.detect_memory_limit(262144);
assert_eq!(result.limit_exceeded, LimitExceeded::Time);
}
#[test]
fn detect_memory_limit_noop_when_ok_status() {
let mut result = ExecutionResult {
status: ExecutionStatus::Ok,
cg_memory: Some(262144),
..Default::default()
};
result.detect_memory_limit(262144);
assert_eq!(result.limit_exceeded, LimitExceeded::NotExceeded);
}
#[test]
fn detect_memory_limit_noop_when_no_cg_memory() {
let mut result = ExecutionResult {
status: ExecutionStatus::Signaled,
signal: Some(9),
cg_memory: None,
..Default::default()
};
result.detect_memory_limit(262144);
assert_eq!(result.limit_exceeded, LimitExceeded::NotExceeded);
}
#[test]
fn mount_config_default_read_only() {
let mount = MountConfig {
source: "/src".to_string(),
target: "/dest".to_string(),
writable: false,
optional: false,
};
assert!(!mount.writable);
}
}
#[cfg(test)]
mod proptests {
use proptest::prelude::*;
use super::*;
proptest! {
#[test]
fn with_overrides_identity(
time in proptest::option::of(0.0f64..1000.0),
wall_time in proptest::option::of(0.0f64..1000.0),
memory in proptest::option::of(0u64..1_000_000),
stack in proptest::option::of(0u64..1_000_000),
procs in proptest::option::of(0u32..100),
output in proptest::option::of(0u64..1_000_000),
open_files in proptest::option::of(0u32..1000),
extra in proptest::option::of(0.0f64..10.0),
) {
let base = ResourceLimits {
time_limit: time,
wall_time_limit: wall_time,
memory_limit: memory,
stack_limit: stack,
max_processes: procs,
max_output: output,
max_open_files: open_files,
extra_time: extra,
};
let empty = ResourceLimits {
time_limit: None,
wall_time_limit: None,
memory_limit: None,
stack_limit: None,
max_processes: None,
max_output: None,
max_open_files: None,
extra_time: None,
};
let result = base.with_overrides(&empty);
prop_assert_eq!(result.time_limit, base.time_limit);
prop_assert_eq!(result.wall_time_limit, base.wall_time_limit);
prop_assert_eq!(result.memory_limit, base.memory_limit);
prop_assert_eq!(result.stack_limit, base.stack_limit);
prop_assert_eq!(result.max_processes, base.max_processes);
prop_assert_eq!(result.max_output, base.max_output);
prop_assert_eq!(result.max_open_files, base.max_open_files);
prop_assert_eq!(result.extra_time, base.extra_time);
}
#[test]
fn with_overrides_full_override(
base_time in proptest::option::of(0.0f64..1000.0),
override_time in 0.0f64..1000.0,
) {
let base = ResourceLimits {
time_limit: base_time,
..Default::default()
};
let overrides = ResourceLimits {
time_limit: Some(override_time),
..Default::default()
};
let result = base.with_overrides(&overrides);
prop_assert_eq!(result.time_limit, Some(override_time));
}
#[test]
fn limit_exceeded_from_message_never_panics(msg in ".*") {
let _ = LimitExceeded::from_message(Some(&msg));
}
#[test]
fn execution_status_from_isolate_never_panics(status in ".*") {
let _ = ExecutionStatus::from_isolate_status(&status);
}
}
}