use crate::test_support::Scheduler;
#[derive(Clone, Copy)]
#[non_exhaustive]
pub struct Payload {
pub name: &'static str,
pub kind: PayloadKind,
pub output: OutputFormat,
pub default_args: &'static [&'static str],
pub default_checks: &'static [Check],
pub metrics: &'static [MetricHint],
pub include_files: &'static [&'static str],
pub uses_parent_pgrp: bool,
pub known_flags: Option<&'static [&'static str]>,
pub metric_bounds: Option<&'static MetricBounds>,
}
impl std::fmt::Debug for Payload {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Payload")
.field("name", &self.name)
.field("kind", &self.kind)
.field("output", &self.output)
.field("default_args_len", &self.default_args.len())
.field("default_checks_len", &self.default_checks.len())
.field("metrics_len", &self.metrics.len())
.finish()
}
}
#[derive(Clone, Copy)]
pub enum PayloadKind {
Scheduler(&'static Scheduler),
Binary(&'static str),
}
impl std::fmt::Debug for PayloadKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PayloadKind::Scheduler(s) => f.debug_tuple("Scheduler").field(&s.name).finish(),
PayloadKind::Binary(name) => f.debug_tuple("Binary").field(name).finish(),
}
}
}
impl Payload {
pub const KERNEL_DEFAULT: Payload = Payload::new(
"kernel_default",
PayloadKind::Scheduler(&Scheduler::EEVDF),
OutputFormat::ExitCode,
&[],
&[],
&[],
&[],
false,
None,
None,
);
pub const fn display_name(&self) -> &'static str {
self.name
}
pub const fn as_scheduler(&self) -> Option<&'static Scheduler> {
match self.kind {
PayloadKind::Scheduler(s) => Some(s),
PayloadKind::Binary(_) => None,
}
}
pub const fn is_scheduler(&self) -> bool {
matches!(self.kind, PayloadKind::Scheduler(_))
}
#[allow(clippy::too_many_arguments)]
pub const fn new(
name: &'static str,
kind: PayloadKind,
output: OutputFormat,
default_args: &'static [&'static str],
default_checks: &'static [Check],
metrics: &'static [MetricHint],
include_files: &'static [&'static str],
uses_parent_pgrp: bool,
known_flags: Option<&'static [&'static str]>,
metric_bounds: Option<&'static MetricBounds>,
) -> Payload {
Payload {
name,
kind,
output,
default_args,
default_checks,
metrics,
include_files,
uses_parent_pgrp,
known_flags,
metric_bounds,
}
}
pub const fn from_scheduler(sched: &'static Scheduler) -> Payload {
Payload::new(
sched.name,
PayloadKind::Scheduler(sched),
OutputFormat::ExitCode,
&[],
&[],
&[],
&[],
false,
None,
None,
)
}
pub const fn binary(name: &'static str, binary: &'static str) -> Payload {
Payload::new(
name,
PayloadKind::Binary(binary),
OutputFormat::ExitCode,
&[],
&[],
&[],
&[],
false,
None,
None,
)
}
pub const fn scheduler_name(&self) -> &'static str {
match self.kind {
PayloadKind::Scheduler(s) => s.name,
PayloadKind::Binary(_) => "kernel_default",
}
}
pub const fn scheduler_binary(&self) -> Option<&'static crate::test_support::SchedulerSpec> {
match self.kind {
PayloadKind::Scheduler(s) => Some(&s.binary),
PayloadKind::Binary(_) => None,
}
}
pub const fn has_active_scheduling(&self) -> bool {
match self.kind {
PayloadKind::Scheduler(s) => s.binary.has_active_scheduling(),
PayloadKind::Binary(_) => false,
}
}
pub const fn flags(&self) -> &'static [&'static crate::scenario::flags::FlagDecl] {
match self.kind {
PayloadKind::Scheduler(s) => s.flags,
PayloadKind::Binary(_) => &[],
}
}
pub const fn sysctls(&self) -> &'static [crate::test_support::Sysctl] {
match self.kind {
PayloadKind::Scheduler(s) => s.sysctls,
PayloadKind::Binary(_) => &[],
}
}
pub const fn kargs(&self) -> &'static [&'static str] {
match self.kind {
PayloadKind::Scheduler(s) => s.kargs,
PayloadKind::Binary(_) => &[],
}
}
pub const fn sched_args(&self) -> &'static [&'static str] {
match self.kind {
PayloadKind::Scheduler(s) => s.sched_args,
PayloadKind::Binary(_) => &[],
}
}
pub const fn cgroup_parent(&self) -> Option<crate::test_support::CgroupPath> {
match self.kind {
PayloadKind::Scheduler(s) => s.cgroup_parent,
PayloadKind::Binary(_) => None,
}
}
pub const fn config_file(&self) -> Option<&'static str> {
match self.kind {
PayloadKind::Scheduler(s) => s.config_file,
PayloadKind::Binary(_) => None,
}
}
pub const fn assert(&self) -> &'static crate::assert::Assert {
match self.kind {
PayloadKind::Scheduler(s) => &s.assert,
PayloadKind::Binary(_) => &crate::assert::Assert::NO_OVERRIDES,
}
}
pub fn supported_flag_names(&self) -> Vec<&'static str> {
match self.kind {
PayloadKind::Scheduler(s) => s.supported_flag_names(),
PayloadKind::Binary(_) => Vec::new(),
}
}
pub fn flag_args(&self, name: &str) -> Option<&'static [&'static str]> {
match self.kind {
PayloadKind::Scheduler(s) => s.flag_args(name),
PayloadKind::Binary(_) => None,
}
}
pub const fn topology(&self) -> crate::test_support::Topology {
match self.kind {
PayloadKind::Scheduler(s) => s.topology,
PayloadKind::Binary(_) => crate::test_support::Topology::DEFAULT_FOR_PAYLOAD,
}
}
pub const fn constraints(&self) -> crate::test_support::TopologyConstraints {
match self.kind {
PayloadKind::Scheduler(s) => s.constraints,
PayloadKind::Binary(_) => crate::test_support::TopologyConstraints::DEFAULT,
}
}
pub fn generate_profiles(
&self,
required: &[&'static str],
excluded: &[&'static str],
) -> Vec<crate::scenario::FlagProfile> {
match self.kind {
PayloadKind::Scheduler(s) => s.generate_profiles(required, excluded),
PayloadKind::Binary(_) => vec![crate::scenario::FlagProfile { flags: Vec::new() }],
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum OutputFormat {
ExitCode,
Json,
LlmExtract(Option<&'static str>),
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum Polarity {
HigherBetter,
LowerBetter,
TargetValue(f64),
Unknown,
}
impl Polarity {
pub const fn from_higher_is_worse(higher_is_worse: bool) -> Polarity {
if higher_is_worse {
Polarity::LowerBetter
} else {
Polarity::HigherBetter
}
}
pub fn target(target: f64) -> Polarity {
assert!(
target.is_finite(),
"Polarity::TargetValue target must be finite, got {target}"
);
Polarity::TargetValue(target)
}
}
#[derive(Debug, Clone, Copy)]
pub struct MetricHint {
pub name: &'static str,
pub polarity: Polarity,
pub unit: &'static str,
}
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub struct MetricBounds {
pub min_count: Option<usize>,
pub value_min: Option<f64>,
pub value_max: Option<f64>,
}
impl MetricBounds {
pub const NONE: MetricBounds = MetricBounds {
min_count: None,
value_min: None,
value_max: None,
};
pub const fn new(
min_count: Option<usize>,
value_min: Option<f64>,
value_max: Option<f64>,
) -> MetricBounds {
MetricBounds {
min_count,
value_min,
value_max,
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum Check {
Min { metric: &'static str, value: f64 },
Max { metric: &'static str, value: f64 },
Range {
metric: &'static str,
lo: f64,
hi: f64,
},
Exists(&'static str),
ExitCodeEq(i32),
}
impl Check {
pub const fn min(metric: &'static str, value: f64) -> Check {
Check::Min { metric, value }
}
pub const fn max(metric: &'static str, value: f64) -> Check {
Check::Max { metric, value }
}
pub const fn range(metric: &'static str, lo: f64, hi: f64) -> Check {
Check::Range { metric, lo, hi }
}
pub const fn exists(metric: &'static str) -> Check {
Check::Exists(metric)
}
pub const fn exit_code_eq(expected: i32) -> Check {
Check::ExitCodeEq(expected)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum MetricSource {
Json,
LlmExtract,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum MetricStream {
Stdout,
Stderr,
Synthesized,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Metric {
pub name: String,
pub value: f64,
pub polarity: Polarity,
pub unit: String,
pub source: MetricSource,
pub stream: MetricStream,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PayloadMetrics {
pub payload_index: usize,
pub metrics: Vec<Metric>,
pub exit_code: i32,
}
impl PayloadMetrics {
pub fn get(&self, name: &str) -> Option<f64> {
self.metrics
.iter()
.find(|m| m.name == name)
.map(|m| m.value)
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub(crate) struct WireMetricHint {
pub name: String,
pub polarity: Polarity,
pub unit: String,
}
impl From<&MetricHint> for WireMetricHint {
fn from(h: &MetricHint) -> Self {
Self {
name: h.name.to_string(),
polarity: h.polarity,
unit: h.unit.to_string(),
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub(crate) struct RawPayloadOutput {
pub payload_index: usize,
pub stdout: String,
pub stderr: String,
pub hint: Option<String>,
pub metric_hints: Vec<WireMetricHint>,
pub metric_bounds: Option<MetricBounds>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn payload_kernel_default_const_is_scheduler_kind() {
assert!(matches!(
Payload::KERNEL_DEFAULT.kind,
PayloadKind::Scheduler(_)
));
assert_eq!(Payload::KERNEL_DEFAULT.display_name(), "kernel_default");
assert!(matches!(
Payload::KERNEL_DEFAULT.output,
OutputFormat::ExitCode
));
assert!(Payload::KERNEL_DEFAULT.default_args.is_empty());
assert!(Payload::KERNEL_DEFAULT.default_checks.is_empty());
assert!(Payload::KERNEL_DEFAULT.metrics.is_empty());
}
#[test]
fn payload_kernel_default_wraps_scheduler_eevdf() {
match Payload::KERNEL_DEFAULT.kind {
PayloadKind::Scheduler(s) => {
assert_eq!(s.name, Scheduler::EEVDF.name);
}
PayloadKind::Binary(_) => panic!("EEVDF should be Scheduler-kind, got Binary"),
}
}
#[test]
fn payload_binary_const_constructor_shape() {
const P: Payload = Payload::binary("fio_payload", "fio");
assert_eq!(P.name, "fio_payload");
assert!(matches!(P.kind, PayloadKind::Binary("fio")));
assert!(matches!(P.output, OutputFormat::ExitCode));
assert!(P.default_args.is_empty());
assert!(P.default_checks.is_empty());
assert!(P.metrics.is_empty());
assert!(!P.is_scheduler());
assert!(P.as_scheduler().is_none());
}
#[test]
fn check_constructors() {
assert!(matches!(Check::min("x", 1.0), Check::Min { .. }));
assert!(matches!(Check::max("x", 1.0), Check::Max { .. }));
assert!(matches!(Check::range("x", 1.0, 2.0), Check::Range { .. }));
assert!(matches!(Check::exists("x"), Check::Exists("x")));
assert!(matches!(Check::exit_code_eq(0), Check::ExitCodeEq(0)));
}
#[test]
fn metric_set_get_returns_value() {
let pm = PayloadMetrics {
payload_index: 0,
metrics: vec![Metric {
name: "iops".to_string(),
value: 1000.0,
polarity: Polarity::HigherBetter,
unit: "iops".to_string(),
source: MetricSource::Json,
stream: MetricStream::Stdout,
}],
exit_code: 0,
};
assert_eq!(pm.get("iops"), Some(1000.0));
assert_eq!(pm.get("missing"), None);
}
#[test]
fn polarity_target_value_carries_data() {
let p = Polarity::TargetValue(42.0);
match p {
Polarity::TargetValue(v) => assert_eq!(v, 42.0),
_ => panic!("expected TargetValue variant"),
}
}
#[test]
fn output_format_variants() {
let _: OutputFormat = OutputFormat::ExitCode;
let _: OutputFormat = OutputFormat::Json;
let _: OutputFormat = OutputFormat::LlmExtract(None);
let _: OutputFormat = OutputFormat::LlmExtract(Some("focus on iops"));
}
#[test]
fn metric_source_serde_round_trip() {
let js = serde_json::to_string(&MetricSource::Json).unwrap();
let de: MetricSource = serde_json::from_str(&js).unwrap();
assert_eq!(de, MetricSource::Json);
let js = serde_json::to_string(&MetricSource::LlmExtract).unwrap();
let de: MetricSource = serde_json::from_str(&js).unwrap();
assert_eq!(de, MetricSource::LlmExtract);
}
#[test]
fn metric_stream_serde_round_trip() {
for s in [MetricStream::Stdout, MetricStream::Stderr] {
let js = serde_json::to_string(&s).expect("serialize");
let de: MetricStream = serde_json::from_str(&js).expect("deserialize");
assert_eq!(
de, s,
"MetricStream::{s:?} wire format must round-trip \
identically; serialized as {js}, deserialized to \
{de:?}",
);
}
}
#[test]
fn polarity_serde_round_trip() {
for p in [
Polarity::HigherBetter,
Polarity::LowerBetter,
Polarity::TargetValue(2.78),
Polarity::Unknown,
] {
let js = serde_json::to_string(&p).unwrap();
let de: Polarity = serde_json::from_str(&js).unwrap();
assert_eq!(de, p);
}
}
#[test]
fn payload_kind_binary_construction_and_match() {
const FIO: Payload = Payload {
name: "fio",
kind: PayloadKind::Binary("fio"),
output: OutputFormat::Json,
default_args: &[],
default_checks: &[],
metrics: &[],
include_files: &[],
uses_parent_pgrp: false,
known_flags: None,
metric_bounds: None,
};
match FIO.kind {
PayloadKind::Binary(name) => assert_eq!(name, "fio"),
PayloadKind::Scheduler(_) => panic!("expected Binary, got Scheduler"),
}
assert!(!FIO.is_scheduler());
assert!(FIO.as_scheduler().is_none());
}
const _MIN: Check = Check::min("x", 1.0);
const _MAX: Check = Check::max("x", 2.0);
const _RANGE: Check = Check::range("x", 1.0, 2.0);
const _EXISTS: Check = Check::exists("x");
const _EXIT: Check = Check::exit_code_eq(0);
const _KERNEL_DEFAULT_REF: &Payload = &Payload::KERNEL_DEFAULT;
const _KERNEL_DEFAULT_IS_SCHED: bool = Payload::KERNEL_DEFAULT.is_scheduler();
const _KERNEL_DEFAULT_DISPLAY: &str = Payload::KERNEL_DEFAULT.display_name();
const _PAYLOAD_CONST_BUILD: Payload = Payload {
name: "fio",
kind: PayloadKind::Binary("fio"),
output: OutputFormat::Json,
default_args: &["--output-format=json"],
default_checks: &[Check::exit_code_eq(0)],
metrics: &[MetricHint {
name: "jobs.0.read.iops",
polarity: Polarity::HigherBetter,
unit: "iops",
}],
include_files: &[],
uses_parent_pgrp: false,
known_flags: None,
metric_bounds: None,
};
#[test]
fn const_bindings_are_usable() {
assert!(matches!(_MIN, Check::Min { .. }));
assert!(matches!(_MAX, Check::Max { .. }));
assert!(matches!(_RANGE, Check::Range { .. }));
assert!(matches!(_EXISTS, Check::Exists("x")));
assert!(matches!(_EXIT, Check::ExitCodeEq(0)));
assert_eq!(_KERNEL_DEFAULT_REF.name, "kernel_default");
const { assert!(_KERNEL_DEFAULT_IS_SCHED) };
assert_eq!(_KERNEL_DEFAULT_DISPLAY, "kernel_default");
}
#[test]
fn polarity_from_higher_is_worse_flips_sense() {
assert_eq!(Polarity::from_higher_is_worse(true), Polarity::LowerBetter);
assert_eq!(
Polarity::from_higher_is_worse(false),
Polarity::HigherBetter
);
}
#[test]
fn higher_is_worse_polarity_round_trip() {
use crate::stats::MetricDef;
let m = MetricDef {
name: "t",
polarity: Polarity::from_higher_is_worse(true),
default_abs: 0.0,
default_rel: 0.0,
display_unit: "",
accessor: |_| None,
};
assert_eq!(m.polarity, Polarity::LowerBetter);
assert!(m.higher_is_worse(), "LowerBetter → higher_is_worse = true");
let m = MetricDef {
name: "f",
polarity: Polarity::from_higher_is_worse(false),
default_abs: 0.0,
default_rel: 0.0,
display_unit: "",
accessor: |_| None,
};
assert_eq!(m.polarity, Polarity::HigherBetter);
assert!(
!m.higher_is_worse(),
"HigherBetter → higher_is_worse = false"
);
}
#[test]
fn higher_is_worse_covers_all_polarity_variants() {
use crate::stats::MetricDef;
fn make(p: Polarity) -> MetricDef {
MetricDef {
name: "x",
polarity: p,
default_abs: 0.0,
default_rel: 0.0,
display_unit: "",
accessor: |_| None,
}
}
assert!(!make(Polarity::HigherBetter).higher_is_worse());
assert!(make(Polarity::LowerBetter).higher_is_worse());
assert!(make(Polarity::TargetValue(42.0)).higher_is_worse());
assert!(make(Polarity::Unknown).higher_is_worse());
}
#[test]
fn polarity_target_accepts_finite() {
let p = Polarity::target(0.5);
assert_eq!(p, Polarity::TargetValue(0.5));
}
#[test]
#[should_panic(expected = "Polarity::TargetValue target must be finite")]
fn polarity_target_rejects_nan_panics() {
let _ = Polarity::target(f64::NAN);
}
#[test]
#[should_panic(expected = "Polarity::TargetValue target must be finite")]
fn polarity_target_rejects_positive_infinity_panics() {
let _ = Polarity::target(f64::INFINITY);
}
#[test]
#[should_panic(expected = "Polarity::TargetValue target must be finite")]
fn polarity_target_rejects_negative_infinity_panics() {
let _ = Polarity::target(f64::NEG_INFINITY);
}
#[test]
fn polarity_target_nan_serializes_as_null_and_fails_to_round_trip() {
let p = Polarity::TargetValue(f64::NAN);
let s = serde_json::to_string(&p).expect("NaN→null serialization is the current behavior");
assert_eq!(s, "{\"TargetValue\":null}");
assert!(
serde_json::from_str::<Polarity>(&s).is_err(),
"the null-coerced round-trip must fail to deserialize so a NaN written \
by an un-guarded producer cannot silently re-enter a run",
);
}
#[test]
fn polarity_target_nan_cannot_deserialize_from_non_json_literals() {
assert!(serde_json::from_str::<Polarity>("{\"TargetValue\":NaN}").is_err());
assert!(serde_json::from_str::<Polarity>("{\"TargetValue\":Infinity}").is_err());
assert!(serde_json::from_str::<Polarity>("{\"TargetValue\":-Infinity}").is_err());
}
#[test]
fn check_range_reversed_bounds_fails_every_finite_value() {
let reversed = Check::range("iops", 100.0, 50.0); match reversed {
Check::Range { metric, lo, hi } => {
assert_eq!(metric, "iops");
assert!(
lo > hi,
"constructor does not reorder bounds: lo={lo}, hi={hi}",
);
}
_ => panic!("expected Range variant"),
}
}
#[test]
fn payload_debug_renders_identity_fields() {
let s = format!("{:?}", Payload::KERNEL_DEFAULT);
assert!(s.contains("Payload"), "debug output: {s}");
assert!(s.contains("eevdf"), "debug output: {s}");
assert!(
s.contains("kind: Scheduler(\"eevdf\")"),
"debug output: {s}"
);
}
#[test]
fn payload_kind_debug_renders_variant_and_identity() {
let binary = PayloadKind::Binary("fio");
let s = format!("{binary:?}");
assert!(s.contains("Binary"), "debug output: {s}");
assert!(s.contains("fio"), "debug output: {s}");
let sched = Payload::KERNEL_DEFAULT.kind;
let s = format!("{sched:?}");
assert!(s.contains("Scheduler"), "debug output: {s}");
assert!(s.contains("eevdf"), "debug output: {s}");
}
#[test]
fn output_format_derive_debug_clone_copy() {
let a = OutputFormat::Json;
let b = a; let _ = format!("{a:?} {b:?}"); }
#[test]
fn as_scheduler_extracts_ref_for_scheduler_kind() {
let s = Payload::KERNEL_DEFAULT
.as_scheduler()
.expect("Scheduler kind");
assert_eq!(s.name, "eevdf");
}
#[test]
fn payload_clone_preserves_identity() {
let a = Payload::KERNEL_DEFAULT;
assert_eq!(a.name, Payload::KERNEL_DEFAULT.name);
assert_eq!(a.is_scheduler(), Payload::KERNEL_DEFAULT.is_scheduler());
assert_eq!(a.as_scheduler().map(|s| s.name), Some("eevdf"));
}
#[test]
fn raw_payload_output_serde_round_trip_carries_both_streams() {
let original = RawPayloadOutput {
payload_index: 17,
stdout: "stdout document with metrics: {\"iops\": 100}\n".to_string(),
stderr: "stderr fallback document: {\"latency\": 42}\n".to_string(),
hint: Some("focus on iops".to_string()),
metric_hints: vec![
WireMetricHint {
name: "iops".to_string(),
polarity: Polarity::HigherBetter,
unit: "iops".to_string(),
},
WireMetricHint {
name: "latency".to_string(),
polarity: Polarity::LowerBetter,
unit: "ns".to_string(),
},
],
metric_bounds: None,
};
let bytes = serde_json::to_vec(&original).expect("RawPayloadOutput must always serialize");
let restored: RawPayloadOutput =
serde_json::from_slice(&bytes).expect("wire format must round-trip");
assert_eq!(restored.payload_index, original.payload_index);
assert_eq!(
restored.stdout, original.stdout,
"stdout must round-trip byte-for-byte; lost stdout would degrade \
the host's stdout-primary extraction silently",
);
assert_eq!(
restored.stderr, original.stderr,
"stderr must round-trip byte-for-byte; lost stderr would silently \
defeat the stderr-fallback contract for payloads (e.g. schbench) \
that emit structured output on stderr only",
);
assert_eq!(restored.hint, original.hint);
assert_eq!(restored.metric_hints.len(), original.metric_hints.len());
for (got, want) in restored
.metric_hints
.iter()
.zip(original.metric_hints.iter())
{
assert_eq!(got.name, want.name);
assert_eq!(got.polarity, want.polarity);
assert_eq!(got.unit, want.unit);
}
}
#[test]
fn raw_payload_output_serde_round_trip_empty_streams_preserved() {
let original = RawPayloadOutput {
payload_index: 0,
stdout: String::new(),
stderr: String::new(),
hint: None,
metric_hints: Vec::new(),
metric_bounds: None,
};
let bytes = serde_json::to_vec(&original).expect("serialize");
let restored: RawPayloadOutput = serde_json::from_slice(&bytes).expect("deserialize");
assert_eq!(restored.payload_index, 0);
assert!(
restored.stdout.is_empty(),
"empty stdout must survive round-trip as empty string, not vanish to None or null"
);
assert!(
restored.stderr.is_empty(),
"empty stderr must survive round-trip as empty string"
);
assert!(restored.hint.is_none(), "absent hint must survive as None");
assert!(
restored.metric_hints.is_empty(),
"empty metric_hints must survive as empty Vec"
);
}
#[test]
fn raw_payload_output_serde_round_trip_stdout_only() {
let original = RawPayloadOutput {
payload_index: 3,
stdout: r#"{"throughput": 9000}"#.to_string(),
stderr: String::new(),
hint: None,
metric_hints: Vec::new(),
metric_bounds: None,
};
let bytes = serde_json::to_vec(&original).expect("serialize");
let restored: RawPayloadOutput = serde_json::from_slice(&bytes).expect("deserialize");
assert_eq!(restored.stdout, original.stdout);
assert!(restored.stderr.is_empty(), "stderr must remain empty");
}
#[test]
fn raw_payload_output_serde_round_trip_stderr_only() {
let original = RawPayloadOutput {
payload_index: 9,
stdout: String::new(),
stderr: r#"{"latency_p99": 1234}"#.to_string(),
hint: None,
metric_hints: Vec::new(),
metric_bounds: None,
};
let bytes = serde_json::to_vec(&original).expect("serialize");
let restored: RawPayloadOutput = serde_json::from_slice(&bytes).expect("deserialize");
assert!(restored.stdout.is_empty(), "stdout must remain empty");
assert_eq!(restored.stderr, original.stderr);
}
}