refactor(proto): consolidate tests
This commit is contained in:
13 files changed
+1000
-1802
No files matched your search
@@ -0,0 +1,399 @@
|
||||
use irtt_proto::{
|
||||
varint, Clock, ParamPresence, Params, ProtoError, ReceivedStats, ServerFill, StampAt,
|
||||
};
|
||||
use proptest::prelude::*;
|
||||
|
||||
fn int(tag: u64, value: i64) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
varint::encode_uvarint(tag, &mut out);
|
||||
varint::encode_varint(value, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
fn fill(value: &[u8]) -> Vec<u8> {
|
||||
let mut out = vec![9];
|
||||
varint::encode_uvarint(value.len() as u64, &mut out);
|
||||
out.extend_from_slice(value);
|
||||
out
|
||||
}
|
||||
|
||||
fn presence(p: ParamPresence) -> [bool; 9] {
|
||||
[
|
||||
p.protocol_version,
|
||||
p.duration_ns,
|
||||
p.interval_ns,
|
||||
p.length,
|
||||
p.received_stats,
|
||||
p.stamp_at,
|
||||
p.clock,
|
||||
p.dscp,
|
||||
p.server_fill,
|
||||
]
|
||||
}
|
||||
|
||||
fn assert_error(bytes: &[u8], expected: ProtoError) {
|
||||
assert_eq!(Params::decode(bytes).unwrap_err(), expected, "{bytes:02x?}");
|
||||
assert_eq!(
|
||||
Params::decode_with_presence(bytes).unwrap_err(),
|
||||
expected,
|
||||
"{bytes:02x?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_encodings_preserve_raw_dscp_and_utf8_byte_lengths() {
|
||||
for (params, bytes) in [
|
||||
(
|
||||
Params {
|
||||
dscp: 46,
|
||||
..Params::default()
|
||||
},
|
||||
vec![8, 92],
|
||||
),
|
||||
(
|
||||
Params {
|
||||
dscp: 184,
|
||||
..Params::default()
|
||||
},
|
||||
vec![8, 0xf0, 2],
|
||||
),
|
||||
(
|
||||
Params {
|
||||
server_fill: Some(ServerFill {
|
||||
value: "rand".into(),
|
||||
}),
|
||||
..Params::default()
|
||||
},
|
||||
vec![9, 4, b'r', b'a', b'n', b'd'],
|
||||
),
|
||||
(
|
||||
Params {
|
||||
server_fill: Some(ServerFill { value: "é".into() }),
|
||||
..Params::default()
|
||||
},
|
||||
vec![9, 2, 0xc3, 0xa9],
|
||||
),
|
||||
] {
|
||||
assert_eq!(params.encode(), bytes);
|
||||
assert_eq!(Params::decode(&bytes).unwrap(), params);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wire_defaults_and_explicit_zero_presence() {
|
||||
let defaults = Params {
|
||||
protocol_version: 0,
|
||||
duration_ns: 0,
|
||||
interval_ns: 0,
|
||||
length: 0,
|
||||
received_stats: ReceivedStats::None,
|
||||
stamp_at: StampAt::None,
|
||||
clock: Clock::Unspecified,
|
||||
dscp: 0,
|
||||
server_fill: None,
|
||||
};
|
||||
let absent = Params::decode_with_presence(&[]).unwrap();
|
||||
assert_eq!(absent.params, defaults);
|
||||
assert_eq!(presence(absent.presence), [false; 9]);
|
||||
assert_eq!(defaults.encode(), Vec::<u8>::new());
|
||||
assert!(!defaults.clock.has_wall());
|
||||
assert!(!defaults.clock.has_mono());
|
||||
|
||||
// Clock is the sole integer tag for which explicit zero is invalid.
|
||||
for tag in [1, 2, 3, 4, 5, 6, 8] {
|
||||
let bytes = int(tag, 0);
|
||||
let decoded = Params::decode_with_presence(&bytes).unwrap();
|
||||
assert_eq!(decoded.params, defaults);
|
||||
let mut expected = [false; 9];
|
||||
expected[tag as usize - 1] = true;
|
||||
assert_eq!(presence(decoded.presence), expected, "tag {tag}");
|
||||
assert_eq!(Params::decode(&bytes).unwrap(), decoded.params);
|
||||
}
|
||||
let empty_fill = Params::decode_with_presence(&[9, 0]).unwrap();
|
||||
assert_eq!(
|
||||
empty_fill.params.server_fill,
|
||||
Some(ServerFill {
|
||||
value: String::new()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
presence(empty_fill.presence),
|
||||
[false, false, false, false, false, false, false, false, true]
|
||||
);
|
||||
assert_eq!(empty_fill.params.encode(), [9, 0]);
|
||||
assert_eq!(
|
||||
Params {
|
||||
protocol_version: 1,
|
||||
..defaults
|
||||
}
|
||||
.encode(),
|
||||
[1, 2]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_tags_map_to_their_fields_and_report_isolated_presence() {
|
||||
for (tag, value, expected) in [
|
||||
(
|
||||
1,
|
||||
1,
|
||||
Params {
|
||||
protocol_version: 1,
|
||||
..Params::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
2,
|
||||
5,
|
||||
Params {
|
||||
duration_ns: 5,
|
||||
..Params::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
3,
|
||||
6,
|
||||
Params {
|
||||
interval_ns: 6,
|
||||
..Params::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
4,
|
||||
7,
|
||||
Params {
|
||||
length: 7,
|
||||
..Params::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
8,
|
||||
184,
|
||||
Params {
|
||||
dscp: 184,
|
||||
..Params::default()
|
||||
},
|
||||
),
|
||||
] {
|
||||
let decoded = Params::decode_with_presence(&int(tag, value)).unwrap();
|
||||
assert_eq!(decoded.params, expected);
|
||||
let mut expected_presence = [false; 9];
|
||||
expected_presence[tag as usize - 1] = true;
|
||||
assert_eq!(presence(decoded.presence), expected_presence);
|
||||
}
|
||||
for (tag, modes) in [
|
||||
(
|
||||
5,
|
||||
vec![
|
||||
ReceivedStats::None as i64,
|
||||
ReceivedStats::Count as i64,
|
||||
ReceivedStats::Window as i64,
|
||||
ReceivedStats::Both as i64,
|
||||
],
|
||||
),
|
||||
(
|
||||
6,
|
||||
vec![
|
||||
StampAt::None as i64,
|
||||
StampAt::Send as i64,
|
||||
StampAt::Receive as i64,
|
||||
StampAt::Both as i64,
|
||||
StampAt::Midpoint as i64,
|
||||
],
|
||||
),
|
||||
(
|
||||
7,
|
||||
vec![
|
||||
Clock::Wall as i64,
|
||||
Clock::Monotonic as i64,
|
||||
Clock::Both as i64,
|
||||
],
|
||||
),
|
||||
] {
|
||||
for (index, mode) in modes.into_iter().enumerate() {
|
||||
let value = index as i64 + if tag == 7 { 1 } else { 0 };
|
||||
let decoded = Params::decode_with_presence(&int(tag, value)).unwrap();
|
||||
let actual = match tag {
|
||||
5 => decoded.params.received_stats as i64,
|
||||
6 => decoded.params.stamp_at as i64,
|
||||
7 => decoded.params.clock as i64,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
assert_eq!(actual, mode);
|
||||
let mut expected_presence = [false; 9];
|
||||
expected_presence[tag as usize - 1] = true;
|
||||
assert_eq!(presence(decoded.presence), expected_presence);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_values_and_invalid_enums_are_rejected_by_both_decoders() {
|
||||
for (tag, value, name) in [
|
||||
(5, 4, "ReceivedStats"),
|
||||
(6, 5, "StampAt"),
|
||||
(7, 0, "Clock"),
|
||||
(7, 4, "Clock"),
|
||||
(5, -1, "ReceivedStats"),
|
||||
(6, -1, "StampAt"),
|
||||
(7, -1, "Clock"),
|
||||
] {
|
||||
assert_error(&int(tag, value), ProtoError::InvalidEnum { name, value });
|
||||
}
|
||||
for bytes in [&[1, 0x80][..], &[99], &[0x80], &[9, 0x80]] {
|
||||
assert_error(bytes, ProtoError::TruncatedVarint);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_fill_accepts_valid_bytes_and_enforces_length_and_utf8_boundaries() {
|
||||
for value in [
|
||||
"",
|
||||
"rand",
|
||||
"0123456789abcdef0123456789abcdef",
|
||||
"éééééééééééééééé",
|
||||
] {
|
||||
let expected = Params {
|
||||
server_fill: Some(ServerFill {
|
||||
value: value.into(),
|
||||
}),
|
||||
..Params::default()
|
||||
};
|
||||
let bytes = fill(value.as_bytes());
|
||||
assert_eq!(expected.encode(), bytes);
|
||||
let decoded = Params::decode_with_presence(&bytes).unwrap();
|
||||
assert_eq!(decoded.params, expected);
|
||||
assert!(decoded.presence.server_fill);
|
||||
assert_eq!(Params::decode(&bytes).unwrap(), expected);
|
||||
}
|
||||
assert_error(
|
||||
&fill(b"0123456789abcdef0123456789abcdefx"),
|
||||
ProtoError::ParameterLengthTooLarge { tag: 9, length: 33 },
|
||||
);
|
||||
assert_error(&[9, 4, b'a', b'b'], ProtoError::MalformedParams);
|
||||
assert_error(&[9, 1, 0xff], ProtoError::InvalidUtf8);
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[test]
|
||||
fn server_fill_length_too_large_for_usize_is_rejected() {
|
||||
let mut bytes = vec![9];
|
||||
let length = u64::from(u32::MAX) + 1;
|
||||
varint::encode_uvarint(length, &mut bytes);
|
||||
assert_error(
|
||||
&bytes,
|
||||
ProtoError::ParameterLengthTooLarge { tag: 9, length },
|
||||
);
|
||||
}
|
||||
|
||||
fn string_strategy() -> impl Strategy<Value = String> {
|
||||
// Eight arbitrary Unicode scalars occupy at most the allowed 32 bytes.
|
||||
proptest::collection::vec(any::<char>(), 0..=8).prop_map(|chars| chars.into_iter().collect())
|
||||
}
|
||||
|
||||
fn params_strategy() -> impl Strategy<Value = Params> {
|
||||
(
|
||||
any::<[i64; 5]>(),
|
||||
prop::sample::select(vec![
|
||||
ReceivedStats::None,
|
||||
ReceivedStats::Count,
|
||||
ReceivedStats::Window,
|
||||
ReceivedStats::Both,
|
||||
]),
|
||||
prop::sample::select(vec![
|
||||
StampAt::None,
|
||||
StampAt::Send,
|
||||
StampAt::Receive,
|
||||
StampAt::Both,
|
||||
StampAt::Midpoint,
|
||||
]),
|
||||
prop::sample::select(vec![
|
||||
Clock::Unspecified,
|
||||
Clock::Wall,
|
||||
Clock::Monotonic,
|
||||
Clock::Both,
|
||||
]),
|
||||
prop::option::of(string_strategy()),
|
||||
)
|
||||
.prop_map(
|
||||
|(scalar, received_stats, stamp_at, clock, server_fill)| Params {
|
||||
protocol_version: scalar[0],
|
||||
duration_ns: scalar[1],
|
||||
interval_ns: scalar[2],
|
||||
length: scalar[3],
|
||||
dscp: scalar[4],
|
||||
received_stats,
|
||||
stamp_at,
|
||||
clock,
|
||||
server_fill: server_fill.map(|value| ServerFill { value }),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn all_tags(params: &Params) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
// Deliberately bypass encode's omission: duplicate zero values must be present.
|
||||
for (tag, value) in [
|
||||
(1, params.protocol_version),
|
||||
(2, params.duration_ns),
|
||||
(3, params.interval_ns),
|
||||
(4, params.length),
|
||||
(5, params.received_stats as i64),
|
||||
(6, params.stamp_at as i64),
|
||||
(7, params.clock as i64),
|
||||
(8, params.dscp),
|
||||
] {
|
||||
out.extend_from_slice(&int(tag, value));
|
||||
}
|
||||
out.extend_from_slice(&fill(params.server_fill.as_ref().unwrap().value.as_bytes()));
|
||||
out
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn valid_params_round_trip_with_omission_presence(params in params_strategy()) {
|
||||
let encoded = params.encode();
|
||||
let decoded = Params::decode_with_presence(&encoded).unwrap();
|
||||
prop_assert_eq!(&decoded.params, ¶ms);
|
||||
prop_assert_eq!(Params::decode(&encoded).unwrap(), params.clone());
|
||||
prop_assert_eq!(presence(decoded.presence), [
|
||||
params.protocol_version != 0, params.duration_ns != 0,
|
||||
params.interval_ns != 0, params.length != 0,
|
||||
params.received_stats != ReceivedStats::None, params.stamp_at != StampAt::None,
|
||||
params.clock != Clock::Unspecified, params.dscp != 0, params.server_fill.is_some(),
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_known_tags_are_last_wins(
|
||||
mut first in params_strategy(), mut second in params_strategy(),
|
||||
first_clock in 1_i64..=3, second_clock in 1_i64..=3,
|
||||
first_fill in string_strategy(), second_fill in string_strategy(),
|
||||
) {
|
||||
first.clock = Clock::try_from(first_clock).unwrap();
|
||||
second.clock = Clock::try_from(second_clock).unwrap();
|
||||
first.server_fill = Some(ServerFill { value: first_fill });
|
||||
second.server_fill = Some(ServerFill { value: second_fill });
|
||||
let mut bytes = all_tags(&first);
|
||||
bytes.extend_from_slice(&all_tags(&second));
|
||||
let decoded = Params::decode_with_presence(&bytes).unwrap();
|
||||
prop_assert_eq!(&decoded.params, &second);
|
||||
prop_assert_eq!(Params::decode(&bytes).unwrap(), second);
|
||||
prop_assert_eq!(presence(decoded.presence), [true; 9]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_scalar_tags_do_not_disturb_known_params(
|
||||
params in params_strategy(), tag in 10_u64..=u16::MAX as u64, value: u64,
|
||||
) {
|
||||
let encoded = params.encode();
|
||||
let expected = Params::decode_with_presence(&encoded).unwrap();
|
||||
let mut unknown = Vec::new();
|
||||
varint::encode_uvarint(tag, &mut unknown);
|
||||
varint::encode_uvarint(value, &mut unknown);
|
||||
for bytes in [[unknown.as_slice(), &encoded].concat(), [&encoded, unknown.as_slice()].concat()] {
|
||||
prop_assert_eq!(Params::decode_with_presence(&bytes).unwrap(), expected.clone());
|
||||
prop_assert_eq!(Params::decode(&bytes).unwrap(), params.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user