s2n-quic-core 0.16.0

Internal crate used by s2n-quic
Documentation
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
use super::*;
use crate::{
    event,
    packet::number::PacketNumberSpace,
    path,
    recovery::congestion_controller::PathPublisher,
    time::{Clock, NoopClock},
};
use core::time::Duration;

#[macro_export]
macro_rules! assert_delta {
    ($x:expr, $y:expr, $d:expr) => {
        assert!(
            ($x - $y).abs() < $d,
            "assertion failed: `({:?} - {:?}).abs() < {:?})`",
            $x,
            $y,
            $d
        );
    };
}

fn bytes_to_packets(bytes: f32, max_datagram_size: u16) -> f32 {
    bytes / max_datagram_size as f32
}

#[test]
//= https://www.rfc-editor.org/rfc/rfc8312#section-4.1
//= type=test
fn w_cubic() {
    let max_datagram_size = 1200;
    let mut cubic = Cubic::new(max_datagram_size);

    // 2_764_800 is used because it can be divided by 1200 and then have a cubic
    // root result in an integer value.
    cubic.multiplicative_decrease(2_764_800.0);
    assert_delta!(
        cubic.w_max,
        bytes_to_packets(2_764_800.0, max_datagram_size),
        0.001
    );

    let mut t = Duration::from_secs(0);

    // W_cubic(0)=W_max*beta_cubic
    assert_delta!(cubic.w_max * BETA_CUBIC, cubic.w_cubic(t), 0.001);

    // K = cubic_root(W_max*(1-beta_cubic)/C)
    // K = cubic_root(2304 * 0.75) = 12
    assert_eq!(cubic.k, Duration::from_secs(12));

    //= https://www.rfc-editor.org/rfc/rfc8312#section-5.1
    //= type=test
    //# Therefore, C SHOULD be set to 0.4.

    // W_cubic(t) = C*(t-K)^3 + W_max
    // W_cubic(t) = .4*(t-12)^3 + 2304
    // W_cubic(15) = .4*27 + 2304 = 2314.8
    t = Duration::from_secs(15);
    assert_delta!(cubic.w_cubic(t), 2314.8, 0.001);

    // W_cubic(10) = .4*-8 + 2304 = 2300.8
    t = Duration::from_secs(10);
    assert_delta!(cubic.w_cubic(t), 2300.8, 0.001);
}

#[test]
//= https://www.rfc-editor.org/rfc/rfc8312#section-4.6
//= type=test
fn w_est() {
    let max_datagram_size = 1200;
    let mut cubic = Cubic::new(max_datagram_size);
    cubic.w_max = 100.0;
    let t = Duration::from_secs(6);
    let rtt = Duration::from_millis(300);

    // W_est(t) = W_max*beta_cubic + [3*(1-beta_cubic)/(1+beta_cubic)] * (t/RTT)
    // W_est(6) = 100*.7 + [3*(1-.7)/(1+.7)] * (6/.3)
    // W_est(6) = 70 + 0.5294117647 * 20 = 80.588235294

    assert_delta!(cubic.w_est(t, rtt), 80.5882, 0.001);
}

#[allow(clippy::float_cmp)]
#[test]
//= https://www.rfc-editor.org/rfc/rfc8312#section-4.5
//= type=test
fn multiplicative_decrease() {
    let max_datagram_size = 1200.0;
    let mut cubic = Cubic::new(max_datagram_size as u16);
    cubic.w_max = bytes_to_packets(10000.0, max_datagram_size as u16);

    assert_eq!(
        cubic.multiplicative_decrease(100_000.0),
        (100_000.0 * BETA_CUBIC)
    );
    // Window max was not less than the last max, so not fast convergence
    assert_delta!(cubic.w_last_max, cubic.w_max, 0.001);
    assert_delta!(cubic.w_max, 100_000.0 / max_datagram_size, 0.001);

    assert_eq!(
        cubic.multiplicative_decrease(80000.0),
        (80000.0 * BETA_CUBIC)
    );
    //= https://www.rfc-editor.org/rfc/rfc8312#section-4.6
    //= type=test
    //# To speed up this bandwidth release by
    //# existing flows, the following mechanism called "fast convergence"
    //# SHOULD be implemented.
    // Window max was less than the last max, so fast convergence applies
    assert_delta!(cubic.w_last_max, 80000.0 / max_datagram_size, 0.001);
    // W_max = W_max*(1.0+beta_cubic)/2.0 = W_max * .85
    assert_delta!(cubic.w_max, 80000.0 * 0.85 / max_datagram_size, 0.001);

    //= https://www.rfc-editor.org/rfc/rfc8312#section-4.5
    //= type=test
    //# Parameter beta_cubic SHOULD be set to 0.7.
    assert_eq!(0.7, BETA_CUBIC);
}

#[test]
//= https://www.rfc-editor.org/rfc/rfc9002#section-7.8
//= type=test
fn is_congestion_limited() {
    let max_datagram_size = 1000;
    let mut cc = CubicCongestionController::new(max_datagram_size);
    cc.congestion_window = 1000.0;
    cc.bytes_in_flight = BytesInFlight::new(100);

    assert!(cc.is_congestion_limited());

    cc.congestion_window = 1100.0;

    assert!(!cc.is_congestion_limited());

    cc.bytes_in_flight = BytesInFlight::new(2000);

    assert!(cc.is_congestion_limited());
}

