LeVCS/crates/levcs-protocol/tests/proptest_codec.rs

79 lines
2.8 KiB
Rust

//! Property tests for the pack codec.
//!
//! The hand-rolled fuzz suite in `fuzz.rs` covers panic-resistance under
//! random and mutated bytes. This file complements it with *structured*
//! round-trip properties: build valid packs by construction, encode them,
//! decode the result, and assert the entries come back identical.
//!
//! When proptest finds a counterexample it shrinks toward the smallest
//! input that still fails, which is far more debuggable than the seed-
//! based fuzz harness.
use levcs_protocol::Pack;
use proptest::collection::vec;
use proptest::prelude::*;
/// One pack entry: an object_type byte (1..=5 — Blob/Tree/Commit/Release/
/// Authority per pack.rs) and a body of up to ~10 KiB.
fn entry_strategy() -> impl Strategy<Value = (u8, Vec<u8>)> {
(1u8..=5u8, vec(any::<u8>(), 0..10_240))
}
proptest! {
/// Round-trip: any sequence of valid entries encodes to bytes that
/// decode back to those same entries (object_type + body).
#[test]
fn pack_roundtrip_arbitrary_entries(
es in vec(entry_strategy(), 0..16)
) {
let mut pk = Pack::new();
for (t, b) in &es {
pk.push(*t, b.clone());
}
let encoded = pk.encode();
let decoded = Pack::decode(&encoded).expect("valid pack must decode");
prop_assert_eq!(decoded.entries.len(), es.len());
for (i, (t, b)) in es.iter().enumerate() {
prop_assert_eq!(decoded.entries[i].object_type, *t);
prop_assert_eq!(&decoded.entries[i].bytes, b);
}
}
/// `decode_prefix` consumes exactly the bytes the encoder emitted —
/// trailing bytes appended after a valid pack must be reported as
/// unconsumed, not silently swallowed.
#[test]
fn pack_decode_prefix_reports_consumed_bytes_correctly(
es in vec(entry_strategy(), 0..8),
tail in vec(any::<u8>(), 0..64),
) {
let mut pk = Pack::new();
for (t, b) in &es {
pk.push(*t, b.clone());
}
let mut encoded = pk.encode();
let pack_len = encoded.len();
encoded.extend_from_slice(&tail);
let (decoded, consumed) = Pack::decode_prefix(&encoded)
.expect("valid pack prefix must decode");
prop_assert_eq!(consumed, pack_len);
prop_assert_eq!(decoded.entries.len(), es.len());
}
/// Single-byte truncation of a valid pack must never panic.
/// (The fuzz suite covers random truncation; this nails down the
/// shrunk one-byte-short case proptest gravitates toward.)
#[test]
fn pack_truncated_by_one_does_not_panic(
es in vec(entry_strategy(), 1..8)
) {
let mut pk = Pack::new();
for (t, b) in &es {
pk.push(*t, b.clone());
}
let encoded = pk.encode();
let truncated = &encoded[..encoded.len() - 1];
let _ = Pack::decode(truncated);
}
}