/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 | | } |