use std::time::Duration;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use thiserror::Error;
pub const LIMITS_MEMORY_WASM32_MAX_BYTES: u64 = 4 * 1024 * 1024 * 1024;
pub const LIMITS_MEMORY_WASM32_PAGE_BYTES: u64 = 64 * 1024;
pub const LIMITS_WALL_CLOCK_MAX: Duration = Duration::from_secs(3600);
pub const LIMITS_CPU_MILLICORES_MAX: u32 = 128_000;
pub const LIMITS_FUEL_MAX: u64 = 1_000_000_000_000;
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct LimitsSpec {
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "ser_byte_size",
deserialize_with = "de_byte_size"
)]
pub memory: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fuel: Option<u64>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "ser_duration",
deserialize_with = "de_duration"
)]
pub wall_clock: Option<Duration>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "ser_millicores",
deserialize_with = "de_millicores"
)]
pub cpu: Option<u32>,
}
impl LimitsSpec {
#[must_use]
pub const fn is_empty(&self) -> bool {
self.memory().is_none()
&& self.fuel().is_none()
&& self.wall_clock().is_none()
&& self.cpu().is_none()
}
#[must_use]
pub const fn memory(&self) -> Option<u64> {
self.memory
}
#[must_use]
pub const fn fuel(&self) -> Option<u64> {
self.fuel
}
#[must_use]
pub const fn wall_clock(&self) -> Option<Duration> {
self.wall_clock
}
#[must_use]
pub const fn cpu(&self) -> Option<u32> {
self.cpu
}
pub fn validate(&self) -> Result<(), LimitsError> {
if let Some(m) = self.memory() {
crate::render::require_positive_quantum_multiple_bounded_u64(
m,
LIMITS_MEMORY_WASM32_PAGE_BYTES,
LIMITS_MEMORY_WASM32_MAX_BYTES,
|| LimitsError::MemoryZero,
LimitsError::memory_below_wasm32_page,
LimitsError::memory_exceeds_wasm32_cap,
LimitsError::memory_not_page_multiple,
)?;
}
if let Some(f) = self.fuel() {
crate::render::require_positive_bounded_u64(
f,
LIMITS_FUEL_MAX,
|| LimitsError::FuelZero,
LimitsError::fuel_exceeds_cap,
)?;
}
if let Some(w) = self.wall_clock() {
crate::render::require_positive_canonical_bounded_duration(
w,
LIMITS_WALL_CLOCK_MAX,
|| LimitsError::WallClockZero,
LimitsError::wall_clock_not_canonical,
LimitsError::wall_clock_exceeds_cap,
)?;
}
if let Some(m) = self.cpu() {
crate::render::require_positive_bounded_u32(
m,
LIMITS_CPU_MILLICORES_MAX,
|| LimitsError::CpuZero,
LimitsError::cpu_exceeds_cap,
)?;
}
Ok(())
}
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum LimitsError {
#[error("byte-size: missing magnitude in {0:?}")]
EmptyByteSize(String),
#[error("byte-size: unknown unit {unit:?} (expected one of B, KB, MB, GB, KiB, MiB, GiB)")]
UnknownByteUnit { unit: String },
#[error("byte-size: failed to parse magnitude {0:?}")]
BadByteMagnitude(String),
#[error(
"byte-size: magnitude {value:?} is not a non-negative integer — the canonical \
authoring form for `:limits :memory` is `<integer><unit>` (e.g. `\"1024\"`, \
`\"64MiB\"`, `\"1GiB\"`) with no decimal point and no leading `+` sign. A \
fractional / decimal-shaped magnitude (`\"1.5KiB\"`, `\"1.0MiB\"`, `\"0.5GiB\"`, \
`\"+1024\"`) round-trips through `render_byte_size` to a *different* canonical \
form (`\"1536\"`, `\"1MiB\"`, `\"512MiB\"`, `\"1KiB\"`) on first serialize — \
breaking the THEORY.md §V.2.7 render-determinism contract every typed slot \
carries. Pick an integer magnitude in the unit that divides cleanly (write \
`\"1536\"` instead of `\"1.5KiB\"`; `\"512MiB\"` instead of `\"0.5GiB\"`)"
)]
NonIntegerByteMagnitude { value: String },
#[error(
"byte-size: magnitude {value:?} has a non-canonical leading zero — the canonical \
authoring form for `:limits :memory` is `<integer><unit>` (e.g. `\"64MiB\"`, \
`\"1GiB\"`, `\"512KiB\"`, `\"1024\"`) with no leading-zero padding on the magnitude. \
A leading-zero magnitude (`\"064MiB\"`, `\"01024\"`, `\"00KiB\"`, `\"0500MB\"`) round-trips \
through `render_byte_size` to a *different* canonical form (`\"64MiB\"`, `\"1KiB\"`, \
`\"0\"`, `\"500MB\"`) on first serialize — breaking the THEORY.md Part V \
render-determinism contract every typed slot carries. Strip the leading zeros \
(write `\"64MiB\"` instead of `\"064MiB\"`)"
)]
LeadingZeroByteMagnitude { value: String },
#[error(
"byte-size: value {value:?} contains whitespace byte 0x{byte:02x} — the canonical \
authoring form for `:limits :memory` is `<integer><unit>` (e.g. `\"64MiB\"`, \
`\"1GiB\"`, `\"512KiB\"`, `\"1024\"`) with no whitespace bytes anywhere. A \
whitespace-carrying shape (`\" 64MiB\"`, `\"64MiB \"`, `\"64 MiB\"`, `\"\\t64MiB\"`, \
`\"64MiB\\n\"`) round-trips through `render_byte_size` to a *different* canonical \
form (`\"64MiB\"`) on first serialize — breaking the THEORY.md Part V \
render-determinism contract every typed slot carries. Strip every whitespace byte \
(write `\"64MiB\"` verbatim)"
)]
WhitespaceInByteSize { value: String, byte: u8 },
#[error(
"byte-size: value {value:?} contains a non-ASCII Unicode whitespace character \
{ch:?} (U+{codepoint:04X}) — the canonical authoring form for `:limits :memory` \
is `<integer><unit>` (e.g. `\"64MiB\"`, `\"1GiB\"`, `\"512KiB\"`, `\"1024\"`) \
with no whitespace characters anywhere (ASCII or Unicode). A non-ASCII-whitespace-\
carrying shape (`\"\\u{{00A0}}64MiB\"` — paste-from-typography NBSP prefix; \
`\"64MiB\\u{{2028}}\"` — paste-from-web-doc line-separator suffix; \
`\"64\\u{{2003}}MiB\"` — paste-from-typography EM-SPACE between magnitude and \
unit) survives the pre-existing `u8::is_ascii_whitespace` byte-scan (none of \
its bytes match the ASCII whitespace set) but `str::trim` (which uses \
`char::is_whitespace` — the Unicode `White_Space` property, strictly wider than \
the ASCII byte set) silently strips it at parse entry, and the value round-trips \
through `render_byte_size` to a *different* canonical form (`\"64MiB\"`) on \
first serialize — breaking the THEORY.md Part V render-determinism contract \
every typed slot carries. Strip every non-ASCII whitespace character (write \
`\"64MiB\"` verbatim with only ASCII bytes)"
)]
NonAsciiWhitespaceInByteSize {
value: String,
ch: char,
codepoint: u32,
},
#[error("duration: missing magnitude in {0:?}")]
EmptyDuration(String),
#[error("duration: unknown unit {unit:?} (expected one of ms, s, m, h)")]
UnknownDurationUnit { unit: String },
#[error("duration: failed to parse magnitude {0:?}")]
BadDurationMagnitude(String),
#[error(
"duration: magnitude {value:?} is not a non-negative integer — the canonical \
authoring form for `:limits :wall-clock` is `<integer><unit>` (e.g. `\"30s\"`, \
`\"500ms\"`, `\"2m\"`, `\"1h\"`) with no decimal point and no leading `+` sign. A \
fractional / decimal-shaped magnitude (`\"1.5s\"`, `\"1.0s\"`, `\"0.5m\"`, \
`\"+30s\"`, `\"-30s\"`) round-trips through `render_duration` to a *different* \
canonical form (`\"1500ms\"`, `\"1s\"`, `\"30s\"`, `\"30s\"`) on first serialize \
— breaking the THEORY.md Part V render-determinism contract every typed slot \
carries. Pick an integer magnitude in the unit that divides cleanly (write \
`\"1500ms\"` instead of `\"1.5s\"`; `\"30s\"` instead of `\"0.5m\"`)"
)]
NonIntegerDurationMagnitude { value: String },
#[error(
"duration: magnitude {value:?} has a non-canonical leading zero — the canonical \
authoring form for `:limits :wall-clock` is `<integer><unit>` (e.g. `\"30s\"`, \
`\"500ms\"`, `\"2m\"`, `\"1h\"`) with no leading-zero padding on the magnitude. \
A leading-zero magnitude (`\"030s\"`, `\"00s\"`, `\"01h\"`, `\"0500ms\"`) round-trips \
through `render_duration` to a *different* canonical form (`\"30s\"`, `\"0s\"`, \
`\"1h\"`, `\"500ms\"`) on first serialize — breaking the THEORY.md Part V \
render-determinism contract every typed slot carries. Strip the leading zeros \
(write `\"30s\"` instead of `\"030s\"`)"
)]
LeadingZeroDurationMagnitude { value: String },
#[error(
"duration: value {value:?} contains whitespace byte 0x{byte:02x} — the canonical \
authoring form for `:limits :wall-clock` is `<integer><unit>` (e.g. `\"30s\"`, \
`\"500ms\"`, `\"2m\"`, `\"1h\"`) with no whitespace bytes anywhere. A \
whitespace-carrying shape (`\" 30s\"`, `\"30s \"`, `\"30 s\"`, `\"\\t30s\"`, \
`\"30s\\n\"`) round-trips through `render_duration` to a *different* canonical form \
(`\"30s\"`) on first serialize — breaking the THEORY.md Part V render-determinism \
contract every typed slot carries. Strip every whitespace byte (write `\"30s\"` \
verbatim)"
)]
WhitespaceInDuration { value: String, byte: u8 },
#[error(
"duration: value {value:?} contains a non-ASCII Unicode whitespace character \
{ch:?} (U+{codepoint:04X}) — the canonical authoring form for `:limits :wall-clock` \
is `<integer><unit>` (e.g. `\"30s\"`, `\"500ms\"`, `\"2m\"`, `\"1h\"`) with no \
whitespace characters anywhere (ASCII or Unicode). A non-ASCII-whitespace-\
carrying shape (`\"\\u{{00A0}}30s\"` — paste-from-typography NBSP prefix; \
`\"30s\\u{{2028}}\"` — paste-from-web-doc line-separator suffix; \
`\"30\\u{{2003}}s\"` — paste-from-typography EM-SPACE between magnitude and \
unit) survives the pre-existing `u8::is_ascii_whitespace` byte-scan (none of \
its bytes match the ASCII whitespace set) but `str::trim` (which uses \
`char::is_whitespace` — the Unicode `White_Space` property, strictly wider than \
the ASCII byte set) silently strips it at parse entry, and the value round-trips \
through `render_duration` to a *different* canonical form (`\"30s\"`) on first \
serialize — breaking the THEORY.md Part V render-determinism contract every \
typed slot carries. Strip every non-ASCII whitespace character (write `\"30s\"` \
verbatim with only ASCII bytes)"
)]
NonAsciiWhitespaceInDuration {
value: String,
ch: char,
codepoint: u32,
},
#[error("millicores: bad value {0:?} (expected `<int>m` or `<int>`)")]
BadMillicores(String),
#[error(
"millicores: magnitude {value:?} is not a non-negative integer — the canonical \
authoring form for `:limits :cpu` is `<integer>m` (Kubernetes millicores, e.g. \
`\"500m\"` for half a core, `\"2000m\"` for two cores) or the bare-core \
shorthand `<integer>` (e.g. `\"2\"` = `\"2000m\"`), with no decimal point and \
no leading `+` sign. A fractional / decimal-shaped magnitude (`\"1.5\"`, \
`\"500.0m\"`, `\"+500m\"`, `\"-100m\"`) round-trips through `render_millicores` \
to a *different* canonical form (`\"1500m\"`, `\"500m\"`, `\"500m\"`, \
parse-rejection) on first serialize — breaking the THEORY.md Part V \
render-determinism contract every typed slot carries. Pick an integer magnitude \
in millicores (write `\"1500m\"` instead of `\"1.5\"`; `\"500m\"` instead of \
`\"500.0m\"`)"
)]
NonIntegerMillicoreMagnitude { value: String },
#[error(
"millicores: magnitude {value:?} has a non-canonical leading zero — the canonical \
authoring form for `:limits :cpu` is `<integer>m` (Kubernetes millicores, e.g. \
`\"500m\"` for half a core, `\"2000m\"` for two cores) or the bare-core shorthand \
`<integer>` (e.g. `\"2\"` = `\"2000m\"`) with no leading-zero padding on the \
magnitude. A leading-zero magnitude (`\"0500m\"`, `\"00m\"`, `\"02\"`, `\"01500m\"`) \
round-trips through `render_millicores` to a *different* canonical form (`\"500m\"`, \
`\"0m\"`, `\"2000m\"`, `\"1500m\"`) on first serialize — breaking the THEORY.md Part \
V render-determinism contract every typed slot carries. Strip the leading zeros \
(write `\"500m\"` instead of `\"0500m\"`; `\"2\"` instead of `\"02\"`)"
)]
LeadingZeroMillicoreMagnitude { value: String },
#[error(
"millicores: value {value:?} contains whitespace byte 0x{byte:02x} — the canonical \
authoring form for `:limits :cpu` is `<integer>m` (Kubernetes millicores, e.g. \
`\"500m\"`, `\"2000m\"`) or the bare-core shorthand `<integer>` (e.g. `\"2\"`) \
with no whitespace bytes anywhere. A whitespace-carrying shape (`\" 500m\"`, \
`\"500m \"`, `\"500 m\"`, `\"\\t500m\"`, `\"500m\\n\"`) round-trips through \
`render_millicores` to a *different* canonical form (`\"500m\"`) on first \
serialize — breaking the THEORY.md Part V render-determinism contract every \
typed slot carries. Strip every whitespace byte (write `\"500m\"` verbatim)"
)]
WhitespaceInMillicores { value: String, byte: u8 },
#[error(
"millicores: value {value:?} contains a non-ASCII Unicode whitespace character \
{ch:?} (U+{codepoint:04X}) — the canonical authoring form for `:limits :cpu` is \
`<integer>m` (Kubernetes millicores, e.g. `\"500m\"`, `\"2000m\"`) or the \
bare-core shorthand `<integer>` (e.g. `\"2\"`) with no whitespace characters \
anywhere (ASCII or Unicode). A non-ASCII-whitespace-carrying shape \
(`\"\\u{{00A0}}500m\"` — paste-from-typography NBSP prefix; \
`\"500m\\u{{2028}}\"` — paste-from-web-doc line-separator suffix; \
`\"500\\u{{2003}}m\"` — paste-from-typography EM-SPACE between magnitude and \
unit) survives the pre-existing `u8::is_ascii_whitespace` byte-scan (none of \
its bytes match the ASCII whitespace set) but `str::trim` (which uses \
`char::is_whitespace` — the Unicode `White_Space` property, strictly wider than \
the ASCII byte set) silently strips it at parse entry, and the value round-trips \
through `render_millicores` to a *different* canonical form (`\"500m\"`) on \
first serialize — breaking the THEORY.md Part V render-determinism contract \
every typed slot carries. Strip every non-ASCII whitespace character (write \
`\"500m\"` verbatim with only ASCII bytes)"
)]
NonAsciiWhitespaceInMillicores {
value: String,
ch: char,
codepoint: u32,
},
#[error(
":limits :memory must be > 0 — wasmtime StoreLimits refuses a zero memory cap; omit the field for unbounded"
)]
MemoryZero,
#[error(
":limits :memory ({bytes} bytes) is below the wasm32-wasip2 linear-memory page size (64 KiB = 65536 bytes) — a sub-page cap cannot hold a single wasm linear memory page, so instantiation of any component declaring `(memory 1)` traps with `memory minimum size of 1 pages exceeds memory limits` and a `(memory 0)` component traps the first `memory.grow(1)`. Pin a value ≥ 64 KiB (e.g. `\"64KiB\"`, `\"1MiB\"`, `\"64MiB\"`) or omit the field for unbounded"
)]
MemoryBelowWasm32Page { bytes: u64 },
#[error(
":limits :memory ({bytes} bytes) exceeds the wasm32-wasip2 linear-memory ceiling (4 GiB = 4294967296 bytes); pin a value ≤ 4 GiB or omit the field for unbounded"
)]
MemoryExceedsWasm32Cap { bytes: u64 },
#[error(
":limits :memory ({bytes} bytes) carries a sub-page residue the wasm32-wasip2 \
linear-memory model cannot honor — the wasm spec defines linear memory in \
fixed 64 KiB pages (LIMITS_MEMORY_WASM32_PAGE_BYTES = 65536 bytes) and \
wasmtime's StoreLimits::memory_size is consumed as a page-quantized ceiling: \
the engine can grow at most floor({bytes} / 65536) pages, and the bytes in \
[floor({bytes} / 65536) * 65536, {bytes}] are structural dead space the \
runtime cannot honor. Pin a page-aligned value in 64KiB..=4GiB \
(the canonical authoring magnitudes — `\"64KiB\"`, `\"128KiB\"`, `\"1MiB\"`, \
`\"64MiB\"`, `\"1GiB\"`, `\"4GiB\"` — every power-of-1024 unit the byte-size \
codec emits divides cleanly by the page size) or omit the field for unbounded"
)]
MemoryNotPageMultiple { bytes: u64 },
#[error(
":limits :fuel must be > 0 — wasmtime traps the first instruction at fuel=0; omit the field for unbounded"
)]
FuelZero,
#[error(
":limits :fuel ({fuel} instructions) exceeds the per-process ceiling \
(LIMITS_FUEL_MAX = 1_000_000_000_000 = 10^12 wasm instructions) — a value \
above this cap turns the typed per-call fuel counter into a no-op budget: \
the sibling `:wall-clock` cap (LIMITS_WALL_CLOCK_MAX = 1h = 3600s) fires \
before the fuel counter could ever be drained (wasmtime's documented \
fuel-tracked execution rate sits at ~10^8–10^9 fuel-units per second on \
modern x86_64 / aarch64 hosts running wasmtime through Cranelift, so the \
largest realistic per-call fuel budget reachable within 1h sits at ~3.6 × \
10^11–3.6 × 10^12 fuel-units, and a value above 10^12 is structurally \
unreachable as a per-call counter), so the typed `:fuel` slot becomes a \
declared-but-no-op contract far from the source caixa.lisp. Pin a value \
in 1..=1_000_000_000_000 (the canonical caixa Servico runs in the \
10^6..=10^9 fuel band — the in-tree `Caixa::template` documentation and \
`caixa-feira` examples carry `:fuel 1_000_000` = 10^6, peer to \
wasmtime's official `Store::set_fuel(1_000_000)` example in the wasmtime \
book; production-shape per-request fuel budgets sit in the 10^7..=10^9 \
band for compute-bound workloads) or omit :fuel to express `no per-call \
fuel budget on this axis` (the wasm-engine then relies entirely on the \
sibling `:wall-clock` cgroup / Kubernetes activeDeadlineSeconds deadline)"
)]
FuelExceedsCap { fuel: u64 },
#[error(
":limits :wall-clock must be > 0 — a zero deadline expires before the call starts; omit the field for unbounded"
)]
WallClockZero,
#[error(
":limits :wall-clock ({wall_clock:?}) carries a sub-millisecond residue the typed `:wall-clock` duration codec cannot round-trip — \
the codec truncates to `as_millis()` before picking the canonical unit, so a value with `subsec_nanos() % 1_000_000 != 0` either \
truncates on first serialize (e.g. `Duration::from_micros(1500)` → \"1ms\" → `Duration::from_millis(1)` ≠ original) or renders \
as \"0s\" the `WallClockZero` arm then rejects on re-validate. Pin an integer-millisecond magnitude in the canonical authoring form \
(`<integer><unit>` for unit ∈ {{ms, s, m, h}}, e.g. `\"500ms\"`, `\"30s\"`, `\"2m\"`, `\"1h\"`) or omit the field for unbounded"
)]
WallClockNotCanonical { wall_clock: Duration },
#[error(
":limits :wall-clock ({wall_clock:?}) exceeds the per-process ceiling \
(LIMITS_WALL_CLOCK_MAX = 1h = 3600s) — a value above this cap turns the typed \
per-call deadline into a nominal-only contract (the wasm-engine's epoch-deadline \
cancellation reaches for a `Duration` so long no realistic synchronous wasm call \
can hit it), and the MESH-COMPOSITION §V \"no infinite blocking\" CSE invariant \
degenerates to enforcement only at the per-Servico cgroup / Kubernetes \
activeDeadlineSeconds layer — far above the per-call granularity the typed \
`:limits :wall-clock` slot is meant to express. Pin a value in 1ms..=1h \
(Envoy / Istio / Linkerd production per-request playbooks all recommend ≤ 60s; \
AWS App Mesh / ingress-nginx typical ≤ 300s; the longest per-request \
`proxy_read_timeout` ingress-nginx documents maxes out at the same 3600s ceiling) \
or omit :wall-clock to express `no per-process deadline on this axis` (the \
deadline then relies entirely on the cluster-level cgroup / pod \
activeDeadlineSeconds bound)"
)]
WallClockExceedsCap { wall_clock: Duration },
#[error(
":limits :cpu must be > 0m — a zero cgroup share starves the process; omit the field for unbounded"
)]
CpuZero,
#[error(
":limits :cpu ({millicores}m) exceeds the per-process ceiling \
(LIMITS_CPU_MILLICORES_MAX = 128_000m = 128 cores) — a value above this cap is \
structurally unschedulable on every commercially-common managed-Kubernetes node \
pool (GKE Standard / EKS managed / AKS default general-purpose SKU ladders top out \
at 128 vCPU per node; AWS m7i.32xlarge / c7i.32xlarge, Azure HBv3-128rs, GCP \
c3-standard-128 all sit at the same 128-vCPU ceiling), so the resulting \
`pleme-computeunit` chart's `resources.requests.cpu` lands as a hint the \
Kubernetes scheduler cannot bind to any node — the pod sits `Pending` indefinitely \
with a `0/N nodes are available: N Insufficient cpu` event, and the typed `:cpu` \
slot becomes an unschedulable contract far from the source caixa.lisp. The \
wasm32-wasip2 single-threaded execution model the canonical caixa Servico targets \
reinforces the structural argument: a single wasm component cannot saturate more \
than one core, so even the Lunatic-style supervised-multi-process host bounds its \
useful CPU request to the host node's vCPU count. Pin a value in 1m..=128000m \
(the canonical caixa Servico runs in the 100m..=2000m band — every in-tree \
example uses 500m; AWS App Mesh / Envoy / Istio per-pod CPU production playbooks \
all sit ≤ 8000m / 8 cores; the longest documented per-Servico CPU request any \
pleme-io substrate playbook recommends maxes at ~16 cores) or omit :cpu to \
express `no per-process CPU hint on this axis` (the cgroup share then defaults to \
the cluster-level `LimitRange` / `ResourceQuota` policy the operator pins on the \
host namespace)"
)]
CpuExceedsCap { millicores: u32 },
}
fn parse_byte_size(s: &str) -> Result<u64, LimitsError> {
crate::render::reject_whitespace(
s,
|byte| LimitsError::whitespace_in_byte_size(s, byte),
|ch| LimitsError::non_ascii_whitespace_in_byte_size(s, ch),
)?;
let s = s.trim();
if s.is_empty() {
return Err(LimitsError::EmptyByteSize(s.into()));
}
let (num_part, unit) = crate::render::split_magnitude_and_alpha_unit(s);
let num_trim = num_part.trim();
let digit_only = crate::render::is_digit_only_magnitude(num_trim);
if !digit_only {
let numeric = num_trim.parse::<f64>().is_ok() || num_trim.parse::<i64>().is_ok();
if numeric {
return Err(LimitsError::non_integer_byte_magnitude(num_trim));
}
return Err(LimitsError::BadByteMagnitude(num_part.into()));
}
if crate::render::is_leading_zero_padded_magnitude(num_trim) {
return Err(LimitsError::leading_zero_byte_magnitude(num_trim));
}
let num: u64 = num_trim.parse::<u64>().map_err(|_| {
LimitsError::BadByteMagnitude(format!("{num_trim} (digit-only magnitude overflows u64)"))
})?;
let multiplier: u64 = match unit.trim() {
"" | "B" => 1,
"KB" => 1_000,
"MB" => 1_000_000,
"GB" => 1_000_000_000,
"KiB" => 1024,
"MiB" => 1024 * 1024,
"GiB" => 1024 * 1024 * 1024,
other => {
return Err(LimitsError::unknown_byte_unit(other));
}
};
num.checked_mul(multiplier).ok_or_else(|| {
LimitsError::BadByteMagnitude(format!(
"{num_trim}{unit_trim} overflows u64 (magnitude × unit > 2^64-1)",
unit_trim = unit.trim()
))
})
}
fn render_byte_size(n: u64) -> String {
const UNITS: &[(u64, &str)] = &[
(1024 * 1024 * 1024, "GiB"),
(1024 * 1024, "MiB"),
(1024, "KiB"),
];
for (mult, label) in UNITS {
if n >= *mult && n.is_multiple_of(*mult) {
return format!("{}{label}", n / mult);
}
}
format!("{n}")
}
fn ser_byte_size<S: Serializer>(v: &Option<u64>, s: S) -> Result<S::Ok, S::Error> {
crate::render::serialize_option_via_str(v, s, render_byte_size)
}
fn de_byte_size<'de, D: Deserializer<'de>>(d: D) -> Result<Option<u64>, D::Error> {
crate::render::deserialize_option_via_str(d, parse_byte_size)
}
fn parse_duration(s: &str) -> Result<Duration, LimitsError> {
crate::render::reject_whitespace(
s,
|byte| LimitsError::whitespace_in_duration(s, byte),
|ch| LimitsError::non_ascii_whitespace_in_duration(s, ch),
)?;
let s = s.trim();
if s.is_empty() {
return Err(LimitsError::EmptyDuration(s.into()));
}
let (num_part, unit) = crate::render::split_magnitude_and_alpha_unit(s);
let num_trim = num_part.trim();
let digit_only = crate::render::is_digit_only_magnitude(num_trim);
if !digit_only {
let numeric = num_trim.parse::<f64>().is_ok() || num_trim.parse::<i64>().is_ok();
if numeric {
return Err(LimitsError::non_integer_duration_magnitude(num_trim));
}
return Err(LimitsError::BadDurationMagnitude(num_part.into()));
}
if crate::render::is_leading_zero_padded_magnitude(num_trim) {
return Err(LimitsError::leading_zero_duration_magnitude(num_trim));
}
let num: u64 = num_trim.parse::<u64>().map_err(|_| {
LimitsError::BadDurationMagnitude(format!(
"{num_trim} (digit-only magnitude overflows u64)"
))
})?;
let unit_trim = unit.trim();
let dur = crate::render::duration_from_integer_magnitude_and_unit(num, unit_trim).map_err(
|e| match e {
crate::render::DurationUnitError::Overflow { multiplier } => {
LimitsError::BadDurationMagnitude(format!(
"{num_trim}{unit_trim} overflows u64 (magnitude × {multiplier} > 2^64-1)"
))
}
crate::render::DurationUnitError::UnknownUnit => {
LimitsError::unknown_duration_unit(unit_trim)
}
},
)?;
Ok(dur)
}
fn ser_duration<S: Serializer>(v: &Option<Duration>, s: S) -> Result<S::Ok, S::Error> {
crate::render::serialize_option_via_str(v, s, crate::supervisor::duration_codec::render)
}
fn de_duration<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Duration>, D::Error> {
crate::render::deserialize_option_via_str(d, parse_duration)
}
fn parse_millicores(s: &str) -> Result<u32, LimitsError> {
crate::render::reject_whitespace(
s,
|byte| LimitsError::whitespace_in_millicores(s, byte),
|ch| LimitsError::non_ascii_whitespace_in_millicores(s, ch),
)?;
let s_trim = s.trim();
if s_trim.is_empty() {
return Err(LimitsError::BadMillicores(s.into()));
}
let (magnitude, has_m_suffix) = match s_trim.strip_suffix('m') {
Some(stripped) => (stripped.trim(), true),
None => (s_trim, false),
};
if magnitude.is_empty() {
return Err(LimitsError::BadMillicores(s.into()));
}
let digit_only = crate::render::is_digit_only_magnitude(magnitude);
if !digit_only {
let numeric = magnitude.parse::<f64>().is_ok() || magnitude.parse::<i64>().is_ok();
if numeric {
return Err(LimitsError::non_integer_millicore_magnitude(magnitude));
}
return Err(LimitsError::BadMillicores(s.into()));
}
if crate::render::is_leading_zero_padded_magnitude(magnitude) {
return Err(LimitsError::leading_zero_millicore_magnitude(magnitude));
}
let num: u32 = magnitude.parse::<u32>().map_err(|_| {
LimitsError::BadMillicores(format!("{magnitude} (digit-only magnitude overflows u32)"))
})?;
if has_m_suffix {
Ok(num)
} else {
num.checked_mul(1000).ok_or_else(|| {
LimitsError::BadMillicores(format!(
"{magnitude} cores × 1000 overflows u32 (write the value in millicores: max \"{}m\")",
u32::MAX
))
})
}
}
fn render_millicores(m: u32) -> String {
format!("{m}m")
}
fn ser_millicores<S: Serializer>(v: &Option<u32>, s: S) -> Result<S::Ok, S::Error> {
crate::render::serialize_option_via_str(v, s, render_millicores)
}
fn de_millicores<'de, D: Deserializer<'de>>(d: D) -> Result<Option<u32>, D::Error> {
crate::render::deserialize_option_via_str(d, parse_millicores)
}
macro_rules! limits_codec_value_only_ctors {
($($ctor:ident => $variant:ident),* $(,)?) => {
impl LimitsError {
$(
#[doc = concat!(
"Construct a [`LimitsError::",
stringify!($variant),
"`] naming the offending magnitude `value`. Folds the ",
"uniform `{ value: value.to_string() }` single-slot ",
"construction onto one substrate primitive so every ",
"wire-up on this variant reads through one dispatch ",
"rather than the pre-lift three-line struct-literal ",
"block."
)]
#[must_use]
pub fn $ctor(value: &str) -> Self {
Self::$variant { value: value.to_string() }
}
)*
}
};
}
limits_codec_value_only_ctors! {
non_integer_byte_magnitude => NonIntegerByteMagnitude,
leading_zero_byte_magnitude => LeadingZeroByteMagnitude,
non_integer_duration_magnitude => NonIntegerDurationMagnitude,
leading_zero_duration_magnitude => LeadingZeroDurationMagnitude,
non_integer_millicore_magnitude => NonIntegerMillicoreMagnitude,
leading_zero_millicore_magnitude => LeadingZeroMillicoreMagnitude,
}
macro_rules! limits_codec_unit_only_ctors {
($($ctor:ident => $variant:ident),* $(,)?) => {
impl LimitsError {
$(
#[doc = concat!(
"Construct a [`LimitsError::",
stringify!($variant),
"`] naming the offending magnitude `unit`. Folds the ",
"uniform `{ unit: unit.to_string() }` single-slot ",
"construction onto one substrate primitive so every ",
"wire-up on this variant reads through one dispatch ",
"rather than the pre-lift two-line struct-literal ",
"block."
)]
#[must_use]
pub fn $ctor(unit: &str) -> Self {
Self::$variant { unit: unit.to_string() }
}
)*
}
};
}
limits_codec_unit_only_ctors! {
unknown_byte_unit => UnknownByteUnit,
unknown_duration_unit => UnknownDurationUnit,
}
macro_rules! limits_codec_value_byte_ctors {
($($ctor:ident => $variant:ident),* $(,)?) => {
impl LimitsError {
$(
#[doc = concat!(
"Construct a [`LimitsError::",
stringify!($variant),
"`] naming the offending magnitude `value` and the ",
"raw ASCII-whitespace `byte` that fell inside it. ",
"Folds the uniform `{ value: value.to_string(), byte }` ",
"two-slot construction onto one substrate primitive so ",
"every wire-up on this variant reads through one dispatch ",
"rather than the pre-lift four-line struct-literal block."
)]
#[must_use]
pub fn $ctor(value: &str, byte: u8) -> Self {
Self::$variant { value: value.to_string(), byte }
}
)*
}
};
}
limits_codec_value_byte_ctors! {
whitespace_in_byte_size => WhitespaceInByteSize,
whitespace_in_duration => WhitespaceInDuration,
whitespace_in_millicores => WhitespaceInMillicores,
}
macro_rules! limits_codec_value_char_ctors {
($($ctor:ident => $variant:ident),* $(,)?) => {
impl LimitsError {
$(
#[doc = concat!(
"Construct a [`LimitsError::",
stringify!($variant),
"`] naming the offending magnitude `value` and the ",
"non-ASCII Unicode whitespace `ch` that fell inside it. ",
"Folds the uniform `{ value: value.to_string(), ch, ",
"codepoint: ch as u32 }` three-slot construction onto ",
"one substrate primitive so every wire-up on this ",
"variant reads through one dispatch rather than the ",
"pre-lift five-line struct-literal block. The load-",
"bearing `codepoint = ch as u32` derivation is pulled ",
"inside the ctor body so every future consumer of the ",
"variant carries it through one canonical path."
)]
#[must_use]
pub fn $ctor(value: &str, ch: char) -> Self {
Self::$variant {
value: value.to_string(),
ch,
codepoint: ch as u32,
}
}
)*
}
};
}
limits_codec_value_char_ctors! {
non_ascii_whitespace_in_byte_size => NonAsciiWhitespaceInByteSize,
non_ascii_whitespace_in_duration => NonAsciiWhitespaceInDuration,
non_ascii_whitespace_in_millicores => NonAsciiWhitespaceInMillicores,
}
macro_rules! limits_scalar_ctors {
($($ctor:ident => $variant:ident { $field:ident: $ty:ty }),* $(,)?) => {
impl LimitsError {
$(
#[doc = concat!(
"Construct a [`LimitsError::",
stringify!($variant),
"`] naming the offending per-`:limits` `",
stringify!($field),
"` scalar. Folds the uniform `Self::",
stringify!($variant),
" { ",
stringify!($field),
" }` one-field `Copy`-pass-through struct-literal onto ",
"one substrate primitive so every per-axis wire-up on ",
"this variant reads through one dispatch — as a bare ",
"function pointer in the `impl FnOnce(",
stringify!($ty),
") -> LimitsError` bracket-closure slot every ",
"`crate::render::require_positive_bounded_*` / ",
"`crate::render::require_positive_canonical_bounded_*` / ",
"`crate::render::require_positive_quantum_multiple_bounded_*` ",
"gate carries — rather than the pre-lift open-coded ",
"one-line closure over the same one-field struct-literal. ",
"`const fn` preserves the `Copy`-pass-through's ",
"zero-runtime-work property verbatim."
)]
#[must_use]
pub const fn $ctor($field: $ty) -> Self {
Self::$variant { $field }
}
)*
}
};
}
limits_scalar_ctors! {
memory_below_wasm32_page => MemoryBelowWasm32Page { bytes: u64 },
memory_exceeds_wasm32_cap => MemoryExceedsWasm32Cap { bytes: u64 },
memory_not_page_multiple => MemoryNotPageMultiple { bytes: u64 },
fuel_exceeds_cap => FuelExceedsCap { fuel: u64 },
wall_clock_not_canonical => WallClockNotCanonical { wall_clock: Duration },
wall_clock_exceeds_cap => WallClockExceedsCap { wall_clock: Duration },
cpu_exceeds_cap => CpuExceedsCap { millicores: u32 },
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_byte_size_known_units() {
assert_eq!(parse_byte_size("64MiB").unwrap(), 64 * 1024 * 1024);
assert_eq!(parse_byte_size("1GiB").unwrap(), 1024 * 1024 * 1024);
assert_eq!(parse_byte_size("512KiB").unwrap(), 512 * 1024);
assert_eq!(parse_byte_size("1KB").unwrap(), 1_000);
assert_eq!(parse_byte_size("1024").unwrap(), 1024);
}
#[test]
fn parse_byte_size_rejects_unknown() {
assert!(matches!(
parse_byte_size("1YiB"),
Err(LimitsError::UnknownByteUnit { .. })
));
assert!(matches!(
parse_byte_size("not-a-number"),
Err(LimitsError::BadByteMagnitude(_))
));
}
#[test]
fn parse_duration_known_units() {
assert_eq!(parse_duration("30s").unwrap(), Duration::from_secs(30));
assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
assert_eq!(parse_duration("2m").unwrap(), Duration::from_secs(120));
assert_eq!(parse_duration("1h").unwrap(), Duration::from_secs(3600));
}
#[test]
fn parse_millicores_both_forms() {
assert_eq!(parse_millicores("500m").unwrap(), 500);
assert_eq!(parse_millicores("2").unwrap(), 2000);
}
#[test]
fn render_byte_size_canonical() {
assert_eq!(render_byte_size(64 * 1024 * 1024), "64MiB");
assert_eq!(render_byte_size(1024 * 1024 * 1024), "1GiB");
assert_eq!(render_byte_size(1024), "1KiB");
assert_eq!(render_byte_size(123), "123");
}
#[test]
fn ser_byte_size_routes_through_render_serialize_option_via_str_canonical() {
for n in [
0u64,
1,
1023,
1024,
64 * 1024 * 1024,
4 * 1024 * 1024 * 1024,
] {
let limits = LimitsSpec {
memory: Some(n),
fuel: None,
wall_clock: None,
cpu: None,
};
let json: serde_json::Value =
serde_json::from_str(&serde_json::to_string(&limits).unwrap()).unwrap();
let emitted = json[crate::render::M2_LIMITS_KEY_MEMORY]
.as_str()
.expect("memory must serialize to a string");
let canonical = render_byte_size(n);
assert_eq!(
emitted, canonical,
"ser_byte_size drifted from render_byte_size via \
serialize_option_via_str on {n} bytes",
);
}
}
#[test]
fn de_byte_size_routes_through_render_deserialize_option_via_str_canonical() {
for raw in ["64MiB", "1024", "0", "4GiB"] {
let field = crate::render::M2_LIMITS_KEY_MEMORY;
let payload = format!("{{\"{field}\":\"{raw}\"}}");
let limits: LimitsSpec =
serde_json::from_str(&payload).expect("canonical memory string must round-trip");
let canonical = parse_byte_size(raw).expect("parse_byte_size accepts canonical form");
assert_eq!(
limits.memory,
Some(canonical),
"de_byte_size drifted from parse_byte_size via \
deserialize_option_via_str on {raw:?}",
);
}
let field = crate::render::M2_LIMITS_KEY_MEMORY;
let null_payload = format!("{{\"{field}\":null}}");
let empty: LimitsSpec = serde_json::from_str(&null_payload)
.expect("null memory field must fold to LimitsSpec::memory = None");
assert_eq!(
empty.memory, None,
"de_byte_size must fold null → None via \
deserialize_option_via_str's null-arm",
);
let bad_payload = format!("{{\"{field}\":\"64XiB\"}}");
let err = serde_json::from_str::<LimitsSpec>(&bad_payload)
.expect_err("bogus memory string must surface the parser's error");
let err_text = err.to_string();
assert!(
err_text.contains("64XiB") || err_text.contains("XiB"),
"de_byte_size must surface parse_byte_size's typed \
LimitsError through serde::de::Error::custom — got \
{err_text:?}",
);
}
#[test]
fn ser_duration_routes_through_supervisor_duration_codec_render_canonical() {
for d in [
Duration::from_secs(30),
Duration::from_millis(500),
Duration::from_secs(120),
Duration::from_secs(3600),
Duration::from_millis(0),
Duration::from_millis(1500),
] {
let limits = LimitsSpec {
memory: None,
fuel: None,
wall_clock: Some(d),
cpu: None,
};
let json: serde_json::Value =
serde_json::from_str(&serde_json::to_string(&limits).unwrap()).unwrap();
let emitted = json[crate::render::M2_LIMITS_KEY_WALL_CLOCK]
.as_str()
.expect("wall_clock must serialize to a string");
let canonical = crate::supervisor::duration_codec::render(d);
assert_eq!(
emitted, canonical,
"ser_duration drifted from supervisor::duration_codec::render on {d:?}",
);
}
}
#[test]
fn parse_byte_size_routes_whitespace_through_render_reject_whitespace_canonical() {
for (raw, byte) in [
(" 64MiB", 0x20u8),
("64MiB ", 0x20u8),
("64 MiB", 0x20u8),
("\t64MiB", 0x09u8),
("64MiB\n", 0x0Au8),
] {
let err = parse_byte_size(raw)
.expect_err("ASCII-whitespace-carrying byte-size input must be rejected");
let via_primitive = crate::render::reject_whitespace::<LimitsError, _, _>(
raw,
|b| LimitsError::WhitespaceInByteSize {
value: raw.into(),
byte: b,
},
|ch| LimitsError::NonAsciiWhitespaceInByteSize {
value: raw.into(),
ch,
codepoint: ch as u32,
},
)
.expect_err("primitive must reject the same ASCII-whitespace shape");
assert_eq!(
err, via_primitive,
"parse_byte_size drifted from crate::render::reject_whitespace \
on ASCII-whitespace input {raw:?}"
);
assert!(
matches!(
err,
LimitsError::WhitespaceInByteSize { value: ref v, byte: b } if v == raw && b == byte
),
"parse_byte_size must surface WhitespaceInByteSize {{ value: {raw:?}, byte: 0x{byte:02x} }}"
);
}
for (raw, expected_ch) in [
("\u{00A0}64MiB", '\u{00A0}'),
("64\u{2003}MiB", '\u{2003}'),
("64MiB\u{2028}", '\u{2028}'),
("\u{3000}64MiB", '\u{3000}'),
] {
let err = parse_byte_size(raw)
.expect_err("non-ASCII-whitespace-carrying byte-size input must be rejected");
let via_primitive = crate::render::reject_whitespace::<LimitsError, _, _>(
raw,
|b| LimitsError::WhitespaceInByteSize {
value: raw.into(),
byte: b,
},
|ch| LimitsError::NonAsciiWhitespaceInByteSize {
value: raw.into(),
ch,
codepoint: ch as u32,
},
)
.expect_err("primitive must reject the same non-ASCII-whitespace shape");
assert_eq!(
err, via_primitive,
"parse_byte_size drifted from crate::render::reject_whitespace \
on non-ASCII-whitespace input {raw:?}"
);
assert!(
matches!(
err,
LimitsError::NonAsciiWhitespaceInByteSize { value: ref v, ch, codepoint }
if v == raw && ch == expected_ch && codepoint == expected_ch as u32
),
"parse_byte_size must surface NonAsciiWhitespaceInByteSize \
{{ value: {raw:?}, ch: {expected_ch:?}, codepoint: {cp:#06X} }}",
cp = expected_ch as u32
);
}
}
#[test]
fn limits_round_trip_through_json() {
let limits = LimitsSpec {
memory: Some(64 * 1024 * 1024),
fuel: Some(1_000_000),
wall_clock: Some(Duration::from_secs(30)),
cpu: Some(500),
};
let json = serde_json::to_string(&limits).unwrap();
let back: LimitsSpec = serde_json::from_str(&json).unwrap();
assert_eq!(limits, back);
}
#[test]
fn empty_limits_serialises_to_empty_object() {
let limits = LimitsSpec::default();
assert!(limits.is_empty());
let json = serde_json::to_string(&limits).unwrap();
assert_eq!(json, "{}");
}
#[test]
fn limits_spec_serde_keys_match_lifted_m2_limits_key_consts() {
let limits = LimitsSpec {
memory: Some(64 * 1024 * 1024),
fuel: Some(1_000_000),
wall_clock: Some(Duration::from_secs(30)),
cpu: Some(500),
};
let json = serde_json::to_string(&limits).unwrap();
for key in [
crate::render::M2_LIMITS_KEY_MEMORY,
crate::render::M2_LIMITS_KEY_FUEL,
crate::render::M2_LIMITS_KEY_WALL_CLOCK,
crate::render::M2_LIMITS_KEY_CPU,
] {
let quoted = format!("\"{key}\"");
assert!(
json.contains("ed),
"serialized LimitsSpec must carry the lifted \
M2_LIMITS_KEY_* byte-sequence {quoted} verbatim in \
the JSON emission (got: {json})",
);
}
}
#[test]
fn m2_limits_key_consts_are_pairwise_distinct() {
let all = [
crate::render::M2_LIMITS_KEY_MEMORY,
crate::render::M2_LIMITS_KEY_FUEL,
crate::render::M2_LIMITS_KEY_WALL_CLOCK,
crate::render::M2_LIMITS_KEY_CPU,
];
for (i, a) in all.iter().enumerate() {
for b in all.iter().skip(i + 1) {
assert_ne!(
a, b,
"M2_LIMITS_KEY_* consts must be pairwise-distinct \
canonical byte-sequences — got `{a}` == `{b}`",
);
}
}
}
#[test]
fn m2_limits_key_consts_are_lower_camel_case_shape() {
for key in [
crate::render::M2_LIMITS_KEY_MEMORY,
crate::render::M2_LIMITS_KEY_FUEL,
crate::render::M2_LIMITS_KEY_WALL_CLOCK,
crate::render::M2_LIMITS_KEY_CPU,
] {
assert!(
!key.is_empty(),
"M2_LIMITS_KEY_* must be non-empty (got {key:?})"
);
let first = key.chars().next().unwrap();
assert!(
first.is_ascii_lowercase(),
"M2_LIMITS_KEY_* must lead with an ASCII-lowercase byte \
(got {key:?}, leads with {first:?})",
);
assert!(
key.chars().all(|c| c.is_ascii_alphanumeric()),
"M2_LIMITS_KEY_* must be ASCII-alphanumeric only \
— no `_` / `-` / `:` / `.` / whitespace (got {key:?})",
);
}
}
#[test]
fn validate_accepts_default_unbounded_limits() {
LimitsSpec::default().validate().unwrap();
}
#[test]
fn validate_accepts_full_nonzero_limits() {
let l = LimitsSpec {
memory: Some(64 * 1024 * 1024),
fuel: Some(1_000_000),
wall_clock: Some(Duration::from_secs(30)),
cpu: Some(500),
};
l.validate().unwrap();
}
#[test]
fn validate_rejects_zero_memory() {
let l = LimitsSpec {
memory: Some(0),
..Default::default()
};
assert_eq!(l.validate().unwrap_err(), LimitsError::MemoryZero);
}
#[test]
fn validate_rejects_zero_fuel() {
let l = LimitsSpec {
fuel: Some(0),
..Default::default()
};
assert_eq!(l.validate().unwrap_err(), LimitsError::FuelZero);
}
#[test]
fn validate_rejects_zero_wall_clock() {
let l = LimitsSpec {
wall_clock: Some(Duration::ZERO),
..Default::default()
};
assert_eq!(l.validate().unwrap_err(), LimitsError::WallClockZero);
}
#[test]
fn validate_rejects_zero_cpu() {
let l = LimitsSpec {
cpu: Some(0),
..Default::default()
};
assert_eq!(l.validate().unwrap_err(), LimitsError::CpuZero);
}
#[test]
fn validate_rejects_first_zero_axis_deterministically() {
let l = LimitsSpec {
memory: Some(0),
fuel: Some(0),
wall_clock: Some(Duration::ZERO),
cpu: Some(0),
};
assert_eq!(l.validate().unwrap_err(), LimitsError::MemoryZero);
}
#[test]
fn wasm32_memory_cap_matches_parsed_4_gib() {
assert_eq!(
parse_byte_size("4GiB").unwrap(),
LIMITS_MEMORY_WASM32_MAX_BYTES
);
assert_eq!(LIMITS_MEMORY_WASM32_MAX_BYTES, 4 * 1024 * 1024 * 1024);
assert_eq!(LIMITS_MEMORY_WASM32_MAX_BYTES, 1u64 << 32);
}
#[test]
fn validate_accepts_memory_at_wasm32_cap() {
let l = LimitsSpec {
memory: Some(LIMITS_MEMORY_WASM32_MAX_BYTES),
..Default::default()
};
l.validate().unwrap();
}
#[test]
fn validate_rejects_memory_one_byte_above_wasm32_cap() {
let bytes = LIMITS_MEMORY_WASM32_MAX_BYTES + 1;
let l = LimitsSpec {
memory: Some(bytes),
..Default::default()
};
assert_eq!(
l.validate().unwrap_err(),
LimitsError::MemoryExceedsWasm32Cap { bytes }
);
}
#[test]
fn validate_rejects_memory_8_gib() {
let bytes = parse_byte_size("8GiB").unwrap();
let l = LimitsSpec {
memory: Some(bytes),
..Default::default()
};
assert_eq!(
l.validate().unwrap_err(),
LimitsError::MemoryExceedsWasm32Cap { bytes }
);
}
#[test]
fn validate_memory_zero_takes_precedence_over_cap_check() {
let l = LimitsSpec {
memory: Some(0),
..Default::default()
};
assert_eq!(l.validate().unwrap_err(), LimitsError::MemoryZero);
}
#[test]
fn validate_rejects_memory_cap_before_other_axes() {
let bytes = LIMITS_MEMORY_WASM32_MAX_BYTES + 1024;
let l = LimitsSpec {
memory: Some(bytes),
fuel: Some(0),
wall_clock: Some(Duration::ZERO),
cpu: Some(0),
};
assert_eq!(
l.validate().unwrap_err(),
LimitsError::MemoryExceedsWasm32Cap { bytes }
);
}
#[test]
fn above_cap_value_still_round_trips_through_serde() {
let l = LimitsSpec {
memory: Some(LIMITS_MEMORY_WASM32_MAX_BYTES + 1),
..Default::default()
};
let json = serde_json::to_string(&l).unwrap();
let back: LimitsSpec = serde_json::from_str(&json).unwrap();
assert_eq!(l, back);
assert!(back.validate().is_err());
}
#[test]
fn wasm32_memory_page_matches_parsed_64_kib() {
assert_eq!(
parse_byte_size("64KiB").unwrap(),
LIMITS_MEMORY_WASM32_PAGE_BYTES
);
assert_eq!(LIMITS_MEMORY_WASM32_PAGE_BYTES, 64 * 1024);
assert_eq!(LIMITS_MEMORY_WASM32_PAGE_BYTES, 1u64 << 16);
assert_eq!(
LIMITS_MEMORY_WASM32_MAX_BYTES / LIMITS_MEMORY_WASM32_PAGE_BYTES,
1u64 << 16,
"the wasm32 page count cap is 2^16 pages exactly",
);
assert_eq!(
LIMITS_MEMORY_WASM32_MAX_BYTES % LIMITS_MEMORY_WASM32_PAGE_BYTES,
0
);
}
#[test]
fn validate_rejects_memory_below_wasm32_page() {
let bytes = parse_byte_size("32KiB").unwrap();
let l = LimitsSpec {
memory: Some(bytes),
..Default::default()
};
assert_eq!(
l.validate().unwrap_err(),
LimitsError::MemoryBelowWasm32Page { bytes }
);
}
#[test]
fn validate_rejects_memory_one_byte_below_page() {
let bytes = LIMITS_MEMORY_WASM32_PAGE_BYTES - 1;
let l = LimitsSpec {
memory: Some(bytes),
..Default::default()
};
assert_eq!(
l.validate().unwrap_err(),
LimitsError::MemoryBelowWasm32Page { bytes }
);
}
#[test]
fn validate_rejects_memory_one_byte() {
let l = LimitsSpec {
memory: Some(1),
..Default::default()
};
assert_eq!(
l.validate().unwrap_err(),
LimitsError::MemoryBelowWasm32Page { bytes: 1 }
);
}
#[test]
fn validate_accepts_memory_at_wasm32_page() {
let l = LimitsSpec {
memory: Some(LIMITS_MEMORY_WASM32_PAGE_BYTES),
..Default::default()
};
l.validate().unwrap();
}
#[test]
fn validate_accepts_multi_page_memory() {
for bytes in [
LIMITS_MEMORY_WASM32_PAGE_BYTES,
2 * LIMITS_MEMORY_WASM32_PAGE_BYTES,
64 * 1024 * 1024,
1024 * 1024 * 1024,
LIMITS_MEMORY_WASM32_MAX_BYTES,
] {
let l = LimitsSpec {
memory: Some(bytes),
..Default::default()
};
l.validate()
.unwrap_or_else(|e| panic!("multi-page {bytes} must validate, got {e:?}"));
}
}
#[test]
fn validate_memory_zero_takes_precedence_over_page_floor() {
let l = LimitsSpec {
memory: Some(0),
..Default::default()
};
assert_eq!(l.validate().unwrap_err(), LimitsError::MemoryZero);
}
#[test]
fn validate_memory_page_floor_takes_precedence_over_other_axes() {
let bytes = LIMITS_MEMORY_WASM32_PAGE_BYTES / 2;
let l = LimitsSpec {
memory: Some(bytes),
fuel: Some(0),
wall_clock: Some(Duration::ZERO),
cpu: Some(0),
};
assert_eq!(
l.validate().unwrap_err(),
LimitsError::MemoryBelowWasm32Page { bytes }
);
}
#[test]
fn memory_page_floor_diagnostic_carries_offending_bytes() {
let l = LimitsSpec {
memory: Some(50_000),
..Default::default()
};
let err = l.validate().unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("50000"),
"diagnostic must carry the offending byte count verbatim (got {msg:?})"
);
assert!(
msg.contains("64 KiB") || msg.contains("65536"),
"diagnostic must name the page-size floor (got {msg:?})"
);
}
#[test]
fn below_page_value_still_round_trips_through_serde() {
let l = LimitsSpec {
memory: Some(LIMITS_MEMORY_WASM32_PAGE_BYTES - 1),
..Default::default()
};
let json = serde_json::to_string(&l).unwrap();
let back: LimitsSpec = serde_json::from_str(&json).unwrap();
assert_eq!(l, back);
assert!(back.validate().is_err());
}
#[test]
fn validate_rejects_memory_one_byte_above_page() {
let bytes = LIMITS_MEMORY_WASM32_PAGE_BYTES + 1;
let l = LimitsSpec {
memory: Some(bytes),
..Default::default()
};
assert_eq!(
l.validate().unwrap_err(),
LimitsError::MemoryNotPageMultiple { bytes }
);
}
#[test]
fn validate_rejects_memory_just_below_two_pages() {
let bytes = 2 * LIMITS_MEMORY_WASM32_PAGE_BYTES - 1;
let l = LimitsSpec {
memory: Some(bytes),
..Default::default()
};
assert_eq!(
l.validate().unwrap_err(),
LimitsError::MemoryNotPageMultiple { bytes }
);
}
#[test]
fn validate_rejects_memory_100000_bytes() {
let bytes = parse_byte_size("100000").unwrap();
let l = LimitsSpec {
memory: Some(bytes),
..Default::default()
};
assert_eq!(
l.validate().unwrap_err(),
LimitsError::MemoryNotPageMultiple { bytes }
);
}
#[test]
fn validate_accepts_every_page_aligned_value_through_serde() {
for s in ["64KiB", "128KiB", "1MiB", "64MiB", "1GiB", "4GiB"] {
let bytes = parse_byte_size(s).unwrap();
assert_eq!(
bytes % LIMITS_MEMORY_WASM32_PAGE_BYTES,
0,
"canonical byte-size codec output {s:?} ({bytes}) must be page-aligned",
);
let l = LimitsSpec {
memory: Some(bytes),
..Default::default()
};
l.validate()
.unwrap_or_else(|e| panic!("canonical {s:?} = {bytes} must validate, got {e:?}"));
}
}
#[test]
fn validate_memory_below_page_takes_precedence_over_page_multiple() {
let l = LimitsSpec {
memory: Some(1),
..Default::default()
};
assert_eq!(
l.validate().unwrap_err(),
LimitsError::MemoryBelowWasm32Page { bytes: 1 }
);
}
#[test]
fn validate_memory_cap_takes_precedence_over_page_multiple() {
let bytes = LIMITS_MEMORY_WASM32_MAX_BYTES + 1;
let l = LimitsSpec {
memory: Some(bytes),
..Default::default()
};
assert_eq!(
l.validate().unwrap_err(),
LimitsError::MemoryExceedsWasm32Cap { bytes }
);
}
#[test]
fn validate_rejects_memory_page_multiple_before_other_axes() {
let bytes = LIMITS_MEMORY_WASM32_PAGE_BYTES + 1;
let l = LimitsSpec {
memory: Some(bytes),
fuel: Some(0),
wall_clock: Some(Duration::ZERO),
cpu: Some(0),
};
assert_eq!(
l.validate().unwrap_err(),
LimitsError::MemoryNotPageMultiple { bytes }
);
}
#[test]
fn memory_page_multiple_diagnostic_carries_offending_bytes() {
let bytes = LIMITS_MEMORY_WASM32_PAGE_BYTES + 12345;
let l = LimitsSpec {
memory: Some(bytes),
..Default::default()
};
let err = l.validate().unwrap_err();
let msg = err.to_string();
assert!(
msg.contains(&bytes.to_string()),
"diagnostic must carry the offending byte count verbatim (got {msg:?})"
);
assert!(
msg.contains("64 KiB") || msg.contains("65536") || msg.contains("page"),
"diagnostic must name the page-size granularity (got {msg:?})"
);
}
#[test]
fn sub_page_residue_value_still_round_trips_through_serde() {
let bytes = LIMITS_MEMORY_WASM32_PAGE_BYTES + 1;
let l = LimitsSpec {
memory: Some(bytes),
..Default::default()
};
let json = serde_json::to_string(&l).unwrap();
let back: LimitsSpec = serde_json::from_str(&json).unwrap();
assert_eq!(l, back);
assert!(back.validate().is_err());
}
#[test]
fn validate_memory_axis_routes_through_quantum_multiple_bounded_helper() {
let quantum = LIMITS_MEMORY_WASM32_PAGE_BYTES;
let cap = LIMITS_MEMORY_WASM32_MAX_BYTES;
let cases: &[(u64, LimitsError)] = &[
(0, LimitsError::MemoryZero),
(1, LimitsError::MemoryBelowWasm32Page { bytes: 1 }),
(
quantum - 1,
LimitsError::MemoryBelowWasm32Page { bytes: quantum - 1 },
),
(
cap + 1,
LimitsError::MemoryExceedsWasm32Cap { bytes: cap + 1 },
),
(
cap + quantum,
LimitsError::MemoryExceedsWasm32Cap {
bytes: cap + quantum,
},
),
(
quantum + 1,
LimitsError::MemoryNotPageMultiple { bytes: quantum + 1 },
),
(
quantum + 12_345,
LimitsError::MemoryNotPageMultiple {
bytes: quantum + 12_345,
},
),
];
for (bytes, expected) in cases {
let l = LimitsSpec {
memory: Some(*bytes),
..Default::default()
};
assert_eq!(
l.validate().unwrap_err(),
*expected,
"memory={bytes} must surface the {expected:?} arm via the substrate helper",
);
}
for bytes in [quantum, quantum * 2, quantum * 100, cap] {
let l = LimitsSpec {
memory: Some(bytes),
..Default::default()
};
l.validate().unwrap();
}
}
#[test]
fn parse_byte_size_rejects_fractional_kib() {
let err = parse_byte_size("1.5KiB").unwrap_err();
assert!(
matches!(err, LimitsError::NonIntegerByteMagnitude { ref value } if value == "1.5"),
"got {err:?}"
);
}
#[test]
fn parse_byte_size_rejects_decimal_shaped_integer() {
let err = parse_byte_size("1.0MiB").unwrap_err();
assert!(
matches!(err, LimitsError::NonIntegerByteMagnitude { ref value } if value == "1.0"),
"got {err:?}"
);
}
#[test]
fn parse_byte_size_rejects_half_gib() {
let err = parse_byte_size("0.5GiB").unwrap_err();
assert!(
matches!(err, LimitsError::NonIntegerByteMagnitude { ref value } if value == "0.5"),
"got {err:?}"
);
}
#[test]
fn parse_byte_size_rejects_scientific_notation_via_unit_arm() {
let err = parse_byte_size("1e3KiB").unwrap_err();
assert!(
matches!(err, LimitsError::UnknownByteUnit { ref unit } if unit == "e3KiB"),
"got {err:?}"
);
}
#[test]
fn parse_byte_size_rejects_leading_plus() {
let err = parse_byte_size("+1024").unwrap_err();
assert!(
matches!(err, LimitsError::NonIntegerByteMagnitude { ref value } if value == "+1024"),
"got {err:?}"
);
}
#[test]
fn parse_byte_size_continues_to_accept_integer_magnitudes() {
assert_eq!(parse_byte_size("1024").unwrap(), 1024);
assert_eq!(parse_byte_size("1KiB").unwrap(), 1024);
assert_eq!(parse_byte_size("64MiB").unwrap(), 64 * 1024 * 1024);
assert_eq!(parse_byte_size("1GiB").unwrap(), 1024 * 1024 * 1024);
assert_eq!(parse_byte_size("1000KB").unwrap(), 1_000_000);
}
#[test]
fn parse_byte_size_round_trips_through_render_for_every_canonical_form() {
for n in [1u64, 1023, 1024, 1536, 64 * 1024 * 1024, 1024 * 1024 * 1024] {
let rendered = render_byte_size(n);
let reparsed = parse_byte_size(&rendered)
.unwrap_or_else(|e| panic!("render({n}) = {rendered:?} must reparse, got {e:?}"));
assert_eq!(
reparsed, n,
"round-trip drift on {n}: rendered={rendered:?}, reparsed={reparsed}",
);
}
}
#[test]
fn parse_byte_size_keeps_bad_magnitude_for_unparseable_input() {
let err = parse_byte_size("abc").unwrap_err();
assert!(
matches!(err, LimitsError::BadByteMagnitude(_)),
"got {err:?}"
);
let err = parse_byte_size("--1").unwrap_err();
assert!(
matches!(err, LimitsError::BadByteMagnitude(_)),
"got {err:?}"
);
}
#[test]
fn parse_byte_size_overflow_surfaces_as_bad_magnitude() {
let err = parse_byte_size("18446744073709551615KiB").unwrap_err();
let LimitsError::BadByteMagnitude(reason) = err else {
panic!("expected BadByteMagnitude(overflow), got other variant");
};
assert!(
reason.contains("overflow"),
"overflow diagnostic must mention overflow (got {reason:?})"
);
}
#[test]
fn parse_byte_size_rejects_leading_zero_magnitude() {
let err = parse_byte_size("064MiB").unwrap_err();
assert!(
matches!(err, LimitsError::LeadingZeroByteMagnitude { ref value } if value == "064"),
"got {err:?}"
);
}
#[test]
fn parse_byte_size_rejects_multi_digit_zero_magnitude() {
let err = parse_byte_size("00MiB").unwrap_err();
assert!(
matches!(err, LimitsError::LeadingZeroByteMagnitude { ref value } if value == "00"),
"got {err:?}"
);
}
#[test]
fn parse_byte_size_rejects_leading_zero_in_gib_unit() {
let err = parse_byte_size("01GiB").unwrap_err();
assert!(
matches!(err, LimitsError::LeadingZeroByteMagnitude { ref value } if value == "01"),
"got {err:?}"
);
}
#[test]
fn parse_byte_size_rejects_leading_zero_in_kib_unit() {
let err = parse_byte_size("0512KiB").unwrap_err();
assert!(
matches!(err, LimitsError::LeadingZeroByteMagnitude { ref value } if value == "0512"),
"got {err:?}"
);
}
#[test]
fn parse_byte_size_rejects_leading_zero_in_decimal_units() {
for (s, expected) in [("0500MB", "0500"), ("01KB", "01"), ("00GB", "00")] {
let err = parse_byte_size(s).unwrap_err();
assert!(
matches!(err, LimitsError::LeadingZeroByteMagnitude { value: ref v } if v == expected),
"got {err:?} for {s:?}"
);
}
}
#[test]
fn parse_byte_size_rejects_leading_zero_bare_integer() {
let err = parse_byte_size("01024").unwrap_err();
assert!(
matches!(err, LimitsError::LeadingZeroByteMagnitude { ref value } if value == "01024"),
"got {err:?}"
);
}
#[test]
fn parse_byte_size_accepts_single_zero_magnitude_at_codec_layer() {
assert_eq!(parse_byte_size("0").unwrap(), 0);
assert_eq!(parse_byte_size("0B").unwrap(), 0);
assert_eq!(parse_byte_size("0KiB").unwrap(), 0);
assert_eq!(parse_byte_size("0MiB").unwrap(), 0);
assert_eq!(parse_byte_size("0GiB").unwrap(), 0);
assert_eq!(parse_byte_size("0KB").unwrap(), 0);
}
#[test]
fn parse_byte_size_accepts_canonical_magnitude_with_leading_one() {
assert_eq!(parse_byte_size("1").unwrap(), 1);
assert_eq!(parse_byte_size("1KiB").unwrap(), 1024);
assert_eq!(parse_byte_size("1MiB").unwrap(), 1024 * 1024);
assert_eq!(parse_byte_size("1GiB").unwrap(), 1024 * 1024 * 1024);
assert_eq!(parse_byte_size("64MiB").unwrap(), 64 * 1024 * 1024);
assert_eq!(parse_byte_size("9").unwrap(), 9);
}
#[test]
fn de_byte_size_rejects_leading_zero_through_serde() {
let json = r#"{"memory":"064MiB"}"#;
let err = serde_json::from_str::<LimitsSpec>(json).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("leading zero"),
"serde diagnostic must surface the leading-zero reason verbatim (got {msg:?})"
);
}
#[test]
fn parse_byte_size_rejects_leading_whitespace() {
let err = parse_byte_size(" 64MiB").unwrap_err();
assert!(
matches!(err, LimitsError::WhitespaceInByteSize { ref value, byte } if value == " 64MiB" && byte == 0x20),
"got {err:?}"
);
let msg = err.to_string();
assert!(
msg.contains("whitespace byte 0x20"),
"diagnostic must surface the offending byte verbatim (got {msg:?})"
);
assert!(
msg.contains("THEORY.md"),
"diagnostic must cite the render-determinism contract (got {msg:?})"
);
}
#[test]
fn parse_byte_size_rejects_trailing_whitespace() {
let err = parse_byte_size("64MiB ").unwrap_err();
assert!(
matches!(err, LimitsError::WhitespaceInByteSize { ref value, byte } if value == "64MiB " && byte == 0x20),
"got {err:?}"
);
}
#[test]
fn parse_byte_size_rejects_internal_whitespace_between_magnitude_and_unit() {
let err = parse_byte_size("64 MiB").unwrap_err();
assert!(
matches!(err, LimitsError::WhitespaceInByteSize { ref value, byte } if value == "64 MiB" && byte == 0x20),
"got {err:?}"
);
}
#[test]
fn parse_byte_size_rejects_tab_byte() {
let err = parse_byte_size("\t64MiB").unwrap_err();
assert!(
matches!(err, LimitsError::WhitespaceInByteSize { ref value, byte } if value == "\t64MiB" && byte == 0x09),
"got {err:?}"
);
}
#[test]
fn parse_byte_size_rejects_trailing_newline() {
let err = parse_byte_size("64MiB\n").unwrap_err();
assert!(
matches!(err, LimitsError::WhitespaceInByteSize { ref value, byte } if value == "64MiB\n" && byte == 0x0a),
"got {err:?}"
);
}
#[test]
fn parse_byte_size_accepts_whitespace_free_canonical_forms() {
assert_eq!(parse_byte_size("64MiB").unwrap(), 64 * 1024 * 1024);
assert_eq!(parse_byte_size("1GiB").unwrap(), 1024 * 1024 * 1024);
assert_eq!(parse_byte_size("512KiB").unwrap(), 512 * 1024);
assert_eq!(parse_byte_size("1KB").unwrap(), 1_000);
assert_eq!(parse_byte_size("1024").unwrap(), 1024);
assert_eq!(parse_byte_size("0").unwrap(), 0);
}
#[test]
fn de_byte_size_rejects_whitespace_through_serde() {
let json = r#"{"memory":" 64MiB"}"#;
let err = serde_json::from_str::<LimitsSpec>(json).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("whitespace byte"),
"serde diagnostic must surface the whitespace reason verbatim (got {msg:?})"
);
assert!(
msg.contains("0x20"),
"serde diagnostic must name the offending byte (got {msg:?})"
);
let json = r#"{"memory":"64MiB"}"#;
let l: LimitsSpec = serde_json::from_str(json).unwrap();
assert_eq!(l.memory, Some(64 * 1024 * 1024));
}
#[test]
fn parse_byte_size_rejects_leading_nbsp() {
let s = "\u{00A0}64MiB";
let err = parse_byte_size(s).unwrap_err();
assert!(
matches!(err, LimitsError::NonAsciiWhitespaceInByteSize { ref value, ch, codepoint } if value == s && ch == '\u{00A0}' && codepoint == 0x00A0),
"got {err:?}"
);
let msg = err.to_string();
assert!(
msg.contains("U+00A0"),
"diagnostic must surface the codepoint verbatim (got {msg:?})"
);
assert!(
msg.contains("THEORY.md"),
"diagnostic must cite the render-determinism contract (got {msg:?})"
);
}
#[test]
fn parse_byte_size_rejects_internal_line_separator() {
let s = "64\u{2028}MiB";
let err = parse_byte_size(s).unwrap_err();
assert!(
matches!(err, LimitsError::NonAsciiWhitespaceInByteSize { ref value, ch, codepoint } if value == s && ch == '\u{2028}' && codepoint == 0x2028),
"got {err:?}"
);
}
#[test]
fn parse_byte_size_rejects_trailing_ideographic_space() {
let s = "64MiB\u{3000}";
let err = parse_byte_size(s).unwrap_err();
assert!(
matches!(err, LimitsError::NonAsciiWhitespaceInByteSize { ref value, ch, codepoint } if value == s && ch == '\u{3000}' && codepoint == 0x3000),
"got {err:?}"
);
}
#[test]
fn parse_byte_size_accepts_ascii_only_canonical_forms_after_unicode_arm() {
assert_eq!(parse_byte_size("64MiB").unwrap(), 64 * 1024 * 1024);
assert_eq!(parse_byte_size("1GiB").unwrap(), 1024 * 1024 * 1024);
assert_eq!(parse_byte_size("512KiB").unwrap(), 512 * 1024);
assert_eq!(parse_byte_size("1024").unwrap(), 1024);
}
#[test]
fn parse_duration_rejects_fractional_seconds() {
let err = parse_duration("1.5s").unwrap_err();
assert!(
matches!(err, LimitsError::NonIntegerDurationMagnitude { ref value } if value == "1.5"),
"got {err:?}"
);
}
#[test]
fn parse_duration_rejects_decimal_shaped_integer() {
let err = parse_duration("1.0s").unwrap_err();
assert!(
matches!(err, LimitsError::NonIntegerDurationMagnitude { ref value } if value == "1.0"),
"got {err:?}"
);
}
#[test]
fn parse_duration_rejects_half_minute() {
let err = parse_duration("0.5m").unwrap_err();
assert!(
matches!(err, LimitsError::NonIntegerDurationMagnitude { ref value } if value == "0.5"),
"got {err:?}"
);
}
#[test]
fn parse_duration_rejects_leading_plus() {
let err = parse_duration("+30s").unwrap_err();
assert!(
matches!(err, LimitsError::NonIntegerDurationMagnitude { ref value } if value == "+30"),
"got {err:?}"
);
}
#[test]
fn parse_duration_rejects_negative_seconds_via_integer_gate() {
let err = parse_duration("-30s").unwrap_err();
assert!(
matches!(err, LimitsError::NonIntegerDurationMagnitude { ref value } if value == "-30"),
"got {err:?}"
);
}
#[test]
fn parse_duration_continues_to_accept_integer_magnitudes() {
assert_eq!(parse_duration("0s").unwrap(), Duration::ZERO);
assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
assert_eq!(parse_duration("30s").unwrap(), Duration::from_secs(30));
assert_eq!(parse_duration("2m").unwrap(), Duration::from_secs(120));
assert_eq!(parse_duration("1h").unwrap(), Duration::from_secs(3600));
assert_eq!(parse_duration("3600").unwrap(), Duration::from_secs(3600));
}
#[test]
fn parse_duration_round_trips_through_render_for_every_canonical_form() {
for d in [
Duration::from_millis(1),
Duration::from_millis(500),
Duration::from_millis(1500),
Duration::from_secs(1),
Duration::from_secs(30),
Duration::from_secs(60),
Duration::from_secs(120),
Duration::from_secs(3600),
] {
let rendered = crate::supervisor::duration_codec::render(d);
let reparsed = parse_duration(&rendered)
.unwrap_or_else(|e| panic!("render({d:?}) = {rendered:?} must reparse, got {e:?}"));
assert_eq!(
reparsed, d,
"round-trip drift on {d:?}: rendered={rendered:?}, reparsed={reparsed:?}",
);
}
}
#[test]
fn parse_duration_keeps_bad_magnitude_for_unparseable_input() {
let err = parse_duration("abcs").unwrap_err();
assert!(
matches!(err, LimitsError::BadDurationMagnitude(_)),
"got {err:?}"
);
let err = parse_duration("--1s").unwrap_err();
assert!(
matches!(err, LimitsError::BadDurationMagnitude(_)),
"got {err:?}"
);
}
#[test]
fn parse_duration_overflow_surfaces_as_bad_magnitude() {
let err = parse_duration("18446744073709551615h").unwrap_err();
let LimitsError::BadDurationMagnitude(reason) = err else {
panic!("expected BadDurationMagnitude(overflow), got other variant");
};
assert!(
reason.contains("overflow"),
"overflow diagnostic must mention overflow (got {reason:?})"
);
}
#[test]
fn parse_duration_rejects_leading_zero_magnitude() {
let err = parse_duration("030s").unwrap_err();
assert!(
matches!(err, LimitsError::LeadingZeroDurationMagnitude { ref value } if value == "030"),
"got {err:?}"
);
}
#[test]
fn parse_duration_rejects_multi_digit_zero_magnitude() {
let err = parse_duration("00s").unwrap_err();
assert!(
matches!(err, LimitsError::LeadingZeroDurationMagnitude { ref value } if value == "00"),
"got {err:?}"
);
}
#[test]
fn parse_duration_rejects_leading_zero_in_hour_window() {
let err = parse_duration("01h").unwrap_err();
assert!(
matches!(err, LimitsError::LeadingZeroDurationMagnitude { ref value } if value == "01"),
"got {err:?}"
);
}
#[test]
fn parse_duration_rejects_leading_zero_bare_integer_as_seconds() {
let err = parse_duration("030").unwrap_err();
assert!(
matches!(err, LimitsError::LeadingZeroDurationMagnitude { ref value } if value == "030"),
"got {err:?}"
);
}
#[test]
fn parse_duration_accepts_single_zero_magnitude_at_codec_layer() {
assert_eq!(parse_duration("0s").unwrap(), Duration::ZERO);
assert_eq!(parse_duration("0ms").unwrap(), Duration::ZERO);
assert_eq!(parse_duration("0m").unwrap(), Duration::ZERO);
assert_eq!(parse_duration("0h").unwrap(), Duration::ZERO);
assert_eq!(parse_duration("0").unwrap(), Duration::ZERO);
}
#[test]
fn parse_duration_accepts_canonical_magnitude_with_leading_one() {
assert_eq!(parse_duration("1ms").unwrap(), Duration::from_millis(1));
assert_eq!(parse_duration("1s").unwrap(), Duration::from_secs(1));
assert_eq!(parse_duration("1m").unwrap(), Duration::from_secs(60));
assert_eq!(parse_duration("1h").unwrap(), Duration::from_secs(3600));
assert_eq!(parse_duration("100ms").unwrap(), Duration::from_millis(100));
assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
}
#[test]
fn parse_duration_rejects_leading_whitespace() {
let err = parse_duration(" 30s").unwrap_err();
assert!(
matches!(err, LimitsError::WhitespaceInDuration { ref value, byte } if value == " 30s" && byte == 0x20),
"got {err:?}"
);
let msg = err.to_string();
assert!(
msg.contains("whitespace byte 0x20"),
"diagnostic must surface the offending byte verbatim (got {msg:?})"
);
assert!(
msg.contains("THEORY.md"),
"diagnostic must cite the render-determinism contract (got {msg:?})"
);
}
#[test]
fn parse_duration_rejects_trailing_whitespace() {
let err = parse_duration("30s ").unwrap_err();
assert!(
matches!(err, LimitsError::WhitespaceInDuration { ref value, byte } if value == "30s " && byte == 0x20),
"got {err:?}"
);
}
#[test]
fn parse_duration_rejects_internal_whitespace_between_magnitude_and_unit() {
let err = parse_duration("30 s").unwrap_err();
assert!(
matches!(err, LimitsError::WhitespaceInDuration { ref value, byte } if value == "30 s" && byte == 0x20),
"got {err:?}"
);
}
#[test]
fn parse_duration_rejects_tab_byte() {
let err = parse_duration("\t30s").unwrap_err();
assert!(
matches!(err, LimitsError::WhitespaceInDuration { ref value, byte } if value == "\t30s" && byte == 0x09),
"got {err:?}"
);
}
#[test]
fn parse_duration_rejects_trailing_newline() {
let err = parse_duration("30s\n").unwrap_err();
assert!(
matches!(err, LimitsError::WhitespaceInDuration { ref value, byte } if value == "30s\n" && byte == 0x0a),
"got {err:?}"
);
}
#[test]
fn parse_duration_accepts_whitespace_free_canonical_forms() {
assert_eq!(parse_duration("30s").unwrap(), Duration::from_secs(30));
assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
assert_eq!(parse_duration("2m").unwrap(), Duration::from_secs(120));
assert_eq!(parse_duration("1h").unwrap(), Duration::from_secs(3600));
assert_eq!(parse_duration("0s").unwrap(), Duration::ZERO);
assert_eq!(parse_duration("3600").unwrap(), Duration::from_secs(3600));
}
#[test]
fn de_duration_rejects_whitespace_through_serde() {
let json = r#"{"wallClock":" 30s"}"#;
let err = serde_json::from_str::<LimitsSpec>(json).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("whitespace byte"),
"serde diagnostic must surface the whitespace reason verbatim (got {msg:?})"
);
assert!(
msg.contains("0x20"),
"serde diagnostic must name the offending byte (got {msg:?})"
);
let json = r#"{"wallClock":"30s"}"#;
let l: LimitsSpec = serde_json::from_str(json).unwrap();
assert_eq!(l.wall_clock, Some(Duration::from_secs(30)));
}
#[test]
fn parse_duration_rejects_leading_nbsp() {
let s = "\u{00A0}30s";
let err = parse_duration(s).unwrap_err();
assert!(
matches!(err, LimitsError::NonAsciiWhitespaceInDuration { ref value, ch, codepoint } if value == s && ch == '\u{00A0}' && codepoint == 0x00A0),
"got {err:?}"
);
let msg = err.to_string();
assert!(
msg.contains("U+00A0"),
"diagnostic must name codepoint (got {msg:?})"
);
}
#[test]
fn parse_duration_rejects_internal_em_space() {
let s = "30\u{2003}s";
let err = parse_duration(s).unwrap_err();
assert!(
matches!(err, LimitsError::NonAsciiWhitespaceInDuration { ref value, ch, codepoint } if value == s && ch == '\u{2003}' && codepoint == 0x2003),
"got {err:?}"
);
}
#[test]
fn parse_duration_accepts_ascii_only_canonical_forms_after_unicode_arm() {
assert_eq!(parse_duration("30s").unwrap(), Duration::from_secs(30));
assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
assert_eq!(parse_duration("1h").unwrap(), Duration::from_secs(3600));
}
#[test]
fn de_duration_rejects_leading_zero_through_serde() {
let json = r#"{"wallClock":"030s"}"#;
let err = serde_json::from_str::<LimitsSpec>(json).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("leading zero"),
"serde diagnostic must surface the leading-zero reason verbatim (got {msg:?})"
);
let json = r#"{"wallClock":"30s"}"#;
let l: LimitsSpec = serde_json::from_str(json).unwrap();
assert_eq!(l.wall_clock, Some(Duration::from_secs(30)));
}
#[test]
fn de_duration_rejects_fractional_value_through_serde() {
let json = r#"{"wallClock":"1.5s"}"#;
let err = serde_json::from_str::<LimitsSpec>(json).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("non-negative integer"),
"serde diagnostic must surface the integer-magnitude reason verbatim \
(got {msg:?})"
);
let json = r#"{"wallClock":"1500ms"}"#;
let l: LimitsSpec = serde_json::from_str(json).unwrap();
assert_eq!(l.wall_clock, Some(Duration::from_millis(1500)));
}
#[test]
fn de_byte_size_rejects_fractional_value_through_serde() {
let json = r#"{"memory":"1.5KiB"}"#;
let err = serde_json::from_str::<LimitsSpec>(json).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("non-negative integer"),
"serde diagnostic must surface the integer-magnitude reason verbatim (got {msg:?})"
);
let json = r#"{"memory":"1536"}"#;
let l: LimitsSpec = serde_json::from_str(json).unwrap();
assert_eq!(l.memory, Some(1536));
}
#[test]
fn parse_millicores_rejects_fractional_magnitude() {
let err = parse_millicores("1.5").unwrap_err();
assert!(
matches!(err, LimitsError::NonIntegerMillicoreMagnitude { ref value } if value == "1.5"),
"got {err:?}"
);
}
#[test]
fn parse_millicores_rejects_decimal_shaped_integer_with_suffix() {
let err = parse_millicores("500.0m").unwrap_err();
assert!(
matches!(err, LimitsError::NonIntegerMillicoreMagnitude { ref value } if value == "500.0"),
"got {err:?}"
);
}
#[test]
fn parse_millicores_rejects_decimal_shaped_integer_bare_core() {
let err = parse_millicores("2.0").unwrap_err();
assert!(
matches!(err, LimitsError::NonIntegerMillicoreMagnitude { ref value } if value == "2.0"),
"got {err:?}"
);
}
#[test]
fn parse_millicores_rejects_leading_plus_sign_with_suffix() {
let err = parse_millicores("+500m").unwrap_err();
assert!(
matches!(err, LimitsError::NonIntegerMillicoreMagnitude { ref value } if value == "+500"),
"got {err:?}"
);
}
#[test]
fn parse_millicores_rejects_leading_plus_sign_bare_core() {
let err = parse_millicores("+2").unwrap_err();
assert!(
matches!(err, LimitsError::NonIntegerMillicoreMagnitude { ref value } if value == "+2"),
"got {err:?}"
);
}
#[test]
fn parse_millicores_rejects_leading_minus_sign() {
let err = parse_millicores("-100m").unwrap_err();
assert!(
matches!(err, LimitsError::NonIntegerMillicoreMagnitude { ref value } if value == "-100"),
"got {err:?}"
);
}
#[test]
fn parse_millicores_rejects_empty_string() {
let err = parse_millicores("").unwrap_err();
assert!(matches!(err, LimitsError::BadMillicores(_)), "got {err:?}");
}
#[test]
fn parse_millicores_rejects_bare_unit_with_no_magnitude() {
let err = parse_millicores("m").unwrap_err();
assert!(matches!(err, LimitsError::BadMillicores(_)), "got {err:?}");
}
#[test]
fn parse_millicores_garbage_still_falls_through_to_bad_millicores() {
let err = parse_millicores("abc").unwrap_err();
assert!(matches!(err, LimitsError::BadMillicores(_)), "got {err:?}");
let err = parse_millicores("--1m").unwrap_err();
assert!(matches!(err, LimitsError::BadMillicores(_)), "got {err:?}");
let err = parse_millicores("foo").unwrap_err();
assert!(matches!(err, LimitsError::BadMillicores(_)), "got {err:?}");
}
#[test]
fn parse_millicores_u32_overflow_with_suffix_surfaces_as_overflow() {
let err = parse_millicores("4294967296m").unwrap_err();
let LimitsError::BadMillicores(reason) = err else {
panic!("expected BadMillicores(overflow), got other variant");
};
assert!(
reason.contains("overflow"),
"overflow diagnostic must mention overflow (got {reason:?})"
);
}
#[test]
fn parse_millicores_bare_core_overflow_surfaces_as_overflow() {
let err = parse_millicores("4294968").unwrap_err();
let LimitsError::BadMillicores(reason) = err else {
panic!("expected BadMillicores(× 1000 overflow), got other variant");
};
assert!(
reason.contains("overflow"),
"× 1000 overflow diagnostic must mention overflow (got {reason:?})"
);
}
#[test]
fn parse_millicores_continues_to_accept_canonical_forms() {
assert_eq!(parse_millicores("0m").unwrap(), 0);
assert_eq!(parse_millicores("500m").unwrap(), 500);
assert_eq!(parse_millicores("1500m").unwrap(), 1500);
assert_eq!(parse_millicores("2000m").unwrap(), 2000);
assert_eq!(parse_millicores("0").unwrap(), 0);
assert_eq!(parse_millicores("2").unwrap(), 2000);
assert_eq!(parse_millicores("4").unwrap(), 4000);
}
#[test]
fn parse_millicores_round_trips_through_render_for_every_canonical_form() {
for m in [0u32, 1, 100, 500, 1000, 1500, 2000, 12345] {
let rendered = render_millicores(m);
let reparsed = parse_millicores(&rendered)
.unwrap_or_else(|e| panic!("render({m}) = {rendered:?} must reparse, got {e:?}"));
assert_eq!(
reparsed, m,
"round-trip drift on {m}: rendered={rendered:?}, reparsed={reparsed}",
);
}
}
#[test]
fn de_millicores_rejects_leading_plus_through_serde() {
let json = r#"{"cpu":"+500m"}"#;
let err = serde_json::from_str::<LimitsSpec>(json).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("non-negative integer"),
"serde diagnostic must surface the integer-magnitude reason verbatim \
(got {msg:?})"
);
let json = r#"{"cpu":"500m"}"#;
let l: LimitsSpec = serde_json::from_str(json).unwrap();
assert_eq!(l.cpu, Some(500));
}
#[test]
fn parse_millicores_rejects_leading_zero_magnitude_with_suffix() {
let err = parse_millicores("0500m").unwrap_err();
assert!(
matches!(err, LimitsError::LeadingZeroMillicoreMagnitude { ref value } if value == "0500"),
"got {err:?}"
);
}
#[test]
fn parse_millicores_rejects_multi_digit_zero_magnitude_with_suffix() {
let err = parse_millicores("00m").unwrap_err();
assert!(
matches!(err, LimitsError::LeadingZeroMillicoreMagnitude { ref value } if value == "00"),
"got {err:?}"
);
}
#[test]
fn parse_millicores_rejects_leading_zero_bare_core() {
let err = parse_millicores("02").unwrap_err();
assert!(
matches!(err, LimitsError::LeadingZeroMillicoreMagnitude { ref value } if value == "02"),
"got {err:?}"
);
}
#[test]
fn parse_millicores_rejects_leading_zero_multi_digit_with_suffix() {
let err = parse_millicores("01500m").unwrap_err();
assert!(
matches!(err, LimitsError::LeadingZeroMillicoreMagnitude { ref value } if value == "01500"),
"got {err:?}"
);
}
#[test]
fn parse_millicores_accepts_single_zero_magnitude_at_codec_layer() {
assert_eq!(parse_millicores("0").unwrap(), 0);
assert_eq!(parse_millicores("0m").unwrap(), 0);
}
#[test]
fn parse_millicores_accepts_canonical_magnitude_with_leading_one() {
assert_eq!(parse_millicores("1m").unwrap(), 1);
assert_eq!(parse_millicores("500m").unwrap(), 500);
assert_eq!(parse_millicores("1500m").unwrap(), 1500);
assert_eq!(parse_millicores("9000m").unwrap(), 9000);
assert_eq!(parse_millicores("1").unwrap(), 1000);
assert_eq!(parse_millicores("2").unwrap(), 2000);
assert_eq!(parse_millicores("9").unwrap(), 9000);
}
#[test]
fn de_millicores_rejects_leading_zero_through_serde() {
let json = r#"{"cpu":"0500m"}"#;
let err = serde_json::from_str::<LimitsSpec>(json).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("leading zero"),
"serde diagnostic must surface the leading-zero reason verbatim \
(got {msg:?})"
);
let json = r#"{"cpu":"500m"}"#;
let l: LimitsSpec = serde_json::from_str(json).unwrap();
assert_eq!(l.cpu, Some(500));
}
#[test]
fn parse_millicores_rejects_leading_whitespace() {
let err = parse_millicores(" 500m").unwrap_err();
assert!(
matches!(err, LimitsError::WhitespaceInMillicores { ref value, byte } if value == " 500m" && byte == 0x20),
"got {err:?}"
);
let msg = err.to_string();
assert!(
msg.contains("whitespace byte 0x20"),
"diagnostic must surface the offending byte verbatim (got {msg:?})"
);
assert!(
msg.contains("THEORY.md"),
"diagnostic must cite the render-determinism contract (got {msg:?})"
);
}
#[test]
fn parse_millicores_rejects_trailing_whitespace() {
let err = parse_millicores("500m ").unwrap_err();
assert!(
matches!(err, LimitsError::WhitespaceInMillicores { ref value, byte } if value == "500m " && byte == 0x20),
"got {err:?}"
);
}
#[test]
fn parse_millicores_rejects_internal_whitespace_between_magnitude_and_unit() {
let err = parse_millicores("500 m").unwrap_err();
assert!(
matches!(err, LimitsError::WhitespaceInMillicores { ref value, byte } if value == "500 m" && byte == 0x20),
"got {err:?}"
);
}
#[test]
fn parse_millicores_rejects_tab_byte() {
let err = parse_millicores("\t500m").unwrap_err();
assert!(
matches!(err, LimitsError::WhitespaceInMillicores { ref value, byte } if value == "\t500m" && byte == 0x09),
"got {err:?}"
);
}
#[test]
fn parse_millicores_rejects_trailing_newline() {
let err = parse_millicores("500m\n").unwrap_err();
assert!(
matches!(err, LimitsError::WhitespaceInMillicores { ref value, byte } if value == "500m\n" && byte == 0x0a),
"got {err:?}"
);
}
#[test]
fn parse_millicores_accepts_whitespace_free_canonical_forms() {
assert_eq!(parse_millicores("500m").unwrap(), 500);
assert_eq!(parse_millicores("2000m").unwrap(), 2000);
assert_eq!(parse_millicores("1m").unwrap(), 1);
assert_eq!(parse_millicores("0m").unwrap(), 0);
assert_eq!(parse_millicores("2").unwrap(), 2000);
assert_eq!(parse_millicores("0").unwrap(), 0);
}
#[test]
fn de_millicores_rejects_whitespace_through_serde() {
let json = r#"{"cpu":" 500m"}"#;
let err = serde_json::from_str::<LimitsSpec>(json).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("whitespace byte"),
"serde diagnostic must surface the whitespace reason verbatim (got {msg:?})"
);
assert!(
msg.contains("0x20"),
"serde diagnostic must name the offending byte (got {msg:?})"
);
let json = r#"{"cpu":"500m"}"#;
let l: LimitsSpec = serde_json::from_str(json).unwrap();
assert_eq!(l.cpu, Some(500));
}
#[test]
fn parse_millicores_rejects_leading_nbsp() {
let s = "\u{00A0}500m";
let err = parse_millicores(s).unwrap_err();
assert!(
matches!(err, LimitsError::NonAsciiWhitespaceInMillicores { ref value, ch, codepoint } if value == s && ch == '\u{00A0}' && codepoint == 0x00A0),
"got {err:?}"
);
let msg = err.to_string();
assert!(
msg.contains("U+00A0"),
"diagnostic must surface the codepoint verbatim (got {msg:?})"
);
assert!(
msg.contains("THEORY.md"),
"diagnostic must cite the render-determinism contract (got {msg:?})"
);
}
#[test]
fn parse_millicores_rejects_internal_em_space() {
let s = "500\u{2003}m";
let err = parse_millicores(s).unwrap_err();
assert!(
matches!(err, LimitsError::NonAsciiWhitespaceInMillicores { ref value, ch, codepoint } if value == s && ch == '\u{2003}' && codepoint == 0x2003),
"got {err:?}"
);
}
#[test]
fn parse_millicores_rejects_trailing_line_separator() {
let s = "500m\u{2028}";
let err = parse_millicores(s).unwrap_err();
assert!(
matches!(err, LimitsError::NonAsciiWhitespaceInMillicores { ref value, ch, codepoint } if value == s && ch == '\u{2028}' && codepoint == 0x2028),
"got {err:?}"
);
}
#[test]
fn parse_millicores_accepts_ascii_only_canonical_forms_after_unicode_arm() {
assert_eq!(parse_millicores("500m").unwrap(), 500);
assert_eq!(parse_millicores("2000m").unwrap(), 2000);
assert_eq!(parse_millicores("1m").unwrap(), 1);
assert_eq!(parse_millicores("2").unwrap(), 2000);
}
#[test]
fn validate_rejects_sub_millisecond_wall_clock() {
let l = LimitsSpec {
wall_clock: Some(Duration::from_micros(1500)),
..Default::default()
};
match l.validate().unwrap_err() {
LimitsError::WallClockNotCanonical { wall_clock } => {
assert_eq!(wall_clock, Duration::from_micros(1500));
}
other => panic!("expected WallClockNotCanonical, got {other:?}"),
}
}
#[test]
fn validate_rejects_one_nanosecond_wall_clock() {
let l = LimitsSpec {
wall_clock: Some(Duration::from_nanos(1)),
..Default::default()
};
match l.validate().unwrap_err() {
LimitsError::WallClockNotCanonical { wall_clock } => {
assert_eq!(wall_clock, Duration::from_nanos(1));
}
other => panic!("expected WallClockNotCanonical, got {other:?}"),
}
}
#[test]
fn validate_rejects_nanosecond_past_canonical_boundary() {
let w = Duration::from_nanos(1_000_001);
let l = LimitsSpec {
wall_clock: Some(w),
..Default::default()
};
assert_eq!(
l.validate().unwrap_err(),
LimitsError::WallClockNotCanonical { wall_clock: w }
);
}
#[test]
fn validate_accepts_integer_millisecond_wall_clock_values() {
for w in [
Duration::from_millis(1),
Duration::from_millis(500),
Duration::from_millis(1500),
Duration::from_secs(1),
Duration::from_secs(30),
Duration::from_secs(60),
Duration::from_secs(120),
Duration::from_secs(3600),
] {
let l = LimitsSpec {
wall_clock: Some(w),
..Default::default()
};
l.validate()
.unwrap_or_else(|e| panic!("integer-ms {w:?} must validate, got {e:?}"));
}
}
#[test]
fn validate_wall_clock_zero_takes_precedence_over_canonical_gate() {
let l = LimitsSpec {
wall_clock: Some(Duration::ZERO),
..Default::default()
};
assert_eq!(l.validate().unwrap_err(), LimitsError::WallClockZero);
}
#[test]
fn wall_clock_canonical_diagnostic_carries_offending_duration() {
let w = Duration::from_micros(500);
let l = LimitsSpec {
wall_clock: Some(w),
..Default::default()
};
let err = l.validate().unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("500"),
"diagnostic must carry the offending magnitude verbatim (got {msg:?})"
);
}
#[test]
fn wall_clock_validated_value_round_trips_through_codec() {
for w in [
Duration::from_millis(1),
Duration::from_millis(1500),
Duration::from_secs(30),
Duration::from_secs(3600),
] {
let l = LimitsSpec {
wall_clock: Some(w),
..Default::default()
};
l.validate().unwrap();
let json = serde_json::to_string(&l).unwrap();
let back: LimitsSpec = serde_json::from_str(&json).unwrap();
assert_eq!(
back.wall_clock, l.wall_clock,
"every validated :wall-clock must round-trip losslessly through the codec"
);
}
}
#[test]
fn validate_rejects_wall_clock_above_cap() {
let w = LIMITS_WALL_CLOCK_MAX + Duration::from_secs(1);
let l = LimitsSpec {
wall_clock: Some(w),
..Default::default()
};
assert_eq!(
l.validate().unwrap_err(),
LimitsError::WallClockExceedsCap { wall_clock: w }
);
}
#[test]
fn validate_rejects_wall_clock_one_millisecond_above_cap() {
let w = LIMITS_WALL_CLOCK_MAX + Duration::from_millis(1);
let l = LimitsSpec {
wall_clock: Some(w),
..Default::default()
};
assert_eq!(
l.validate().unwrap_err(),
LimitsError::WallClockExceedsCap { wall_clock: w }
);
}
#[test]
fn validate_rejects_wall_clock_far_above_cap() {
for w in [
Duration::from_secs(86_400), Duration::from_secs(604_800), Duration::from_secs(1_000_000), ] {
let l = LimitsSpec {
wall_clock: Some(w),
..Default::default()
};
assert_eq!(
l.validate().unwrap_err(),
LimitsError::WallClockExceedsCap { wall_clock: w }
);
}
}
#[test]
fn validate_accepts_wall_clock_at_cap() {
let l = LimitsSpec {
wall_clock: Some(LIMITS_WALL_CLOCK_MAX),
..Default::default()
};
l.validate()
.expect("wall_clock == LIMITS_WALL_CLOCK_MAX must validate");
}
#[test]
fn validate_accepts_wall_clock_typical_values() {
for w in [
Duration::from_millis(1),
Duration::from_millis(500),
Duration::from_secs(1),
Duration::from_secs(10),
Duration::from_secs(15), Duration::from_secs(30),
Duration::from_secs(60), Duration::from_secs(300), Duration::from_secs(900), Duration::from_secs(1800),
Duration::from_secs(3600), ] {
let l = LimitsSpec {
wall_clock: Some(w),
..Default::default()
};
l.validate()
.unwrap_or_else(|e| panic!("wall_clock={w:?} must validate; got {e:?}"));
}
}
#[test]
fn wall_clock_zero_takes_precedence_over_cap() {
let l = LimitsSpec {
wall_clock: Some(Duration::ZERO),
..Default::default()
};
assert_eq!(
l.validate().unwrap_err(),
LimitsError::WallClockZero,
"Duration::ZERO must surface the zero-floor diagnostic, not the cap diagnostic"
);
}
#[test]
fn wall_clock_canonical_takes_precedence_over_cap() {
let w = LIMITS_WALL_CLOCK_MAX + Duration::from_nanos(1);
let l = LimitsSpec {
wall_clock: Some(w),
..Default::default()
};
assert_eq!(
l.validate().unwrap_err(),
LimitsError::WallClockNotCanonical { wall_clock: w },
"sub-ms above-cap value must surface the canonical-form diagnostic, not the cap diagnostic"
);
}
#[test]
fn wall_clock_cap_diagnostic_carries_offending_value() {
let w = Duration::from_secs(7200); let l = LimitsSpec {
wall_clock: Some(w),
..Default::default()
};
let err = l.validate().unwrap_err();
assert!(
matches!(err, LimitsError::WallClockExceedsCap { wall_clock } if wall_clock == w),
"got {err:?}"
);
let msg = err.to_string();
assert!(
msg.contains("7200"),
":limits :wall-clock cap diagnostic must carry the offending value verbatim (got: {msg})"
);
}
#[test]
fn wall_clock_cap_pins_canonical_value() {
assert_eq!(LIMITS_WALL_CLOCK_MAX, Duration::from_secs(3600));
assert_eq!(LIMITS_WALL_CLOCK_MAX.as_millis(), 3_600_000);
assert_eq!(LIMITS_WALL_CLOCK_MAX, crate::POLICY_TIMEOUT_MAX);
assert_eq!(LIMITS_WALL_CLOCK_MAX, crate::POLICY_BREAKER_WINDOW_MAX);
}
#[test]
fn wall_clock_cap_value_round_trips_through_codec() {
let l = LimitsSpec {
wall_clock: Some(LIMITS_WALL_CLOCK_MAX),
..Default::default()
};
let json = serde_json::to_string(&l).unwrap();
assert!(
json.contains("\"1h\""),
"the LIMITS_WALL_CLOCK_MAX value must render to the canonical \"1h\" form (got: {json})"
);
let back: LimitsSpec = serde_json::from_str(&json).unwrap();
assert_eq!(back.wall_clock, Some(LIMITS_WALL_CLOCK_MAX));
l.validate()
.expect("LIMITS_WALL_CLOCK_MAX itself must pass validate");
}
#[test]
fn validate_rejects_cpu_above_cap() {
let m = LIMITS_CPU_MILLICORES_MAX + 1;
let l = LimitsSpec {
cpu: Some(m),
..Default::default()
};
assert_eq!(
l.validate().unwrap_err(),
LimitsError::CpuExceedsCap { millicores: m }
);
}
#[test]
fn validate_rejects_cpu_far_above_cap() {
for m in [1_000_000_u32, 10_000_000, u32::MAX] {
let l = LimitsSpec {
cpu: Some(m),
..Default::default()
};
assert_eq!(
l.validate().unwrap_err(),
LimitsError::CpuExceedsCap { millicores: m }
);
}
}
#[test]
fn validate_accepts_cpu_at_cap() {
let l = LimitsSpec {
cpu: Some(LIMITS_CPU_MILLICORES_MAX),
..Default::default()
};
l.validate()
.expect("cpu == LIMITS_CPU_MILLICORES_MAX must validate");
}
#[test]
fn validate_accepts_cpu_typical_values() {
for m in [
1_u32, 100, 500, 1_000, 2_000, 4_000, 8_000, 16_000, 32_000, 64_000, 128_000, ] {
let l = LimitsSpec {
cpu: Some(m),
..Default::default()
};
l.validate()
.unwrap_or_else(|e| panic!("cpu={m}m must validate; got {e:?}"));
}
}
#[test]
fn cpu_zero_takes_precedence_over_cap() {
let l = LimitsSpec {
cpu: Some(0),
..Default::default()
};
assert_eq!(
l.validate().unwrap_err(),
LimitsError::CpuZero,
"Some(0) must surface the zero-floor diagnostic, not the cap diagnostic"
);
}
#[test]
fn validate_rejects_cpu_cap_after_earlier_axes() {
let l = LimitsSpec {
memory: Some(0),
fuel: None,
wall_clock: None,
cpu: Some(LIMITS_CPU_MILLICORES_MAX + 1),
};
assert_eq!(
l.validate().unwrap_err(),
LimitsError::MemoryZero,
"earlier-axis violation must take precedence over later-axis cap violation"
);
}
#[test]
fn cpu_cap_diagnostic_carries_offending_value() {
let m = 256_000_u32; let l = LimitsSpec {
cpu: Some(m),
..Default::default()
};
let err = l.validate().unwrap_err();
assert!(
matches!(err, LimitsError::CpuExceedsCap { millicores } if millicores == m),
"got {err:?}"
);
let msg = err.to_string();
assert!(
msg.contains("256000"),
":limits :cpu cap diagnostic must carry the offending value verbatim (got: {msg})"
);
}
#[test]
fn cpu_cap_pins_canonical_value() {
assert_eq!(LIMITS_CPU_MILLICORES_MAX, 128_000);
assert_eq!(LIMITS_CPU_MILLICORES_MAX, 128 * 1000);
}
#[test]
fn cpu_cap_value_round_trips_through_codec() {
let l = LimitsSpec {
cpu: Some(LIMITS_CPU_MILLICORES_MAX),
..Default::default()
};
let json = serde_json::to_string(&l).unwrap();
assert!(
json.contains("\"128000m\""),
"the LIMITS_CPU_MILLICORES_MAX value must render to the canonical \"128000m\" form (got: {json})"
);
let back: LimitsSpec = serde_json::from_str(&json).unwrap();
assert_eq!(back.cpu, Some(LIMITS_CPU_MILLICORES_MAX));
l.validate()
.expect("LIMITS_CPU_MILLICORES_MAX itself must pass validate");
}
#[test]
fn validate_rejects_fuel_above_cap() {
let f = LIMITS_FUEL_MAX + 1;
let l = LimitsSpec {
fuel: Some(f),
..Default::default()
};
assert_eq!(
l.validate().unwrap_err(),
LimitsError::FuelExceedsCap { fuel: f }
);
}
#[test]
fn validate_rejects_fuel_far_above_cap() {
for f in [LIMITS_FUEL_MAX * 10, LIMITS_FUEL_MAX * 1_000, u64::MAX] {
let l = LimitsSpec {
fuel: Some(f),
..Default::default()
};
assert_eq!(
l.validate().unwrap_err(),
LimitsError::FuelExceedsCap { fuel: f }
);
}
}
#[test]
fn validate_accepts_fuel_at_cap() {
let l = LimitsSpec {
fuel: Some(LIMITS_FUEL_MAX),
..Default::default()
};
l.validate().expect("fuel == LIMITS_FUEL_MAX must validate");
}
#[test]
fn validate_accepts_fuel_typical_values() {
for f in [
1_u64, 1_000, 1_000_000, 10_000_000, 100_000_000, 1_000_000_000, 100_000_000_000, 500_000_000_000, 1_000_000_000_000, ] {
let l = LimitsSpec {
fuel: Some(f),
..Default::default()
};
l.validate()
.unwrap_or_else(|e| panic!("fuel={f} must validate; got {e:?}"));
}
}
#[test]
fn fuel_zero_takes_precedence_over_cap() {
let l = LimitsSpec {
fuel: Some(0),
..Default::default()
};
assert_eq!(
l.validate().unwrap_err(),
LimitsError::FuelZero,
"Some(0) must surface the zero-floor diagnostic, not the cap diagnostic"
);
}
#[test]
fn validate_rejects_fuel_cap_after_earlier_axes() {
let l = LimitsSpec {
memory: Some(0),
fuel: Some(LIMITS_FUEL_MAX + 1),
wall_clock: None,
cpu: None,
};
assert_eq!(
l.validate().unwrap_err(),
LimitsError::MemoryZero,
"earlier-axis violation must take precedence over later-axis cap violation"
);
}
#[test]
fn validate_rejects_fuel_cap_before_later_axes() {
let l = LimitsSpec {
memory: None,
fuel: Some(LIMITS_FUEL_MAX + 1),
wall_clock: Some(Duration::ZERO),
cpu: Some(0),
};
assert_eq!(
l.validate().unwrap_err(),
LimitsError::FuelExceedsCap {
fuel: LIMITS_FUEL_MAX + 1
},
":fuel cap diagnostic must take precedence over later-axis zero-floor diagnostics"
);
}
#[test]
fn fuel_cap_diagnostic_carries_offending_value() {
let f = 5_000_000_000_000_u64; let l = LimitsSpec {
fuel: Some(f),
..Default::default()
};
let err = l.validate().unwrap_err();
assert!(
matches!(err, LimitsError::FuelExceedsCap { fuel } if fuel == f),
"got {err:?}"
);
let msg = err.to_string();
assert!(
msg.contains("5000000000000"),
":limits :fuel cap diagnostic must carry the offending value verbatim (got: {msg})"
);
}
#[test]
fn fuel_cap_pins_canonical_value() {
assert_eq!(LIMITS_FUEL_MAX, 1_000_000_000_000);
assert_eq!(LIMITS_FUEL_MAX, 10_u64.pow(12));
}
#[test]
fn fuel_cap_value_round_trips_through_serde() {
let l = LimitsSpec {
fuel: Some(LIMITS_FUEL_MAX),
..Default::default()
};
let json = serde_json::to_string(&l).unwrap();
assert!(
json.contains("1000000000000"),
"the LIMITS_FUEL_MAX value must render verbatim as the bare integer 10^12 \
(got: {json})"
);
let back: LimitsSpec = serde_json::from_str(&json).unwrap();
assert_eq!(back.fuel, Some(LIMITS_FUEL_MAX));
l.validate()
.expect("LIMITS_FUEL_MAX itself must pass validate");
}
#[test]
fn limits_memory_returns_option_u64_byte_equal_across_permutations() {
for memory in [
None,
Some(LIMITS_MEMORY_WASM32_PAGE_BYTES),
Some(64 * 1024 * 1024),
] {
let l = LimitsSpec {
memory,
..LimitsSpec::default()
};
assert_eq!(
l.memory(),
memory,
"LimitsSpec::memory must return :limits :memory verbatim \
(got {:?}, expected {memory:?})",
l.memory(),
);
assert_eq!(
l.memory(),
l.memory,
"LimitsSpec::memory must byte-equal the raw .memory \
field access across every value in the accept-set",
);
}
}
#[test]
fn limits_is_empty_memory_arm_routes_through_accessor() {
let empty = LimitsSpec::default();
assert!(
empty.is_empty(),
"LimitsSpec::default() must be is_empty() — every axis \
defaults to None",
);
for memory in [
Some(LIMITS_MEMORY_WASM32_PAGE_BYTES),
Some(64 * 1024 * 1024),
Some(LIMITS_MEMORY_WASM32_MAX_BYTES),
] {
let l = LimitsSpec {
memory,
..LimitsSpec::default()
};
assert!(
!l.is_empty(),
"LimitsSpec::is_empty must return false when :memory \
is {memory:?} — the emptiness predicate reads \"any \
axis carries a value\", not \"any axis carries a \
value above a threshold\"",
);
assert_eq!(
l.memory().is_none(),
l.is_empty(),
"when :memory is the only set axis, is_empty() must \
equal memory().is_none() — the accessor and the \
emptiness predicate must route through the same \
substrate-primitive typed dispatch on the :memory \
arm",
);
}
}
#[test]
fn limits_memory_projects_option_u64_by_copy() {
for memory in [
None,
Some(LIMITS_MEMORY_WASM32_PAGE_BYTES),
Some(64 * 1024 * 1024),
Some(LIMITS_MEMORY_WASM32_MAX_BYTES),
] {
let l = LimitsSpec {
memory,
..LimitsSpec::default()
};
let first = l.memory();
let second = l.memory();
assert_eq!(
first, second,
"LimitsSpec::memory must be idempotent — two \
successive calls on the same &self must return the \
same Option<u64>",
);
assert_eq!(
first, memory,
"LimitsSpec::memory must return :limits :memory \
verbatim by copy — got {first:?}, expected {memory:?}",
);
}
}
#[test]
#[allow(clippy::too_many_lines)]
fn validate_memory_arms_route_through_lifted_memory_accessor() {
for memory in [
None,
Some(0), Some(1), Some(LIMITS_MEMORY_WASM32_PAGE_BYTES - 1), Some(LIMITS_MEMORY_WASM32_PAGE_BYTES), Some(LIMITS_MEMORY_WASM32_PAGE_BYTES + 1), Some(2 * LIMITS_MEMORY_WASM32_PAGE_BYTES), Some(LIMITS_MEMORY_WASM32_MAX_BYTES), Some(LIMITS_MEMORY_WASM32_MAX_BYTES + 1), ] {
let l = LimitsSpec {
memory,
..LimitsSpec::default()
};
assert_eq!(
l.memory(),
l.memory,
"LimitsSpec::memory() must byte-equal the raw \
.memory field for {memory:?} — an accessor detour \
that dropped the raw slot's Option<u64> verbatim \
would silently split validate's :memory arms from \
every peer emit-site consumer that also routes \
through the accessor (the future wasmtime \
Store::limiter wire path, the caixa-helm \
resources.limits.memory materializer)",
);
let first = l.validate();
let second = l.validate();
assert_eq!(
first, second,
"LimitsSpec::validate must be idempotent on :memory \
{memory:?} — two successive calls must surface the \
same variant/Ok discriminant, catching any accessor \
detour that would introduce a value-dependent side \
effect on the &self projection",
);
}
assert_eq!(
LimitsSpec {
memory: Some(0),
..LimitsSpec::default()
}
.validate(),
Err(LimitsError::MemoryZero),
"MemoryZero must fire on Some(0) via the accessor projection",
);
assert_eq!(
LimitsSpec {
memory: Some(1),
..LimitsSpec::default()
}
.validate(),
Err(LimitsError::MemoryBelowWasm32Page { bytes: 1 }),
"MemoryBelowWasm32Page must fire on Some(1) via the accessor projection",
);
assert_eq!(
LimitsSpec {
memory: Some(LIMITS_MEMORY_WASM32_MAX_BYTES + 1),
..LimitsSpec::default()
}
.validate(),
Err(LimitsError::MemoryExceedsWasm32Cap {
bytes: LIMITS_MEMORY_WASM32_MAX_BYTES + 1
}),
"MemoryExceedsWasm32Cap must fire on one-past-cap via the accessor projection",
);
assert_eq!(
LimitsSpec {
memory: Some(LIMITS_MEMORY_WASM32_PAGE_BYTES + 1),
..LimitsSpec::default()
}
.validate(),
Err(LimitsError::MemoryNotPageMultiple {
bytes: LIMITS_MEMORY_WASM32_PAGE_BYTES + 1
}),
"MemoryNotPageMultiple must fire on one-past-page-floor via the accessor projection",
);
assert_eq!(
LimitsSpec {
memory: Some(LIMITS_MEMORY_WASM32_PAGE_BYTES),
..LimitsSpec::default()
}
.validate(),
Ok(()),
"at-page-floor must pass validate via the accessor projection",
);
assert_eq!(
LimitsSpec {
memory: Some(LIMITS_MEMORY_WASM32_MAX_BYTES),
..LimitsSpec::default()
}
.validate(),
Ok(()),
"at-cap must pass validate via the accessor projection",
);
}
#[test]
fn limits_fuel_returns_option_u64_byte_equal_across_permutations() {
for fuel in [None, Some(1_u64), Some(1_000_000_u64)] {
let l = LimitsSpec {
fuel,
..LimitsSpec::default()
};
assert_eq!(
l.fuel(),
fuel,
"LimitsSpec::fuel must return :limits :fuel verbatim \
(got {:?}, expected {fuel:?})",
l.fuel(),
);
assert_eq!(
l.fuel(),
l.fuel,
"LimitsSpec::fuel must byte-equal the raw .fuel \
field access across every value in the accept-set",
);
}
}
#[test]
fn limits_is_empty_fuel_arm_routes_through_accessor() {
let empty = LimitsSpec::default();
assert!(
empty.is_empty(),
"LimitsSpec::default() must be is_empty() — every axis \
defaults to None",
);
for fuel in [Some(1_u64), Some(1_000_000_u64), Some(LIMITS_FUEL_MAX)] {
let l = LimitsSpec {
fuel,
..LimitsSpec::default()
};
assert!(
!l.is_empty(),
"LimitsSpec::is_empty must return false when :fuel \
is {fuel:?} — the emptiness predicate reads \"any \
axis carries a value\", not \"any axis carries a \
value above a threshold\"",
);
assert_eq!(
l.fuel().is_none(),
l.is_empty(),
"when :fuel is the only set axis, is_empty() must \
equal fuel().is_none() — the accessor and the \
emptiness predicate must route through the same \
substrate-primitive typed dispatch on the :fuel \
arm",
);
}
}
#[test]
fn limits_fuel_projects_option_u64_by_copy() {
for fuel in [
None,
Some(1_u64),
Some(1_000_000_u64),
Some(LIMITS_FUEL_MAX),
] {
let l = LimitsSpec {
fuel,
..LimitsSpec::default()
};
let first = l.fuel();
let second = l.fuel();
assert_eq!(
first, second,
"LimitsSpec::fuel must be idempotent — two \
successive calls on the same &self must return the \
same Option<u64>",
);
assert_eq!(
first, fuel,
"LimitsSpec::fuel must return :limits :fuel \
verbatim by copy — got {first:?}, expected {fuel:?}",
);
}
}
#[test]
fn limits_wall_clock_returns_option_duration_byte_equal_across_permutations() {
for wall_clock in [
None,
Some(Duration::from_millis(1)),
Some(Duration::from_secs(30)),
] {
let l = LimitsSpec {
wall_clock,
..LimitsSpec::default()
};
assert_eq!(
l.wall_clock(),
wall_clock,
"LimitsSpec::wall_clock must return :limits :wall-clock verbatim \
(got {:?}, expected {wall_clock:?})",
l.wall_clock(),
);
assert_eq!(
l.wall_clock(),
l.wall_clock,
"LimitsSpec::wall_clock must byte-equal the raw .wall_clock \
field access across every value in the accept-set",
);
}
}
#[test]
fn limits_is_empty_wall_clock_arm_routes_through_accessor() {
let empty = LimitsSpec::default();
assert!(
empty.is_empty(),
"LimitsSpec::default() must be is_empty() — every axis \
defaults to None",
);
for wall_clock in [
Some(Duration::from_millis(1)),
Some(Duration::from_secs(30)),
Some(LIMITS_WALL_CLOCK_MAX),
] {
let l = LimitsSpec {
wall_clock,
..LimitsSpec::default()
};
assert!(
!l.is_empty(),
"LimitsSpec::is_empty must return false when :wall-clock \
is {wall_clock:?} — the emptiness predicate reads \"any \
axis carries a value\", not \"any axis carries a \
value above a threshold\"",
);
assert_eq!(
l.wall_clock().is_none(),
l.is_empty(),
"when :wall-clock is the only set axis, is_empty() must \
equal wall_clock().is_none() — the accessor and the \
emptiness predicate must route through the same \
substrate-primitive typed dispatch on the :wall-clock \
arm",
);
}
}
#[test]
fn limits_wall_clock_projects_option_duration_by_copy() {
for wall_clock in [
None,
Some(Duration::from_millis(1)),
Some(Duration::from_secs(30)),
Some(LIMITS_WALL_CLOCK_MAX),
] {
let l = LimitsSpec {
wall_clock,
..LimitsSpec::default()
};
let first = l.wall_clock();
let second = l.wall_clock();
assert_eq!(
first, second,
"LimitsSpec::wall_clock must be idempotent — two \
successive calls on the same &self must return the \
same Option<Duration>",
);
assert_eq!(
first, wall_clock,
"LimitsSpec::wall_clock must return :limits :wall-clock \
verbatim by copy — got {first:?}, expected {wall_clock:?}",
);
}
}
#[test]
fn limits_cpu_returns_option_u32_byte_equal_across_permutations() {
for cpu in [None, Some(1_u32), Some(500_u32)] {
let l = LimitsSpec {
cpu,
..LimitsSpec::default()
};
assert_eq!(
l.cpu(),
cpu,
"LimitsSpec::cpu must return :limits :cpu verbatim \
(got {:?}, expected {cpu:?})",
l.cpu(),
);
assert_eq!(
l.cpu(),
l.cpu,
"LimitsSpec::cpu must byte-equal the raw .cpu \
field access across every value in the accept-set",
);
}
}
#[test]
fn limits_is_empty_cpu_arm_routes_through_accessor() {
let empty = LimitsSpec::default();
assert!(
empty.is_empty(),
"LimitsSpec::default() must be is_empty() — every axis \
defaults to None",
);
for cpu in [Some(1_u32), Some(500_u32), Some(LIMITS_CPU_MILLICORES_MAX)] {
let l = LimitsSpec {
cpu,
..LimitsSpec::default()
};
assert!(
!l.is_empty(),
"LimitsSpec::is_empty must return false when :cpu \
is {cpu:?} — the emptiness predicate reads \"any \
axis carries a value\", not \"any axis carries a \
value above a threshold\"",
);
assert_eq!(
l.cpu().is_none(),
l.is_empty(),
"when :cpu is the only set axis, is_empty() must \
equal cpu().is_none() — the accessor and the \
emptiness predicate must route through the same \
substrate-primitive typed dispatch on the :cpu \
arm",
);
}
}
#[test]
fn limits_cpu_projects_option_u32_by_copy() {
for cpu in [
None,
Some(1_u32),
Some(500_u32),
Some(LIMITS_CPU_MILLICORES_MAX),
] {
let l = LimitsSpec {
cpu,
..LimitsSpec::default()
};
let first = l.cpu();
let second = l.cpu();
assert_eq!(
first, second,
"LimitsSpec::cpu must be idempotent — two \
successive calls on the same &self must return the \
same Option<u32>",
);
assert_eq!(
first, cpu,
"LimitsSpec::cpu must return :limits :cpu \
verbatim by copy — got {first:?}, expected {cpu:?}",
);
}
}
#[test]
fn non_integer_byte_magnitude_ctor_matches_struct_literal_wrap() {
let value = "1.5KiB";
assert_eq!(
LimitsError::non_integer_byte_magnitude(value),
LimitsError::NonIntegerByteMagnitude {
value: value.to_string(),
},
);
}
#[test]
fn leading_zero_byte_magnitude_ctor_matches_struct_literal_wrap() {
let value = "064MiB";
assert_eq!(
LimitsError::leading_zero_byte_magnitude(value),
LimitsError::LeadingZeroByteMagnitude {
value: value.to_string(),
},
);
}
#[test]
fn non_integer_duration_magnitude_ctor_matches_struct_literal_wrap() {
let value = "1.5s";
assert_eq!(
LimitsError::non_integer_duration_magnitude(value),
LimitsError::NonIntegerDurationMagnitude {
value: value.to_string(),
},
);
}
#[test]
fn leading_zero_duration_magnitude_ctor_matches_struct_literal_wrap() {
let value = "030s";
assert_eq!(
LimitsError::leading_zero_duration_magnitude(value),
LimitsError::LeadingZeroDurationMagnitude {
value: value.to_string(),
},
);
}
#[test]
fn non_integer_millicore_magnitude_ctor_matches_struct_literal_wrap() {
let value = "1.5";
assert_eq!(
LimitsError::non_integer_millicore_magnitude(value),
LimitsError::NonIntegerMillicoreMagnitude {
value: value.to_string(),
},
);
}
#[test]
fn leading_zero_millicore_magnitude_ctor_matches_struct_literal_wrap() {
let value = "0500m";
assert_eq!(
LimitsError::leading_zero_millicore_magnitude(value),
LimitsError::LeadingZeroMillicoreMagnitude {
value: value.to_string(),
},
);
}
#[test]
fn unknown_byte_unit_ctor_matches_struct_literal_wrap() {
let unit = "TiB";
assert_eq!(
LimitsError::unknown_byte_unit(unit),
LimitsError::UnknownByteUnit {
unit: unit.to_string(),
},
);
}
#[test]
fn unknown_duration_unit_ctor_matches_struct_literal_wrap() {
let unit = "d";
assert_eq!(
LimitsError::unknown_duration_unit(unit),
LimitsError::UnknownDurationUnit {
unit: unit.to_string(),
},
);
}
#[test]
fn limits_codec_unit_only_ctors_route_unit_verbatim_across_every_variant() {
for unit in ["", "TiB", "\u{00A0}", "μs"] {
assert_eq!(
LimitsError::unknown_byte_unit(unit),
LimitsError::UnknownByteUnit {
unit: unit.to_string(),
},
);
assert_eq!(
LimitsError::unknown_duration_unit(unit),
LimitsError::UnknownDurationUnit {
unit: unit.to_string(),
},
);
}
}
#[test]
fn whitespace_in_byte_size_ctor_matches_struct_literal_wrap() {
let value = " 64MiB";
let byte: u8 = 0x20;
assert_eq!(
LimitsError::whitespace_in_byte_size(value, byte),
LimitsError::WhitespaceInByteSize {
value: value.to_string(),
byte,
},
);
}
#[test]
fn whitespace_in_duration_ctor_matches_struct_literal_wrap() {
let value = " 30s";
let byte: u8 = 0x09;
assert_eq!(
LimitsError::whitespace_in_duration(value, byte),
LimitsError::WhitespaceInDuration {
value: value.to_string(),
byte,
},
);
}
#[test]
fn whitespace_in_millicores_ctor_matches_struct_literal_wrap() {
let value = " 500m";
let byte: u8 = 0x0A;
assert_eq!(
LimitsError::whitespace_in_millicores(value, byte),
LimitsError::WhitespaceInMillicores {
value: value.to_string(),
byte,
},
);
}
#[test]
fn non_ascii_whitespace_in_byte_size_ctor_matches_struct_literal_wrap() {
let value = "\u{00A0}64MiB";
let ch = '\u{00A0}';
assert_eq!(
LimitsError::non_ascii_whitespace_in_byte_size(value, ch),
LimitsError::NonAsciiWhitespaceInByteSize {
value: value.to_string(),
ch,
codepoint: ch as u32,
},
);
}
#[test]
fn non_ascii_whitespace_in_duration_ctor_matches_struct_literal_wrap() {
let value = "30s\u{2028}";
let ch = '\u{2028}';
assert_eq!(
LimitsError::non_ascii_whitespace_in_duration(value, ch),
LimitsError::NonAsciiWhitespaceInDuration {
value: value.to_string(),
ch,
codepoint: ch as u32,
},
);
}
#[test]
fn non_ascii_whitespace_in_millicores_ctor_matches_struct_literal_wrap() {
let value = "500\u{2003}m";
let ch = '\u{2003}';
assert_eq!(
LimitsError::non_ascii_whitespace_in_millicores(value, ch),
LimitsError::NonAsciiWhitespaceInMillicores {
value: value.to_string(),
ch,
codepoint: ch as u32,
},
);
}
#[test]
fn limits_codec_value_char_ctors_route_codepoint_through_ch_as_u32_uniformly() {
for ch in [
'\u{00A0}', '\u{2028}', '\u{2003}', '\u{202F}', '\u{3000}', ] {
let value = format!("prefix{ch}suffix");
let expected_codepoint = ch as u32;
assert!(matches!(
LimitsError::non_ascii_whitespace_in_byte_size(&value, ch),
LimitsError::NonAsciiWhitespaceInByteSize { codepoint, .. } if codepoint == expected_codepoint,
));
assert!(matches!(
LimitsError::non_ascii_whitespace_in_duration(&value, ch),
LimitsError::NonAsciiWhitespaceInDuration { codepoint, .. } if codepoint == expected_codepoint,
));
assert!(matches!(
LimitsError::non_ascii_whitespace_in_millicores(&value, ch),
LimitsError::NonAsciiWhitespaceInMillicores { codepoint, .. } if codepoint == expected_codepoint,
));
}
}
#[test]
fn memory_below_wasm32_page_ctor_matches_struct_literal_wrap() {
let bytes = LIMITS_MEMORY_WASM32_PAGE_BYTES - 1;
assert_eq!(
LimitsError::memory_below_wasm32_page(bytes),
LimitsError::MemoryBelowWasm32Page { bytes },
"generated memory_below_wasm32_page ctor must produce byte-equal \
`LimitsError::MemoryBelowWasm32Page` to the pre-lift struct-literal \
wrap on the same `Copy`-`u64` fixture",
);
}
#[test]
fn memory_exceeds_wasm32_cap_ctor_matches_struct_literal_wrap() {
let bytes = LIMITS_MEMORY_WASM32_MAX_BYTES + LIMITS_MEMORY_WASM32_PAGE_BYTES;
assert_eq!(
LimitsError::memory_exceeds_wasm32_cap(bytes),
LimitsError::MemoryExceedsWasm32Cap { bytes },
"generated memory_exceeds_wasm32_cap ctor must produce byte-equal \
`LimitsError::MemoryExceedsWasm32Cap` to the pre-lift struct-literal \
wrap on the same `Copy`-`u64` fixture",
);
}
#[test]
fn memory_not_page_multiple_ctor_matches_struct_literal_wrap() {
let bytes = LIMITS_MEMORY_WASM32_PAGE_BYTES + 1;
assert_eq!(
LimitsError::memory_not_page_multiple(bytes),
LimitsError::MemoryNotPageMultiple { bytes },
"generated memory_not_page_multiple ctor must produce byte-equal \
`LimitsError::MemoryNotPageMultiple` to the pre-lift struct-literal \
wrap on the same `Copy`-`u64` fixture",
);
}
#[test]
fn fuel_exceeds_cap_ctor_matches_struct_literal_wrap() {
let fuel = LIMITS_FUEL_MAX + 1;
assert_eq!(
LimitsError::fuel_exceeds_cap(fuel),
LimitsError::FuelExceedsCap { fuel },
"generated fuel_exceeds_cap ctor must produce byte-equal \
`LimitsError::FuelExceedsCap` to the pre-lift struct-literal wrap \
on the same `Copy`-`u64` fixture",
);
}
#[test]
fn wall_clock_not_canonical_ctor_matches_struct_literal_wrap() {
let wall_clock = Duration::from_micros(1_500);
assert_eq!(
LimitsError::wall_clock_not_canonical(wall_clock),
LimitsError::WallClockNotCanonical { wall_clock },
"generated wall_clock_not_canonical ctor must produce byte-equal \
`LimitsError::WallClockNotCanonical` to the pre-lift struct-literal \
wrap on the same `Copy`-`Duration` fixture",
);
}
#[test]
fn wall_clock_exceeds_cap_ctor_matches_struct_literal_wrap() {
let wall_clock = LIMITS_WALL_CLOCK_MAX + Duration::from_millis(1);
assert_eq!(
LimitsError::wall_clock_exceeds_cap(wall_clock),
LimitsError::WallClockExceedsCap { wall_clock },
"generated wall_clock_exceeds_cap ctor must produce byte-equal \
`LimitsError::WallClockExceedsCap` to the pre-lift struct-literal \
wrap on the same `Copy`-`Duration` fixture",
);
}
#[test]
fn cpu_exceeds_cap_ctor_matches_struct_literal_wrap() {
let millicores = LIMITS_CPU_MILLICORES_MAX + 1;
assert_eq!(
LimitsError::cpu_exceeds_cap(millicores),
LimitsError::CpuExceedsCap { millicores },
"generated cpu_exceeds_cap ctor must produce byte-equal \
`LimitsError::CpuExceedsCap` to the pre-lift struct-literal wrap \
on the same `Copy`-`u32` fixture",
);
}
#[test]
fn limits_scalar_ctors_route_field_through_copy_uniformly() {
let below_page = LIMITS_MEMORY_WASM32_PAGE_BYTES - 137;
let above_mem_cap = LIMITS_MEMORY_WASM32_MAX_BYTES + LIMITS_MEMORY_WASM32_PAGE_BYTES;
let page_plus_one = LIMITS_MEMORY_WASM32_PAGE_BYTES + 1;
let above_fuel_cap = LIMITS_FUEL_MAX + 137;
let sub_ms = Duration::from_micros(1_500);
let above_hour = LIMITS_WALL_CLOCK_MAX + Duration::from_secs(1);
let above_cpu_cap = LIMITS_CPU_MILLICORES_MAX + 137;
assert_eq!(
LimitsError::memory_below_wasm32_page(below_page),
LimitsError::MemoryBelowWasm32Page { bytes: below_page },
);
assert_eq!(
LimitsError::memory_exceeds_wasm32_cap(above_mem_cap),
LimitsError::MemoryExceedsWasm32Cap {
bytes: above_mem_cap,
},
);
assert_eq!(
LimitsError::memory_not_page_multiple(page_plus_one),
LimitsError::MemoryNotPageMultiple {
bytes: page_plus_one,
},
);
assert_eq!(
LimitsError::fuel_exceeds_cap(above_fuel_cap),
LimitsError::FuelExceedsCap {
fuel: above_fuel_cap,
},
);
assert_eq!(
LimitsError::wall_clock_not_canonical(sub_ms),
LimitsError::WallClockNotCanonical { wall_clock: sub_ms },
);
assert_eq!(
LimitsError::wall_clock_exceeds_cap(above_hour),
LimitsError::WallClockExceedsCap {
wall_clock: above_hour,
},
);
assert_eq!(
LimitsError::cpu_exceeds_cap(above_cpu_cap),
LimitsError::CpuExceedsCap {
millicores: above_cpu_cap,
},
);
}
#[test]
fn limits_scalar_ctors_are_const_zero_runtime_work() {
const MEM_BELOW: LimitsError = LimitsError::memory_below_wasm32_page(1);
const MEM_CAP: LimitsError =
LimitsError::memory_exceeds_wasm32_cap(LIMITS_MEMORY_WASM32_MAX_BYTES + 1);
const MEM_NOT_MULTIPLE: LimitsError =
LimitsError::memory_not_page_multiple(LIMITS_MEMORY_WASM32_PAGE_BYTES + 1);
const FUEL_CAP: LimitsError = LimitsError::fuel_exceeds_cap(LIMITS_FUEL_MAX + 1);
const WALL_NC: LimitsError =
LimitsError::wall_clock_not_canonical(Duration::from_micros(1));
const WALL_CAP: LimitsError =
LimitsError::wall_clock_exceeds_cap(Duration::from_secs(3_601));
const CPU_CAP: LimitsError = LimitsError::cpu_exceeds_cap(LIMITS_CPU_MILLICORES_MAX + 1);
assert!(matches!(
MEM_BELOW,
LimitsError::MemoryBelowWasm32Page { .. }
));
assert!(matches!(
MEM_CAP,
LimitsError::MemoryExceedsWasm32Cap { .. }
));
assert!(matches!(
MEM_NOT_MULTIPLE,
LimitsError::MemoryNotPageMultiple { .. }
));
assert!(matches!(FUEL_CAP, LimitsError::FuelExceedsCap { .. }));
assert!(matches!(WALL_NC, LimitsError::WallClockNotCanonical { .. }));
assert!(matches!(WALL_CAP, LimitsError::WallClockExceedsCap { .. }));
assert!(matches!(CPU_CAP, LimitsError::CpuExceedsCap { .. }));
}
}