#[test]
fn is_congestion_window_under_utilized() {
    let max_datagram_size = 1200;
    let mut cc = CubicCongestionController::new(max_datagram_size);
    cc.congestion_window = 12000.0;

    // In Slow Start, the window is under utilized if it is less than half full
    cc.bytes_in_flight = BytesInFlight::new(5999);
    cc.state = SlowStart;
    assert!(cc.is_congestion_window_under_utilized());

    cc.bytes_in_flight = BytesInFlight::new(6000);
    assert!(!cc.is_congestion_window_under_utilized());

    cc.state = State::congestion_avoidance(NoopClock.get_time());
    assert!(cc.is_congestion_window_under_utilized());

    // In Congestion Avoidance, the window is under utilized if there are more than
    // 3 * MTU bytes available in the congestion window (12000 - 3 * 1200 = 8400)
    cc.bytes_in_flight = BytesInFlight::new(8399);
    assert!(cc.is_congestion_window_under_utilized());

    cc.bytes_in_flight = BytesInFlight::new(8400);
    assert!(!cc.is_congestion_window_under_utilized());
}

//= https://www.rfc-editor.org/rfc/rfc9002#section-7.2
//= type=test
//# Endpoints SHOULD use an initial congestion
//# window of ten times the maximum datagram size (max_datagram_size),
//# while limiting the window to the larger of 14,720 bytes or twice the
//# maximum datagram size.
#[test]
fn initial_window() {
    let mut max_datagram_size = 1200;
    assert_eq!(
        (max_datagram_size * 10) as u32,
        CubicCongestionController::initial_window(max_datagram_size)
    );

    max_datagram_size = 2000;
    assert_eq!(
        14720,
        CubicCongestionController::initial_window(max_datagram_size)
    );

    max_datagram_size = 8000;
    assert_eq!(
        (max_datagram_size * 2) as u32,
        CubicCongestionController::initial_window(max_datagram_size)
    );
}

//= https://www.rfc-editor.org/rfc/rfc9002#section-7.2
//= type=test
//# The RECOMMENDED
//# value is 2 * max_datagram_size.
#[test]
fn minimum_window_equals_two_times_max_datagram_size() {
    let max_datagram_size = 1200;
    let cc = CubicCongestionController::new(max_datagram_size);

    assert_delta!(
        (2 * max_datagram_size) as f32,
        cc.cubic.minimum_window(),
        0.001
    );
}

#[test]
fn on_packet_sent() {
    let mut cc = CubicCongestionController::new(1000);
    let mut publisher = event::testing::Publisher::snapshot();
    let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
    let mut rtt_estimator = RttEstimator::default();
    let now = NoopClock.get_time();

    cc.congestion_window = 100_000.0;

    // Last sent packet time updated to t10
    cc.on_packet_sent(
        now + Duration::from_secs(10),
        1,
        None,
        &rtt_estimator,
        &mut publisher,
    );

    assert_eq!(cc.bytes_in_flight, 1);

    // Latest RTT is 100ms
    rtt_estimator.update_rtt(
        Duration::from_millis(0),
        Duration::from_millis(100),
        now,
        true,
        PacketNumberSpace::ApplicationData,
    );

    //= https://www.rfc-editor.org/rfc/rfc8312#section-4.8
    //= type=test
    //# CUBIC MUST employ a slow-start algorithm, when the cwnd is no more
    //# than ssthresh.  Among the slow-start algorithms, CUBIC MAY choose the
    //# standard TCP slow start [RFC5681] in general networks, or the limited
    //# slow start [RFC3742] or hybrid slow start [HR08] for fast and long-
    //# distance networks.

    // Round one of hybrid slow start
    cc.on_rtt_update(now, now, &rtt_estimator, &mut publisher);

    assert!(cc.state.is_slow_start());

    // Latest RTT is 200ms
    rtt_estimator.update_rtt(
        Duration::from_millis(0),
        Duration::from_millis(200),
        now,
        true,
        PacketNumberSpace::ApplicationData,
    );

    // Last sent packet time updated to t20
    cc.on_packet_sent(
        now + Duration::from_secs(20),
        1,
        None,
        &rtt_estimator,
        &mut publisher,
    );

    assert_eq!(cc.bytes_in_flight, 2);
    assert!(cc.state.is_slow_start());

    // Round two of hybrid slow start
    for _i in 1..=8 {
        cc.on_rtt_update(
            now + Duration::from_secs(10),
            now + Duration::from_secs(10),
            &rtt_estimator,
            &mut publisher,
        );
    }

    assert!(!cc.state.is_slow_start());
    assert_delta!(cc.slow_start.threshold, 100_000.0, 0.001);
}

