refactor(client): separate accepted negotiation state from peer evidence

This commit is contained in:
Nils Andreas Svee committed 2026-10-04 18:19:28 +07:00
1 parent f62e6a9953
commit d467211f41
32 files changed
+507 -447

No files matched your search

@@ -52,25 +52,24 @@ async fn exercise_client_server() {
let opened = client.open().await.unwrap();
match &opened {
OpenOutcome::Started {
OpenOutcome::Started(irtt_client::SessionStarted {
remote,
token,
negotiated,
event,
} => {
negotiation: negotiated,
..
}) => {
assert_eq!(*remote, server_addr);
assert_ne!(*token, 0);
assert_eq!(negotiated.params.received_stats, ReceivedStats::Both);
assert_eq!(negotiated.params.stamp_at, StampAt::Both);
assert_eq!(negotiated.params.clock, Clock::Both);
assert_eq!(negotiated.accepted.received_stats, ReceivedStats::Both);
assert_eq!(negotiated.accepted.stamp_at, StampAt::Both);
assert_eq!(negotiated.accepted.clock, Clock::Both);
// A default client requests no fill, and this open succeeded under
// the default strict negotiation policy — which it could not have
// if the server had answered an absent request with its own default
// descriptor. The server still fills the payload with it.
assert_eq!(negotiated.params.server_fill, None);
assert!(matches!(event, ClientEvent::SessionStarted { .. }));
assert_eq!(negotiated.accepted.server_fill, None);
}
OpenOutcome::NoTestCompleted { .. } => panic!("normal client unexpectedly ran no-test"),
OpenOutcome::NoTestCompleted(_) => panic!("normal client unexpectedly ran no-test"),
}
assert!(matches!(
@@ -327,7 +327,7 @@ fn ignored_open_traffic_cannot_extend_the_absolute_attempt_deadline() {
.unwrap();
assert!(matches!(
client.open().await.unwrap(),
OpenOutcome::Started { .. }
OpenOutcome::Started(_)
));
});
assert_eq!(server.finish().len(), 2);
@@ -404,7 +404,7 @@ fn authenticated_async_lifecycle_negotiates_dscp() {
.await
.unwrap();
let opened = client.open().await.unwrap();
assert_eq!(open_negotiated(&opened).params.dscp, 184);
assert_eq!(open_negotiated(&opened).accepted.dscp, 46);
client.send_probe().await.unwrap();
assert!(matches!(client.recv().await.unwrap().as_slice(), [ClientEvent::EchoReply { seq: 0, .. }]));
assert!(matches!(client.close().await.unwrap().as_slice(), [ClientEvent::SessionClosed { token, .. }] if *token == open_token(&opened)));
@@ -450,17 +450,23 @@ fn blocking_and_async_peer_close_are_semantically_equivalent() {
}
}
fn open_negotiated(outcome: &OpenOutcome) -> &irtt_client::NegotiatedParams {
fn open_negotiated(outcome: &OpenOutcome) -> &irtt_client::NegotiationResult {
match outcome {
OpenOutcome::Started { negotiated, .. }
| OpenOutcome::NoTestCompleted { negotiated, .. } => negotiated,
OpenOutcome::Started(irtt_client::SessionStarted {
negotiation: negotiated,
..
})
| OpenOutcome::NoTestCompleted(irtt_client::NoTestCompleted {
negotiation: negotiated,
..
}) => negotiated,
}
}
fn open_token(outcome: &OpenOutcome) -> u64 {
match outcome {
OpenOutcome::Started { token, .. } => *token,
OpenOutcome::NoTestCompleted { .. } => panic!("no-test open has no session token"),
OpenOutcome::Started(irtt_client::SessionStarted { token, .. }) => *token,
OpenOutcome::NoTestCompleted(_) => panic!("no-test open has no session token"),
}
}
@@ -577,8 +583,8 @@ fn blocking_and_async_roll_back_a_failed_open_and_accept_a_retry() {
// state that refuses the retry.
assert_eq!(error_name(&blocking_first), "open timeout");
assert_eq!(error_name(&blocking_first), error_name(&async_first));
assert!(matches!(blocking_second, OpenOutcome::Started { .. }));
assert!(matches!(async_second, OpenOutcome::Started { .. }));
assert!(matches!(blocking_second, OpenOutcome::Started(_)));
assert!(matches!(async_second, OpenOutcome::Started(_)));
assert_eq!(
open_negotiated(&blocking_second),
open_negotiated(&async_second)
@@ -610,10 +616,7 @@ fn blocking_and_async_complete_every_caller_paced_probe() {
.set_recv_timeout(Some(Duration::from_millis(200)))
.unwrap();
let outcome = blocking.open().unwrap();
assert_eq!(
blocking.negotiated_params(),
Some(open_negotiated(&outcome))
);
assert_eq!(blocking.negotiation(), Some(open_negotiated(&outcome)));
assert!(!blocking.has_pending_probes());
// Oversleeping cannot invalidate this: low-level sends remain caller-owned
// after the negotiated duration. The compliant server has no duration cap.
@@ -636,7 +639,7 @@ fn blocking_and_async_complete_every_caller_paced_probe() {
.await
.unwrap();
let outcome = client.open().await.unwrap();
assert_eq!(client.negotiated_params(), Some(open_negotiated(&outcome)));
assert_eq!(client.negotiation(), Some(open_negotiated(&outcome)));
tokio::time::sleep(Duration::from_millis(2)).await;
let mut sent = Vec::new();
let mut replies = Vec::new();
@@ -81,10 +81,16 @@ fn server_fill_negotiated_params_reflect_accepted_value() {
.unwrap();
let outcome = client.open().unwrap();
let negotiated = match outcome {
irtt_client::OpenOutcome::Started { negotiated, .. } => negotiated,
irtt_client::OpenOutcome::Started(irtt_client::SessionStarted {
negotiation: negotiated,
..
}) => negotiated,
other => panic!("expected started outcome, got {other:?}"),
};
assert_eq!(negotiated.params.server_fill, params.server_fill);
assert_eq!(
negotiated.accepted.server_fill,
params.server_fill.map(|fill| fill.value)
);
client.send_probe().unwrap();
assert!(matches!(
@@ -144,7 +150,7 @@ fn server_fill_loose_allows_server_to_remove_fill() {
open_observation_params(&run).server_fill,
requested.server_fill
);
assert_eq!(run.negotiated.params.server_fill, None);
assert_eq!(run.negotiated.accepted.server_fill, None);
}
// ─── Echo behavior ───
@@ -169,11 +175,11 @@ fn server_fill_does_not_affect_client_side_packet_layout() {
let (echo_len_without, _, _, _) = echo_observation(&run_without);
assert_eq!(echo_len_with, echo_len_without);
assert_eq!(
test_echo_packet_len(false, &run_with.negotiated.params),
test_echo_packet_len(false, &run_with.negotiated.peer_params),
echo_len_with
);
assert_eq!(
test_echo_packet_len(false, &run_without.negotiated.params),
test_echo_packet_len(false, &run_without.negotiated.peer_params),
echo_len_without
);
}
@@ -10,7 +10,7 @@
use std::time::Duration;
use irtt_client::{
AsyncClient, ClientConfig, NegotiationPolicy, NegotiationRestriction, OpenOutcome, OpenPolicy,
AsyncClient, ClientConfig, NegotiationChange, NegotiationPolicy, OpenOutcome, OpenPolicy,
SessionRequest,
};
use irtt_proto::{
@@ -166,21 +166,23 @@ async fn exercise_fallback_negotiation() {
.await
.unwrap();
match loose.open().await.unwrap() {
OpenOutcome::Started { negotiated, .. } => {
OpenOutcome::Started(irtt_client::SessionStarted {
negotiation: negotiated,
..
}) => {
assert_eq!(
negotiated
.params
.server_fill
.map(|fill| fill.value)
.as_deref(),
negotiated.accepted.server_fill.as_deref(),
Some("pattern:69727474"),
"the server reports the default descriptor it fell back to"
);
assert!(negotiated
.restrictions
.contains(&NegotiationRestriction::ServerFillChanged));
.changes
.contains(&NegotiationChange::ServerFillChanged {
requested: Some("bogus".to_owned()),
negotiated: Some("pattern:69727474".to_owned())
}));
}
OpenOutcome::NoTestCompleted { .. } => panic!("normal client unexpectedly ran no-test"),
OpenOutcome::NoTestCompleted(_) => panic!("normal client unexpectedly ran no-test"),
}
loose.close().await.unwrap();
+6 -14
View File
@@ -31,7 +31,7 @@ fn hmac_open_success_negotiates_without_warnings() {
},
);
assert_eq!(run.negotiated.params, params);
assert_eq!(run.negotiated.peer_params, params);
match &run.observations[0] {
ServerObservation::Open { params: got, hmac } => {
assert_eq!(got, &params);
@@ -249,21 +249,13 @@ fn non_hmac_client_open_does_not_set_hmac_flag() {
fn assert_started(outcome: OpenOutcome, expected: &irtt_proto::Params) {
match outcome {
OpenOutcome::Started {
OpenOutcome::Started(irtt_client::SessionStarted {
token,
negotiated,
event:
ClientEvent::SessionStarted {
token: event_token,
negotiated: event_negotiated,
..
},
negotiation: negotiated,
..
} => {
}) => {
assert_eq!(token, TOKEN);
assert_eq!(event_token, TOKEN);
assert_eq!(negotiated.params, *expected);
assert_eq!(event_negotiated.params, *expected);
assert_eq!(negotiated.peer_params, *expected);
}
other => panic!("expected started open outcome, got {other:?}"),
}
@@ -297,7 +289,7 @@ fn backend_hmac_correct_key_succeeds() {
.set_recv_timeout(Some(Duration::from_millis(500)))
.unwrap();
let outcome = client.open().unwrap();
assert!(matches!(outcome, OpenOutcome::Started { .. }));
assert!(matches!(outcome, OpenOutcome::Started(_)));
let sent = client.send_probe().unwrap();
assert_eq!(sent.len(), 1);
@@ -93,7 +93,7 @@ fn backend_large_packet_smoke() {
.unwrap();
let outcome = client.open().unwrap();
assert!(matches!(outcome, irtt_client::OpenOutcome::Started { .. }));
assert!(matches!(outcome, irtt_client::OpenOutcome::Started(_)));
client.send_probe().unwrap();
let events = client.recv_once().unwrap();
@@ -162,7 +162,7 @@ async fn target_updates_preserve_identical_sessions_and_retire_removed_generatio
match events.recv().await.unwrap() {
ManagedEvent::Client {
target,
event: ClientEvent::SessionStarted { .. },
event: ClientEvent::SessionStarted(_),
} if target == first => panic!("identical update reopened the session"),
ManagedEvent::Client {
target,
@@ -10,7 +10,7 @@
use std::time::Duration;
use irtt_client::{
AsyncClient, ClientConfig, NegotiationPolicy, NegotiationRestriction, OpenOutcome, OpenPolicy,
AsyncClient, ClientConfig, NegotiationChange, NegotiationPolicy, OpenOutcome, OpenPolicy,
SessionRequest,
};
use irtt_proto::{Clock, ReceivedStats, StampAt};
@@ -40,22 +40,25 @@ async fn exercise_timestamp_restriction() {
let mut loose = client(server_addr, NegotiationPolicy::Loose).await;
let negotiated = match loose.open().await.unwrap() {
OpenOutcome::Started { negotiated, .. } => negotiated,
OpenOutcome::NoTestCompleted { .. } => panic!("normal client unexpectedly ran no-test"),
OpenOutcome::Started(irtt_client::SessionStarted {
negotiation: negotiated,
..
}) => negotiated,
OpenOutcome::NoTestCompleted(_) => panic!("normal client unexpectedly ran no-test"),
};
assert_eq!(
negotiated.params.stamp_at,
negotiated.accepted.stamp_at,
StampAt::Midpoint,
"a request for both instants is answered with the midpoint"
);
assert_eq!(
negotiated.params.clock,
negotiated.accepted.clock,
Clock::Both,
"the clock is not restricted with it"
);
assert!(negotiated
.restrictions
.contains(&NegotiationRestriction::StampAtChanged {
.changes
.contains(&NegotiationChange::StampAtChanged {
requested: StampAt::Both,
negotiated: StampAt::Midpoint,
}));
@@ -112,16 +115,19 @@ async fn exercise_dscp_restriction() {
.await
.unwrap();
let negotiated = match loose.open().await.unwrap() {
OpenOutcome::Started { negotiated, .. } => negotiated,
OpenOutcome::NoTestCompleted { .. } => panic!("normal client unexpectedly ran no-test"),
OpenOutcome::Started(irtt_client::SessionStarted {
negotiation: negotiated,
..
}) => negotiated,
OpenOutcome::NoTestCompleted(_) => panic!("normal client unexpectedly ran no-test"),
};
assert_eq!(
negotiated.params.dscp, 0,
"the raw wire parameter is negotiated to zero"
negotiated.accepted.dscp, 0,
"the accepted DSCP codepoint is zero"
);
assert!(negotiated
.restrictions
.contains(&NegotiationRestriction::DscpChanged {
.changes
.contains(&NegotiationChange::DscpChanged {
requested: 46,
negotiated: 0,
}));
@@ -159,14 +165,21 @@ async fn exercise_default_policy() {
.await
.unwrap();
let negotiated = match client.open().await.unwrap() {
OpenOutcome::Started { negotiated, .. } => negotiated,
OpenOutcome::NoTestCompleted { .. } => panic!("normal client unexpectedly ran no-test"),
OpenOutcome::Started(irtt_client::SessionStarted {
negotiation: negotiated,
..
}) => negotiated,
OpenOutcome::NoTestCompleted(_) => panic!("normal client unexpectedly ran no-test"),
};
assert_eq!(negotiated.restrictions, Vec::new());
assert_eq!(negotiated.params.stamp_at, StampAt::Both);
assert_eq!(negotiated.params.clock, Clock::Both);
assert_eq!(negotiated.params.dscp, 184, "the raw byte for codepoint 46");
assert_eq!(negotiated.changes, Vec::new());
assert_eq!(negotiated.accepted.stamp_at, StampAt::Both);
assert_eq!(negotiated.accepted.clock, Clock::Both);
assert_eq!(negotiated.accepted.dscp, 46);
assert_eq!(
negotiated.peer_params.dscp, 184,
"the raw byte for codepoint 46"
);
probe(&mut client).await;
client.close().await.unwrap();
@@ -7,8 +7,8 @@ mod protocol_options {
use std::time::Duration;
use irtt_client::{
Client, ClientConfig, ClientError, ClientEvent, OneWayDelaySample, ReceivedStatsSample,
RttSample, ServerTiming, SignedDuration,
AcceptedSessionParameters, Client, ClientConfig, ClientError, ClientEvent, OneWayDelaySample,
ReceivedStatsSample, RttSample, ServerTiming, SignedDuration,
};
use irtt_proto::{Clock, Params, ReceivedStats, StampAt, TimestampFields};
@@ -212,7 +212,20 @@ fn loose_open_uses_returned_params_for_echo_layout_and_reply_parsing() {
});
assert_eq!(open_params(&run), &requested);
assert_eq!(run.negotiated.params, returned);
assert_eq!(run.negotiated.peer_params, returned);
assert_eq!(
run.negotiated.accepted,
AcceptedSessionParameters {
duration: Some(Duration::from_secs(3)),
interval: Duration::from_secs(1),
length: 28,
received_stats: ReceivedStats::Count,
stamp_at: StampAt::Receive,
clock: Clock::Wall,
dscp: 0,
server_fill: None,
}
);
assert_echo_uses_params(&run, &returned, false);
let reply = expect_echo_reply(&run.reply);
@@ -253,7 +266,7 @@ fn changed_compatibility_params(requested: &Params) -> Vec<Params> {
}
fn assert_negotiated_echo_use(run: &OneProbeRun, params: &Params, hmac: bool) {
assert_eq!(run.negotiated.params, *params);
assert_eq!(run.negotiated.peer_params, *params);
assert_eq!(open_params(run), params);
assert_echo_uses_params(run, params, hmac);
}
@@ -14,7 +14,7 @@ fn backend_basic_open_echo_close() {
.unwrap();
let outcome = client.open().unwrap();
assert!(matches!(outcome, irtt_client::OpenOutcome::Started { .. }));
assert!(matches!(outcome, irtt_client::OpenOutcome::Started(_)));
let sent = client.send_probe().unwrap();
assert_eq!(sent.len(), 1);
@@ -13,7 +13,7 @@ use std::{
};
use irtt_client::{
Client, ClientConfig, ClientEvent, NegotiatedParams, OpenOutcome, OpenPolicy, SessionRequest,
Client, ClientConfig, ClientEvent, NegotiationResult, OpenOutcome, OpenPolicy, SessionRequest,
};
use irtt_proto::{
compute_hmac_in_place, decode_request, echo_packet_len, encode_echo_reply, encode_open_reply,
@@ -138,7 +138,7 @@ pub enum ServerObservation {
}
pub struct OneProbeRun {
pub negotiated: NegotiatedParams,
pub negotiated: NegotiationResult,
pub sent: ClientEvent,
pub reply: ClientEvent,
pub observations: Vec<ServerObservation>,
@@ -514,10 +514,13 @@ fn recv_request_timeout(socket: &UdpSocket) -> Option<Vec<u8>> {
.map(|(size, _)| buf[..size].to_vec())
}
fn assert_started(outcome: OpenOutcome) -> NegotiatedParams {
fn assert_started(outcome: OpenOutcome) -> NegotiationResult {
match outcome {
OpenOutcome::Started { negotiated, .. } => negotiated,
OpenOutcome::NoTestCompleted { .. } => panic!("expected started outcome"),
OpenOutcome::Started(irtt_client::SessionStarted {
negotiation: negotiated,
..
}) => negotiated,
OpenOutcome::NoTestCompleted(_) => panic!("expected started outcome"),
}
}
@@ -217,12 +217,15 @@ async fn opened_session(server_addr: SocketAddr, dscp: u8) -> OpenSession {
.await
.unwrap();
match client.open().await.unwrap() {
OpenOutcome::Started { negotiated, .. } => assert_eq!(
negotiated.params.dscp,
OpenOutcome::Started(irtt_client::SessionStarted {
negotiation: negotiated,
..
}) => assert_eq!(
negotiated.peer_params.dscp,
i64::from(dscp) << 2,
"the server negotiates the raw byte the codepoint asks for"
),
OpenOutcome::NoTestCompleted { .. } => panic!("a normal client unexpectedly ran no-test"),
OpenOutcome::NoTestCompleted(_) => panic!("a normal client unexpectedly ran no-test"),
}
OpenSession {
client,
@@ -220,11 +220,11 @@ async fn connected_client(server_addr: SocketAddr) -> AsyncClient {
async fn assert_started(client: &mut AsyncClient, server_addr: SocketAddr) {
match client.open().await.unwrap() {
OpenOutcome::Started { remote, token, .. } => {
OpenOutcome::Started(irtt_client::SessionStarted { remote, token, .. }) => {
assert_eq!(remote, server_addr);
assert_ne!(token, 0);
}
OpenOutcome::NoTestCompleted { .. } => panic!("a normal client unexpectedly ran no-test"),
OpenOutcome::NoTestCompleted(_) => panic!("a normal client unexpectedly ran no-test"),
}
}