use std::fmt;
use crate::error::Error;
pub const MIB_PER_GB: u32 = 1024;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SizeRow {
pub baseline_mib: u32,
pub baseline_vcpu: f64,
pub peak_mib: u32,
pub peak_vcpu: f64,
}
pub const SIZE_CLASSES: [SizeRow; 5] = [
SizeRow {
baseline_mib: 512,
baseline_vcpu: 0.25,
peak_mib: 2048,
peak_vcpu: 1.0,
},
SizeRow {
baseline_mib: 1024,
baseline_vcpu: 0.5,
peak_mib: 4096,
peak_vcpu: 2.0,
},
SizeRow {
baseline_mib: 2048,
baseline_vcpu: 1.0,
peak_mib: 8192,
peak_vcpu: 4.0,
},
SizeRow {
baseline_mib: 4096,
baseline_vcpu: 2.0,
peak_mib: 16384,
peak_vcpu: 8.0,
},
SizeRow {
baseline_mib: 8192,
baseline_vcpu: 4.0,
peak_mib: 32768,
peak_vcpu: 16.0,
},
];
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum SizeClass {
Mib512,
Mib1024,
Mib2048,
Mib4096,
Mib8192,
}
impl SizeClass {
pub const DEFAULT: SizeClass = SizeClass::Mib2048;
pub const ALL: [SizeClass; 5] = [
SizeClass::Mib512,
SizeClass::Mib1024,
SizeClass::Mib2048,
SizeClass::Mib4096,
SizeClass::Mib8192,
];
pub fn from_baseline_mib(mib: u32) -> Result<SizeClass, Error> {
if let Some(class) = class_for_baseline_in(&SIZE_CLASSES, mib) {
return Ok(class);
}
let offered = SIZE_CLASSES
.iter()
.map(|row| row.baseline_mib.to_string())
.collect::<Vec<_>>()
.join(", ");
Err(Error::invalid_arg(format!(
"minimumMemoryInMiB={mib} is not a documented size class baseline. The field \
selects a class, it does not size a VM; pass one of {offered} MiB. See \
docs/PLATFORM.md, '`minimumMemoryInMiB` selects a *baseline*, and the guest \
reports the *peak*'."
)))
}
pub fn row(self) -> &'static SizeRow {
row_in(&SIZE_CLASSES, self)
}
pub fn baseline_mib(self) -> u32 {
self.row().baseline_mib
}
pub fn peak_mib(self) -> u32 {
self.row().peak_mib
}
pub fn headroom_mib(self) -> u32 {
headroom_in(&SIZE_CLASSES, self)
}
pub fn baseline_vcpu(self) -> f64 {
self.row().baseline_vcpu
}
pub fn peak_vcpu(self) -> f64 {
self.row().peak_vcpu
}
pub fn baseline_gb(self) -> f64 {
f64::from(self.baseline_mib()) / f64::from(MIB_PER_GB)
}
pub fn peak_gb(self) -> f64 {
f64::from(self.peak_mib()) / f64::from(MIB_PER_GB)
}
const fn index(self) -> usize {
match self {
SizeClass::Mib512 => 0,
SizeClass::Mib1024 => 1,
SizeClass::Mib2048 => 2,
SizeClass::Mib4096 => 3,
SizeClass::Mib8192 => 4,
}
}
}
impl fmt::Display for SizeClass {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let row = self.row();
write!(
f,
"{} GB / {} vCPU baseline (billed while running), with a fixed ceiling of {} GB / \
{} vCPU provisioned from the start (what the guest reports)",
self.baseline_gb(),
row.baseline_vcpu,
self.peak_gb(),
row.peak_vcpu,
)
}
}
fn row_in(table: &[SizeRow], class: SizeClass) -> &SizeRow {
&table[class.index()]
}
fn headroom_in(table: &[SizeRow], class: SizeClass) -> u32 {
let row = row_in(table, class);
row.peak_mib.saturating_sub(row.baseline_mib)
}
fn class_for_baseline_in(table: &[SizeRow], baseline_mib: u32) -> Option<SizeClass> {
SizeClass::ALL
.into_iter()
.find(|class| table[class.index()].baseline_mib == baseline_mib)
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::*;
use crate::error::ErrorKind;
#[test]
fn the_documented_table_carries_the_measured_rows() {
let rows: Vec<(u32, f64, u32, f64)> = SIZE_CLASSES
.iter()
.map(|r| (r.baseline_mib, r.baseline_vcpu, r.peak_mib, r.peak_vcpu))
.collect();
assert_eq!(
rows,
[
(512, 0.25, 2048, 1.0),
(1024, 0.5, 4096, 2.0),
(2048, 1.0, 8192, 4.0),
(4096, 2.0, 16384, 8.0),
(8192, 4.0, 32768, 16.0),
]
);
}
#[test]
fn a_peak_that_is_not_four_times_its_baseline_is_read_not_computed() {
let mut table = SIZE_CLASSES;
table[SizeClass::Mib2048.index()].peak_mib = 9000;
table[SizeClass::Mib2048.index()].peak_vcpu = 3.5;
let row = row_in(&table, SizeClass::Mib2048);
assert_eq!(
row.peak_mib, 9000,
"the peak must come from the table, not from baseline * 4"
);
assert_eq!(row.peak_vcpu, 3.5);
assert_eq!(row.baseline_mib, 2048);
assert_eq!(row.peak_mib * 4 / 4, 9000);
assert_ne!(row.peak_mib, row.baseline_mib * 4);
}
#[test]
fn every_accessor_returns_its_own_row_from_the_shipped_table() {
for class in SizeClass::ALL {
let row = &SIZE_CLASSES[class.index()];
assert_eq!(class.baseline_mib(), row.baseline_mib, "{class:?}");
assert_eq!(class.peak_mib(), row.peak_mib, "{class:?}");
assert_eq!(class.baseline_vcpu(), row.baseline_vcpu, "{class:?}");
assert_eq!(class.peak_vcpu(), row.peak_vcpu, "{class:?}");
}
}
#[test]
fn each_variant_is_pinned_to_the_baseline_its_name_states() {
assert_eq!(SizeClass::Mib512.baseline_mib(), 512);
assert_eq!(SizeClass::Mib1024.baseline_mib(), 1024);
assert_eq!(SizeClass::Mib2048.baseline_mib(), 2048);
assert_eq!(SizeClass::Mib4096.baseline_mib(), 4096);
assert_eq!(SizeClass::Mib8192.baseline_mib(), 8192);
}
#[test]
fn the_five_documented_baselines_select_their_own_class() {
for class in SizeClass::ALL {
let selected = SizeClass::from_baseline_mib(class.baseline_mib())
.expect("a documented baseline is accepted");
assert_eq!(selected, class);
}
}
#[test]
fn an_off_table_baseline_is_refused_naming_the_finding() {
for mib in [0, 1, 511, 513, 1500, 3072, 8191, 16384, u32::MAX] {
let err = SizeClass::from_baseline_mib(mib)
.expect_err("only the five documented baselines are accepted");
assert_eq!(err.kind(), ErrorKind::InvalidArg, "{mib}");
let message = err.to_string();
assert!(
message.contains("not a documented size class baseline"),
"{message}"
);
assert!(
message.contains("selects a class, it does not size a VM"),
"must name the finding: {message}"
);
}
}
#[test]
fn a_peak_that_is_not_also_a_baseline_is_refused() {
assert_eq!(
SizeClass::from_baseline_mib(2048).expect("2048 is a baseline too"),
SizeClass::Mib2048
);
for peak_only in [16384, 32768] {
assert!(
SizeClass::from_baseline_mib(peak_only).is_err(),
"{peak_only}"
);
}
}
#[test]
fn the_default_class_is_two_gigabytes_of_baseline() {
assert_eq!(SizeClass::DEFAULT, SizeClass::Mib2048);
assert_eq!(SizeClass::DEFAULT.baseline_mib(), 2048);
assert_eq!(SizeClass::DEFAULT.baseline_gb(), 2.0);
assert_eq!(SizeClass::DEFAULT.peak_gb(), 8.0);
}
#[test]
fn display_names_the_baseline_and_the_peak_together() {
let described = SizeClass::DEFAULT.to_string();
assert!(described.contains("2 GB / 1 vCPU baseline"), "{described}");
assert!(described.contains("8 GB / 4 vCPU"), "{described}");
assert!(described.contains("billed while running"), "{described}");
assert!(described.contains("what the guest reports"), "{described}");
assert!(
described.contains("provisioned from the start"),
"{described}"
);
assert!(!described.contains("burst"), "{described}");
}
#[test]
fn headroom_follows_a_table_whose_peak_is_not_four_times_its_baseline() {
let mut table = SIZE_CLASSES;
table[SizeClass::Mib2048.index()].peak_mib = 9000;
assert_eq!(
headroom_in(&table, SizeClass::Mib2048),
9000 - 2048,
"headroom must be a difference of table values, not baseline * 3"
);
for class in SizeClass::ALL {
let row = &SIZE_CLASSES[class.index()];
assert_eq!(
class.headroom_mib(),
row.peak_mib - row.baseline_mib,
"{class:?}"
);
}
}
fn plausible_baseline_mib() -> impl Strategy<Value = u32> {
prop_oneof![
2 => any::<u32>(),
5 => 1u32..=8192,
3 => prop::sample::select(SIZE_CLASSES.to_vec())
.prop_flat_map(|row| (row.baseline_mib.saturating_sub(2))..=(row.baseline_mib + 2)),
]
}
proptest! {
#[test]
fn from_baseline_mib_accepts_exactly_the_five_documented_baselines(
mib in plausible_baseline_mib(),
) {
let documented = SIZE_CLASSES.iter().any(|row| row.baseline_mib == mib);
match SizeClass::from_baseline_mib(mib) {
Ok(class) => {
prop_assert!(documented, "{mib} MiB is not a documented baseline but was accepted");
prop_assert_eq!(class.baseline_mib(), mib);
}
Err(err) => {
prop_assert!(!documented, "{} MiB is documented but was refused", mib);
prop_assert_eq!(err.kind(), ErrorKind::InvalidArg);
prop_assert_eq!(err.code(), "ERR_INVALID_ARG");
}
}
}
#[test]
fn the_lookup_reports_whatever_peak_the_table_carries(
peak_mib in 1u32..100_000,
peak_vcpu in 0.1f64..64.0,
) {
let mut table = SIZE_CLASSES;
for class in SizeClass::ALL {
table[class.index()].peak_mib = peak_mib;
table[class.index()].peak_vcpu = peak_vcpu;
}
for class in SizeClass::ALL {
let row = row_in(&table, class);
prop_assert_eq!(row.peak_mib, peak_mib);
prop_assert_eq!(row.peak_vcpu, peak_vcpu);
prop_assert_eq!(row.baseline_mib, class.baseline_mib());
prop_assert_eq!(
class_for_baseline_in(&table, class.baseline_mib()),
Some(class)
);
}
}
}
}