#[test]
fn on_packet_sent_application_limited() {
    let mut cc = CubicCongestionController::new(1000);
    let mut publisher = event::testing::Publisher::snapshot();
    let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
    let rtt_estimator = RttEstimator::default();
    let now = NoopClock.get_time();

    cc.congestion_window = 100_000.0;
    cc.bytes_in_flight = BytesInFlight::new(92_500);
    cc.state = SlowStart;

    // t0: Send a packet in Slow Start
    cc.on_packet_sent(now, 1000, Some(true), &rtt_estimator, &mut publisher);

    assert_eq!(cc.bytes_in_flight, 93_500);
    assert_eq!(cc.time_of_last_sent_packet, Some(now));

    // t10: Enter Congestion Avoidance
    cc.state = State::congestion_avoidance(now + Duration::from_secs(10));

    assert!(!cc.under_utilized);

    // t15: Send a packet in Congestion Avoidance
    cc.on_packet_sent(
        now + Duration::from_secs(15),
        1000,
        Some(true),
        &rtt_estimator,
        &mut publisher,
    );

    assert_eq!(cc.bytes_in_flight, 94_500);
    assert_eq!(
        cc.time_of_last_sent_packet,
        Some(now + Duration::from_secs(15))
    );
    assert!(cc.under_utilized);

    // t20: Send packets to fully utilize the congestion window
    while cc.bytes_in_flight < cc.congestion_window() {
        cc.on_packet_sent(
            now + Duration::from_secs(20),
            1000,
            Some(true),
            &rtt_estimator,
            &mut publisher,
        );
    }

    assert!(!cc.under_utilized);
}

// Confirm `under_utilized` is set properly even when `app_limited` is `None`
#[test]
fn on_packet_sent_none_application_limited() {
    let mut cc = CubicCongestionController::new(1000);
    let mut publisher = event::testing::Publisher::snapshot();
    let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
    let rtt_estimator = RttEstimator::default();
    let now = NoopClock.get_time();

    cc.congestion_window = 100_000.0;
    cc.bytes_in_flight = BytesInFlight::new(92_500);
    cc.state = SlowStart;

    // t0: Send a packet in Slow Start
    cc.on_packet_sent(now, 1000, None, &rtt_estimator, &mut publisher);

    assert_eq!(cc.bytes_in_flight, 93_500);
    assert_eq!(cc.time_of_last_sent_packet, Some(now));

    // t10: Enter Congestion Avoidance
    cc.state = State::congestion_avoidance(now + Duration::from_secs(10));

    assert!(!cc.under_utilized);

    // t15: Send a packet in Congestion Avoidance
    cc.on_packet_sent(
        now + Duration::from_secs(15),
        1000,
        None,
        &rtt_estimator,
        &mut publisher,
    );

    assert_eq!(cc.bytes_in_flight, 94_500);
    assert_eq!(
        cc.time_of_last_sent_packet,
        Some(now + Duration::from_secs(15))
    );
    assert!(cc.under_utilized);

    // t20: Send packets to fully utilize the congestion window
    while cc.bytes_in_flight < cc.congestion_window() {
        cc.on_packet_sent(
            now + Duration::from_secs(20),
            1000,
            None,
            &rtt_estimator,
            &mut publisher,
        );
    }

    assert!(!cc.under_utilized);
}

#[test]
fn on_packet_sent_fast_retransmission() {
    let mut cc = CubicCongestionController::new(1000);
    let mut publisher = event::testing::Publisher::snapshot();
    let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
    let rtt_estimator = RttEstimator::default();
    let now = NoopClock.get_time();

    cc.congestion_window = 100_000.0;
    cc.bytes_in_flight = BytesInFlight::new(99900);
    cc.state = Recovery(now, RequiresTransmission);

    cc.on_packet_sent(
        now + Duration::from_secs(10),
        100,
        None,
        &rtt_estimator,
        &mut publisher,
    );

    assert_eq!(cc.state, Recovery(now, Idle));
}

//= https://www.rfc-editor.org/rfc/rfc8312#section-5.8
//= type=test
//# In case of long periods when cwnd has not been updated due
//# to the application rate limit, such as idle periods, t in Eq. 1 MUST
//# NOT include these periods; otherwise, W_cubic(t) might be very high
//# after restarting from these periods.
#[test]
fn congestion_avoidance_after_idle_period() {
    let mut cc = CubicCongestionController::new(1000);
    let mut publisher = event::testing::Publisher::snapshot();
    let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
    let now = NoopClock.get_time();
    let rtt_estimator = &RttEstimator::default();
    let random = &mut random::testing::Generator::default();

    cc.congestion_window = 6000.0;
    cc.bytes_in_flight = BytesInFlight::new(0);
    cc.state = SlowStart;

    // t0: Send a packet in Slow Start
    cc.on_packet_sent(now, 1000, Some(true), rtt_estimator, &mut publisher);

    assert_eq!(cc.bytes_in_flight, 1000);

    // t10: Enter Congestion Avoidance
    cc.cubic.w_max = 6.0;
    cc.state = State::congestion_avoidance(now + Duration::from_secs(10));

    // t15: Send a packet in Congestion Avoidance while under utilized
    cc.on_packet_sent(
        now + Duration::from_secs(15),
        1000,
        Some(true),
        rtt_estimator,
        &mut publisher,
    );
    assert!(cc.is_congestion_window_under_utilized());

    assert_eq!(cc.bytes_in_flight, 2000);

    // t16: Ack a packet in Congestion Avoidance
    cc.on_ack(
        now,
        1000,
        (),
        rtt_estimator,
        random,
        now + Duration::from_secs(16),
        &mut publisher,
    );
    // Verify the app limited time is set
    assert_eq!(
        cc.state,
        CongestionAvoidance(CongestionAvoidanceTiming {
            start_time: now + Duration::from_secs(10),
            window_increase_time: now + Duration::from_secs(10),
            app_limited_time: Some(now + Duration::from_secs(16)),
        })
    );

    assert_eq!(cc.bytes_in_flight, 1000);

    // t20: Send packets to fully utilize the congestion window
    while cc.bytes_in_flight < cc.congestion_window() {
        cc.on_packet_sent(
            now + Duration::from_secs(20),
            1000,
            Some(false),
            rtt_estimator,
            &mut publisher,
        );
    }

    assert!(!cc.is_congestion_window_under_utilized());

    // t25: Ack a packet in Congestion Avoidance
    cc.on_ack(
        now,
        1000,
        (),
        rtt_estimator,
        random,
        now + Duration::from_secs(25),
        &mut publisher,
    );

    // Verify congestion avoidance start time was moved from t10 to t16 to account
    // for the 6 seconds of under utilized time and the app_limited_time was reset
    assert_eq!(
        cc.state,
        CongestionAvoidance(CongestionAvoidanceTiming {
            start_time: now + Duration::from_secs(16),
            window_increase_time: now + Duration::from_secs(25),
            app_limited_time: None,
        })
    );
    // Verify t does not include the app limited time
    if let CongestionAvoidance(timing) = cc.state {
        assert_eq!(
            Duration::from_secs(9),
            timing.t(now + Duration::from_secs(25))
        );
    }
}

