Skip to main content

netem_trace/model/
pdo.rs

1//! This module contains some predefined packet delivery opportunity (PDO) trace models.
2//!
3//! Enabled with feature `pdo-model` or `model`.
4//!
5//! These traces have no concept of bandwidth. They say when bytes can be delivered,
6//! like a mahimahi trace or the traces recorded by CellReplay.
7//!
8//! ## Predefined models
9//!
10//! - [`StaticHeavyPdo`]: A heavy PDO trace model with a fixed interval and size.
11//! - [`TraceHeavyPdo`]: A heavy PDO trace model replaying a list of `(interval, bytes)`.
12//! - [`RepeatedHeavyPdoPattern`]: A heavy PDO trace model with a repeated pattern.
13//! - [`TraceLightPdo`]: A light PDO trace model replaying a list of [`LightPdo`] records,
14//!   which can be loaded from a CellReplay light PDO trace with [`load_cellreplay_light_pdo_trace`].
15//!
16//! ## Examples
17//!
18//! ```
19//! # use netem_trace::model::StaticHeavyPdoConfig;
20//! # use netem_trace::{Duration, HeavyPdoTrace};
21//! let mut static_pdo = StaticHeavyPdoConfig::new()
22//!     .interval(Duration::from_millis(1))
23//!     .bytes(1500)
24//!     .count(2)
25//!     .build();
26//! assert_eq!(static_pdo.next_heavy_pdo(), Some((Duration::from_millis(1), 1500)));
27//! assert_eq!(static_pdo.next_heavy_pdo(), Some((Duration::from_millis(1), 1500)));
28//! assert_eq!(static_pdo.next_heavy_pdo(), None);
29//! ```
30//!
31//! A more common use case is to build model from a configuration file (e.g. json file):
32//!
33//! ```
34//! # use netem_trace::model::HeavyPdoTraceConfig;
35//! # use netem_trace::{Duration, HeavyPdoTrace};
36//! # #[cfg(feature = "human")]
37//! # let config_file_content = "{\"RepeatedHeavyPdoPatternConfig\":{\"pattern\":[{\"TraceHeavyPdoConfig\":{\"pattern\":[[\"1ms\",1500],[\"0s\",1500]]}}],\"count\":2}}";
38//! // The content would be "{\"RepeatedHeavyPdoPatternConfig\":{\"pattern\":[{\"TraceHeavyPdoConfig\":{\"pattern\":[[{\"secs\":0,\"nanos\":1000000},1500],[{\"secs\":0,\"nanos\":0},1500]]}}],\"count\":2}}"
39//! // if the `human` feature is not enabled.
40//! # #[cfg(not(feature = "human"))]
41//! let config_file_content = "{\"RepeatedHeavyPdoPatternConfig\":{\"pattern\":[{\"TraceHeavyPdoConfig\":{\"pattern\":[[{\"secs\":0,\"nanos\":1000000},1500],[{\"secs\":0,\"nanos\":0},1500]]}}],\"count\":2}}";
42//! let des: Box<dyn HeavyPdoTraceConfig> = serde_json::from_str(config_file_content).unwrap();
43//! let mut model = des.into_model();
44//! assert_eq!(model.next_heavy_pdo(), Some((Duration::from_millis(1), 1500)));
45//! assert_eq!(model.next_heavy_pdo(), Some((Duration::ZERO, 1500)));
46//! assert_eq!(model.next_heavy_pdo(), Some((Duration::from_millis(1), 1500)));
47//! assert_eq!(model.next_heavy_pdo(), Some((Duration::ZERO, 1500)));
48//! assert_eq!(model.next_heavy_pdo(), None);
49//! ```
50use crate::{Delay, Duration, HeavyPdoTrace, LightPdo, LightPdoTrace};
51use dyn_clone::DynClone;
52
53#[cfg(feature = "serde")]
54use serde::{Deserialize, Serialize};
55
56/// This trait is used to convert a heavy PDO trace configuration into a heavy PDO trace model.
57///
58/// Since trace model is often configured with files and often has inner states which
59/// is not suitable to be serialized/deserialized, this trait makes it possible to
60/// separate the configuration part into a simple struct for serialization/deserialization, and
61/// construct the model from the configuration.
62#[cfg_attr(feature = "serde", typetag::serde)]
63pub trait HeavyPdoTraceConfig: DynClone + Send + std::fmt::Debug {
64    fn into_model(self: Box<Self>) -> Box<dyn HeavyPdoTrace>;
65}
66
67dyn_clone::clone_trait_object!(HeavyPdoTraceConfig);
68
69/// This trait is used to convert a light PDO trace configuration into a light PDO trace model.
70///
71/// See [`HeavyPdoTraceConfig`] for why the configuration and the model are separated.
72#[cfg_attr(feature = "serde", typetag::serde)]
73pub trait LightPdoTraceConfig: DynClone + Send + std::fmt::Debug {
74    fn into_model(self: Box<Self>) -> Box<dyn LightPdoTrace>;
75}
76
77dyn_clone::clone_trait_object!(LightPdoTraceConfig);
78
79/// How many bytes one PDO can deliver in CellReplay (both heavy and light PDO traces).
80///
81/// It is the default PDO size of [`StaticHeavyPdo`] and [`TraceLightPdo`].
82/// For mahimahi traces, use `MAHIMAHI_PDO_BYTES` (1504 bytes) instead.
83pub const CELLREPLAY_PDO_SIZE: u64 = 1400;
84
85/// The model of a static heavy PDO trace.
86///
87/// This model always returns the same `(interval, bytes)` for `count` times.
88///
89/// If the `count` is 0, it will be repeated forever.
90///
91/// If `bytes` is not set, it defaults to [`CELLREPLAY_PDO_SIZE`].
92///
93/// ## Examples
94///
95/// ```
96/// # use netem_trace::model::StaticHeavyPdoConfig;
97/// # use netem_trace::{Duration, HeavyPdoTrace};
98/// let mut static_pdo = StaticHeavyPdoConfig::new()
99///     .interval(Duration::from_millis(1))
100///     .bytes(1500)
101///     .count(1)
102///     .build();
103/// assert_eq!(static_pdo.next_heavy_pdo(), Some((Duration::from_millis(1), 1500)));
104/// assert_eq!(static_pdo.next_heavy_pdo(), None);
105/// ```
106#[derive(Default, Debug, Clone, Copy, PartialEq)]
107pub struct StaticHeavyPdo {
108    pub interval: Duration,
109    pub bytes: u64,
110    pub count: usize,
111    current_count: usize,
112}
113
114/// The configuration struct for [`StaticHeavyPdo`].
115///
116/// See [`StaticHeavyPdo`] for more details.
117#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(default))]
118#[derive(Default, Debug, Clone, Copy, PartialEq)]
119pub struct StaticHeavyPdoConfig {
120    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
121    #[cfg_attr(
122        all(feature = "serde", feature = "human"),
123        serde(with = "humantime_serde")
124    )]
125    pub interval: Option<Duration>,
126    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
127    pub bytes: Option<u64>,
128    pub count: usize,
129}
130
131/// The model of a heavy PDO trace replaying a list of `(interval, bytes)`.
132///
133/// Each item is returned once, in order. Use [`RepeatedHeavyPdoPattern`] to repeat it.
134///
135/// ## Examples
136///
137/// ```
138/// # use netem_trace::model::TraceHeavyPdoConfig;
139/// # use netem_trace::{Duration, HeavyPdoTrace};
140/// let mut trace_pdo = TraceHeavyPdoConfig::new()
141///     .pattern(vec![
142///         (Duration::from_millis(1), 3000),
143///         (Duration::from_millis(2), 1500),
144///     ])
145///     .build();
146/// assert_eq!(trace_pdo.next_heavy_pdo(), Some((Duration::from_millis(1), 3000)));
147/// assert_eq!(trace_pdo.next_heavy_pdo(), Some((Duration::from_millis(2), 1500)));
148/// assert_eq!(trace_pdo.next_heavy_pdo(), None);
149/// ```
150#[derive(Default, Debug, Clone, PartialEq)]
151pub struct TraceHeavyPdo {
152    pub pattern: Vec<(Duration, u64)>,
153    pub index: usize,
154}
155
156/// The configuration struct for [`TraceHeavyPdo`].
157///
158/// See [`TraceHeavyPdo`] for more details.
159#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(default))]
160#[derive(Default, Debug, Clone, PartialEq)]
161pub struct TraceHeavyPdoConfig {
162    #[cfg_attr(
163        all(feature = "serde", feature = "human"),
164        serde(with = "heavy_pattern_serde")
165    )]
166    pub pattern: Vec<(Duration, u64)>,
167}
168
169/// The model contains an array of heavy PDO trace models.
170///
171/// Combine multiple heavy PDO trace models into one pattern,
172/// and repeat the pattern for `count` times.
173///
174/// If `count` is 0, the pattern will be repeated forever.
175///
176/// ## Examples
177///
178/// ```
179/// # use netem_trace::model::{HeavyPdoTraceConfig, RepeatedHeavyPdoPatternConfig, StaticHeavyPdoConfig};
180/// # use netem_trace::{Duration, HeavyPdoTrace};
181/// let pat = vec![
182///     Box::new(
183///         StaticHeavyPdoConfig::new()
184///             .interval(Duration::from_millis(1))
185///             .bytes(1500)
186///             .count(1),
187///     ) as Box<dyn HeavyPdoTraceConfig>,
188///     Box::new(
189///         StaticHeavyPdoConfig::new()
190///             .interval(Duration::from_millis(2))
191///             .bytes(3000)
192///             .count(1),
193///     ) as Box<dyn HeavyPdoTraceConfig>,
194/// ];
195/// let mut model = RepeatedHeavyPdoPatternConfig::new().pattern(pat).count(2).build();
196/// assert_eq!(model.next_heavy_pdo(), Some((Duration::from_millis(1), 1500)));
197/// assert_eq!(model.next_heavy_pdo(), Some((Duration::from_millis(2), 3000)));
198/// assert_eq!(model.next_heavy_pdo(), Some((Duration::from_millis(1), 1500)));
199/// assert_eq!(model.next_heavy_pdo(), Some((Duration::from_millis(2), 3000)));
200/// assert_eq!(model.next_heavy_pdo(), None);
201/// ```
202#[derive(Default)]
203pub struct RepeatedHeavyPdoPattern {
204    pub pattern: Vec<Box<dyn HeavyPdoTraceConfig>>,
205    pub count: usize,
206    current_model: Option<Box<dyn HeavyPdoTrace>>,
207    current_cycle: usize,
208    current_pattern: usize,
209}
210
211/// The configuration struct for [`RepeatedHeavyPdoPattern`].
212///
213/// See [`RepeatedHeavyPdoPattern`] for more details.
214#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(default))]
215#[derive(Default, Debug, Clone)]
216pub struct RepeatedHeavyPdoPatternConfig {
217    pub pattern: Vec<Box<dyn HeavyPdoTraceConfig>>,
218    pub count: usize,
219}
220
221/// The model of a light PDO trace replaying a list of [`LightPdo`] records.
222///
223/// Each record is returned once, in order. Every light PDO can deliver `pdo_size` bytes,
224/// which defaults to [`CELLREPLAY_PDO_SIZE`].
225///
226/// ## Examples
227///
228/// ```
229/// # use netem_trace::model::TraceLightPdoConfig;
230/// # use netem_trace::{Delay, Duration, LightPdo, LightPdoTrace};
231/// let record = LightPdo {
232///     timestamp: Duration::from_millis(0),
233///     base_delay: Delay::from_millis(30),
234///     opportunities: vec![Duration::from_millis(0), Duration::from_millis(2)],
235/// };
236/// let mut trace_pdo = TraceLightPdoConfig::new()
237///     .pattern(vec![record.clone()])
238///     .pdo_size(1200)
239///     .build();
240/// assert_eq!(trace_pdo.pdo_size(), 1200);
241/// assert_eq!(trace_pdo.next_light_pdo(), Some(record));
242/// assert_eq!(trace_pdo.next_light_pdo(), None);
243/// ```
244#[derive(Default, Debug, Clone, PartialEq)]
245pub struct TraceLightPdo {
246    pub pattern: Vec<LightPdo>,
247    pub pdo_size: u64,
248    pub index: usize,
249}
250
251/// The configuration struct for [`TraceLightPdo`].
252///
253/// See [`TraceLightPdo`] for more details.
254#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(default))]
255#[derive(Default, Debug, Clone, PartialEq)]
256pub struct TraceLightPdoConfig {
257    pub pattern: Vec<LightPdo>,
258    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
259    pub pdo_size: Option<u64>,
260}
261
262impl HeavyPdoTrace for StaticHeavyPdo {
263    fn next_heavy_pdo(&mut self) -> Option<(Duration, u64)> {
264        if self.count != 0 && self.count == self.current_count {
265            None
266        } else {
267            self.current_count += 1;
268            Some((self.interval, self.bytes))
269        }
270    }
271}
272
273impl HeavyPdoTrace for TraceHeavyPdo {
274    fn next_heavy_pdo(&mut self) -> Option<(Duration, u64)> {
275        let pdo = self.pattern.get(self.index).copied();
276        if pdo.is_some() {
277            self.index += 1;
278        }
279        pdo
280    }
281}
282
283impl HeavyPdoTrace for RepeatedHeavyPdoPattern {
284    fn next_heavy_pdo(&mut self) -> Option<(Duration, u64)> {
285        if self.pattern.is_empty() || (self.count != 0 && self.current_cycle >= self.count) {
286            None
287        } else {
288            if self.current_model.is_none() {
289                self.current_model = Some(self.pattern[self.current_pattern].clone().into_model());
290            }
291            match self.current_model.as_mut().unwrap().next_heavy_pdo() {
292                Some(pdo) => Some(pdo),
293                None => {
294                    self.current_model = None;
295                    self.current_pattern += 1;
296                    if self.current_pattern >= self.pattern.len() {
297                        self.current_pattern = 0;
298                        self.current_cycle += 1;
299                        if self.count != 0 && self.current_cycle >= self.count {
300                            return None;
301                        }
302                    }
303                    self.next_heavy_pdo()
304                }
305            }
306        }
307    }
308}
309
310impl LightPdoTrace for TraceLightPdo {
311    fn next_light_pdo(&mut self) -> Option<LightPdo> {
312        let record = self.pattern.get(self.index).cloned();
313        if record.is_some() {
314            self.index += 1;
315        }
316        record
317    }
318
319    fn pdo_size(&self) -> u64 {
320        self.pdo_size
321    }
322}
323
324impl StaticHeavyPdoConfig {
325    pub fn new() -> Self {
326        Self {
327            interval: None,
328            bytes: None,
329            count: 0,
330        }
331    }
332
333    pub fn interval(mut self, interval: Duration) -> Self {
334        self.interval = Some(interval);
335        self
336    }
337
338    pub fn bytes(mut self, bytes: u64) -> Self {
339        self.bytes = Some(bytes);
340        self
341    }
342
343    pub fn count(mut self, count: usize) -> Self {
344        self.count = count;
345        self
346    }
347
348    pub fn build(self) -> StaticHeavyPdo {
349        StaticHeavyPdo {
350            interval: self.interval.unwrap_or_else(|| Duration::from_millis(1)),
351            bytes: self.bytes.unwrap_or(CELLREPLAY_PDO_SIZE),
352            count: self.count,
353            current_count: 0,
354        }
355    }
356}
357
358impl TraceHeavyPdoConfig {
359    pub fn new() -> Self {
360        Self { pattern: vec![] }
361    }
362
363    pub fn pattern(mut self, pattern: Vec<(Duration, u64)>) -> Self {
364        self.pattern = pattern;
365        self
366    }
367
368    pub fn build(self) -> TraceHeavyPdo {
369        TraceHeavyPdo {
370            pattern: self.pattern,
371            index: 0,
372        }
373    }
374}
375
376impl RepeatedHeavyPdoPatternConfig {
377    pub fn new() -> Self {
378        Self {
379            pattern: vec![],
380            count: 0,
381        }
382    }
383
384    pub fn pattern(mut self, pattern: Vec<Box<dyn HeavyPdoTraceConfig>>) -> Self {
385        self.pattern = pattern;
386        self
387    }
388
389    pub fn count(mut self, count: usize) -> Self {
390        self.count = count;
391        self
392    }
393
394    pub fn build(self) -> RepeatedHeavyPdoPattern {
395        RepeatedHeavyPdoPattern {
396            pattern: self.pattern,
397            count: self.count,
398            current_model: None,
399            current_cycle: 0,
400            current_pattern: 0,
401        }
402    }
403}
404
405impl TraceLightPdoConfig {
406    pub fn new() -> Self {
407        Self {
408            pattern: vec![],
409            pdo_size: None,
410        }
411    }
412
413    pub fn pattern(mut self, pattern: Vec<LightPdo>) -> Self {
414        self.pattern = pattern;
415        self
416    }
417
418    pub fn pdo_size(mut self, pdo_size: u64) -> Self {
419        self.pdo_size = Some(pdo_size);
420        self
421    }
422
423    pub fn build(self) -> TraceLightPdo {
424        TraceLightPdo {
425            pattern: self.pattern,
426            pdo_size: self.pdo_size.unwrap_or(CELLREPLAY_PDO_SIZE),
427            index: 0,
428        }
429    }
430}
431
432/// Load a CellReplay light PDO (packet train) trace to a [`TraceLightPdoConfig`].
433///
434/// Each line of the trace is `timestamp base_delay pdo_0 pdo_1 ...`, separated by spaces,
435/// all in milliseconds. The PDO times are measured from the start of the active period.
436///
437/// Returns `Err` string if the trace is invalid.
438///
439/// ## Examples
440///
441/// ```
442/// # use netem_trace::model::load_cellreplay_light_pdo_trace;
443/// # use netem_trace::{Delay, Duration, LightPdo, LightPdoTrace};
444/// let mut trace = load_cellreplay_light_pdo_trace("0 30 0 2 2\n100 32 0 1")
445///     .unwrap()
446///     .build();
447/// assert_eq!(
448///     trace.next_light_pdo(),
449///     Some(LightPdo {
450///         timestamp: Duration::from_millis(0),
451///         base_delay: Delay::from_millis(30),
452///         opportunities: vec![
453///             Duration::from_millis(0),
454///             Duration::from_millis(2),
455///             Duration::from_millis(2),
456///         ],
457///     })
458/// );
459/// ```
460pub fn load_cellreplay_light_pdo_trace(trace: &str) -> Result<TraceLightPdoConfig, &'static str> {
461    let mut pattern: Vec<LightPdo> = vec![];
462    for line in trace.lines() {
463        let mut words = line.split_whitespace();
464        let mut next_ms = || -> Result<Option<Duration>, &'static str> {
465            match words.next() {
466                Some(word) => word
467                    .parse::<u64>()
468                    .map(|ms| Some(Duration::from_millis(ms)))
469                    .map_err(|_| "values must be nonnegative integers in milliseconds"),
470                None => Ok(None),
471            }
472        };
473        let timestamp = next_ms()?.ok_or("invalid empty line")?;
474        let base_delay: Delay = next_ms()?.ok_or("each line must have a base delay")?;
475        let mut opportunities = vec![];
476        while let Some(opportunity) = next_ms()? {
477            if opportunities.last().is_some_and(|last| opportunity < *last) {
478                return Err("PDO times must be monotonically nondecreasing");
479            }
480            opportunities.push(opportunity);
481        }
482        if pattern
483            .last()
484            .is_some_and(|last| timestamp < last.timestamp)
485        {
486            return Err("timestamps must be monotonically nondecreasing");
487        }
488        pattern.push(LightPdo {
489            timestamp,
490            base_delay,
491            opportunities,
492        });
493    }
494    if pattern.is_empty() {
495        return Err("trace must have at least one line");
496    }
497    Ok(TraceLightPdoConfig::new().pattern(pattern))
498}
499
500macro_rules! impl_heavy_pdo_trace_config {
501    ($name:ident) => {
502        #[cfg_attr(feature = "serde", typetag::serde)]
503        impl HeavyPdoTraceConfig for $name {
504            fn into_model(self: Box<$name>) -> Box<dyn HeavyPdoTrace> {
505                Box::new(self.build())
506            }
507        }
508    };
509}
510
511impl_heavy_pdo_trace_config!(StaticHeavyPdoConfig);
512impl_heavy_pdo_trace_config!(TraceHeavyPdoConfig);
513impl_heavy_pdo_trace_config!(RepeatedHeavyPdoPatternConfig);
514
515#[cfg_attr(feature = "serde", typetag::serde)]
516impl LightPdoTraceConfig for TraceLightPdoConfig {
517    fn into_model(self: Box<TraceLightPdoConfig>) -> Box<dyn LightPdoTrace> {
518        Box::new(self.build())
519    }
520}
521
522impl<Model: HeavyPdoTrace + 'static> From<Model> for Box<dyn HeavyPdoTrace> {
523    fn from(model: Model) -> Self {
524        Box::new(model)
525    }
526}
527
528impl<Model: LightPdoTrace + 'static> From<Model> for Box<dyn LightPdoTrace> {
529    fn from(model: Model) -> Self {
530        Box::new(model)
531    }
532}
533
534/// Turn a [`HeavyPdoTraceConfig`] into a forever repeated [`RepeatedHeavyPdoPatternConfig`].
535pub trait Forever: HeavyPdoTraceConfig {
536    fn forever(self) -> RepeatedHeavyPdoPatternConfig;
537}
538
539/// Implement the [`Forever`] trait for the heavy PDO trace model config (any struct implements [`HeavyPdoTraceConfig`]).
540#[macro_export]
541macro_rules! impl_forever_heavy_pdo {
542    ($name:ident) => {
543        impl Forever for $name {
544            fn forever(self) -> RepeatedHeavyPdoPatternConfig {
545                RepeatedHeavyPdoPatternConfig::new()
546                    .pattern(vec![Box::new(self)])
547                    .count(0)
548            }
549        }
550    };
551}
552
553impl_forever_heavy_pdo!(StaticHeavyPdoConfig);
554impl_forever_heavy_pdo!(TraceHeavyPdoConfig);
555
556impl Forever for RepeatedHeavyPdoPatternConfig {
557    fn forever(self) -> RepeatedHeavyPdoPatternConfig {
558        self.count(0)
559    }
560}
561
562#[cfg(all(feature = "serde", feature = "human"))]
563mod heavy_pattern_serde {
564    use super::Duration;
565    use serde::{Deserialize, Deserializer, Serialize, Serializer};
566
567    pub fn serialize<S: Serializer>(
568        v: &[(Duration, u64)],
569        serializer: S,
570    ) -> Result<S::Ok, S::Error> {
571        v.iter()
572            .map(|(interval, bytes)| (humantime_serde::Serde::from(interval), bytes))
573            .collect::<Vec<_>>()
574            .serialize(serializer)
575    }
576
577    pub fn deserialize<'de, D: Deserializer<'de>>(
578        deserializer: D,
579    ) -> Result<Vec<(Duration, u64)>, D::Error> {
580        let v: Vec<(humantime_serde::Serde<Duration>, u64)> = Vec::deserialize(deserializer)?;
581        Ok(v.into_iter()
582            .map(|(interval, bytes)| (interval.into_inner(), bytes))
583            .collect())
584    }
585}
586
587#[cfg(test)]
588mod test {
589    use super::*;
590
591    #[test]
592    fn test_static_heavy_pdo() {
593        let mut model = StaticHeavyPdoConfig::new()
594            .interval(Duration::from_millis(2))
595            .bytes(1400)
596            .count(2)
597            .build();
598        assert_eq!(
599            model.next_heavy_pdo(),
600            Some((Duration::from_millis(2), 1400))
601        );
602        assert_eq!(
603            model.next_heavy_pdo(),
604            Some((Duration::from_millis(2), 1400))
605        );
606        assert_eq!(model.next_heavy_pdo(), None);
607    }
608
609    #[test]
610    fn test_default_pdo_size() {
611        let mut heavy = StaticHeavyPdoConfig::new().count(1).build();
612        assert_eq!(
613            heavy.next_heavy_pdo(),
614            Some((Duration::from_millis(1), CELLREPLAY_PDO_SIZE))
615        );
616
617        let light = TraceLightPdoConfig::new().build();
618        assert_eq!(light.pdo_size(), CELLREPLAY_PDO_SIZE);
619        let light = load_cellreplay_light_pdo_trace("0 30 0").unwrap().build();
620        assert_eq!(light.pdo_size(), CELLREPLAY_PDO_SIZE);
621        let light = TraceLightPdoConfig::new().pdo_size(1500).build();
622        assert_eq!(light.pdo_size(), 1500);
623    }
624
625    #[test]
626    fn test_forever() {
627        let mut model = TraceHeavyPdoConfig::new()
628            .pattern(vec![(Duration::from_millis(1), 1500)])
629            .forever()
630            .build();
631        for _ in 0..5 {
632            assert_eq!(
633                model.next_heavy_pdo(),
634                Some((Duration::from_millis(1), 1500))
635            );
636        }
637    }
638
639    #[test]
640    fn test_empty_repeated() {
641        let mut model = RepeatedHeavyPdoPatternConfig::new().build();
642        assert_eq!(model.next_heavy_pdo(), None);
643    }
644
645    #[test]
646    fn test_load_cellreplay_light_pdo() {
647        assert_eq!(
648            load_cellreplay_light_pdo_trace(""),
649            Err("trace must have at least one line")
650        );
651        assert_eq!(
652            load_cellreplay_light_pdo_trace("0 30\n\n10 30"),
653            Err("invalid empty line")
654        );
655        assert_eq!(
656            load_cellreplay_light_pdo_trace("0"),
657            Err("each line must have a base delay")
658        );
659        assert_eq!(
660            load_cellreplay_light_pdo_trace("0 30 0 x"),
661            Err("values must be nonnegative integers in milliseconds")
662        );
663        assert_eq!(
664            load_cellreplay_light_pdo_trace("0 30 0 3 2"),
665            Err("PDO times must be monotonically nondecreasing")
666        );
667        assert_eq!(
668            load_cellreplay_light_pdo_trace("10 30 0\n5 30 0"),
669            Err("timestamps must be monotonically nondecreasing")
670        );
671
672        // a line with no PDO is fine, it means no light PDO for that period
673        let mut trace = load_cellreplay_light_pdo_trace("0 30 0 1 1 4\n100 25\n")
674            .unwrap()
675            .build();
676        assert_eq!(
677            trace.next_light_pdo(),
678            Some(LightPdo {
679                timestamp: Duration::ZERO,
680                base_delay: Delay::from_millis(30),
681                opportunities: [0, 1, 1, 4].map(Duration::from_millis).to_vec(),
682            })
683        );
684        assert_eq!(
685            trace.next_light_pdo(),
686            Some(LightPdo {
687                timestamp: Duration::from_millis(100),
688                base_delay: Delay::from_millis(25),
689                opportunities: vec![],
690            })
691        );
692        assert_eq!(trace.next_light_pdo(), None);
693    }
694
695    #[test]
696    #[cfg(feature = "serde")]
697    fn test_serde() {
698        let heavy = Box::new(
699            RepeatedHeavyPdoPatternConfig::new()
700                .pattern(vec![Box::new(TraceHeavyPdoConfig::new().pattern(vec![
701                    (Duration::from_millis(1), 1500),
702                    (Duration::ZERO, 1500),
703                ])) as Box<dyn HeavyPdoTraceConfig>])
704                .count(2),
705        ) as Box<dyn HeavyPdoTraceConfig>;
706        let ser_str = serde_json::to_string(&heavy).unwrap();
707        #[cfg(feature = "human")]
708        let des_str = "{\"RepeatedHeavyPdoPatternConfig\":{\"pattern\":[{\"TraceHeavyPdoConfig\":{\"pattern\":[[\"1ms\",1500],[\"0s\",1500]]}}],\"count\":2}}";
709        #[cfg(not(feature = "human"))]
710        let des_str = "{\"RepeatedHeavyPdoPatternConfig\":{\"pattern\":[{\"TraceHeavyPdoConfig\":{\"pattern\":[[{\"secs\":0,\"nanos\":1000000},1500],[{\"secs\":0,\"nanos\":0},1500]]}}],\"count\":2}}";
711        assert_eq!(ser_str, des_str);
712        let des: Box<dyn HeavyPdoTraceConfig> = serde_json::from_str(des_str).unwrap();
713        let mut model = des.into_model();
714        assert_eq!(
715            model.next_heavy_pdo(),
716            Some((Duration::from_millis(1), 1500))
717        );
718
719        let light = Box::new(load_cellreplay_light_pdo_trace("5 30 0 2").unwrap())
720            as Box<dyn LightPdoTraceConfig>;
721        let ser_str = serde_json::to_string(&light).unwrap();
722        #[cfg(feature = "human")]
723        let des_str = "{\"TraceLightPdoConfig\":{\"pattern\":[{\"timestamp\":\"5ms\",\"base_delay\":\"30ms\",\"opportunities\":[\"0s\",\"2ms\"]}]}}";
724        #[cfg(not(feature = "human"))]
725        let des_str = "{\"TraceLightPdoConfig\":{\"pattern\":[{\"timestamp\":{\"secs\":0,\"nanos\":5000000},\"base_delay\":{\"secs\":0,\"nanos\":30000000},\"opportunities\":[{\"secs\":0,\"nanos\":0},{\"secs\":0,\"nanos\":2000000}]}]}}";
726        assert_eq!(ser_str, des_str);
727        let des: Box<dyn LightPdoTraceConfig> = serde_json::from_str(des_str).unwrap();
728        let mut model = des.into_model();
729        assert_eq!(
730            model.next_light_pdo().unwrap().opportunities,
731            vec![Duration::ZERO, Duration::from_millis(2)]
732        );
733        assert_eq!(model.pdo_size(), CELLREPLAY_PDO_SIZE);
734
735        // pdo_size is kept in the config when it is set
736        let light = Box::new(
737            TraceLightPdoConfig::new()
738                .pattern(vec![LightPdo::default()])
739                .pdo_size(1500),
740        ) as Box<dyn LightPdoTraceConfig>;
741        let ser_str = serde_json::to_string(&light).unwrap();
742        assert!(ser_str.ends_with(",\"pdo_size\":1500}}"));
743        let des: Box<dyn LightPdoTraceConfig> = serde_json::from_str(&ser_str).unwrap();
744        assert_eq!(des.into_model().pdo_size(), 1500);
745    }
746}