Coverage Report

Created: 2025-12-31 06:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/neqo/test-fixture/src/sim/mod.rs
Line
Count
Source
1
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
2
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
3
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
4
// option. This file may not be copied, modified, or distributed
5
// except according to those terms.
6
7
#![expect(clippy::unwrap_used, reason = "This is test code.")]
8
9
/// Tests with simulated network components.
10
pub mod connection;
11
mod delay;
12
mod drop;
13
pub mod http3_connection;
14
mod mtu;
15
pub mod rng;
16
mod taildrop;
17
18
use std::{
19
    cell::RefCell,
20
    cmp::min,
21
    fmt::Debug,
22
    fs::{create_dir_all, File},
23
    ops::{Deref, DerefMut},
24
    path::PathBuf,
25
    rc::Rc,
26
    time::{Duration, Instant},
27
};
28
29
use neqo_common::{qdebug, qerror, qinfo, qtrace, Datagram, Encoder};
30
use neqo_transport::Output;
31
use rng::Random;
32
use NodeState::{Active, Idle, Waiting};
33
34
use crate::now;
35
36
pub mod network {
37
    pub use super::{
38
        delay::{Delay, RandomDelay},
39
        drop::Drop,
40
        mtu::Mtu,
41
        taildrop::TailDrop,
42
    };
43
}
44
45
type Rng = Rc<RefCell<Random>>;
46
47
/// A macro that turns a list of values into boxed versions of the same.
48
#[macro_export]
49
macro_rules! boxed {
50
    [$($v:expr),+ $(,)?] => {
51
        vec![ $( Box::new($v) as _ ),+ ]
52
    };
53
}
54
55
/// Create a simulation test case.  This takes either two or three arguments.
56
///
57
/// The two argument form takes a bare name (`ident`), a comma, and an array of
58
/// items that implement `Node`.
59
///
60
/// The three argument form adds a setup block that can be used to construct a
61
/// complex value that is then shared between all nodes.  The values in the
62
/// three-argument form have to be closures (or functions) that accept a reference
63
/// to the value returned by the setup.
64
#[macro_export]
65
macro_rules! simulate {
66
    ($n:ident, [ $($v:expr),+ $(,)? ] $(,)?) => {
67
        simulate!($n, (), [ $(|_| $v),+ ]);
68
    };
69
    ($n:ident, $setup:expr, [ $( $v:expr ),+ $(,)? ] $(,)?) => {
70
        #[test]
71
        fn $n() {
72
            let fixture = $setup;
73
            let mut nodes: Vec<Box<dyn $crate::sim::Node>> = Vec::new();
74
            $(
75
                let f: Box<dyn FnOnce(&_) -> _> = Box::new($v);
76
                nodes.push(Box::new(f(&fixture)));
77
            )*
78
            Simulator::new(stringify!($n), nodes).run();
79
        }
80
    };
81
}
82
83
pub trait Node: Debug {
84
0
    fn init(&mut self, _rng: Rng, _now: Instant) {}
85
    /// Perform processing.  This optionally takes a datagram and produces either
86
    /// another data, a time that the simulator needs to wait, or nothing.
87
    fn process(&mut self, d: Option<Datagram>, now: Instant) -> Output;
88
    /// This is called after setup is complete and before the main processing starts.
89
0
    fn prepare(&mut self, _now: Instant) {}
90
    /// An node can report when it considers itself "done".
91
    /// Prior to calling `prepare`, this should return `true` if it is ready.
92
0
    fn done(&self) -> bool {
93
0
        true
94
0
    }
95
    /// Print out a summary of the state of the node.
96
0
    fn print_summary(&self, _test_name: &str) {}
97
}
98
99
/// The state of a single node.  Nodes will be activated if they are `Active`
100
/// or if the previous node in the loop generated a datagram.  Nodes that return
101
/// `true` from `Node::done` will be activated as normal.
102
#[derive(Clone, Copy, Debug, PartialEq)]
103
enum NodeState {
104
    /// The node just produced a datagram.  It should be activated again as soon as possible.
105
    Active,
106
    /// The node is waiting.
107
    Waiting(Instant),
108
    /// The node became idle.
109
    Idle,
110
}
111
112
#[derive(Debug)]
113
struct NodeHolder {
114
    node: Box<dyn Node>,
115
    state: NodeState,
116
}
117
118
impl NodeHolder {
119
0
    fn ready(&self, now: Instant) -> bool {
120
0
        match self.state {
121
0
            Active => true,
122
0
            Waiting(t) => t <= now,
123
0
            Idle => false,
124
        }
125
0
    }
126
}
127
128
impl Deref for NodeHolder {
129
    type Target = dyn Node;
130
0
    fn deref(&self) -> &Self::Target {
131
0
        self.node.as_ref()
132
0
    }
133
}
134
135
impl DerefMut for NodeHolder {
136
0
    fn deref_mut(&mut self) -> &mut Self::Target {
137
0
        self.node.as_mut()
138
0
    }
139
}
140
141
/// The status of the processing of an event.
142
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143
pub enum GoalStatus {
144
    /// The event didn't result in doing anything; the goal is waiting for something.
145
    Waiting,
146
    /// An action was taken as a result of the event.
147
    Active,
148
    /// The goal was accomplished.
149
    Done,
150
}
151
152
pub struct Simulator {
153
    name: String,
154
    nodes: Vec<NodeHolder>,
155
    rng: Rng,
156
}
157
158
impl Simulator {
159
0
    pub fn new<A: AsRef<str>, I: IntoIterator<Item = Box<dyn Node>>>(name: A, nodes: I) -> Self {
160
0
        let name = String::from(name.as_ref());
161
        // The first node is marked as Active, the rest are idle.
162
0
        let mut it = nodes.into_iter();
163
0
        let nodes = it
164
0
            .next()
165
0
            .map(|node| NodeHolder {
166
0
                node,
167
0
                state: Active,
168
0
            })
169
0
            .into_iter()
170
0
            .chain(it.map(|node| NodeHolder { node, state: Idle }))
171
0
            .collect::<Vec<_>>();
172
0
        let mut sim = Self {
173
0
            name,
174
0
            nodes,
175
0
            rng: Rc::default(),
176
0
        };
177
        // Seed from the `SIMULATION_SEED` environment variable, if set.
178
0
        if let Ok(seed) = std::env::var("SIMULATION_SEED") {
179
0
            sim.seed_str(seed);
180
0
        }
181
        // Dump the seed to a file to the directory in the `DUMP_SIMULATION_SEEDS` environment
182
        // variable, if set.
183
0
        if let Ok(dir) = std::env::var("DUMP_SIMULATION_SEEDS") {
184
0
            if create_dir_all(&dir).is_err() {
185
0
                qerror!("Failed to create directory {dir}");
186
            } else {
187
0
                let seed_str = sim.rng.borrow().seed_str();
188
0
                let path = PathBuf::from(format!("{dir}/{}-{seed_str}", sim.name));
189
0
                if File::create(&path).is_err() {
190
0
                    qerror!("Failed to write seed to {}", path.to_string_lossy());
191
0
                }
192
            }
193
0
        }
194
0
        sim
195
0
    }
196
197
    /// Seed from a hex string.
198
    /// # Panics
199
    /// When the provided string is not 32 bytes of hex (64 characters).
200
0
    pub fn seed_str<A: AsRef<str>>(&mut self, seed: A) {
201
0
        let seed = <[u8; 32]>::try_from(Encoder::from_hex(seed).as_ref()).unwrap();
202
0
        self.rng = Rc::new(RefCell::new(Random::new(&seed)));
203
0
    }
204
205
0
    fn next_time(&self, now: Instant) -> Instant {
206
0
        let mut next = None;
207
0
        for n in &self.nodes {
208
0
            match n.state {
209
0
                Idle => (),
210
0
                Active => return now,
211
0
                Waiting(a) => next = Some(next.map_or(a, |b| min(a, b))),
212
            }
213
        }
214
0
        next.expect("a node cannot be idle and not done")
215
0
    }
216
217
0
    fn process_loop(&mut self, start: Instant, mut now: Instant) -> Instant {
218
0
        let mut dgram = None;
219
        loop {
220
0
            for n in &mut self.nodes {
221
0
                if dgram.is_none() && !n.ready(now) {
222
0
                    qdebug!("[{}] skipping {:?}", self.name, n.node);
223
0
                    continue;
224
0
                }
225
226
0
                qdebug!("[{}] processing {:?}", self.name, n.node);
227
0
                let res = n.process(dgram.take(), now);
228
0
                n.state = match res {
229
0
                    Output::Datagram(d) => {
230
0
                        qtrace!("[{}]  => datagram {}", self.name, d.len());
231
0
                        dgram = Some(d);
232
0
                        Active
233
                    }
234
0
                    Output::Callback(delay) => {
235
0
                        qtrace!("[{}]  => callback {delay:?}", self.name);
236
0
                        assert_ne!(delay, Duration::new(0, 0));
237
0
                        Waiting(now + delay)
238
                    }
239
                    Output::None => {
240
0
                        qtrace!("[{}]  => nothing", self.name);
241
0
                        assert!(n.done(), "nodes should be done when they go idle");
242
0
                        Idle
243
                    }
244
                };
245
            }
246
247
0
            if self.nodes.iter().all(|n| n.done()) {
248
0
                return now;
249
0
            }
250
251
0
            if dgram.is_none() {
252
0
                let next = self.next_time(now);
253
0
                if next > now {
254
0
                    qdebug!(
255
                        "[{}] advancing time by {:?} to {:?}",
256
                        self.name,
257
0
                        next - now,
258
0
                        next - start
259
                    );
260
0
                    now = next;
261
0
                }
262
0
            }
263
        }
264
0
    }
265
266
    #[must_use]
267
0
    pub fn setup(mut self) -> ReadySimulator {
268
0
        let start = now();
269
270
0
        qinfo!("{}: seed {}", self.name, self.rng.borrow().seed_str());
271
0
        for n in &mut self.nodes {
272
0
            n.init(Rc::clone(&self.rng), start);
273
0
        }
274
275
0
        let setup_start = Instant::now();
276
0
        let now = self.process_loop(start, start);
277
0
        let setup_time = now - start;
278
0
        qinfo!(
279
            "{t}: Setup took {wall:?} (wall) {setup_time:?} (simulated)",
280
            t = self.name,
281
0
            wall = setup_start.elapsed(),
282
        );
283
284
0
        for n in &mut self.nodes {
285
0
            n.prepare(now);
286
0
        }
287
288
0
        ReadySimulator {
289
0
            sim: self,
290
0
            start,
291
0
            now,
292
0
        }
293
0
    }
294
295
    /// Runs the simulation.
296
    /// # Panics
297
    /// When sanity checks fail in unexpected ways; this is a testing function after all.
298
0
    pub fn run(self) {
299
0
        self.setup().run();
300
0
    }
301
302
0
    fn print_summary(&self) {
303
0
        for n in &self.nodes {
304
0
            n.print_summary(&self.name);
305
0
        }
306
0
    }
307
}
308
309
pub struct ReadySimulator {
310
    sim: Simulator,
311
    start: Instant,
312
    now: Instant,
313
}
314
315
impl ReadySimulator {
316
    #[expect(
317
        clippy::must_use_candidate,
318
        reason = "run duration only needed in some tests"
319
    )]
320
0
    pub fn run(mut self) -> Duration {
321
0
        let real_start = Instant::now();
322
0
        let end = self.sim.process_loop(self.start, self.now);
323
0
        let sim_time = end - self.now;
324
0
        qinfo!(
325
            "{t}: Simulation took {wall:?} (wall) {sim_time:?} (simulated)",
326
            t = self.sim.name,
327
0
            wall = real_start.elapsed(),
328
        );
329
0
        self.sim.print_summary();
330
0
        sim_time
331
0
    }
332
}