#[test]
fn congestion_avoidance_after_fast_convergence() {
    let max_datagram_size = 1200;
    let mut cc = CubicCongestionController::new(max_datagram_size);
    let mut publisher = event::testing::Publisher::snapshot();
    let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
    let now = NoopClock.get_time();
    let random = &mut random::testing::Generator::default();
    cc.bytes_in_flight = BytesInFlight::new(100);
    cc.congestion_window = 80_000.0;
    cc.cubic.w_last_max = bytes_to_packets(100_000.0, max_datagram_size);

    cc.on_packet_lost(100, (), false, false, random, now, &mut publisher);
    assert_delta!(cc.congestion_window, 80_000.0 * BETA_CUBIC, 0.001);

    // Window max was less than the last max, so fast convergence applies
    assert_delta!(
        cc.cubic.w_last_max,
        80000.0 / max_datagram_size as f32,
        0.001
    );
    // W_max = W_max*(1.0+beta_cubic)/2.0 = W_max * .85
    assert_delta!(
        cc.cubic.w_max,
        80000.0 * 0.85 / max_datagram_size as f32,
        0.001
    );

    let prev_cwnd = cc.congestion_window;

    // Enter congestion avoidance
    cc.congestion_avoidance(
        Duration::from_millis(10),
        Duration::from_millis(100),
        100,
        f32::MAX,
    );

    // Verify congestion window has increased
    assert!(cc.congestion_window > prev_cwnd);
}

#[test]
fn congestion_avoidance_after_rtt_improvement() {
    let max_datagram_size = 1200;
    let mut cc = CubicCongestionController::new(max_datagram_size);
    cc.bytes_in_flight = BytesInFlight::new(100);
    cc.congestion_window = 80_000.0;
    cc.cubic.w_max = cc.congestion_window / 1200.0;

    // Enter congestion avoidance with a long rtt
    cc.congestion_avoidance(
        Duration::from_millis(10),
        Duration::from_millis(750),
        100,
        f32::MAX,
    );

    // At this point the target is less than the congestion window
    let prev_cwnd = cc.congestion_window;
    assert!(
        cc.cubic.w_cubic(Duration::from_secs(0)) < bytes_to_packets(prev_cwnd, max_datagram_size)
    );

    // Receive another ack, now with a short rtt
    cc.congestion_avoidance(
        Duration::from_millis(20),
        Duration::from_millis(10),
        100,
        f32::MAX,
    );

    // Verify congestion window did not change
    assert_delta!(cc.congestion_window, prev_cwnd, 0.001);
}

#[test]
fn congestion_avoidance_with_small_min_rtt() {
    let max_datagram_size = 1200;
    let mut cc = CubicCongestionController::new(max_datagram_size);
    cc.bytes_in_flight = BytesInFlight::new(100);
    cc.congestion_window = 80_000.0;
    cc.cubic.w_max = cc.congestion_window / 1200.0;

    cc.congestion_avoidance(
        Duration::from_millis(100),
        Duration::from_millis(1),
        100,
        f32::MAX,
    );

    // Verify the window grew by half the sent bytes
    assert_delta!(cc.congestion_window, 80_050.0, 0.001);
}

#[test]
fn congestion_avoidance_max_cwnd() {
    let max_datagram_size = 1200;
    let mut cc = CubicCongestionController::new(max_datagram_size);
    cc.bytes_in_flight = BytesInFlight::new(100);
    cc.congestion_window = 80_000.0;
    cc.cubic.w_max = bytes_to_packets(100_000.0, max_datagram_size);

    cc.congestion_avoidance(
        Duration::from_millis(300),
        Duration::from_millis(100),
        1200,
        80_100.0,
    );

    // Verify the window did not exceed the max cwnd
    assert_delta!(cc.congestion_window, 80_100.0, 0.001);
}

