#![allow(clippy::items_after_statements)]
#![allow(clippy::too_many_lines)]
use std::time::{Duration, Instant};
use super::*;
use super::congestion::{Controller, NewReno};
use super::frame::{Ack, Frame};
use super::recovery::{Recovery, RttEstimator, SentFrame, SentPacket};
use super::stream_id::Dir;
use super::streams::StreamRef;
use super::testfix::*;
use crate::constants::{
AEAD_TAG_LEN, DATA_HEADER_LEN, INITIAL_WINDOW, K_GRANULARITY, K_INITIAL_RTT, MAX_DATAGRAM,
MINIMUM_WINDOW, STREAMS_CREDIT_BATCH,
};
use crate::packet::ReferenceSuite;
type Suite = ReferenceSuite;
fn ms(n: u64) -> Duration {
Duration::from_millis(n)
}
fn us(n: u64) -> Duration {
Duration::from_micros(n)
}
const INITIAL_PTO: Duration = Duration::from_millis(1024);
fn tag_ref() -> StreamRef {
let mut c: Connection<Suite> = Connection::connecting([0x11u8; 32]);
c.open(Dir::Uni)
.expect("§16.9: open() before install is legal")
}
fn tag(r: StreamRef, counter: u64) -> SentFrame {
SentFrame::Stream {
r,
range: counter * 100..counter * 100 + 10,
fin: false,
}
}
fn sent(counter: u64, time_sent: Instant, size: u64, r: StreamRef) -> SentPacket {
SentPacket {
counter,
time_sent,
size,
app_limited: false,
path_gen: 0,
frames: vec![tag(r, counter)],
}
}
fn ack_of(counters: &[u64], ack_delay_us: u64) -> Ack {
let mut c: Vec<u64> = counters.to_vec();
c.sort_unstable();
c.dedup();
assert!(!c.is_empty(), "an ACK acknowledges at least one counter");
let mut runs: Vec<(u64, u64)> = Vec::new();
for &n in &c {
match runs.last_mut() {
Some((_, hi)) if *hi + 1 == n => *hi = n,
_ => runs.push((n, n)),
}
}
runs.reverse();
let (lo0, hi0) = runs[0];
let mut ranges = Vec::new();
let mut prev_lo = lo0;
for &(lo, hi) in &runs[1..] {
ranges.push((prev_lo - hi - 2, hi - lo));
prev_lo = lo;
}
Ack {
largest: hi0,
ack_delay: ack_delay_us,
first_range: hi0 - lo0,
ranges,
}
}
fn ack_bytes(ack: &Ack) -> Vec<u8> {
let mut out = Vec::new();
Frame::Ack(ack.clone()).encode(&mut out);
out
}
fn counter_of(dgram: &[u8]) -> u64 {
u64::from_le_bytes(
dgram[6..14]
.try_into()
.expect("§3.4: the data header is 14 bytes"),
)
}
fn counters_of(d: &Drained) -> Vec<u64> {
d.transmits().iter().map(|t| counter_of(&t.data)).collect()
}
fn total_bytes(d: &Drained) -> u64 {
d.transmits().iter().map(|t| t.data.len() as u64).sum()
}
mod rtt {
use super::*;
#[test]
fn before_any_sample_the_estimator_reads_the_initial_seed() {
let e = RttEstimator::new();
assert!(!e.has_sample(), "§14.4's precondition starts false");
assert_eq!(e.smoothed_rtt(), K_INITIAL_RTT, "§13.1: 333 ms");
assert_eq!(e.rttvar(), K_INITIAL_RTT / 2, "§13.1: 166.5 ms");
assert_eq!(
e.pto_interval(),
INITIAL_PTO,
"§13.3: 333 + max(4 · 166.5, 1) + 25 = 1024 ms"
);
}
#[test]
fn the_first_sample_replaces_the_seed_outright() {
let mut e = RttEstimator::new();
e.sample(ms(100), Duration::ZERO);
assert!(e.has_sample());
assert_eq!(e.smoothed_rtt(), ms(100), "§13.1: smoothed = latest");
assert_eq!(e.rttvar(), ms(50), "§13.1: rttvar = latest / 2");
}
#[test]
fn a_later_sample_follows_the_seven_eighths_recurrence_exactly() {
let mut e = RttEstimator::new();
e.sample(ms(100), Duration::ZERO);
e.sample(ms(200), Duration::ZERO);
assert_eq!(e.rttvar(), us(62_500), "¾ · 50 + ¼ · |100 − 200|");
assert_eq!(e.smoothed_rtt(), us(112_500), "⅞ · 100 + ⅛ · 200");
}
#[test]
fn the_peer_ack_delay_is_subtracted_when_the_result_stays_above_min_rtt() {
let mut e = RttEstimator::new();
e.sample(ms(100), Duration::ZERO);
e.sample(ms(200), ms(20));
assert_eq!(e.rttvar(), us(57_500));
assert_eq!(e.smoothed_rtt(), ms(110));
}
#[test]
fn the_peer_ack_delay_is_refused_when_it_would_push_the_sample_below_min_rtt() {
let mut e = RttEstimator::new();
e.sample(ms(100), Duration::ZERO);
e.sample(ms(110), ms(25));
assert_eq!(e.rttvar(), ms(40));
assert_eq!(e.smoothed_rtt(), us(101_250));
assert!(
e.smoothed_rtt() >= ms(100),
"§13.1: the estimate never falls below min_rtt through ack_delay"
);
}
#[test]
fn the_peer_ack_delay_is_capped_before_the_min_rtt_guard_is_applied() {
let mut e = RttEstimator::new();
e.sample(ms(100), Duration::ZERO);
e.sample(ms(200), ms(100));
assert_eq!(e.smoothed_rtt(), us(109_375), "the cap binds at 25 ms");
}
#[test]
fn the_loss_delay_takes_the_larger_of_smoothed_and_latest() {
let mut e = RttEstimator::new();
e.sample(ms(100), Duration::ZERO);
assert_eq!(
e.loss_delay(),
us(112_500),
"with smoothed == latest == 100, 9/8 · 100"
);
e.sample(ms(300), Duration::ZERO);
assert_eq!(e.smoothed_rtt(), ms(125), "⅞ · 100 + ⅛ · 300");
assert_eq!(e.loss_delay(), us(337_500), "9/8 · max(125, 300)");
}
#[test]
fn the_loss_delay_never_falls_below_the_granularity_floor() {
let mut e = RttEstimator::new();
e.sample(us(100), Duration::ZERO);
assert_eq!(e.loss_delay(), K_GRANULARITY, "§13.2's 1 ms floor");
}
#[test]
fn the_pto_interval_is_smoothed_plus_four_rttvar_plus_max_ack_delay() {
let mut e = RttEstimator::new();
e.sample(ms(100), Duration::ZERO);
assert_eq!(e.pto_interval(), ms(325), "100 + 200 + 25");
}
#[test]
fn reseeding_lets_min_rtt_rise() {
let mut e = RttEstimator::new();
e.sample(ms(100), Duration::ZERO);
e.reseed_min_rtt();
e.sample(ms(200), ms(25));
assert_eq!(
e.smoothed_rtt(),
us(112_500),
"§13.1: the re-seeded min_rtt is 200, so the delay is refused"
);
}
}
mod loss {
use super::*;
fn burst_of_four() -> (Recovery, StreamRef, Instant) {
let t = t0();
let r = tag_ref();
let mut rec = Recovery::new();
for (c, size) in [(0u64, 1000u64), (1, 1100), (2, 1200), (3, 1300)] {
rec.on_sent(sent(c, t, size, r));
}
(rec, r, t)
}
#[test]
fn the_packet_threshold_declares_exactly_three_counters_below_the_largest() {
let (mut rec, r, t) = burst_of_four();
let out = rec.on_ack(t + ms(100), &ack_of(&[3], 0), 3);
assert_eq!(out.acked, vec![tag(r, 3)], "one packet newly acknowledged");
assert_eq!(
out.lost,
vec![tag(r, 0)],
"§13.2: 3 below the largest acked, and nothing else"
);
assert_eq!(
rec.bytes_in_flight(),
1100 + 1200,
"counters 1 and 2 survive with their sizes intact"
);
assert_eq!(
rec.loss_deadline(),
Some(t + us(112_500)),
"§13.2: the survivors arm the timer at time_sent + loss_delay"
);
}
#[test]
fn two_counters_below_the_largest_is_not_yet_lost() {
let (mut rec, _r, t) = burst_of_four();
let out = rec.on_ack(t + ms(100), &ack_of(&[2], 0), 3);
assert!(
out.lost.is_empty(),
"§13.2: nothing is 3 or more below the largest acked"
);
assert!(
out.congestion.is_none(),
"§14.3: no loss, no congestion event"
);
assert_eq!(rec.bytes_in_flight(), 1000 + 1100 + 1300);
}
fn transfer_with_newer_packets_outstanding() -> (Recovery, StreamRef, Instant) {
let t = t0();
let r = tag_ref();
let mut rec = Recovery::new();
for c in 0u64..3 {
rec.on_sent(sent(c, t, 1000 + c * 100, r));
}
rec.on_sent(sent(3, t + ms(400), 1300, r));
rec.on_sent(sent(4, t + ms(401), 1400, r));
rec.on_sent(sent(5, t + ms(401), 1500, r));
(rec, r, t)
}
#[test]
fn packets_above_the_largest_acked_never_arm_the_loss_timer() {
let (mut rec, r, t) = transfer_with_newer_packets_outstanding();
let out = rec.on_ack(t + ms(500), &ack_of(&[1, 2, 3], 0), 5);
assert_eq!(
out.acked,
vec![tag(r, 1), tag(r, 2), tag(r, 3)],
"ascending counter order"
);
assert_eq!(out.lost, vec![tag(r, 0)]);
assert_eq!(
rec.bytes_in_flight(),
1400 + 1500,
"counters 4 and 5 are untouched — the walk does not judge them"
);
assert_eq!(
rec.loss_deadline(),
None,
"ruling 131: no survivor below largest_acked, so nothing to arm"
);
}
#[test]
fn the_loss_timeout_walk_never_judges_packets_above_the_largest_acked() {
let (mut rec, _r, t) = transfer_with_newer_packets_outstanding();
let _ = rec.on_ack(t + ms(500), &ack_of(&[1, 2, 3], 0), 5);
let out = rec.on_loss_timeout(t + ms(5_500));
assert!(
out.lost.is_empty(),
"ruling 131: entries above largest_acked are not judged at all"
);
assert!(out.congestion.is_none());
assert_eq!(rec.bytes_in_flight(), 1400 + 1500);
}
#[test]
fn the_loss_timer_arms_at_the_earliest_surviving_send_time() {
let t = t0();
let r = tag_ref();
let mut rec = Recovery::new();
rec.on_sent(sent(0, t, 1000, r));
rec.on_sent(sent(1, t + ms(90), 1100, r));
rec.on_sent(sent(2, t + ms(95), 1200, r));
rec.on_sent(sent(3, t + ms(100), 1300, r));
let out = rec.on_ack(t + ms(200), &ack_of(&[3], 0), 3);
assert_eq!(out.lost, vec![tag(r, 0)], "3 below the largest acked");
assert_eq!(
rec.loss_deadline(),
Some(t + ms(90) + us(112_500)),
"§13.2: the minimum over survivors 1 and 2, not the maximum"
);
}
fn at_the_time_threshold(younger_by: Duration) -> (Recovery, StreamRef, Instant) {
let t = t0();
let r = tag_ref();
let mut rec = Recovery::new();
rec.on_sent(sent(0, t + younger_by, 1000, r));
rec.on_sent(sent(1, t + us(12_500), 1100, r));
(rec, r, t)
}
#[test]
fn a_packet_exactly_loss_delay_old_is_lost() {
let (mut rec, r, t) = at_the_time_threshold(Duration::ZERO);
let out = rec.on_ack(t + us(112_500), &ack_of(&[1], 0), 1);
assert_eq!(
out.lost,
vec![tag(r, 0)],
"ruling 141: at exactly `loss_delay` the packet is lost, or the \
`Loss` timer fires on nothing"
);
assert_eq!(rec.bytes_in_flight(), 0);
}
#[test]
fn a_packet_one_nanosecond_younger_than_loss_delay_is_not_yet_lost() {
let (mut rec, _r, t) = at_the_time_threshold(Duration::from_nanos(1));
let out = rec.on_ack(t + us(112_500), &ack_of(&[1], 0), 1);
assert!(
out.lost.is_empty(),
"ruling 141: `>=`, so one tick under the threshold is not lost"
);
assert_eq!(rec.bytes_in_flight(), 1000, "counter 0 survives");
}
#[test]
fn the_loss_deadline_is_recomputed_rather_than_accumulated() {
let (mut rec, r, t) = burst_of_four();
let _ = rec.on_ack(t + ms(100), &ack_of(&[3], 0), 3);
assert!(
rec.loss_deadline().is_some(),
"precondition: counters 1 and 2 armed it"
);
let out = rec.on_ack(t + ms(120), &ack_of(&[1, 2, 3], 0), 3);
assert_eq!(out.acked, vec![tag(r, 1), tag(r, 2)], "1 and 2 are new");
assert_eq!(
rec.loss_deadline(),
None,
"§13.2: nothing below largest_acked survives, so nothing is armed"
);
assert!(rec.is_empty(), "the map is empty");
assert_eq!(rec.bytes_in_flight(), 0);
}
#[test]
fn the_loss_timeout_declares_the_survivors_the_ack_walk_left() {
let (mut rec, r, t) = burst_of_four();
let _ = rec.on_ack(t + ms(100), &ack_of(&[3], 0), 3);
let deadline = rec.loss_deadline().expect("armed by counters 1 and 2");
let out = rec.on_loss_timeout(deadline + Duration::from_nanos(1));
assert_eq!(out.lost, vec![tag(r, 1), tag(r, 2)], "ascending order");
assert_eq!(rec.bytes_in_flight(), 0);
assert_eq!(rec.loss_deadline(), None, "nothing left to arm");
let ev = out.congestion.expect("§14.3: a loss episode");
assert_eq!(ev.lost_bytes, 1100 + 1200, "both packets, summed");
assert_eq!(ev.sent_time, t, "the earliest lost packet's send time");
assert!(!ev.is_persistent);
}
#[test]
fn the_loss_timer_firing_at_its_own_deadline_declares_the_packet() {
let (mut rec, _r, t) = burst_of_four();
let _ = rec.on_ack(t + ms(100), &ack_of(&[3], 0), 3);
let deadline = rec.loss_deadline().expect("armed");
let out = rec.on_loss_timeout(deadline);
assert!(
!out.lost.is_empty(),
"ruling 141: a `Loss` firing that declares nothing re-arms at its \
own deadline and spins the driver"
);
assert_eq!(
rec.loss_deadline(),
None,
"and nothing is left to arm for: a deadline surviving its own \
firing is the livelock in its other form"
);
}
#[test]
fn an_ack_that_acknowledges_nothing_new_is_a_total_no_op() {
let (mut rec, _r, t) = burst_of_four();
let first = rec.on_ack(t + ms(100), &ack_of(&[3], 0), 3);
assert_eq!(first.acked.len(), 1, "precondition");
let flight = rec.bytes_in_flight();
let out = rec.on_ack(t + ms(150), &ack_of(&[3], 0), 3);
assert!(out.acked.is_empty());
assert!(out.lost.is_empty());
assert!(out.ack_events.is_empty(), "§14: nothing to grow the window");
assert!(out.congestion.is_none());
assert_eq!(rec.bytes_in_flight(), flight, "the map did not move");
}
#[test]
fn an_ack_above_the_highest_sealed_counter_is_ignored_whole() {
let (mut rec, _r, t) = burst_of_four();
let out = rec.on_ack(t + ms(100), &ack_of(&[9], 0), 3);
assert!(out.acked.is_empty());
assert!(out.lost.is_empty());
assert!(out.congestion.is_none());
assert_eq!(
rec.bytes_in_flight(),
1000 + 1100 + 1200 + 1300,
"§12.5: the whole frame is ignored, including its largest"
);
assert_eq!(rec.loss_deadline(), None);
}
}
mod sampling {
use super::*;
#[test]
fn an_ack_whose_largest_was_already_acknowledged_yields_no_sample() {
let t = t0();
let r = tag_ref();
let mut rec = Recovery::new();
for c in 0u64..3 {
rec.on_sent(sent(c, t, 1200, r));
}
let _ = rec.on_ack(t + ms(100), &ack_of(&[2], 0), 2);
assert_eq!(
rec.rtt().smoothed_rtt(),
ms(100),
"precondition: the first sample"
);
let out = rec.on_ack(t + ms(500), &ack_of(&[1, 2], 0), 2);
assert_eq!(
out.acked,
vec![tag(r, 1)],
"counter 1 *is* newly acknowledged — the frame is not ignored"
);
assert_eq!(
rec.rtt().smoothed_rtt(),
ms(100),
"ruling 138: largest was already acknowledged, so no sample"
);
}
#[test]
fn the_sample_measures_from_the_largest_newly_acknowledged_packet() {
let t = t0();
let r = tag_ref();
let mut rec = Recovery::new();
rec.on_sent(sent(0, t, 1200, r));
rec.on_sent(sent(1, t + ms(50), 1200, r));
let _ = rec.on_ack(t + ms(150), &ack_of(&[0, 1], 0), 1);
assert_eq!(
rec.rtt().smoothed_rtt(),
ms(100),
"§13.1 with RFC 9002 §5.1: measured from counter 1"
);
}
}
mod pto {
use super::*;
#[test]
fn the_first_pto_deadline_is_the_send_plus_one_thousand_and_twenty_four_ms() {
let t = t0();
let r = tag_ref();
let mut rec = Recovery::new();
rec.on_sent(sent(0, t, 1200, r));
assert_eq!(rec.pto_deadline(), Some(t + INITIAL_PTO));
}
#[test]
fn the_pto_is_disarmed_when_the_sent_map_empties() {
let t = t0();
let r = tag_ref();
let mut rec = Recovery::new();
assert_eq!(rec.pto_deadline(), None, "nothing sent, nothing armed");
rec.on_sent(sent(0, t, 1200, r));
assert_eq!(rec.pto_deadline(), Some(t + INITIAL_PTO), "armed");
let _ = rec.on_ack(t + ms(100), &ack_of(&[0], 0), 0);
assert!(rec.is_empty(), "precondition: the map drained");
assert_eq!(
rec.pto_deadline(),
None,
"§13.3: disarmed when the map empties"
);
}
#[test]
fn the_pto_anchors_at_the_last_ack_eliciting_send() {
let t = t0();
let r = tag_ref();
let mut rec = Recovery::new();
rec.on_sent(sent(0, t, 1200, r));
rec.on_sent(sent(1, t + ms(200), 1200, r));
assert_eq!(rec.pto_deadline(), Some(t + ms(200) + INITIAL_PTO));
}
#[test]
fn each_pto_firing_doubles_the_interval() {
let t = t0();
let r = tag_ref();
let mut rec = Recovery::new();
rec.on_sent(sent(0, t, 1200, r));
rec.on_pto_timeout();
assert_eq!(rec.pto_deadline(), Some(t + INITIAL_PTO * 2), "2×");
rec.on_pto_timeout();
assert_eq!(rec.pto_deadline(), Some(t + INITIAL_PTO * 4), "4×");
rec.on_pto_timeout();
assert_eq!(rec.pto_deadline(), Some(t + INITIAL_PTO * 8), "8× — 2³");
rec.on_pto_timeout();
assert_eq!(
rec.pto_deadline(),
Some(t + INITIAL_PTO * 8),
"§13.3: the fourth firing is pinned, not 16×"
);
}
#[test]
fn the_pto_backoff_multiplier_caps_at_eight() {
let t = t0();
let r = tag_ref();
let mut rec = Recovery::new();
rec.on_sent(sent(0, t, 1200, r));
for _ in 0..3 {
rec.on_pto_timeout();
}
assert_eq!(rec.pto_deadline(), Some(t + INITIAL_PTO * 8), "2³");
rec.on_pto_timeout();
assert_eq!(
rec.pto_deadline(),
Some(t + INITIAL_PTO * 8),
"§13.3: the cap holds at the fourth firing"
);
for _ in 0..60 {
rec.on_pto_timeout();
}
assert_eq!(
rec.pto_deadline(),
Some(t + INITIAL_PTO * 8),
"64 firings: pinned at 8×, and specifically not the 256× that \
`1u32 << pto_count` gives — legal, silent, and 32× too slow"
);
}
#[test]
fn pto_count_resets_when_any_packet_is_newly_acknowledged() {
let t = t0();
let r = tag_ref();
let mut rec = Recovery::new();
rec.on_sent(sent(0, t, 1200, r));
rec.on_sent(sent(1, t, 1200, r));
for _ in 0..3 {
rec.on_pto_timeout();
}
assert_eq!(
rec.pto_deadline(),
Some(t + INITIAL_PTO * 8),
"precondition: backed off"
);
let _ = rec.on_ack(t + ms(100), &ack_of(&[1], 0), 1);
assert_eq!(
rec.pto_deadline(),
Some(t + ms(325)),
"§13.3: pto_count back to 0, on the freshly sampled base"
);
}
}
mod persistent_congestion {
use super::*;
fn sampled() -> (Recovery, StreamRef, Instant) {
let t = t0();
let r = tag_ref();
let mut rec = Recovery::new();
rec.on_sent(sent(0, t, 1200, r));
let _ = rec.on_ack(t + ms(100), &ack_of(&[0], 0), 0);
(rec, r, t + ms(100))
}
#[test]
fn two_far_apart_losses_with_nothing_acked_between_are_persistent() {
let (mut rec, r, t1) = sampled();
rec.on_sent(sent(1, t1, 1100, r));
rec.on_sent(sent(2, t1 + ms(1000), 1200, r));
rec.on_sent(sent(3, t1 + ms(2000), 1300, r));
let out = rec.on_ack(t1 + ms(2100), &ack_of(&[3], 0), 3);
assert_eq!(out.lost, vec![tag(r, 1), tag(r, 2)]);
let ev = out.congestion.expect("§14.3: a loss episode");
assert!(
ev.is_persistent,
"§14.4: 1000 ms apart, nothing acked between, and a prior sample exists"
);
assert_eq!(ev.sent_time, t1, "the earliest lost packet's send time");
assert_eq!(ev.lost_bytes, 1100 + 1200);
}
#[test]
fn a_run_broken_by_an_acknowledged_packet_is_not_persistent() {
let (mut rec, r, t1) = sampled();
rec.on_sent(sent(1, t1, 1100, r));
rec.on_sent(sent(2, t1 + ms(500), 1200, r));
rec.on_sent(sent(3, t1 + ms(1000), 1300, r));
rec.on_sent(sent(4, t1 + ms(2000), 1400, r));
let out = rec.on_ack(t1 + ms(2100), &ack_of(&[2, 4], 0), 4);
assert_eq!(out.lost, vec![tag(r, 1), tag(r, 3)]);
assert_eq!(out.acked, vec![tag(r, 2), tag(r, 4)]);
let ev = out.congestion.expect("§14.3: still a loss episode");
assert!(
!ev.is_persistent,
"§14.4: counter 2 was acknowledged between the two losses"
);
assert_eq!(ev.lost_bytes, 1100 + 1300);
}
#[test]
fn the_persistent_period_ignores_the_pto_backoff() {
let (mut rec, r, t1) = sampled();
rec.on_sent(sent(1, t1, 1100, r));
rec.on_sent(sent(2, t1 + ms(1000), 1200, r));
rec.on_sent(sent(3, t1 + ms(2000), 1300, r));
for _ in 0..3 {
rec.on_pto_timeout();
}
let out = rec.on_ack(t1 + ms(2100), &ack_of(&[3], 0), 3);
let ev = out.congestion.expect("§14.3: a loss episode");
assert!(
ev.is_persistent,
"§14.4: the period is a property of the path, not of the probe count"
);
}
#[test]
fn a_burst_of_close_together_losses_is_one_ordinary_episode() {
let t = t0();
let r = tag_ref();
let mut rec = Recovery::new();
rec.on_sent(sent(0, t, 1000, r));
rec.on_sent(sent(1, t + ms(10), 1100, r));
rec.on_sent(sent(2, t + ms(20), 1200, r));
rec.on_sent(sent(3, t + ms(500), 1300, r));
let out = rec.on_ack(t + ms(600), &ack_of(&[3], 0), 3);
assert_eq!(out.lost, vec![tag(r, 0), tag(r, 1), tag(r, 2)]);
let ev = out.congestion.expect("§14.3: one event for the episode");
assert!(
!ev.is_persistent,
"20 ms apart is not persistent congestion"
);
assert_eq!(
ev.lost_bytes,
1000 + 1100 + 1200,
"§14.3: once per episode, over the whole lost set"
);
assert_eq!(ev.sent_time, t, "the earliest, not the latest");
}
}
mod newreno {
use super::*;
#[test]
fn a_new_controller_starts_at_the_initial_window_in_slow_start() {
let c = NewReno::new();
assert_eq!(c.window(), INITIAL_WINDOW, "§14.2: 12 000 B");
assert_eq!(c.ssthresh(), u64::MAX, "§14.2: ssthresh starts at u64::MAX");
assert_eq!(c.recovery_start(), None, "§14.3: nothing is fenced yet");
}
#[test]
fn slow_start_grows_by_exactly_the_bytes_acknowledged() {
let t = t0();
let mut c = NewReno::new();
for (i, bytes) in [500u64, 700, 1200].into_iter().enumerate() {
let at = t + ms(i as u64 + 1);
c.on_sent(at, bytes);
c.on_ack(at + ms(50), at, bytes, false);
}
assert_eq!(
c.window(),
INITIAL_WINDOW + 2400,
"12 000 + 500 + 700 + 1200"
);
}
fn in_congestion_avoidance(t: Instant) -> NewReno {
let mut c = NewReno::new();
c.on_congestion_event(t, t, false, 1200);
assert_eq!(c.window(), 6_000, "precondition");
assert_eq!(c.ssthresh(), 6_000, "precondition");
c
}
#[test]
fn congestion_avoidance_adds_one_datagram_per_crossing_and_carries_the_remainder() {
let t = t0();
let mut c = in_congestion_avoidance(t);
let sent_at = t + ms(1);
c.on_ack(t + ms(10), sent_at, 5_000, false);
assert_eq!(c.window(), 6_000, "accumulator 5 000, no crossing");
c.on_ack(t + ms(20), sent_at, 5_000, false);
assert_eq!(c.window(), 7_200, "crossed once; 4 000 carried");
c.on_ack(t + ms(30), sent_at, 3_200, false);
assert_eq!(c.window(), 8_400, "the carried 4 000 is what crosses here");
c.on_ack(t + ms(40), sent_at, 20_000, false);
assert_eq!(c.window(), 10_800, "one ack, two crossings");
}
#[test]
fn a_congestion_event_halves_the_window_and_sets_ssthresh_to_it() {
let t = t0();
let mut c = NewReno::new();
c.on_congestion_event(t + ms(100), t, false, 1200);
assert_eq!(c.window(), 6_000);
assert_eq!(c.ssthresh(), 6_000, "§14.3: ssthresh = the new cwnd");
assert_eq!(
c.recovery_start(),
Some(t + ms(100)),
"§14.3: the period starts at the event, not at the send"
);
}
#[test]
fn a_second_event_for_a_packet_sent_before_the_recovery_period_does_not_cut_again() {
let t = t0();
let mut c = NewReno::new();
c.on_congestion_event(t + ms(100), t, false, 1200);
c.on_congestion_event(t + ms(110), t - ms(10), false, 1200);
assert_eq!(c.window(), 6_000, "sent strictly before the marker");
c.on_congestion_event(t + ms(120), t + ms(100), false, 1200);
assert_eq!(
c.window(),
6_000,
"§14.3's test is `sent_time <= recovery_start`: the equal case is fenced"
);
}
#[test]
fn a_new_episode_after_the_recovery_period_cuts_again() {
let t = t0();
let mut c = NewReno::new();
c.on_congestion_event(t + ms(100), t, false, 1200);
c.on_congestion_event(t + ms(300), t + ms(200), false, 1200);
assert_eq!(c.window(), 3_000, "§14.3: a fresh episode, a fresh cut");
assert_eq!(c.ssthresh(), 3_000);
assert_eq!(c.recovery_start(), Some(t + ms(300)));
}
#[test]
fn the_window_never_falls_below_the_minimum() {
let t = t0();
let mut c = NewReno::new();
let mut at = t;
for expected in [6_000u64, 3_000, MINIMUM_WINDOW, MINIMUM_WINDOW] {
c.on_congestion_event(at + ms(10), at + ms(5), false, 1200);
assert_eq!(c.window(), expected);
at += ms(20);
}
}
#[test]
fn acknowledgements_of_packets_sent_before_the_recovery_period_do_not_grow_it() {
let t = t0();
let mut c = in_congestion_avoidance(t);
c.on_ack(t + ms(10), t - ms(50), 6_000, false);
assert_eq!(c.window(), 6_000, "sent strictly before the marker");
c.on_ack(t + ms(20), t, 6_000, false);
assert_eq!(
c.window(),
6_000,
"sent exactly at the marker: still fenced"
);
c.on_ack(t + ms(30), t + Duration::from_nanos(1), 6_000, false);
assert_eq!(
c.window(),
7_200,
"one nanosecond after the marker, the ack grows the window"
);
}
#[test]
fn an_app_limited_acknowledgement_does_not_grow_the_window() {
let t = t0();
let mut c = NewReno::new();
c.on_ack(t + ms(10), t, 1200, true);
assert_eq!(c.window(), INITIAL_WINDOW, "§14.5: app-limited, no growth");
c.on_ack(t + ms(20), t, 1200, false);
assert_eq!(c.window(), INITIAL_WINDOW + 1200, "not app-limited, growth");
}
#[test]
fn a_persistent_congestion_event_collapses_the_window_to_the_minimum() {
let t = t0();
let mut c = NewReno::new();
c.on_congestion_event(t + ms(100), t, true, 2400);
assert_eq!(c.window(), MINIMUM_WINDOW, "§14.4: collapsed, not halved");
assert_eq!(c.ssthresh(), 6_000, "§14.3's cut still sets ssthresh");
}
#[test]
fn persistent_congestion_does_not_clear_the_recovery_period() {
let t = t0();
let mut c = NewReno::new();
c.on_congestion_event(t + ms(100), t, true, 2400);
assert_eq!(
c.recovery_start(),
Some(t + ms(100)),
"ruling 139: the marker is not cleared"
);
c.on_ack(t + ms(110), t + ms(50), 6_000, false);
assert_eq!(
c.window(),
MINIMUM_WINDOW,
"the pre-collapse flight is still fenced from growing it"
);
}
}
mod admission {
use super::*;
fn solo_with_uni(t: Instant) -> (Solo, StreamRef) {
let mut s = Solo::installed_at(t);
let r = s.conn.open(Dir::Uni).expect("§9.1: our own uni space");
(s, r)
}
#[test]
fn the_initial_window_admits_exactly_ten_full_datagrams() {
let t = t0();
let (mut s, r) = solo_with_uni(t);
write_all(&mut s.conn, t, r, &ramp(0, 24_000));
let d = drain(&mut s.conn);
let ts = d.transmits();
assert_eq!(ts.len(), 10, "§14.5: 10 × 1200 fills 12 000 exactly");
for x in &ts {
assert_eq!(x.data.len(), MAX_DATAGRAM, "§8.6: the packets are full");
}
assert_eq!(total_bytes(&d), INITIAL_WINDOW);
assert_eq!(
s.conn.bytes_in_flight(),
INITIAL_WINDOW,
"ruling 136: the datagram, not the plaintext"
);
assert_eq!(s.conn.congestion_window(), INITIAL_WINDOW);
}
#[test]
fn a_tracked_packets_size_is_the_whole_datagram() {
let t = t0();
let (mut s, r) = solo_with_uni(t);
write_all(&mut s.conn, t, r, &ramp(0, 500));
let d = drain(&mut s.conn);
let ts = d.transmits();
assert_eq!(ts.len(), 1, "500 B is one packet");
assert_eq!(
s.conn.bytes_in_flight(),
ts[0].data.len() as u64,
"ruling 136: DATA_HEADER_LEN + ciphertext + AEAD_TAG_LEN"
);
assert!(
s.conn.bytes_in_flight() >= 500 + (DATA_HEADER_LEN + AEAD_TAG_LEN) as u64,
"the header and tag are counted, not just the payload"
);
}
#[test]
fn acknowledging_the_flight_returns_bytes_in_flight_to_zero() {
let t = t0();
let (mut s, r) = solo_with_uni(t);
write_all(&mut s.conn, t, r, &ramp(0, 500));
let d = drain(&mut s.conn);
let cs = counters_of(&d);
assert_eq!(cs.len(), 1, "precondition");
assert!(s.conn.bytes_in_flight() > 0, "precondition");
let _ = s.deliver(t + ms(50), &ack_bytes(&ack_of(&cs, 0)));
assert_eq!(s.conn.bytes_in_flight(), 0, "the flight is empty");
assert_eq!(
s.conn.smoothed_rtt(),
ms(50),
"§13.1: the first sample, measured through the core"
);
}
#[test]
fn the_gate_holds_the_backlog_until_the_flight_is_acknowledged() {
let t = t0();
let (mut s, r) = solo_with_uni(t);
write_all(&mut s.conn, t, r, &ramp(0, 24_000));
let d = drain(&mut s.conn);
assert_eq!(d.transmits().len(), 10, "precondition: the window is full");
s.conn.handle_timeout(t + ms(1));
let idle = drain(&mut s.conn);
assert!(
idle.transmits().is_empty(),
"§14.5: no room, so nothing more is sealed"
);
let after = s.deliver(t + ms(50), &ack_bytes(&ack_of(&counters_of(&d), 0)));
assert!(!after.transmits().is_empty(), "the window reopened");
assert!(
s.conn.bytes_in_flight() <= s.conn.congestion_window(),
"§14.5's gate still holds after the reopen"
);
}
#[test]
fn a_pto_probe_is_sent_with_the_window_full_and_is_counted_in_flight() {
let t = t0();
let (mut s, r) = solo_with_uni(t);
write_all(&mut s.conn, t, r, &ramp(0, 24_000));
let d = drain(&mut s.conn);
assert_eq!(d.transmits().len(), 10, "precondition");
let full = s.conn.bytes_in_flight();
assert_eq!(full, INITIAL_WINDOW, "precondition: not one byte of room");
assert_eq!(
d.deadline,
Some(t + INITIAL_PTO),
"§13.3 through the core: the PTO is armed at the send"
);
s.conn.handle_timeout(t + INITIAL_PTO);
let probe = drain(&mut s.conn);
let ts = probe.transmits();
assert_eq!(ts.len(), 1, "§13.4: one ack-eliciting packet per firing");
assert_eq!(
s.conn.bytes_in_flight(),
full + ts[0].data.len() as u64,
"ruling 43: exempt from admission, never from accounting"
);
}
#[test]
fn a_packet_that_emptied_the_queue_earns_no_window_growth() {
let t = t0();
let (mut s, r) = solo_with_uni(t);
write_all(&mut s.conn, t, r, &ramp(0, 500));
let d = drain(&mut s.conn);
assert_eq!(d.transmits().len(), 1, "precondition");
let _ = s.deliver(t + ms(50), &ack_bytes(&ack_of(&counters_of(&d), 0)));
assert_eq!(
s.conn.congestion_window(),
INITIAL_WINDOW,
"§14.5: the sender ran out of data, not of window"
);
}
#[test]
fn a_window_limited_flight_grows_the_window_by_every_byte_it_carried() {
let t = t0();
let (mut s, r) = solo_with_uni(t);
write_all(&mut s.conn, t, r, &ramp(0, 24_000));
let d = drain(&mut s.conn);
assert_eq!(d.transmits().len(), 10, "precondition");
let _ = s.deliver(t + ms(50), &ack_bytes(&ack_of(&counters_of(&d), 0)));
assert_eq!(
s.conn.congestion_window(),
INITIAL_WINDOW * 2,
"§14.2 slow start: cwnd grows by the 12 000 B acknowledged"
);
}
#[test]
fn a_pure_ack_packet_is_never_tracked_in_flight() {
let t = t0();
let mut s = Solo::installed_at(t);
let d = s.deliver_stream_bytes(t, Solo::peer_uni(0), 0, 2048, false);
assert!(
!d.transmits().is_empty(),
"§12.4: the second ack-eliciting packet owes an ACK now"
);
assert_eq!(
s.conn.bytes_in_flight(),
0,
"§13.5: only ack-eliciting packets enter the map"
);
}
}
mod fin {
use super::*;
fn finished_stream(t: Instant) -> (Solo, StreamRef) {
let mut s = Solo::installed_at(t);
let r = s.conn.open(Dir::Uni).expect("§9.1: our own uni space");
write_all(&mut s.conn, t, r, &ramp(0, 500));
s.conn
.finish(t, r)
.expect("§9.5: finish on a live send half");
let _ = drain(&mut s.conn);
(s, r)
}
fn finished_events(d: &Drained, r: StreamRef) -> usize {
d.count_events(|e| matches!(e, ConnEvent::StreamFinished { r: q } if *q == r))
}
#[test]
fn a_range_ending_at_the_final_size_without_the_fin_does_not_finish_the_stream() {
let t = t0();
let (mut s, r) = finished_stream(t);
s.conn.on_ack_range(t + ms(10), r, 0..500, false);
let d = drain(&mut s.conn);
assert_eq!(
finished_events(&d, r),
0,
"ruling 113: the FIN is carried, and this frame did not carry it"
);
assert!(
s.conn.stream_id(r).is_some(),
"§9.7: not fully closed, so the entry survives"
);
}
#[test]
fn the_range_that_carried_the_fin_finishes_the_stream() {
let t = t0();
let (mut s, r) = finished_stream(t);
s.conn.on_ack_range(t + ms(10), r, 0..500, true);
let d = drain(&mut s.conn);
assert_eq!(finished_events(&d, r), 1, "§9.7: DataRecvd, once");
assert_eq!(
s.conn.stream_id(r),
None,
"§9.2: fully closed, freed, and the watermark advanced"
);
}
#[test]
fn a_lost_range_without_the_fin_does_not_resend_the_fin() {
let t = t0();
let (mut s, r) = finished_stream(t);
s.conn.on_lost_range(t + ms(10), r, 0..500, false);
let d = drain(&mut s.conn);
let frames = s.drain_frames(&d);
assert!(
frames
.iter()
.any(|f| matches!(f, Wire::Stream { fin: false, .. })),
"§8.7 `ranges`: the lost range returns to the pending set"
);
assert!(
!frames
.iter()
.any(|f| matches!(f, Wire::Stream { fin: true, .. })),
"ruling 113: this frame did not carry the FIN, so nothing re-sends it"
);
}
#[test]
fn a_lost_range_that_carried_the_fin_resends_it() {
let t = t0();
let (mut s, r) = finished_stream(t);
s.conn.on_lost_range(t + ms(10), r, 0..500, true);
let d = drain(&mut s.conn);
let frames = s.drain_frames(&d);
assert!(
frames
.iter()
.any(|f| matches!(f, Wire::Stream { fin: true, .. })),
"§8.7: the FIN rides the retransmission of the range that carried it"
);
}
}
mod watermark {
use super::*;
#[test]
fn acknowledging_our_own_streams_closure_grants_the_peer_nothing() {
let t = t0();
let mut s = Solo::installed_at(t);
let mut refs = Vec::new();
for _ in 0..STREAMS_CREDIT_BATCH {
let r = s.conn.open(Dir::Uni).expect("§9.1: our own uni space");
write_all(&mut s.conn, t, r, &ramp(0, 100));
s.conn.finish(t, r).expect("§9.5");
refs.push(r);
}
let _ = drain(&mut s.conn);
let mut finished = 0usize;
for (i, r) in refs.iter().enumerate() {
s.conn.on_ack_range(t + ms(10 + i as u64), *r, 0..100, true);
let d = drain(&mut s.conn);
finished += d.count_events(|e| matches!(e, ConnEvent::StreamFinished { .. }));
let frames = s.drain_frames(&d);
assert!(
!frames.iter().any(|f| matches!(f, Wire::MaxStreamsUni(_))),
"§10.4: our own stream's closure grants the peer nothing"
);
}
assert_eq!(
finished, STREAMS_CREDIT_BATCH as usize,
"all eight fully closed — otherwise the assertion above is vacuous"
);
}
#[test]
fn retiring_peer_opened_streams_does_grant_the_peer_credit() {
let t = t0();
let mut s = Solo::installed_at(t);
for i in 0..STREAMS_CREDIT_BATCH {
let f = stream_frame(Solo::peer_uni(i), 0, &ramp(0, 10), true);
let _ = s.deliver(t, &f);
}
let claimed = accept_all(&mut s.conn, Dir::Uni);
assert_eq!(
claimed.len(),
STREAMS_CREDIT_BATCH as usize,
"precondition: eight peer-opened streams"
);
for (_, r) in &claimed {
abandon_recv(&mut s.conn, t + ms(1), *r);
}
let d = drain(&mut s.conn);
let frames = s.drain_frames(&d);
assert!(
frames.iter().any(|f| matches!(f, Wire::MaxStreamsUni(_))),
"§10.4: a peer-opened stream's closure does earn the peer credit"
);
}
#[test]
fn a_stream_frame_naming_a_local_index_we_have_closed_is_a_no_op() {
let t = t0();
let mut s = Solo::installed_at(t);
let r = s.conn.open(Dir::Bi).expect("§9.1: our own bidi space");
write_all(&mut s.conn, t, r, &ramp(0, 100));
s.conn.finish(t, r).expect("§9.5");
let _ = drain(&mut s.conn);
s.conn.on_ack_range(t + ms(10), r, 0..100, true);
abandon_recv(&mut s.conn, t + ms(11), r);
let _ = drain(&mut s.conn);
assert_eq!(
s.conn.stream_id(r),
None,
"precondition: both halves gone, so the index is fully closed"
);
let id = raw_id(0, Dir::Bi, false);
let d = s.deliver(t + ms(20), &stream_frame(id, 0, &ramp(0, 8), false));
assert_alive(&d);
assert_eq!(s.conn.accept(Dir::Bi), None, "§8.4: ACKed, never re-opened");
}
}
mod path_generation {
use super::*;
#[test]
fn a_sent_packet_carries_a_u32_path_generation_held_at_zero() {
let p = sent(0, t0(), 1200, tag_ref());
let generation: u32 = p.path_gen;
assert_eq!(
generation, 0,
"ruling 137: held at 0 until slice 7 moves it"
);
}
}