diff --git a/Cargo.toml b/Cargo.toml index 33f5978..1057c64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ rand_distr = { version = "0.5.1", optional = true } serde = { version = "1.0", features = ["derive"], optional = true } serde_json = { version = "1.0", optional = true } statrs = { version = "0.18.0", optional = true } -typetag = { version = "0.2.5", optional = true } +typetag = { version = "0.2.22", optional = true } [dev-dependencies] figment = { version = "0.10.19", features = ["json"] } @@ -43,12 +43,14 @@ model = [ "delay-per-packet-model", "loss-model", "duplicate-model", + "rwnd-model", ] bw-model = ["dep:rand", "dep:rand_distr", "dep:dyn-clone"] delay-model = ["dep:dyn-clone"] delay-per-packet-model = ["dep:dyn-clone"] loss-model = ["dep:dyn-clone"] duplicate-model = ["dep:dyn-clone"] +rwnd-model = ["dep:dyn-clone"] serde = ["dep:serde", "dep:typetag", "bandwidth/serde"] mahimahi = ["dep:itertools"] human = [ diff --git a/src/lib.rs b/src/lib.rs index bbdc68b..3775c28 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -113,6 +113,7 @@ pub use mahimahi::{load_mahimahi_trace, Mahimahi, MahimahiExt}; feature = "delay-model", feature = "loss-model", feature = "duplicate-model", + feature = "rwnd-model", feature = "model", ))] pub mod model; @@ -237,6 +238,59 @@ pub trait DuplicateTrace: Send { fn next_duplicate(&mut self) -> Option<(DuplicatePattern, Duration)>; } +/// The action a rwnd trace instructs the receiver to take at a single step. +/// +/// At most one action is present per step; a step that only reconfigures the +/// receive buffer (`set_rcv_buf`) without any read or observed-remaining update +/// leaves [`RwndDecision::action`] as `None`. +/// +/// - `AppRead` drives the receiver model by simulating the application reading +/// `bytes` from the receive buffer; the resulting rwnd is computed from the +/// buffer state. +/// - `Remaining` skips the simulation and directly enforces an observed rwnd +/// of `rwnd` bytes — useful for replaying captured traces where only the +/// advertised window is known. +#[derive(Debug, Clone, PartialEq)] +pub enum RwndAction { + /// The simulated application reads this many bytes from the receive buffer at this step. + AppRead { bytes: u64 }, + /// The remaining rwnd value observed immediately after the app consumes data at this step. + Remaining { rwnd: u64 }, +} + +/// A single receive-side decision emitted by a [`RwndTrace`]. +/// +/// Each step of a rwnd trace produces one `RwndDecision` paired with a +/// [`Duration`] (see [`RwndTrace`]). Both fields are optional and independent: +/// a step may resize the socket buffer, advance the receive model, both, or +/// neither (though a step that sets neither is effectively a no-op). +#[derive(Debug, Clone, PartialEq)] +pub struct RwndDecision { + /// If `Some`, reconfigure the socket's receive buffer to this size at this step. + pub set_rcv_buf: Option, + /// If `Some`, the app-read or observed-remaining action for this step. + pub action: Option, +} + +/// This is a trait that represents a trace of receive-window decisions over time. +/// +/// The trace is a sequence of `(rwnd_decision, duration)` pairs. The decision +/// describes how the socket's receive buffer, the application's read behavior, +/// and/or the observed remaining window change at this step; the duration is +/// how long this configuration lasts before the next step applies. +/// +/// For example, if the sequence is +/// `[(set_rcv_buf=64KB, app_read=1KB, 1s), (rwnd_remaining=32KB, 2s)]`, +/// then the receive buffer is resized to 64KB and the app reads 1KB for 1s, +/// then the observed rwnd becomes 32KB for 2s. +/// +/// Each `next_rwnd` call returns **the next decision and its duration** in the +/// sequence, or **None** when the trace is exhausted. Mirrors the shape of +/// [`BwTrace`], [`DelayTrace`], and [`LossTrace`]. +pub trait RwndTrace: Send { + fn next_rwnd(&mut self) -> Option<(RwndDecision, Duration)>; +} + #[cfg(test)] mod test { use model::TraceBwConfig; diff --git a/src/model/mod.rs b/src/model/mod.rs index d94df95..a545f83 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -1,15 +1,16 @@ -//! This module contains pre-defined models for BwTrace, DelayTrace, LossTrace and DuplicateTrace. +//! This module contains pre-defined models for BwTrace, DelayTrace, LossTrace, DuplicateTrace and RwndTrace. //! //! A model has two parts: a configuration struct and a model struct. //! The configuration struct is used to configure the model and //! used for serialization/deserialization if `serde` feature is enabled. -//! The model struct which implements trait `BwTrace`, `DelayTrace`, `LossTrace` or `DuplicateTrace` +//! The model struct which implements trait `BwTrace`, `DelayTrace`, `LossTrace`, `DuplicateTrace` or `RwndTrace` //! is used to generate the trace and maintain inner states. //! //! Enable `bw-model` feature to use the BwTrace models. //! Enable `delay-model` feature to use the DelayTrace models. //! Enable `loss-model` feature to use the LossTrace models. //! Enable `duplicate-model` feature to use the DuplicateTrace models. +//! Enable `rwnd-model` feature to use the RwndTrace models. #[cfg(feature = "bw-model")] pub mod bw; @@ -60,5 +61,13 @@ pub use duplicate::{DuplicateTraceConfig, RepeatedDuplicatePatternConfig, Static #[cfg(feature = "duplicate-model")] pub use duplicate::{RepeatedDuplicatePattern, StaticDuplicate}; +#[cfg(feature = "rwnd-model")] +pub mod rwnd; + +#[cfg(feature = "rwnd-model")] +pub use rwnd::{RepeatedRwndPattern, StaticRwnd}; +#[cfg(feature = "rwnd-model")] +pub use rwnd::{RepeatedRwndPatternConfig, RwndTraceConfig, StaticRwndConfig}; + #[cfg(feature = "truncated-normal")] pub mod solve_truncate; diff --git a/src/model/rwnd.rs b/src/model/rwnd.rs new file mode 100644 index 0000000..62506db --- /dev/null +++ b/src/model/rwnd.rs @@ -0,0 +1,558 @@ +//! This module contains some predefined rwnd trace models. +//! +//! Enabled with feature `rwnd-model` or `model`. +//! +//! ## Predefined models +//! +//! - [`StaticRwnd`]: A trace model with a single rwnd decision. +//! - [`RepeatedRwndPattern`]: A trace model with a repeated rwnd pattern. +//! +//! ## Examples +//! +//! An example to build model from configuration: +//! +//! ``` +//! # use netem_trace::model::StaticRwndConfig; +//! # use netem_trace::{Duration, RwndTrace, RwndAction}; +//! let mut static_rwnd = StaticRwndConfig::new() +//! .set_rcv_buf(65536) +//! .app_read(1024) +//! .duration(Duration::from_secs(1)) +//! .build(); +//! let (decision, duration) = static_rwnd.next_rwnd().unwrap(); +//! assert_eq!(decision.set_rcv_buf, Some(65536)); +//! assert_eq!(decision.action, Some(RwndAction::AppRead { bytes: 1024 })); +//! assert_eq!(duration, Duration::from_secs(1)); +//! assert_eq!(static_rwnd.next_rwnd(), None); +//! ``` +//! +//! A more common use case is to build model from a configuration file (e.g. json file): +//! +//! ``` +//! # use netem_trace::model::{StaticRwndConfig, RwndTraceConfig}; +//! # use netem_trace::{Duration, RwndTrace, RwndAction}; +//! # #[cfg(feature = "human")] +//! # let config_file_content = "{\"RepeatedRwndPatternConfig\":{\"pattern\":[{\"StaticRwndConfig\":{\"duration\":\"1s\",\"set_rcv_buf\":65536,\"app_read_bytes\":1024}},{\"StaticRwndConfig\":{\"duration\":\"1s\",\"rwnd_remaining\":32768}}],\"count\":2}}"; +//! // The content would be "{\"RepeatedRwndPatternConfig\":{\"pattern\":[{\"StaticRwndConfig\":{\"duration\":{\"secs\":1,\"nanos\":0},\"set_rcv_buf\":65536,\"app_read_bytes\":1024}},{\"StaticRwndConfig\":{\"duration\":{\"secs\":1,\"nanos\":0},\"rwnd_remaining\":32768}}],\"count\":2}}" +//! // if the `human` feature is not enabled. +//! # #[cfg(not(feature = "human"))] +//! let config_file_content = "{\"RepeatedRwndPatternConfig\":{\"pattern\":[{\"StaticRwndConfig\":{\"duration\":{\"secs\":1,\"nanos\":0},\"set_rcv_buf\":65536,\"app_read_bytes\":1024}},{\"StaticRwndConfig\":{\"duration\":{\"secs\":1,\"nanos\":0},\"rwnd_remaining\":32768}}],\"count\":2}}"; +//! let des: Box = serde_json::from_str(config_file_content).unwrap(); +//! let mut model = des.into_model(); +//! let (decision, _) = model.next_rwnd().unwrap(); +//! assert_eq!(decision.action, Some(RwndAction::AppRead { bytes: 1024 })); +//! let (decision, _) = model.next_rwnd().unwrap(); +//! assert_eq!(decision.action, Some(RwndAction::Remaining { rwnd: 32768 })); +//! let (decision, _) = model.next_rwnd().unwrap(); +//! assert_eq!(decision.action, Some(RwndAction::AppRead { bytes: 1024 })); +//! let (decision, _) = model.next_rwnd().unwrap(); +//! assert_eq!(decision.action, Some(RwndAction::Remaining { rwnd: 32768 })); +//! assert_eq!(model.next_rwnd(), None); +//! ``` +//! +//! At most one of `app_read_bytes` or `rwnd_remaining` may be set per step — +//! never both. A step with neither produces [`RwndDecision::action`] as `None`, +//! which is valid for steps that only reconfigure the receive buffer. +use crate::{Duration, RwndAction, RwndDecision, RwndTrace}; +use dyn_clone::DynClone; + +/// This trait is used to convert a rwnd trace configuration into a rwnd trace model. +/// +/// Since trace model is often configured with files and often has inner states which +/// is not suitable to be serialized/deserialized, this trait makes it possible to +/// separate the configuration part into a simple struct for serialization/deserialization, and +/// construct the model from the configuration. +#[cfg_attr(feature = "serde", typetag::serde)] +pub trait RwndTraceConfig: DynClone + Send { + fn into_model(self: Box) -> Box; +} + +dyn_clone::clone_trait_object!(RwndTraceConfig); + +#[cfg(feature = "serde")] +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +/// The model of a static rwnd trace: a single decision valid for one duration. +/// +/// ## Examples +/// +/// ``` +/// # use netem_trace::model::StaticRwndConfig; +/// # use netem_trace::{Duration, RwndTrace, RwndAction}; +/// let mut static_rwnd = StaticRwndConfig::new() +/// .set_rcv_buf(65536) +/// .app_read(1024) +/// .duration(Duration::from_secs(1)) +/// .build(); +/// let (decision, duration) = static_rwnd.next_rwnd().unwrap(); +/// assert_eq!(decision.set_rcv_buf, Some(65536)); +/// assert_eq!(decision.action, Some(RwndAction::AppRead { bytes: 1024 })); +/// assert_eq!(duration, Duration::from_secs(1)); +/// assert_eq!(static_rwnd.next_rwnd(), None); +/// ``` +#[derive(Debug, Clone)] +pub struct StaticRwnd { + pub decision: RwndDecision, + pub duration: Option, +} + +/// The configuration struct for [`StaticRwnd`]. +/// +/// The serialized JSON form is **flat**: a step looks like +/// `{"duration":"1s","set_rcv_buf":65536,"app_read_bytes":1024}` (or +/// `{"duration":"1s","rwnd_remaining":32768}`), never with an `action` wrapper. +/// +/// At most one of `app_read_bytes` / `rwnd_remaining` may be set; the deserializer +/// rejects inputs where both are present. A step with neither is valid and produces +/// [`RwndDecision::action`] as `None` (useful for steps that only reconfigure the +/// receive buffer). +#[derive(Debug, Clone, Default)] +pub struct StaticRwndConfig { + pub duration: Option, + pub set_rcv_buf: Option, + pub action: Option, +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for StaticRwndConfig { + fn deserialize>(deserializer: D) -> Result { + #[derive(Deserialize, Default)] + #[serde(default)] + struct Helper { + #[cfg_attr(feature = "human", serde(with = "humantime_serde"))] + #[serde(default)] + duration: Option, + #[serde(default)] + set_rcv_buf: Option, + #[serde(default)] + app_read_bytes: Option, + #[serde(default)] + rwnd_remaining: Option, + } + + let h = Helper::deserialize(deserializer)?; + let action = match (h.app_read_bytes, h.rwnd_remaining) { + (Some(bytes), None) => Some(RwndAction::AppRead { bytes }), + (None, Some(rwnd)) => Some(RwndAction::Remaining { rwnd }), + (Some(_), Some(_)) => { + return Err(serde::de::Error::custom( + "rwnd step cannot set both `app_read_bytes` and `rwnd_remaining`", + )); + } + (None, None) => None, + }; + Ok(Self { + duration: h.duration, + set_rcv_buf: h.set_rcv_buf, + action, + }) + } +} + +#[cfg(feature = "serde")] +impl Serialize for StaticRwndConfig { + fn serialize(&self, serializer: S) -> Result { + #[derive(Serialize)] + struct Out { + #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "human", serde(with = "humantime_serde"))] + duration: Option, + #[serde(skip_serializing_if = "Option::is_none")] + set_rcv_buf: Option, + #[serde(skip_serializing_if = "Option::is_none")] + app_read_bytes: Option, + #[serde(skip_serializing_if = "Option::is_none")] + rwnd_remaining: Option, + } + + let (app_read_bytes, rwnd_remaining) = match &self.action { + Some(RwndAction::AppRead { bytes }) => (Some(*bytes), None), + Some(RwndAction::Remaining { rwnd }) => (None, Some(*rwnd)), + None => (None, None), + }; + Out { + duration: self.duration, + set_rcv_buf: self.set_rcv_buf, + app_read_bytes, + rwnd_remaining, + } + .serialize(serializer) + } +} + +/// The model contains an array of rwnd trace models. +/// +/// Combine multiple rwnd trace models into one rwnd pattern, +/// and repeat the pattern for `count` times. +/// +/// If `count` is 0, the pattern will be repeated forever. +/// +/// ## Examples +/// +/// The most common use case is to read from a configuration file and +/// deserialize it into a [`RepeatedRwndPatternConfig`]: +/// +/// ``` +/// # use netem_trace::model::{StaticRwndConfig, RwndTraceConfig}; +/// # use netem_trace::{Duration, RwndTrace, RwndAction}; +/// # #[cfg(feature = "human")] +/// # let config_file_content = "{\"RepeatedRwndPatternConfig\":{\"pattern\":[{\"StaticRwndConfig\":{\"duration\":\"1s\",\"set_rcv_buf\":65536,\"app_read_bytes\":1024}},{\"StaticRwndConfig\":{\"duration\":\"1s\",\"rwnd_remaining\":32768}}],\"count\":2}}"; +/// # #[cfg(not(feature = "human"))] +/// let config_file_content = "{\"RepeatedRwndPatternConfig\":{\"pattern\":[{\"StaticRwndConfig\":{\"duration\":{\"secs\":1,\"nanos\":0},\"set_rcv_buf\":65536,\"app_read_bytes\":1024}},{\"StaticRwndConfig\":{\"duration\":{\"secs\":1,\"nanos\":0},\"rwnd_remaining\":32768}}],\"count\":2}}"; +/// let des: Box = serde_json::from_str(config_file_content).unwrap(); +/// let mut model = des.into_model(); +/// let (decision, _) = model.next_rwnd().unwrap(); +/// assert_eq!(decision.action, Some(RwndAction::AppRead { bytes: 1024 })); +/// ``` +pub struct RepeatedRwndPattern { + pub pattern: Vec>, + pub count: usize, + current_model: Option>, + current_cycle: usize, + current_pattern: usize, +} + +/// The configuration struct for [`RepeatedRwndPattern`]. +/// +/// See [`RepeatedRwndPattern`] for more details. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(default))] +#[derive(Default, Clone)] +pub struct RepeatedRwndPatternConfig { + pub pattern: Vec>, + pub count: usize, +} + +impl RwndTrace for StaticRwnd { + fn next_rwnd(&mut self) -> Option<(RwndDecision, Duration)> { + if let Some(duration) = self.duration.take() { + if duration.is_zero() { + None + } else { + Some((self.decision.clone(), duration)) + } + } else { + None + } + } +} + +impl RwndTrace for RepeatedRwndPattern { + fn next_rwnd(&mut self) -> Option<(RwndDecision, Duration)> { + let pattern_len = self.pattern.len(); + // Allow at most pattern_len + 1 consecutive inner-None results before + // giving up. The +1 covers a possibly-exhausted current_model at entry; + // after that, each remaining slot is a fresh clone whose behaviour is + // deterministic. If all pattern_len fresh clones return None, the + // pattern will never produce a value regardless of count. + let mut budget = pattern_len + 1; + loop { + if pattern_len == 0 || (self.count != 0 && self.current_cycle >= self.count) { + return None; + } + if budget == 0 { + return None; + } + if self.current_model.is_none() { + self.current_model = Some(self.pattern[self.current_pattern].clone().into_model()); + } + match self.current_model.as_mut().unwrap().next_rwnd() { + Some(item) => return Some(item), + None => { + self.current_model = None; + budget -= 1; + self.current_pattern += 1; + if self.current_pattern >= pattern_len { + self.current_pattern = 0; + self.current_cycle += 1; + if self.count != 0 && self.current_cycle >= self.count { + return None; + } + } + } + } + } + } +} + +impl StaticRwndConfig { + pub fn new() -> Self { + Self { + duration: None, + set_rcv_buf: None, + action: None, + } + } + + pub fn duration(mut self, duration: Duration) -> Self { + self.duration = Some(duration); + self + } + + pub fn set_rcv_buf(mut self, set_rcv_buf: u64) -> Self { + self.set_rcv_buf = Some(set_rcv_buf); + self + } + + pub fn app_read(mut self, bytes: u64) -> Self { + self.action = Some(RwndAction::AppRead { bytes }); + self + } + + pub fn remaining(mut self, rwnd: u64) -> Self { + self.action = Some(RwndAction::Remaining { rwnd }); + self + } + + pub fn build(self) -> StaticRwnd { + StaticRwnd { + decision: RwndDecision { + set_rcv_buf: self.set_rcv_buf, + action: self.action, + }, + duration: Some(self.duration.unwrap_or_else(|| Duration::from_secs(1))), + } + } +} + +impl RepeatedRwndPatternConfig { + pub fn new() -> Self { + Self { + pattern: vec![], + count: 0, + } + } + + pub fn pattern(mut self, pattern: Vec>) -> Self { + self.pattern = pattern; + self + } + + pub fn count(mut self, count: usize) -> Self { + self.count = count; + self + } + + pub fn build(self) -> RepeatedRwndPattern { + RepeatedRwndPattern { + pattern: self.pattern, + count: self.count, + current_model: None, + current_cycle: 0, + current_pattern: 0, + } + } +} + +macro_rules! impl_rwnd_trace_config { + ($name:ident) => { + #[cfg_attr(feature = "serde", typetag::serde)] + impl RwndTraceConfig for $name { + fn into_model(self: Box<$name>) -> Box { + Box::new(self.build()) + } + } + }; +} + +impl_rwnd_trace_config!(StaticRwndConfig); +impl_rwnd_trace_config!(RepeatedRwndPatternConfig); + +#[cfg(test)] +mod test { + use super::*; + use crate::model::StaticRwndConfig; + use crate::RwndTrace; + + #[test] + fn test_static_rwnd_model_app_read() { + let mut static_rwnd = StaticRwndConfig::new() + .set_rcv_buf(65536) + .app_read(1024) + .duration(Duration::from_secs(1)) + .build(); + let (decision, duration) = static_rwnd.next_rwnd().unwrap(); + assert_eq!(decision.set_rcv_buf, Some(65536)); + assert_eq!(decision.action, Some(RwndAction::AppRead { bytes: 1024 })); + assert_eq!(duration, Duration::from_secs(1)); + assert_eq!(static_rwnd.next_rwnd(), None); + } + + #[test] + fn test_static_rwnd_model_remaining() { + let mut static_rwnd = StaticRwndConfig::new() + .remaining(32768) + .duration(Duration::from_secs(2)) + .build(); + let (decision, duration) = static_rwnd.next_rwnd().unwrap(); + assert_eq!(decision.set_rcv_buf, None); + assert_eq!(decision.action, Some(RwndAction::Remaining { rwnd: 32768 })); + assert_eq!(duration, Duration::from_secs(2)); + assert_eq!(static_rwnd.next_rwnd(), None); + } + + #[test] + fn test_repeated_rwnd_pattern() { + let pat = vec![ + Box::new( + StaticRwndConfig::new() + .app_read(1024) + .duration(Duration::from_secs(1)), + ) as Box, + Box::new( + StaticRwndConfig::new() + .remaining(32768) + .duration(Duration::from_secs(1)), + ) as Box, + ]; + let mut model = RepeatedRwndPatternConfig::new() + .pattern(pat) + .count(2) + .build(); + let next = model.next_rwnd().unwrap(); + assert_eq!(next.0.action, Some(RwndAction::AppRead { bytes: 1024 })); + assert_eq!(next.1, Duration::from_secs(1)); + let next = model.next_rwnd().unwrap(); + assert_eq!(next.0.action, Some(RwndAction::Remaining { rwnd: 32768 })); + let next = model.next_rwnd().unwrap(); + assert_eq!(next.0.action, Some(RwndAction::AppRead { bytes: 1024 })); + let next = model.next_rwnd().unwrap(); + assert_eq!(next.0.action, Some(RwndAction::Remaining { rwnd: 32768 })); + assert_eq!(model.next_rwnd(), None); + } + + #[test] + #[cfg(feature = "serde")] + fn test_serde_roundtrip_app_read() { + let cfg = Box::new( + StaticRwndConfig::new() + .set_rcv_buf(65536) + .app_read(1024) + .duration(Duration::from_secs(1)), + ) as Box; + let ser_str = serde_json::to_string(&cfg).unwrap(); + #[cfg(feature = "human")] + let expected = "{\"StaticRwndConfig\":{\"duration\":\"1s\",\"set_rcv_buf\":65536,\"app_read_bytes\":1024}}"; + #[cfg(not(feature = "human"))] + let expected = "{\"StaticRwndConfig\":{\"duration\":{\"secs\":1,\"nanos\":0},\"set_rcv_buf\":65536,\"app_read_bytes\":1024}}"; + assert_eq!(ser_str, expected); + + let des: Box = serde_json::from_str(&ser_str).unwrap(); + let mut model = des.into_model(); + let (decision, duration) = model.next_rwnd().unwrap(); + assert_eq!(decision.set_rcv_buf, Some(65536)); + assert_eq!(decision.action, Some(RwndAction::AppRead { bytes: 1024 })); + assert_eq!(duration, Duration::from_secs(1)); + } + + #[test] + #[cfg(feature = "serde")] + fn test_serde_roundtrip_remaining() { + let cfg = Box::new( + StaticRwndConfig::new() + .remaining(32768) + .duration(Duration::from_secs(1)), + ) as Box; + let ser_str = serde_json::to_string(&cfg).unwrap(); + #[cfg(feature = "human")] + let expected = "{\"StaticRwndConfig\":{\"duration\":\"1s\",\"rwnd_remaining\":32768}}"; + #[cfg(not(feature = "human"))] + let expected = "{\"StaticRwndConfig\":{\"duration\":{\"secs\":1,\"nanos\":0},\"rwnd_remaining\":32768}}"; + assert_eq!(ser_str, expected); + + let des: Box = serde_json::from_str(&ser_str).unwrap(); + let mut model = des.into_model(); + let (decision, _) = model.next_rwnd().unwrap(); + assert_eq!(decision.action, Some(RwndAction::Remaining { rwnd: 32768 })); + } + + #[test] + #[cfg(feature = "serde")] + fn test_serde_rejects_both() { + // Omit duration to avoid the human/non-human format ambiguity; we're testing + // the action constraint, not duration parsing. + let json = "{\"StaticRwndConfig\":{\"app_read_bytes\":1024,\"rwnd_remaining\":32768}}"; + let result: Result, _> = serde_json::from_str(json); + let err = result + .err() + .expect("deserialization should have failed") + .to_string(); + assert!( + err.contains("cannot set both"), + "expected 'cannot set both' in error, got: {err}" + ); + } + + #[test] + fn test_static_rwnd_set_rcv_buf_only() { + let mut model = StaticRwndConfig::new() + .set_rcv_buf(131072) + .duration(Duration::from_secs(1)) + .build(); + let (decision, duration) = model.next_rwnd().unwrap(); + assert_eq!(decision.set_rcv_buf, Some(131072)); + assert_eq!(decision.action, None); + assert_eq!(duration, Duration::from_secs(1)); + assert_eq!(model.next_rwnd(), None); + } + + #[test] + #[cfg(feature = "serde")] + fn test_serde_roundtrip_set_rcv_buf_only() { + let cfg = Box::new( + StaticRwndConfig::new() + .set_rcv_buf(131072) + .duration(Duration::from_secs(1)), + ) as Box; + let ser_str = serde_json::to_string(&cfg).unwrap(); + #[cfg(feature = "human")] + let expected = "{\"StaticRwndConfig\":{\"duration\":\"1s\",\"set_rcv_buf\":131072}}"; + #[cfg(not(feature = "human"))] + let expected = + "{\"StaticRwndConfig\":{\"duration\":{\"secs\":1,\"nanos\":0},\"set_rcv_buf\":131072}}"; + assert_eq!(ser_str, expected); + + let des: Box = serde_json::from_str(&ser_str).unwrap(); + let mut model = des.into_model(); + let (decision, duration) = model.next_rwnd().unwrap(); + assert_eq!(decision.set_rcv_buf, Some(131072)); + assert_eq!(decision.action, None); + assert_eq!(duration, Duration::from_secs(1)); + assert_eq!(model.next_rwnd(), None); + } + + #[test] + #[cfg(feature = "serde")] + fn test_serde_action_none_when_neither_set() { + // A step with only set_rcv_buf and no action fields should deserialize to action: None. + let json = "{\"StaticRwndConfig\":{\"set_rcv_buf\":65536}}"; + let des: Box = serde_json::from_str(json).unwrap(); + let mut model = des.into_model(); + let (decision, _) = model.next_rwnd().unwrap(); + assert_eq!(decision.set_rcv_buf, Some(65536)); + assert_eq!(decision.action, None); + } + + #[test] + fn test_repeated_rwnd_pattern_all_zero_duration_terminates() { + // All inner models have duration == 0 and return None immediately. + // With count == 0 (infinite repeat) the old recursive implementation + // would spin forever; the loop-based one must return None promptly. + let pat = vec![ + Box::new( + StaticRwndConfig::new() + .app_read(1024) + .duration(Duration::ZERO), + ) as Box, + Box::new( + StaticRwndConfig::new() + .remaining(32768) + .duration(Duration::ZERO), + ) as Box, + ]; + let mut model = RepeatedRwndPatternConfig::new() + .pattern(pat) + .count(0) // infinite + .build(); + assert_eq!(model.next_rwnd(), None); + } +}