#[test]
fn on_packet_lost() {
    let mut cc = CubicCongestionController::new(1000);
    let mut publisher = event::testing::Publisher::snapshot();
    let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
    let now = NoopClock.get_time();
    let random = &mut random::testing::Generator::default();
    cc.congestion_window = 100_000.0;
    cc.bytes_in_flight = BytesInFlight::new(100_000);
    cc.state = SlowStart;

    cc.on_packet_lost(
        100,
        (),
        false,
        false,
        random,
        now + Duration::from_secs(10),
        &mut publisher,
    );

    assert_eq!(cc.bytes_in_flight, 100_000u32 - 100);
    //= https://www.rfc-editor.org/rfc/rfc9002#section-7.3.1
    //= type=test
    //# The sender MUST exit slow start and enter a recovery period when a
    //# packet is lost or when the ECN-CE count reported by its peer
    //# increases.
    assert_eq!(
        cc.state,
        Recovery(now + Duration::from_secs(10), RequiresTransmission)
    );
    //= https://www.rfc-editor.org/rfc/rfc9002#section-7.3.2
    //= type=test
    //# Implementations MAY reduce the congestion window immediately upon
    //# entering a recovery period or use other mechanisms, such as
    //# Proportional Rate Reduction [PRR], to reduce the congestion window
    //# more gradually.
    assert_delta!(cc.congestion_window, 100_000.0 * BETA_CUBIC, 0.001);
    assert_delta!(cc.slow_start.threshold, 100_000.0 * BETA_CUBIC, 0.001);
}

#[test]
fn on_packet_lost_below_minimum_window() {
    let mut cc = CubicCongestionController::new(1000);
    let mut publisher = event::testing::Publisher::snapshot();
    let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
    let now = NoopClock.get_time();
    let random = &mut random::testing::Generator::default();
    cc.congestion_window = cc.cubic.minimum_window();
    cc.bytes_in_flight = BytesInFlight::new(cc.congestion_window());
    cc.state = State::congestion_avoidance(now);

    cc.on_packet_lost(
        100,
        (),
        false,
        false,
        random,
        now + Duration::from_secs(10),
        &mut publisher,
    );

    assert_delta!(cc.congestion_window, cc.cubic.minimum_window(), 0.001);
}

#[test]
fn on_packet_lost_already_in_recovery() {
    let mut cc = CubicCongestionController::new(1000);
    let mut publisher = event::testing::Publisher::snapshot();
    let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
    let now = NoopClock.get_time();
    let random = &mut random::testing::Generator::default();
    cc.congestion_window = 10000.0;
    cc.bytes_in_flight = BytesInFlight::new(1000);
    cc.state = Recovery(now, Idle);

    // break up on_packet_loss into two call to confirm double call
    // behavior is valid (50 + 50 = 100 lost bytes)
    cc.on_packet_lost(50, (), false, false, random, now, &mut publisher);
    cc.on_packet_lost(50, (), false, false, random, now, &mut publisher);

    // No change to the congestion window
    assert_delta!(cc.congestion_window, 10000.0, 0.001);
}

//= https://www.rfc-editor.org/rfc/rfc9002#section-7.6.2
//= type=test
//# When persistent congestion is declared, the sender's congestion
//# window MUST be reduced to the minimum congestion window
//# (kMinimumWindow), similar to a TCP sender's response on an RTO
//# [RFC5681].
#[test]
fn on_packet_lost_persistent_congestion() {
    let mut cc = CubicCongestionController::new(1000);
    let mut publisher = event::testing::Publisher::snapshot();
    let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
    let now = NoopClock.get_time();
    let random = &mut random::testing::Generator::default();
    cc.congestion_window = 10000.0;
    cc.bytes_in_flight = BytesInFlight::new(1000);
    cc.state = Recovery(now, Idle);

    cc.on_packet_lost(100, (), true, false, random, now, &mut publisher);

    assert!(cc.state.is_slow_start());
    assert_eq!(cc.state, SlowStart);
    assert_delta!(cc.congestion_window, cc.cubic.minimum_window(), 0.001);
    assert_delta!(cc.cubic.w_max, 0.0, 0.001);
    assert_delta!(cc.cubic.w_last_max, 0.0, 0.001);
    assert_eq!(cc.cubic.k, Duration::from_millis(0));
}

//= https://www.rfc-editor.org/rfc/rfc9002#section-7.2
//= type=test
//# If the maximum datagram size changes during the connection, the
//# initial congestion window SHOULD be recalculated with the new size.

//= https://www.rfc-editor.org/rfc/rfc8899#section-3
//= type=test
//# A PL that maintains the congestion window in terms of a limit to
//# the number of outstanding fixed-size packets SHOULD adapt this
//# limit to compensate for the size of the actual packets.
#[test]
fn on_mtu_update_increase() {
    let mut mtu = 5000;
    let cwnd_in_packets = 100_000f32;
    let cwnd_in_bytes = cwnd_in_packets / mtu as f32;
    let mut cc = CubicCongestionController::new(mtu);
    let mut publisher = event::testing::Publisher::snapshot();
    let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
    cc.congestion_window = cwnd_in_packets;

    mtu = 10000;
    cc.on_mtu_update(mtu, &mut publisher);
    assert_eq!(cc.max_datagram_size, mtu);
    assert_eq!(cc.cubic.max_datagram_size, mtu);

    assert_delta!(cc.congestion_window, 200_000.0, 0.001);

    //= https://www.rfc-editor.org/rfc/rfc8899#section-3
    //= type=test
    //# An update to the PLPMTU (or MPS) MUST NOT increase the congestion
    //# window measured in bytes [RFC4821].
    assert_delta!(cc.congestion_window / mtu as f32, cwnd_in_bytes, 0.001);
}

