use std::sync::mpsc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use crate::RealmId;
use crate::crypto::CipherId;
use crate::crypto::kdf::derive_mk;
use crate::pager::format::data_page::body_capacity;
use crate::pager::freelist::{chain_capacity, count_chain, read_chain, rewrite_chain, write_chain};
use crate::pager::{PageKind, Pager, PagerConfig};
use crate::vfs::memory::MemVfs;
use proptest::prelude::*;
use proptest::test_runner::{TestCaseError, TestCaseResult, TestRunner};
const PAGE: usize = 4096;
const REALM: RealmId = RealmId::new([0x7B; 16]);
const POOL: usize = 6;
const FIRST_PAGE: u64 = 4;
const BUMP_BASE: u64 = 1_000;
const PAGE_HEADER_LEN: usize = 12;
const ENTRY_LEN: usize = 16;
fn cases() -> u32 {
std::env::var("PAGEDB_PROPTEST_CASES")
.ok()
.and_then(|raw| raw.parse().ok())
.unwrap_or(32)
}
fn walk_timeout() -> Duration {
Duration::from_millis(2_000 + u64::from(cases()) * 100)
}
fn config() -> ProptestConfig {
ProptestConfig {
cases: cases(),
failure_persistence: None,
..ProptestConfig::default()
}
}
fn block_on<F: std::future::Future>(future: F) -> F::Output {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(future)
}
async fn fresh_pager() -> Arc<Pager<MemVfs>> {
let mk = derive_mk(&[0x7C; 32], &[0u8; 16], 0).unwrap();
let config = PagerConfig {
page_size: PAGE,
buffer_pool_pages: 64,
segment_cache_pages: 64,
cipher_id: CipherId::Aes256Gcm,
mk_epoch: 0,
main_db_file_id: [0x7D; 16],
main_db_path: "/main.db".into(),
anchor_budget: 1_000_000,
dek_lru_capacity: 16,
observer_retry_count: 0,
metrics_enabled: false,
};
Arc::new(Pager::open(MemVfs::new(), mk, config).await.unwrap())
}
fn run_bounded<S>(
label: &'static str,
strategy: S,
timeout: Duration,
test: impl Fn(S::Value) -> TestCaseResult + Send + 'static,
) where
S: Strategy + Send + 'static,
S::Value: std::fmt::Debug,
{
let last_input = Arc::new(Mutex::new(String::from("<none>")));
let thread_input = Arc::clone(&last_input);
let (sender, receiver) = mpsc::channel();
std::thread::spawn(move || {
let mut runner = TestRunner::new(config());
let outcome = runner.run(&strategy, move |value| {
*thread_input
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = format!("{value:?}");
test(value)
});
let _ = sender.send(outcome.map_err(|error| error.to_string()));
});
match receiver.recv_timeout(timeout) {
Ok(Ok(())) => {}
Ok(Err(message)) => panic!("{label}: {message}"),
Err(_) => {
let input = last_input
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
panic!("{label}: did not terminate within {timeout:?}; last input: {input}");
}
}
}
fn forged_chain_body(next: u64, declared_count: u32, entries: &[(u64, u64)]) -> Vec<u8> {
let mut body = vec![0u8; body_capacity(PAGE)];
body[0..8].copy_from_slice(&next.to_le_bytes());
body[8..12].copy_from_slice(&declared_count.to_le_bytes());
for (index, (commit_id, page_id)) in entries.iter().enumerate() {
let offset = PAGE_HEADER_LEN + index * ENTRY_LEN;
if offset + ENTRY_LEN > body.len() {
break;
}
body[offset..offset + 8].copy_from_slice(&commit_id.to_le_bytes());
body[offset + 8..offset + 16].copy_from_slice(&page_id.to_le_bytes());
}
body
}
fn pool_page_id(slot: usize) -> u64 {
match slot % (POOL + 1) {
0 => 0,
occupied => FIRST_PAGE + occupied as u64 - 1,
}
}
type ChainSpec = Vec<(usize, u32, Vec<(u64, u64)>)>;
fn chain_spec() -> impl Strategy<Value = ChainSpec> {
prop::collection::vec(
(
0usize..=POOL,
prop_oneof![
0u32..8,
Just(u32::MAX),
Just(chain_capacity(PAGE) as u32),
Just(chain_capacity(PAGE) as u32 + 1),
any::<u32>(),
],
prop::collection::vec((any::<u64>(), any::<u64>()), 0..=4),
),
1..=POOL,
)
}
#[test]
fn generated_freelist_chains_terminate_or_report_corruption() {
run_bounded(
"freelist read_chain over a generated pointer graph",
chain_spec(),
walk_timeout(),
|pages| {
block_on(async {
let pager = fresh_pager().await;
for (index, (next_slot, declared_count, entries)) in pages.iter().enumerate() {
let body =
forged_chain_body(pool_page_id(*next_slot), *declared_count, entries);
pager
.write_main_page(FIRST_PAGE + index as u64, REALM, PageKind::Free, &body)
.await
.unwrap();
}
let _ = read_chain(&pager, REALM, FIRST_PAGE).await;
let _ = count_chain(&pager, REALM, FIRST_PAGE).await;
});
Ok(())
},
);
}
#[test]
fn wholly_random_freelist_bodies_never_panic() {
run_bounded(
"freelist read_chain over wholly random page bodies",
prop::collection::vec(
prop::collection::vec(any::<u8>(), body_capacity(PAGE)),
1..=3,
),
walk_timeout(),
|bodies| {
block_on(async {
let pager = fresh_pager().await;
for (index, body) in bodies.iter().enumerate() {
pager
.write_main_page(FIRST_PAGE + index as u64, REALM, PageKind::Free, body)
.await
.unwrap();
}
let _ = read_chain(&pager, REALM, FIRST_PAGE).await;
let _ = count_chain(&pager, REALM, FIRST_PAGE).await;
});
Ok(())
},
);
}
#[test]
fn written_freelist_chains_read_back_without_loss() {
run_bounded(
"freelist rewrite_chain / read_chain round trip",
(
prop::collection::vec(FIRST_PAGE..BUMP_BASE, 0..=400),
0usize..=8,
any::<u64>(),
),
walk_timeout(),
|(raw_page_ids, host_count, commit_id)| {
let mut page_ids = raw_page_ids;
page_ids.sort_unstable();
page_ids.dedup();
let entries: Vec<(u64, u64)> = page_ids
.iter()
.map(|&page_id| (commit_id, page_id))
.collect();
let hosts: Vec<u64> = page_ids.iter().take(host_count).copied().collect();
let outcome = block_on(async {
let pager = fresh_pager().await;
let (head, _next) =
rewrite_chain(&pager, REALM, PAGE, entries, hosts, BUMP_BASE, 0).await?;
read_chain(&pager, REALM, head).await
});
let (read_entries, chain_pages) =
outcome.map_err(|error| TestCaseError::fail(format!("{error:?}")))?;
let mut recovered: Vec<u64> = read_entries.iter().map(|&(_, id)| id).collect();
let carved: Vec<u64> = chain_pages
.iter()
.copied()
.filter(|id| *id < BUMP_BASE)
.collect();
for id in &carved {
prop_assert!(!recovered.contains(id), "page {id} is both entry and chain");
}
recovered.extend(carved);
recovered.sort_unstable();
prop_assert_eq!(recovered, page_ids);
Ok(())
},
);
}
#[test]
fn hand_laid_chains_of_generated_shape_read_back_exactly() {
run_bounded(
"freelist write_chain over generated page layouts",
(
prop::collection::vec(FIRST_PAGE..=(FIRST_PAGE + POOL as u64 - 1), 1..=POOL),
prop::collection::vec((any::<u64>(), any::<u64>()), 0..=8),
),
walk_timeout(),
|(mut chain_pages, entries)| {
chain_pages.sort_unstable();
chain_pages.dedup();
prop_assume!(entries.len() <= chain_capacity(PAGE) * chain_pages.len());
let outcome = block_on(async {
let pager = fresh_pager().await;
let head = write_chain(&pager, REALM, PAGE, &chain_pages, &entries, 0).await?;
read_chain(&pager, REALM, head).await
});
let (read_entries, _) =
outcome.map_err(|error| TestCaseError::fail(format!("{error:?}")))?;
prop_assert_eq!(read_entries, entries);
Ok(())
},
);
}