Skip to content

Commit 0ecb02b

Browse files
andyzzhaoclaude
andcommitted
chore(property-vals): property-based tests for the wire codec
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent f686969 commit 0ecb02b

1 file changed

Lines changed: 90 additions & 0 deletions

File tree

rust/property-vals-rs/src/wire.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,11 @@ fn get_byte(buf: &[u8], pos: &mut usize) -> Result<u8, DecodeError> {
139139

140140
#[cfg(test)]
141141
mod tests {
142+
use proptest::prelude::*;
143+
142144
use super::*;
145+
use crate::producer::Outgoing;
146+
use crate::types::IngestableEvent;
143147

144148
fn round_trip(team_id: i64, pt: PropertyType, key: &str, value: &str, count: u64) {
145149
let buf = encode(team_id, pt, key, value, count);
@@ -224,4 +228,90 @@ mod tests {
224228
"binary {binary} should be under half of json {json}"
225229
);
226230
}
231+
232+
fn arb_property_type() -> impl Strategy<Value = PropertyType> {
233+
prop_oneof![
234+
Just(PropertyType::Event),
235+
Just(PropertyType::Person),
236+
any::<u8>().prop_map(PropertyType::Group),
237+
]
238+
}
239+
240+
proptest! {
241+
#[test]
242+
fn round_trips_any_message(
243+
team_id: i64,
244+
property_type in arb_property_type(),
245+
key in ".*",
246+
value in ".*",
247+
count: u64,
248+
) {
249+
let decoded = decode(&encode(team_id, property_type, &key, &value, count)).unwrap();
250+
prop_assert_eq!(decoded.team_id, team_id);
251+
prop_assert_eq!(decoded.property_type, property_type);
252+
prop_assert_eq!(decoded.property_key, key);
253+
prop_assert_eq!(decoded.property_value, value);
254+
prop_assert_eq!(decoded.property_count, count);
255+
}
256+
257+
// Flipping the producer's wire format must not change what the
258+
// merger sees: both encodings of a message decode identically.
259+
#[test]
260+
fn binary_and_json_decode_agree(
261+
team_id: i64,
262+
property_type in arb_property_type(),
263+
key in ".*",
264+
value in ".*",
265+
count: u64,
266+
) {
267+
let json = serde_json::to_vec(&Outgoing {
268+
team_id,
269+
property_type,
270+
property_key: &key,
271+
property_value: &value,
272+
property_count: count,
273+
})
274+
.unwrap();
275+
let binary = encode(team_id, property_type, &key, &value, count);
276+
277+
let from_json = PropertyValueMessage::decode(&json).unwrap();
278+
let from_binary = PropertyValueMessage::decode(&binary).unwrap();
279+
prop_assert_eq!(&from_json.team_id, &from_binary.team_id);
280+
prop_assert_eq!(&from_json.property_type, &from_binary.property_type);
281+
prop_assert_eq!(&from_json.property_key, &from_binary.property_key);
282+
prop_assert_eq!(&from_json.property_value, &from_binary.property_value);
283+
prop_assert_eq!(&from_json.property_count, &from_binary.property_count);
284+
}
285+
286+
// Random bytes after a valid magic drive the parser deep into the
287+
// varint/string paths; it must reject or accept, never panic.
288+
#[test]
289+
fn decode_never_panics(garbage in proptest::collection::vec(any::<u8>(), 0..256)) {
290+
drop(decode(&garbage));
291+
let mut with_magic = MAGIC.to_vec();
292+
with_magic.extend_from_slice(&garbage);
293+
drop(decode(&with_magic));
294+
}
295+
296+
#[test]
297+
fn corrupted_encodings_never_panic_and_truncations_error(
298+
team_id: i64,
299+
property_type in arb_property_type(),
300+
key in ".*",
301+
value in ".*",
302+
count: u64,
303+
flipped_byte in any::<prop::sample::Index>(),
304+
flipped_bit in 0u8..8,
305+
) {
306+
let buf = encode(team_id, property_type, &key, &value, count);
307+
308+
let mut corrupted = buf.clone();
309+
let i = flipped_byte.index(corrupted.len());
310+
corrupted[i] ^= 1 << flipped_bit;
311+
drop(decode(&corrupted));
312+
313+
let cut = flipped_byte.index(buf.len());
314+
prop_assert!(decode(&buf[..cut]).is_err(), "truncation at {} must error", cut);
315+
}
316+
}
227317
}

0 commit comments

Comments
 (0)