use std::fmt;
use std::time::Duration;
use rust_decimal::Decimal;
use rust_decimal::prelude::{FromStr, ToPrimitive};
use rust_decimal_macros::dec;
use crate::error::Error;
use crate::region::Region;
use crate::sizing::SizeClass;
pub const SECONDS_PER_MONTH: Decimal = dec!(2628000);
pub const HOURS_PER_MONTH: Decimal = dec!(730);
pub const STALE_AFTER_DAYS: i64 = 90;
const SECONDS_PER_DAY: u64 = 24 * 60 * 60;
pub const MINIMUM_RETENTION: Duration = Duration::from_secs(7 * SECONDS_PER_DAY);
pub fn seconds_of(duration: Duration) -> Decimal {
Decimal::from(duration.as_secs()) + Decimal::from(duration.subsec_nanos()) / dec!(1000000000)
}
pub fn gb_decimal(gb: f64) -> Result<Decimal, Error> {
if !gb.is_finite() || gb < 0.0 {
return Err(Error::invalid_arg(format!(
"{gb} GB is not a quantity that can be priced: a size must be finite and \
non-negative, and a negative one would render as a credit on the report"
)));
}
Decimal::from_str(&gb.to_string()).map_err(|source| {
Error::invalid_arg(format!(
"{gb} GB cannot be represented exactly in decimal, and money arithmetic here is \
decimal end to end so that a figure can be reconciled against the rate table"
))
.with_source(source)
})
}
pub fn duration_of_secs_f64(seconds: f64) -> Result<Duration, Error> {
crate::duration::of_secs_f64_because(
seconds,
", and a negative one would silently produce a credit on the report",
)
}
fn render_amount(amount: Decimal) -> String {
let places = if amount.abs() >= Decimal::ONE { 2 } else { 6 };
let mut rendered = amount.round_dp(places);
rendered.rescale(places);
rendered.to_string()
}
fn render_quantity(quantity: Decimal) -> String {
quantity.round_dp(6).normalize().to_string()
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct CalendarDate(jiff::civil::Date);
impl CalendarDate {
pub const fn from_ymd(year: i32, month: u32, day: u32) -> CalendarDate {
CalendarDate(jiff::civil::Date::constant(
year as i16,
month as i8,
day as i8,
))
}
pub fn try_from_ymd(year: i32, month: u32, day: u32) -> Result<CalendarDate, Error> {
let refusal = || {
Error::invalid_arg(format!(
"{year:04}-{month:02}-{day:02} is not a calendar day, so it cannot date a rate \
table: month must be 1-12 and day must be within that month"
))
};
if !(1..=12).contains(&month) || !(1..=31).contains(&day) || i16::try_from(year).is_err() {
return Err(refusal());
}
jiff::civil::Date::new(year as i16, month as i8, day as i8)
.map(CalendarDate)
.map_err(|_| refusal())
}
pub fn from_unix_secs(secs: u64) -> Result<CalendarDate, Error> {
i64::try_from(secs / 86_400)
.ok()
.and_then(Self::from_day_number)
.ok_or_else(|| {
Error::invalid_arg(format!(
"{secs} seconds after the Unix epoch is past 9999-12-31, the last day a \
rate table can be dated. A time in milliseconds looks like this: pass \
whole seconds"
))
})
}
pub fn year(self) -> i32 {
i32::from(self.0.year())
}
pub fn month(self) -> u32 {
self.0.month() as u32
}
pub fn day(self) -> u32 {
self.0.day() as u32
}
pub fn day_number(self) -> i64 {
i64::from(self.0.since(EPOCH).expect("dates are in range").get_days())
}
fn from_day_number(day_number: i64) -> Option<CalendarDate> {
let span = jiff::Span::new().try_days(day_number).ok()?;
EPOCH.checked_add(span).ok().map(CalendarDate)
}
pub fn days_since(self, earlier: CalendarDate) -> i64 {
self.day_number() - earlier.day_number()
}
}
const EPOCH: jiff::civil::Date = jiff::civil::Date::constant(1970, 1, 1);
impl fmt::Display for CalendarDate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{:04}-{:02}-{:02}",
self.year(),
self.month(),
self.day()
)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Provenance {
Measured,
Projected,
}
impl Provenance {
pub fn as_str(self) -> &'static str {
match self {
Provenance::Measured => "measured",
Provenance::Projected => "projected",
}
}
}
impl fmt::Display for Provenance {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum DurationP {
Measured(Duration),
Projected(Duration),
}
impl DurationP {
pub fn measured_secs_f64(seconds: f64) -> Result<DurationP, Error> {
Ok(DurationP::Measured(duration_of_secs_f64(seconds)?))
}
pub fn projected_secs_f64(seconds: f64) -> Result<DurationP, Error> {
Ok(DurationP::Projected(duration_of_secs_f64(seconds)?))
}
pub fn duration(self) -> Duration {
match self {
DurationP::Measured(duration) | DurationP::Projected(duration) => duration,
}
}
pub fn provenance(self) -> Provenance {
match self {
DurationP::Measured(_) => Provenance::Measured,
DurationP::Projected(_) => Provenance::Projected,
}
}
pub fn seconds(self) -> Decimal {
seconds_of(self.duration())
}
pub fn seconds_f64(self) -> f64 {
self.duration().as_secs_f64()
}
pub fn is_measured(self) -> bool {
matches!(self, DurationP::Measured(_))
}
}
impl fmt::Display for DurationP {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}s ({})", self.seconds().normalize(), self.provenance())
}
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct EstimatedUsd(Decimal);
impl EstimatedUsd {
pub const ZERO: EstimatedUsd = EstimatedUsd(Decimal::ZERO);
pub fn new(amount: Decimal) -> EstimatedUsd {
EstimatedUsd(amount)
}
pub fn amount(self) -> Decimal {
self.0
}
pub fn amount_string(self) -> String {
render_amount(self.0)
}
pub fn parse(text: &str) -> Result<EstimatedUsd, Error> {
let amount = Decimal::from_str(text.trim()).map_err(|source| {
Error::invalid_arg(format!(
"{text:?} is not a dollar figure: money arithmetic here is decimal end to \
end, so the value must read as a plain decimal like 1.50"
))
.with_source(source)
})?;
if amount.is_sign_negative() {
return Err(Error::invalid_arg(format!(
"a budget cannot be negative: {text}"
)));
}
Ok(EstimatedUsd(amount))
}
}
impl std::ops::Add for EstimatedUsd {
type Output = EstimatedUsd;
fn add(self, other: EstimatedUsd) -> EstimatedUsd {
EstimatedUsd(self.0 + other.0)
}
}
impl std::iter::Sum for EstimatedUsd {
fn sum<I: Iterator<Item = EstimatedUsd>>(iter: I) -> EstimatedUsd {
iter.fold(EstimatedUsd::ZERO, std::ops::Add::add)
}
}
impl fmt::Display for EstimatedUsd {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "~${} (estimated)", render_amount(self.0))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Amount {
Estimated(EstimatedUsd),
Unpriced {
reason: String,
},
}
impl Amount {
pub fn estimated(amount: Decimal) -> Amount {
Amount::Estimated(EstimatedUsd::new(amount))
}
pub fn unpriced(reason: impl Into<String>) -> Amount {
Amount::Unpriced {
reason: reason.into(),
}
}
pub fn estimate(&self) -> Option<EstimatedUsd> {
match self {
Amount::Estimated(usd) => Some(*usd),
Amount::Unpriced { .. } => None,
}
}
pub fn unpriced_reason(&self) -> Option<&str> {
match self {
Amount::Estimated(_) => None,
Amount::Unpriced { reason } => Some(reason),
}
}
pub fn kind(&self) -> &'static str {
match self {
Amount::Estimated(_) => "estimated-usd",
Amount::Unpriced { .. } => "unpriced",
}
}
}
impl fmt::Display for Amount {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Amount::Estimated(usd) => write!(f, "{usd}"),
Amount::Unpriced { reason } => write!(f, "unpriced — {reason}"),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct UnpricedLine {
pub phase: CostPhase,
pub reason: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Total {
Exact(EstimatedUsd),
AtLeast {
floor: EstimatedUsd,
unpriced: Vec<UnpricedLine>,
},
}
impl Total {
pub fn of<'a>(amounts: impl IntoIterator<Item = (CostPhase, &'a Amount)>) -> Total {
let mut floor = EstimatedUsd::ZERO;
let mut unpriced: Vec<UnpricedLine> = Vec::new();
for (phase, amount) in amounts {
match amount {
Amount::Estimated(usd) => floor = floor + *usd,
Amount::Unpriced { reason } => unpriced.push(UnpricedLine {
phase,
reason: reason.clone(),
}),
}
}
if unpriced.is_empty() {
Total::Exact(floor)
} else {
Total::AtLeast { floor, unpriced }
}
}
pub fn floor(&self) -> EstimatedUsd {
match self {
Total::Exact(usd) => *usd,
Total::AtLeast { floor, .. } => *floor,
}
}
pub fn is_lower_bound(&self) -> bool {
matches!(self, Total::AtLeast { .. })
}
pub fn unpriced_lines(&self) -> &[UnpricedLine] {
match self {
Total::Exact(_) => &[],
Total::AtLeast { unpriced, .. } => unpriced,
}
}
pub fn unpriced_reasons(&self) -> Vec<String> {
self.unpriced_lines()
.iter()
.map(|line| line.reason.clone())
.collect()
}
pub fn unpriced_phase_names(&self) -> Vec<&'static str> {
let mut names: Vec<&'static str> = self
.unpriced_lines()
.iter()
.map(|line| line.phase.as_str())
.collect();
names.sort_unstable();
names.dedup();
names
}
}
impl fmt::Display for Total {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Total::Exact(usd) => write!(f, "{usd}"),
Total::AtLeast { floor, unpriced } => write!(
f,
"at least {floor}, plus {} unpriced ({})",
unpriced.len(),
self.unpriced_phase_names().join(", ")
),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RateTable {
region: Region,
source_url: String,
retrieved: CalendarDate,
vcpu_second: Decimal,
gb_second: Decimal,
storage_gb_month: Decimal,
snapshot_read_gb: Decimal,
snapshot_write_gb: Decimal,
minimum_retention: Duration,
}
impl RateTable {
pub fn region(&self) -> &Region {
&self.region
}
pub fn source_url(&self) -> &str {
&self.source_url
}
pub fn retrieved(&self) -> CalendarDate {
self.retrieved
}
pub fn vcpu_second(&self) -> Decimal {
self.vcpu_second
}
pub fn gb_second(&self) -> Decimal {
self.gb_second
}
pub fn storage_gb_month(&self) -> Decimal {
self.storage_gb_month
}
pub fn snapshot_read_gb(&self) -> Decimal {
self.snapshot_read_gb
}
pub fn snapshot_write_gb(&self) -> Decimal {
self.snapshot_write_gb
}
pub fn minimum_retention(&self) -> Duration {
self.minimum_retention
}
pub fn minimum_retention_days(&self) -> u64 {
self.minimum_retention.as_secs() / SECONDS_PER_DAY
}
pub fn per_request(&self) -> Decimal {
Decimal::ZERO
}
pub fn bills_vcpu_and_memory_separately(&self) -> bool {
true
}
pub fn free_tier(&self) -> bool {
false
}
pub fn minimum_billing_increment(&self) -> Option<Duration> {
None
}
pub fn age_days(&self, today: CalendarDate) -> i64 {
today.days_since(self.retrieved)
}
pub fn is_stale(&self, today: CalendarDate) -> bool {
self.age_days(today) > STALE_AFTER_DAYS
}
pub fn staleness(&self, today: CalendarDate) -> Option<String> {
if !self.is_stale(today) {
return None;
}
Some(format!(
"rate table for {} was retrieved {}, {} days ago (stale after {STALE_AFTER_DAYS}) — \
re-read {} before trusting these figures",
self.region,
self.retrieved,
self.age_days(today),
self.source_url,
))
}
}
pub fn pinned_rates() -> RateTable {
RateTable {
region: Region::UsEast1,
source_url: "https://aws.amazon.com/lambda/pricing/".to_string(),
retrieved: CalendarDate::from_ymd(2026, 8, 7),
vcpu_second: dec!(0.0000276944),
gb_second: dec!(0.0000036667),
storage_gb_month: dec!(0.0811111030),
snapshot_read_gb: dec!(0.00155),
snapshot_write_gb: dec!(0.0038),
minimum_retention: MINIMUM_RETENTION,
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum CatalogLine {
VcpuSecond,
MemoryGbSecond,
SnapshotStorageGbHour,
SnapshotReadGb,
SnapshotWriteGb,
}
impl CatalogLine {
pub const ALL: [CatalogLine; 5] = [
CatalogLine::VcpuSecond,
CatalogLine::MemoryGbSecond,
CatalogLine::SnapshotStorageGbHour,
CatalogLine::SnapshotReadGb,
CatalogLine::SnapshotWriteGb,
];
pub fn group(self) -> &'static str {
match self {
CatalogLine::VcpuSecond => "AWS-Lambda-MicroVM-vCPU-Second-ARM",
CatalogLine::MemoryGbSecond => "AWS-Lambda-MicroVM-Memory-GB-Second-ARM",
CatalogLine::SnapshotStorageGbHour => "AWS-Lambda-MicroVM-Snapshot-Storage-GB-Hour",
CatalogLine::SnapshotReadGb => "AWS-Lambda-MicroVM-Snapshot-Read-GB",
CatalogLine::SnapshotWriteGb => "AWS-Lambda-MicroVM-Snapshot-Write-GB",
}
}
pub fn field(self) -> &'static str {
match self {
CatalogLine::VcpuSecond => "vcpu_second",
CatalogLine::MemoryGbSecond => "gb_second",
CatalogLine::SnapshotStorageGbHour => "storage_gb_month",
CatalogLine::SnapshotReadGb => "snapshot_read_gb",
CatalogLine::SnapshotWriteGb => "snapshot_write_gb",
}
}
pub fn unit(self) -> &'static str {
match self {
CatalogLine::VcpuSecond => "vCPU-Seconds",
CatalogLine::MemoryGbSecond => "GB-Seconds",
CatalogLine::SnapshotStorageGbHour => "GB-Hours",
CatalogLine::SnapshotReadGb | CatalogLine::SnapshotWriteGb => "GB",
}
}
pub fn x86_group(self) -> Option<&'static str> {
match self {
CatalogLine::VcpuSecond => Some("AWS-Lambda-MicroVM-vCPU-Second"),
CatalogLine::MemoryGbSecond => Some("AWS-Lambda-MicroVM-Memory-GB-Second"),
CatalogLine::SnapshotStorageGbHour
| CatalogLine::SnapshotReadGb
| CatalogLine::SnapshotWriteGb => None,
}
}
pub fn is_per_hour(self) -> bool {
matches!(self, CatalogLine::SnapshotStorageGbHour)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CatalogEntry {
pub group: String,
pub unit: String,
pub usd: Decimal,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct RateCatalog {
entries: Vec<CatalogEntry>,
}
impl RateCatalog {
pub fn new() -> RateCatalog {
RateCatalog {
entries: Vec::new(),
}
}
#[must_use]
pub fn with_entry(
mut self,
group: impl Into<String>,
unit: impl Into<String>,
usd: Decimal,
) -> RateCatalog {
self.entries.push(CatalogEntry {
group: group.into(),
unit: unit.into(),
usd,
});
self
}
fn matching(&self, group: &str) -> Vec<&CatalogEntry> {
self.entries
.iter()
.filter(|entry| entry.group == group)
.collect()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
impl RateTable {
pub fn from_catalog(
region: Region,
source_url: impl Into<String>,
retrieved: CalendarDate,
catalog: &RateCatalog,
) -> Result<RateTable, Error> {
let mut rates: Vec<Decimal> = Vec::with_capacity(CatalogLine::ALL.len());
for line in CatalogLine::ALL {
let matches = catalog.matching(line.group());
let entry = match matches.as_slice() {
[] => return Err(missing_line_error(®ion, line, catalog)),
[only] => *only,
many => {
return Err(Error::invalid_arg(format!(
"{} in {region} returned {} products where one group in one region has \
always been one product; the catalog gained a dimension this client \
does not know how to choose between, and picking one would silently \
select a rate",
line.group(),
many.len(),
)));
}
};
if entry.unit != line.unit() {
return Err(Error::invalid_arg(format!(
"{} in {region} is now quoted per {:?}, not {:?}; the conversion into the \
rate table no longer holds and every downstream figure would still look \
plausible",
line.group(),
entry.unit,
line.unit(),
)));
}
rates.push(if line.is_per_hour() {
entry.usd * HOURS_PER_MONTH
} else {
entry.usd
});
}
Ok(RateTable {
region,
source_url: source_url.into(),
retrieved,
vcpu_second: rates[0],
gb_second: rates[1],
storage_gb_month: rates[2],
snapshot_read_gb: rates[3],
snapshot_write_gb: rates[4],
minimum_retention: MINIMUM_RETENTION,
})
}
}
fn missing_line_error(region: &Region, line: CatalogLine, catalog: &RateCatalog) -> Error {
if catalog.is_empty() {
return Error::invalid_arg(format!(
"the rate catalog has no MicroVM line items for {region} at all, so {} cannot be \
filled. Measured 2026-08-07, pricing coverage and service availability coincide: a \
region that does not price MicroVMs does not run them either, and answers \
lambda-microvms with AccessDeniedException and a null message that reads exactly \
like an IAM problem it is not. Check the region before auditing a policy",
line.field(),
));
}
let Some(x86_group) = line.x86_group() else {
return Error::invalid_arg(format!(
"{region} prices MicroVMs but has no {} line item, so {} cannot be filled",
line.group(),
line.field(),
));
};
match catalog.matching(x86_group).first() {
Some(sibling) => Error::invalid_arg(format!(
"{region} has no {}, only the x86 rate {x86_group} at ${}. MicroVMs are ARM64-only \
— the Architecture shape is enum ['ARM_64'] with no other member — so the x86 rate \
can never apply and is not substituted for {}; doing so would overstate every \
compute figure by roughly 18%",
line.group(),
sibling.usd,
line.field(),
)),
None => Error::invalid_arg(format!(
"{region} has neither {} nor {x86_group}, so {} cannot be filled",
line.group(),
line.field(),
)),
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum BillingLine {
Vcpu,
Memory,
SnapshotStorage,
SnapshotRead,
SnapshotWrite,
}
impl BillingLine {
pub fn as_str(self) -> &'static str {
match self {
BillingLine::Vcpu => "vcpu",
BillingLine::Memory => "memory",
BillingLine::SnapshotStorage => "snapshot-storage",
BillingLine::SnapshotRead => "snapshot-read",
BillingLine::SnapshotWrite => "snapshot-write",
}
}
}
impl fmt::Display for BillingLine {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum CostPhase {
ImageBuild,
ImageStorage,
Launch,
Running,
Suspended,
Suspend,
Resume,
}
impl CostPhase {
pub const ALL: [CostPhase; 7] = [
CostPhase::ImageBuild,
CostPhase::ImageStorage,
CostPhase::Launch,
CostPhase::Running,
CostPhase::Suspended,
CostPhase::Suspend,
CostPhase::Resume,
];
pub fn as_str(self) -> &'static str {
match self {
CostPhase::ImageBuild => "image-build",
CostPhase::ImageStorage => "image-storage",
CostPhase::Launch => "launch",
CostPhase::Running => "running",
CostPhase::Suspended => "suspended",
CostPhase::Suspend => "suspend",
CostPhase::Resume => "resume",
}
}
}
impl FromStr for CostPhase {
type Err = Error;
fn from_str(phase: &str) -> Result<CostPhase, Error> {
CostPhase::ALL
.into_iter()
.find(|candidate| candidate.as_str() == phase)
.ok_or_else(|| {
let offered = CostPhase::ALL
.iter()
.map(|candidate| candidate.as_str())
.collect::<Vec<_>>()
.join(", ");
Error::invalid_arg(format!(
"{phase:?} is not a cost phase; the phases are {offered}"
))
})
}
}
impl fmt::Display for CostPhase {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.pad(self.as_str())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LineItem {
pub phase: CostPhase,
pub line: Option<BillingLine>,
pub quantity: Decimal,
pub unit: String,
pub amount: Amount,
pub duration: Option<DurationP>,
pub note: String,
}
impl LineItem {
pub fn quantity_string(&self) -> String {
render_quantity(self.quantity)
}
}
impl fmt::Display for LineItem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let consumed = format!("{} {}", self.quantity_string(), self.unit);
let head = format!("{:<14} {:<26} {}", self.phase, consumed, self.amount);
if self.note.is_empty() {
f.write_str(&head)
} else {
write!(f, "{head} [{}]", self.note)
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CostReport {
label: String,
size: SizeClass,
rates: RateTable,
items: Vec<LineItem>,
staleness: Option<String>,
}
impl CostReport {
pub fn label(&self) -> &str {
&self.label
}
pub fn size(&self) -> SizeClass {
self.size
}
pub fn rates(&self) -> &RateTable {
&self.rates
}
pub fn items(&self) -> &[LineItem] {
&self.items
}
pub fn staleness(&self) -> Option<&str> {
self.staleness.as_deref()
}
pub fn priced(&self) -> impl Iterator<Item = &LineItem> {
self.items
.iter()
.filter(|item| item.amount.estimate().is_some())
}
pub fn unpriced(&self) -> impl Iterator<Item = &LineItem> {
self.items
.iter()
.filter(|item| item.amount.unpriced_reason().is_some())
}
pub fn total(&self) -> Total {
Total::of(self.items.iter().map(|item| (item.phase, &item.amount)))
}
pub fn is_complete(&self) -> bool {
self.unpriced().next().is_none()
}
pub fn fully_measured(&self) -> bool {
let mut durations = self
.items
.iter()
.filter_map(|item| item.duration)
.peekable();
durations.peek().is_some() && durations.all(DurationP::is_measured)
}
pub fn by_phase(&self, phase: CostPhase) -> impl Iterator<Item = &LineItem> {
self.items.iter().filter(move |item| item.phase == phase)
}
pub fn render(&self) -> String {
let mut lines = vec![
format!("{} — {}", self.label, self.size),
format!(
"dollars are estimates derived from published {} rates (retrieved {}); only Cost \
Explorer knows the bill",
self.rates.region, self.rates.retrieved,
),
];
if let Some(warning) = &self.staleness {
lines.push(format!("WARNING: {warning}"));
}
lines.extend(self.items.iter().map(|item| format!(" {item}")));
lines.push(format!("total: {}", self.total()));
lines.join("\n")
}
}
fn compute_lines(
size: SizeClass,
duration: DurationP,
rates: &RateTable,
phase: CostPhase,
) -> Result<[LineItem; 2], Error> {
let seconds = duration.seconds();
let vcpu = gb_decimal(size.baseline_vcpu())?;
let memory = gb_decimal(size.baseline_gb())?;
Ok([
LineItem {
phase,
line: Some(BillingLine::Vcpu),
quantity: vcpu * seconds,
unit: "vCPU-seconds".to_string(),
amount: Amount::estimated(vcpu * seconds * rates.vcpu_second()),
duration: Some(duration),
note: format!("{vcpu} vCPU baseline"),
},
LineItem {
phase,
line: Some(BillingLine::Memory),
quantity: memory * seconds,
unit: "GB-seconds".to_string(),
amount: Amount::estimated(memory * seconds * rates.gb_second()),
duration: Some(duration),
note: format!("{memory} GB baseline, billed separately from vCPU"),
},
])
}
fn storage_line(
phase: CostPhase,
gb: Decimal,
held: DurationP,
rates: &RateTable,
) -> Result<LineItem, Error> {
let floor = seconds_of(rates.minimum_retention());
let held_seconds = held.seconds();
let billed = held_seconds.max(floor);
let quantity = gb * billed / SECONDS_PER_MONTH;
let mut note = format!("{} GB held {held}", gb.normalize());
if billed > held_seconds {
note.push_str(&format!(
"; billed {}-day minimum retention ({floor}s) instead",
rates.minimum_retention_days(),
));
}
Ok(LineItem {
phase,
line: Some(BillingLine::SnapshotStorage),
quantity,
unit: "GB-months".to_string(),
amount: Amount::estimated(quantity * rates.storage_gb_month()),
duration: Some(held),
note,
})
}
fn transfer_line(
phase: CostPhase,
line: BillingLine,
gb: Decimal,
count: u32,
rates: &RateTable,
note_suffix: &str,
) -> LineItem {
let rate = if line == BillingLine::SnapshotWrite {
rates.snapshot_write_gb()
} else {
rates.snapshot_read_gb()
};
let quantity = gb * Decimal::from(count);
LineItem {
phase,
line: Some(line),
quantity,
unit: "GB".to_string(),
amount: Amount::estimated(quantity * rate),
duration: None,
note: format!("{count} x {} GB{note_suffix}", gb.normalize()),
}
}
pub const BUILD_UNPRICED_REASON: &str = "AWS does not publish whether the server-side image build is billed as compute; the build \
runs a real MicroVM, so treating it as free would understate the run";
fn build_line(duration: Option<DurationP>) -> LineItem {
LineItem {
phase: CostPhase::ImageBuild,
line: None,
quantity: duration.map_or(Decimal::ZERO, DurationP::seconds),
unit: if duration.is_some() {
"seconds".to_string()
} else {
"seconds (untimed)".to_string()
},
amount: Amount::unpriced(BUILD_UNPRICED_REASON),
duration,
note: "unknown, not zero — see docs/PLATFORM.md, 'Not published'".to_string(),
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct RunUsage {
pub running: Option<DurationP>,
pub suspended: Option<DurationP>,
pub image_build: Option<DurationP>,
pub image_gb: Option<f64>,
pub image_retained: Option<DurationP>,
pub suspend_resume_cycles: u32,
pub snapshot_gb: Option<f64>,
pub launched: bool,
}
impl RunUsage {
pub fn launched() -> RunUsage {
RunUsage {
launched: true,
..RunUsage::default()
}
}
pub fn infer_launched(&self) -> bool {
self.running
.is_some_and(|running| !running.duration().is_zero())
|| implies_an_image(self.image_gb)
}
}
fn implies_an_image(image_gb: Option<f64>) -> bool {
image_gb.is_some_and(|gb| gb != 0.0)
}
pub const DEFAULT_RUN_LABEL: &str = "run";
pub const DEFAULT_ESTIMATE_LABEL: &str = "estimate";
pub fn run_report(
size: SizeClass,
usage: &RunUsage,
rates: &RateTable,
today: CalendarDate,
label: impl Into<String>,
) -> Result<CostReport, Error> {
let snapshot = match usage.snapshot_gb {
Some(gb) => gb_decimal(gb)?,
None => gb_decimal(size.baseline_gb())?,
};
let image_gb = usage.image_gb.map(gb_decimal).transpose()?;
let mut items: Vec<LineItem> = Vec::new();
if let Some(image_gb) = image_gb {
items.push(build_line(usage.image_build));
let held = usage
.image_retained
.unwrap_or(DurationP::Projected(rates.minimum_retention()));
items.push(storage_line(
CostPhase::ImageStorage,
image_gb,
held,
rates,
)?);
} else if usage.image_build.is_some() {
items.push(build_line(usage.image_build));
}
if usage.launched {
let read_gb = image_gb.unwrap_or(snapshot);
items.push(transfer_line(
CostPhase::Launch,
BillingLine::SnapshotRead,
read_gb,
1,
rates,
"; 'launch or resume' shares one rate, and which snapshot a launch reads is \
undocumented",
));
}
if let Some(running) = usage.running {
items.extend(compute_lines(size, running, rates, CostPhase::Running)?);
}
if let Some(suspended) = usage.suspended {
items.push(storage_line(
CostPhase::Suspended,
snapshot,
suspended,
rates,
)?);
}
if usage.suspend_resume_cycles > 0 {
items.push(transfer_line(
CostPhase::Suspend,
BillingLine::SnapshotWrite,
snapshot,
usage.suspend_resume_cycles,
rates,
"",
));
items.push(transfer_line(
CostPhase::Resume,
BillingLine::SnapshotRead,
snapshot,
usage.suspend_resume_cycles,
rates,
"",
));
}
Ok(CostReport {
label: label.into(),
size,
rates: rates.clone(),
items,
staleness: rates.staleness(today),
})
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct PlanUsage {
pub running_seconds: f64,
pub suspended_seconds: f64,
pub image_gb: Option<f64>,
pub image_retained_seconds: Option<f64>,
pub suspend_resume_cycles: u32,
pub snapshot_gb: Option<f64>,
pub launched: bool,
}
impl PlanUsage {
pub fn launched() -> PlanUsage {
PlanUsage {
launched: true,
..PlanUsage::default()
}
}
pub fn infer_launched(&self) -> bool {
self.running_seconds > 0.0 || implies_an_image(self.image_gb)
}
}
pub fn estimate_run(
size: SizeClass,
plan: &PlanUsage,
rates: &RateTable,
today: CalendarDate,
label: impl Into<String>,
) -> Result<CostReport, Error> {
let projected = |seconds: f64| -> Result<Option<DurationP>, Error> {
if seconds == 0.0 {
return Ok(None);
}
Ok(Some(DurationP::projected_secs_f64(seconds)?))
};
let usage = RunUsage {
running: projected(plan.running_seconds)?,
suspended: projected(plan.suspended_seconds)?,
image_build: None,
image_gb: plan.image_gb,
image_retained: plan
.image_retained_seconds
.map(DurationP::projected_secs_f64)
.transpose()?,
suspend_resume_cycles: plan.suspend_resume_cycles,
snapshot_gb: plan.snapshot_gb,
launched: plan.launched,
};
run_report(size, &usage, rates, today, label)
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ResidencyComparison {
size: SizeClass,
hold: DurationP,
cycles: u32,
running: CostReport,
suspended: CostReport,
rates: RateTable,
}
impl ResidencyComparison {
pub fn size(&self) -> SizeClass {
self.size
}
pub fn hold(&self) -> DurationP {
self.hold
}
pub fn cycles(&self) -> u32 {
self.cycles
}
pub fn running(&self) -> &CostReport {
&self.running
}
pub fn suspended(&self) -> &CostReport {
&self.suspended
}
pub fn ratio(&self) -> Decimal {
self.running.total().floor().amount() / self.suspended.total().floor().amount()
}
pub fn per_cycle(&self) -> Result<EstimatedUsd, Error> {
let gb = gb_decimal(self.size.baseline_gb())?;
Ok(EstimatedUsd::new(
gb * (self.rates.snapshot_write_gb() + self.rates.snapshot_read_gb()),
))
}
pub fn break_even_seconds(&self) -> Result<Decimal, Error> {
let vcpu = gb_decimal(self.size.baseline_vcpu())?;
let gb = gb_decimal(self.size.baseline_gb())?;
let running_per_sec = vcpu * self.rates.vcpu_second() + gb * self.rates.gb_second();
let storage_per_sec = gb * self.rates.storage_gb_month() / SECONDS_PER_MONTH;
let floor_sec = seconds_of(self.rates.minimum_retention());
let churn = self.per_cycle()?.amount();
let candidate = (churn + floor_sec * storage_per_sec) / running_per_sec;
if candidate > floor_sec {
return Ok(churn / (running_per_sec - storage_per_sec));
}
Ok(candidate)
}
pub fn break_even_seconds_f64(&self) -> Result<f64, Error> {
let exact = self.break_even_seconds()?;
exact.to_f64().ok_or_else(|| {
Error::invalid_arg(format!(
"the break-even hold of {exact}s does not fit in an f64, so it cannot be reported \
as a JSON number"
))
})
}
pub fn render(&self) -> Result<String, Error> {
Ok([
format!("{} held {}", self.size, self.hold),
format!(" running: {}", self.running.total()),
format!(
" suspended: {} ({} cycle(s) included)",
self.suspended.total(),
self.cycles,
),
format!(
" ratio: {}x cheaper suspended",
self.ratio().round_dp(1)
),
format!(
" per cycle: {} — break-even hold {}s, so avoid churn rather than residency",
self.per_cycle()?,
self.break_even_seconds()?.round_dp(0),
),
]
.join("\n"))
}
}
pub const DEFAULT_RESIDENCY_CYCLES: u32 = 1;
pub const DEFAULT_REPORT_CYCLES: u32 = 0;
pub fn compare_residency(
size: SizeClass,
hold: Duration,
cycles: u32,
rates: &RateTable,
today: CalendarDate,
) -> Result<ResidencyComparison, Error> {
let hold = DurationP::Projected(hold);
let running = run_report(
size,
&RunUsage {
running: Some(hold),
launched: false,
..RunUsage::default()
},
rates,
today,
"left running",
)?;
let suspended = run_report(
size,
&RunUsage {
suspended: Some(hold),
suspend_resume_cycles: cycles,
launched: false,
..RunUsage::default()
},
rates,
today,
"suspended",
)?;
Ok(ResidencyComparison {
size,
hold,
cycles,
running,
suspended,
rates: rates.clone(),
})
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OnBreach {
Warn,
Abort,
}
impl OnBreach {
pub const ALL: [OnBreach; 2] = [OnBreach::Warn, OnBreach::Abort];
pub fn as_str(self) -> &'static str {
match self {
OnBreach::Warn => "warn",
OnBreach::Abort => "abort",
}
}
}
impl FromStr for OnBreach {
type Err = Error;
fn from_str(text: &str) -> Result<OnBreach, Error> {
OnBreach::ALL
.into_iter()
.find(|judgement| judgement.as_str() == text)
.ok_or_else(|| {
Error::invalid_arg(format!(
"{text:?} is not a judgement on a breach: it's \"warn\" or \"abort\", and \
there is no default, because a breach of a lower-bound total has already \
been exceeded by an unknown margin"
))
})
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Budget {
pub max: EstimatedUsd,
pub on_breach: OnBreach,
}
impl Budget {
pub fn new(max: EstimatedUsd, on_breach: OnBreach) -> Budget {
Budget { max, on_breach }
}
pub fn check(&self, report: &CostReport) -> BudgetVerdict {
let total = report.total();
BudgetVerdict {
budget: *self,
floor: total.floor(),
unpriced: total.unpriced_phase_names(),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BudgetVerdict {
budget: Budget,
floor: EstimatedUsd,
unpriced: Vec<&'static str>,
}
impl BudgetVerdict {
pub fn budget(&self) -> Budget {
self.budget
}
pub fn floor(&self) -> EstimatedUsd {
self.floor
}
pub fn is_lower_bound(&self) -> bool {
!self.unpriced.is_empty()
}
pub fn unpriced_phases(&self) -> &[&'static str] {
&self.unpriced
}
pub fn breached(&self) -> bool {
self.floor > self.budget.max
}
pub fn aborts(&self) -> bool {
self.breached() && self.budget.on_breach == OnBreach::Abort
}
pub fn overage(&self) -> Option<EstimatedUsd> {
self.breached()
.then(|| EstimatedUsd::new(self.floor.amount() - self.budget.max.amount()))
}
pub fn basis(&self) -> &'static str {
if self.is_lower_bound() {
"lower-bound"
} else {
"exact"
}
}
pub fn render(&self) -> String {
let floor = self.floor.amount_string();
let caveat = if self.unpriced.is_empty() {
String::new()
} else {
format!(
", and {} line(s) are unpriced ({}), so the true figure is larger by an \
unknown margin",
self.unpriced.len(),
self.unpriced.join(", ")
)
};
match self.overage() {
Some(overage) => format!(
"budget: the {} total ${floor} exceeds --max-cost ${} by {}${}{caveat}",
if self.unpriced.is_empty() {
"estimated"
} else {
"priced-floor"
},
self.budget.max.amount_string(),
if self.unpriced.is_empty() {
""
} else {
"at least "
},
overage.amount_string(),
),
None if self.unpriced.is_empty() => format!(
"budget: the estimated total ${floor} is within --max-cost ${}",
self.max_figure()
),
None => format!(
"budget: the priced floor ${floor} is within --max-cost ${}, but that floor is \
a lower bound{caveat}",
self.max_figure()
),
}
}
pub fn render_dense(&self) -> String {
format!(
"budget\t{}\t{}\t{}\t{}",
self.max_figure(),
self.basis(),
if self.breached() {
"breached"
} else {
"within"
},
self.overage()
.map_or_else(|| "-".to_string(), EstimatedUsd::amount_string),
)
}
pub fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"maxUsd": self.max_figure(),
"onBreach": self.budget.on_breach.as_str(),
"basis": self.basis(),
"breached": self.breached(),
"overageAtLeastUsd": self.overage().map(EstimatedUsd::amount_string),
})
}
fn max_figure(&self) -> String {
self.budget.max.amount().to_string()
}
}
impl LineItem {
pub fn to_json(&self) -> serde_json::Value {
let amount = match &self.amount {
Amount::Estimated(usd) => serde_json::json!({
"kind": "estimated-usd",
"usd": usd.amount().to_string(),
}),
Amount::Unpriced { reason } => serde_json::json!({
"kind": "unpriced",
"reason": reason,
}),
};
serde_json::json!({
"phase": self.phase.as_str(),
"line": self.line.map(|line| line.as_str()),
"quantity": self.quantity.to_string(),
"unit": self.unit,
"amount": amount,
"duration": self.duration.map(|duration| serde_json::json!({
"seconds": duration.seconds_f64(),
"provenance": duration.provenance().as_str(),
})),
"note": self.note,
})
}
}
impl CostReport {
pub fn to_json(&self) -> serde_json::Value {
let size = self.size;
let total = self.total();
serde_json::json!({
"label": self.label,
"size": {
"baselineMib": size.baseline_mib(),
"baselineVcpu": size.baseline_vcpu(),
"peakMib": size.peak_mib(),
"peakVcpu": size.peak_vcpu(),
"headroomMib": size.headroom_mib(),
"describe": size.to_string(),
},
"rates": {
"region": self.rates.region().as_str(),
"retrieved": self.rates.retrieved().to_string(),
"sourceUrl": self.rates.source_url(),
},
"estimated": true,
"fullyMeasured": self.fully_measured(),
"complete": self.is_complete(),
"staleness": self.staleness(),
"items": self.items.iter().map(LineItem::to_json).collect::<Vec<_>>(),
"total": {
"priced": total.floor().amount().to_string(),
"isLowerBound": total.is_lower_bound(),
"render": total.to_string(),
},
})
}
}
impl ResidencyComparison {
pub fn to_json(&self) -> Result<serde_json::Value, Error> {
Ok(serde_json::json!({
"holdSeconds": self.hold.seconds_f64(),
"cycles": self.cycles,
"running": self.running.to_json(),
"suspended": self.suspended.to_json(),
"ratio": self.ratio().to_string(),
"perCycleUsd": self.per_cycle()?.amount().to_string(),
"breakEvenSeconds": self.break_even_seconds_f64()?,
"render": self.render()?,
}))
}
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::*;
use crate::error::ErrorKind;
const MONTH: Duration = Duration::from_secs(730 * 3600);
#[test]
fn a_unix_time_floors_to_its_utc_day() {
let day = |secs| {
CalendarDate::from_unix_secs(secs)
.expect("in range")
.to_string()
};
assert_eq!(day(0), "1970-01-01");
assert_eq!(day(86_399), "1970-01-01");
assert_eq!(day(86_400), "1970-01-02");
assert_eq!(day(1_790_294_400), "2026-09-25");
assert_eq!(day(1_790_294_399), "2026-09-24");
assert_eq!(
CalendarDate::from_unix_secs(0)
.expect("in range")
.day_number(),
0
);
assert_eq!(day(253_402_300_799), "9999-12-31");
}
#[test]
fn a_unix_time_past_the_calendar_is_refused() {
for secs in [1_790_294_400_000, 253_402_300_800, u64::MAX] {
let error = CalendarDate::from_unix_secs(secs).expect_err("past 9999-12-31");
assert_eq!(error.kind(), ErrorKind::InvalidArg, "{secs}");
assert!(error.to_string().contains("milliseconds"), "{error}");
}
}
fn fresh_day() -> CalendarDate {
CalendarDate::from_ymd(2026, 8, 8)
}
fn rates() -> RateTable {
pinned_rates()
}
#[test]
fn a_budget_parses_from_the_text_the_caller_typed() {
assert_eq!(
EstimatedUsd::parse("1.50").expect("a plain decimal"),
EstimatedUsd::new(dec!(1.50))
);
assert_eq!(
EstimatedUsd::parse(" 0.1 ").expect("whitespace is not meaning"),
EstimatedUsd::new(dec!(0.1))
);
assert_eq!(
EstimatedUsd::parse("0").expect("zero is a legitimate budget"),
EstimatedUsd::ZERO
);
}
#[test]
fn a_budget_that_does_not_read_as_money_is_refused() {
for text in ["abc", "", "$1.50", "-0.01"] {
let error = EstimatedUsd::parse(text).expect_err(text);
assert_eq!(error.kind(), ErrorKind::InvalidArg, "{text}");
}
}
fn priced(report: &CostReport, phase: CostPhase) -> Decimal {
let items: Vec<&LineItem> = report.by_phase(phase).collect();
assert_eq!(items.len(), 1, "{phase} should have exactly one line item");
items[0]
.amount
.estimate()
.unwrap_or_else(|| panic!("{phase} should be priced"))
.amount()
}
fn running_report(size: SizeClass, duration: DurationP) -> CostReport {
run_report(
size,
&RunUsage {
running: Some(duration),
launched: false,
..RunUsage::default()
},
&rates(),
fresh_day(),
"run",
)
.expect("a running phase at a documented size prices")
}
#[test]
fn every_rate_byte_matches_the_python_literal() {
let rates = rates();
assert_eq!(rates.region(), &Region::UsEast1);
assert_eq!(rates.vcpu_second(), dec!(0.0000276944));
assert_eq!(rates.gb_second(), dec!(0.0000036667));
assert_eq!(rates.storage_gb_month(), dec!(0.0811111030));
assert_eq!(rates.snapshot_read_gb(), dec!(0.00155));
assert_eq!(rates.snapshot_write_gb(), dec!(0.0038));
assert_eq!(rates.minimum_retention(), Duration::from_secs(604_800));
assert_eq!(rates.retrieved(), CalendarDate::from_ymd(2026, 8, 7));
assert!(rates.source_url().contains("aws.amazon.com"));
assert_eq!(rates.snapshot_write_gb().to_string(), "0.0038");
assert_eq!(rates.storage_gb_month().to_string(), "0.0811111030");
}
#[test]
fn the_storage_rate_is_the_hourly_figure_times_a_seven_hundred_thirty_hour_month() {
assert_eq!(SECONDS_PER_MONTH / dec!(3600), HOURS_PER_MONTH);
assert_eq!(HOURS_PER_MONTH, dec!(730));
assert_eq!(rates().storage_gb_month(), dec!(0.0001111111) * dec!(730));
assert_ne!(
rates().storage_gb_month(),
dec!(0.08),
"the old hand-read value was 1.37% low"
);
}
#[test]
fn the_documented_billing_facts_are_data_not_prose() {
let rates = rates();
assert!(rates.bills_vcpu_and_memory_separately());
assert_eq!(rates.per_request(), Decimal::ZERO);
assert!(!rates.free_tier());
assert_eq!(rates.minimum_billing_increment(), None);
}
#[test]
fn a_rate_table_past_ninety_days_attaches_a_warning_to_the_report() {
let rates = rates();
let just_past = CalendarDate::from_ymd(2026, 11, 6);
assert_eq!(just_past.days_since(rates.retrieved()), 91);
assert!(rates.is_stale(just_past));
let report = run_report(
SizeClass::Mib2048,
&RunUsage {
running: Some(DurationP::Measured(Duration::from_secs(60))),
launched: false,
..RunUsage::default()
},
&rates,
just_past,
"run",
)
.expect("a stale table still prices");
let warning = report
.staleness()
.expect("a report from a stale table carries the warning");
assert!(warning.contains("91 days ago"), "{warning}");
assert!(warning.contains("stale after 90"), "{warning}");
assert!(warning.contains(rates.source_url()), "{warning}");
}
#[test]
fn a_fresh_rate_table_is_silent_and_ninety_days_exactly_is_still_fresh() {
let rates = rates();
let ninety = CalendarDate::from_ymd(2026, 11, 5);
assert_eq!(ninety.days_since(rates.retrieved()), 90);
assert!(!rates.is_stale(ninety));
assert_eq!(rates.staleness(ninety), None);
let report = running_report(
SizeClass::Mib2048,
DurationP::Measured(Duration::from_secs(60)),
);
assert_eq!(report.staleness(), None);
}
#[test]
fn the_day_number_agrees_with_the_gregorian_calendar() {
assert_eq!(CalendarDate::from_ymd(1970, 1, 1).day_number(), 0);
assert_eq!(CalendarDate::from_ymd(2026, 8, 7).day_number(), 20_672);
assert_eq!(CalendarDate::from_ymd(2024, 2, 29).day_number(), 19_782);
assert_eq!(CalendarDate::from_ymd(1969, 12, 31).day_number(), -1);
assert_eq!(
CalendarDate::from_ymd(2024, 3, 1).days_since(CalendarDate::from_ymd(2024, 2, 28)),
2
);
assert_eq!(
CalendarDate::from_ymd(2023, 3, 1).days_since(CalendarDate::from_ymd(2023, 2, 28)),
1
);
}
#[test]
fn the_pinned_dates_are_real_calendar_days() {
for (year, month, day) in [(2026, 8, 7), (2026, 8, 8), (2026, 11, 5), (2026, 11, 6)] {
CalendarDate::try_from_ymd(year, month, day)
.unwrap_or_else(|err| panic!("{year}-{month}-{day} should be a real day: {err}"));
}
}
#[test]
fn a_date_that_is_not_a_calendar_day_is_refused() {
for (year, month, day) in [
(2026, 2, 30),
(2026, 13, 1),
(2026, 0, 1),
(2026, 1, 0),
(2026, 1, 32),
(2023, 2, 29),
] {
let err = CalendarDate::try_from_ymd(year, month, day)
.expect_err("only calendar days date a rate table");
assert_eq!(err.kind(), ErrorKind::InvalidArg, "{year}-{month}-{day}");
}
assert!(
CalendarDate::try_from_ymd(2024, 2, 29).is_ok(),
"a leap day"
);
}
#[test]
fn a_date_renders_as_iso_eight_six_oh_one() {
assert_eq!(CalendarDate::from_ymd(2026, 8, 7).to_string(), "2026-08-07");
assert_eq!(
CalendarDate::from_ymd(2026, 12, 31).to_string(),
"2026-12-31"
);
}
#[test]
fn a_duration_carries_how_it_is_known() {
let timed = DurationP::Measured(Duration::from_secs(60));
let planned = DurationP::Projected(Duration::from_secs(60));
assert_eq!(timed.provenance(), Provenance::Measured);
assert_eq!(planned.provenance(), Provenance::Projected);
assert!(timed.is_measured());
assert!(!planned.is_measured());
assert_ne!(timed, planned);
assert_eq!(timed.duration(), planned.duration());
assert_eq!(timed.to_string(), "60s (measured)");
assert_eq!(planned.to_string(), "60s (projected)");
}
#[test]
fn a_negative_or_non_finite_duration_is_refused_at_the_float_boundary() {
for seconds in [-1.0, -0.001, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
let err = DurationP::measured_secs_f64(seconds)
.expect_err("only a finite non-negative float is a duration");
assert_eq!(err.kind(), ErrorKind::InvalidArg, "{seconds}");
assert!(
err.to_string().contains("credit on the report"),
"must name why: {err}"
);
}
assert_eq!(
DurationP::measured_secs_f64(60.5)
.expect("a plain float is a duration")
.seconds(),
dec!(60.5)
);
}
#[test]
fn every_dollar_figure_renders_as_an_estimate() {
assert_eq!(
EstimatedUsd::new(dec!(1.23)).to_string(),
"~$1.23 (estimated)"
);
let report = running_report(SizeClass::Mib2048, DurationP::Measured(MONTH));
let text = report.render();
assert!(text.contains("estimates derived from published"), "{text}");
assert!(text.contains("only Cost Explorer knows the bill"), "{text}");
assert!(text.contains("2026-08-07"), "{text}");
}
#[test]
fn a_measured_second_still_yields_an_estimated_dollar() {
let report = running_report(
SizeClass::Mib2048,
DurationP::Measured(Duration::from_secs(3600)),
);
assert!(report.fully_measured());
assert!(
report
.items()
.iter()
.all(|item| item.amount.estimate().is_some())
);
assert!(report.total().to_string().contains("estimated"));
}
#[test]
fn an_estimate_is_never_fully_measured_and_a_timed_run_is() {
let projected = estimate_run(
SizeClass::Mib2048,
&PlanUsage {
running_seconds: 3600.0,
..PlanUsage::default()
},
&rates(),
fresh_day(),
"estimate",
)
.expect("a plan prices");
assert!(!projected.fully_measured());
assert!(
running_report(
SizeClass::Mib2048,
DurationP::Measured(Duration::from_secs(3600))
)
.fully_measured()
);
}
#[test]
fn a_sub_cent_figure_and_a_monthly_one_each_render_readably() {
assert_eq!(
EstimatedUsd::new(dec!(0.0000276944)).amount_string(),
"0.000028"
);
assert_eq!(EstimatedUsd::new(dec!(92.0512345)).amount_string(), "92.05");
assert_eq!(EstimatedUsd::new(Decimal::ONE).amount_string(), "1.00");
assert_eq!(
EstimatedUsd::new(dec!(0.999999)).amount_string(),
"0.999999"
);
assert_eq!(EstimatedUsd::new(dec!(1.125)).amount_string(), "1.12");
assert_eq!(EstimatedUsd::new(dec!(1.135)).amount_string(), "1.14");
assert_eq!(EstimatedUsd::new(dec!(2)).amount_string(), "2.00");
}
#[test]
fn the_image_build_is_unpriced_rather_than_free() {
let report = run_report(
SizeClass::Mib2048,
&RunUsage {
image_gb: Some(2.0),
image_build: Some(DurationP::Measured(Duration::from_secs(300))),
..RunUsage::launched()
},
&rates(),
fresh_day(),
"run",
)
.expect("a build prices as unpriced");
let build: Vec<&LineItem> = report.by_phase(CostPhase::ImageBuild).collect();
assert_eq!(build.len(), 1);
let reason = build[0]
.amount
.unpriced_reason()
.expect("the build has no published rate");
assert!(reason.contains("does not publish"), "{reason}");
assert_eq!(
build[0].line, None,
"no billing line exists to attribute it to"
);
assert_eq!(build[0].amount.estimate(), None);
assert_ne!(
build[0].amount,
Amount::Estimated(EstimatedUsd::ZERO),
"unpriced is a claim about the documentation, zero is a claim about the bill"
);
assert_eq!(build[0].amount.kind(), "unpriced");
}
#[test]
fn an_unpriced_line_makes_the_total_a_lower_bound_that_names_it() {
let report = run_report(
SizeClass::Mib2048,
&RunUsage {
image_gb: Some(2.0),
image_build: Some(DurationP::Measured(Duration::from_secs(300))),
..RunUsage::launched()
},
&rates(),
fresh_day(),
"run",
)
.expect("a build prices");
assert!(!report.is_complete());
let total = report.total();
assert!(total.is_lower_bound());
assert!(matches!(total, Total::AtLeast { .. }));
assert_eq!(total.unpriced_reasons().len(), 1);
assert!(total.unpriced_reasons()[0].contains("does not publish"));
let rendered = total.to_string();
assert!(rendered.starts_with("at least "), "{rendered}");
assert!(rendered.contains("1 unpriced"), "{rendered}");
let expected: EstimatedUsd = report
.priced()
.map(|item| item.amount.estimate().expect("priced"))
.sum();
assert_eq!(total.floor(), expected);
}
#[test]
fn the_rendered_lower_bound_is_the_oracles_string_verbatim() {
let report = run_report(
SizeClass::Mib2048,
&RunUsage {
running: Some(DurationP::Measured(Duration::from_secs(3600))),
image_build: Some(DurationP::Measured(Duration::from_secs(300))),
image_gb: Some(2.0),
..RunUsage::launched()
},
&rates(),
fresh_day(),
"run",
)
.expect("a build prices");
assert_eq!(
report.total().to_string(),
"at least ~$0.166533 (estimated), plus 1 unpriced (image-build)"
);
let amounts = [
(CostPhase::Running, Amount::unpriced("second")),
(CostPhase::ImageBuild, Amount::unpriced("first")),
(CostPhase::ImageBuild, Amount::unpriced("also first")),
];
let total = Total::of(amounts.iter().map(|(phase, amount)| (*phase, amount)));
assert_eq!(total.unpriced_phase_names(), ["image-build", "running"]);
assert_eq!(
total.to_string(),
"at least ~$0.000000 (estimated), plus 3 unpriced (image-build, running)",
"three lines, two names — the count is lines and the parenthetical is phases"
);
assert_eq!(total.unpriced_reasons(), ["second", "first", "also first"]);
}
#[test]
fn every_phase_parses_back_from_its_display_and_nothing_else_does() {
for phase in CostPhase::ALL {
assert_eq!(
phase.as_str().parse::<CostPhase>().expect("a known phase"),
phase,
"{} did not round-trip through as_str",
phase.as_str()
);
assert_eq!(
phase
.to_string()
.parse::<CostPhase>()
.expect("a known phase"),
phase,
"{phase} did not round-trip through Display"
);
match phase {
CostPhase::ImageBuild
| CostPhase::ImageStorage
| CostPhase::Launch
| CostPhase::Running
| CostPhase::Suspended
| CostPhase::Suspend
| CostPhase::Resume => {}
}
}
assert_eq!(
CostPhase::ALL.len(),
7,
"the seven phases the billing model has"
);
for wrong in ["suspending", "image_build", "RUNNING", "", "total"] {
let error = wrong
.parse::<CostPhase>()
.expect_err("only the seven spellings parse");
assert_eq!(error.kind(), ErrorKind::InvalidArg);
let message = error.to_string();
for phase in CostPhase::ALL {
assert!(
message.contains(phase.as_str()),
"the refusal has to offer every phase so a typo is self-correcting, \
{phase} missing from: {message}"
);
}
}
}
#[test]
fn every_rendered_line_puts_its_amount_in_the_same_column() {
let report = run_report(
SizeClass::Mib2048,
&RunUsage {
running: Some(DurationP::Measured(Duration::from_secs(3600))),
image_build: Some(DurationP::Measured(Duration::from_secs(300))),
image_gb: Some(2.0),
suspend_resume_cycles: 1,
..RunUsage::launched()
},
&rates(),
fresh_day(),
"run",
)
.expect("a report");
let text = report.render();
let offsets: Vec<(usize, &str)> = text
.lines()
.filter(|line| line.starts_with(" "))
.map(|line| {
let amount = line
.find("~$")
.or_else(|| line.find("unpriced"))
.unwrap_or_else(|| panic!("every line has an amount: {line}"));
(amount, line)
})
.collect();
assert!(offsets.len() >= 7, "{text}");
let (first, first_line) = offsets[0];
for (offset, line) in &offsets {
assert_eq!(
*offset, first,
"the amount column has to agree across lines:\n{first_line}\n{line}"
);
}
assert_eq!(
format!("{:<14}|", CostPhase::Launch),
"launch |",
"a phase in a width-bearing format slot has to pad"
);
}
#[test]
fn a_total_over_only_priced_lines_is_exact_and_names_nothing() {
let amounts = [Amount::estimated(dec!(1)), Amount::estimated(dec!(2))];
let total = Total::of(amounts.iter().map(|amount| (CostPhase::Running, amount)));
assert_eq!(total, Total::Exact(EstimatedUsd::new(dec!(3))));
assert!(!total.is_lower_bound());
assert!(total.unpriced_reasons().is_empty());
assert_eq!(total.to_string(), "~$3.00 (estimated)");
assert_eq!(
Total::of(std::iter::empty()),
Total::Exact(EstimatedUsd::ZERO)
);
}
#[test]
fn a_report_with_no_image_is_complete() {
let report = running_report(
SizeClass::Mib2048,
DurationP::Measured(Duration::from_secs(60)),
);
assert!(report.is_complete());
assert!(!report.total().is_lower_bound());
assert!(matches!(report.total(), Total::Exact(_)));
}
#[test]
fn running_bills_vcpu_and_memory_as_two_separate_lines() {
let report = running_report(
SizeClass::Mib2048,
DurationP::Measured(Duration::from_secs(3600)),
);
let lines: Vec<Option<BillingLine>> = report
.by_phase(CostPhase::Running)
.map(|item| item.line)
.collect();
assert_eq!(lines, [Some(BillingLine::Vcpu), Some(BillingLine::Memory)]);
}
#[test]
fn running_bills_the_baseline_and_never_the_peak() {
let size = SizeClass::Mib2048;
let report = running_report(size, DurationP::Measured(Duration::from_secs(3600)));
let rates = rates();
let memory: Vec<&LineItem> = report
.by_phase(CostPhase::Running)
.filter(|item| item.line == Some(BillingLine::Memory))
.collect();
assert_eq!(
memory[0].amount.estimate().expect("priced").amount(),
dec!(2) * dec!(3600) * rates.gb_second()
);
let vcpu: Vec<&LineItem> = report
.by_phase(CostPhase::Running)
.filter(|item| item.line == Some(BillingLine::Vcpu))
.collect();
assert_eq!(
vcpu[0].amount.estimate().expect("priced").amount(),
dec!(1) * dec!(3600) * rates.vcpu_second()
);
assert_eq!(memory[0].quantity, dec!(2) * dec!(3600));
assert_ne!(
memory[0].quantity,
gb_decimal(size.peak_gb()).expect("a documented peak") * dec!(3600),
"the memory line must read baseline_gb, not peak_gb"
);
assert_ne!(
vcpu[0].quantity,
gb_decimal(size.peak_vcpu()).expect("a documented peak") * dec!(3600),
"the vcpu line must read baseline_vcpu, not peak_vcpu"
);
}
#[test]
fn every_size_class_bills_its_own_baseline() {
let rates = rates();
for size in SizeClass::ALL {
let report = running_report(size, DurationP::Measured(Duration::from_secs(1)));
let memory: Vec<&LineItem> = report
.by_phase(CostPhase::Running)
.filter(|item| item.line == Some(BillingLine::Memory))
.collect();
let expected = gb_decimal(size.baseline_gb()).expect("a documented baseline");
assert_eq!(memory[0].quantity, expected, "{size:?}");
assert_eq!(
memory[0].amount.estimate().expect("priced").amount(),
expected * rates.gb_second(),
"{size:?}"
);
}
}
#[test]
fn a_month_of_running_costs_about_a_hundred_dollars() {
let report = running_report(SizeClass::Mib2048, DurationP::Projected(MONTH));
let total = report.total().floor().amount();
assert!(
dec!(80) < total && total < dec!(120),
"a month at baseline should be roughly $100, got {total}"
);
}
#[test]
fn a_suspended_vm_pays_storage_and_no_compute() {
let report = run_report(
SizeClass::Mib2048,
&RunUsage {
suspended: Some(DurationP::Measured(MONTH)),
launched: false,
..RunUsage::default()
},
&rates(),
fresh_day(),
"suspended",
)
.expect("a suspended hold prices");
let lines: Vec<Option<BillingLine>> = report
.by_phase(CostPhase::Suspended)
.map(|item| item.line)
.collect();
assert_eq!(lines, [Some(BillingLine::SnapshotStorage)]);
assert!(
!report
.items()
.iter()
.any(|item| item.line == Some(BillingLine::Vcpu)
|| item.line == Some(BillingLine::Memory)),
"a frozen VM has no compute line at all"
);
let total = report.total().floor().amount();
assert!(dec!(0.10) < total && total < dec!(0.25), "{total}");
}
#[test]
fn image_storage_bills_the_one_week_minimum_for_a_sixty_second_image() {
let rates = rates();
let report = run_report(
SizeClass::Mib2048,
&RunUsage {
image_gb: Some(2.0),
image_build: Some(DurationP::Measured(Duration::from_secs(300))),
image_retained: Some(DurationP::Measured(Duration::from_secs(60))),
launched: false,
..RunUsage::default()
},
&rates,
fresh_day(),
"build",
)
.expect("a held image prices");
let week = dec!(604800);
assert_eq!(
priced(&report, CostPhase::ImageStorage),
dec!(2) * week / SECONDS_PER_MONTH * rates.storage_gb_month()
);
let amount = priced(&report, CostPhase::ImageStorage);
assert!(
dec!(0.03) < amount && amount < dec!(0.05),
"roughly four cents, got {amount}"
);
let storage: Vec<&LineItem> = report.by_phase(CostPhase::ImageStorage).collect();
assert!(
storage[0].note.contains("minimum retention"),
"{}",
storage[0].note
);
assert!(storage[0].note.contains("604800s"), "{}", storage[0].note);
assert_eq!(
storage[0].duration,
Some(DurationP::Measured(Duration::from_secs(60)))
);
}
#[test]
fn the_retention_note_reads_its_day_count_off_the_rate_row() {
let week = rates();
assert_eq!(week.minimum_retention_days(), 7);
assert_eq!(
week.minimum_retention(),
Duration::from_secs(week.minimum_retention_days() * 24 * 60 * 60),
"the days accessor and the duration must describe the same window"
);
let mut fortnight = week.clone();
fortnight.minimum_retention = Duration::from_secs(14 * 24 * 60 * 60);
assert_eq!(fortnight.minimum_retention_days(), 14);
let item = storage_line(
CostPhase::ImageStorage,
dec!(2),
DurationP::Measured(Duration::from_secs(60)),
&fortnight,
)
.expect("a floored hold prices");
assert!(
item.note.contains("14-day minimum retention"),
"the note must quote the row's own window: {}",
item.note
);
assert!(
!item.note.contains("7-day"),
"a second day-count convention survived in the message: {}",
item.note
);
}
#[test]
fn a_long_hold_bills_actual_time_rather_than_the_minimum() {
let rates = rates();
let report = run_report(
SizeClass::Mib2048,
&RunUsage {
image_gb: Some(2.0),
image_retained: Some(DurationP::Measured(MONTH)),
launched: false,
..RunUsage::default()
},
&rates,
fresh_day(),
"build",
)
.expect("a long hold prices");
assert_eq!(
priced(&report, CostPhase::ImageStorage),
dec!(2) * rates.storage_gb_month(),
);
let storage: Vec<&LineItem> = report.by_phase(CostPhase::ImageStorage).collect();
assert!(
!storage[0].note.contains("minimum retention"),
"the note must not claim a floor that was not applied: {}",
storage[0].note
);
}
#[test]
fn a_briefly_held_suspend_snapshot_bills_the_same_floor() {
let report = run_report(
SizeClass::Mib2048,
&RunUsage {
suspended: Some(DurationP::Measured(Duration::from_secs(30))),
launched: false,
..RunUsage::default()
},
&rates(),
fresh_day(),
"suspended",
)
.expect("a brief suspension prices");
let items: Vec<&LineItem> = report.by_phase(CostPhase::Suspended).collect();
assert!(
items[0].note.contains("minimum retention"),
"{}",
items[0].note
);
assert_eq!(
priced(&report, CostPhase::Suspended),
dec!(2) * dec!(604800) / SECONDS_PER_MONTH * rates().storage_gb_month(),
);
}
#[test]
fn a_suspend_resume_cycle_bills_a_write_and_a_read() {
let rates = rates();
let report = run_report(
SizeClass::Mib2048,
&RunUsage {
suspend_resume_cycles: 1,
launched: false,
..RunUsage::default()
},
&rates,
fresh_day(),
"cycle",
)
.expect("a cycle prices");
assert_eq!(
priced(&report, CostPhase::Suspend),
dec!(2) * rates.snapshot_write_gb()
);
assert_eq!(
priced(&report, CostPhase::Resume),
dec!(2) * rates.snapshot_read_gb()
);
assert_ne!(
rates.snapshot_write_gb(),
rates.snapshot_read_gb(),
"the two rates differ, which is why the two lines must not share one"
);
let total = report.total().floor().amount();
assert!(dec!(0.010) < total && total < dec!(0.012), "{total}");
}
#[test]
fn cycles_scale_the_transition_cost() {
let cycles_cost = |count: u32| {
run_report(
SizeClass::Mib2048,
&RunUsage {
suspend_resume_cycles: count,
launched: false,
..RunUsage::default()
},
&rates(),
fresh_day(),
"cycle",
)
.expect("a cycle prices")
.total()
.floor()
.amount()
};
assert_eq!(cycles_cost(10), cycles_cost(1) * dec!(10));
assert_eq!(cycles_cost(0), Decimal::ZERO);
}
#[test]
fn a_smaller_baseline_is_a_real_cost_lever() {
let small = running_report(SizeClass::Mib512, DurationP::Projected(MONTH))
.total()
.floor()
.amount();
let large = running_report(SizeClass::Mib8192, DurationP::Projected(MONTH))
.total()
.floor()
.amount();
assert_eq!(large, small * dec!(16));
}
#[test]
fn an_estimate_labels_every_duration_projected() {
let report = estimate_run(
SizeClass::Mib2048,
&PlanUsage {
running_seconds: 3600.0,
suspended_seconds: 600.0,
image_gb: Some(2.0),
..PlanUsage::launched()
},
&rates(),
fresh_day(),
"estimate",
)
.expect("a plan prices");
let durations: Vec<DurationP> = report
.items()
.iter()
.filter_map(|item| item.duration)
.collect();
assert!(
!durations.is_empty(),
"the estimate must attribute durations, not just totals"
);
for duration in durations {
assert_eq!(
duration.provenance(),
Provenance::Projected,
"every duration in a plan is projected"
);
}
assert!(!report.fully_measured());
}
#[test]
fn an_estimate_and_a_measured_run_agree_on_the_arithmetic() {
let projected = estimate_run(
SizeClass::Mib2048,
&PlanUsage {
running_seconds: 3600.0,
..PlanUsage::default()
},
&rates(),
fresh_day(),
"estimate",
)
.expect("a plan prices");
let measured = running_report(
SizeClass::Mib2048,
DurationP::Measured(Duration::from_secs(3600)),
);
assert_eq!(projected.total(), measured.total());
let strip = |report: &CostReport| -> Vec<(CostPhase, Option<BillingLine>, Decimal)> {
report
.items()
.iter()
.map(|item| (item.phase, item.line, item.quantity))
.collect()
};
assert_eq!(strip(&projected), strip(&measured));
assert!(!projected.fully_measured() && measured.fully_measured());
}
#[test]
fn a_plan_with_no_phases_has_no_durations_at_all() {
let report = estimate_run(
SizeClass::Mib2048,
&PlanUsage::default(),
&rates(),
fresh_day(),
"estimate",
)
.expect("an empty plan prices");
assert!(report.items().is_empty());
assert_eq!(report.total(), Total::Exact(EstimatedUsd::ZERO));
assert!(!report.fully_measured());
}
#[test]
fn an_estimate_with_an_image_is_a_lower_bound_too() {
let report = estimate_run(
SizeClass::Mib2048,
&PlanUsage {
image_gb: Some(2.0),
..PlanUsage::launched()
},
&rates(),
fresh_day(),
"estimate",
)
.expect("a plan with an image prices");
assert!(!report.is_complete());
assert!(report.total().is_lower_bound());
let build: Vec<&LineItem> = report.by_phase(CostPhase::ImageBuild).collect();
assert_eq!(build[0].duration, None);
assert_eq!(build[0].unit, "seconds (untimed)");
}
#[test]
fn an_off_table_baseline_never_reaches_a_cost_figure() {
let err =
SizeClass::from_baseline_mib(1500).expect_err("1500 MiB is not a documented baseline");
assert_eq!(err.kind(), ErrorKind::InvalidArg);
assert!(
err.to_string()
.contains("not a documented size class baseline")
);
let size = SizeClass::from_baseline_mib(1024).expect("1024 MiB is documented");
assert_eq!(size.baseline_gb(), 1.0);
}
#[test]
fn a_non_finite_or_negative_size_is_refused_at_the_float_boundary() {
for gb in [-1.0, f64::NAN, f64::INFINITY] {
let err = gb_decimal(gb).expect_err("only a finite non-negative size prices");
assert_eq!(err.kind(), ErrorKind::InvalidArg, "{gb}");
}
assert!(gb_decimal(1e30).is_err());
assert_eq!(gb_decimal(0.1).expect("0.1 is representable"), dec!(0.1));
assert_eq!(gb_decimal(0.25).expect("0.25 is representable"), dec!(0.25));
}
fn recorded_catalog() -> RateCatalog {
RateCatalog::new()
.with_entry(
"AWS-Lambda-MicroVM-vCPU-Second-ARM",
"vCPU-Seconds",
dec!(0.0000276944),
)
.with_entry(
"AWS-Lambda-MicroVM-vCPU-Second",
"vCPU-Seconds",
dec!(0.0000326557),
)
.with_entry(
"AWS-Lambda-MicroVM-Memory-GB-Second-ARM",
"GB-Seconds",
dec!(0.0000036667),
)
.with_entry(
"AWS-Lambda-MicroVM-Memory-GB-Second",
"GB-Seconds",
dec!(0.0000043235),
)
.with_entry(
"AWS-Lambda-MicroVM-Snapshot-Storage-GB-Hour",
"GB-Hours",
dec!(0.0001111111),
)
.with_entry(
"AWS-Lambda-MicroVM-Snapshot-Read-GB",
"GB",
dec!(0.0015467699),
)
.with_entry(
"AWS-Lambda-MicroVM-Snapshot-Write-GB",
"GB",
dec!(0.0037977138),
)
}
fn without(group: &str) -> RateCatalog {
let mut catalog = RateCatalog::new();
for line in CatalogLine::ALL {
for candidate in [line.group()].into_iter().chain(line.x86_group()) {
if candidate == group {
continue;
}
let entry = recorded_catalog()
.matching(candidate)
.first()
.map(|entry| (*entry).clone());
if let Some(entry) = entry {
catalog = catalog.with_entry(entry.group, entry.unit, entry.usd);
}
}
}
catalog
}
#[test]
fn the_recorded_catalog_fills_a_table_that_matches_the_pinned_one() {
let fetched = RateTable::from_catalog(
Region::UsEast1,
"aws pricing api",
CalendarDate::from_ymd(2026, 8, 7),
&recorded_catalog(),
)
.expect("the recorded catalog is complete");
assert_eq!(fetched.vcpu_second(), rates().vcpu_second());
assert_eq!(fetched.gb_second(), rates().gb_second());
assert_eq!(fetched.storage_gb_month(), rates().storage_gb_month());
for (fetched, pinned) in [
(fetched.snapshot_read_gb(), rates().snapshot_read_gb()),
(fetched.snapshot_write_gb(), rates().snapshot_write_gb()),
] {
let relative = (fetched - pinned).abs() / pinned;
assert!(relative < dec!(0.005), "{fetched} vs {pinned}");
assert_ne!(fetched, pinned, "the pinned figure really is a rounding");
}
assert_eq!(fetched.minimum_retention(), MINIMUM_RETENTION);
}
#[test]
fn a_catalog_with_only_the_x86_compute_line_is_refused_not_substituted() {
let catalog = without("AWS-Lambda-MicroVM-vCPU-Second-ARM");
let err = RateTable::from_catalog(
Region::UsEast1,
"aws pricing api",
CalendarDate::from_ymd(2026, 8, 7),
&catalog,
)
.expect_err("a missing ARM line is refused, never substituted");
assert_eq!(err.kind(), ErrorKind::InvalidArg);
let message = err.to_string();
assert!(message.contains("ARM64-only"), "{message}");
assert!(
message.contains("0.0000326557"),
"the error must name the rate it refuses to substitute: {message}"
);
assert!(message.contains("18%"), "{message}");
assert!(message.contains("vcpu_second"), "{message}");
}
#[test]
fn the_memory_line_refuses_its_x86_sibling_too() {
let err = RateTable::from_catalog(
Region::UsEast1,
"aws pricing api",
CalendarDate::from_ymd(2026, 8, 7),
&without("AWS-Lambda-MicroVM-Memory-GB-Second-ARM"),
)
.expect_err("a missing ARM memory line is refused");
let message = err.to_string();
assert!(message.contains("ARM64-only"), "{message}");
assert!(message.contains("0.0000043235"), "{message}");
assert!(message.contains("gb_second"), "{message}");
}
#[test]
fn both_compute_rates_missing_still_names_the_field() {
let mut catalog = without("AWS-Lambda-MicroVM-Memory-GB-Second-ARM");
catalog = {
let mut rebuilt = RateCatalog::new();
for entry in catalog.entries.iter() {
if entry.group != "AWS-Lambda-MicroVM-Memory-GB-Second" {
rebuilt = rebuilt.with_entry(&*entry.group, &*entry.unit, entry.usd);
}
}
rebuilt
};
let err = RateTable::from_catalog(
Region::UsEast1,
"aws pricing api",
CalendarDate::from_ymd(2026, 8, 7),
&catalog,
)
.expect_err("neither compute line means no table");
let message = err.to_string();
assert!(message.contains("gb_second"), "{message}");
assert!(message.contains("neither"), "{message}");
}
#[test]
fn a_missing_snapshot_rate_names_no_x86_sibling() {
let err = RateTable::from_catalog(
Region::UsEast1,
"aws pricing api",
CalendarDate::from_ymd(2026, 8, 7),
&without("AWS-Lambda-MicroVM-Snapshot-Write-GB"),
)
.expect_err("a missing snapshot rate means no table");
let message = err.to_string();
assert!(message.contains("snapshot_write_gb"), "{message}");
assert!(!message.contains("ARM64-only"), "{message}");
}
#[test]
fn an_empty_catalog_reads_as_an_unpriced_region_not_a_renamed_line() {
let err = RateTable::from_catalog(
Region::unlisted("eu-central-1"),
"aws pricing api",
CalendarDate::from_ymd(2026, 8, 7),
&RateCatalog::new(),
)
.expect_err("an empty catalog means no table");
let message = err.to_string();
assert!(message.contains("AccessDeniedException"), "{message}");
assert!(message.contains("null message"), "{message}");
assert!(
message.contains("before auditing a policy"),
"the caveat that saves someone reading an IAM policy that is fine: {message}"
);
assert!(!message.contains("ARM64-only"), "{message}");
}
#[test]
fn a_restated_unit_is_refused_rather_than_silently_rescaled() {
let catalog = without("AWS-Lambda-MicroVM-Snapshot-Storage-GB-Hour").with_entry(
"AWS-Lambda-MicroVM-Snapshot-Storage-GB-Hour",
"GB-Months",
dec!(0.0001111111),
);
let err = RateTable::from_catalog(
Region::UsEast1,
"aws pricing api",
CalendarDate::from_ymd(2026, 8, 7),
&catalog,
)
.expect_err("a restated unit is refused");
let message = err.to_string();
assert!(message.contains("quoted per \"GB-Months\""), "{message}");
assert!(message.contains("still look plausible"), "{message}");
}
#[test]
fn two_products_for_one_group_is_refused_rather_than_arbitrated() {
let catalog = recorded_catalog().with_entry(
"AWS-Lambda-MicroVM-vCPU-Second-ARM",
"vCPU-Seconds",
dec!(0.0000999999),
);
let err = RateTable::from_catalog(
Region::UsEast1,
"aws pricing api",
CalendarDate::from_ymd(2026, 8, 7),
&catalog,
)
.expect_err("two products for one group is refused");
assert!(err.to_string().contains("silently select a rate"), "{err}");
}
#[test]
fn a_regional_table_needs_no_second_arithmetic_path() {
let tokyo_catalog = RateCatalog::new()
.with_entry(
"AWS-Lambda-MicroVM-vCPU-Second-ARM",
"vCPU-Seconds",
dec!(0.0000322421),
)
.with_entry(
"AWS-Lambda-MicroVM-Memory-GB-Second-ARM",
"GB-Seconds",
dec!(0.0000042688),
)
.with_entry(
"AWS-Lambda-MicroVM-Snapshot-Storage-GB-Hour",
"GB-Hours",
dec!(0.0001333333),
)
.with_entry(
"AWS-Lambda-MicroVM-Snapshot-Read-GB",
"GB",
dec!(0.0018548941),
)
.with_entry(
"AWS-Lambda-MicroVM-Snapshot-Write-GB",
"GB",
dec!(0.0046556039),
);
let tokyo = RateTable::from_catalog(
Region::ApNortheast1,
"aws pricing api",
fresh_day(),
&tokyo_catalog,
)
.expect("Tokyo's catalog is complete");
assert!(tokyo.vcpu_second() / rates().vcpu_second() > dec!(1.16));
assert!(tokyo.snapshot_write_gb() / rates().snapshot_write_gb() > dec!(1.22));
let here = run_report(
SizeClass::Mib2048,
&RunUsage {
running: Some(DurationP::Measured(Duration::from_secs(3600))),
launched: false,
..RunUsage::default()
},
&tokyo,
fresh_day(),
"run",
)
.expect("a Tokyo report prices");
let there = running_report(
SizeClass::Mib2048,
DurationP::Measured(Duration::from_secs(3600)),
);
assert!(here.total().floor() > there.total().floor());
assert!(here.render().contains("ap-northeast-1"));
}
#[test]
fn the_catalog_lines_match_the_python_rate_line_table() {
let described: Vec<(&str, &str, &str, Option<&str>)> = CatalogLine::ALL
.iter()
.map(|line| (line.field(), line.group(), line.unit(), line.x86_group()))
.collect();
assert_eq!(
described,
[
(
"vcpu_second",
"AWS-Lambda-MicroVM-vCPU-Second-ARM",
"vCPU-Seconds",
Some("AWS-Lambda-MicroVM-vCPU-Second")
),
(
"gb_second",
"AWS-Lambda-MicroVM-Memory-GB-Second-ARM",
"GB-Seconds",
Some("AWS-Lambda-MicroVM-Memory-GB-Second")
),
(
"storage_gb_month",
"AWS-Lambda-MicroVM-Snapshot-Storage-GB-Hour",
"GB-Hours",
None
),
(
"snapshot_read_gb",
"AWS-Lambda-MicroVM-Snapshot-Read-GB",
"GB",
None
),
(
"snapshot_write_gb",
"AWS-Lambda-MicroVM-Snapshot-Write-GB",
"GB",
None
),
]
);
assert!(CatalogLine::SnapshotStorageGbHour.is_per_hour());
assert_eq!(
CatalogLine::ALL
.iter()
.filter(|line| line.is_per_hour())
.count(),
1
);
}
fn month_comparison() -> ResidencyComparison {
compare_residency(SizeClass::Mib2048, MONTH, 1, &rates(), fresh_day())
.expect("a month-long comparison prices")
}
#[test]
fn suspended_is_two_orders_of_magnitude_cheaper_than_running() {
assert!(month_comparison().ratio() > dec!(100));
}
#[test]
fn the_comparison_includes_the_per_cycle_cost() {
let comparison = month_comparison();
let rates = rates();
assert_eq!(
comparison.per_cycle().expect("a documented size").amount(),
dec!(2) * (rates.snapshot_write_gb() + rates.snapshot_read_gb())
);
let rendered = comparison.render().expect("a comparison renders");
assert!(rendered.contains("avoid churn"), "{rendered}");
assert!(rendered.contains("break-even hold"), "{rendered}");
}
#[test]
fn the_break_even_hold_at_two_gigabytes_matches_the_python_oracle() {
let comparison = month_comparison();
let exact = comparison.break_even_seconds().expect("a documented size");
let oracle = dec!(1371.291648347883680961978771);
assert!(
(exact - oracle).abs() < dec!(0.000000001),
"the Rust figure {exact} must match the Python oracle {oracle}"
);
assert_eq!(
comparison
.break_even_seconds_f64()
.expect("a documented size"),
1371.2916483478837
);
assert_eq!(
comparison.per_cycle().expect("a documented size").amount(),
dec!(0.01070)
);
assert_eq!(comparison.ratio().round_dp(4), dec!(532.3380));
}
#[test]
fn churn_below_break_even_costs_more_than_leaving_the_vm_running() {
let break_even = month_comparison()
.break_even_seconds()
.expect("a documented size");
assert!(break_even > Decimal::ZERO);
let suspended_beats_running = |hold: Duration| {
let comparison = compare_residency(SizeClass::Mib2048, hold, 1, &rates(), fresh_day())
.expect("a comparison prices");
comparison.suspended().total().floor() < comparison.running().total().floor()
};
let at = |factor: Decimal| {
Duration::from_secs_f64(
(break_even * factor)
.to_f64()
.expect("the break-even hold fits in an f64"),
)
};
assert!(
!suspended_beats_running(at(dec!(0.9))),
"just under the break-even hold, a cycle must lose money"
);
assert!(
suspended_beats_running(at(dec!(1.1))),
"just over it, a cycle must save"
);
}
#[test]
fn the_comparison_excludes_the_image_so_the_ratio_is_not_diluted() {
let comparison = month_comparison();
for report in [comparison.running(), comparison.suspended()] {
let phases: Vec<CostPhase> = report.items().iter().map(|item| item.phase).collect();
assert!(!phases.contains(&CostPhase::ImageBuild), "{phases:?}");
assert!(!phases.contains(&CostPhase::ImageStorage), "{phases:?}");
assert!(report.is_complete());
assert!(!phases.contains(&CostPhase::Launch), "{phases:?}");
}
}
#[test]
fn a_comparison_is_never_a_measured_report() {
let comparison = month_comparison();
assert_eq!(comparison.hold().provenance(), Provenance::Projected);
assert!(!comparison.running().fully_measured());
assert!(!comparison.suspended().fully_measured());
}
#[test]
fn a_zero_length_hold_still_has_a_finite_ratio() {
let comparison =
compare_residency(SizeClass::Mib2048, Duration::ZERO, 1, &rates(), fresh_day())
.expect("a zero hold prices");
assert!(comparison.suspended().total().floor().amount() > Decimal::ZERO);
assert_eq!(comparison.ratio(), Decimal::ZERO);
}
fn plausible_seconds() -> impl Strategy<Value = u64> {
prop_oneof![
4 => 0u64..604_800,
3 => 604_800u64..(730 * 3600 * 12),
2 => 604_795u64..=604_805,
1 => 0u64..u32::MAX as u64,
]
}
proptest! {
#[test]
fn a_total_is_a_lower_bound_exactly_when_something_is_unpriced(
figures in prop::collection::vec(
(
prop::sample::select(&[
CostPhase::ImageBuild,
CostPhase::Running,
CostPhase::Suspended,
][..]),
prop_oneof![
(0u64..1_000_000).prop_map(|cents| Amount::estimated(
Decimal::from(cents) / dec!(100)
)),
"[a-z ]{1,20}".prop_map(Amount::unpriced),
],
),
0..12,
),
) {
let expected_floor: Decimal = figures
.iter()
.filter_map(|(_, amount)| amount.estimate())
.map(EstimatedUsd::amount)
.sum();
let expected_reasons: Vec<String> = figures
.iter()
.filter_map(|(_, amount)| amount.unpriced_reason())
.map(str::to_string)
.collect();
let expected_names: Vec<&str> = {
let mut names: Vec<&str> = figures
.iter()
.filter(|(_, amount)| amount.unpriced_reason().is_some())
.map(|(phase, _)| phase.as_str())
.collect();
names.sort_unstable();
names.dedup();
names
};
let total = Total::of(figures.iter().map(|(phase, amount)| (*phase, amount)));
prop_assert_eq!(total.is_lower_bound(), !expected_reasons.is_empty());
prop_assert_eq!(total.floor().amount(), expected_floor);
prop_assert_eq!(total.unpriced_reasons(), expected_reasons.as_slice());
prop_assert_eq!(total.unpriced_phase_names(), expected_names.as_slice());
match &total {
Total::Exact(_) => prop_assert!(expected_reasons.is_empty()),
Total::AtLeast { unpriced, .. } => prop_assert!(!unpriced.is_empty()),
}
prop_assert_eq!(
total.to_string().starts_with("at least "),
total.is_lower_bound()
);
if total.is_lower_bound() {
prop_assert_eq!(
total.to_string(),
format!(
"at least {}, plus {} unpriced ({})",
total.floor(),
expected_reasons.len(),
expected_names.join(", ")
)
);
}
}
#[test]
fn compute_figures_are_exactly_the_decimal_reference(
seconds in plausible_seconds(),
class_index in 0usize..5,
) {
let size = SizeClass::ALL[class_index];
let rates = rates();
let duration = Duration::from_secs(seconds);
let report = run_report(
size,
&RunUsage {
running: Some(DurationP::Measured(duration)),
launched: false,
..RunUsage::default()
},
&rates,
fresh_day(),
"run",
)?;
let exact_seconds = Decimal::from(seconds);
let vcpu = gb_decimal(size.baseline_vcpu())?;
let memory = gb_decimal(size.baseline_gb())?;
let expected_vcpu = vcpu * exact_seconds * rates.vcpu_second();
let expected_memory = memory * exact_seconds * rates.gb_second();
let mut lines = report.by_phase(CostPhase::Running);
let vcpu_line = lines.next().expect("a vcpu line");
let memory_line = lines.next().expect("a memory line");
prop_assert_eq!(vcpu_line.amount.estimate().expect("priced").amount(), expected_vcpu);
prop_assert_eq!(memory_line.amount.estimate().expect("priced").amount(), expected_memory);
prop_assert_eq!(
report.total(),
Total::Exact(EstimatedUsd::new(expected_vcpu + expected_memory))
);
prop_assert_eq!(memory_line.quantity, memory * exact_seconds);
if seconds > 0 {
prop_assert_ne!(
memory_line.quantity,
gb_decimal(size.peak_gb())? * exact_seconds
);
}
}
#[test]
fn storage_bills_the_greater_of_the_hold_and_the_retention_floor(
seconds in plausible_seconds(),
) {
let rates = rates();
let report = run_report(
SizeClass::Mib2048,
&RunUsage {
suspended: Some(DurationP::Measured(Duration::from_secs(seconds))),
launched: false,
..RunUsage::default()
},
&rates,
fresh_day(),
"suspended",
)?;
let floor = seconds_of(rates.minimum_retention());
let billed = Decimal::from(seconds).max(floor);
let expected = dec!(2) * billed / SECONDS_PER_MONTH;
let items: Vec<&LineItem> = report.by_phase(CostPhase::Suspended).collect();
prop_assert_eq!(items[0].quantity, expected);
prop_assert_eq!(
items[0].amount.estimate().expect("priced").amount(),
expected * rates.storage_gb_month()
);
prop_assert_eq!(
items[0].note.contains("minimum retention"),
Decimal::from(seconds) < floor
);
prop_assert!(items[0].quantity >= dec!(2) * floor / SECONDS_PER_MONTH);
}
#[test]
fn every_duration_in_a_plan_is_projected(
running in 0u64..1_000_000,
suspended in 0u64..1_000_000,
retained in prop::option::of(1u64..1_000_000),
cycles in 0u32..5,
image in prop::option::of(0.5f64..64.0),
) {
let report = estimate_run(
SizeClass::Mib2048,
&PlanUsage {
running_seconds: running as f64,
suspended_seconds: suspended as f64,
image_gb: image,
image_retained_seconds: retained.map(|seconds| seconds as f64),
suspend_resume_cycles: cycles,
snapshot_gb: None,
launched: true,
},
&rates(),
fresh_day(),
"estimate",
)?;
for item in report.items() {
if let Some(duration) = item.duration {
prop_assert_eq!(duration.provenance(), Provenance::Projected);
}
}
prop_assert!(!report.fully_measured());
prop_assert_eq!(report.total().is_lower_bound(), image.is_some());
}
#[test]
fn a_plan_and_a_timed_run_of_the_same_shape_price_identically(
running in 1u64..1_000_000,
cycles in 0u32..5,
) {
let projected = estimate_run(
SizeClass::Mib2048,
&PlanUsage {
running_seconds: running as f64,
suspend_resume_cycles: cycles,
..PlanUsage::default()
},
&rates(),
fresh_day(),
"estimate",
)?;
let measured = run_report(
SizeClass::Mib2048,
&RunUsage {
running: Some(DurationP::Measured(Duration::from_secs(running))),
suspend_resume_cycles: cycles,
launched: false,
..RunUsage::default()
},
&rates(),
fresh_day(),
"run",
)?;
prop_assert_eq!(projected.total(), measured.total());
prop_assert!(!projected.fully_measured());
}
#[test]
fn the_float_boundary_reads_the_decimal_spelling_not_the_binary_value(
gb in 0.0f64..1_000_000.0,
) {
let converted = gb_decimal(gb)?;
prop_assert_eq!(converted.to_string(), gb.to_string());
prop_assert!(converted >= Decimal::ZERO);
}
#[test]
fn staleness_is_the_ninety_day_verdict_and_carries_the_age(
age_days in -10i64..400,
) {
let rates = rates();
let today = CalendarDate::from_day_number(
rates.retrieved().day_number() + age_days
)
.expect("in range");
prop_assert_eq!(rates.age_days(today), age_days);
prop_assert_eq!(rates.is_stale(today), age_days > STALE_AFTER_DAYS);
match rates.staleness(today) {
Some(warning) => {
prop_assert!(age_days > STALE_AFTER_DAYS);
let expected = format!("{age_days} days ago");
prop_assert!(warning.contains(&expected), "{}", warning);
}
None => prop_assert!(age_days <= STALE_AFTER_DAYS),
}
let report = run_report(
SizeClass::Mib2048,
&RunUsage {
running: Some(DurationP::Measured(Duration::from_secs(60))),
launched: false,
..RunUsage::default()
},
&rates,
today,
"run",
)?;
prop_assert_eq!(report.staleness().is_some(), age_days > STALE_AFTER_DAYS);
}
#[test]
fn a_day_number_round_trips_through_its_calendar_date(
day_number in -30_000i64..30_000,
) {
let date = CalendarDate::from_day_number(day_number).expect("in range");
prop_assert_eq!(date.day_number(), day_number);
prop_assert!(
CalendarDate::try_from_ymd(date.year(), date.month(), date.day()).is_ok(),
"{date} is not a calendar day"
);
}
}
fn a_create_and_destroy_report() -> CostReport {
run_report(
SizeClass::Mib2048,
&RunUsage {
running: Some(DurationP::Measured(Duration::from_secs(3600))),
image_gb: Some(2.0),
image_build: Some(DurationP::Measured(Duration::from_secs(600))),
..RunUsage::launched()
},
&pinned_rates(),
fresh_day(),
"run img",
)
.expect("a report")
}
#[test]
fn a_launch_is_inferred_from_running_time_or_a_real_image_only() {
let plans = [
(PlanUsage::default(), false),
(
PlanUsage {
running_seconds: 60.0,
..PlanUsage::default()
},
true,
),
(
PlanUsage {
suspended_seconds: 3600.0,
..PlanUsage::default()
},
false,
),
(
PlanUsage {
suspend_resume_cycles: 2,
..PlanUsage::default()
},
false,
),
(
PlanUsage {
image_gb: Some(2.0),
..PlanUsage::default()
},
true,
),
(
PlanUsage {
image_gb: Some(0.0),
..PlanUsage::default()
},
false,
),
];
for (plan, launched) in plans {
assert_eq!(plan.infer_launched(), launched, "{plan:?}");
}
let runs = [
(RunUsage::default(), false),
(
RunUsage {
running: Some(DurationP::Measured(Duration::from_secs(60))),
..RunUsage::default()
},
true,
),
(
RunUsage {
running: Some(DurationP::Measured(Duration::ZERO)),
..RunUsage::default()
},
false,
),
(
RunUsage {
suspended: Some(DurationP::Measured(Duration::from_secs(60))),
..RunUsage::default()
},
false,
),
(
RunUsage {
image_gb: Some(2.0),
..RunUsage::default()
},
true,
),
(
RunUsage {
image_gb: Some(0.0),
..RunUsage::default()
},
false,
),
];
for (usage, launched) in runs {
assert_eq!(usage.infer_launched(), launched, "{usage:?}");
}
}
#[test]
fn a_report_defaults_to_no_cycle_and_a_comparison_to_one() {
assert_eq!(DEFAULT_REPORT_CYCLES, 0);
assert_eq!(
RunUsage::default().suspend_resume_cycles,
DEFAULT_REPORT_CYCLES
);
assert_eq!(
PlanUsage::default().suspend_resume_cycles,
DEFAULT_REPORT_CYCLES
);
let report = estimate_run(
SizeClass::Mib2048,
&PlanUsage {
running_seconds: 600.0,
..PlanUsage::launched()
},
&rates(),
fresh_day(),
DEFAULT_ESTIMATE_LABEL,
)
.expect("a report");
let phases: Vec<CostPhase> = report.items().iter().map(|item| item.phase).collect();
assert_eq!(
phases,
[CostPhase::Launch, CostPhase::Running, CostPhase::Running]
);
assert_eq!(DEFAULT_RESIDENCY_CYCLES, 1);
let comparison = compare_residency(
SizeClass::Mib2048,
Duration::from_secs(3600),
DEFAULT_RESIDENCY_CYCLES,
&rates(),
fresh_day(),
)
.expect("a comparison");
let suspended: Vec<CostPhase> = comparison
.suspended()
.items()
.iter()
.map(|item| item.phase)
.collect();
assert_eq!(
suspended,
[CostPhase::Suspended, CostPhase::Suspend, CostPhase::Resume]
);
}
#[test]
fn an_unpriced_line_omits_the_usd_key_rather_than_reporting_zero() {
let json = a_create_and_destroy_report().to_json();
let items = json["items"].as_array().expect("items");
let build = items
.iter()
.find(|item| item["phase"] == "image-build")
.expect("the build line is always present when an image is");
assert_eq!(build["amount"]["kind"], "unpriced");
assert!(
build["amount"].get("usd").is_none(),
"a null would be summed as zero: {build}"
);
assert!(
build["amount"]["reason"]
.as_str()
.expect("a reason")
.contains("does not publish"),
"{build}"
);
assert_eq!(json["total"]["isLowerBound"], true);
assert!(
json["total"]["render"]
.as_str()
.expect("a render")
.contains("at least"),
"{}",
json["total"]["render"]
);
assert_eq!(json["complete"], false);
}
#[test]
fn every_dollar_is_a_string_and_every_seconds_figure_is_a_number() {
let json = a_create_and_destroy_report().to_json();
let mut durations = 0;
for item in json["items"].as_array().expect("items") {
if let Some(usd) = item["amount"].get("usd") {
assert!(usd.is_string(), "{item}");
usd.as_str()
.expect("a string")
.parse::<f64>()
.expect("still parses as a figure");
}
assert!(item["quantity"].is_string(), "{item}");
if !item["duration"].is_null() {
durations += 1;
assert!(
item["duration"]["seconds"].is_number(),
"a number, matching cli.py:743: {item}"
);
}
}
assert!(
durations > 0,
"vacuous unless a duration was present: {json}"
);
assert!(json["total"]["priced"].is_string());
let seconds: Vec<f64> = json["items"]
.as_array()
.expect("items")
.iter()
.filter(|item| !item["duration"].is_null())
.map(|item| item["duration"]["seconds"].as_f64().expect("a number"))
.collect();
assert!(seconds.contains(&600.0), "{seconds:?}");
assert!(seconds.contains(&604_800.0), "{seconds:?}");
let comparison = compare_residency(
SizeClass::Mib2048,
Duration::from_secs(3600),
1,
&pinned_rates(),
fresh_day(),
)
.expect("a comparison")
.to_json()
.expect("renders");
assert_eq!(comparison["holdSeconds"].as_f64(), Some(3600.0));
assert!(comparison["breakEvenSeconds"].is_number());
assert!(comparison["perCycleUsd"].is_string());
}
#[test]
fn a_measured_report_labels_the_timed_phases_measured_and_the_retention_projected() {
let measured = a_create_and_destroy_report().to_json();
let labelled: Vec<(&str, &str)> = measured["items"]
.as_array()
.expect("items")
.iter()
.filter(|item| !item["duration"].is_null())
.map(|item| {
(
item["phase"].as_str().unwrap_or_default(),
item["duration"]["provenance"].as_str().unwrap_or_default(),
)
})
.collect();
assert!(
labelled.contains(&("running", "measured")),
"a clock timed the run: {labelled:?}"
);
assert!(
labelled.contains(&("image-build", "measured")),
"a clock timed the build: {labelled:?}"
);
assert!(
labelled.contains(&("image-storage", "projected")),
"nobody timed the one-week retention floor: {labelled:?}"
);
assert_eq!(
measured["fullyMeasured"], false,
"a report carrying a projected retention is not fully measured"
);
let estimate = estimate_run(
SizeClass::Mib2048,
&PlanUsage {
running_seconds: 3600.0,
..PlanUsage::launched()
},
&pinned_rates(),
fresh_day(),
DEFAULT_ESTIMATE_LABEL,
)
.expect("an estimate");
let json = estimate.to_json();
assert_eq!(json["fullyMeasured"], false);
for item in json["items"].as_array().expect("items") {
if !item["duration"].is_null() {
assert_eq!(item["duration"]["provenance"], "projected", "{item}");
}
}
let timed_only = run_report(
SizeClass::Mib2048,
&RunUsage {
running: Some(DurationP::Measured(Duration::from_secs(60))),
..RunUsage::launched()
},
&pinned_rates(),
fresh_day(),
"run",
)
.expect("a report");
assert_eq!(timed_only.to_json()["fullyMeasured"], true);
assert_eq!(measured["estimated"], true);
assert_eq!(json["estimated"], true);
}
#[test]
fn the_report_json_carries_the_pythons_key_set() {
let json = a_create_and_destroy_report().to_json();
let mut keys: Vec<&String> = json.as_object().expect("object").keys().collect();
keys.sort();
assert_eq!(
keys,
[
"complete",
"estimated",
"fullyMeasured",
"items",
"label",
"rates",
"size",
"staleness",
"total",
]
);
let mut size_keys: Vec<&String> =
json["size"].as_object().expect("object").keys().collect();
size_keys.sort();
assert_eq!(
size_keys,
[
"baselineMib",
"baselineVcpu",
"describe",
"headroomMib",
"peakMib",
"peakVcpu"
]
);
let mut item_keys: Vec<&String> = json["items"][0]
.as_object()
.expect("object")
.keys()
.collect();
item_keys.sort();
assert_eq!(
item_keys,
[
"amount", "duration", "line", "note", "phase", "quantity", "unit"
]
);
}
#[test]
fn a_comparison_carries_the_per_cycle_cost_and_the_break_even_hold() {
let comparison = compare_residency(
SizeClass::Mib2048,
Duration::from_secs(3600),
1,
&pinned_rates(),
fresh_day(),
)
.expect("a comparison");
let json = comparison.to_json().expect("renders");
assert!(json["perCycleUsd"].is_string());
assert!(
json["ratio"].is_string(),
"the ratio is not money but is decimal"
);
let break_even = json["breakEvenSeconds"].as_f64().expect("a number");
assert!(break_even > 0.0, "{break_even}");
assert!(
json["render"]
.as_str()
.expect("a render")
.contains("avoid churn"),
"the conclusion the numbers support has to travel with them"
);
}
#[test]
fn a_stale_rate_table_puts_its_warning_in_the_envelope() {
let rates = pinned_rates();
let retrieved = rates.retrieved();
let stale_day =
CalendarDate::try_from_ymd(retrieved.year() + 1, retrieved.month(), retrieved.day())
.expect("a real day one year on");
let report = run_report(
SizeClass::Mib2048,
&RunUsage {
running: Some(DurationP::Measured(Duration::from_secs(60))),
..RunUsage::launched()
},
&rates,
stale_day,
"run",
)
.expect("a report");
let json = report.to_json();
assert!(json["staleness"].is_string(), "{json}");
assert!(
json["staleness"].as_str().expect("text").contains("days"),
"{json}"
);
}
fn an_exact_report() -> CostReport {
running_report(
SizeClass::Mib2048,
DurationP::Measured(Duration::from_secs(3600)),
)
}
#[test]
fn a_budget_verdict_follows_the_judgement_and_the_total_it_was_given() {
let exact = an_exact_report();
let floor = exact.total().floor();
let lower = a_create_and_destroy_report();
assert!(lower.total().is_lower_bound());
let lower_floor = lower.total().floor();
let over = |figure: EstimatedUsd| EstimatedUsd::new(figure.amount() - dec!(0.001));
let rows = [
(&exact, floor, OnBreach::Abort, false, false, "exact"),
(&exact, over(floor), OnBreach::Warn, true, false, "exact"),
(&exact, over(floor), OnBreach::Abort, true, true, "exact"),
(
&lower,
over(lower_floor),
OnBreach::Warn,
true,
false,
"lower-bound",
),
(
&lower,
over(lower_floor),
OnBreach::Abort,
true,
true,
"lower-bound",
),
(
&lower,
lower_floor,
OnBreach::Abort,
false,
false,
"lower-bound",
),
];
for (report, max, on_breach, breached, aborts, basis) in rows {
let verdict = Budget::new(max, on_breach).check(report);
let row = format!("{} under {} ({basis})", max.amount(), on_breach.as_str());
assert_eq!(verdict.breached(), breached, "breached: {row}");
assert_eq!(verdict.aborts(), aborts, "aborts: {row}");
assert_eq!(verdict.basis(), basis, "basis: {row}");
assert_eq!(verdict.overage().is_some(), breached, "overage: {row}");
assert_eq!(verdict.to_json()["breached"], breached, "{row}");
assert_eq!(verdict.to_json()["onBreach"], on_breach.as_str(), "{row}");
}
let breach = Budget::new(over(lower_floor), OnBreach::Warn).check(&lower);
assert_eq!(breach.overage(), Some(EstimatedUsd::new(dec!(0.001))));
assert!(
breach.render().contains("by at least"),
"{}",
breach.render()
);
assert!(
breach.render().contains("unknown margin"),
"{}",
breach.render()
);
let within = Budget::new(lower_floor, OnBreach::Warn).check(&lower);
assert!(
within.render().contains("that floor is a lower bound"),
"{}",
within.render()
);
assert_eq!(within.unpriced_phases(), ["image-build"]);
let exact_within = Budget::new(floor, OnBreach::Warn).check(&exact);
assert!(exact_within.unpriced_phases().is_empty());
let text = exact_within.render();
assert!(text.starts_with("budget: the estimated total $"), "{text}");
assert!(!text.contains("lower bound"), "{text}");
}
#[test]
fn an_empty_report_is_judged_as_an_exact_zero() {
let empty = run_report(
SizeClass::Mib2048,
&RunUsage::default(),
&pinned_rates(),
fresh_day(),
DEFAULT_RUN_LABEL,
)
.expect("an empty report");
let verdict = Budget::new(EstimatedUsd::ZERO, OnBreach::Abort).check(&empty);
assert!(!verdict.is_lower_bound());
assert!(!verdict.breached());
assert_eq!(verdict.render_dense(), "budget\t0\texact\twithin\t-");
}
#[test]
fn a_judgement_parses_from_its_spelling_only() {
for judgement in OnBreach::ALL {
assert_eq!(judgement.as_str().parse::<OnBreach>().ok(), Some(judgement));
}
for bad in ["", "Warn", "abort ", "fail"] {
let error = bad.parse::<OnBreach>().expect_err("not a judgement");
assert_eq!(error.kind(), ErrorKind::InvalidArg, "{bad:?}");
}
}
}