@mark1russell7/harness 0.14.3 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,481 @@
1
+ //! load HOST PORT --plan PATH [--go PATH] [--sample-ms N] [--max-s N]
2
+ //!
3
+ //! An open-loop driver of an HTTP/1.1 server. Each event is a JSON line stamped on CLOCK_MONOTONIC.
4
+ //! The file --plan has the format systems-load/1: a list of streams. Each stream has a key, a
5
+ //! schedule of the format rigor-schedule/1 and a list of requests. Arrival i of a stream sends request i modulo
6
+ //! the length of the list. Each request is the whole text of an HTTP/1.1 request.
7
+ //!
8
+ //! Each stream has its own connection, thus a slow reply delays only the arrivals of its own stream.
9
+ //! One shared connection gives each request the queue of all the requests before it. Then a heavy
10
+ //! stream moves each other stream, and the noise of one tenant on the others cannot be measured.
11
+ //!
12
+ //! The driver opens each connection and writes ready. Then it waits for the file --go. Then it
13
+ //! sends each arrival at its time, and a send does not wait for a reply (coordinated omission).
14
+ //! The requests of a stream share its connection, and the replies come in their order. The lines
15
+ //! sent and replied name the stream (k) and the arrival (i), and a replied line names the status.
16
+ //! It ends with a done line when each connection has each reply or has ended. An error, an end or
17
+ //! the deadline leaves the rest without a reply.
18
+
19
+ use harness_native::{arrivals_in, emit, emit_at};
20
+ use prober::{errno_name, monotonic_ns};
21
+ use serde_json::{json, Value};
22
+ use std::error::Error;
23
+ use std::io::{ErrorKind, Read, Write};
24
+ use std::net::TcpStream;
25
+ use std::os::fd::AsRawFd;
26
+ use std::path::Path;
27
+ use std::time::Duration;
28
+
29
+ /// The longest head of a response that the driver reads: more is an error of the server.
30
+ const HEAD_LIMIT: usize = 65_536;
31
+
32
+ /// One stream of a plan: its key, its arrivals in ns from the start, and its requests.
33
+ struct Stream {
34
+ key: String,
35
+ times: Vec<u64>,
36
+ requests: Vec<Vec<u8>>,
37
+ }
38
+
39
+ /// The streams of a file of the format systems-load/1.
40
+ fn plan_of(text: &str) -> Result<Vec<Stream>, Box<dyn Error>> {
41
+ let plan: Value = serde_json::from_str(text)?;
42
+ if plan["format"] != "systems-load/1" {
43
+ return Err(format!("the plan has the format {}, not systems-load/1", plan["format"]).into());
44
+ }
45
+ let streams = plan["streams"].as_array().ok_or("the plan has no list of streams")?;
46
+ if streams.is_empty() {
47
+ return Err("the plan has no stream".into());
48
+ }
49
+ let mut keys = std::collections::HashSet::new();
50
+ streams
51
+ .iter()
52
+ .map(|stream| -> Result<Stream, Box<dyn Error>> {
53
+ let key = stream["key"].as_str().ok_or("a stream has no key")?.to_string();
54
+ if !keys.insert(key.clone()) {
55
+ return Err(format!("two streams have the key {key}").into());
56
+ }
57
+ let times = arrivals_in(&stream["schedule"])?;
58
+ let requests = stream["requests"]
59
+ .as_array()
60
+ .ok_or_else(|| format!("the stream {key} has no list of requests"))?
61
+ .iter()
62
+ .map(|request| request.as_str().map(|text| text.as_bytes().to_vec()).ok_or_else(|| format!("a request of the stream {key} is not text")))
63
+ .collect::<Result<Vec<_>, String>>()?;
64
+ if requests.is_empty() {
65
+ return Err(format!("the stream {key} has no request").into());
66
+ }
67
+ Ok(Stream { key, times, requests })
68
+ })
69
+ .collect()
70
+ }
71
+
72
+ fn find(bytes: &[u8], needle: &[u8]) -> Option<usize> {
73
+ bytes.windows(needle.len()).position(|window| window == needle)
74
+ }
75
+
76
+ /// The end of a chunked body that starts at `at`, or None while the body is not all there.
77
+ fn chunked_end(bytes: &[u8], mut at: usize) -> Result<Option<usize>, String> {
78
+ loop {
79
+ let Some(line) = find(&bytes[at..], b"\r\n") else { return Ok(None) };
80
+ let text = std::str::from_utf8(&bytes[at..at + line]).map_err(|_| "a chunk size that is not UTF-8".to_string())?;
81
+ let digits = text.split(';').next().unwrap_or("").trim();
82
+ let size = usize::from_str_radix(digits, 16).map_err(|_| format!("the chunk size {text:?}"))?;
83
+ at += line + 2;
84
+ if size == 0 {
85
+ // the trailers, if any, end at an empty line
86
+ if bytes.len() < at + 2 {
87
+ return Ok(None);
88
+ }
89
+ if &bytes[at..at + 2] == b"\r\n" {
90
+ return Ok(Some(at + 2));
91
+ }
92
+ return Ok(find(&bytes[at..], b"\r\n\r\n").map(|end| at + end + 4));
93
+ }
94
+ if bytes.len() < at + size + 2 {
95
+ return Ok(None);
96
+ }
97
+ if &bytes[at + size..at + size + 2] != b"\r\n" {
98
+ return Err("a chunk with no CRLF after it".into());
99
+ }
100
+ at += size + 2;
101
+ }
102
+ }
103
+
104
+ /// The length and the status of the first response in `bytes`, or None while it is not all there.
105
+ /// A response has a Content-Length or a chunked body, or it has no body (204 and 304). A response
106
+ /// with no length ends only at the end of its connection, thus the driver refuses it.
107
+ fn response_length(bytes: &[u8]) -> Result<Option<(usize, u16)>, String> {
108
+ let Some(end) = find(bytes, b"\r\n\r\n") else {
109
+ return if bytes.len() > HEAD_LIMIT { Err(format!("a head of more than {HEAD_LIMIT} bytes")) } else { Ok(None) };
110
+ };
111
+ let head = std::str::from_utf8(&bytes[..end]).map_err(|_| "a head that is not UTF-8".to_string())?;
112
+ let mut lines = head.split("\r\n");
113
+ let status_line = lines.next().unwrap_or("");
114
+ let mut parts = status_line.splitn(3, ' ');
115
+ let version = parts.next().unwrap_or("");
116
+ let status = parts.next().and_then(|code| code.parse::<u16>().ok());
117
+ let Some(status) = status.filter(|_| version.starts_with("HTTP/1.")) else {
118
+ return Err(format!("the status line {status_line:?}"));
119
+ };
120
+ if status < 200 {
121
+ return Err(format!("an interim response {status}: the driver sends no Expect and no Upgrade"));
122
+ }
123
+ let (mut length, mut chunked) = (None, false);
124
+ for line in lines {
125
+ let Some((name, value)) = line.split_once(':') else { return Err(format!("the header line {line:?}")) };
126
+ let value = value.trim();
127
+ if name.eq_ignore_ascii_case("content-length") {
128
+ length = Some(value.parse::<usize>().map_err(|_| format!("the length {value:?}"))?);
129
+ } else if name.eq_ignore_ascii_case("transfer-encoding") {
130
+ chunked = value.split(',').any(|coding| coding.trim().eq_ignore_ascii_case("chunked"));
131
+ }
132
+ }
133
+ let body = end + 4;
134
+ if status == 204 || status == 304 {
135
+ return Ok(Some((body, status)));
136
+ }
137
+ if chunked {
138
+ return Ok(chunked_end(bytes, body)?.map(|whole| (whole, status)));
139
+ }
140
+ match length {
141
+ Some(length) => Ok((bytes.len() >= body + length).then_some((body + length, status))),
142
+ None => Err(format!("a response of status {status} with no length")),
143
+ }
144
+ }
145
+
146
+ /// One stream on its connection, and where it is in its schedule.
147
+ struct Connection {
148
+ stream: Stream,
149
+ socket: TcpStream,
150
+ /// the next arrival to send, and the bytes of it already written
151
+ next: usize,
152
+ written: usize,
153
+ answered: usize,
154
+ /// the socket took no more bytes: the next write waits for POLLOUT
155
+ blocked: bool,
156
+ open: bool,
157
+ /// the bytes received and not yet a whole response
158
+ buffer: Vec<u8>,
159
+ }
160
+
161
+ impl Connection {
162
+ fn open(host: &str, port: u16, stream: Stream) -> std::io::Result<Self> {
163
+ let socket = TcpStream::connect((host, port))?;
164
+ socket.set_nodelay(true)?;
165
+ socket.set_nonblocking(true)?;
166
+ Ok(Self { stream, socket, next: 0, written: 0, answered: 0, blocked: false, open: true, buffer: Vec::new() })
167
+ }
168
+
169
+ fn finished(&self) -> bool {
170
+ !self.open || self.answered == self.stream.times.len()
171
+ }
172
+
173
+ fn fail(&mut self, out: &mut dyn FnMut(&str, u64, Value), error: &std::io::Error) {
174
+ out("error", monotonic_ns(), json!({ "k": self.stream.key, "errno": errno_name(error.raw_os_error().unwrap_or(0)) }));
175
+ self.open = false;
176
+ }
177
+
178
+ /// Writes each arrival that is due at `start`, until the socket takes no more.
179
+ fn send_due(&mut self, start: u64, out: &mut dyn FnMut(&str, u64, Value)) {
180
+ while self.open && !self.blocked && self.next < self.stream.times.len() && monotonic_ns() >= start + self.stream.times[self.next] {
181
+ let request = &self.stream.requests[self.next % self.stream.requests.len()];
182
+ let t = monotonic_ns();
183
+ match self.socket.write(&request[self.written..]) {
184
+ Ok(n) => {
185
+ self.written += n;
186
+ if self.written == request.len() {
187
+ out("sent", t, json!({ "k": self.stream.key, "i": self.next }));
188
+ self.next += 1;
189
+ self.written = 0;
190
+ }
191
+ }
192
+ Err(error) if error.kind() == ErrorKind::WouldBlock => self.blocked = true,
193
+ Err(error) if error.kind() == ErrorKind::Interrupted => {}
194
+ Err(error) => {
195
+ out("error", monotonic_ns(), json!({ "k": self.stream.key, "errno": errno_name(error.raw_os_error().unwrap_or(0)) }));
196
+ self.open = false;
197
+ }
198
+ }
199
+ }
200
+ }
201
+
202
+ /// Reads what the socket holds, and gives a replied line for each whole response.
203
+ fn receive(&mut self, buf: &mut [u8], out: &mut dyn FnMut(&str, u64, Value)) {
204
+ while self.open {
205
+ match self.socket.read(buf) {
206
+ Ok(0) => {
207
+ out("eof", monotonic_ns(), json!({ "k": self.stream.key }));
208
+ self.open = false;
209
+ }
210
+ Ok(n) => {
211
+ let t = monotonic_ns();
212
+ self.buffer.extend_from_slice(&buf[..n]);
213
+ self.take_responses(t, out);
214
+ }
215
+ Err(error) if error.kind() == ErrorKind::WouldBlock => return,
216
+ Err(error) if error.kind() == ErrorKind::Interrupted => {}
217
+ Err(error) => self.fail(out, &error),
218
+ }
219
+ }
220
+ }
221
+
222
+ fn take_responses(&mut self, t: u64, out: &mut dyn FnMut(&str, u64, Value)) {
223
+ loop {
224
+ match response_length(&self.buffer) {
225
+ Ok(None) => return,
226
+ Ok(Some((length, status))) => {
227
+ self.buffer.drain(..length);
228
+ out("replied", t, json!({ "k": self.stream.key, "i": self.answered, "status": status }));
229
+ self.answered += 1;
230
+ }
231
+ Err(why) => {
232
+ out("error", t, json!({ "k": self.stream.key, "why": why }));
233
+ self.open = false;
234
+ return;
235
+ }
236
+ }
237
+ }
238
+ }
239
+ }
240
+
241
+ /// Waits up to `ns` for the open connections. A wait in ms is too coarse for a schedule, thus this
242
+ /// one is ppoll(). It gives the revents of each connection, 0 for one that is not open.
243
+ fn wait_for(connections: &[Connection], ns: u64) -> std::io::Result<Vec<i16>> {
244
+ let mut polled: Vec<libc::pollfd> = connections
245
+ .iter()
246
+ .map(|connection| {
247
+ let writing = if connection.blocked { libc::POLLOUT } else { 0 };
248
+ // a negative fd is skipped by ppoll(), and its revents stay 0
249
+ let fd = if connection.open { connection.socket.as_raw_fd() } else { -1 };
250
+ libc::pollfd { fd, events: libc::POLLIN | libc::POLLRDHUP | writing, revents: 0 }
251
+ })
252
+ .collect();
253
+ let time = libc::timespec { tv_sec: (ns / 1_000_000_000) as libc::time_t, tv_nsec: (ns % 1_000_000_000) as libc::c_long };
254
+ // SAFETY: the pollfds and the timespec live on this frame, with their exact count, and no signal mask
255
+ match unsafe { libc::ppoll(polled.as_mut_ptr(), polled.len() as libc::nfds_t, &time, std::ptr::null()) } {
256
+ -1 if std::io::Error::last_os_error().kind() == ErrorKind::Interrupted => Ok(vec![0; connections.len()]),
257
+ -1 => Err(std::io::Error::last_os_error()),
258
+ _ => Ok(polled.iter().map(|entry| entry.revents).collect()),
259
+ }
260
+ }
261
+
262
+ /// Runs the schedule of each connection from `start`, until each connection is finished or the
263
+ /// deadline passes. A wait is at most `tick` ns.
264
+ fn drive(connections: &mut [Connection], start: u64, deadline: u64, tick: u64, out: &mut dyn FnMut(&str, u64, Value)) -> Result<(), Box<dyn Error>> {
265
+ let mut buf = vec![0u8; 65_536];
266
+ loop {
267
+ for connection in connections.iter_mut() {
268
+ connection.send_due(start, out);
269
+ }
270
+ if connections.iter().all(Connection::finished) {
271
+ let requests: usize = connections.iter().map(|connection| connection.next).sum();
272
+ out("done", monotonic_ns(), json!({ "requests": requests }));
273
+ return Ok(());
274
+ }
275
+ let now = monotonic_ns();
276
+ if now > deadline {
277
+ out("deadline", now, json!({}));
278
+ return Ok(());
279
+ }
280
+ let due = connections
281
+ .iter()
282
+ .filter(|connection| connection.open && !connection.blocked)
283
+ .filter_map(|connection| connection.stream.times.get(connection.next))
284
+ .map(|at| (start + at).saturating_sub(now))
285
+ .fold(tick, u64::min);
286
+ let revents = wait_for(connections, due)?;
287
+ for (connection, events) in connections.iter_mut().zip(revents) {
288
+ if events & libc::POLLOUT != 0 {
289
+ connection.blocked = false;
290
+ }
291
+ if events & !libc::POLLOUT != 0 {
292
+ connection.receive(&mut buf, out);
293
+ }
294
+ }
295
+ }
296
+ }
297
+
298
+ struct Args {
299
+ host: String,
300
+ port: u16,
301
+ plan: String,
302
+ go: String,
303
+ sample: Duration,
304
+ max: Duration,
305
+ }
306
+
307
+ fn args() -> Result<Args, Box<dyn Error>> {
308
+ let raw: Vec<String> = std::env::args().skip(1).collect();
309
+ let value = |flag: &str| raw.iter().position(|arg| arg == flag).and_then(|at| raw.get(at + 1)).cloned();
310
+ Ok(Args {
311
+ host: raw.first().ok_or("the target's host comes first")?.clone(),
312
+ port: raw.get(1).ok_or("then its port")?.parse()?,
313
+ plan: value("--plan").ok_or("--plan is required")?,
314
+ go: value("--go").unwrap_or_else(|| "/tmp/go".into()),
315
+ sample: Duration::from_micros((value("--sample-ms").map(|v| v.parse::<f64>()).transpose()?.unwrap_or(2.0) * 1000.0) as u64),
316
+ max: Duration::from_secs_f64(value("--max-s").map(|v| v.parse::<f64>()).transpose()?.unwrap_or(120.0)),
317
+ })
318
+ }
319
+
320
+ fn run() -> Result<(), Box<dyn Error>> {
321
+ let args = args()?;
322
+ let boot_id = std::fs::read_to_string("/proc/sys/kernel/random/boot_id")?.trim().to_string();
323
+ let streams = plan_of(&std::fs::read_to_string(&args.plan)?)?;
324
+ emit("start", json!({ "boot_id": boot_id, "mode": "load", "streams": streams.len() }));
325
+ let deadline = monotonic_ns() + args.max.as_nanos() as u64;
326
+ let mut connections = Vec::with_capacity(streams.len());
327
+ for stream in streams {
328
+ let key = stream.key.clone();
329
+ let connection = Connection::open(&args.host, args.port, stream)?;
330
+ // the harness finds the socket of a stream in the records of the kernel by this address
331
+ emit("connected", json!({ "k": key, "local": connection.socket.local_addr()?.to_string() }));
332
+ connections.push(connection);
333
+ }
334
+ emit("ready", json!({ "streams": connections.len() }));
335
+ while !Path::new(&args.go).exists() {
336
+ if monotonic_ns() > deadline {
337
+ emit("deadline", json!({}));
338
+ return Ok(());
339
+ }
340
+ std::thread::sleep(args.sample);
341
+ }
342
+ let start = monotonic_ns();
343
+ let arrivals: usize = connections.iter().map(|connection| connection.stream.times.len()).sum();
344
+ emit_at("scheduled", start, json!({ "streams": connections.len(), "arrivals": arrivals }));
345
+ drive(&mut connections, start, deadline, args.sample.as_nanos() as u64, &mut |event, t, fields| emit_at(event, t, fields))
346
+ }
347
+
348
+ fn main() {
349
+ if let Err(error) = run() {
350
+ emit("fail", json!({ "why": error.to_string() }));
351
+ std::process::exit(1);
352
+ }
353
+ }
354
+
355
+ #[cfg(test)]
356
+ mod tests {
357
+ use super::*;
358
+ use std::net::TcpListener;
359
+
360
+ const OK: &[u8] = b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok";
361
+
362
+ #[test]
363
+ fn a_response_is_whole_after_its_length_or_its_last_chunk() {
364
+ assert_eq!(response_length(OK), Ok(Some((40, 200))));
365
+ assert_eq!(response_length(&OK[..39]), Ok(None));
366
+ let empty = b"HTTP/1.1 204 No Content\r\nX: y\r\n\r\n";
367
+ assert_eq!(response_length(empty), Ok(Some((empty.len(), 204))));
368
+ let chunked = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nabc\r\n0\r\n\r\n";
369
+ assert_eq!(response_length(chunked), Ok(Some((chunked.len(), 200))));
370
+ for cut in 0..chunked.len() {
371
+ assert_eq!(response_length(&chunked[..cut]), Ok(None), "cut at {cut}");
372
+ }
373
+ let trailers = b"HTTP/1.1 500 Error\r\ntransfer-encoding: chunked\r\n\r\n2;x=y\r\nab\r\n0\r\nT: 1\r\n\r\n";
374
+ assert_eq!(response_length(trailers), Ok(Some((trailers.len(), 500))));
375
+ }
376
+
377
+ #[test]
378
+ fn a_response_that_the_driver_cannot_frame_is_an_error() {
379
+ assert!(response_length(b"HTTP/1.1 200 OK\r\n\r\nbody").is_err());
380
+ assert!(response_length(b"HTTP/1.1 100 Continue\r\n\r\n").is_err());
381
+ assert!(response_length(b"SPDY/3 200 OK\r\nContent-Length: 0\r\n\r\n").is_err());
382
+ assert!(response_length(b"HTTP/1.1 200 OK\r\nContent-Length: x\r\n\r\n").is_err());
383
+ assert!(response_length(b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nzz\r\n").is_err());
384
+ assert!(response_length(&vec![b'a'; HEAD_LIMIT + 1]).is_err());
385
+ }
386
+
387
+ #[test]
388
+ fn a_pipeline_of_responses_splits_at_each_end_in_reads_of_any_size() {
389
+ let parts: [&[u8]; 4] = [OK, b"HTTP/1.1 204 No Content\r\n\r\n", b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n1\r\na\r\n0\r\n\r\n", OK];
390
+ let bytes = parts.concat();
391
+ for size in 1..bytes.len() {
392
+ let (mut buffer, mut statuses) = (Vec::new(), Vec::new());
393
+ for read in bytes.chunks(size) {
394
+ buffer.extend_from_slice(read);
395
+ while let Some((length, status)) = response_length(&buffer).unwrap() {
396
+ buffer.drain(..length);
397
+ statuses.push(status);
398
+ }
399
+ }
400
+ assert_eq!(statuses, [200, 204, 200, 200], "reads of {size}");
401
+ assert!(buffer.is_empty());
402
+ }
403
+ }
404
+
405
+ #[test]
406
+ fn a_plan_gives_its_streams_and_refuses_a_bad_one() {
407
+ let schedule = r#"{"format":"rigor-schedule/1","times":[0,1.5]}"#;
408
+ let plan = format!(r#"{{"format":"systems-load/1","streams":[{{"key":"t0","schedule":{schedule},"requests":["A","B"]}}]}}"#);
409
+ let streams = plan_of(&plan).unwrap();
410
+ assert_eq!(streams.len(), 1);
411
+ assert_eq!(streams[0].key, "t0");
412
+ assert_eq!(streams[0].times, [0, 1_500_000]);
413
+ assert_eq!(streams[0].requests, [b"A".to_vec(), b"B".to_vec()]);
414
+ assert!(plan_of(r#"{"format":"other","streams":[]}"#).is_err());
415
+ assert!(plan_of(r#"{"format":"systems-load/1","streams":[]}"#).is_err());
416
+ let twice = format!(r#"{{"format":"systems-load/1","streams":[{{"key":"a","schedule":{schedule},"requests":["A"]}},{{"key":"a","schedule":{schedule},"requests":["A"]}}]}}"#);
417
+ assert!(plan_of(&twice).is_err());
418
+ let none = format!(r#"{{"format":"systems-load/1","streams":[{{"key":"a","schedule":{schedule},"requests":[]}}]}}"#);
419
+ assert!(plan_of(&none).is_err());
420
+ }
421
+
422
+ /// A server that answers each request of each connection with OK, after `delay` for the key `slow`.
423
+ fn serve(connections: usize, slow: Duration) -> u16 {
424
+ let listener = TcpListener::bind("127.0.0.1:0").unwrap();
425
+ let port = listener.local_addr().unwrap().port();
426
+ std::thread::spawn(move || {
427
+ for socket in listener.incoming().take(connections) {
428
+ let mut socket = socket.unwrap();
429
+ std::thread::spawn(move || {
430
+ let (mut buffer, mut buf) = (Vec::new(), [0u8; 4096]);
431
+ while let Ok(n) = socket.read(&mut buf) {
432
+ if n == 0 {
433
+ return;
434
+ }
435
+ buffer.extend_from_slice(&buf[..n]);
436
+ while let Some(end) = find(&buffer, b"\r\n\r\n") {
437
+ let request: Vec<u8> = buffer.drain(..end + 4).collect();
438
+ if find(&request, b"slow").is_some() {
439
+ std::thread::sleep(slow);
440
+ }
441
+ if socket.write_all(OK).is_err() {
442
+ return;
443
+ }
444
+ }
445
+ }
446
+ });
447
+ }
448
+ });
449
+ port
450
+ }
451
+
452
+ fn stream(key: &str, times: &[u64]) -> Stream {
453
+ let request = format!("GET /{key} HTTP/1.1\r\nHost: t\r\n\r\n");
454
+ Stream { key: key.into(), times: times.to_vec(), requests: vec![request.into_bytes()] }
455
+ }
456
+
457
+ #[test]
458
+ fn each_stream_gets_each_reply_and_a_slow_stream_delays_only_itself() {
459
+ let port = serve(2, Duration::from_millis(200));
460
+ let ms = 1_000_000;
461
+ let mut connections = vec![
462
+ Connection::open("127.0.0.1", port, stream("slow", &[0, ms, 2 * ms])).unwrap(),
463
+ Connection::open("127.0.0.1", port, stream("fast", &[0, ms, 2 * ms])).unwrap(),
464
+ ];
465
+ let mut events: Vec<(String, u64, Value)> = Vec::new();
466
+ let start = monotonic_ns();
467
+ let deadline = start + 10_000 * ms;
468
+ drive(&mut connections, start, deadline, ms, &mut |event, t, fields| events.push((event.into(), t, fields))).unwrap();
469
+ let replied = |key: &str| -> Vec<u64> {
470
+ events.iter().filter(|(event, _, fields)| event == "replied" && fields["k"] == key).map(|(_, t, _)| t - start).collect()
471
+ };
472
+ assert_eq!(replied("slow").len(), 3);
473
+ assert_eq!(replied("fast").len(), 3);
474
+ assert!(events.iter().filter(|(event, _, _)| event == "replied").all(|(_, _, fields)| fields["status"] == 200));
475
+ // the last reply of the fast stream comes before the first of the slow one
476
+ assert!(replied("fast")[2] < replied("slow")[0], "{events:?}");
477
+ let sent = events.iter().filter(|(event, _, _)| event == "sent").count();
478
+ assert_eq!(sent, 6);
479
+ assert_eq!(events.last().map(|(event, _, fields)| (event.as_str(), fields["requests"].clone())), Some(("done", json!(6))));
480
+ }
481
+ }