refactor(irtt-client): move cadence into managed runner
This commit is contained in:
31 files changed
+716
-4106
No files matched your search
@@ -17,191 +17,6 @@ fn open_fails_after_close() {
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_sends_one_close_packet_with_negotiated_token() {
|
||||
let config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
||||
let params = params_from_config(&config).unwrap();
|
||||
let server = start_fake_server(move |socket, tx| {
|
||||
let (_, peer) = recv_request(&socket, &tx);
|
||||
let reply = open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None);
|
||||
socket.send_to(&reply, peer).unwrap();
|
||||
let _ = recv_request(&socket, &tx);
|
||||
});
|
||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
||||
assert_open_started(client.open().unwrap());
|
||||
let sent_at = ClientTimestamp {
|
||||
wall: SystemTime::UNIX_EPOCH + Duration::from_secs(1_234),
|
||||
mono: Instant::now() + Duration::from_secs(5),
|
||||
};
|
||||
client.test_hooks.close_sent_at.set(Some(sent_at));
|
||||
let events = client.close().unwrap();
|
||||
assert!(matches!(
|
||||
events.as_slice(),
|
||||
[ClientEvent::SessionClosed {
|
||||
token: TOKEN,
|
||||
at,
|
||||
..
|
||||
}] if *at == sent_at
|
||||
));
|
||||
assert!(!client.runtime.is_open());
|
||||
assert!(client.schedule.is_none());
|
||||
assert_eq!(client.applied_traffic_class, None);
|
||||
let packets: Vec<_> = server.rx.iter().take(2).collect();
|
||||
let close = &packets[1];
|
||||
assert_eq!(close[3], flags::FLAG_CLOSE);
|
||||
assert_eq!(u64::from_le_bytes(close[4..12].try_into().unwrap()), TOKEN);
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_event_reservation_failure_precedes_dscp_clear_and_send() {
|
||||
let params = default_params();
|
||||
let server = start_fake_server(move |socket, tx| {
|
||||
let (_, peer) = recv_request(&socket, &tx);
|
||||
socket
|
||||
.send_to(
|
||||
&open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None),
|
||||
peer,
|
||||
)
|
||||
.unwrap();
|
||||
let _ = recv_request(&socket, &tx);
|
||||
});
|
||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
||||
assert_open_started(client.open().unwrap());
|
||||
client.test_hooks.fail_close_event_reserve.set(true);
|
||||
client.test_hooks.fail_close_dscp_clear.set(true);
|
||||
|
||||
assert!(matches!(
|
||||
client.close(),
|
||||
Err(ClientError::AllocationFailed {
|
||||
operation: "close event result",
|
||||
..
|
||||
})
|
||||
));
|
||||
assert_eq!(client.test_hooks.close_send_attempts.get(), 0);
|
||||
assert!(client.test_hooks.fail_close_dscp_clear.get());
|
||||
assert!(client.runtime.is_open());
|
||||
assert!(client.schedule.is_some());
|
||||
assert_eq!(client.applied_traffic_class, Some(0));
|
||||
|
||||
client.test_hooks.fail_close_dscp_clear.set(false);
|
||||
client.close().unwrap();
|
||||
assert_eq!(client.test_hooks.close_send_attempts.get(), 1);
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_dscp_clear_failure_precedes_send_and_preserves_open_state() {
|
||||
let params = default_params();
|
||||
let server = start_fake_server(move |socket, tx| {
|
||||
let (_, peer) = recv_request(&socket, &tx);
|
||||
socket
|
||||
.send_to(
|
||||
&open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None),
|
||||
peer,
|
||||
)
|
||||
.unwrap();
|
||||
let _ = recv_request(&socket, &tx);
|
||||
});
|
||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
||||
assert_open_started(client.open().unwrap());
|
||||
client.test_hooks.fail_close_dscp_clear.set(true);
|
||||
|
||||
assert!(matches!(
|
||||
client.close(),
|
||||
Err(ClientError::SocketOption { .. })
|
||||
));
|
||||
assert_eq!(client.test_hooks.close_send_attempts.get(), 0);
|
||||
assert!(client.runtime.is_open());
|
||||
assert!(client.schedule.is_some());
|
||||
assert_eq!(client.applied_traffic_class, Some(0));
|
||||
|
||||
client.close().unwrap();
|
||||
assert_eq!(client.test_hooks.close_send_attempts.get(), 1);
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_send_failure_leaves_machine_and_schedule_open_for_retry() {
|
||||
let params = default_params();
|
||||
let server = start_fake_server(move |socket, tx| {
|
||||
let (_, peer) = recv_request(&socket, &tx);
|
||||
socket
|
||||
.send_to(
|
||||
&open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None),
|
||||
peer,
|
||||
)
|
||||
.unwrap();
|
||||
let _ = recv_request(&socket, &tx);
|
||||
});
|
||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
||||
assert_open_started(client.open().unwrap());
|
||||
let sent_at = ClientTimestamp {
|
||||
wall: SystemTime::UNIX_EPOCH + Duration::from_secs(2_345),
|
||||
mono: Instant::now() + Duration::from_secs(6),
|
||||
};
|
||||
client.test_hooks.close_sent_at.set(Some(sent_at));
|
||||
client.test_hooks.fail_close_send.set(true);
|
||||
|
||||
assert!(matches!(client.close(), Err(ClientError::Socket(_))));
|
||||
assert_eq!(client.test_hooks.close_sent_at.get(), Some(sent_at));
|
||||
assert!(client.runtime.is_open());
|
||||
assert!(client.schedule.is_some());
|
||||
assert_eq!(client.applied_traffic_class, Some(0));
|
||||
assert_eq!(client.test_hooks.close_send_attempts.get(), 1);
|
||||
|
||||
assert!(matches!(
|
||||
client.close().unwrap().as_slice(),
|
||||
[ClientEvent::SessionClosed {
|
||||
token: TOKEN,
|
||||
at,
|
||||
..
|
||||
}] if *at == sent_at
|
||||
));
|
||||
assert_eq!(client.test_hooks.close_sent_at.get(), None);
|
||||
assert_eq!(client.test_hooks.close_send_attempts.get(), 2);
|
||||
assert!(matches!(client.close(), Err(ClientError::AlreadyClosed)));
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_successful_close_commits_before_length_mismatch() {
|
||||
let params = default_params();
|
||||
let server = start_fake_server(move |socket, tx| {
|
||||
let (_, peer) = recv_request(&socket, &tx);
|
||||
socket
|
||||
.send_to(
|
||||
&open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None),
|
||||
peer,
|
||||
)
|
||||
.unwrap();
|
||||
let _ = recv_request(&socket, &tx);
|
||||
});
|
||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
||||
assert_open_started(client.open().unwrap());
|
||||
let expected = client.runtime.prepare_close().unwrap().bytes.len();
|
||||
let sent_at = ClientTimestamp {
|
||||
wall: SystemTime::UNIX_EPOCH + Duration::from_secs(3_456),
|
||||
mono: Instant::now() + Duration::from_secs(7),
|
||||
};
|
||||
client.test_hooks.close_sent_at.set(Some(sent_at));
|
||||
client.test_hooks.close_reported_len.set(Some(expected - 1));
|
||||
|
||||
assert!(matches!(
|
||||
client.close(),
|
||||
Err(ClientError::DatagramLengthMismatch {
|
||||
expected: error_expected,
|
||||
actual,
|
||||
}) if error_expected == expected && actual + 1 == expected
|
||||
));
|
||||
assert!(!client.runtime.is_open());
|
||||
assert!(client.schedule.is_none());
|
||||
assert_eq!(client.applied_traffic_class, None);
|
||||
assert_eq!(client.test_hooks.close_sent_at.get(), None);
|
||||
assert!(matches!(client.close(), Err(ClientError::AlreadyClosed)));
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_probe_fails_after_close() {
|
||||
let params = default_params();
|
||||
@@ -232,135 +47,6 @@ fn send_probe_fails_after_close() {
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_flagged_echo_reply_emits_reply_then_closes_without_sending_close() {
|
||||
let params = default_params();
|
||||
let server = start_fake_server({
|
||||
let params = params.clone();
|
||||
move |socket, tx| {
|
||||
let (_, peer) = recv_request(&socket, &tx);
|
||||
let reply = open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None);
|
||||
socket.send_to(&reply, peer).unwrap();
|
||||
|
||||
let (request, _) = recv_request(&socket, &tx);
|
||||
let seq = u32::from_le_bytes(request[12..16].try_into().unwrap());
|
||||
let reply = echo_reply_packet_with_flags(
|
||||
TOKEN,
|
||||
seq,
|
||||
¶ms,
|
||||
&TimestampFields::default(),
|
||||
None,
|
||||
FLAG_REPLY | flags::FLAG_CLOSE,
|
||||
);
|
||||
socket.send_to(&reply, peer).unwrap();
|
||||
|
||||
socket
|
||||
.set_read_timeout(Some(Duration::from_millis(250)))
|
||||
.unwrap();
|
||||
while recv_request_timeout(&socket, &tx).is_some() {}
|
||||
}
|
||||
});
|
||||
let config = ClientConfig {
|
||||
socket_config: crate::SocketConfig {
|
||||
recv_timeout: Some(Duration::from_millis(200)),
|
||||
..Default::default()
|
||||
},
|
||||
..default_test_config(server.addr)
|
||||
};
|
||||
let mut client = Client::connect(config).unwrap();
|
||||
assert_open_started(client.open().unwrap());
|
||||
client.send_probe().unwrap();
|
||||
|
||||
let events = client.recv_once().unwrap();
|
||||
assert!(matches!(
|
||||
events.first(),
|
||||
Some(ClientEvent::EchoReply { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
events.get(1),
|
||||
Some(ClientEvent::SessionClosed { token: TOKEN, .. })
|
||||
));
|
||||
assert_eq!(events.len(), 2);
|
||||
assert!(client.next_send_deadline().is_none());
|
||||
assert!(client.schedule.is_none());
|
||||
assert!(matches!(
|
||||
client.send_probe(),
|
||||
Err(ClientError::AlreadyClosed)
|
||||
));
|
||||
|
||||
let first = server.rx.recv_timeout(Duration::from_millis(100)).unwrap();
|
||||
let second = server.rx.recv_timeout(Duration::from_millis(100)).unwrap();
|
||||
assert_eq!(first[3] & FLAG_OPEN, FLAG_OPEN);
|
||||
assert_eq!(second[3] & flags::FLAG_CLOSE, 0);
|
||||
assert!(server.rx.recv_timeout(Duration::from_millis(400)).is_err());
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peer_close_dscp_cleanup_failure_preserves_events_and_closed_state() {
|
||||
let params = default_params();
|
||||
let server = start_fake_server({
|
||||
let params = params.clone();
|
||||
move |socket, tx| {
|
||||
let (_, peer) = recv_request(&socket, &tx);
|
||||
socket
|
||||
.send_to(
|
||||
&open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None),
|
||||
peer,
|
||||
)
|
||||
.unwrap();
|
||||
let (request, _) = recv_request(&socket, &tx);
|
||||
let seq = u32::from_le_bytes(request[12..16].try_into().unwrap());
|
||||
socket
|
||||
.send_to(
|
||||
&echo_reply_packet_with_flags(
|
||||
TOKEN,
|
||||
seq,
|
||||
¶ms,
|
||||
&TimestampFields::default(),
|
||||
None,
|
||||
FLAG_REPLY | flags::FLAG_CLOSE,
|
||||
),
|
||||
peer,
|
||||
)
|
||||
.unwrap();
|
||||
socket
|
||||
.set_read_timeout(Some(Duration::from_millis(250)))
|
||||
.unwrap();
|
||||
while recv_request_timeout(&socket, &tx).is_some() {}
|
||||
}
|
||||
});
|
||||
let mut client = Client::connect(ClientConfig {
|
||||
socket_config: crate::SocketConfig {
|
||||
recv_timeout: Some(Duration::from_millis(200)),
|
||||
..Default::default()
|
||||
},
|
||||
..default_test_config(server.addr)
|
||||
})
|
||||
.unwrap();
|
||||
assert_open_started(client.open().unwrap());
|
||||
client.send_probe().unwrap();
|
||||
client.test_hooks.fail_close_dscp_clear.set(true);
|
||||
|
||||
assert!(matches!(
|
||||
client.recv_once().unwrap().as_slice(),
|
||||
[
|
||||
ClientEvent::EchoReply { .. },
|
||||
ClientEvent::SessionClosed { token: TOKEN, .. }
|
||||
]
|
||||
));
|
||||
assert!(client.is_peer_closed());
|
||||
assert!(client.schedule.is_none());
|
||||
assert_eq!(client.applied_traffic_class, Some(0));
|
||||
assert_eq!(client.test_hooks.close_send_attempts.get(), 0);
|
||||
let open = server.rx.recv_timeout(Duration::from_millis(100)).unwrap();
|
||||
let echo = server.rx.recv_timeout(Duration::from_millis(100)).unwrap();
|
||||
assert_ne!(open[3] & FLAG_OPEN, 0);
|
||||
assert_eq!(echo[3] & flags::FLAG_CLOSE, 0);
|
||||
assert!(server.rx.recv_timeout(Duration::from_millis(400)).is_err());
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recv_available_stops_after_peer_close() {
|
||||
let params = default_params();
|
||||
@@ -415,7 +101,6 @@ fn normal_echo_reply_does_not_close_session() {
|
||||
|
||||
let events = client.recv_once().unwrap();
|
||||
assert!(matches!(events.as_slice(), [ClientEvent::EchoReply { .. }]));
|
||||
assert!(client.next_send_deadline().is_some());
|
||||
assert!(client.send_probe().is_ok());
|
||||
|
||||
client.close().unwrap();
|
||||
@@ -752,3 +437,66 @@ fn wrong_token_close_flag_does_not_close_session() {
|
||||
client.close().unwrap();
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_flagged_echo_reply_emits_reply_then_closes_without_sending_close() {
|
||||
let params = default_params();
|
||||
let server = start_fake_server({
|
||||
let params = params.clone();
|
||||
move |socket, tx| {
|
||||
let (_, peer) = recv_request(&socket, &tx);
|
||||
let reply = open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None);
|
||||
socket.send_to(&reply, peer).unwrap();
|
||||
|
||||
let (request, _) = recv_request(&socket, &tx);
|
||||
let seq = u32::from_le_bytes(request[12..16].try_into().unwrap());
|
||||
let reply = echo_reply_packet_with_flags(
|
||||
TOKEN,
|
||||
seq,
|
||||
¶ms,
|
||||
&TimestampFields::default(),
|
||||
None,
|
||||
FLAG_REPLY | flags::FLAG_CLOSE,
|
||||
);
|
||||
socket.send_to(&reply, peer).unwrap();
|
||||
|
||||
socket
|
||||
.set_read_timeout(Some(Duration::from_millis(250)))
|
||||
.unwrap();
|
||||
while recv_request_timeout(&socket, &tx).is_some() {}
|
||||
}
|
||||
});
|
||||
let config = ClientConfig {
|
||||
socket_config: crate::SocketConfig {
|
||||
recv_timeout: Some(Duration::from_millis(200)),
|
||||
..Default::default()
|
||||
},
|
||||
..default_test_config(server.addr)
|
||||
};
|
||||
let mut client = Client::connect(config).unwrap();
|
||||
assert_open_started(client.open().unwrap());
|
||||
client.send_probe().unwrap();
|
||||
|
||||
let events = client.recv_once().unwrap();
|
||||
assert!(matches!(
|
||||
events.first(),
|
||||
Some(ClientEvent::EchoReply { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
events.get(1),
|
||||
Some(ClientEvent::SessionClosed { token: TOKEN, .. })
|
||||
));
|
||||
assert_eq!(events.len(), 2);
|
||||
|
||||
assert!(matches!(
|
||||
client.send_probe(),
|
||||
Err(ClientError::AlreadyClosed)
|
||||
));
|
||||
|
||||
let first = server.rx.recv_timeout(Duration::from_millis(100)).unwrap();
|
||||
let second = server.rx.recv_timeout(Duration::from_millis(100)).unwrap();
|
||||
assert_eq!(first[3] & FLAG_OPEN, FLAG_OPEN);
|
||||
assert_eq!(second[3] & flags::FLAG_CLOSE, 0);
|
||||
assert!(server.rx.recv_timeout(Duration::from_millis(400)).is_err());
|
||||
server.join();
|
||||
}
|
||||
@@ -82,80 +82,9 @@ fn normal_open_uses_negotiated_traffic_class_not_requested_dscp() {
|
||||
target_os = "redox",
|
||||
target_os = "solaris",
|
||||
target_os = "illumos",
|
||||
target_os = "haiku",
|
||||
target_os = "haiku"
|
||||
)))]
|
||||
fn failed_close_send_restores_negotiated_traffic_class_and_keeps_session_open() {
|
||||
let mut params = default_params();
|
||||
// Raw wire TOS/Traffic Class byte for DSCP codepoint 46 (EF).
|
||||
params.dscp = 184;
|
||||
let server = start_fake_server(move |socket, tx| {
|
||||
let (_, peer) = recv_request(&socket, &tx);
|
||||
socket
|
||||
.send_to(
|
||||
&open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None),
|
||||
peer,
|
||||
)
|
||||
.unwrap();
|
||||
let _ = recv_request(&socket, &tx);
|
||||
});
|
||||
let mut config = default_test_config(server.addr);
|
||||
config.dscp = 46;
|
||||
let mut client = Client::connect(config).unwrap();
|
||||
assert_open_started(client.open().unwrap());
|
||||
client.test_hooks.fail_close_send.set(true);
|
||||
|
||||
let error = client.close().unwrap_err();
|
||||
|
||||
assert!(matches!(error, ClientError::Socket(_)));
|
||||
assert!(client.runtime.is_open());
|
||||
assert!(client.schedule.is_some());
|
||||
assert_eq!(client.applied_traffic_class, Some(184));
|
||||
assert_eq!(
|
||||
socket_traffic_class(&client.socket, client.remote).unwrap() & 0xfc,
|
||||
184
|
||||
);
|
||||
client.close().unwrap();
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_close_keeps_send_error_primary_when_dscp_restoration_also_fails() {
|
||||
let params = default_params();
|
||||
let server = start_fake_server(move |socket, tx| {
|
||||
let (_, peer) = recv_request(&socket, &tx);
|
||||
socket
|
||||
.send_to(
|
||||
&open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None),
|
||||
peer,
|
||||
)
|
||||
.unwrap();
|
||||
let _ = recv_request(&socket, &tx);
|
||||
});
|
||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
||||
assert_open_started(client.open().unwrap());
|
||||
client.test_hooks.fail_close_send.set(true);
|
||||
client.test_hooks.fail_dscp_restore.set(true);
|
||||
|
||||
let error = client.close().unwrap_err();
|
||||
|
||||
assert!(matches!(error, ClientError::Socket(_)));
|
||||
assert!(error.to_string().contains("injected close send failure"));
|
||||
assert!(client.runtime.is_open());
|
||||
assert!(client.schedule.is_some());
|
||||
assert_eq!(client.applied_traffic_class, Some(0));
|
||||
client.close().unwrap();
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(any(
|
||||
target_os = "fuchsia",
|
||||
target_os = "redox",
|
||||
target_os = "solaris",
|
||||
target_os = "illumos",
|
||||
target_os = "haiku",
|
||||
)))]
|
||||
fn authenticated_peer_close_clears_schedule_and_negotiated_traffic_class() {
|
||||
fn authenticated_peer_close_clears_negotiated_traffic_class() {
|
||||
let mut params = default_params();
|
||||
// Raw wire TOS/Traffic Class byte for DSCP codepoint 46 (EF).
|
||||
params.dscp = 184;
|
||||
@@ -198,7 +127,7 @@ fn authenticated_peer_close_clears_schedule_and_negotiated_traffic_class() {
|
||||
ClientEvent::SessionClosed { .. }
|
||||
]
|
||||
));
|
||||
assert!(client.schedule.is_none());
|
||||
|
||||
assert_eq!(client.applied_traffic_class, None);
|
||||
assert_eq!(
|
||||
socket_traffic_class(&client.socket, client.remote).unwrap(),
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
use super::*;
|
||||
use crate::config::{NegotiationPolicy, DEFAULT_OPEN_TIMEOUTS};
|
||||
use crate::{
|
||||
session::machine::{
|
||||
compute_one_way, compute_rtt, params_from_config, sequence_is_after, sequence_is_before,
|
||||
unix_epoch_ns_i64, update_highest_received,
|
||||
},
|
||||
session::negotiate_params,
|
||||
NegotiatedParams, RunMode, SignedDuration, WarningKind, MAX_UDP_PAYLOAD_LENGTH,
|
||||
};
|
||||
use irtt_proto::{flags, Params, TimestampFields, PROTOCOL_VERSION};
|
||||
use irtt_proto::{
|
||||
flags::FLAG_HMAC, flags::FLAG_OPEN, flags::FLAG_REPLY, verify_packet_hmac, Clock,
|
||||
ReceivedStats, StampAt, HMAC_SIZE, MAGIC,
|
||||
|
||||
@@ -1,22 +1,5 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn no_test_success_validates_params() {
|
||||
let mut config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
||||
config.run_mode = RunMode::NoTest;
|
||||
let params = params_from_config(&config).unwrap();
|
||||
let server = no_test_server(params.clone(), 0);
|
||||
config.server_addr = server.addr.to_string();
|
||||
let mut client = Client::connect(config).unwrap();
|
||||
assert!(client.next_send_deadline().is_none());
|
||||
let negotiated = assert_no_test_completed(client.open().unwrap());
|
||||
assert_eq!(negotiated.params, params);
|
||||
assert!(client.next_send_deadline().is_none());
|
||||
assert!(client.schedule.is_none());
|
||||
assert_eq!(client.applied_traffic_class, None);
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_test_rejects_non_close_open_reply() {
|
||||
let mut config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
||||
@@ -48,32 +31,6 @@ fn no_test_rejects_non_close_open_reply() {
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_test_cleanup_send_failure_preserves_unexpected_reply() {
|
||||
let mut config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
||||
config.run_mode = RunMode::NoTest;
|
||||
let params = params_from_config(&config).unwrap();
|
||||
let server = start_fake_server(move |socket, tx| {
|
||||
let (_, peer) = recv_request(&socket, &tx);
|
||||
socket
|
||||
.send_to(
|
||||
&open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None),
|
||||
peer,
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
config.server_addr = server.addr.to_string();
|
||||
let mut client = Client::connect(config).unwrap();
|
||||
client.test_hooks.fail_cleanup_send.set(true);
|
||||
|
||||
assert!(matches!(
|
||||
client.open(),
|
||||
Err(ClientError::UnexpectedNoTestReply)
|
||||
));
|
||||
assert!(client.runtime.prepare_open_request().is_ok());
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_test_rejects_non_zero_token_with_close_reply() {
|
||||
let mut config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
||||
@@ -156,3 +113,19 @@ fn open_fails_after_no_test_completed() {
|
||||
assert!(matches!(client.open(), Err(ClientError::AlreadyCompleted)));
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_test_success_validates_params() {
|
||||
let mut config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
||||
config.run_mode = RunMode::NoTest;
|
||||
let params = params_from_config(&config).unwrap();
|
||||
let server = no_test_server(params.clone(), 0);
|
||||
config.server_addr = server.addr.to_string();
|
||||
let mut client = Client::connect(config).unwrap();
|
||||
|
||||
let negotiated = assert_no_test_completed(client.open().unwrap());
|
||||
assert_eq!(negotiated.params, params);
|
||||
|
||||
assert_eq!(client.applied_traffic_class, None);
|
||||
server.join();
|
||||
}
|
||||
@@ -1,37 +1,5 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn successful_open_handshake() {
|
||||
let config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
||||
let params = params_from_config(&config).unwrap();
|
||||
let server = open_success_server(params.clone());
|
||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
||||
|
||||
let negotiated = assert_open_started(client.open().unwrap());
|
||||
assert_eq!(negotiated.params, params);
|
||||
assert!(client.schedule.is_some());
|
||||
assert_eq!(client.applied_traffic_class, Some(0));
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn successful_open_restores_configured_receive_timeout() {
|
||||
let config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
||||
let params = params_from_config(&config).unwrap();
|
||||
let server = open_success_server(params);
|
||||
let mut config = default_test_config(server.addr);
|
||||
config.socket_config.recv_timeout = Some(Duration::from_millis(75));
|
||||
let mut client = Client::connect(config).unwrap();
|
||||
|
||||
assert_open_started(client.open().unwrap());
|
||||
|
||||
assert_eq!(
|
||||
client.test_hooks.last_restored_read_timeout.get(),
|
||||
Some(Duration::from_millis(75))
|
||||
);
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_fails_when_already_open() {
|
||||
let config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
||||
@@ -64,84 +32,6 @@ fn open_retries_after_first_timeout() {
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_survives_interrupted_receive_within_same_attempt() {
|
||||
let server = start_fake_server(|socket, tx| {
|
||||
let (first, peer) = recv_request(&socket, &tx);
|
||||
let params = Params::decode(&first[4..]).unwrap();
|
||||
let reply = open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None);
|
||||
socket.send_to(&reply, peer).unwrap();
|
||||
});
|
||||
let config = ClientConfig {
|
||||
open_timeouts: vec![Duration::from_millis(500)],
|
||||
..default_test_config(server.addr)
|
||||
};
|
||||
let mut client = Client::connect(config).unwrap();
|
||||
// Several interrupted receive attempts before the real reply arrives
|
||||
// must not be treated as a timeout, must not retransmit the open
|
||||
// request, and must not advance to a next configured attempt.
|
||||
client.test_hooks.inject_recv_interrupted.set(5);
|
||||
|
||||
let outcome = client.open().unwrap();
|
||||
assert_open_started(outcome);
|
||||
// Exactly one request reached the server: interruptions were absorbed
|
||||
// inside the single configured attempt.
|
||||
assert_eq!(server.rx.iter().take(1).count(), 1);
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_interrupted_receive_does_not_extend_the_attempt_deadline() {
|
||||
// A fake server that never replies: with a short attempt timeout, the
|
||||
// real question is whether interruptions can push the effective wait
|
||||
// past the configured deadline. They must not: the deadline stays
|
||||
// absolute across any number of interruptions.
|
||||
let server = start_fake_server(|socket, tx| {
|
||||
let _ = recv_request(&socket, &tx);
|
||||
});
|
||||
let config = ClientConfig {
|
||||
open_timeouts: vec![Duration::from_millis(200)],
|
||||
..default_test_config(server.addr)
|
||||
};
|
||||
let mut client = Client::connect(config).unwrap();
|
||||
// Each interruption carries no timing information of its own, so the
|
||||
// loop must keep re-checking the real deadline rather than treating an
|
||||
// interruption as "more time available" or a reason to fail outright.
|
||||
client.test_hooks.inject_recv_interrupted.set(20);
|
||||
|
||||
let started = Instant::now();
|
||||
let result = client.open();
|
||||
let elapsed = started.elapsed();
|
||||
|
||||
assert!(matches!(result, Err(ClientError::OpenTimeout)));
|
||||
assert!(
|
||||
elapsed < Duration::from_millis(800),
|
||||
"interrupted receives must not extend the attempt deadline, took {elapsed:?}"
|
||||
);
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_timeout_after_all_timeouts() {
|
||||
let server = timeout_server(Duration::from_millis(700));
|
||||
let config = ClientConfig {
|
||||
open_timeouts: vec![Duration::from_millis(200), Duration::from_millis(200)],
|
||||
socket_config: crate::SocketConfig {
|
||||
recv_timeout: Some(Duration::from_millis(50)),
|
||||
..Default::default()
|
||||
},
|
||||
..default_test_config(server.addr)
|
||||
};
|
||||
let mut client = Client::connect(config).unwrap();
|
||||
assert!(matches!(client.open(), Err(ClientError::OpenTimeout)));
|
||||
assert_eq!(server.rx.iter().take(2).count(), 2);
|
||||
assert_eq!(
|
||||
client.test_hooks.last_restored_read_timeout.get(),
|
||||
Some(Duration::from_millis(50))
|
||||
);
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn protocol_version_mismatch_fails() {
|
||||
let mut config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
||||
@@ -175,41 +65,6 @@ fn server_rejection_fails_in_normal_mode() {
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_untrusted_datagrams_before_valid_reply_use_one_attempt() {
|
||||
let params = default_params();
|
||||
let server = start_fake_server(move |socket, tx| {
|
||||
let (_, peer) = recv_request(&socket, &tx);
|
||||
let valid = open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None);
|
||||
let mut wrong_type = valid.clone();
|
||||
wrong_type[3] = FLAG_REPLY;
|
||||
let mut reserved_flags = valid.clone();
|
||||
reserved_flags[3] |= 0x10;
|
||||
let mut bad_magic = valid.clone();
|
||||
bad_magic[0] ^= 0xff;
|
||||
let unexpected_hmac =
|
||||
open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, Some(b"unexpected"));
|
||||
|
||||
for packet in [
|
||||
vec![0_u8],
|
||||
bad_magic,
|
||||
wrong_type,
|
||||
reserved_flags,
|
||||
unexpected_hmac,
|
||||
valid,
|
||||
] {
|
||||
socket.send_to(&packet, peer).unwrap();
|
||||
}
|
||||
});
|
||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
||||
|
||||
assert_open_started(client.open().unwrap());
|
||||
assert_eq!(server.rx.iter().take(1).count(), 1);
|
||||
assert!(server.rx.try_recv().is_err());
|
||||
assert!(client.schedule.is_some());
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignored_datagrams_do_not_restart_the_attempt_deadline() {
|
||||
let server = start_fake_server(move |socket, tx| {
|
||||
@@ -237,60 +92,6 @@ fn ignored_datagrams_do_not_restart_the_attempt_deadline() {
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_ignored_open_datagrams_eventually_time_out() {
|
||||
let server = start_fake_server(move |socket, tx| {
|
||||
for _ in 0..2 {
|
||||
let (_, peer) = recv_request(&socket, &tx);
|
||||
socket.send_to(&[0_u8], peer).unwrap();
|
||||
socket.send_to(&MAGIC, peer).unwrap();
|
||||
}
|
||||
thread::sleep(Duration::from_millis(300));
|
||||
});
|
||||
let config = ClientConfig {
|
||||
open_timeouts: vec![Duration::from_millis(200), Duration::from_millis(200)],
|
||||
socket_config: crate::SocketConfig {
|
||||
recv_timeout: Some(Duration::from_millis(50)),
|
||||
..Default::default()
|
||||
},
|
||||
..default_test_config(server.addr)
|
||||
};
|
||||
let mut client = Client::connect(config).unwrap();
|
||||
|
||||
assert!(matches!(client.open(), Err(ClientError::OpenTimeout)));
|
||||
assert_eq!(server.rx.iter().take(2).count(), 2);
|
||||
assert!(server.rx.try_recv().is_err());
|
||||
assert_eq!(
|
||||
client.test_hooks.last_restored_read_timeout.get(),
|
||||
Some(Duration::from_millis(50))
|
||||
);
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trusted_zero_token_normal_reply_is_terminal() {
|
||||
let params = default_params();
|
||||
let server = start_fake_server(move |socket, tx| {
|
||||
let (_, peer) = recv_request(&socket, &tx);
|
||||
// encode_open_reply rejects a zero token without FLAG_CLOSE, so this
|
||||
// deliberately non-compliant reply is built by encoding a normal
|
||||
// reply with a placeholder token and then zeroing the token field.
|
||||
let mut reply = open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None);
|
||||
reply[HMAC_OFFSET..HMAC_OFFSET + 8].copy_from_slice(&0_u64.to_le_bytes());
|
||||
socket.send_to(&reply, peer).unwrap();
|
||||
});
|
||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
client.open(),
|
||||
Err(ClientError::Protocol(irtt_proto::ProtoError::ZeroToken))
|
||||
));
|
||||
assert_eq!(server.rx.iter().take(1).count(), 1);
|
||||
assert!(client.runtime.prepare_open_request().is_ok());
|
||||
assert!(client.schedule.is_none());
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opening_deadline_overflow_occurs_before_send() {
|
||||
let server = timeout_server(Duration::from_millis(250));
|
||||
@@ -305,47 +106,6 @@ fn opening_deadline_overflow_occurs_before_send() {
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schedule_duration_overflow_cannot_partially_open() {
|
||||
let server = start_fake_server(|_socket, _tx| {});
|
||||
let client = Client::connect(default_test_config(server.addr)).unwrap();
|
||||
let reply = irtt_proto::OpenReply {
|
||||
flags: FLAG_OPEN | FLAG_REPLY,
|
||||
token: TOKEN,
|
||||
params: params_from_config(client.runtime.config()).unwrap(),
|
||||
};
|
||||
let mut latest = Instant::now();
|
||||
let mut step = Duration::MAX;
|
||||
while !step.is_zero() {
|
||||
if let Some(candidate) = latest.checked_add(step) {
|
||||
latest = candidate;
|
||||
}
|
||||
step /= 2;
|
||||
}
|
||||
let opened_at = ClientTimestamp {
|
||||
mono: latest,
|
||||
wall: SystemTime::now(),
|
||||
};
|
||||
let machine = client
|
||||
.runtime
|
||||
.prepare_open_acceptance(reply, opened_at)
|
||||
.unwrap();
|
||||
let previous_recv_len = client.recv_buffer.len();
|
||||
|
||||
let failure = client.prepare_client_open(machine, opened_at).unwrap_err();
|
||||
|
||||
assert!(matches!(
|
||||
failure.primary,
|
||||
ClientError::NegotiationRejected { .. }
|
||||
));
|
||||
assert!(failure.machine.cleanup_close_packet().is_some());
|
||||
assert!(client.runtime.prepare_open_request().is_ok());
|
||||
assert!(client.schedule.is_none());
|
||||
assert_eq!(client.applied_traffic_class, None);
|
||||
assert_eq!(client.recv_buffer.len(), previous_recv_len);
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_token_negotiation_failure_sends_cleanup_close() {
|
||||
let mut returned = default_params();
|
||||
@@ -377,146 +137,143 @@ fn post_token_negotiation_failure_sends_cleanup_close() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cleanup_send_failure_does_not_replace_primary_open_error() {
|
||||
let mut returned = default_params();
|
||||
returned.interval_ns += 1;
|
||||
let server = start_fake_server(move |socket, tx| {
|
||||
let (_, peer) = recv_request(&socket, &tx);
|
||||
socket
|
||||
.send_to(
|
||||
&open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, &returned, None),
|
||||
peer,
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
fn successful_open_handshake() {
|
||||
let config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
||||
let params = params_from_config(&config).unwrap();
|
||||
let server = open_success_server(params.clone());
|
||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
||||
client.test_hooks.fail_cleanup_send.set(true);
|
||||
|
||||
assert!(matches!(
|
||||
client.open(),
|
||||
Err(ClientError::NegotiationRejected { .. })
|
||||
));
|
||||
assert!(client.runtime.prepare_open_request().is_ok());
|
||||
let negotiated = assert_open_started(client.open().unwrap());
|
||||
assert_eq!(negotiated.params, params);
|
||||
|
||||
assert_eq!(client.applied_traffic_class, Some(0));
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn receive_buffer_reservation_failure_precedes_dscp_application() {
|
||||
let mut returned = default_params();
|
||||
// Raw wire TOS/Traffic Class byte for DSCP codepoint 46 (EF), matching
|
||||
// what the client below requests so negotiation succeeds unchanged.
|
||||
returned.dscp = 184;
|
||||
let server = start_fake_server(move |socket, tx| {
|
||||
let (_, peer) = recv_request(&socket, &tx);
|
||||
socket
|
||||
.send_to(
|
||||
&open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, &returned, None),
|
||||
peer,
|
||||
)
|
||||
.unwrap();
|
||||
let _ = recv_request(&socket, &tx);
|
||||
});
|
||||
fn successful_open_restores_configured_receive_timeout() {
|
||||
let config = default_test_config(SocketAddr::from(([127, 0, 0, 1], 1)));
|
||||
let params = params_from_config(&config).unwrap();
|
||||
let server = open_success_server(params);
|
||||
let mut config = default_test_config(server.addr);
|
||||
config.dscp = 46;
|
||||
config.socket_config.recv_timeout = Some(Duration::from_millis(75));
|
||||
let mut client = Client::connect(config).unwrap();
|
||||
client
|
||||
.test_hooks
|
||||
.recv_buffer_len_override
|
||||
.set(Some(usize::MAX));
|
||||
client.test_hooks.fail_open_dscp.set(true);
|
||||
|
||||
assert!(matches!(
|
||||
client.open(),
|
||||
Err(ClientError::AllocationFailed {
|
||||
operation: "negotiated receive buffer",
|
||||
..
|
||||
})
|
||||
));
|
||||
assert!(client.test_hooks.fail_open_dscp.get());
|
||||
assert!(client.runtime.prepare_open_request().is_ok());
|
||||
assert!(client.schedule.is_none());
|
||||
assert_eq!(client.applied_traffic_class, None);
|
||||
assert_open_started(client.open().unwrap());
|
||||
|
||||
assert_eq!(
|
||||
client.socket.read_timeout().unwrap(),
|
||||
Some(Duration::from_millis(75))
|
||||
);
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dscp_application_failure_leaves_machine_connected() {
|
||||
let params = default_params();
|
||||
let server = start_fake_server(move |socket, tx| {
|
||||
let (_, peer) = recv_request(&socket, &tx);
|
||||
socket
|
||||
.send_to(
|
||||
&open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None),
|
||||
peer,
|
||||
)
|
||||
.unwrap();
|
||||
let _ = recv_request(&socket, &tx);
|
||||
});
|
||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
||||
client.test_hooks.fail_open_dscp.set(true);
|
||||
|
||||
assert!(matches!(
|
||||
client.open(),
|
||||
Err(ClientError::SocketOption {
|
||||
operation: "set negotiated DSCP",
|
||||
..
|
||||
})
|
||||
));
|
||||
assert!(client.test_hooks.prepared_active_session_before_dscp.get());
|
||||
assert!(client.runtime.prepare_open_request().is_ok());
|
||||
assert!(client.schedule.is_none());
|
||||
assert_eq!(client.applied_traffic_class, None);
|
||||
fn open_timeout_after_all_timeouts() {
|
||||
let server = timeout_server(Duration::from_millis(700));
|
||||
let config = ClientConfig {
|
||||
open_timeouts: vec![Duration::from_millis(200), Duration::from_millis(200)],
|
||||
socket_config: crate::SocketConfig {
|
||||
recv_timeout: Some(Duration::from_millis(50)),
|
||||
..Default::default()
|
||||
},
|
||||
..default_test_config(server.addr)
|
||||
};
|
||||
let mut client = Client::connect(config).unwrap();
|
||||
assert!(matches!(client.open(), Err(ClientError::OpenTimeout)));
|
||||
assert_eq!(server.rx.iter().take(2).count(), 2);
|
||||
assert_eq!(
|
||||
client.socket.read_timeout().unwrap(),
|
||||
Some(Duration::from_millis(50))
|
||||
);
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_timeout_restoration_failure_leaves_machine_connected() {
|
||||
fn multiple_untrusted_datagrams_before_valid_reply_use_one_attempt() {
|
||||
let params = default_params();
|
||||
let server = start_fake_server(move |socket, tx| {
|
||||
let (_, peer) = recv_request(&socket, &tx);
|
||||
socket
|
||||
.send_to(
|
||||
&open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None),
|
||||
peer,
|
||||
)
|
||||
.unwrap();
|
||||
let _ = recv_request(&socket, &tx);
|
||||
let valid = open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None);
|
||||
let mut wrong_type = valid.clone();
|
||||
wrong_type[3] = FLAG_REPLY;
|
||||
let mut reserved_flags = valid.clone();
|
||||
reserved_flags[3] |= 0x10;
|
||||
let mut bad_magic = valid.clone();
|
||||
bad_magic[0] ^= 0xff;
|
||||
let unexpected_hmac =
|
||||
open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, Some(b"unexpected"));
|
||||
|
||||
for packet in [
|
||||
vec![0_u8],
|
||||
bad_magic,
|
||||
wrong_type,
|
||||
reserved_flags,
|
||||
unexpected_hmac,
|
||||
valid,
|
||||
] {
|
||||
socket.send_to(&packet, peer).unwrap();
|
||||
}
|
||||
});
|
||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
||||
client.test_hooks.fail_open_timeout_restore.set(true);
|
||||
|
||||
assert!(matches!(
|
||||
client.open(),
|
||||
Err(ClientError::ReadTimeoutRestore { .. })
|
||||
));
|
||||
assert!(client.runtime.prepare_open_request().is_ok());
|
||||
assert!(client.schedule.is_none());
|
||||
assert_eq!(client.applied_traffic_class, None);
|
||||
assert_open_started(client.open().unwrap());
|
||||
assert_eq!(server.rx.iter().take(1).count(), 1);
|
||||
assert!(server.rx.try_recv().is_err());
|
||||
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dscp_rollback_failure_keeps_timeout_restoration_primary() {
|
||||
fn only_ignored_open_datagrams_eventually_time_out() {
|
||||
let server = start_fake_server(move |socket, tx| {
|
||||
for _ in 0..2 {
|
||||
let (_, peer) = recv_request(&socket, &tx);
|
||||
socket.send_to(&[0_u8], peer).unwrap();
|
||||
socket.send_to(&MAGIC, peer).unwrap();
|
||||
}
|
||||
thread::sleep(Duration::from_millis(300));
|
||||
});
|
||||
let config = ClientConfig {
|
||||
open_timeouts: vec![Duration::from_millis(200), Duration::from_millis(200)],
|
||||
socket_config: crate::SocketConfig {
|
||||
recv_timeout: Some(Duration::from_millis(50)),
|
||||
..Default::default()
|
||||
},
|
||||
..default_test_config(server.addr)
|
||||
};
|
||||
let mut client = Client::connect(config).unwrap();
|
||||
|
||||
assert!(matches!(client.open(), Err(ClientError::OpenTimeout)));
|
||||
assert_eq!(server.rx.iter().take(2).count(), 2);
|
||||
assert!(server.rx.try_recv().is_err());
|
||||
assert_eq!(
|
||||
client.socket.read_timeout().unwrap(),
|
||||
Some(Duration::from_millis(50))
|
||||
);
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trusted_zero_token_normal_reply_is_terminal() {
|
||||
let params = default_params();
|
||||
let server = start_fake_server(move |socket, tx| {
|
||||
let (_, peer) = recv_request(&socket, &tx);
|
||||
socket
|
||||
.send_to(
|
||||
&open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None),
|
||||
peer,
|
||||
)
|
||||
.unwrap();
|
||||
let _ = recv_request(&socket, &tx);
|
||||
// encode_open_reply rejects a zero token without FLAG_CLOSE, so this
|
||||
// deliberately non-compliant reply is built by encoding a normal
|
||||
// reply with a placeholder token and then zeroing the token field.
|
||||
let mut reply = open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None);
|
||||
reply[HMAC_OFFSET..HMAC_OFFSET + 8].copy_from_slice(&0_u64.to_le_bytes());
|
||||
socket.send_to(&reply, peer).unwrap();
|
||||
});
|
||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
||||
client.test_hooks.fail_open_timeout_restore.set(true);
|
||||
client.test_hooks.fail_dscp_restore.set(true);
|
||||
|
||||
let error = client.open().unwrap_err();
|
||||
|
||||
assert!(matches!(error, ClientError::ReadTimeoutRestore { .. }));
|
||||
assert!(matches!(
|
||||
client.open(),
|
||||
Err(ClientError::Protocol(irtt_proto::ProtoError::ZeroToken))
|
||||
));
|
||||
assert_eq!(server.rx.iter().take(1).count(), 1);
|
||||
assert!(client.runtime.prepare_open_request().is_ok());
|
||||
assert!(client.schedule.is_none());
|
||||
assert_eq!(client.applied_traffic_class, None);
|
||||
|
||||
server.join();
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
use super::*;
|
||||
|
||||
/// An interrupted receive (`io::ErrorKind::Interrupted`, e.g. `EINTR`) must be
|
||||
/// invisible to the caller: retried transparently, with the eventual reply
|
||||
/// classified exactly as if no interruption had occurred.
|
||||
#[test]
|
||||
fn interrupted_receive_is_retried_and_the_reply_is_still_classified() {
|
||||
let params = default_params();
|
||||
let server = start_fake_server(move |socket, tx| {
|
||||
let (_, peer) = recv_request(&socket, &tx);
|
||||
let reply = open_reply(FLAG_OPEN | FLAG_REPLY, TOKEN, ¶ms, None);
|
||||
socket.send_to(&reply, peer).unwrap();
|
||||
|
||||
let mut buf = [0_u8; 2048];
|
||||
let (size, _) = socket.recv_from(&mut buf).unwrap();
|
||||
let seq = u32::from_le_bytes(buf[12..16].try_into().unwrap());
|
||||
let _ = size;
|
||||
let ts = TimestampFields::default();
|
||||
let reply_packet = echo_reply_packet(TOKEN, seq, ¶ms, &ts, None);
|
||||
socket.send_to(&reply_packet, peer).unwrap();
|
||||
});
|
||||
let config = ClientConfig {
|
||||
socket_config: crate::SocketConfig {
|
||||
recv_timeout: Some(Duration::from_secs(2)),
|
||||
..Default::default()
|
||||
},
|
||||
..default_test_config(server.addr)
|
||||
};
|
||||
let mut client = Client::connect(config).unwrap();
|
||||
assert_open_started(client.open().unwrap());
|
||||
|
||||
client.send_probe().unwrap();
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
|
||||
client.test_hooks.inject_recv_interrupted.set(10);
|
||||
let events = client.recv_once().unwrap();
|
||||
|
||||
assert_eq!(events.len(), 1);
|
||||
assert!(
|
||||
matches!(&events[0], ClientEvent::EchoReply { seq: 0, .. }),
|
||||
"expected a normal EchoReply despite interrupted receive attempts, got {:?}",
|
||||
events[0]
|
||||
);
|
||||
|
||||
client.close().unwrap();
|
||||
server.join();
|
||||
}
|
||||
|
||||
/// One logical receive keeps one timeout budget: repeated interruptions must
|
||||
/// not extend a `recv_once` call past its configured receive timeout.
|
||||
#[test]
|
||||
fn interrupted_receive_does_not_extend_the_configured_timeout() {
|
||||
let params = default_params();
|
||||
let server = silent_open_server(params);
|
||||
let config = ClientConfig {
|
||||
socket_config: crate::SocketConfig {
|
||||
recv_timeout: Some(Duration::from_millis(100)),
|
||||
..Default::default()
|
||||
},
|
||||
..default_test_config(server.addr)
|
||||
};
|
||||
let mut client = Client::connect(config).unwrap();
|
||||
assert_open_started(client.open().unwrap());
|
||||
|
||||
// No reply is ever coming; only interrupted attempts occur before the
|
||||
// real socket takes over and legitimately times out.
|
||||
client.test_hooks.inject_recv_interrupted.set(20);
|
||||
let started = Instant::now();
|
||||
let events = client.recv_once().unwrap();
|
||||
let elapsed = started.elapsed();
|
||||
|
||||
assert!(events.is_empty());
|
||||
assert!(
|
||||
elapsed < Duration::from_millis(500),
|
||||
"interrupted receives must not extend the configured receive timeout, took {elapsed:?}"
|
||||
);
|
||||
|
||||
client.close().unwrap();
|
||||
server.join();
|
||||
}
|
||||
@@ -20,9 +20,7 @@ fn kernel_rx_timestamp_unavailable_skip(test_name: &str) {
|
||||
}
|
||||
|
||||
mod burst_recv;
|
||||
mod interrupted_receive;
|
||||
mod packet_length;
|
||||
mod probe_schedule;
|
||||
mod reply_classification;
|
||||
mod reply_decode;
|
||||
mod reply_metadata;
|
||||
|
||||
@@ -1,487 +0,0 @@
|
||||
use super::*;
|
||||
|
||||
fn probe_timestamps(permission_at: Instant, sent_at: ClientTimestamp) -> ProbeSendTimestamps {
|
||||
ProbeSendTimestamps {
|
||||
permission_at,
|
||||
send_anchor: sent_at,
|
||||
sent_at,
|
||||
send_call_start: sent_at.mono,
|
||||
send_finished_at: sent_at.mono,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_probe_fails_before_open() {
|
||||
let server = start_fake_server(|_socket, _tx| {});
|
||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
||||
assert!(matches!(client.send_probe(), Err(ClientError::NotOpen)));
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_probe_sends_valid_echo_request() {
|
||||
let params = default_params();
|
||||
let server = silent_open_server(params.clone());
|
||||
let config = ClientConfig {
|
||||
socket_config: crate::SocketConfig {
|
||||
recv_timeout: Some(Duration::from_millis(200)),
|
||||
..Default::default()
|
||||
},
|
||||
..default_test_config(server.addr)
|
||||
};
|
||||
let mut client = Client::connect(config).unwrap();
|
||||
assert!(client.next_send_deadline().is_none());
|
||||
assert_open_started(client.open().unwrap());
|
||||
let events = client.send_probe().unwrap();
|
||||
assert_eq!(events.len(), 1);
|
||||
match &events[0] {
|
||||
ClientEvent::EchoSent {
|
||||
seq, remote, bytes, ..
|
||||
} => {
|
||||
assert_eq!(*seq, 0);
|
||||
assert_eq!(*remote, server.addr);
|
||||
assert_eq!(*bytes, echo_packet_len(false, ¶ms));
|
||||
}
|
||||
other => panic!("expected EchoSent, got {other:?}"),
|
||||
}
|
||||
thread::sleep(Duration::from_millis(30));
|
||||
let packets: Vec<_> = server.rx.try_iter().collect();
|
||||
let echo_reqs: Vec<_> = packets
|
||||
.iter()
|
||||
.filter(|p| p.len() >= 4 && p[3] & FLAG_OPEN == 0)
|
||||
.collect();
|
||||
let echo_req = echo_reqs.first().unwrap();
|
||||
assert_eq!(&echo_req[..3], &MAGIC);
|
||||
assert_eq!(echo_req[3], 0x00);
|
||||
let req_token = u64::from_le_bytes(echo_req[4..12].try_into().unwrap());
|
||||
assert_eq!(req_token, TOKEN);
|
||||
let seq = u32::from_le_bytes(echo_req[12..16].try_into().unwrap());
|
||||
assert_eq!(seq, 0);
|
||||
client.close().unwrap();
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocking_short_probe_commits_before_presentation_length_error() {
|
||||
let params = default_params();
|
||||
let server = silent_open_server(params.clone());
|
||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
||||
assert_open_started(client.open().unwrap());
|
||||
|
||||
let expected = echo_packet_len(false, ¶ms);
|
||||
let initial_deadline = client.next_send_deadline().unwrap();
|
||||
client.probe_reported_len = Some(expected - 1);
|
||||
|
||||
assert!(matches!(
|
||||
client.send_probe(),
|
||||
Err(ClientError::DatagramLengthMismatch {
|
||||
expected: error_expected,
|
||||
actual,
|
||||
}) if error_expected == expected && actual + 1 == expected
|
||||
));
|
||||
assert_eq!(client.runtime.packets_sent(), 1);
|
||||
assert_eq!(
|
||||
client.next_send_deadline(),
|
||||
Some(initial_deadline + client.runtime.config().interval)
|
||||
);
|
||||
|
||||
client.close().unwrap();
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocking_failed_probe_send_preserves_machine_and_schedule() {
|
||||
let params = default_params();
|
||||
let server = silent_open_server(params);
|
||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
||||
assert_open_started(client.open().unwrap());
|
||||
|
||||
let initial_deadline = client.next_send_deadline().unwrap();
|
||||
client.probe_send_error = true;
|
||||
|
||||
assert!(matches!(client.send_probe(), Err(ClientError::Socket(_))));
|
||||
assert_eq!(client.runtime.packets_sent(), 0);
|
||||
assert_eq!(client.next_send_deadline(), Some(initial_deadline));
|
||||
|
||||
assert!(matches!(
|
||||
client.send_probe().unwrap().as_slice(),
|
||||
[ClientEvent::EchoSent { seq: 0, .. }]
|
||||
));
|
||||
|
||||
client.close().unwrap();
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocking_timeout_finalization_overflow_does_not_send_or_commit() {
|
||||
let params = default_params();
|
||||
let server = silent_open_server(params);
|
||||
let config = ClientConfig {
|
||||
probe_timeout: Duration::MAX,
|
||||
..default_test_config(server.addr)
|
||||
};
|
||||
let mut client = Client::connect(config).unwrap();
|
||||
assert_open_started(client.open().unwrap());
|
||||
|
||||
let initial_deadline = client.next_send_deadline().unwrap();
|
||||
let sent_at = ClientTimestamp {
|
||||
mono: Instant::now(),
|
||||
wall: SystemTime::now(),
|
||||
};
|
||||
|
||||
assert!(matches!(
|
||||
client.send_probe_at(probe_timestamps(initial_deadline, sent_at)),
|
||||
Err(ClientError::DurationOverflow)
|
||||
));
|
||||
assert_eq!(client.runtime.packets_sent(), 0);
|
||||
assert_eq!(client.next_send_deadline(), Some(initial_deadline));
|
||||
thread::sleep(Duration::from_millis(30));
|
||||
assert!(server
|
||||
.rx
|
||||
.try_iter()
|
||||
.all(|packet| packet.len() >= 4 && packet[3] & FLAG_OPEN != 0));
|
||||
|
||||
client.close().unwrap();
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocking_probe_separates_permission_committed_send_and_send_call_timing() {
|
||||
let duration = Duration::from_secs(1);
|
||||
let interval = Duration::from_millis(100);
|
||||
let params = Params {
|
||||
duration_ns: 1_000_000_000,
|
||||
interval_ns: 100_000_000,
|
||||
..default_params()
|
||||
};
|
||||
let server = silent_open_server(params.clone());
|
||||
let config = ClientConfig {
|
||||
duration: Some(duration),
|
||||
interval,
|
||||
..default_test_config(server.addr)
|
||||
};
|
||||
let mut client = Client::connect(config).unwrap();
|
||||
assert_open_started(client.open().unwrap());
|
||||
|
||||
let opened_mono = Instant::now()
|
||||
.checked_sub(Duration::from_secs(2))
|
||||
.expect("test host monotonic clock has at least two seconds of history");
|
||||
client.schedule = Some(
|
||||
ProbeSchedule::new(
|
||||
opened_mono,
|
||||
&NegotiatedParams {
|
||||
params,
|
||||
restrictions: Vec::new(),
|
||||
},
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let permission_at = opened_mono + Duration::from_millis(500);
|
||||
// Distinct pre-send anchor and post-send `sent_at`: the anchor drives
|
||||
// the operational timeout deadline, while `sent_at` (captured after the
|
||||
// send call) is the public measurement/timer_error endpoint. They are
|
||||
// deliberately decoupled by this PR.
|
||||
let send_anchor = ClientTimestamp {
|
||||
wall: SystemTime::now(),
|
||||
mono: opened_mono + Duration::from_millis(525),
|
||||
};
|
||||
let sent_at = ClientTimestamp {
|
||||
wall: SystemTime::now(),
|
||||
mono: opened_mono + Duration::from_millis(534),
|
||||
};
|
||||
let timestamps = ProbeSendTimestamps {
|
||||
permission_at,
|
||||
send_anchor,
|
||||
sent_at,
|
||||
send_call_start: opened_mono + Duration::from_millis(526),
|
||||
send_finished_at: opened_mono + Duration::from_millis(533),
|
||||
};
|
||||
|
||||
let events = client.send_probe_at(timestamps).unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
events.as_slice(),
|
||||
[ClientEvent::EchoSent {
|
||||
scheduled_at,
|
||||
sent_at: event_sent_at,
|
||||
send_call,
|
||||
timer_error,
|
||||
..
|
||||
}] if *scheduled_at == opened_mono
|
||||
&& *event_sent_at == sent_at
|
||||
&& *send_call == Duration::from_millis(7)
|
||||
&& *timer_error == Duration::from_millis(534)
|
||||
));
|
||||
assert_eq!(client.next_send_deadline(), Some(opened_mono + interval));
|
||||
assert_eq!(client.runtime.packets_sent(), 1);
|
||||
// The timeout deadline is anchored to the pre-send `send_anchor`, not
|
||||
// to the post-send `sent_at` captured 9ms later.
|
||||
let timeout_at = send_anchor.mono + client.probe_timeout();
|
||||
assert_ne!(timeout_at, sent_at.mono + client.probe_timeout());
|
||||
assert!(client
|
||||
.poll_timeouts_at(timeout_at - Duration::from_nanos(1))
|
||||
.unwrap()
|
||||
.is_empty());
|
||||
assert!(matches!(
|
||||
client.poll_timeouts_at(timeout_at).unwrap().as_slice(),
|
||||
[ClientEvent::EchoLoss {
|
||||
sent_at: loss_sent_at,
|
||||
..
|
||||
}] if *loss_sent_at == sent_at
|
||||
));
|
||||
|
||||
client.close().unwrap();
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocking_send_call_excludes_wrapped_history_cleanup() {
|
||||
let params = default_params();
|
||||
let server = silent_open_server(params);
|
||||
let mut client = Client::connect(default_test_config(server.addr)).unwrap();
|
||||
assert_open_started(client.open().unwrap());
|
||||
|
||||
let scheduled_at = client.next_send_deadline().unwrap();
|
||||
let sent_at = ClientTimestamp {
|
||||
wall: SystemTime::now(),
|
||||
mono: scheduled_at + Duration::from_millis(5),
|
||||
};
|
||||
client.runtime.seed_wrapped_probe_history_for_test(sent_at);
|
||||
let timestamps = ProbeSendTimestamps {
|
||||
permission_at: scheduled_at,
|
||||
send_anchor: sent_at,
|
||||
sent_at,
|
||||
send_call_start: scheduled_at + Duration::from_millis(6),
|
||||
send_finished_at: scheduled_at + Duration::from_millis(13),
|
||||
};
|
||||
|
||||
let events = client.send_probe_at(timestamps).unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
events.as_slice(),
|
||||
[ClientEvent::EchoSent {
|
||||
seq: 0,
|
||||
send_call,
|
||||
..
|
||||
}] if *send_call == Duration::from_millis(7)
|
||||
));
|
||||
assert!(!client.runtime.has_timed_out_metadata());
|
||||
|
||||
client.close().unwrap();
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_probe_respects_finite_duration_exclusive_end() {
|
||||
let params = Params {
|
||||
protocol_version: 1,
|
||||
duration_ns: 1_000_000_000,
|
||||
interval_ns: 500_000_000,
|
||||
received_stats: ReceivedStats::Both,
|
||||
stamp_at: StampAt::Both,
|
||||
clock: Clock::Both,
|
||||
..Params::default()
|
||||
};
|
||||
let server = silent_open_server(params.clone());
|
||||
let config = ClientConfig {
|
||||
duration: Some(Duration::from_secs(1)),
|
||||
interval: Duration::from_millis(500),
|
||||
socket_config: crate::SocketConfig {
|
||||
recv_timeout: Some(Duration::from_millis(200)),
|
||||
..Default::default()
|
||||
},
|
||||
..default_test_config(server.addr)
|
||||
};
|
||||
let mut client = Client::connect(config).unwrap();
|
||||
assert_open_started(client.open().unwrap());
|
||||
|
||||
let start = client.next_send_deadline().unwrap();
|
||||
let interval = Duration::from_millis(500);
|
||||
|
||||
let now0 = ClientTimestamp {
|
||||
mono: start,
|
||||
wall: SystemTime::now(),
|
||||
};
|
||||
assert!(client
|
||||
.send_probe_at(probe_timestamps(now0.mono, now0))
|
||||
.is_ok());
|
||||
|
||||
let now1 = ClientTimestamp {
|
||||
mono: start + interval,
|
||||
wall: SystemTime::now(),
|
||||
};
|
||||
assert!(client
|
||||
.send_probe_at(probe_timestamps(now1.mono, now1))
|
||||
.is_ok());
|
||||
|
||||
let now2 = ClientTimestamp {
|
||||
mono: start + Duration::from_secs(1),
|
||||
wall: SystemTime::now(),
|
||||
};
|
||||
let events = client
|
||||
.send_probe_at(probe_timestamps(now2.mono, now2))
|
||||
.unwrap();
|
||||
assert!(events.is_empty());
|
||||
assert!(client.next_send_deadline().is_none());
|
||||
|
||||
client.close().unwrap();
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_probe_skips_missed_schedule_slots() {
|
||||
let interval = Duration::from_millis(10);
|
||||
let params = Params {
|
||||
protocol_version: 1,
|
||||
duration_ns: 0,
|
||||
interval_ns: 10_000_000,
|
||||
received_stats: ReceivedStats::Both,
|
||||
stamp_at: StampAt::Both,
|
||||
clock: Clock::Both,
|
||||
..Params::default()
|
||||
};
|
||||
let server = echo_server(params);
|
||||
let config = ClientConfig {
|
||||
duration: None,
|
||||
interval,
|
||||
..default_test_config(server.addr)
|
||||
};
|
||||
let mut client = Client::connect(config).unwrap();
|
||||
assert_open_started(client.open().unwrap());
|
||||
|
||||
let first_deadline = client.next_send_deadline().unwrap();
|
||||
let permission_at = first_deadline + Duration::from_millis(45);
|
||||
let delayed_send = ClientTimestamp {
|
||||
mono: first_deadline + Duration::from_millis(48),
|
||||
wall: SystemTime::now(),
|
||||
};
|
||||
let timestamps = ProbeSendTimestamps {
|
||||
permission_at,
|
||||
send_anchor: delayed_send,
|
||||
sent_at: delayed_send,
|
||||
send_call_start: first_deadline + Duration::from_millis(49),
|
||||
send_finished_at: first_deadline + Duration::from_millis(55),
|
||||
};
|
||||
let events = client
|
||||
.send_managed_probe_at(first_deadline, timestamps)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(events.len(), 1);
|
||||
assert!(matches!(
|
||||
events[0],
|
||||
ClientEvent::EchoSent {
|
||||
seq: 0,
|
||||
scheduled_at,
|
||||
sent_at,
|
||||
send_call,
|
||||
timer_error,
|
||||
..
|
||||
} if scheduled_at == first_deadline + Duration::from_millis(40)
|
||||
&& sent_at == delayed_send
|
||||
&& send_call == Duration::from_millis(6)
|
||||
&& timer_error == Duration::from_millis(8)
|
||||
));
|
||||
assert_eq!(
|
||||
client.next_send_deadline(),
|
||||
Some(first_deadline + Duration::from_millis(50))
|
||||
);
|
||||
|
||||
client.close().unwrap();
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_probe_skip_preserves_finite_run_end() {
|
||||
let interval = Duration::from_millis(10);
|
||||
let duration = Duration::from_millis(50);
|
||||
let params = Params {
|
||||
protocol_version: 1,
|
||||
duration_ns: 50_000_000,
|
||||
interval_ns: 10_000_000,
|
||||
received_stats: ReceivedStats::Both,
|
||||
stamp_at: StampAt::Both,
|
||||
clock: Clock::Both,
|
||||
..Params::default()
|
||||
};
|
||||
let server = echo_server(params);
|
||||
let config = ClientConfig {
|
||||
duration: Some(duration),
|
||||
interval,
|
||||
..default_test_config(server.addr)
|
||||
};
|
||||
let mut client = Client::connect(config).unwrap();
|
||||
assert_open_started(client.open().unwrap());
|
||||
|
||||
let first_deadline = client.next_send_deadline().unwrap();
|
||||
let delayed_send = ClientTimestamp {
|
||||
mono: first_deadline + Duration::from_millis(45),
|
||||
wall: SystemTime::now(),
|
||||
};
|
||||
let first = client
|
||||
.send_managed_probe_at(
|
||||
first_deadline,
|
||||
probe_timestamps(delayed_send.mono, delayed_send),
|
||||
)
|
||||
.unwrap();
|
||||
let second_sent_at = ClientTimestamp {
|
||||
mono: first_deadline + Duration::from_millis(46),
|
||||
wall: SystemTime::now(),
|
||||
};
|
||||
let second = client
|
||||
.send_managed_probe_at(
|
||||
first_deadline + duration,
|
||||
probe_timestamps(second_sent_at.mono, second_sent_at),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(first.len(), 1);
|
||||
assert!(second.is_empty());
|
||||
assert!(client.next_send_deadline().is_none());
|
||||
|
||||
client.close().unwrap();
|
||||
server.join();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn receive_wait_is_bounded_by_next_ten_millisecond_deadline() {
|
||||
let now = Instant::now();
|
||||
let deadline = now + Duration::from_millis(10);
|
||||
|
||||
assert_eq!(
|
||||
bounded_receive_timeout(
|
||||
Some(deadline),
|
||||
Some(Duration::from_millis(20)),
|
||||
Duration::from_millis(20),
|
||||
now,
|
||||
),
|
||||
Some(Duration::from_millis(10))
|
||||
);
|
||||
assert_eq!(
|
||||
bounded_receive_timeout(
|
||||
Some(now),
|
||||
Some(Duration::from_millis(20)),
|
||||
Duration::from_millis(20),
|
||||
now,
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connect_rejects_invalid_probe_limits() {
|
||||
for config in [
|
||||
ClientConfig {
|
||||
max_pending_probes: 0,
|
||||
..ClientConfig::default()
|
||||
},
|
||||
ClientConfig {
|
||||
probe_timeout: Duration::ZERO,
|
||||
..ClientConfig::default()
|
||||
},
|
||||
] {
|
||||
assert!(matches!(
|
||||
Client::connect(config),
|
||||
Err(ClientError::InvalidConfig { .. })
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user