use crate::recovery::journal::{
ApplyJournalRecord, JournalAction, decode_journal_stream, decode_record, encode_journal_pages,
};
use proptest::prelude::*;
const PAGE: usize = 4096;
const STREAM_PREFIX_LEN: usize = 4;
fn cases() -> u32 {
std::env::var("PAGEDB_PROPTEST_CASES")
.ok()
.and_then(|raw| raw.parse().ok())
.unwrap_or(32)
}
fn config() -> ProptestConfig {
ProptestConfig {
cases: cases(),
failure_persistence: None,
..ProptestConfig::default()
}
}
fn perturb(mut bytes: Vec<u8>, edits: &[(usize, u8)]) -> Vec<u8> {
if bytes.is_empty() {
return bytes;
}
for &(index, value) in edits {
let at = index % bytes.len();
bytes[at] = value;
}
bytes
}
fn actions() -> impl Strategy<Value = Vec<JournalAction>> {
prop::collection::vec(
prop_oneof![
any::<[u8; 16]>().prop_map(|segment_id| JournalAction::Promote { segment_id }),
(any::<[u8; 16]>(), any::<u64>()).prop_map(|(segment_id, tombstone_commit_id)| {
JournalAction::Tombstone {
segment_id,
tombstone_commit_id,
}
}),
],
0..=24,
)
}
fn stream_for(record: &ApplyJournalRecord) -> Option<Vec<u8>> {
let pages = encode_journal_pages(record, PAGE).ok()?;
Some(pages.concat())
}
proptest! {
#![proptest_config(config())]
#[test]
fn random_bytes_never_panic_the_journal_decoders(
bytes in prop::collection::vec(any::<u8>(), 0..=512),
) {
let _ = decode_journal_stream(&bytes);
let _ = decode_record(&bytes);
}
#[test]
fn hostile_stream_length_never_panics(
declared_len in any::<u32>(),
body in prop::collection::vec(any::<u8>(), 0..=256),
) {
let mut stream = declared_len.to_le_bytes().to_vec();
stream.extend_from_slice(&body);
let _ = decode_journal_stream(&stream);
}
#[test]
fn hostile_action_count_is_rejected_before_allocating(
target_commit_id in any::<u64>(),
action_count in any::<u32>(),
tail in prop::collection::vec(any::<u8>(), 0..=64),
) {
let mut record = target_commit_id.to_le_bytes().to_vec();
record.extend_from_slice(&action_count.to_le_bytes());
record.extend_from_slice(&tail);
let representable = (action_count as usize) <= tail.len() / 17;
let decoded = decode_record(&record);
if !representable {
prop_assert!(
decoded.is_err(),
"action_count={} over a {}-byte tail must not be accepted",
action_count,
tail.len()
);
}
}
#[test]
fn valid_records_round_trip_through_the_stream(
target_commit_id in any::<u64>(),
actions in actions(),
) {
let record = ApplyJournalRecord { target_commit_id, actions };
let Some(stream) = stream_for(&record) else {
return Ok(());
};
let decoded = decode_journal_stream(&stream).unwrap();
prop_assert_eq!(decoded.target_commit_id, record.target_commit_id);
prop_assert_eq!(decoded.actions.len(), record.actions.len());
}
#[test]
fn perturbed_valid_stream_never_panics(
target_commit_id in any::<u64>(),
actions in actions(),
edits in prop::collection::vec((any::<usize>(), any::<u8>()), 1..=12),
) {
let record = ApplyJournalRecord { target_commit_id, actions };
let Some(stream) = stream_for(&record) else {
return Ok(());
};
let mutated = perturb(stream, &edits);
let _ = decode_journal_stream(&mutated);
if mutated.len() > STREAM_PREFIX_LEN {
let _ = decode_record(&mutated[STREAM_PREFIX_LEN..]);
}
}
#[test]
fn truncated_valid_stream_never_panics(
target_commit_id in any::<u64>(),
actions in actions(),
keep in 0usize..=(PAGE * 2),
) {
let record = ApplyJournalRecord { target_commit_id, actions };
let Some(stream) = stream_for(&record) else {
return Ok(());
};
let truncated = &stream[..keep.min(stream.len())];
let _ = decode_journal_stream(truncated);
}
}