tests: clean up more tests across the project
Rust CI / cargo fmt (push) Successful in 17s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Successful in 33s
Rust CI / cargo clippy (push) Successful in 34s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Failing after 33s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Failing after 9s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Failing after 8s
Rust CI / cargo doc (push) Successful in 22s
Fuzz Smoke / cargo fuzz smoke (push) Failing after 27s
Supply chain / cargo-deny advisories (push) Successful in 7s
Supply chain / cargo-deny bans licenses sources (push) Successful in 6s
Supply chain / actionlint (push) Successful in 3s
Netem Interop / netem interop (push) Successful in 1m3s

This commit is contained in:
Nils Andreas Svee committed 2026-10-02 12:47:27 +07:00
1 parent dc5b19059e
commit 74e7532965
17 files changed
+28 -1435

No files matched your search

+4 -71
View File
@@ -1,74 +1,7 @@
use super::*;
#[test]
fn params_from_config_maps_compatibility_fields() {
let config = ClientConfig {
duration: Some(Duration::from_secs(5)),
interval: Duration::from_millis(250),
length: 1472,
received_stats: ReceivedStats::Window,
stamp_at: StampAt::Midpoint,
clock: Clock::Wall,
dscp: 46,
hmac_key: Some(b"secret".to_vec()),
server_fill: Some("rand".to_owned()),
..ClientConfig::default()
};
let params = params_from_config(&config).unwrap();
assert_eq!(params.protocol_version, PROTOCOL_VERSION);
assert_eq!(params.duration_ns, 5_000_000_000);
assert_eq!(params.interval_ns, 250_000_000);
assert_eq!(params.length, 1472);
assert_eq!(params.received_stats, ReceivedStats::Window);
assert_eq!(params.stamp_at, StampAt::Midpoint);
assert_eq!(params.clock, Clock::Wall);
assert_eq!(
params.dscp, 184,
"config DSCP codepoint 46 must be shifted into raw wire byte 184"
);
assert_eq!(
params.server_fill.as_ref().map(|fill| fill.value.as_str()),
Some("rand")
);
assert_eq!(config.hmac_key.as_deref(), Some(b"secret".as_slice()));
}
#[test]
fn params_from_config_accepts_boundary_values() {
for length in [0, 1, 1472, 4096, MAX_UDP_PAYLOAD_LENGTH] {
let config = ClientConfig {
length,
..ClientConfig::default()
};
assert_eq!(
params_from_config(&config).unwrap().length,
i64::from(length)
);
}
let config = ClientConfig {
dscp: 63,
..ClientConfig::default()
};
assert_eq!(
params_from_config(&config).unwrap().dscp,
252,
"boundary codepoint 63 must be shifted into raw wire byte 252"
);
}
#[test]
fn params_from_config_encodes_continuous_duration_as_zero() {
let config = ClientConfig {
duration: None,
..ClientConfig::default()
};
assert_eq!(params_from_config(&config).unwrap().duration_ns, 0);
}
#[test]
fn params_from_config_rejects_invalid_values() {
fn connect_rejects_invalid_configuration() {
let i64_max_ns = u64::try_from(i64::MAX).unwrap();
let too_large = Duration::from_nanos(i64_max_ns) + Duration::from_nanos(1);
let cases = [
@@ -151,10 +84,10 @@ fn params_from_config_rejects_invalid_values() {
for (name, config, expected_reason) in cases {
assert!(
matches!(
params_from_config(&config),
Err(ClientError::InvalidConfig { reason }) if reason.contains(expected_reason)
Client::connect(config),
Err(ClientError::InvalidConfig { .. })
),
"{name} should fail with InvalidConfig containing {expected_reason:?}"
"{name} should fail with InvalidConfig ({expected_reason})"
);
}
}
+1 -121
View File
@@ -1,6 +1,5 @@
use std::{
future::{poll_fn, Future},
mem,
net::{SocketAddr, UdpSocket},
pin::Pin,
sync::{Arc, Condvar, Mutex},
@@ -16,7 +15,7 @@ use irtt_proto::{
use tokio::runtime::{Builder, Runtime};
use super::*;
use crate::{ClientAuthConfig, NegotiationPolicy, RunMode};
use crate::{ClientAuthConfig, RunMode};
const TOKEN: u64 = 0x1234_5678_90ab_cdef;
@@ -766,52 +765,6 @@ fn terminal_targets_count_toward_retained_generation_limit() {
assert!(task.snapshot().targets.is_empty());
}
#[test]
fn pruning_leaves_no_stale_target_work() {
let (mut task, _) = ManagedClient::task(
config(ManagedPacing::Staggered),
vec![
ManagedTargetConfig::new("one", "127.0.0.1:9"),
ManagedTargetConfig::new("two", "127.0.0.1:10"),
ManagedTargetConfig::new("three", "127.0.0.1:11"),
],
)
.unwrap();
task.cursor = 2;
task.timeout_cursor = 2;
task.send_cursor = 2;
task.scan_remaining = 3;
task.burst_remaining = 3;
task.stagger_remaining = 3;
task.targets[2].desired = false;
task.targets[2].state = TargetState::Terminal;
task.prune_undesired_terminal();
// Timeout discovery indexes `targets` with `timeout_cursor` directly, so a
// prune must leave it safe to use; the target and send passes modulo their
// own cursors. Pending per-pass work must not outlive the targets it was
// scheduled for either.
assert_eq!(task.targets.len(), 2);
assert!(task.timeout_cursor < task.targets.len());
assert!(task.scan_remaining <= task.targets.len());
assert!(task.burst_remaining <= task.targets.len());
assert!(task.stagger_remaining <= task.targets.len());
assert!(!task.poll_timeout_pass(Instant::now()));
for target in &mut task.targets {
target.desired = false;
target.state = TargetState::Terminal;
}
task.prune_undesired_terminal();
assert!(task.targets.is_empty());
assert_eq!(task.scan_remaining, 0);
assert_eq!(task.burst_remaining, 0);
assert_eq!(task.stagger_remaining, 0);
assert!(!task.poll_timeout_pass(Instant::now()));
}
#[test]
fn dynamic_rejections_leave_transaction_state_unchanged() {
let mut managed = config(ManagedPacing::Staggered);
@@ -1562,79 +1515,6 @@ fn staggered_pacing_fairness() {
second.finish();
}
#[test]
fn stagger_gate_tracks_active_membership() {
let first =
start_server_negotiating(ServerBehavior::Echo, None, Some(Duration::from_millis(100)));
let second_gate = Arc::new(PacketGate::default());
let second = start_server_with_gates(
ServerBehavior::Echo,
None,
Some(Duration::from_secs(1)),
Some(Arc::clone(&second_gate)),
None,
);
let mut managed = config(ManagedPacing::Staggered);
managed.client.duration = Some(Duration::from_millis(1_250));
managed.client.interval = Duration::from_secs(1);
managed.client.negotiation_policy = NegotiationPolicy::Loose;
managed.client.open_timeouts = vec![Duration::from_secs(2)];
let (task, handle) = ManagedClient::task(
managed,
vec![target("first", first.addr), target("second", second.addr)],
)
.unwrap();
let runtime = runtime();
let mut task = Box::pin(task);
drive_task_until(&runtime, &mut task, |task| {
task.targets[0].counters.packets_sent == 1
});
let one_target_last = task.last_stagger_send.unwrap();
let one_target_gate = task.send_gate.unwrap();
assert_eq!(
task.send_gate,
one_target_last.checked_add(Duration::from_millis(100))
);
second_gate.release();
drive_task_until(&runtime, &mut task, |task| task.active_count() == 2);
let last = task.last_stagger_send.unwrap();
let gate = task.send_gate.unwrap();
assert_eq!(gate, last.checked_add(Duration::from_millis(50)).unwrap());
assert!(gate <= one_target_gate);
assert!(gate > last && gate < last.checked_add(Duration::from_millis(100)).unwrap());
drive_task_until(&runtime, &mut task, |task| {
task.targets[1].counters.packets_sent == 1
});
let accepted_at = Instant::now();
task.record_stagger_acceptance(
SendResult::Failed { accepted: true },
Some(Duration::from_millis(50)),
accepted_at,
);
assert_eq!(
task.send_gate,
accepted_at.checked_add(Duration::from_millis(50))
);
let preserved_gate = task.send_gate;
let TargetState::Active { client } =
mem::replace(&mut task.targets[0].state, TargetState::Terminal)
else {
panic!("fast target was not active before removal");
};
assert!(task.begin_drain(0, client, ManagedTargetEndReason::Stopped, Instant::now()));
assert_eq!(task.send_gate, preserved_gate);
drive_task_until(&runtime, &mut task, |task| {
task.targets[1].counters.packets_sent == 2
});
drop(handle.stop());
let outcome = runtime.block_on(task);
assert_eq!(outcome.failed_target_outcomes, 0);
first.finish();
second.finish();
}
/// The stagger gate must not fold each send's observed wakeup latency into
/// every later slot.
///
-278
View File
@@ -1177,10 +1177,6 @@ mod tests {
}
}
fn connected_machine(config: ClientConfig) -> SessionMachine {
SessionMachine::new(config, "127.0.0.1:2112".parse().unwrap()).unwrap()
}
fn normal_open_reply(machine: &SessionMachine, token: u64) -> OpenReply {
OpenReply {
flags: flags::FLAG_OPEN | flags::FLAG_REPLY,
@@ -1189,210 +1185,6 @@ mod tests {
}
}
fn encoded_open_reply(
machine: &SessionMachine,
reply: &OpenReply,
) -> Result<Vec<u8>, irtt_proto::ProtoError> {
irtt_proto::encode_open_reply(reply, machine.config.hmac_key.as_deref())
}
#[test]
fn malformed_wrong_direction_and_invalid_flags_are_ignored() {
let machine = connected_machine(ClientConfig::default());
assert!(matches!(
machine.inspect_open_datagram(&[0_u8]),
Ok(OpenDatagramDisposition::Ignore)
));
let request = machine.prepare_open_request().unwrap();
assert!(matches!(
machine.inspect_open_datagram(&request.bytes),
Ok(OpenDatagramDisposition::Ignore)
));
let reply = normal_open_reply(&machine, 0x1020_3040_5060_7080);
let mut reserved = encoded_open_reply(&machine, &reply).unwrap();
reserved[3] |= 0x10;
assert!(matches!(
machine.inspect_open_datagram(&reserved),
Ok(OpenDatagramDisposition::Ignore)
));
assert!(matches!(machine.state, MachineState::Connected));
}
#[test]
fn missing_unexpected_and_bad_hmac_are_ignored() {
let key = b"open-key".to_vec();
let authenticated = connected_machine(ClientConfig {
hmac_key: Some(key.clone()),
..ClientConfig::default()
});
let reply = normal_open_reply(&authenticated, 0x1020_3040_5060_7080);
let plain = irtt_proto::encode_open_reply(&reply, None).unwrap();
assert!(matches!(
authenticated.inspect_open_datagram(&plain),
Ok(OpenDatagramDisposition::Ignore)
));
let mut bad = encoded_open_reply(&authenticated, &reply).unwrap();
*bad.last_mut().unwrap() ^= 0x80;
assert!(matches!(
authenticated.inspect_open_datagram(&bad),
Ok(OpenDatagramDisposition::Ignore)
));
let plain_machine = connected_machine(ClientConfig::default());
let unexpected = irtt_proto::encode_open_reply(&reply, Some(&key)).unwrap();
assert!(matches!(
plain_machine.inspect_open_datagram(&unexpected),
Ok(OpenDatagramDisposition::Ignore)
));
}
#[test]
fn authenticated_zero_token_is_trusted_and_terminal() {
let key = b"open-key".to_vec();
let machine = connected_machine(ClientConfig {
hmac_key: Some(key.clone()),
..ClientConfig::default()
});
let reply = normal_open_reply(&machine, 0x1020_3040_5060_7080);
let mut packet = encoded_open_reply(&machine, &reply).unwrap();
let token_offset = 4 + irtt_proto::HMAC_SIZE;
packet[token_offset..token_offset + 8].fill(0);
irtt_proto::compute_hmac_in_place(&key, &mut packet, 4).unwrap();
assert!(matches!(
machine.inspect_open_datagram(&packet),
Err(ClientError::Protocol(irtt_proto::ProtoError::ZeroToken))
));
assert!(matches!(machine.state, MachineState::Connected));
}
#[test]
fn trusted_invalid_parameter_encoding_is_terminal() {
let machine = connected_machine(ClientConfig::default());
let mut packet = irtt_proto::MAGIC.to_vec();
packet.push(flags::FLAG_OPEN | flags::FLAG_REPLY);
packet.extend_from_slice(&0x1020_3040_5060_7080_u64.to_le_bytes());
packet.push(0x80);
assert!(matches!(
machine.inspect_open_datagram(&packet),
Err(ClientError::Protocol(
irtt_proto::ProtoError::TruncatedVarint
))
));
assert!(matches!(machine.state, MachineState::Connected));
}
#[test]
fn trusted_rejection_version_and_run_mode_failures_do_not_open() {
let machine = connected_machine(ClientConfig::default());
let rejection = OpenReply {
flags: flags::FLAG_OPEN | flags::FLAG_REPLY | flags::FLAG_CLOSE,
token: 0,
params: machine.requested.clone(),
};
assert!(matches!(
machine
.prepare_open_acceptance(rejection, timestamp(Instant::now()))
.unwrap_err()
.primary,
ClientError::ServerRejected
));
let mut version = normal_open_reply(&machine, 0x1020_3040_5060_7080);
version.params.protocol_version += 1;
assert!(matches!(
machine
.prepare_open_acceptance(version, timestamp(Instant::now()))
.unwrap_err()
.primary,
ClientError::ProtocolVersionMismatch { .. }
));
let no_test = connected_machine(ClientConfig {
run_mode: RunMode::NoTest,
..ClientConfig::default()
});
assert!(matches!(
no_test
.prepare_open_acceptance(
normal_open_reply(&no_test, 0x1020_3040_5060_7080),
timestamp(Instant::now()),
)
.unwrap_err()
.primary,
ClientError::UnexpectedNoTestReply
));
assert!(matches!(machine.state, MachineState::Connected));
assert!(matches!(no_test.state, MachineState::Connected));
}
#[test]
fn no_test_non_close_reply_prepares_authenticated_cleanup_without_state_change() {
let key = b"cleanup-key".to_vec();
let machine = connected_machine(ClientConfig {
run_mode: RunMode::NoTest,
hmac_key: Some(key.clone()),
..ClientConfig::default()
});
let token = 0x1020_3040_5060_7080;
let failure = machine
.prepare_open_acceptance(
normal_open_reply(&machine, token),
timestamp(Instant::now()),
)
.unwrap_err();
assert!(matches!(
failure.primary,
ClientError::UnexpectedNoTestReply
));
let cleanup = failure.cleanup_close.as_deref().unwrap();
assert_eq!(cleanup[3], flags::FLAG_CLOSE | flags::FLAG_HMAC);
irtt_proto::verify_hmac(&key, cleanup, 4).unwrap();
assert_eq!(
u64::from_le_bytes(
cleanup[4 + irtt_proto::HMAC_SIZE..12 + irtt_proto::HMAC_SIZE]
.try_into()
.unwrap()
),
token
);
drop(failure);
assert!(matches!(machine.state, MachineState::Connected));
}
#[test]
fn no_test_close_replies_do_not_prepare_cleanup() {
let machine = connected_machine(ClientConfig {
run_mode: RunMode::NoTest,
..ClientConfig::default()
});
let reply = |token| OpenReply {
flags: flags::FLAG_OPEN | flags::FLAG_REPLY | flags::FLAG_CLOSE,
token,
params: machine.requested.clone(),
};
let prepared = machine
.prepare_open_acceptance(reply(0), timestamp(Instant::now()))
.unwrap();
assert!(prepared.cleanup_close_packet().is_none());
let failure = machine
.prepare_open_acceptance(reply(0x1020_3040_5060_7080), timestamp(Instant::now()))
.unwrap_err();
assert!(matches!(
failure.primary,
ClientError::NonZeroNoTestToken { .. }
));
assert!(failure.cleanup_close.is_none());
}
#[test]
fn uncommitted_probe_preparation_changes_no_authoritative_state() {
let mut machine = open_machine(4, Duration::from_secs(1));
@@ -1417,30 +1209,6 @@ mod tests {
assert_eq!(session.pending.len(), 0);
}
#[test]
fn stale_prepared_probe_is_rejected_without_changing_state() {
let mut machine = open_machine(4, Duration::from_secs(1));
let stale = machine.prepare_probe().unwrap();
let accepted = machine.prepare_probe().unwrap();
let sent_at = timestamp(Instant::now());
let preflight = machine.preflight_probe_commit(&accepted).unwrap();
let commit = machine.finalize_probe_commit(preflight, sent_at).unwrap();
machine.commit_probe_sent(commit, sent_at, accepted.bytes.len());
assert!(matches!(
machine.preflight_probe_commit(&stale),
Err(ClientError::StalePreparedProbe {
prepared_seq: 0,
next_wire_seq: 1,
})
));
let session = active(&machine);
assert_eq!(session.next_wire_seq, 1);
assert_eq!(session.packets_sent, 1);
assert_eq!(session.pending.len(), 1);
}
#[test]
fn repeated_would_block_style_preflight_commits_once() {
let mut machine = open_machine(4, Duration::from_secs(1));
@@ -1462,23 +1230,6 @@ mod tests {
assert_eq!(session.pending.len(), 1);
}
#[test]
fn pending_capacity_exhaustion_is_detected_before_commit() {
let mut machine = open_machine(1, Duration::from_secs(1));
let first = machine.prepare_probe().unwrap();
let sent_at = timestamp(Instant::now());
let preflight = machine.preflight_probe_commit(&first).unwrap();
let commit = machine.finalize_probe_commit(preflight, sent_at).unwrap();
machine.commit_probe_sent(commit, sent_at, first.bytes.len());
let second = machine.prepare_probe().unwrap();
assert!(matches!(
machine.preflight_probe_commit(&second),
Err(ClientError::PendingLimitExceeded { limit: 1 })
));
assert_eq!(active(&machine).packets_sent, 1);
}
#[test]
fn pending_sequence_collision_is_detected_before_commit() {
let mut machine = open_machine(2, Duration::from_secs(1));
@@ -1525,35 +1276,6 @@ mod tests {
assert_eq!(active(&machine).pending.len(), 0);
}
#[test]
fn exhaustive_timeout_polling_returns_every_due_loss() {
let mut machine = open_machine(4, Duration::from_secs(1));
let now = Instant::now();
for seq in [2, 1, 0] {
let sent_at = timestamp(now - Duration::from_secs(u64::from(seq) + 1));
let session = active_mut(&mut machine);
session.pending.preflight_insert(seq).unwrap();
session.pending.commit_insert(PendingProbe {
wire_seq: seq,
sent_at,
timeout_at: sent_at.mono + Duration::from_secs(1),
tx_not_before_wall: sent_at.wall,
kernel_tx_timestamp: None,
});
}
let events = machine.poll_timeouts_at(now).unwrap();
assert!(matches!(
events.as_slice(),
[
ClientEvent::EchoLoss { seq: 2, .. },
ClientEvent::EchoLoss { seq: 1, .. },
ClientEvent::EchoLoss { seq: 0, .. },
]
));
assert_eq!(active(&machine).pending.len(), 0);
}
#[test]
fn wrapping_sequence_from_max_to_zero_remains_valid() {
let mut machine = open_machine(2, Duration::from_secs(1));
@@ -396,73 +396,6 @@ mod tests {
}
}
#[test]
fn strict_rejects_changed_negotiated_fields() {
let requested = default_params();
let mut returned = requested.clone();
returned.length = 128;
assert_eq!(
rejection_reason(&requested, &returned, NegotiationPolicy::Strict),
NegotiationRestriction::LengthReduced {
requested: requested.length,
negotiated: returned.length,
}
.message()
);
let mut returned = requested.clone();
returned.dscp = 0;
assert_eq!(
rejection_reason(&requested, &returned, NegotiationPolicy::Strict),
NegotiationRestriction::DscpChanged {
requested: requested.dscp >> 2,
negotiated: returned.dscp >> 2,
}
.message()
);
let mut returned = requested.clone();
returned.received_stats = ReceivedStats::Count;
assert_eq!(
rejection_reason(&requested, &returned, NegotiationPolicy::Strict),
NegotiationRestriction::ReceivedStatsChanged {
requested: requested.received_stats,
negotiated: returned.received_stats,
}
.message()
);
let mut returned = requested.clone();
returned.stamp_at = StampAt::Midpoint;
assert_eq!(
rejection_reason(&requested, &returned, NegotiationPolicy::Strict),
NegotiationRestriction::StampAtChanged {
requested: requested.stamp_at,
negotiated: returned.stamp_at,
}
.message()
);
let mut returned = requested.clone();
returned.clock = Clock::Wall;
assert_eq!(
rejection_reason(&requested, &returned, NegotiationPolicy::Strict),
NegotiationRestriction::ClockChanged {
requested: requested.clock,
negotiated: returned.clock,
}
.message()
);
let mut returned = requested.clone();
returned.server_fill = None;
assert_eq!(
rejection_reason(&requested, &returned, NegotiationPolicy::Strict),
NegotiationRestriction::ServerFillChanged.message()
);
}
#[test]
fn loose_duration_negotiation_uses_run_duration_semantics() {
let requested = default_params();
-19
View File
@@ -174,25 +174,6 @@ mod tests {
assert_eq!(normalize_server_addr("[::1]:1234"), "[::1]:1234");
}
#[test]
fn address_family_filtering_remains_unchanged() {
let ipv4 = SocketAddr::from(([127, 0, 0, 1], DEFAULT_PORT));
let ipv6 = SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 1], DEFAULT_PORT));
let mut config = ClientConfig::default();
assert!(address_family_allowed(&config, ipv4));
assert!(address_family_allowed(&config, ipv6));
config.socket_config.ipv4_only = true;
assert!(address_family_allowed(&config, ipv4));
assert!(!address_family_allowed(&config, ipv6));
config.socket_config.ipv4_only = false;
config.socket_config.ipv6_only = true;
assert!(!address_family_allowed(&config, ipv4));
assert!(address_family_allowed(&config, ipv6));
}
#[cfg(feature = "tokio")]
#[test]
fn tokio_ipv6_only_is_applied_before_bind() {