//= https://www.rfc-editor.org/rfc/rfc9002#section-6.4
//= type=test
//# The sender MUST discard all recovery state associated with
//# those packets and MUST remove them from the count of bytes in flight.
#[test]
fn on_packet_discarded() {
    let mut cc = CubicCongestionController::new(5000);
    let mut publisher = event::testing::Publisher::snapshot();
    let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
    cc.bytes_in_flight = BytesInFlight::new(10000);

    cc.on_packet_discarded(1000, &mut publisher);

    assert_eq!(cc.bytes_in_flight, 10000 - 1000);

    let now = NoopClock.get_time();
    cc.state = Recovery(now, FastRetransmission::RequiresTransmission);

    cc.on_packet_discarded(1000, &mut publisher);

    assert_eq!(Recovery(now, FastRetransmission::Idle), cc.state);
}

//= https://www.rfc-editor.org/rfc/rfc9002#section-7.8
//= type=test
//# When bytes in flight is smaller than the congestion window and
//# sending is not pacing limited, the congestion window is
//# underutilized.  This can happen due to insufficient application data
//# or flow control limits.  When this occurs, the congestion window
//# SHOULD NOT be increased in either slow start or congestion avoidance.
#[test]
fn on_packet_ack_limited() {
    let mut cc = CubicCongestionController::new(5000);
    let mut publisher = event::testing::Publisher::snapshot();
    let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
    let now = NoopClock.get_time();
    let random = &mut random::testing::Generator::default();
    cc.congestion_window = 100_000.0;
    cc.bytes_in_flight = BytesInFlight::new(10000);
    cc.under_utilized = true;
    cc.state = SlowStart;

    cc.on_ack(
        now,
        1,
        (),
        &RttEstimator::default(),
        random,
        now,
        &mut publisher,
    );

    assert_delta!(cc.congestion_window, 100_000.0, 0.001);

    cc.state = State::congestion_avoidance(now);

    cc.on_ack(
        now,
        1,
        (),
        &RttEstimator::default(),
        random,
        now,
        &mut publisher,
    );

    assert_delta!(cc.congestion_window, 100_000.0, 0.001);
}

#[test]
#[should_panic]
fn on_packet_ack_timestamp_regression() {
    let mut cc = CubicCongestionController::new(5000);
    let mut publisher = event::testing::Publisher::snapshot();
    let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
    let now = NoopClock.get_time() + Duration::from_secs(1);
    let rtt_estimator = RttEstimator::default();
    let random = &mut random::testing::Generator::default();
    cc.congestion_window = 100_000.0;
    cc.bytes_in_flight = BytesInFlight::new(10000);
    cc.under_utilized = true;
    cc.state = State::congestion_avoidance(now);

    cc.on_ack(now, 1, (), &rtt_estimator, random, now, &mut publisher);

    assert_eq!(
        State::CongestionAvoidance(CongestionAvoidanceTiming {
            start_time: now,
            window_increase_time: now,
            app_limited_time: Some(now),
        }),
        cc.state
    );

    cc.on_ack(
        now,
        1,
        (),
        &rtt_estimator,
        random,
        now - Duration::from_secs(1),
        &mut publisher,
    );
}

#[test]
fn on_packet_ack_utilized_then_under_utilized() {
    let mut cc = CubicCongestionController::new(5000);
    let mut publisher = event::testing::Publisher::snapshot();
    let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
    let now = NoopClock.get_time();
    let mut rtt_estimator = RttEstimator::default();
    let random = &mut random::testing::Generator::default();
    rtt_estimator.update_rtt(
        Duration::from_secs(0),
        Duration::from_millis(200),
        now,
        true,
        PacketNumberSpace::ApplicationData,
    );
    cc.congestion_window = 100_000.0;
    cc.state = SlowStart;

    cc.on_packet_sent(now, 60_000, Some(true), &rtt_estimator, &mut publisher);
    cc.on_ack(now, 10_000, (), &rtt_estimator, random, now, &mut publisher);
    let cwnd = cc.congestion_window();

    assert!(!cc.under_utilized);
    assert!(cwnd > 100_000);

    // Now the window is under utilized, but we still grow the window until more packets are sent
    assert!(cc.is_congestion_window_under_utilized());
    cc.on_ack(
        now,
        1200,
        (),
        &rtt_estimator,
        random,
        now + Duration::from_millis(100),
        &mut publisher,
    );
    assert!(cc.congestion_window() > cwnd);

    // A large amount is acked, but the window should only grow to the max_cwnd
    cc.on_ack(
        now,
        40_000,
        (),
        &rtt_estimator,
        random,
        now + Duration::from_millis(100),
        &mut publisher,
    );
    // 60_000 is the highest bytes in flight * 2 for the slow start max_cwnd multiplier
    assert_eq!(60_000 * 2, cc.congestion_window());

    let cwnd = cc.congestion_window();

    // Now the application has had a chance to send more data, but it didn't send enough to
    // utilize the congestion window, so the window does not grow.
    cc.on_packet_sent(now, 1200, Some(true), &rtt_estimator, &mut publisher);
    assert!(cc.under_utilized);
    cc.on_ack(
        now,
        1200,
        (),
        &rtt_estimator,
        random,
        now + Duration::from_millis(201),
        &mut publisher,
    );
    assert_eq!(cc.congestion_window(), cwnd);
}

