1#![forbid(unsafe_code)]
2
3#[cfg(feature = "stats")]
4pub mod summary;
5
6use std::{
7 fmt::Write as _,
8 net::SocketAddr,
9 time::{Duration, SystemTime, UNIX_EPOCH},
10};
11
12use clap::{Parser, ValueEnum};
13use irtt_client::{
14 ClientConfig, ClientEvent, NegotiatedParams, NegotiationPolicy, OneWayDelaySample, PacketMeta,
15 ReceivedStatsSample, RttSample, ServerTiming, SocketConfig, WarningKind, MAX_DSCP_CODEPOINT,
16 MAX_SERVER_FILL_BYTES, MAX_TTL, MAX_UDP_PAYLOAD_LENGTH,
17};
18use irtt_proto::{Clock, ReceivedStats, StampAt};
19
20const DEFAULT_RECV_TIMEOUT: Duration = Duration::from_millis(20);
21#[derive(Debug, Clone, Parser)]
22#[command(name = "irtt-rs", about = "Minimal IRTT-compatible client")]
23pub struct CliArgs {
24 pub server: String,
26
27 #[arg(long, default_value = "10s", value_parser = parse_test_duration)]
29 pub duration: Duration,
30
31 #[arg(long, default_value = "1s", value_parser = parse_duration)]
33 pub interval: Duration,
34
35 #[arg(long, default_value_t = 0, value_parser = parse_length)]
37 pub length: u32,
38
39 #[arg(long)]
41 pub hmac: Option<String>,
42
43 #[arg(long, value_enum, default_value_t = ClockArg::Both)]
45 pub clock: ClockArg,
46
47 #[arg(
49 long = "tstamp",
50 visible_alias = "timestamps",
51 value_enum,
52 value_name = "MODE",
53 default_value_t = TimestampArg::Both
54 )]
55 pub tstamp: TimestampArg,
56
57 #[arg(
59 long = "stats",
60 value_enum,
61 default_value_t = ReceivedStatsArg::Both,
62 help = "Server received-stats negotiation mode"
63 )]
64 pub stats: ReceivedStatsArg,
65
66 #[arg(
68 long = "sfill",
69 visible_alias = "server-fill",
70 value_name = "STRING",
71 value_parser = parse_server_fill
72 )]
73 pub server_fill: Option<String>,
74
75 #[arg(long, default_value_t = 0, value_name = "0..=63", value_parser = parse_dscp)]
77 pub dscp: u8,
78
79 #[arg(long, value_name = "1..=255", value_parser = parse_ttl)]
81 pub ttl: Option<u32>,
82
83 #[arg(long)]
85 pub loose: bool,
86
87 #[arg(long, value_enum, default_value_t = OutputMode::Human)]
89 pub output: OutputMode,
90
91 #[arg(long)]
93 pub verbose: bool,
94}
95
96impl CliArgs {
97 pub fn to_client_config(&self) -> ClientConfig {
98 ClientConfig {
99 server_addr: self.server.clone(),
100 duration: (!self.is_continuous()).then_some(self.duration),
101 interval: self.interval,
102 length: self.length,
103 received_stats: self.stats.into(),
104 stamp_at: self.timestamp_mode().into(),
105 clock: self.clock.into(),
106 dscp: self.dscp,
107 hmac_key: self.hmac.as_ref().map(|key| key.as_bytes().to_vec()),
108 server_fill: self.server_fill.clone(),
109 negotiation_policy: if self.loose {
110 NegotiationPolicy::Loose
111 } else {
112 NegotiationPolicy::Strict
113 },
114 socket_config: SocketConfig {
115 ttl: self.ttl,
116 recv_timeout: Some(DEFAULT_RECV_TIMEOUT),
117 ..SocketConfig::default()
118 },
119 ..ClientConfig::default()
120 }
121 }
122
123 pub fn is_continuous(&self) -> bool {
124 self.duration == Duration::ZERO
125 }
126
127 pub fn timestamp_mode(&self) -> TimestampArg {
128 self.tstamp
129 }
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
133pub enum ClockArg {
134 Wall,
135 Monotonic,
136 Both,
137}
138
139impl From<ClockArg> for Clock {
140 fn from(value: ClockArg) -> Self {
141 match value {
142 ClockArg::Wall => Self::Wall,
143 ClockArg::Monotonic => Self::Monotonic,
144 ClockArg::Both => Self::Both,
145 }
146 }
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
150pub enum TimestampArg {
151 None,
152 Send,
153 Receive,
154 Both,
155 Midpoint,
156}
157
158impl From<TimestampArg> for StampAt {
159 fn from(value: TimestampArg) -> Self {
160 match value {
161 TimestampArg::None => Self::None,
162 TimestampArg::Send => Self::Send,
163 TimestampArg::Receive => Self::Receive,
164 TimestampArg::Both => Self::Both,
165 TimestampArg::Midpoint => Self::Midpoint,
166 }
167 }
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
171pub enum ReceivedStatsArg {
172 None,
173 Count,
174 Window,
175 Both,
176}
177
178impl From<ReceivedStatsArg> for ReceivedStats {
179 fn from(value: ReceivedStatsArg) -> Self {
180 match value {
181 ReceivedStatsArg::None => Self::None,
182 ReceivedStatsArg::Count => Self::Count,
183 ReceivedStatsArg::Window => Self::Window,
184 ReceivedStatsArg::Both => Self::Both,
185 }
186 }
187}
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
190pub enum OutputMode {
191 Human,
193 Machine,
195 Simple,
197 RttUs,
199}
200
201impl OutputMode {
202 pub fn prints_summary(self) -> bool {
203 matches!(self, Self::Human)
204 }
205}
206
207pub fn parse_duration(input: &str) -> Result<Duration, String> {
208 let (number, unit) = split_duration(input)?;
209 let value: u64 = number
210 .parse()
211 .map_err(|_| format!("invalid duration value {input:?}"))?;
212 if value == 0 {
213 return Err("duration must be greater than zero".to_owned());
214 }
215 match unit {
216 "ms" => Ok(Duration::from_millis(value)),
217 "s" => Ok(Duration::from_secs(value)),
218 "m" => value
219 .checked_mul(60)
220 .map(Duration::from_secs)
221 .ok_or_else(|| "duration is too large".to_owned()),
222 _ => Err(format!(
223 "unsupported duration unit {unit:?}; use ms, s, or m"
224 )),
225 }
226}
227
228pub fn parse_test_duration(input: &str) -> Result<Duration, String> {
229 if input == "0" {
230 return Ok(Duration::ZERO);
231 }
232 let (number, unit) = split_duration(input)?;
233 let value: u64 = number
234 .parse()
235 .map_err(|_| format!("invalid duration value {input:?}"))?;
236 if value == 0 {
237 return Ok(Duration::ZERO);
238 }
239 match unit {
240 "ms" => Ok(Duration::from_millis(value)),
241 "s" => Ok(Duration::from_secs(value)),
242 "m" => value
243 .checked_mul(60)
244 .map(Duration::from_secs)
245 .ok_or_else(|| "duration is too large".to_owned()),
246 _ => Err(format!(
247 "unsupported duration unit {unit:?}; use ms, s, or m"
248 )),
249 }
250}
251
252fn split_duration(input: &str) -> Result<(&str, &str), String> {
253 let split = input
254 .find(|ch: char| !ch.is_ascii_digit())
255 .ok_or_else(|| "duration must include a unit: ms, s, or m".to_owned())?;
256 let (number, unit) = input.split_at(split);
257 if number.is_empty() || unit.is_empty() {
258 return Err("duration must include a positive value and unit".to_owned());
259 }
260 if number.starts_with('-') {
261 return Err("duration must be greater than zero".to_owned());
262 }
263 Ok((number, unit))
264}
265
266pub fn parse_length(input: &str) -> Result<u32, String> {
267 let length: u32 = input
268 .parse()
269 .map_err(|_| format!("invalid packet length {input:?}"))?;
270 if length > MAX_UDP_PAYLOAD_LENGTH {
271 return Err(format!("packet length must be <= {MAX_UDP_PAYLOAD_LENGTH}"));
272 }
273 Ok(length)
274}
275
276pub fn parse_server_fill(input: &str) -> Result<String, String> {
277 if input.is_empty() {
278 return Err("server fill must not be empty".to_owned());
279 }
280 let len = input.len();
281 if len > MAX_SERVER_FILL_BYTES {
282 return Err(format!(
283 "server fill must be <= {MAX_SERVER_FILL_BYTES} bytes, got {len}"
284 ));
285 }
286 Ok(input.to_owned())
287}
288
289pub fn parse_dscp(input: &str) -> Result<u8, String> {
290 let value: u8 = input
291 .parse()
292 .map_err(|_| format!("invalid DSCP codepoint {input:?}"))?;
293 if value > MAX_DSCP_CODEPOINT {
294 return Err(format!("DSCP codepoint must be <= {MAX_DSCP_CODEPOINT}"));
295 }
296 Ok(value)
297}
298
299pub fn parse_ttl(input: &str) -> Result<u32, String> {
300 let value: u32 = input
301 .parse()
302 .map_err(|_| format!("invalid TTL/hop limit {input:?}"))?;
303 if value == 0 || value > MAX_TTL {
304 return Err(format!("TTL/hop limit must be in range 1..={MAX_TTL}"));
305 }
306 Ok(value)
307}
308
309pub fn format_event(event: &ClientEvent, mode: OutputMode) -> Option<String> {
310 if matches!(event, ClientEvent::EchoSent { .. }) {
311 return None;
312 }
313 match mode {
314 OutputMode::RttUs => format_rtt_us(event),
315 OutputMode::Human => Some(format_human_event(event, None)),
316 OutputMode::Machine => Some(format_machine(event)),
317 OutputMode::Simple => Some(format_simple(event)),
318 }
319}
320
321#[derive(Debug, Clone, PartialEq, Eq, Default)]
322pub struct HumanEventStats {
323 pub contributed_sample: bool,
324 pub ipdv_pairs: Vec<HumanIpdvPair>,
325}
326
327#[derive(Debug, Clone, Copy, PartialEq, Eq)]
328pub struct HumanIpdvPair {
329 pub previous_logical_seq: u64,
330 pub current_logical_seq: u64,
331 pub rtt_ipdv: Duration,
332 pub send_ipdv: Option<Duration>,
333 pub receive_ipdv: Option<Duration>,
334}
335
336#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
337pub struct HumanOutputOptions {
338 pub verbose: bool,
339}
340
341#[cfg(feature = "stats")]
342impl From<irtt_stats::EventStatsUpdate> for HumanEventStats {
343 fn from(value: irtt_stats::EventStatsUpdate) -> Self {
344 Self {
345 contributed_sample: value.contributed_sample,
346 ipdv_pairs: value.ipdv_pairs.into_iter().map(Into::into).collect(),
347 }
348 }
349}
350
351#[cfg(feature = "stats")]
352impl From<irtt_stats::IpdvPairUpdate> for HumanIpdvPair {
353 fn from(value: irtt_stats::IpdvPairUpdate) -> Self {
354 Self {
355 previous_logical_seq: value.previous_logical_seq,
356 current_logical_seq: value.current_logical_seq,
357 rtt_ipdv: value.rtt_ipdv,
358 send_ipdv: value.send_ipdv,
359 receive_ipdv: value.receive_ipdv,
360 }
361 }
362}
363
364pub fn format_human_event(event: &ClientEvent, stats: Option<HumanEventStats>) -> String {
365 format_human_event_with_options(event, stats, HumanOutputOptions::default())
366}
367
368pub fn format_human_event_with_options(
369 event: &ClientEvent,
370 stats: Option<HumanEventStats>,
371 options: HumanOutputOptions,
372) -> String {
373 let stats = stats.as_ref();
374
375 match event {
376 ClientEvent::SessionStarted { remote, token, .. } => {
377 format!("session started remote={remote} token={token:#x}")
378 }
379 ClientEvent::NoTestCompleted { remote, .. } => {
380 format!("no-test completed remote={remote}")
381 }
382 ClientEvent::SessionClosed { remote, token, .. } => {
383 format!("session closed remote={remote} token={token:#x}")
384 }
385 ClientEvent::EchoSent { .. } => String::new(),
386 ClientEvent::EchoReply {
387 seq,
388 logical_seq,
389 rtt,
390 server_timing,
391 one_way,
392 received_stats,
393 ..
394 } => {
395 let mut out = format!("seq={seq}");
396 if options.verbose {
397 write!(out, " logical_seq={logical_seq}").unwrap();
398 }
399 write!(out, " rtt={}", format_duration(rtt.effective)).unwrap();
400 write_human_one_way(&mut out, *one_way);
401 write!(
402 out,
403 " ipdv={}",
404 format_human_ipdv(stats, Some(*logical_seq))
405 )
406 .unwrap();
407 if let Some(processing) = server_timing.and_then(|timing| timing.processing) {
408 write!(out, " proc={}", format_duration(processing)).unwrap();
409 }
410 if options.verbose {
411 write_human_received_stats(&mut out, *received_stats);
412 }
413 out
414 }
415 ClientEvent::EchoLoss {
416 seq, logical_seq, ..
417 } => {
418 format!("loss seq={seq} logical_seq={logical_seq}")
419 }
420 ClientEvent::DuplicateReply { seq, remote, .. } => {
421 format!("duplicate seq={seq} remote={remote}")
422 }
423 ClientEvent::LateReply {
424 seq,
425 logical_seq,
426 highest_seen,
427 remote,
428 rtt,
429 one_way,
430 received_stats,
431 ..
432 } => {
433 let mut out = format!(
434 "late seq={seq} logical_seq={} highest_seen={highest_seen} remote={remote}",
435 optional_u64(*logical_seq)
436 );
437 if let Some(rtt) = rtt {
438 write!(out, " rtt={}", format_duration(rtt.effective)).unwrap();
439 write_human_one_way(&mut out, *one_way);
440 write!(out, " ipdv={}", format_human_ipdv(stats, *logical_seq)).unwrap();
441 }
442 write_human_received_stats(&mut out, *received_stats);
443 out
444 }
445 ClientEvent::Warning { kind, message } => {
446 format!("warning kind={} message={message}", warning_kind(*kind))
447 }
448 }
449}
450
451fn format_rtt_us(event: &ClientEvent) -> Option<String> {
452 match event {
453 ClientEvent::EchoReply { rtt, .. } => Some(duration_us(rtt.effective).to_string()),
454 _ => None,
455 }
456}
457
458fn format_machine(event: &ClientEvent) -> String {
459 let mut out = String::new();
460 match event {
461 ClientEvent::SessionStarted {
462 remote,
463 token,
464 negotiated,
465 at,
466 } => {
467 write_common(&mut out, "session_started");
468 write_remote(&mut out, *remote);
469 write_token(&mut out, *token);
470 write_wall(&mut out, "event_wall_ns", at.wall);
471 write_negotiated(&mut out, negotiated);
472 }
473 ClientEvent::NoTestCompleted {
474 remote,
475 negotiated,
476 at,
477 } => {
478 write_common(&mut out, "no_test_completed");
479 write_remote(&mut out, *remote);
480 write_wall(&mut out, "event_wall_ns", at.wall);
481 write_negotiated(&mut out, negotiated);
482 }
483 ClientEvent::SessionClosed { remote, token, at } => {
484 write_common(&mut out, "session_closed");
485 write_remote(&mut out, *remote);
486 write_token(&mut out, *token);
487 write_wall(&mut out, "event_wall_ns", at.wall);
488 }
489 ClientEvent::EchoSent { .. } => {}
490 ClientEvent::EchoReply {
491 seq,
492 logical_seq,
493 remote,
494 sent_at,
495 received_at,
496 rtt,
497 server_timing,
498 one_way,
499 received_stats,
500 bytes: _,
501 packet_meta,
502 } => {
503 write_common(&mut out, "echo_reply");
504 write_seq(&mut out, *seq, Some(*logical_seq));
505 write_remote(&mut out, *remote);
506 write_wall(&mut out, "client_send_wall_ns", sent_at.wall);
507 write_wall(&mut out, "client_receive_wall_ns", received_at.wall);
508 write_rtt(&mut out, rtt);
509 write_server_timing(&mut out, *server_timing);
510 write_one_way(&mut out, *one_way);
511 write_received_stats(&mut out, *received_stats);
512 write_packet_meta(&mut out, *packet_meta);
513 }
514 ClientEvent::EchoLoss {
515 seq,
516 logical_seq,
517 sent_at,
518 ..
519 } => {
520 write_common(&mut out, "loss");
521 write_seq(&mut out, *seq, Some(*logical_seq));
522 write_wall(&mut out, "client_send_wall_ns", sent_at.wall);
523 out.push_str(" warning=loss");
524 }
525 ClientEvent::DuplicateReply {
526 seq,
527 remote,
528 received_at,
529 bytes: _,
530 } => {
531 write_common(&mut out, "duplicate");
532 write_seq(&mut out, *seq, None);
533 write_remote(&mut out, *remote);
534 write_wall(&mut out, "client_receive_wall_ns", received_at.wall);
535 out.push_str(" warning=duplicate");
536 }
537 ClientEvent::LateReply {
538 seq,
539 logical_seq,
540 highest_seen,
541 remote,
542 sent_at,
543 received_at,
544 rtt,
545 server_timing,
546 one_way,
547 received_stats,
548 bytes: _,
549 packet_meta,
550 } => {
551 write_common(&mut out, "late");
552 write_seq(&mut out, *seq, *logical_seq);
553 write_remote(&mut out, *remote);
554 write!(out, " highest_seen={highest_seen}").unwrap();
555 if let Some(sent_at) = sent_at {
556 write_wall(&mut out, "client_send_wall_ns", sent_at.wall);
557 }
558 write_wall(&mut out, "client_receive_wall_ns", received_at.wall);
559 if let Some(rtt) = rtt {
560 write_rtt(&mut out, rtt);
561 }
562 write_server_timing(&mut out, *server_timing);
563 write_one_way(&mut out, *one_way);
564 write_received_stats(&mut out, *received_stats);
565 write_packet_meta(&mut out, *packet_meta);
566 out.push_str(" warning=late");
567 }
568 ClientEvent::Warning { kind, message } => {
569 write_common(&mut out, "warning");
570 write!(
571 out,
572 " warning_kind={} message={}",
573 warning_kind(*kind),
574 escape_value(message)
575 )
576 .unwrap();
577 }
578 }
579 out
580}
581
582fn format_simple(event: &ClientEvent) -> String {
583 match event {
584 ClientEvent::SessionStarted { remote, token, .. } => {
585 format!("session started remote={remote} token={token:#x}")
586 }
587 ClientEvent::NoTestCompleted { remote, .. } => {
588 format!("no-test completed remote={remote}")
589 }
590 ClientEvent::SessionClosed { remote, token, .. } => {
591 format!("session closed remote={remote} token={token:#x}")
592 }
593 ClientEvent::EchoSent { .. } => String::new(),
594 ClientEvent::EchoReply {
595 seq,
596 logical_seq,
597 remote,
598 rtt,
599 server_timing,
600 bytes: _,
601 ..
602 } => {
603 let mut out = format!(
604 "reply seq={seq} logical_seq={logical_seq} remote={remote} rtt_us={}",
605 duration_us(rtt.effective)
606 );
607 if let Some(raw) = rtt.adjusted.map(|_| rtt.raw) {
608 write!(out, " raw_rtt_us={}", duration_us(raw)).unwrap();
609 }
610 if let Some(processing) = server_timing.and_then(|timing| timing.processing) {
611 write!(out, " server_processing_us={}", duration_us(processing)).unwrap();
612 }
613 out
614 }
615 ClientEvent::EchoLoss {
616 seq, logical_seq, ..
617 } => {
618 format!("loss seq={seq} logical_seq={logical_seq}")
619 }
620 ClientEvent::DuplicateReply { seq, remote, .. } => {
621 format!("duplicate seq={seq} remote={remote}")
622 }
623 ClientEvent::LateReply {
624 seq,
625 logical_seq,
626 highest_seen,
627 remote,
628 rtt,
629 ..
630 } => {
631 let mut out = format!(
632 "late seq={seq} logical_seq={} highest_seen={highest_seen} remote={remote}",
633 optional_u64(*logical_seq)
634 );
635 if let Some(rtt) = rtt {
636 write!(out, " rtt_us={}", duration_us(rtt.effective)).unwrap();
637 }
638 out
639 }
640 ClientEvent::Warning { kind, message } => {
641 format!("warning kind={} message={message}", warning_kind(*kind))
642 }
643 }
644}
645
646fn write_common(out: &mut String, event: &str) {
647 write!(out, "event={event}").unwrap();
648}
649
650fn write_seq(out: &mut String, seq: u32, logical_seq: Option<u64>) {
651 write!(out, " seq={seq}").unwrap();
652 if let Some(logical_seq) = logical_seq {
653 write!(out, " logical_seq={logical_seq}").unwrap();
654 }
655}
656
657fn write_remote(out: &mut String, remote: SocketAddr) {
658 write!(out, " remote={remote}").unwrap();
659}
660
661fn write_token(out: &mut String, token: u64) {
662 write!(out, " token={token:#x}").unwrap();
663}
664
665fn write_wall(out: &mut String, key: &str, wall: SystemTime) {
666 if let Ok(duration) = wall.duration_since(UNIX_EPOCH) {
667 write!(out, " {key}={}", duration.as_nanos()).unwrap();
668 }
669}
670
671fn write_negotiated(out: &mut String, negotiated: &NegotiatedParams) {
672 write!(
673 out,
674 " duration_ns={} interval_ns={} payload_length={}",
675 negotiated.params.duration_ns, negotiated.params.interval_ns, negotiated.params.length
676 )
677 .unwrap();
678}
679
680fn write_rtt(out: &mut String, rtt: &RttSample) {
681 write!(
682 out,
683 " raw_rtt_us={} effective_rtt_us={}",
684 duration_us(rtt.raw),
685 duration_us(rtt.effective)
686 )
687 .unwrap();
688 if let Some(adjusted) = rtt.adjusted {
689 write!(out, " adjusted_rtt_us={}", duration_us(adjusted)).unwrap();
690 }
691}
692
693fn write_server_timing(out: &mut String, timing: Option<ServerTiming>) {
694 if let Some(timing) = timing {
695 write_optional_i64(out, "server_receive_wall_ns", timing.receive_wall_ns);
696 write_optional_i64(out, "server_receive_mono_ns", timing.receive_mono_ns);
697 write_optional_i64(out, "server_send_wall_ns", timing.send_wall_ns);
698 write_optional_i64(out, "server_send_mono_ns", timing.send_mono_ns);
699 write_optional_i64(out, "server_midpoint_wall_ns", timing.midpoint_wall_ns);
700 write_optional_i64(out, "server_midpoint_mono_ns", timing.midpoint_mono_ns);
701 if let Some(processing) = timing.processing {
702 write!(out, " server_processing_us={}", duration_us(processing)).unwrap();
703 }
704 }
705}
706
707fn write_one_way(out: &mut String, one_way: Option<OneWayDelaySample>) {
708 if let Some(one_way) = one_way {
709 if let Some(value) = one_way.client_to_server {
710 write!(out, " client_to_server_us={}", duration_us(value)).unwrap();
711 }
712 if let Some(value) = one_way.server_to_client {
713 write!(out, " server_to_client_us={}", duration_us(value)).unwrap();
714 }
715 }
716}
717
718fn write_received_stats(out: &mut String, stats: Option<ReceivedStatsSample>) {
719 if let Some(stats) = stats {
720 if let Some(count) = stats.count {
721 write!(out, " server_received_count={count}").unwrap();
722 }
723 if let Some(window) = stats.window {
724 write!(out, " server_received_window={window:#x}").unwrap();
725 }
726 }
727}
728
729fn write_packet_meta(out: &mut String, meta: PacketMeta) {
730 write_optional_u8(out, "traffic_class", meta.traffic_class);
731 write_optional_u8(out, "dscp", meta.dscp);
732 write_optional_u8(out, "ecn", meta.ecn);
733 match meta.kernel_rx_timestamp {
734 Some(timestamp) => write_wall(out, "kernel_rx_ns", timestamp),
735 None => write!(out, " kernel_rx_ns=none").unwrap(),
736 }
737}
738
739fn write_human_one_way(out: &mut String, one_way: Option<OneWayDelaySample>) {
740 match one_way {
741 Some(one_way) => {
742 write!(
743 out,
744 " rd={} sd={}",
745 format_optional_duration(one_way.server_to_client),
746 format_optional_duration(one_way.client_to_server)
747 )
748 .unwrap();
749 }
750 None => out.push_str(" rd=n/a sd=n/a"),
751 }
752}
753
754fn write_human_received_stats(out: &mut String, stats: Option<ReceivedStatsSample>) {
755 if let Some(stats) = stats {
756 if let Some(count) = stats.count {
757 write!(out, " server_received={count}").unwrap();
758 }
759 if let Some(window) = stats.window {
760 write!(out, " server_window={window:#x}").unwrap();
761 }
762 }
763}
764
765fn format_human_ipdv(stats: Option<&HumanEventStats>, logical_seq: Option<u64>) -> String {
766 let Some(stats) = stats else {
767 return "n/a".to_owned();
768 };
769
770 let pair = logical_seq
771 .and_then(|seq| {
772 stats
773 .ipdv_pairs
774 .iter()
775 .find(|pair| pair.current_logical_seq == seq)
776 })
777 .or_else(|| stats.ipdv_pairs.last());
778
779 pair.map(|pair| format_duration(pair.rtt_ipdv))
780 .unwrap_or_else(|| "n/a".to_owned())
781}
782
783fn write_optional_u8(out: &mut String, key: &str, value: Option<u8>) {
784 match value {
785 Some(value) => write!(out, " {key}={value}").unwrap(),
786 None => write!(out, " {key}=none").unwrap(),
787 }
788}
789
790fn write_optional_i64(out: &mut String, key: &str, value: Option<i64>) {
791 if let Some(value) = value {
792 write!(out, " {key}={value}").unwrap();
793 }
794}
795
796fn duration_us(duration: Duration) -> u128 {
797 duration.as_micros()
798}
799
800fn format_optional_duration(duration: Option<Duration>) -> String {
801 duration
802 .map(format_duration)
803 .unwrap_or_else(|| "n/a".to_owned())
804}
805
806fn format_duration(duration: Duration) -> String {
807 format_ns(duration.as_nanos() as f64)
808}
809
810fn format_ns(ns: f64) -> String {
811 if ns < 1_000.0 {
812 format!("{ns:.0}ns")
813 } else if ns < 1_000_000.0 {
814 format!("{:.1}µs", ns / 1_000.0)
815 } else if ns < 1_000_000_000.0 {
816 format!("{:.1}ms", ns / 1_000_000.0)
817 } else {
818 format!("{:.3}s", ns / 1_000_000_000.0)
819 }
820}
821
822fn optional_u64(value: Option<u64>) -> String {
823 value
824 .map(|value| value.to_string())
825 .unwrap_or_else(|| "none".to_owned())
826}
827
828fn warning_kind(kind: WarningKind) -> &'static str {
829 match kind {
830 WarningKind::MalformedOrUnrelatedPacket => "malformed_or_unrelated_packet",
831 WarningKind::WrongToken => "wrong_token",
832 WarningKind::UntrackedReply => "untracked_reply",
833 }
834}
835
836fn escape_value(value: &str) -> String {
837 value
838 .replace('\\', "\\\\")
839 .replace(' ', "\\s")
840 .replace('\n', "\\n")
841 .replace('\r', "\\r")
842 .replace('\t', "\\t")
843}
844
845#[cfg(test)]
846mod tests {
847 use super::*;
848 use clap::{CommandFactory, Parser};
849 use irtt_client::{ClientTimestamp, PacketMeta, RttSample, SignedDuration};
850 use irtt_proto::{Params, PROTOCOL_VERSION};
851 use std::{
852 net::{IpAddr, Ipv4Addr, SocketAddr},
853 time::Instant,
854 };
855
856 fn test_timestamp(offset: Duration) -> ClientTimestamp {
857 ClientTimestamp {
858 wall: UNIX_EPOCH + offset,
859 mono: Instant::now() + offset,
860 }
861 }
862
863 fn test_remote() -> SocketAddr {
864 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 2112)
865 }
866
867 fn parse(args: &[&str]) -> Result<CliArgs, clap::Error> {
868 let mut argv = vec!["irtt-rs"];
869 argv.extend_from_slice(args);
870 CliArgs::try_parse_from(argv)
871 }
872
873 fn reply_event() -> ClientEvent {
874 reply_event_with_meta(PacketMeta::default())
875 }
876
877 fn reply_event_with_meta(packet_meta: PacketMeta) -> ClientEvent {
878 ClientEvent::EchoReply {
879 seq: 7,
880 logical_seq: 8,
881 remote: test_remote(),
882 sent_at: test_timestamp(Duration::from_secs(1)),
883 received_at: test_timestamp(Duration::from_secs(1) + Duration::from_micros(1500)),
884 rtt: RttSample {
885 raw: Duration::from_micros(1500),
886 adjusted: Some(Duration::from_micros(1200)),
887 effective: Duration::from_micros(1200),
888 adjusted_signed: Some(SignedDuration { ns: 1_200_000 }),
889 effective_signed: SignedDuration { ns: 1_200_000 },
890 },
891 server_timing: Some(ServerTiming {
892 receive_wall_ns: Some(1_000),
893 receive_mono_ns: Some(2_000),
894 send_wall_ns: Some(301_000),
895 send_mono_ns: Some(302_000),
896 midpoint_wall_ns: None,
897 midpoint_mono_ns: None,
898 processing: Some(Duration::from_micros(300)),
899 }),
900 one_way: Some(OneWayDelaySample {
901 client_to_server: Some(Duration::from_micros(400)),
902 server_to_client: Some(Duration::from_micros(500)),
903 }),
904 received_stats: Some(ReceivedStatsSample {
905 count: Some(9),
906 window: Some(0x7),
907 }),
908 bytes: 64,
909 packet_meta,
910 }
911 }
912
913 fn late_event_with_meta(packet_meta: PacketMeta) -> ClientEvent {
914 ClientEvent::LateReply {
915 seq: 4,
916 logical_seq: None,
917 highest_seen: 9,
918 remote: test_remote(),
919 sent_at: None,
920 received_at: test_timestamp(Duration::from_secs(1)),
921 rtt: None,
922 server_timing: None,
923 one_way: None,
924 received_stats: None,
925 bytes: 64,
926 packet_meta,
927 }
928 }
929
930 fn packet_meta(traffic_class: u8, dscp: u8, ecn: u8) -> PacketMeta {
931 PacketMeta {
932 traffic_class: Some(traffic_class),
933 dscp: Some(dscp),
934 ecn: Some(ecn),
935 kernel_rx_timestamp: None,
936 }
937 }
938
939 fn packet_meta_with_timestamp(timestamp: Option<SystemTime>) -> PacketMeta {
940 PacketMeta {
941 kernel_rx_timestamp: timestamp,
942 ..PacketMeta::default()
943 }
944 }
945
946 fn assert_machine_packet_meta(
947 line: &str,
948 traffic_class: &str,
949 dscp: &str,
950 ecn: &str,
951 kernel_rx_ns: &str,
952 ) {
953 assert!(line.contains(&format!("traffic_class={traffic_class}")));
954 assert!(line.contains(&format!("dscp={dscp}")));
955 assert!(line.contains(&format!("ecn={ecn}")));
956 assert!(line.contains(&format!("kernel_rx_ns={kernel_rx_ns}")));
957 }
958
959 #[test]
960 fn parses_valid_defaults() {
961 let args = parse(&["127.0.0.1:2112"]).unwrap();
962 assert_eq!(args.server, "127.0.0.1:2112");
963 assert_eq!(args.duration, Duration::from_secs(10));
964 assert_eq!(args.interval, Duration::from_secs(1));
965 assert_eq!(args.length, 0);
966 assert_eq!(args.hmac, None);
967 assert_eq!(args.clock, ClockArg::Both);
968 assert_eq!(args.tstamp, TimestampArg::Both);
969 assert_eq!(args.timestamp_mode(), TimestampArg::Both);
970 assert_eq!(args.stats, ReceivedStatsArg::Both);
971 assert_eq!(args.server_fill, None);
972 assert_eq!(args.dscp, 0);
973 assert_eq!(args.ttl, None);
974 assert!(!args.loose);
975 assert_eq!(args.output, OutputMode::Human);
976
977 let config = args.to_client_config();
978 assert_eq!(config.duration, Some(Duration::from_secs(10)));
979 assert_eq!(config.interval, Duration::from_secs(1));
980 assert_eq!(config.length, 0);
981 assert_eq!(config.received_stats, ReceivedStats::Both);
982 assert_eq!(config.stamp_at, StampAt::Both);
983 assert_eq!(config.clock, Clock::Both);
984 assert_eq!(config.dscp, 0);
985 assert_eq!(config.socket_config.ttl, None);
986 assert_eq!(config.server_fill, None);
987 assert_eq!(config.negotiation_policy, NegotiationPolicy::Strict);
988 assert!(!args.is_continuous());
989 }
990
991 #[test]
992 fn parses_durations() {
993 assert_eq!(parse_duration("1ms").unwrap(), Duration::from_millis(1));
994 assert_eq!(parse_duration("2s").unwrap(), Duration::from_secs(2));
995 assert_eq!(parse_duration("3m").unwrap(), Duration::from_secs(180));
996 assert_eq!(parse_test_duration("0").unwrap(), Duration::ZERO);
997 assert_eq!(parse_test_duration("0s").unwrap(), Duration::ZERO);
998 assert_eq!(
999 parse_test_duration("1ms").unwrap(),
1000 Duration::from_millis(1)
1001 );
1002 }
1003
1004 #[test]
1005 fn duration_zero_selects_continuous_config() {
1006 let args = parse(&["--duration", "0", "127.0.0.1:2112"]).unwrap();
1007 assert!(args.is_continuous());
1008 assert_eq!(args.duration, Duration::ZERO);
1009 assert_eq!(args.to_client_config().duration, None);
1010 }
1011
1012 #[test]
1013 fn parses_output_clock_and_timestamp_modes() {
1014 let args = parse(&[
1015 "--output",
1016 "machine",
1017 "--clock",
1018 "wall",
1019 "--tstamp",
1020 "send",
1021 "127.0.0.1:2112",
1022 ])
1023 .unwrap();
1024 assert_eq!(args.output, OutputMode::Machine);
1025 assert_eq!(args.clock, ClockArg::Wall);
1026 assert_eq!(args.timestamp_mode(), TimestampArg::Send);
1027 assert_eq!(args.to_client_config().stamp_at, StampAt::Send);
1028
1029 let args = parse(&[
1030 "--output",
1031 "rtt-us",
1032 "--clock",
1033 "monotonic",
1034 "--timestamps",
1035 "receive",
1036 "127.0.0.1:2112",
1037 ])
1038 .unwrap();
1039 assert_eq!(args.output, OutputMode::RttUs);
1040 assert_eq!(args.clock, ClockArg::Monotonic);
1041 assert_eq!(args.timestamp_mode(), TimestampArg::Receive);
1042 assert_eq!(args.to_client_config().stamp_at, StampAt::Receive);
1043
1044 let args = parse(&["--output", "human", "127.0.0.1:2112"]).unwrap();
1045 assert_eq!(args.output, OutputMode::Human);
1046 }
1047
1048 #[test]
1049 fn maps_tstamp_modes() {
1050 for (value, expected) in [
1051 ("none", StampAt::None),
1052 ("send", StampAt::Send),
1053 ("receive", StampAt::Receive),
1054 ("both", StampAt::Both),
1055 ("midpoint", StampAt::Midpoint),
1056 ] {
1057 let args = parse(&["--tstamp", value, "127.0.0.1:2112"]).unwrap();
1058 assert_eq!(args.to_client_config().stamp_at, expected);
1059 }
1060 }
1061
1062 #[test]
1063 fn timestamps_alias_accepts_midpoint() {
1064 let args = parse(&["--timestamps", "midpoint", "127.0.0.1:2112"]).unwrap();
1065 assert_eq!(args.timestamp_mode(), TimestampArg::Midpoint);
1066 assert_eq!(args.to_client_config().stamp_at, StampAt::Midpoint);
1067 }
1068
1069 #[test]
1070 fn rejects_duplicate_timestamp_options() {
1071 assert!(parse(&[
1072 "--tstamp",
1073 "send",
1074 "--timestamps",
1075 "receive",
1076 "127.0.0.1:2112"
1077 ])
1078 .is_err());
1079 }
1080
1081 #[test]
1082 fn maps_received_stats_modes() {
1083 for (value, expected) in [
1084 ("none", ReceivedStats::None),
1085 ("count", ReceivedStats::Count),
1086 ("window", ReceivedStats::Window),
1087 ("both", ReceivedStats::Both),
1088 ] {
1089 let args = parse(&["--stats", value, "127.0.0.1:2112"]).unwrap();
1090 assert_eq!(args.to_client_config().received_stats, expected);
1091 }
1092
1093 let args = parse(&["127.0.0.1:2112"]).unwrap();
1094 assert_eq!(args.to_client_config().received_stats, ReceivedStats::Both);
1095 }
1096
1097 #[test]
1098 fn maps_sfill_server_fill_options() {
1099 let args = parse(&["--sfill", "abc", "127.0.0.1:2112"]).unwrap();
1100 assert_eq!(args.to_client_config().server_fill.as_deref(), Some("abc"));
1101
1102 let args = parse(&["--server-fill", "abc", "127.0.0.1:2112"]).unwrap();
1103 assert_eq!(args.to_client_config().server_fill.as_deref(), Some("abc"));
1104
1105 let max = "0123456789abcdef0123456789abcdef";
1106 let args = parse(&["--sfill", max, "127.0.0.1:2112"]).unwrap();
1107 assert_eq!(args.to_client_config().server_fill.as_deref(), Some(max));
1108 }
1109
1110 #[test]
1111 fn maps_dscp_codepoints() {
1112 for value in ["0", "46", "63"] {
1113 let args = parse(&["--dscp", value, "127.0.0.1:2112"]).unwrap();
1114 assert_eq!(args.to_client_config().dscp, value.parse::<u8>().unwrap());
1115 }
1116 }
1117
1118 #[test]
1119 fn maps_ttl_values() {
1120 for value in ["1", "64", "255"] {
1121 let args = parse(&["--ttl", value, "127.0.0.1:2112"]).unwrap();
1122 assert_eq!(
1123 args.to_client_config().socket_config.ttl,
1124 Some(value.parse::<u32>().unwrap())
1125 );
1126 }
1127 }
1128
1129 #[test]
1130 fn maps_length_option() {
1131 let args = parse(&["--length", "1472", "127.0.0.1:2112"]).unwrap();
1132 assert_eq!(args.length, 1472);
1133 assert_eq!(args.to_client_config().length, 1472);
1134
1135 let args = parse(&["127.0.0.1:2112"]).unwrap();
1136 assert_eq!(args.length, 0);
1137 assert_eq!(args.to_client_config().length, 0);
1138 }
1139
1140 #[test]
1141 fn maps_loose_negotiation() {
1142 let args = parse(&["127.0.0.1:2112"]).unwrap();
1143 assert_eq!(
1144 args.to_client_config().negotiation_policy,
1145 NegotiationPolicy::Strict
1146 );
1147
1148 let args = parse(&["--loose", "127.0.0.1:2112"]).unwrap();
1149 assert_eq!(
1150 args.to_client_config().negotiation_policy,
1151 NegotiationPolicy::Loose
1152 );
1153 }
1154
1155 #[test]
1156 fn help_lists_advanced_protocol_options() {
1157 let help = CliArgs::command().render_help().to_string();
1158 assert!(help.contains("--tstamp <MODE>"));
1159 assert!(help.contains("--stats <STATS>"));
1160 assert!(help.contains("--sfill <STRING>"));
1161 assert!(help.contains("--dscp <0..=63>"));
1162 assert!(help.contains("--ttl <1..=255>"));
1163 assert!(help.contains("--loose"));
1164 assert!(help.contains("DSCP codepoint"));
1165 assert!(help.contains("up to 32 bytes"));
1166 assert!(help.contains("not negotiated"));
1167 }
1168
1169 #[test]
1170 fn rejects_invalid_argument_values() {
1171 for args in [
1172 &["--duration", "-1s", "127.0.0.1:2112"][..],
1173 &["--duration", "5", "127.0.0.1:2112"],
1174 &["--duration", "1h", "127.0.0.1:2112"],
1175 &["--interval", "0ms", "127.0.0.1:2112"],
1176 &["--interval", "-1ms", "127.0.0.1:2112"],
1177 &["--sfill", "", "127.0.0.1:2112"],
1178 &[
1179 "--sfill",
1180 "0123456789abcdef0123456789abcdefx",
1181 "127.0.0.1:2112",
1182 ],
1183 &["--dscp", "64", "127.0.0.1:2112"],
1184 &["--dscp", "-1", "127.0.0.1:2112"],
1185 &["--dscp", "abc", "127.0.0.1:2112"],
1186 &["--ttl", "0", "127.0.0.1:2112"],
1187 &["--ttl", "256", "127.0.0.1:2112"],
1188 &["--ttl", "-1", "127.0.0.1:2112"],
1189 &["--ttl", "abc", "127.0.0.1:2112"],
1190 &["--length", "-1", "127.0.0.1:2112"],
1191 &["--length", "65508", "127.0.0.1:2112"],
1192 ] {
1193 assert!(parse(args).is_err(), "expected parse failure for {args:?}");
1194 }
1195 }
1196
1197 #[test]
1198 fn rtt_us_prints_only_effective_reply_rtt() {
1199 assert_eq!(
1200 format_event(&reply_event(), OutputMode::RttUs),
1201 Some("1200".to_owned())
1202 );
1203 assert_eq!(
1204 format_event(
1205 &ClientEvent::Warning {
1206 kind: WarningKind::WrongToken,
1207 message: "bad".to_owned(),
1208 },
1209 OutputMode::RttUs
1210 ),
1211 None
1212 );
1213 }
1214
1215 #[test]
1216 fn machine_prints_stable_key_value_fields() {
1217 let line = format_event(&reply_event(), OutputMode::Machine).unwrap();
1218 assert!(line.starts_with("event=echo_reply "));
1219 assert!(line.contains("seq=7"));
1220 assert!(line.contains("logical_seq=8"));
1221 assert!(line.contains("remote=127.0.0.1:2112"));
1222 assert!(line.contains("client_send_wall_ns=1000000000"));
1223 assert!(line.contains("client_receive_wall_ns=1001500000"));
1224 assert!(!line.contains(" sent_ns="));
1225 assert!(!line.contains(" received_ns="));
1226 assert!(line.contains("raw_rtt_us=1500"));
1227 assert!(line.contains("adjusted_rtt_us=1200"));
1228 assert!(line.contains("effective_rtt_us=1200"));
1229 assert!(line.contains("server_processing_us=300"));
1230 assert!(line.contains("server_received_count=9"));
1231 }
1232
1233 #[test]
1234 fn machine_echo_reply_metadata_unavailable_prints_none() {
1235 let line = format_event(&reply_event(), OutputMode::Machine).unwrap();
1236
1237 assert_machine_packet_meta(&line, "none", "none", "none", "none");
1238 }
1239
1240 #[test]
1241 fn machine_echo_reply_metadata_observed_zero_prints_zero() {
1242 let line = format_event(
1243 &reply_event_with_meta(packet_meta(0, 0, 0)),
1244 OutputMode::Machine,
1245 )
1246 .unwrap();
1247
1248 assert_machine_packet_meta(&line, "0", "0", "0", "none");
1249 }
1250
1251 #[test]
1252 fn machine_echo_reply_metadata_dscp_46_ecn_0_prints_values() {
1253 let line = format_event(
1254 &reply_event_with_meta(packet_meta(184, 46, 0)),
1255 OutputMode::Machine,
1256 )
1257 .unwrap();
1258
1259 assert_machine_packet_meta(&line, "184", "46", "0", "none");
1260 }
1261
1262 #[test]
1263 fn machine_echo_reply_metadata_dscp_46_ecn_2_prints_values() {
1264 let line = format_event(
1265 &reply_event_with_meta(packet_meta(186, 46, 2)),
1266 OutputMode::Machine,
1267 )
1268 .unwrap();
1269
1270 assert_machine_packet_meta(&line, "186", "46", "2", "none");
1271 }
1272
1273 #[test]
1274 fn machine_echo_reply_metadata_kernel_rx_timestamp_prints_ns() {
1275 let line = format_event(
1276 &reply_event_with_meta(packet_meta_with_timestamp(Some(
1277 UNIX_EPOCH + Duration::new(1, 234),
1278 ))),
1279 OutputMode::Machine,
1280 )
1281 .unwrap();
1282
1283 assert_machine_packet_meta(&line, "none", "none", "none", "1000000234");
1284 }
1285
1286 #[test]
1287 fn machine_late_reply_metadata_unavailable_prints_none() {
1288 let line = format_event(
1289 &late_event_with_meta(PacketMeta::default()),
1290 OutputMode::Machine,
1291 )
1292 .unwrap();
1293
1294 assert!(line.starts_with("event=late "));
1295 assert_machine_packet_meta(&line, "none", "none", "none", "none");
1296 }
1297
1298 #[test]
1299 fn machine_late_reply_metadata_observed_values_prints_values() {
1300 let line = format_event(
1301 &late_event_with_meta(packet_meta(186, 46, 2)),
1302 OutputMode::Machine,
1303 )
1304 .unwrap();
1305
1306 assert!(line.starts_with("event=late "));
1307 assert_machine_packet_meta(&line, "186", "46", "2", "none");
1308 }
1309
1310 #[test]
1311 fn machine_late_reply_metadata_kernel_rx_timestamp_prints_ns() {
1312 let line = format_event(
1313 &late_event_with_meta(packet_meta_with_timestamp(Some(
1314 UNIX_EPOCH + Duration::new(2, 345),
1315 ))),
1316 OutputMode::Machine,
1317 )
1318 .unwrap();
1319
1320 assert!(line.starts_with("event=late "));
1321 assert_machine_packet_meta(&line, "none", "none", "none", "2000000345");
1322 }
1323
1324 #[test]
1325 fn simple_prints_readable_reply_line() {
1326 let line = format_event(&reply_event(), OutputMode::Simple).unwrap();
1327 assert_eq!(
1328 line,
1329 "reply seq=7 logical_seq=8 remote=127.0.0.1:2112 rtt_us=1200 raw_rtt_us=1500 server_processing_us=300"
1330 );
1331 }
1332
1333 #[test]
1334 fn human_uses_readable_per_event_lines() {
1335 let line = format_event(&reply_event(), OutputMode::Human).unwrap();
1336 assert!(line.starts_with("seq=7 rtt=1.2ms"));
1337 assert!(line.contains("rd=500.0µs"));
1338 assert!(line.contains("sd=400.0µs"));
1339 assert!(line.contains("ipdv=n/a"));
1340 assert!(line.contains("proc=300.0µs"));
1341 assert!(!line.contains("logical_seq="));
1342 assert!(!line.contains("server_received="));
1343 assert!(!line.contains("server_window="));
1344 assert!(!line.contains("rtt_us="));
1345 assert_ne!(
1346 line,
1347 format_event(&reply_event(), OutputMode::Simple).unwrap()
1348 );
1349 assert!(OutputMode::Human.prints_summary());
1350 assert!(!OutputMode::Simple.prints_summary());
1351 assert!(!OutputMode::Machine.prints_summary());
1352 assert!(!OutputMode::RttUs.prints_summary());
1353 }
1354
1355 #[test]
1356 fn verbose_human_reply_includes_extra_fields() {
1357 let line = format_human_event_with_options(
1358 &reply_event(),
1359 None,
1360 HumanOutputOptions { verbose: true },
1361 );
1362
1363 assert!(line.contains("logical_seq=8"));
1364 assert!(line.contains("server_received=9"));
1365 assert!(line.contains("server_window=0x7"));
1366 }
1367
1368 #[test]
1369 fn human_reply_uses_supplied_ipdv_update() {
1370 let line = format_human_event(
1371 &reply_event(),
1372 Some(HumanEventStats {
1373 contributed_sample: true,
1374 ipdv_pairs: vec![HumanIpdvPair {
1375 previous_logical_seq: 7,
1376 current_logical_seq: 8,
1377 rtt_ipdv: Duration::from_micros(47),
1378 send_ipdv: None,
1379 receive_ipdv: None,
1380 }],
1381 }),
1382 );
1383
1384 assert!(line.contains("ipdv=47.0µs"));
1385 }
1386
1387 #[test]
1388 fn human_reply_marks_missing_one_way_delay_unavailable() {
1389 let ClientEvent::EchoReply {
1390 seq,
1391 logical_seq,
1392 remote,
1393 sent_at,
1394 received_at,
1395 rtt,
1396 server_timing,
1397 received_stats,
1398 bytes,
1399 packet_meta,
1400 ..
1401 } = reply_event()
1402 else {
1403 unreachable!();
1404 };
1405 let event = ClientEvent::EchoReply {
1406 seq,
1407 logical_seq,
1408 remote,
1409 sent_at,
1410 received_at,
1411 rtt,
1412 server_timing,
1413 one_way: None,
1414 received_stats,
1415 bytes,
1416 packet_meta,
1417 };
1418
1419 let line = format_human_event(&event, None);
1420
1421 assert!(line.contains("rd=n/a"));
1422 assert!(line.contains("sd=n/a"));
1423 }
1424
1425 #[test]
1426 fn echo_sent_is_not_formatted_for_stream_outputs() {
1427 let ts = test_timestamp(Duration::from_secs(1));
1428 let event = ClientEvent::EchoSent {
1429 seq: 1,
1430 logical_seq: 2,
1431 remote: test_remote(),
1432 scheduled_at: ts.mono,
1433 sent_at: ts,
1434 bytes: 64,
1435 send_call: Duration::from_micros(10),
1436 timer_error: Duration::ZERO,
1437 };
1438
1439 assert!(format_event(&event, OutputMode::RttUs).is_none());
1440 assert!(format_event(&event, OutputMode::Human).is_none());
1441 assert!(format_event(&event, OutputMode::Machine).is_none());
1442 assert!(format_event(&event, OutputMode::Simple).is_none());
1443 }
1444
1445 #[test]
1446 fn warning_and_loss_variants_format_without_panicking() {
1447 let ts = test_timestamp(Duration::from_secs(1));
1448 let events = [
1449 ClientEvent::EchoLoss {
1450 seq: 1,
1451 logical_seq: 2,
1452 sent_at: ts,
1453 timeout_at: ts.mono + Duration::from_secs(1),
1454 },
1455 ClientEvent::DuplicateReply {
1456 seq: 3,
1457 remote: test_remote(),
1458 received_at: ts,
1459 bytes: 64,
1460 },
1461 ClientEvent::LateReply {
1462 seq: 4,
1463 logical_seq: None,
1464 highest_seen: 9,
1465 remote: test_remote(),
1466 sent_at: None,
1467 received_at: ts,
1468 rtt: None,
1469 server_timing: None,
1470 one_way: None,
1471 received_stats: None,
1472 bytes: 64,
1473 packet_meta: PacketMeta::default(),
1474 },
1475 ClientEvent::Warning {
1476 kind: WarningKind::UntrackedReply,
1477 message: "untracked reply".to_owned(),
1478 },
1479 ];
1480
1481 for event in events {
1482 let machine = format_event(&event, OutputMode::Machine).unwrap();
1483 let simple = format_event(&event, OutputMode::Simple).unwrap();
1484 assert!(!machine.is_empty());
1485 assert!(!simple.is_empty());
1486 }
1487 }
1488
1489 #[test]
1490 fn session_events_do_not_print_summary() {
1491 let event = ClientEvent::SessionStarted {
1492 remote: test_remote(),
1493 token: 0x1234,
1494 negotiated: NegotiatedParams {
1495 params: Params {
1496 protocol_version: PROTOCOL_VERSION,
1497 duration_ns: 10_000_000_000,
1498 interval_ns: 1_000_000_000,
1499 ..Params::default()
1500 },
1501 },
1502 at: test_timestamp(Duration::from_secs(1)),
1503 };
1504 let line = format_event(&event, OutputMode::Simple).unwrap();
1505 assert!(!line.contains("summary"));
1506 assert!(!line.contains("packets_sent"));
1507 assert!(!line.contains("packet_loss"));
1508
1509 let line = format_event(&event, OutputMode::Machine).unwrap();
1510 assert!(line.contains("event_wall_ns=1000000000"));
1511 assert!(line.contains("payload_length=0"));
1512 assert!(!line.contains(" at_ns="));
1513 assert!(!line.contains(" length=0"));
1514 }
1515}