test: proptest property testing + cargo-fuzz fuzzing for donglora-cobs

This commit is contained in:
swaits committed 2026-03-30 10:57:57 -06:00
1 parent 1fbbf110bc
commit d7bb127d92
8 files changed
+567 -9

No files matched your search

+1
View File
@@ -10,4 +10,5 @@ default = []
[dev-dependencies]
corncobs = "0.1"
proptest = "1"
rand = "0.8"
+11 -1
View File
@@ -21,11 +21,21 @@ benchmark-optimized for streaming applications.
## Testing
```sh
# Unit tests + property tests + interop
cargo test -p donglora-cobs
# Fuzz testing (requires cargo-fuzz: cargo install cargo-fuzz)
cd crates/donglora-cobs
cargo +nightly fuzz run fuzz_decode -- -max_total_time=60
cargo +nightly fuzz run fuzz_roundtrip -- -max_total_time=60
```
Tests include:
- 18 canonical COBS vectors (Wikipedia / IEEE/ACM spec)
- Round-trip verification for various payload sizes
- 8 interop tests cross-validating against `corncobs` (dev-dependency)
- 8 interop tests cross-validating against `corncobs`
- 500 randomized round-trip payloads verified against `corncobs`
- **5 proptest properties** (~1280 random inputs): round-trip, interop
encode/decode both directions, length bounds, decode-never-panics
- **2 fuzz targets**: `fuzz_decode` (arbitrary bytes → must not panic)
and `fuzz_roundtrip` (encode→decode must match)
+24
View File
@@ -0,0 +1,24 @@
[package]
name = "donglora-cobs-fuzz"
version = "0.0.0"
publish = false
edition = "2021"
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.4"
donglora-cobs = { path = ".." }
[[bin]]
name = "fuzz_decode"
path = "fuzz_targets/fuzz_decode.rs"
test = false
doc = false
[[bin]]
name = "fuzz_roundtrip"
path = "fuzz_targets/fuzz_roundtrip.rs"
test = false
doc = false
@@ -0,0 +1,9 @@
//! Fuzz target: feed arbitrary bytes to decode(). Must never panic.
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
let mut buf = [0u8; 2048];
// Must not panic — may return None on invalid input.
let _ = donglora_cobs::decode(data, &mut buf);
});
@@ -0,0 +1,30 @@
//! Fuzz target: encode then decode arbitrary input. Output must match input.
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
if data.len() > 1024 {
return; // skip huge inputs to keep fuzzing fast
}
let max_enc = donglora_cobs::max_encoded_len(data.len()) + 1;
let mut enc_buf = vec![0u8; max_enc];
let enc_len = match donglora_cobs::encode(data, &mut enc_buf) {
Some(n) => n,
None => return, // encode failed (buffer too small shouldn't happen, but skip)
};
let encoded = &enc_buf[..enc_len];
// Encoded output must never contain 0x00.
assert!(!encoded.contains(&0x00), "encoded contains sentinel");
let mut dec_buf = vec![0u8; data.len() + 1];
let dec_len = donglora_cobs::decode(encoded, &mut dec_buf)
.expect("decode failed after successful encode");
assert_eq!(
&dec_buf[..dec_len], data,
"round-trip mismatch: encode→decode produced different output"
);
});
+69
View File
@@ -486,3 +486,72 @@ mod tests {
}
}
}
// ── Property-based tests (proptest) ─────────────────────────────
#[cfg(test)]
mod proptests {
extern crate alloc;
use alloc::vec;
use alloc::vec::Vec;
use super::*;
use proptest::prelude::*;
// Round-trip: encode then decode must recover the original.
proptest! {
#[test]
fn roundtrip_any(data in proptest::collection::vec(any::<u8>(), 0..1024)) {
let mut enc_buf = vec![0u8; max_encoded_len(data.len()) + 1];
let enc_len = encode(&data, &mut enc_buf).unwrap();
let encoded = &enc_buf[..enc_len];
// Encoded output must never contain 0x00.
prop_assert!(!encoded.contains(&0x00));
let mut dec_buf = vec![0u8; data.len() + 1];
let dec_len = decode(encoded, &mut dec_buf).unwrap();
prop_assert_eq!(&dec_buf[..dec_len], &data[..]);
}
// Our encode → corncobs decode must match.
#[test]
fn interop_our_encode_their_decode(data in proptest::collection::vec(any::<u8>(), 0..1024)) {
let mut enc_buf = vec![0u8; max_encoded_len(data.len()) + 2];
let enc_len = encode(&data, &mut enc_buf).unwrap();
// Append sentinel for corncobs.
enc_buf[enc_len] = 0x00;
let mut dec_buf = vec![0u8; data.len() + 1];
let dec_len = corncobs::decode_buf(&enc_buf[..enc_len + 1], &mut dec_buf).unwrap();
prop_assert_eq!(&dec_buf[..dec_len], &data[..]);
}
// corncobs encode → our decode must match.
#[test]
fn interop_their_encode_our_decode(data in proptest::collection::vec(any::<u8>(), 0..1024)) {
let mut enc_buf = vec![0u8; corncobs::max_encoded_len(data.len())];
let enc_len = corncobs::encode_buf(&data, &mut enc_buf);
// Strip trailing sentinel for our decoder.
let data_end = enc_buf[..enc_len].iter().rposition(|&b| b != 0x00).map_or(0, |i| i + 1);
let mut dec_buf = vec![0u8; data.len() + 1];
let dec_len = decode(&enc_buf[..data_end], &mut dec_buf).unwrap();
prop_assert_eq!(&dec_buf[..dec_len], &data[..]);
}
// Encoded length is always within the theoretical maximum.
#[test]
fn encoded_length_bounded(data in proptest::collection::vec(any::<u8>(), 0..1024)) {
let mut enc_buf = vec![0u8; max_encoded_len(data.len()) + 1];
let enc_len = encode(&data, &mut enc_buf).unwrap();
prop_assert!(enc_len <= max_encoded_len(data.len()));
}
// Decoding random garbage must never panic (returns None on invalid input).
#[test]
fn decode_never_panics(data in proptest::collection::vec(any::<u8>(), 0..512)) {
let mut dec_buf = vec![0u8; data.len() + 256];
let _ = decode(&data, &mut dec_buf); // may return None, must not panic
}
}
}