#[test]
fn on_packet_ack_congestion_avoidance_max_cwnd() {
    let mut cc = CubicCongestionController::new(5000);
    let mut publisher = event::testing::Publisher::snapshot();
    let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
    let now = NoopClock.get_time();
    let mut rtt_estimator = RttEstimator::default();
    let random = &mut random::testing::Generator::default();
    rtt_estimator.update_rtt(
        Duration::from_secs(0),
        Duration::from_millis(200),
        now,
        true,
        PacketNumberSpace::ApplicationData,
    );
    cc.congestion_window = 89_000.0;
    cc.state = State::congestion_avoidance(now);
    cc.cubic.w_max = 100_000.0;

    cc.on_packet_sent(now, 60_000, Some(false), &rtt_estimator, &mut publisher);
    cc.on_ack(now, 60_000, (), &rtt_estimator, random, now, &mut publisher);

    // 60_000 is the highest bytes in flight * 1.5 for the congestion avoidance max_cwnd multiplier
    assert_eq!(90_000, cc.congestion_window());
}

#[test]
//= https://www.rfc-editor.org/rfc/rfc9002#section-7.3.2
//= type=test
fn on_packet_ack_recovery_to_congestion_avoidance() {
    let mut cc = CubicCongestionController::new(5000);
    let mut publisher = event::testing::Publisher::snapshot();
    let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
    let now = NoopClock.get_time();
    let random = &mut random::testing::Generator::default();

    cc.cubic.w_max = bytes_to_packets(25000.0, 5000);
    cc.state = Recovery(now, Idle);
    cc.bytes_in_flight = BytesInFlight::new(25000);
    cc.under_utilized = false;

    cc.on_ack(
        now + Duration::from_millis(1),
        1,
        (),
        &RttEstimator::default(),
        random,
        now + Duration::from_millis(2),
        &mut publisher,
    );

    assert_eq!(
        cc.state,
        State::congestion_avoidance(now + Duration::from_millis(2))
    );
}

#[test]
//= https://www.rfc-editor.org/rfc/rfc9002#section-7.3.2
//= type=test
fn on_packet_ack_slow_start_to_congestion_avoidance() {
    let mut cc = CubicCongestionController::new(5000);
    let mut publisher = event::testing::Publisher::snapshot();
    let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
    let now = NoopClock.get_time();
    let random = &mut random::testing::Generator::default();

    cc.state = SlowStart;
    cc.congestion_window = 10000.0;
    cc.bytes_in_flight = BytesInFlight::new(10000);
    cc.slow_start.threshold = 10050.0;
    cc.under_utilized = false;

    cc.on_ack(
        now,
        100,
        (),
        &RttEstimator::default(),
        random,
        now + Duration::from_millis(2),
        &mut publisher,
    );

    assert_delta!(cc.congestion_window, 10100.0, 0.001);
    assert_delta!(
        cc.packets_to_bytes(cc.cubic.w_max),
        cc.congestion_window,
        0.001
    );
    assert_eq!(cc.cubic.k, Duration::from_secs(0));
    assert_eq!(
        cc.state,
        State::congestion_avoidance(now + Duration::from_millis(2))
    );
}

#[test]
fn on_packet_ack_recovery() {
    let mut cc = CubicCongestionController::new(5000);
    let mut publisher = event::testing::Publisher::snapshot();
    let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
    let now = NoopClock.get_time();
    let random = &mut random::testing::Generator::default();

    cc.state = Recovery(now, Idle);
    cc.congestion_window = 10000.0;
    cc.bytes_in_flight = BytesInFlight::new(10000);

    cc.on_ack(
        now,
        100,
        (),
        &RttEstimator::default(),
        random,
        now + Duration::from_millis(2),
        &mut publisher,
    );

    // Congestion window stays the same in recovery
    assert_delta!(cc.congestion_window, 10000.0, 0.001);
    assert_eq!(cc.state, Recovery(now, Idle));
}

