use super::*;
#[test]
fn scenario_stats_serde_roundtrip() {
let s = ScenarioStats {
cgroups: vec![CgroupStats {
num_workers: 4,
num_cpus: 2,
avg_off_cpu_pct: 50.0,
min_off_cpu_pct: 40.0,
max_off_cpu_pct: 60.0,
spread: 20.0,
max_gap_ms: 150,
max_gap_cpu: 3,
total_migrations: 10,
..Default::default()
}],
total_workers: 4,
total_cpus: 2,
total_migrations: 10,
worst_spread: 20.0,
worst_gap_ms: 150,
worst_gap_cpu: 3,
..Default::default()
};
let json = serde_json::to_string(&s).unwrap();
let s2: ScenarioStats = serde_json::from_str(&json).unwrap();
assert_eq!(s.total_workers, s2.total_workers);
assert_eq!(s.worst_gap_ms, s2.worst_gap_ms);
assert_eq!(s.cgroups.len(), s2.cgroups.len());
assert_eq!(s.cgroups[0].num_workers, s2.cgroups[0].num_workers);
}
#[test]
fn assert_result_serde_roundtrip() {
let r = AssertResult {
passed: false,
skipped: false,
details: vec!["test".into()],
stats: Default::default(),
measurements: std::collections::BTreeMap::new(),
};
let json = serde_json::to_string(&r).unwrap();
let r2: AssertResult = serde_json::from_str(&json).unwrap();
assert_eq!(r.passed, r2.passed);
assert_eq!(r.details, r2.details);
}
#[test]
fn cgroup_stats_missing_required_field_rejected_by_deserialize() {
const REQUIRED_FIELDS: &[&str] = &[
"num_workers",
"num_cpus",
"avg_off_cpu_pct",
"min_off_cpu_pct",
"max_off_cpu_pct",
"spread",
"max_gap_ms",
"max_gap_cpu",
"total_migrations",
"migration_ratio",
"p99_wake_latency_us",
"median_wake_latency_us",
"wake_latency_cv",
"total_iterations",
"mean_run_delay_us",
"worst_run_delay_us",
"page_locality",
"cross_node_migration_ratio",
];
let cg = CgroupStats::default();
let full = match serde_json::to_value(&cg).unwrap() {
serde_json::Value::Object(m) => m,
other => panic!("expected object, got {other:?}"),
};
for field in REQUIRED_FIELDS {
let mut obj = full.clone();
assert!(
obj.remove(*field).is_some(),
"CgroupStats must emit `{field}` for its rejection \
case to be meaningful — the field list in this test \
has drifted from the struct definition",
);
let json = serde_json::Value::Object(obj).to_string();
let err = serde_json::from_str::<CgroupStats>(&json)
.err()
.unwrap_or_else(|| {
panic!("deserialize must reject CgroupStats with `{field}` removed, but succeeded",)
});
let msg = format!("{err}");
assert!(
msg.contains(field),
"missing-field error for `{field}` must name the field; got: {msg}",
);
}
}
#[test]
fn scenario_stats_missing_required_scalar_rejected_by_deserialize() {
const REQUIRED_FIELDS: &[&str] = &[
"cgroups",
"total_workers",
"total_cpus",
"total_migrations",
"worst_spread",
"worst_gap_ms",
"worst_gap_cpu",
"worst_migration_ratio",
"worst_p99_wake_latency_us",
"worst_median_wake_latency_us",
"worst_wake_latency_cv",
"total_iterations",
"worst_mean_run_delay_us",
"worst_run_delay_us",
"worst_page_locality",
"worst_cross_node_migration_ratio",
"worst_wake_latency_tail_ratio",
"worst_iterations_per_worker",
];
let s = ScenarioStats::default();
let full = match serde_json::to_value(&s).unwrap() {
serde_json::Value::Object(m) => m,
other => panic!("expected object, got {other:?}"),
};
for field in REQUIRED_FIELDS {
let mut obj = full.clone();
assert!(
obj.remove(*field).is_some(),
"ScenarioStats must emit `{field}` for its rejection case to be meaningful — \
the field list in this test has drifted from the struct definition",
);
let json = serde_json::Value::Object(obj).to_string();
let err = serde_json::from_str::<ScenarioStats>(&json)
.err()
.unwrap_or_else(|| {
panic!(
"deserialize must reject ScenarioStats with `{field}` removed, but succeeded",
)
});
let msg = format!("{err}");
assert!(
msg.contains(field),
"missing-field error for `{field}` must name the field; got: {msg}",
);
}
}
#[test]
fn scenario_stats_missing_ext_metrics_tolerated_by_deserialize() {
let s = ScenarioStats::default();
let mut obj = match serde_json::to_value(&s).unwrap() {
serde_json::Value::Object(m) => m,
other => panic!("expected object, got {other:?}"),
};
obj.remove("ext_metrics");
let without_ext_metrics = serde_json::Value::Object(obj).to_string();
let parsed: ScenarioStats = serde_json::from_str(&without_ext_metrics)
.expect("deserialize must tolerate missing ext_metrics (the sole exempt field)");
assert!(
parsed.ext_metrics.is_empty(),
"missing ext_metrics must default to empty, got {:?}",
parsed.ext_metrics,
);
}
#[test]
fn assert_result_missing_required_field_rejected_by_deserialize() {
const REQUIRED_FIELDS: &[&str] = &["passed", "skipped", "details", "stats"];
let r = AssertResult {
passed: false,
skipped: false,
details: vec!["detail".into()],
stats: ScenarioStats::default(),
measurements: std::collections::BTreeMap::new(),
};
let full = match serde_json::to_value(&r).unwrap() {
serde_json::Value::Object(m) => m,
other => panic!("expected object, got {other:?}"),
};
for field in REQUIRED_FIELDS {
let mut obj = full.clone();
assert!(
obj.remove(*field).is_some(),
"AssertResult must emit `{field}` for its rejection case to be meaningful",
);
let json = serde_json::Value::Object(obj).to_string();
let err = serde_json::from_str::<AssertResult>(&json).err().unwrap_or_else(
|| panic!(
"deserialize must reject AssertResult with `{field}` removed, but succeeded",
),
);
let msg = format!("{err}");
assert!(
msg.contains(field),
"missing-field error for `{field}` must name the field; got: {msg}",
);
}
}
#[test]
fn assert_result_missing_measurements_tolerated_by_deserialize() {
let json = r#"{"passed":true,"skipped":false,"details":[],"stats":{
"cgroups":[],"total_workers":0,"total_cpus":0,"total_migrations":0,
"worst_spread":0,"worst_gap_ms":0,"worst_gap_cpu":0,
"worst_migration_ratio":0,"worst_p99_wake_latency_us":0,
"worst_median_wake_latency_us":0,"worst_wake_latency_cv":0,
"total_iterations":0,"worst_mean_run_delay_us":0,
"worst_run_delay_us":0,"worst_page_locality":0,
"worst_cross_node_migration_ratio":0,
"worst_wake_latency_tail_ratio":0,"worst_iterations_per_worker":0
}}"#;
let r: AssertResult =
serde_json::from_str(json).expect("missing-measurements must deserialize cleanly");
assert!(r.measurements.is_empty());
}