use crate::config::{MIN_COMPATIBLE_VERSION, PCK_VERSION};
const NEW_FORMAT_MARKER: u8 = 0xFF;
const FORMAT_VERSION: u8 = 1;
pub(super) const PROTOC_VERSION: [u8; 8] = encode_new_format(PCK_VERSION, MIN_COMPATIBLE_VERSION);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct VersionInfo {
pub version: (u8, u8, u16),
pub min_compatible: Option<(u8, u8, u16)>,
}
impl std::fmt::Display for VersionInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (maj, min, pat) = self.version;
write!(f, "{maj}.{min}.{pat}")
}
}
const fn encode_new_format(version: &str, min_compatible: &str) -> [u8; 8] {
let (major, minor, patch) = parse_semver(version);
let (_min_maj, _min_min, min_patch) = parse_semver(min_compatible);
[
NEW_FORMAT_MARKER,
major,
minor,
(patch >> 8) as u8,
patch as u8,
(min_patch >> 8) as u8,
min_patch as u8,
FORMAT_VERSION,
]
}
const fn parse_semver(version: &str) -> (u8, u8, u16) {
let mut major = 0u8;
let mut minor = 0u8;
let mut patch = 0u16;
let mut state = 0u8;
let bytes = version.as_bytes();
let mut i = 0;
while i < bytes.len() {
let c = bytes[i];
if c == b'.' {
state += 1;
} else if c >= b'0' && c <= b'9' {
let digit = (c - b'0') as u16;
match state {
0 => major = (major as u16 * 10 + digit) as u8,
1 => minor = (minor as u16 * 10 + digit) as u8,
2 => patch = patch * 10 + digit,
_ => {}
}
} else {
break; }
i += 1;
}
(major, minor, patch)
}
pub(super) fn parse_version_bytes(bytes: &[u8; 8]) -> VersionInfo {
if bytes[0] == NEW_FORMAT_MARKER && bytes[7] != 0 {
let major = bytes[1];
let minor = bytes[2];
let patch = u16::from_be_bytes([bytes[3], bytes[4]]);
let min_patch = u16::from_be_bytes([bytes[5], bytes[6]]);
VersionInfo {
version: (major, minor, patch),
min_compatible: Some((major, minor, min_patch)),
}
} else {
let major = bytes[0];
let minor = bytes[1];
let patch = bytes[2] as u16;
VersionInfo {
version: (major, minor, patch),
min_compatible: None,
}
}
}
pub(super) fn ver_ge(a: (u8, u8, u16), b: (u8, u8, u16)) -> bool {
a >= b
}
pub(super) fn remote_requires_newer_than_us(local: &[u8; 8], remote_bytes: &[u8; 8]) -> bool {
let remote_info = parse_version_bytes(remote_bytes);
let local_info = parse_version_bytes(local);
remote_info
.min_compatible
.is_some_and(|min| !ver_ge(local_info.version, min))
}
pub(super) fn is_compatible(local: &[u8; 8], remote: &[u8; 8]) -> Result<VersionInfo, String> {
let local_info = parse_version_bytes(local);
let remote_info = parse_version_bytes(remote);
if let Some(local_min) = local_info.min_compatible {
if !ver_ge(remote_info.version, local_min) {
return Err(format!(
"remote {} too old for our min_compatible {}.{}.{}",
remote_info, local_min.0, local_min.1, local_min.2,
));
}
}
if let Some(remote_min) = remote_info.min_compatible {
if !ver_ge(local_info.version, remote_min) {
return Err(format!(
"our {} too old for remote's min_compatible {}.{}.{}",
local_info, remote_min.0, remote_min.1, remote_min.2,
));
}
}
Ok(remote_info)
}
pub(super) fn old_peer_rejection_means_we_must_update(
remote_version_str: &str,
local: &[u8; 8],
) -> bool {
let local_info = parse_version_bytes(local);
let (rmaj, rmin, rpatch) = parse_semver(remote_version_str);
if let Some(local_min) = local_info.min_compatible {
if !ver_ge((rmaj, rmin, rpatch), local_min) {
return false;
}
}
!ver_ge(local_info.version, (rmaj, rmin, rpatch))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_semver() {
assert_eq!(parse_semver("0.1.152"), (0, 1, 152));
assert_eq!(parse_semver("0.1.256"), (0, 1, 256));
assert_eq!(parse_semver("0.1.65535"), (0, 1, 65535));
assert_eq!(parse_semver("1.2.3-alpha"), (1, 2, 3));
assert_eq!(parse_semver("10.20.300"), (10, 20, 300));
}
#[test]
fn test_new_format_roundtrip() {
let encoded = encode_new_format("0.1.153", "0.1.152");
let info = parse_version_bytes(&encoded);
assert_eq!(info.version, (0, 1, 153));
assert_eq!(info.min_compatible, Some((0, 1, 152)));
}
#[test]
fn test_old_format_parsing() {
let old = [0u8, 1, 152, 0, 0, 0, 0, 0];
let info = parse_version_bytes(&old);
assert_eq!(info.version, (0, 1, 152));
assert_eq!(info.min_compatible, None);
}
#[test]
fn test_old_format_with_flags() {
let old = [0u8, 1, 152, 0x12, 0x34, 0x56, 0x78, 0];
let info = parse_version_bytes(&old);
assert_eq!(info.version, (0, 1, 152));
assert_eq!(info.min_compatible, None);
}
#[test]
fn test_new_accepts_old_compatible() {
let new = encode_new_format("0.1.153", "0.1.152");
let old = [0u8, 1, 152, 0, 0, 0, 0, 0];
let result = is_compatible(&new, &old);
assert!(result.is_ok(), "new should accept compatible old peer");
assert_eq!(result.unwrap().version, (0, 1, 152));
}
#[test]
fn test_new_rejects_old_incompatible() {
let new = encode_new_format("0.1.155", "0.1.153");
let old = [0u8, 1, 152, 0, 0, 0, 0, 0];
let result = is_compatible(&new, &old);
assert!(result.is_err(), "new should reject incompatible old peer");
}
#[test]
fn test_new_to_new_bidirectional() {
let a = encode_new_format("0.1.155", "0.1.153");
let b = encode_new_format("0.1.153", "0.1.152");
assert!(is_compatible(&a, &b).is_ok());
assert!(is_compatible(&b, &a).is_ok());
}
#[test]
fn test_new_to_new_asymmetric_reject() {
let a = encode_new_format("0.1.155", "0.1.154");
let b = encode_new_format("0.1.153", "0.1.150");
assert!(
is_compatible(&a, &b).is_err(),
"A should reject B (too old)"
);
assert!(
is_compatible(&b, &a).is_err(),
"B should also reject A (B knows it's too old for A's min)"
);
let a2 = encode_new_format("0.1.155", "0.1.152");
let b2 = encode_new_format("0.1.153", "0.1.150");
assert!(is_compatible(&a2, &b2).is_ok(), "A2 should accept B2");
assert!(is_compatible(&b2, &a2).is_ok(), "B2 should accept A2");
}
#[test]
fn test_patch_256_and_above() {
let v = encode_new_format("0.1.256", "0.1.200");
let info = parse_version_bytes(&v);
assert_eq!(info.version, (0, 1, 256));
assert_eq!(info.min_compatible, Some((0, 1, 200)));
let v2 = encode_new_format("0.1.65535", "0.1.1000");
let info2 = parse_version_bytes(&v2);
assert_eq!(info2.version, (0, 1, 65535));
assert_eq!(info2.min_compatible, Some((0, 1, 1000)));
}
#[test]
fn test_old_peer_rejection_analysis() {
let local = encode_new_format("0.1.153", "0.1.152");
assert!(
!old_peer_rejection_means_we_must_update("0.1.152", &local),
"should not trigger update when we are newer"
);
assert!(
old_peer_rejection_means_we_must_update("0.1.154", &local),
"should trigger update when remote is newer"
);
assert!(
!old_peer_rejection_means_we_must_update("0.1.153", &local),
"should not trigger update for same version"
);
}
#[test]
fn test_protoc_version_is_new_format() {
assert_eq!(
PROTOC_VERSION[0], NEW_FORMAT_MARKER,
"byte 0 must be 0xFF marker"
);
assert_eq!(
PROTOC_VERSION[7], FORMAT_VERSION,
"byte 7 must be format version"
);
}
#[test]
fn test_remote_requires_newer_than_us() {
let local = encode_new_format("0.1.153", "0.1.152");
let remote = encode_new_format("0.1.155", "0.1.154");
assert!(remote_requires_newer_than_us(&local, &remote));
let remote2 = encode_new_format("0.1.155", "0.1.152");
assert!(!remote_requires_newer_than_us(&local, &remote2));
let old_remote = [0u8, 1, 155, 0, 0, 0, 0, 0];
assert!(!remote_requires_newer_than_us(&local, &old_remote));
}
#[test]
fn test_ver_ge() {
assert!(ver_ge((0, 1, 153), (0, 1, 152)));
assert!(ver_ge((0, 1, 152), (0, 1, 152)));
assert!(!ver_ge((0, 1, 151), (0, 1, 152)));
assert!(ver_ge((0, 2, 0), (0, 1, 999)));
assert!(ver_ge((1, 0, 0), (0, 255, 65535)));
}
#[test]
fn test_min_compatible_shares_major_minor_with_version() {
let v = encode_new_format("0.1.200", "0.1.150");
let info = parse_version_bytes(&v);
assert_eq!(info.version, (0, 1, 200));
assert_eq!(info.min_compatible, Some((0, 1, 150)));
let v2 = encode_new_format("1.0.5", "0.1.3");
let info2 = parse_version_bytes(&v2);
assert_eq!(info2.version, (1, 0, 5));
assert_eq!(info2.min_compatible, Some((1, 0, 3)));
}
#[test]
fn test_old_peer_rejection_below_min_compatible() {
let local = encode_new_format("0.1.153", "0.1.152");
assert!(
!old_peer_rejection_means_we_must_update("0.1.100", &local),
"should not trigger update for peer below our min_compatible"
);
}
#[test]
fn test_parse_semver_malformed() {
assert_eq!(parse_semver(""), (0, 0, 0));
assert_eq!(parse_semver("0.1"), (0, 1, 0));
assert_eq!(parse_semver("abc"), (0, 0, 0));
assert_eq!(parse_semver("0.1.2.3"), (0, 1, 2));
assert_eq!(parse_semver("0.1.007"), (0, 1, 7));
}
#[test]
fn test_format_detection_byte7() {
let old = [0u8, 1, 152, 0, 0, 0, 0, 0];
assert!(parse_version_bytes(&old).min_compatible.is_none());
let new = encode_new_format("0.1.153", "0.1.152");
assert_eq!(new[7], FORMAT_VERSION);
assert!(parse_version_bytes(&new).min_compatible.is_some());
let old_255 = [0xFF, 1, 152, 0, 0, 0, 0, 0];
let info = parse_version_bytes(&old_255);
assert!(
info.min_compatible.is_none(),
"byte[0]=0xFF but byte[7]=0 → old format"
);
assert_eq!(info.version, (0xFF, 1, 152));
}
#[test]
fn test_both_old_format_always_compatible() {
let a = [0u8, 1, 152, 0, 0, 0, 0, 0];
let b = [0u8, 1, 151, 0, 0, 0, 0, 0];
assert!(is_compatible(&a, &b).is_ok());
assert!(is_compatible(&b, &a).is_ok());
}
#[test]
fn test_cross_major_version_always_incompatible() {
let v1 = encode_new_format("1.0.5", "1.0.3");
let v0 = encode_new_format("0.1.200", "0.1.150");
assert!(
is_compatible(&v1, &v0).is_err(),
"v1 should reject v0 (cross-major incompatible)"
);
assert!(
is_compatible(&v0, &v1).is_err(),
"v0 should also reject v1 (v0's version < v1's min)"
);
}
}
#[test]
fn test_atomic_handshake_completion_no_packet_loss() {
use super::ConnectionStateManager;
use crate::simulation::VirtualTime;
use crate::transport::packet_data::{PacketData, UnknownEncryption};
use std::net::SocketAddr;
use tokio::net::UdpSocket;
use tokio::sync::mpsc;
let time = VirtualTime::new();
let mut manager: ConnectionStateManager<UdpSocket, VirtualTime> =
ConnectionStateManager::new(time);
let addr: SocketAddr = "127.0.0.1:8000".parse().unwrap();
let (tx, _rx) = mpsc::channel::<PacketData<UnknownEncryption>>(16);
let started = manager.start_gateway_handshake(addr, tx);
assert!(started, "Should start gateway handshake");
assert!(
manager.has_gateway_handshake(&addr),
"Should be in GatewayHandshake state"
);
let (established_tx, _established_rx) = mpsc::channel::<PacketData<UnknownEncryption>>(16);
let completed = manager.complete_gateway_handshake(addr, established_tx);
assert!(completed, "Should complete gateway handshake");
assert!(
manager.is_established(&addr),
"State must be Established immediately after completion - proves no race gap"
);
assert!(
manager.get_state(&addr).is_some(),
"get_state() must return Some immediately after transition"
);
}
#[test]
fn test_recently_closed_prevents_asymmetric_decryption() {
use super::{ConnectionState, ConnectionStateManager, RECENTLY_CLOSED_DURATION};
use crate::simulation::VirtualTime;
use crate::transport::packet_data::{PacketData, UnknownEncryption};
use std::net::SocketAddr;
use std::time::Duration;
use tokio::net::UdpSocket;
use tokio::sync::mpsc;
let time = VirtualTime::new();
let mut manager: ConnectionStateManager<UdpSocket, VirtualTime> =
ConnectionStateManager::new(time.clone());
let addr: SocketAddr = "127.0.0.1:8001".parse().unwrap();
let (tx, _rx) = mpsc::channel::<PacketData<UnknownEncryption>>(16);
manager.start_gateway_handshake(addr, tx);
let (established_tx, _established_rx) = mpsc::channel::<PacketData<UnknownEncryption>>(16);
manager.complete_gateway_handshake(addr, established_tx);
assert!(manager.is_established(&addr));
manager.mark_closed(&addr);
let state = manager.get_state(&addr);
assert!(
state.is_some(),
"State should exist as RecentlyClosed, not be removed"
);
assert!(
matches!(state, Some(ConnectionState::RecentlyClosed { .. })),
"State must be RecentlyClosed - prevents packets from falling through"
);
assert!(!manager.is_established(&addr), "Should not be Established");
assert!(
manager.get_state(&addr).is_some(),
"Should NOT return None - that would trigger asymmetric handler"
);
time.advance(RECENTLY_CLOSED_DURATION + Duration::from_millis(1));
manager.cleanup_expired();
assert!(
manager.get_state(&addr).is_none(),
"State should be cleaned up after expiration"
);
}
#[test]
fn test_short_asym_intro_packet_does_not_panic() {
use super::{ASYM_INTRO_MIN_LEN, MAX_PACKET_SIZE, PROTOC_VERSION, split_asym_intro_payload};
use crate::transport::crypto::TransportKeypair;
use crate::transport::packet_data::{PacketData, UnknownEncryption};
for len in 0..ASYM_INTRO_MIN_LEN {
let payload = vec![0u8; len];
assert!(
split_asym_intro_payload(&payload).is_none(),
"payload of len {len} (< {ASYM_INTRO_MIN_LEN}) must be rejected, not sliced"
);
}
let exact: Vec<u8> = (0..ASYM_INTRO_MIN_LEN as u8).collect();
let (protoc, key) =
split_asym_intro_payload(&exact).expect("minimum-length payload must parse");
assert_eq!(&protoc[..], &exact[..PROTOC_VERSION.len()]);
assert_eq!(&key[..], &exact[PROTOC_VERSION.len()..ASYM_INTRO_MIN_LEN]);
let long = vec![7u8; ASYM_INTRO_MIN_LEN + 32];
assert!(
split_asym_intro_payload(&long).is_some(),
"over-long payload must still parse"
);
let keypair = TransportKeypair::new();
for short_len in [0usize, 1, 7, PROTOC_VERSION.len(), ASYM_INTRO_MIN_LEN - 1] {
let short_payload = vec![0xABu8; short_len];
let encrypted = keypair.public().encrypt(&short_payload);
let packet = PacketData::<UnknownEncryption, MAX_PACKET_SIZE>::from_buf(&encrypted);
let decrypted = packet
.try_decrypt_asym(keypair.secret())
.expect("we encrypted to our own key, so decryption must succeed");
assert_eq!(
decrypted.data().len(),
short_len,
"decrypted payload should round-trip to the original short length"
);
assert!(
split_asym_intro_payload(decrypted.data()).is_none(),
"short asym intro payload (len {short_len}) must be rejected without panicking"
);
}
}
#[tokio::test]
async fn test_try_send_established_never_blocks_when_full() {
use super::ConnectionStateManager;
use super::mock_transport::MockSocket;
use crate::simulation::VirtualTime;
use crate::transport::packet_data::{PacketData, UnknownEncryption};
use std::net::SocketAddr;
use std::time::{Duration, Instant};
use tokio::sync::mpsc;
const CAPACITY: usize = 4;
const ATTEMPTS: usize = 1_000;
const BUDGET: Duration = Duration::from_secs(1);
let time = VirtualTime::new();
let mut manager: ConnectionStateManager<MockSocket, VirtualTime> =
ConnectionStateManager::new(time);
let addr: SocketAddr = "127.0.0.1:8002".parse().unwrap();
let (tx, _rx) = mpsc::channel::<PacketData<UnknownEncryption>>(CAPACITY);
let started = manager.start_gateway_handshake(addr, tx.clone());
assert!(started, "Should start gateway handshake");
let completed = manager.complete_gateway_handshake(addr, tx);
assert!(completed, "Should complete gateway handshake");
let make_packet = || PacketData::<UnknownEncryption>::from_buf([0u8; 32]);
let started_at = Instant::now();
let mut sent_count = 0;
let mut full_count = 0;
for _ in 0..ATTEMPTS {
match manager.try_send_established(&addr, make_packet()) {
Ok(true) => sent_count += 1,
Ok(false) => full_count += 1,
Err(()) => panic!("connection unexpectedly disconnected"),
}
}
let elapsed = started_at.elapsed();
assert!(
elapsed < BUDGET,
"{ATTEMPTS} try_send_established calls took {elapsed:?} (budget {BUDGET:?}) — \
the listener forwarding path must not block, regardless of receiver state"
);
assert!(
sent_count <= CAPACITY,
"Bounded channel (capacity {CAPACITY}) accepted {sent_count} messages with no consumer"
);
assert!(
full_count >= ATTEMPTS - CAPACITY,
"Expected at least {} Full results, got {full_count}",
ATTEMPTS - CAPACITY
);
}
#[tokio::test]
async fn test_try_send_gateway_handshake_never_blocks_when_full() {
use super::ConnectionStateManager;
use super::mock_transport::MockSocket;
use crate::simulation::VirtualTime;
use crate::transport::packet_data::{PacketData, UnknownEncryption};
use std::net::SocketAddr;
use std::time::{Duration, Instant};
use tokio::sync::mpsc;
const CAPACITY: usize = 4;
const ATTEMPTS: usize = 1_000;
const BUDGET: Duration = Duration::from_secs(1);
let time = VirtualTime::new();
let mut manager: ConnectionStateManager<MockSocket, VirtualTime> =
ConnectionStateManager::new(time);
let addr: SocketAddr = "127.0.0.1:8003".parse().unwrap();
let (tx, _rx) = mpsc::channel::<PacketData<UnknownEncryption>>(CAPACITY);
let started = manager.start_gateway_handshake(addr, tx);
assert!(started, "Should start gateway handshake");
let make_packet = || PacketData::<UnknownEncryption>::from_buf([0u8; 32]);
let started_at = Instant::now();
let mut full_count = 0;
for _ in 0..ATTEMPTS {
match manager.try_send_gateway_handshake(&addr, make_packet()) {
Ok(true) => {}
Ok(false) => full_count += 1,
Err(()) => panic!("handshake state unexpectedly missing"),
}
}
let elapsed = started_at.elapsed();
assert!(
elapsed < BUDGET,
"{ATTEMPTS} try_send_gateway_handshake calls took {elapsed:?} (budget {BUDGET:?})"
);
assert!(
full_count >= ATTEMPTS - CAPACITY,
"Expected at least {} Full results, got {full_count}",
ATTEMPTS - CAPACITY
);
}
#[tokio::test]
async fn test_try_send_nat_traversal_never_blocks_when_full() {
use super::ConnectionStateManager;
use super::mock_transport::MockSocket;
use crate::simulation::VirtualTime;
use crate::transport::packet_data::{PacketData, UnknownEncryption};
use std::net::SocketAddr;
use std::time::{Duration, Instant};
use tokio::sync::{mpsc, oneshot};
const CAPACITY: usize = 4;
const ATTEMPTS: usize = 1_000;
const BUDGET: Duration = Duration::from_secs(1);
let time = VirtualTime::new();
let mut manager: ConnectionStateManager<MockSocket, VirtualTime> =
ConnectionStateManager::new(time);
let addr: SocketAddr = "127.0.0.1:8004".parse().unwrap();
let (tx, _rx) = mpsc::channel::<PacketData<UnknownEncryption>>(CAPACITY);
let (result_tx, _result_rx) = oneshot::channel();
let started = manager.start_nat_traversal(addr, tx, result_tx);
assert!(started, "Should start NAT traversal");
let make_packet = || PacketData::<UnknownEncryption>::from_buf([0u8; 32]);
let started_at = Instant::now();
let mut full_count = 0;
for _ in 0..ATTEMPTS {
match manager.try_send_nat_traversal(&addr, make_packet()) {
Ok(super::TrySendNatTraversalOutcome::Sent) => {}
Ok(super::TrySendNatTraversalOutcome::Full) => full_count += 1,
Ok(super::TrySendNatTraversalOutcome::Closed) => {
panic!("receiver still alive; should not be Closed");
}
Err(()) => panic!("NAT traversal state unexpectedly missing"),
}
}
let elapsed = started_at.elapsed();
assert!(
elapsed < BUDGET,
"{ATTEMPTS} try_send_nat_traversal calls took {elapsed:?} (budget {BUDGET:?})"
);
assert!(
full_count >= ATTEMPTS - CAPACITY,
"Expected at least {} Full results, got {full_count}",
ATTEMPTS - CAPACITY
);
}
#[test]
fn test_gateway_connection_rate_limiter() {
use super::{
GW_RAMP_PHASE1_DURATION, GW_RAMP_PHASE1_RATE, GW_RAMP_PHASE2_DURATION, GW_RAMP_PHASE2_RATE,
GatewayConnectionRateLimiter,
};
use crate::simulation::VirtualTime;
use std::time::Duration;
let time = VirtualTime::new();
let mut limiter = GatewayConnectionRateLimiter::new(time.clone(), None);
for _ in 0..GW_RAMP_PHASE1_RATE {
assert!(limiter.try_accept(), "Should accept within phase 1 rate");
}
assert!(
!limiter.try_accept(),
"Should reject when phase 1 rate exceeded"
);
time.advance(Duration::from_millis(1001));
assert!(
limiter.try_accept(),
"Should accept after window reset in phase 1"
);
time.advance(GW_RAMP_PHASE1_DURATION);
assert_eq!(
limiter.current_rate_limit(),
Some(GW_RAMP_PHASE2_RATE),
"Should be in phase 2 after advancing past phase 1 duration"
);
for _ in 0..GW_RAMP_PHASE2_RATE {
assert!(limiter.try_accept(), "Should accept within phase 2 rate");
}
assert!(
!limiter.try_accept(),
"Should reject when phase 2 rate exceeded"
);
time.advance(GW_RAMP_PHASE2_DURATION);
assert_eq!(
limiter.current_rate_limit(),
None,
"Should be unlimited after phase 2 duration"
);
for _ in 0..1000 {
assert!(limiter.try_accept(), "Should accept unlimited in phase 3");
}
}
#[test]
fn test_gateway_rate_limiter_admission_clock_is_max_of_real_and_virtual() {
use super::{GW_RAMP_PHASE1_RATE, GW_RAMP_PHASE2_RATE, GatewayConnectionRateLimiter};
use crate::simulation::VirtualTime;
use std::time::Duration;
fn drive(advance: impl Fn(&mut GatewayConnectionRateLimiter<VirtualTime>)) {
let real = VirtualTime::new();
let virtual_clock = VirtualTime::new();
let mut limiter = GatewayConnectionRateLimiter::new(real, Some(virtual_clock));
for _ in 0..GW_RAMP_PHASE1_RATE {
assert!(limiter.try_accept(), "phase 1 admits within rate");
}
assert!(
!limiter.try_accept(),
"phase 1 rate exhausted (no clock advanced)"
);
advance(&mut limiter);
}
fn adv_virtual(l: &mut GatewayConnectionRateLimiter<VirtualTime>, d: Duration) {
l.virtual_clock.as_ref().unwrap().advance(d);
}
fn adv_real(l: &mut GatewayConnectionRateLimiter<VirtualTime>, d: Duration) {
l.time_source.advance(d);
}
drive(|l| {
adv_virtual(l, Duration::from_millis(1001));
assert!(l.try_accept(), "virtual-clock advance resets the window");
adv_virtual(l, Duration::from_secs(31));
assert_eq!(
l.current_rate_limit(),
Some(GW_RAMP_PHASE2_RATE),
"virtual-clock advance ramps into phase 2"
);
});
drive(|l| {
adv_real(l, Duration::from_millis(1001));
assert!(l.try_accept(), "real-clock advance resets the window");
adv_real(l, Duration::from_secs(31));
assert_eq!(
l.current_rate_limit(),
Some(GW_RAMP_PHASE2_RATE),
"real-clock advance ramps into phase 2"
);
});
drive(|l| {
adv_real(l, Duration::from_secs(20));
adv_virtual(l, Duration::from_secs(20));
assert_eq!(
l.current_rate_limit(),
Some(GW_RAMP_PHASE1_RATE),
"effective elapsed is max(20s, 20s) = 20s (phase 1), not the 40s sum"
);
});
}