use super::segment;
use proptest::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
struct PropItem {
id: u64,
payload: String,
}
fn any_seq() -> impl Strategy<Value = u64> {
0u64..=999_999_999_999
}
proptest! {
#[test]
fn filename_parse_roundtrip(start in any_seq(), end in any_seq()) {
let name = segment::filename(start, end);
let parsed =
segment::parse_filename(&name).expect("filename must parse back to a range");
prop_assert_eq!(parsed.start, start);
prop_assert_eq!(parsed.end, end);
}
#[test]
fn parse_filename_never_panics(s in ".{0,40}") {
let _ = segment::parse_filename(&s);
}
#[test]
fn parsed_range_round_trips_through_filename(s in ".{0,40}") {
if let Some(r) = segment::parse_filename(&s) {
let canonical = segment::filename(r.start, r.end);
let reparsed = segment::parse_filename(&canonical).unwrap();
prop_assert_eq!(reparsed.start, r.start);
prop_assert_eq!(reparsed.end, r.end);
}
}
#[test]
fn encode_decode_payload_roundtrip(
ids in proptest::collection::vec(any_seq(), 0..50)
) {
let items: Vec<PropItem> = ids
.iter()
.map(|&id| PropItem { id, payload: format!("payload-{id}") })
.collect();
let path = std::path::Path::new("prop_test_segment.zst");
let payload = segment::encode_payload(None, 3, path, &items)
.expect("encode must succeed for valid items");
let decoded: Result<Vec<PropItem>, _> =
segment::decode_payload(None, &payload, path);
prop_assert!(decoded.is_ok(), "decode failed: {:?}", decoded.err());
prop_assert_eq!(decoded.unwrap(), items);
}
#[test]
fn envelope_wrap_unwrap_identity(payload_bytes in proptest::collection::vec(any::<u8>(), 0..500)) {
let wrapped = segment::wrap_envelope(&payload_bytes);
let (_version, unwrapped) = segment::unwrap_envelope(&wrapped);
prop_assert_eq!(unwrapped, payload_bytes.as_slice());
}
#[cfg(feature = "encryption")]
#[test]
fn full_write_read_encrypted_roundtrip(
key in any::<[u8; 32]>(),
ids in proptest::collection::vec(any_seq(), 0..30)
) {
let items: Vec<PropItem> = ids
.iter()
.map(|&id| PropItem { id, payload: format!("payload-{id}") })
.collect();
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
let cipher = crate::AesGcmCipher::new(&key);
let end = items.len().saturating_sub(1) as u64;
let range = segment::SegmentRange { start: 0, end };
segment::write(dir, Some(&cipher), 3, range, &items)
.expect("write must succeed");
let read: Result<Vec<PropItem>, _> = segment::read(dir, Some(&cipher), range);
prop_assert!(read.is_ok(), "encrypted read failed: {:?}", read.err());
prop_assert_eq!(read.unwrap(), items);
}
#[test]
fn corrupted_segment_read_never_panics(corruption in proptest::collection::vec(any::<u8>(), 0..512)) {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
let buf = crate::SegmentBuffer::<PropItem>::open(dir, crate::SegmentConfig::default())
.expect("open must succeed");
buf.append(PropItem { id: 0, payload: "seed".into() })
.expect("append must succeed");
buf.flush().expect("flush must succeed");
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|e| e == "zst") {
let _ = std::fs::write(&path, &corruption);
}
}
}
let _ = buf.read_from(0, 100);
}
#[test]
fn recovery_over_arbitrary_directory_never_panics(
name_bytes in proptest::collection::vec(any::<u8>(), 1..32),
file_count in 0u8..8,
blob_seed in any::<u64>()
) {
let name = String::from_utf8_lossy(&name_bytes).into_owned();
if name.is_empty() || name.len() >= 64 || name.contains('/') {
return Ok(()); }
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
let mut blob = Vec::new();
for i in 0..file_count {
blob.extend_from_slice(&blob_seed.wrapping_add(i as u64).to_le_bytes());
blob.extend_from_slice(b"garbage");
let entry_name = if i % 2 == 0 {
format!("seg_{i:012}_{file_count:012}.zst")
} else {
name.clone()
};
let _ = std::fs::write(dir.join(&entry_name), &blob);
}
let _ = crate::SegmentBuffer::<PropItem>::open(dir, crate::SegmentConfig::default());
}
}