feat(server): add open session core #74
No files matched your search
@@ -139,6 +139,16 @@ fn params_from_config_rejects_invalid_values() {
|
|||||||
},
|
},
|
||||||
"dscp",
|
"dscp",
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
// The unspecified clock is the absent-tag wire default, not a
|
||||||
|
// client mode; a client that needs timestamps must name a clock.
|
||||||
|
"unspecified clock",
|
||||||
|
ClientConfig {
|
||||||
|
clock: Clock::Unspecified,
|
||||||
|
..ClientConfig::default()
|
||||||
|
},
|
||||||
|
"clock must be wall, monotonic, or both",
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"empty server fill",
|
"empty server fill",
|
||||||
ClientConfig {
|
ClientConfig {
|
||||||
|
|||||||
@@ -77,6 +77,10 @@ pub struct ClientConfig {
|
|||||||
/// Requested timestamp placement in echo replies.
|
/// Requested timestamp placement in echo replies.
|
||||||
pub stamp_at: StampAt,
|
pub stamp_at: StampAt,
|
||||||
/// Requested server clock sources for timestamp fields.
|
/// Requested server clock sources for timestamp fields.
|
||||||
|
///
|
||||||
|
/// [`Clock::Unspecified`] is the wire default meaning "no clock tag was
|
||||||
|
/// sent" and is not a client mode; opening a session with it is rejected.
|
||||||
|
/// Request [`Clock::Wall`], [`Clock::Monotonic`], or [`Clock::Both`].
|
||||||
pub clock: Clock,
|
pub clock: Clock,
|
||||||
/// Requested DSCP codepoint.
|
/// Requested DSCP codepoint.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use irtt_proto::{
|
use irtt_proto::{
|
||||||
decode_echo_reply, echo_packet_len, encode_request, flags, EchoReply, OpenReply, Params,
|
decode_echo_reply, echo_packet_len, encode_request, flags, Clock, EchoReply, OpenReply, Params,
|
||||||
RequestToEncode, ServerFill, TimestampFields, PROTOCOL_VERSION,
|
RequestToEncode, ServerFill, TimestampFields, PROTOCOL_VERSION,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -989,6 +989,11 @@ fn validate_protocol_config(config: &ClientConfig) -> Result<(), ClientError> {
|
|||||||
reason: "interval must be greater than zero".to_owned(),
|
reason: "interval must be greater than zero".to_owned(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if config.clock == Clock::Unspecified {
|
||||||
|
return Err(ClientError::InvalidConfig {
|
||||||
|
reason: "clock must be wall, monotonic, or both".to_owned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
if config.dscp > MAX_DSCP_CODEPOINT {
|
if config.dscp > MAX_DSCP_CODEPOINT {
|
||||||
return Err(ClientError::InvalidConfig {
|
return Err(ClientError::InvalidConfig {
|
||||||
reason: format!("dscp must be <= {MAX_DSCP_CODEPOINT}"),
|
reason: format!("dscp must be <= {MAX_DSCP_CODEPOINT}"),
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ fn clock_modes_drive_reply_event_clock_fields() {
|
|||||||
assert_timing_fields(reply.server_timing, true, true, false, false, true, true);
|
assert_timing_fields(reply.server_timing, true, true, false, false, true, true);
|
||||||
assert_one_way_presence(reply.one_way, true, true);
|
assert_one_way_presence(reply.one_way, true, true);
|
||||||
}
|
}
|
||||||
|
Clock::Unspecified => unreachable!("clients never request an unspecified clock"),
|
||||||
}
|
}
|
||||||
assert_processing_subtracted(reply);
|
assert_processing_subtracted(reply);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,6 +127,9 @@ fn backend_clock_smoke() {
|
|||||||
assert!(st.send_wall_ns.is_some());
|
assert!(st.send_wall_ns.is_some());
|
||||||
assert!(st.send_mono_ns.is_some());
|
assert!(st.send_mono_ns.is_some());
|
||||||
}
|
}
|
||||||
|
Clock::Unspecified => {
|
||||||
|
unreachable!("clients never request an unspecified clock")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
other => panic!("expected EchoReply, got {other:?}"),
|
other => panic!("expected EchoReply, got {other:?}"),
|
||||||
|
|||||||
@@ -13,8 +13,13 @@ use crate::{
|
|||||||
/// header.
|
/// header.
|
||||||
const MIDPOINT_COMPAT_EXTRA: usize = TIMESTAMP_SIZE;
|
const MIDPOINT_COMPAT_EXTRA: usize = TIMESTAMP_SIZE;
|
||||||
|
|
||||||
|
/// Whether the negotiation selects a midpoint timestamp from exactly one clock,
|
||||||
|
/// which is the only case the upstream dual-field compatibility form applies to.
|
||||||
|
///
|
||||||
|
/// [`Clock::Unspecified`] selects no clock and therefore lays out no midpoint
|
||||||
|
/// field at all, so it is not a single-clock negotiation.
|
||||||
fn is_midpoint_single_clock(params: &Params) -> bool {
|
fn is_midpoint_single_clock(params: &Params) -> bool {
|
||||||
params.stamp_at == StampAt::Midpoint && !matches!(params.clock, Clock::Both)
|
params.stamp_at == StampAt::Midpoint && matches!(params.clock, Clock::Wall | Clock::Monotonic)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Total datagram length of the upstream dual-midpoint compatibility form.
|
/// Total datagram length of the upstream dual-midpoint compatibility form.
|
||||||
@@ -135,7 +140,7 @@ pub fn decode_echo_reply(
|
|||||||
match params.clock {
|
match params.clock {
|
||||||
Clock::Wall => (Some(wall), None),
|
Clock::Wall => (Some(wall), None),
|
||||||
Clock::Monotonic => (None, Some(mono)),
|
Clock::Monotonic => (None, Some(mono)),
|
||||||
Clock::Both => {
|
Clock::Unspecified | Clock::Both => {
|
||||||
unreachable!("midpoint compat only applies to single-clock negotiations")
|
unreachable!("midpoint compat only applies to single-clock negotiations")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ mod tests {
|
|||||||
ReceivedStats::Both => RECV_COUNT_SIZE + RECV_WINDOW_SIZE,
|
ReceivedStats::Both => RECV_COUNT_SIZE + RECV_WINDOW_SIZE,
|
||||||
};
|
};
|
||||||
let clock_count = match clock {
|
let clock_count = match clock {
|
||||||
|
Clock::Unspecified => 0,
|
||||||
Clock::Wall | Clock::Monotonic => 1,
|
Clock::Wall | Clock::Monotonic => 1,
|
||||||
Clock::Both => 2,
|
Clock::Both => 2,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,12 @@
|
|||||||
//! performs no additional validation, so callers that build `Params` manually
|
//! performs no additional validation, so callers that build `Params` manually
|
||||||
//! are responsible for validating those values before sending them.
|
//! are responsible for validating those values before sending them.
|
||||||
//!
|
//!
|
||||||
|
//! Parameters are optional on the wire, and an absent tag takes its wire
|
||||||
|
//! default — zero for every integer field, and [`Clock::Unspecified`] for the
|
||||||
|
//! clock. A receiver that must distinguish an omitted tag from one explicitly
|
||||||
|
//! encoded as zero uses [`Params::decode_with_presence`], which runs the same
|
||||||
|
//! parser and additionally reports a [`ParamPresence`].
|
||||||
|
//!
|
||||||
//! The crate also provides packet layout calculation, packet encoding and
|
//! The crate also provides packet layout calculation, packet encoding and
|
||||||
//! decoding, and optional HMAC placement, computation, and verification
|
//! decoding, and optional HMAC placement, computation, and verification
|
||||||
//! helpers.
|
//! helpers.
|
||||||
@@ -51,7 +57,10 @@ pub use flags::*;
|
|||||||
pub use hmac::{compute_hmac, compute_hmac_in_place, verify_hmac, verify_packet_hmac};
|
pub use hmac::{compute_hmac, compute_hmac_in_place, verify_hmac, verify_packet_hmac};
|
||||||
pub use layout::{echo_header_len, echo_packet_len, PacketLayout};
|
pub use layout::{echo_header_len, echo_packet_len, PacketLayout};
|
||||||
pub use open::{decode_open_reply, encode_open_reply, OpenReply};
|
pub use open::{decode_open_reply, encode_open_reply, OpenReply};
|
||||||
pub use params::{Clock, Params, ReceivedStats, ServerFill, StampAt, MAX_SERVER_FILL_BYTES};
|
pub use params::{
|
||||||
|
Clock, DecodedParams, ParamPresence, Params, ReceivedStats, ServerFill, StampAt,
|
||||||
|
MAX_SERVER_FILL_BYTES,
|
||||||
|
};
|
||||||
pub use request::{
|
pub use request::{
|
||||||
decode_request, encode_request, DecodedRequest, DecodedRequestKind, RequestToEncode,
|
decode_request, encode_request, DecodedRequest, DecodedRequestKind, RequestToEncode,
|
||||||
};
|
};
|
||||||
|
|||||||
+281
-10
@@ -11,6 +11,11 @@ pub const MAX_SERVER_FILL_BYTES: usize = 32;
|
|||||||
/// Higher-level callers should validate user or configuration input before
|
/// Higher-level callers should validate user or configuration input before
|
||||||
/// encoding. The normal `irtt-client` configuration path enforces
|
/// encoding. The normal `irtt-client` configuration path enforces
|
||||||
/// [`MAX_SERVER_FILL_BYTES`] for `server_fill`.
|
/// [`MAX_SERVER_FILL_BYTES`] for `server_fill`.
|
||||||
|
///
|
||||||
|
/// [`Params::default`] is the **wire** default: every integer field is zero and
|
||||||
|
/// [`clock`](Params::clock) is [`Clock::Unspecified`], which is what an open
|
||||||
|
/// request with an empty parameter payload means. It is not a set of sensible
|
||||||
|
/// client settings; a client builds its request from its own configuration.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
pub struct Params {
|
pub struct Params {
|
||||||
pub protocol_version: i64,
|
pub protocol_version: i64,
|
||||||
@@ -63,21 +68,67 @@ impl Params {
|
|||||||
///
|
///
|
||||||
/// This includes invalid enum values, malformed UTF-8, and `server_fill`
|
/// This includes invalid enum values, malformed UTF-8, and `server_fill`
|
||||||
/// values longer than [`MAX_SERVER_FILL_BYTES`].
|
/// values longer than [`MAX_SERVER_FILL_BYTES`].
|
||||||
|
///
|
||||||
|
/// Absent parameters take their wire default, so a caller cannot tell an
|
||||||
|
/// omitted tag from one explicitly encoded as zero. Use
|
||||||
|
/// [`decode_with_presence`](Params::decode_with_presence) when that
|
||||||
|
/// distinction matters; both share one parser.
|
||||||
pub fn decode(input: &[u8]) -> Result<Self> {
|
pub fn decode(input: &[u8]) -> Result<Self> {
|
||||||
|
Self::decode_with_presence(input).map(|decoded| decoded.params)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decodes parameters and additionally reports which known tags appeared.
|
||||||
|
///
|
||||||
|
/// This is the same parser and the same validation as [`decode`], with the
|
||||||
|
/// presence of each known tag retained. A receiver needs it because the
|
||||||
|
/// protocol gives an omitted tag and an explicit zero the same value but
|
||||||
|
/// not the same meaning: an absent Duration or Interval is accepted as the
|
||||||
|
/// wire default zero, while one explicitly encoded as zero is invalid.
|
||||||
|
///
|
||||||
|
/// Presence means the tag appeared at least once. Repeated known tags keep
|
||||||
|
/// last-value-wins, and unknown tags remain ignored and untracked.
|
||||||
|
///
|
||||||
|
/// [`decode`]: Params::decode
|
||||||
|
pub fn decode_with_presence(input: &[u8]) -> Result<DecodedParams> {
|
||||||
let mut params = Self::default();
|
let mut params = Self::default();
|
||||||
|
let mut presence = ParamPresence::default();
|
||||||
let mut pos = 0;
|
let mut pos = 0;
|
||||||
while pos < input.len() {
|
while pos < input.len() {
|
||||||
let (tag, used) = varint::decode_uvarint(&input[pos..])?;
|
let (tag, used) = varint::decode_uvarint(&input[pos..])?;
|
||||||
pos += used;
|
pos += used;
|
||||||
match tag {
|
match tag {
|
||||||
1 => params.protocol_version = read_int(input, &mut pos)?,
|
1 => {
|
||||||
2 => params.duration_ns = read_int(input, &mut pos)?,
|
params.protocol_version = read_int(input, &mut pos)?;
|
||||||
3 => params.interval_ns = read_int(input, &mut pos)?,
|
presence.protocol_version = true;
|
||||||
4 => params.length = read_int(input, &mut pos)?,
|
}
|
||||||
5 => params.received_stats = ReceivedStats::try_from(read_int(input, &mut pos)?)?,
|
2 => {
|
||||||
6 => params.stamp_at = StampAt::try_from(read_int(input, &mut pos)?)?,
|
params.duration_ns = read_int(input, &mut pos)?;
|
||||||
7 => params.clock = Clock::try_from(read_int(input, &mut pos)?)?,
|
presence.duration_ns = true;
|
||||||
8 => params.dscp = read_int(input, &mut pos)?,
|
}
|
||||||
|
3 => {
|
||||||
|
params.interval_ns = read_int(input, &mut pos)?;
|
||||||
|
presence.interval_ns = true;
|
||||||
|
}
|
||||||
|
4 => {
|
||||||
|
params.length = read_int(input, &mut pos)?;
|
||||||
|
presence.length = true;
|
||||||
|
}
|
||||||
|
5 => {
|
||||||
|
params.received_stats = ReceivedStats::try_from(read_int(input, &mut pos)?)?;
|
||||||
|
presence.received_stats = true;
|
||||||
|
}
|
||||||
|
6 => {
|
||||||
|
params.stamp_at = StampAt::try_from(read_int(input, &mut pos)?)?;
|
||||||
|
presence.stamp_at = true;
|
||||||
|
}
|
||||||
|
7 => {
|
||||||
|
params.clock = Clock::try_from(read_int(input, &mut pos)?)?;
|
||||||
|
presence.clock = true;
|
||||||
|
}
|
||||||
|
8 => {
|
||||||
|
params.dscp = read_int(input, &mut pos)?;
|
||||||
|
presence.dscp = true;
|
||||||
|
}
|
||||||
9 => {
|
9 => {
|
||||||
let (len, used) = varint::decode_uvarint(&input[pos..])?;
|
let (len, used) = varint::decode_uvarint(&input[pos..])?;
|
||||||
pos += used;
|
pos += used;
|
||||||
@@ -97,6 +148,7 @@ impl Params {
|
|||||||
.to_owned();
|
.to_owned();
|
||||||
pos += len;
|
pos += len;
|
||||||
params.server_fill = Some(ServerFill { value });
|
params.server_fill = Some(ServerFill { value });
|
||||||
|
presence.server_fill = true;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let (_, used) = varint::decode_uvarint(&input[pos..])?;
|
let (_, used) = varint::decode_uvarint(&input[pos..])?;
|
||||||
@@ -104,10 +156,38 @@ impl Params {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(params)
|
Ok(DecodedParams { params, presence })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Decoded parameters together with which known tags the payload carried.
|
||||||
|
///
|
||||||
|
/// Produced by [`Params::decode_with_presence`].
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
|
pub struct DecodedParams {
|
||||||
|
/// The decoded values. Absent parameters hold their wire default.
|
||||||
|
pub params: Params,
|
||||||
|
/// Which known tags appeared in the payload.
|
||||||
|
pub presence: ParamPresence,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Which known parameter tags a decoded payload carried.
|
||||||
|
///
|
||||||
|
/// A field is `true` when its tag appeared at least once, whatever value it
|
||||||
|
/// carried. Unknown tags are ignored and are not represented here.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub struct ParamPresence {
|
||||||
|
pub protocol_version: bool,
|
||||||
|
pub duration_ns: bool,
|
||||||
|
pub interval_ns: bool,
|
||||||
|
pub length: bool,
|
||||||
|
pub received_stats: bool,
|
||||||
|
pub stamp_at: bool,
|
||||||
|
pub clock: bool,
|
||||||
|
pub dscp: bool,
|
||||||
|
pub server_fill: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct ServerFill {
|
pub struct ServerFill {
|
||||||
pub value: String,
|
pub value: String,
|
||||||
@@ -179,12 +259,31 @@ impl TryFrom<i64> for StampAt {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Which server clock sources supply timestamp fields.
|
||||||
|
///
|
||||||
|
/// # The zero value
|
||||||
|
///
|
||||||
|
/// [`Clock::Unspecified`] is the **wire default**, meaning the Clock tag was
|
||||||
|
/// absent from an open parameter payload — which is valid, and is what an empty
|
||||||
|
/// payload decodes to. It does not mean a peer may send an explicit Clock tag
|
||||||
|
/// encoding zero: [`TryFrom<i64>`](Clock::try_from) still rejects an explicit
|
||||||
|
/// zero as an invalid enum value, so this state is only ever reached by
|
||||||
|
/// omission. [`encode`](Params::encode) omits the tag for `Unspecified` rather
|
||||||
|
/// than emitting an explicit zero, which round-trips absence faithfully.
|
||||||
|
///
|
||||||
|
/// `Unspecified` selects no clock, so [`has_wall`](Clock::has_wall) and
|
||||||
|
/// [`has_mono`](Clock::has_mono) are both false for it and no timestamp field
|
||||||
|
/// is laid out. A client that wants timestamps must request a real clock; the
|
||||||
|
/// `irtt-client` configuration path rejects `Unspecified`.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
#[repr(i64)]
|
#[repr(i64)]
|
||||||
pub enum Clock {
|
pub enum Clock {
|
||||||
|
/// Wire default for an absent Clock tag. Never produced from an explicit
|
||||||
|
/// encoded value.
|
||||||
|
#[default]
|
||||||
|
Unspecified = 0,
|
||||||
Wall = 1,
|
Wall = 1,
|
||||||
Monotonic = 2,
|
Monotonic = 2,
|
||||||
#[default]
|
|
||||||
Both = 3,
|
Both = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,6 +300,11 @@ impl Clock {
|
|||||||
impl TryFrom<i64> for Clock {
|
impl TryFrom<i64> for Clock {
|
||||||
type Error = ProtoError;
|
type Error = ProtoError;
|
||||||
|
|
||||||
|
/// Converts an **explicitly encoded** Clock value.
|
||||||
|
///
|
||||||
|
/// Zero is rejected: it is only valid as an absent tag, never as an encoded
|
||||||
|
/// one. [`Clock::Unspecified`] is therefore unreachable through this
|
||||||
|
/// conversion by design.
|
||||||
fn try_from(value: i64) -> Result<Self> {
|
fn try_from(value: i64) -> Result<Self> {
|
||||||
match value {
|
match value {
|
||||||
1 => Ok(Self::Wall),
|
1 => Ok(Self::Wall),
|
||||||
@@ -244,6 +348,13 @@ mod tests {
|
|||||||
encoded
|
encoded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn encode_int(tag: u64, value: i64) -> Vec<u8> {
|
||||||
|
let mut encoded = Vec::new();
|
||||||
|
varint::encode_uvarint(tag, &mut encoded);
|
||||||
|
varint::encode_varint(value, &mut encoded);
|
||||||
|
encoded
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn params_round_trip() {
|
fn params_round_trip() {
|
||||||
let params = Params {
|
let params = Params {
|
||||||
@@ -452,6 +563,166 @@ mod tests {
|
|||||||
value: 0,
|
value: 0,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Params::decode_with_presence(&encoded),
|
||||||
|
Err(ProtoError::InvalidEnum {
|
||||||
|
name: "Clock",
|
||||||
|
value: 0,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn explicit_clock_values_one_to_three_are_accepted() {
|
||||||
|
for (value, expected) in [(1, Clock::Wall), (2, Clock::Monotonic), (3, Clock::Both)] {
|
||||||
|
let decoded = Params::decode_with_presence(&encode_int(7, value)).unwrap();
|
||||||
|
assert_eq!(decoded.params.clock, expected);
|
||||||
|
assert!(decoded.presence.clock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_payload_decodes_to_wire_defaults_with_nothing_present() {
|
||||||
|
let decoded = Params::decode_with_presence(&[]).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(decoded.params, Params::default());
|
||||||
|
assert_eq!(decoded.params.protocol_version, 0);
|
||||||
|
assert_eq!(decoded.params.duration_ns, 0);
|
||||||
|
assert_eq!(decoded.params.interval_ns, 0);
|
||||||
|
assert_eq!(decoded.params.length, 0);
|
||||||
|
assert_eq!(decoded.params.dscp, 0);
|
||||||
|
assert_eq!(decoded.params.received_stats, ReceivedStats::None);
|
||||||
|
assert_eq!(decoded.params.stamp_at, StampAt::None);
|
||||||
|
// The wire default clock is zero, not `Both`.
|
||||||
|
assert_eq!(decoded.params.clock, Clock::Unspecified);
|
||||||
|
assert!(!decoded.params.clock.has_wall());
|
||||||
|
assert!(!decoded.params.clock.has_mono());
|
||||||
|
assert_eq!(decoded.params.server_fill, None);
|
||||||
|
|
||||||
|
assert_eq!(decoded.presence, ParamPresence::default());
|
||||||
|
assert_eq!(
|
||||||
|
decoded.presence,
|
||||||
|
ParamPresence {
|
||||||
|
protocol_version: false,
|
||||||
|
duration_ns: false,
|
||||||
|
interval_ns: false,
|
||||||
|
length: false,
|
||||||
|
received_stats: false,
|
||||||
|
stamp_at: false,
|
||||||
|
clock: false,
|
||||||
|
dscp: false,
|
||||||
|
server_fill: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn absent_and_explicitly_zero_parameters_share_a_value_but_not_a_presence() {
|
||||||
|
// Duration and Interval are the pair a server must tell apart: absent is
|
||||||
|
// accepted, explicitly zero is not.
|
||||||
|
let absent = Params::decode_with_presence(&encode_int(1, 1)).unwrap();
|
||||||
|
assert_eq!(absent.params.duration_ns, 0);
|
||||||
|
assert!(!absent.presence.duration_ns);
|
||||||
|
assert_eq!(absent.params.interval_ns, 0);
|
||||||
|
assert!(!absent.presence.interval_ns);
|
||||||
|
assert_eq!(absent.params.clock, Clock::Unspecified);
|
||||||
|
assert!(!absent.presence.clock);
|
||||||
|
|
||||||
|
let explicit_duration = Params::decode_with_presence(&encode_int(2, 0)).unwrap();
|
||||||
|
assert_eq!(explicit_duration.params.duration_ns, 0);
|
||||||
|
assert!(explicit_duration.presence.duration_ns);
|
||||||
|
|
||||||
|
let explicit_interval = Params::decode_with_presence(&encode_int(3, 0)).unwrap();
|
||||||
|
assert_eq!(explicit_interval.params.interval_ns, 0);
|
||||||
|
assert!(explicit_interval.presence.interval_ns);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_known_tag_reports_its_own_presence() {
|
||||||
|
let mut encoded = Vec::new();
|
||||||
|
for (tag, value) in [
|
||||||
|
(1, 1),
|
||||||
|
(2, 5),
|
||||||
|
(3, 6),
|
||||||
|
(4, 7),
|
||||||
|
(5, 3),
|
||||||
|
(6, 4),
|
||||||
|
(7, 3),
|
||||||
|
(8, 184),
|
||||||
|
] {
|
||||||
|
encoded.extend_from_slice(&encode_int(tag, value));
|
||||||
|
}
|
||||||
|
encoded.extend_from_slice(&encode_server_fill_value(b"rand"));
|
||||||
|
|
||||||
|
let decoded = Params::decode_with_presence(&encoded).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
decoded.presence,
|
||||||
|
ParamPresence {
|
||||||
|
protocol_version: true,
|
||||||
|
duration_ns: true,
|
||||||
|
interval_ns: true,
|
||||||
|
length: true,
|
||||||
|
received_stats: true,
|
||||||
|
stamp_at: true,
|
||||||
|
clock: true,
|
||||||
|
dscp: true,
|
||||||
|
server_fill: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unspecified_clock_is_encoded_by_omission() {
|
||||||
|
let params = Params {
|
||||||
|
protocol_version: 1,
|
||||||
|
clock: Clock::Unspecified,
|
||||||
|
..Params::default()
|
||||||
|
};
|
||||||
|
let encoded = params.encode();
|
||||||
|
|
||||||
|
assert_eq!(encoded, encode_int(1, 1));
|
||||||
|
assert!(
|
||||||
|
!encoded.windows(2).any(|bytes| bytes[0] == 7),
|
||||||
|
"an unspecified clock must omit the tag rather than encode an explicit zero"
|
||||||
|
);
|
||||||
|
assert_round_trip(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_and_decode_with_presence_agree() {
|
||||||
|
let mut payloads = vec![
|
||||||
|
Vec::new(),
|
||||||
|
encode_int(1, 1),
|
||||||
|
encode_int(2, 0),
|
||||||
|
encode_server_fill_value(b"rand"),
|
||||||
|
];
|
||||||
|
payloads.push(
|
||||||
|
Params {
|
||||||
|
protocol_version: 1,
|
||||||
|
duration_ns: 3_000_000_000,
|
||||||
|
interval_ns: 1_000_000_000,
|
||||||
|
length: 1472,
|
||||||
|
received_stats: ReceivedStats::Both,
|
||||||
|
stamp_at: StampAt::Both,
|
||||||
|
clock: Clock::Both,
|
||||||
|
dscp: 184,
|
||||||
|
server_fill: Some(ServerFill {
|
||||||
|
value: "rand".to_owned(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
.encode(),
|
||||||
|
);
|
||||||
|
// Malformed payloads must fail identically through both entry points.
|
||||||
|
payloads.push(vec![1, 0x80]);
|
||||||
|
payloads.push(encode_int(7, 0));
|
||||||
|
|
||||||
|
for payload in payloads {
|
||||||
|
assert_eq!(
|
||||||
|
Params::decode(&payload),
|
||||||
|
Params::decode_with_presence(&payload).map(|decoded| decoded.params),
|
||||||
|
"decode and decode_with_presence disagreed on {payload:02x?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -413,7 +413,7 @@ fn midpoint_dual_field_reply_is_accepted_wherever_length_identifies_it() {
|
|||||||
// The first wire field must not be mistaken for the
|
// The first wire field must not be mistaken for the
|
||||||
// negotiated monotonic value.
|
// negotiated monotonic value.
|
||||||
Clock::Monotonic => (None, Some(222)),
|
Clock::Monotonic => (None, Some(222)),
|
||||||
Clock::Both => unreachable!(),
|
Clock::Unspecified | Clock::Both => unreachable!(),
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
reply.timestamps.midpoint_wall, expected_wall,
|
reply.timestamps.midpoint_wall, expected_wall,
|
||||||
@@ -463,7 +463,7 @@ fn midpoint_dual_field_reply_of_equal_length_is_accepted_but_not_correctable() {
|
|||||||
assert_eq!(reply.timestamps.midpoint_wall, None);
|
assert_eq!(reply.timestamps.midpoint_wall, None);
|
||||||
assert_eq!(reply.timestamps.midpoint_mono, Some(111));
|
assert_eq!(reply.timestamps.midpoint_mono, Some(111));
|
||||||
}
|
}
|
||||||
Clock::Both => unreachable!(),
|
Clock::Unspecified | Clock::Both => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user