#[test]
fn on_packet_ack_congestion_avoidance() {
    let max_datagram_size = 5000;
    let mut cc = CubicCongestionController::new(max_datagram_size);
    let mut cc2 = CubicCongestionController::new(max_datagram_size);
    let mut publisher = event::testing::Publisher::snapshot();
    let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
    let now = NoopClock.get_time();
    let random = &mut random::testing::Generator::default();

    cc.state = State::congestion_avoidance(now + Duration::from_millis(3300));
    cc.congestion_window = 10000.0;
    cc.bytes_in_flight = BytesInFlight::new(10000);
    cc.cubic.w_max = bytes_to_packets(10000.0, max_datagram_size);
    cc.under_utilized = false;

    cc2.congestion_window = 10000.0;
    cc2.bytes_in_flight = BytesInFlight::new(10000);
    cc2.cubic.w_max = bytes_to_packets(10000.0, max_datagram_size);

    let mut rtt_estimator = RttEstimator::default();
    rtt_estimator.update_rtt(
        Duration::from_secs(0),
        Duration::from_millis(275),
        now,
        true,
        PacketNumberSpace::ApplicationData,
    );

    cc.on_ack(
        now,
        1000,
        (),
        &rtt_estimator,
        random,
        now + Duration::from_millis(4750),
        &mut publisher,
    );

    let t = Duration::from_millis(4750) - Duration::from_millis(3300);
    let rtt = rtt_estimator.min_rtt();

    cc2.congestion_avoidance(t, rtt, 1000, f32::MAX);

    assert_delta!(cc.congestion_window, cc2.congestion_window, 0.001);
}

//= https://www.rfc-editor.org/rfc/rfc8312#section-4.2
//= type=test
//# If so, CUBIC is in the TCP-friendly region and cwnd SHOULD
//# be set to W_est(t) at each reception of an ACK.
#[test]
fn on_packet_ack_congestion_avoidance_tcp_friendly_region() {
    let mut cc = CubicCongestionController::new(5000);

    cc.congestion_window = 10000.0;
    cc.cubic.w_max = 2.5;
    cc.cubic.k = Duration::from_secs_f32(2.823);

    let t = Duration::from_millis(300);
    let rtt = Duration::from_millis(250);

    cc.congestion_avoidance(t, rtt, 5000, f32::MAX);

    assert!(cc.cubic.w_cubic(t) < cc.cubic.w_est(t, rtt));
    assert_delta!(cc.congestion_window, cc.cubic.w_est(t, rtt) * 5000.0, 0.001);
}

//= https://www.rfc-editor.org/rfc/rfc8312#section-4.3
//= type=test
//# In this region, cwnd MUST be incremented by
//# (W_cubic(t+RTT) - cwnd)/cwnd for each received ACK, where
//# W_cubic(t+RTT) is calculated using Eq. 1.
#[test]
fn on_packet_ack_congestion_avoidance_concave_region() {
    let max_datagram_size = 1200;
    let mut cc = CubicCongestionController::new(max_datagram_size as u16);

    cc.congestion_window = 2_400_000.0;
    cc.cubic.w_max = 2304.0;
    cc.cubic.k = Duration::from_secs(12);

    let t = Duration::from_millis(9800);
    let rtt = Duration::from_millis(200);

    cc.congestion_avoidance(t, rtt, 1000, f32::MAX);

    assert!(cc.cubic.w_cubic(t) > cc.cubic.w_est(t, rtt));

    // W_cubic(t+RTT) = C*(t-K)^3 + W_max
    // W_cubic(10) = .4*(-2)^3 + 2304
    // W_cubic(10) = 2300.8

    // cwnd = (W_cubic(t+RTT) - cwnd)/cwnd + cwnd
    // cwnd = ((2300.8 - 2000)/2000 + 2000) * max_datagram_size
    // cwnd = 2400180.48

    assert_delta!(cc.congestion_window, 2_400_180.5, 0.001);
}

//= https://www.rfc-editor.org/rfc/rfc8312#section-4.4
//= type=test
//# In this region, cwnd MUST be incremented by
//# (W_cubic(t+RTT) - cwnd)/cwnd for each received ACK, where
//# W_cubic(t+RTT) is calculated using Eq. 1.
#[test]
fn on_packet_ack_congestion_avoidance_convex_region() {
    let max_datagram_size = 1200;
    let mut cc = CubicCongestionController::new(max_datagram_size);

    cc.congestion_window = 3_600_000.0;
    cc.cubic.w_max = 2304.0;
    cc.cubic.k = Duration::from_secs(12);

    let t = Duration::from_millis(25800);
    let rtt = Duration::from_millis(200);

    cc.congestion_avoidance(t, rtt, 1000, f32::MAX);

    assert!(cc.cubic.w_cubic(t) > cc.cubic.w_est(t, rtt));

    // W_cubic(t+RTT) = C*(t-K)^3 + W_max
    // W_cubic(26) = .4*(14)^3 + 2304
    // W_cubic(26) = 3401.6

    // cwnd = (W_cubic(t+RTT) - cwnd)/cwnd + cwnd
    // cwnd = ((3401.6 - 3000)/3000 + 3000) * max_datagram_size
    // cwnd = 3600160.64

    assert_eq!(cc.congestion_window(), 3_600_160);
}

#[test]
fn on_packet_ack_congestion_avoidance_too_large_increase() {
    let max_datagram_size = 1200;
    let mut cc = CubicCongestionController::new(max_datagram_size);

    cc.congestion_window = 3_600_000.0;
    cc.cubic.w_max = bytes_to_packets(2_764_800.0, max_datagram_size);

    let t = Duration::from_millis(125_800);
    let rtt = Duration::from_millis(200);

    cc.congestion_avoidance(t, rtt, 1000, f32::MAX);

    assert!(cc.cubic.w_cubic(t) > cc.cubic.w_est(t, rtt));
    assert_delta!(cc.congestion_window, 3_600_000.0 + 1000.0 / 2.0, 0.001);
}