mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-08-05 15:13:06 -07:00
519 lines
19 KiB
Rust
519 lines
19 KiB
Rust
//! Data integrity tests for RawVec types (BytesVec and ZeroCopyVec).
|
|
//!
|
|
//! Tests verify that rollback + flush + reopen preserves data correctly.
|
|
|
|
use rawdb::Database;
|
|
use sha2::{Digest, Sha256};
|
|
use std::fs;
|
|
use std::ops::{Deref, DerefMut};
|
|
use std::path::{Path, PathBuf};
|
|
use tempfile::TempDir;
|
|
use vecdb::{
|
|
AnyStoredVec, ImportOptions, ImportableVec, RawStrategy, ReadWriteRawVec, ReadableVec, Result,
|
|
Stamp, StoredVec, VecReader, Version, WritableVec,
|
|
};
|
|
|
|
// ============================================================================
|
|
// Traits for Raw Vec Types (supporting holes/updates)
|
|
// ============================================================================
|
|
|
|
pub trait IntegrityVec: StoredVec<I = usize, T = u32> + DerefMut + ImportableVec + Sized
|
|
where
|
|
Self::Target: IntegrityOps,
|
|
{
|
|
fn import_with_changes<'a>(
|
|
db: &'a Database,
|
|
name: &'a str,
|
|
changes: u16,
|
|
) -> Result<(Self, ImportOptions<'a>)>;
|
|
}
|
|
|
|
pub trait IntegrityOps {
|
|
type Reader;
|
|
|
|
fn update(&mut self, index: usize, value: u32) -> Result<()>;
|
|
fn take(&mut self, index: usize) -> Option<u32>;
|
|
fn stamped_write_with_changes(&mut self, stamp: Stamp) -> Result<()>;
|
|
fn stamp(&self) -> Stamp;
|
|
fn collect(&self) -> Vec<u32>;
|
|
fn collect_holed(&self) -> Vec<Option<u32>>;
|
|
fn get_with_reader(&self, index: usize, reader: &Self::Reader) -> Option<u32>;
|
|
fn reader(&self) -> Self::Reader;
|
|
}
|
|
|
|
// Generic implementations for any ReadWriteRawVec strategy
|
|
impl<S> IntegrityOps for ReadWriteRawVec<usize, u32, S>
|
|
where
|
|
S: RawStrategy<u32>,
|
|
{
|
|
type Reader = VecReader<usize, u32, S>;
|
|
|
|
fn update(&mut self, index: usize, value: u32) -> Result<()> {
|
|
ReadWriteRawVec::update(self, index, value)
|
|
}
|
|
|
|
fn take(&mut self, index: usize) -> Option<u32> {
|
|
let reader = self.reader();
|
|
let result = ReadWriteRawVec::take(self, index, &reader);
|
|
drop(reader);
|
|
result
|
|
}
|
|
|
|
fn stamped_write_with_changes(&mut self, stamp: Stamp) -> Result<()> {
|
|
WritableVec::stamped_write_with_changes(self, stamp)
|
|
}
|
|
|
|
fn stamp(&self) -> Stamp {
|
|
AnyStoredVec::stamp(self)
|
|
}
|
|
|
|
fn collect(&self) -> Vec<u32> {
|
|
ReadableVec::collect(self)
|
|
}
|
|
|
|
fn collect_holed(&self) -> Vec<Option<u32>> {
|
|
ReadWriteRawVec::collect_holed(self)
|
|
}
|
|
|
|
fn get_with_reader(&self, index: usize, reader: &Self::Reader) -> Option<u32> {
|
|
ReadWriteRawVec::get_with_reader(self, index, reader)
|
|
}
|
|
|
|
fn reader(&self) -> Self::Reader {
|
|
ReadWriteRawVec::reader(self)
|
|
}
|
|
}
|
|
|
|
/// Helper to create a temporary test database
|
|
pub fn setup_test_db() -> Result<(Database, TempDir)> {
|
|
let temp_dir = TempDir::new()?;
|
|
let db = Database::open(temp_dir.path())?;
|
|
Ok((db, temp_dir))
|
|
}
|
|
|
|
/// Compute SHA-256 hash of the vecdb data file and regions directory
|
|
/// Only hashes data (file) and regions/*, ignoring changes directory
|
|
fn compute_directory_hash(dir: &Path) -> Result<String, Box<dyn std::error::Error>> {
|
|
let mut hasher = Sha256::new();
|
|
|
|
// Collect all files in sorted order for deterministic hashing
|
|
let mut files: Vec<PathBuf> = Vec::new();
|
|
|
|
// Hash the data file if it exists
|
|
let data_file = dir.join("data");
|
|
if data_file.exists() && data_file.is_file() {
|
|
files.push(data_file);
|
|
}
|
|
|
|
// Hash files in the regions directory, excluding changes subdirectory
|
|
let regions_dir = dir.join("regions");
|
|
if regions_dir.exists() {
|
|
fn collect_files(dir: &Path, files: &mut Vec<PathBuf>) {
|
|
let Ok(entries) = fs::read_dir(dir) else {
|
|
return;
|
|
};
|
|
for entry in entries.filter_map(|e| e.ok()) {
|
|
let path = entry.path();
|
|
if path.components().any(|c| c.as_os_str() == "changes") {
|
|
continue;
|
|
}
|
|
if path.is_dir() {
|
|
collect_files(&path, files);
|
|
} else if path.is_file() {
|
|
files.push(path);
|
|
}
|
|
}
|
|
}
|
|
collect_files(®ions_dir, &mut files);
|
|
}
|
|
|
|
files.sort();
|
|
|
|
// Hash each file's relative path and contents
|
|
for file_path in &files {
|
|
// Hash the relative path
|
|
if let Ok(rel_path) = file_path.strip_prefix(dir) {
|
|
hasher.update(rel_path.to_string_lossy().as_bytes());
|
|
}
|
|
|
|
// Hash the file contents
|
|
let contents = fs::read(file_path)?;
|
|
hasher.update(&contents);
|
|
}
|
|
|
|
let hash = hasher.finalize();
|
|
Ok(hash.iter().map(|b| format!("{:02x}", b)).collect())
|
|
}
|
|
|
|
/// Generic test function for data integrity with rollback, flush, and reopen
|
|
///
|
|
/// This test verifies that after rollback + flush + close + reopen:
|
|
/// 1. Data can be correctly read back using individual gets
|
|
/// 2. Data can be correctly read back using iterators
|
|
/// 3. Redo operations produce the same readable state
|
|
fn run_data_integrity_rollback_flush_reopen<V>() -> Result<(), Box<dyn std::error::Error>>
|
|
where
|
|
V: IntegrityVec + WritableVec<usize, u32>,
|
|
<V as Deref>::Target: IntegrityOps,
|
|
{
|
|
println!("=== Data Integrity Test: Rollback + Flush + Reopen ===\n");
|
|
println!("This test verifies:");
|
|
println!(" • Rollback + flush + reopen preserves data correctly");
|
|
println!(" • Data readable via both gets and iterators");
|
|
println!(" • File hashes track physical layout differences\n");
|
|
|
|
// Create database
|
|
let (database, temp) = setup_test_db()?;
|
|
let test_path = temp.path();
|
|
|
|
let (mut vec, _options) = V::import_with_changes(&database, "vec", 10)?;
|
|
println!("✓ Created vecdb");
|
|
|
|
// Step 1: Do initial work
|
|
println!("\n--- Phase 1: Initial work ---");
|
|
for i in 0..5 {
|
|
vec.push(i);
|
|
}
|
|
vec.deref_mut().stamped_write_with_changes(Stamp::new(1))?;
|
|
println!("✓ Added values [0, 1, 2, 3, 4] and flushed (stamp 1)");
|
|
println!(" Current data: {:?}", vec.deref_mut().collect());
|
|
|
|
// Step 2: More work and flush
|
|
println!("\n--- Phase 2: More work ---");
|
|
for i in 5..10 {
|
|
vec.push(i);
|
|
}
|
|
vec.deref_mut().stamped_write_with_changes(Stamp::new(2))?;
|
|
println!("✓ Added values [5, 6, 7, 8, 9] and flushed (stamp 2)");
|
|
println!(" Current data: {:?}", vec.deref_mut().collect());
|
|
|
|
// Step 3: Checkpoint 1 - Save hash and data state
|
|
println!("\n--- Checkpoint 1 ---");
|
|
let checkpoint1_data = vec.deref_mut().collect_holed();
|
|
let checkpoint1_stamp = vec.deref_mut().stamp();
|
|
let checkpoint1_hash = compute_directory_hash(test_path)?;
|
|
println!("✓ Saved checkpoint1 at stamp {:?}", checkpoint1_stamp);
|
|
println!(" Data: {:?}", checkpoint1_data);
|
|
println!(" Length: {}", vec.len());
|
|
println!(" File hash: {}", checkpoint1_hash);
|
|
|
|
// Step 4-6: Three more operations with flush
|
|
println!("\n--- Phase 3: Three more operations ---");
|
|
|
|
// Operation 1: Update some values
|
|
vec.deref_mut().update(2, 100)?;
|
|
vec.deref_mut().update(7, 200)?;
|
|
vec.deref_mut().stamped_write_with_changes(Stamp::new(3))?;
|
|
println!("✓ Operation 1: Updated index 2→100, 7→200 (stamp 3)");
|
|
println!(" Current data: {:?}", vec.deref_mut().collect());
|
|
|
|
// Operation 2: Add more values
|
|
vec.push(20);
|
|
vec.push(21);
|
|
vec.deref_mut().stamped_write_with_changes(Stamp::new(4))?;
|
|
println!("✓ Operation 2: Added values [20, 21] (stamp 4)");
|
|
println!(" Current data: {:?}", vec.deref_mut().collect());
|
|
|
|
// Operation 3: Create a hole and add value
|
|
let _ = vec.deref_mut().take(5);
|
|
vec.push(30);
|
|
vec.deref_mut().stamped_write_with_changes(Stamp::new(5))?;
|
|
println!("✓ Operation 3: Removed index 5, added value 30 (stamp 5)");
|
|
println!(" Current data: {:?}", vec.deref_mut().collect());
|
|
println!(" Data with holes: {:?}", vec.deref_mut().collect_holed());
|
|
|
|
// Step 7: Checkpoint 2 - Save hash and data state
|
|
println!("\n--- Checkpoint 2 ---");
|
|
let checkpoint2_data = vec.deref_mut().collect_holed();
|
|
let checkpoint2_stamp = vec.deref_mut().stamp();
|
|
let checkpoint2_hash = compute_directory_hash(test_path)?;
|
|
println!("✓ Saved checkpoint2 at stamp {:?}", checkpoint2_stamp);
|
|
println!(" Data: {:?}", checkpoint2_data);
|
|
println!(" Length: {}", vec.len());
|
|
println!(" File hash: {}", checkpoint2_hash);
|
|
|
|
// Step 8: Undo last 3 operations
|
|
println!("\n--- Phase 4: Undo last 3 operations ---");
|
|
vec.rollback()?;
|
|
println!("✓ Rollback 1: Now at stamp {:?}", vec.deref_mut().stamp());
|
|
println!(" Current data: {:?}", vec.deref_mut().collect());
|
|
|
|
vec.rollback()?;
|
|
println!("✓ Rollback 2: Now at stamp {:?}", vec.deref_mut().stamp());
|
|
println!(" Current data: {:?}", vec.deref_mut().collect());
|
|
|
|
vec.rollback()?;
|
|
println!("✓ Rollback 3: Now at stamp {:?}", vec.deref_mut().stamp());
|
|
println!(" Current data: {:?}", vec.deref_mut().collect());
|
|
|
|
// Step 9: Verify in-memory data matches checkpoint1
|
|
println!("\n--- Verification 1: After undo (in-memory) ---");
|
|
let after_undo_data = vec.deref_mut().collect_holed();
|
|
let after_undo_stamp = vec.deref_mut().stamp();
|
|
println!("In-memory state after rollback:");
|
|
println!(
|
|
" Stamp: {:?} (expected: {:?})",
|
|
after_undo_stamp, checkpoint1_stamp
|
|
);
|
|
println!(" Data: {:?}", after_undo_data);
|
|
|
|
assert_eq!(
|
|
after_undo_stamp, checkpoint1_stamp,
|
|
"Stamp mismatch after undo!"
|
|
);
|
|
assert_eq!(
|
|
after_undo_data, checkpoint1_data,
|
|
"In-memory data mismatch after undo!"
|
|
);
|
|
println!("✓ PASS: In-memory data matches checkpoint1 after undo");
|
|
|
|
// Flush and close
|
|
println!("\n--- Step 10: Flush, close, and reopen ---");
|
|
vec.deref_mut()
|
|
.stamped_write_with_changes(checkpoint1_stamp)?;
|
|
let after_flush_hash = compute_directory_hash(test_path)?;
|
|
println!("✓ Flushed to disk");
|
|
println!(" File hash: {}", after_flush_hash);
|
|
if after_flush_hash != checkpoint1_hash {
|
|
println!(
|
|
" Note: Hash differs from checkpoint1 ({}) due to region allocation bug",
|
|
checkpoint1_hash
|
|
);
|
|
}
|
|
|
|
// Drop the vec to close files
|
|
drop(vec);
|
|
println!("✓ Closed vecdb");
|
|
|
|
// Reopen the database
|
|
let (mut vec, _options) = V::import_with_changes(&database, "vec", 10)?;
|
|
println!("✓ Reopened vecdb");
|
|
println!(" Stamp after reopen: {:?}", vec.deref_mut().stamp());
|
|
println!(" Length after reopen: {}", vec.len());
|
|
|
|
// Verify using individual gets
|
|
println!("\n--- Verification 2: After reopen (using gets) ---");
|
|
let reader = vec.deref_mut().reader();
|
|
let mut data_via_gets = Vec::new();
|
|
for i in 0..vec.len() {
|
|
let value = vec.deref_mut().get_with_reader(i, &reader);
|
|
data_via_gets.push(value);
|
|
}
|
|
drop(reader);
|
|
|
|
println!("Data read via gets: {:?}", data_via_gets);
|
|
assert_eq!(
|
|
data_via_gets, checkpoint1_data,
|
|
"Data mismatch reading via gets after reopen!"
|
|
);
|
|
println!("✓ PASS: Data correct when reading via gets");
|
|
|
|
// Verify using iterator
|
|
println!("\n--- Verification 3: After reopen (using iterator) ---");
|
|
let data_via_iter = vec.deref_mut().collect_holed();
|
|
println!("Data read via iterator: {:?}", data_via_iter);
|
|
assert_eq!(
|
|
data_via_iter, checkpoint1_data,
|
|
"Data mismatch reading via iterator after reopen!"
|
|
);
|
|
println!("✓ PASS: Data correct when reading via iterator");
|
|
|
|
// Also test the clean iterator (non-holed)
|
|
let data_via_clean_iter: Vec<u32> = vec.deref_mut().collect();
|
|
let expected_clean: Vec<u32> = checkpoint1_data.iter().filter_map(|x| *x).collect();
|
|
println!("Data via clean iterator: {:?}", data_via_clean_iter);
|
|
assert_eq!(
|
|
data_via_clean_iter, expected_clean,
|
|
"Data mismatch via clean iterator!"
|
|
);
|
|
println!("✓ PASS: Data correct when reading via clean iterator");
|
|
|
|
println!("\n✓ ALL VERIFICATION PASSED: Rollback + flush + reopen preserved data correctly!");
|
|
|
|
// Step 10: Redo the same 3 operations
|
|
println!("\n--- Phase 5: Redo same 3 operations ---");
|
|
|
|
// Redo Operation 1: Update same values
|
|
vec.deref_mut().update(2, 100)?;
|
|
vec.deref_mut().update(7, 200)?;
|
|
vec.deref_mut().stamped_write_with_changes(Stamp::new(3))?;
|
|
println!("✓ Redo operation 1: Updated index 2→100, 7→200 (stamp 3)");
|
|
println!(" Current data: {:?}", vec.deref_mut().collect());
|
|
|
|
// Redo Operation 2: Add same values
|
|
vec.push(20);
|
|
vec.push(21);
|
|
vec.deref_mut().stamped_write_with_changes(Stamp::new(4))?;
|
|
println!("✓ Redo operation 2: Added values [20, 21] (stamp 4)");
|
|
println!(" Current data: {:?}", vec.deref_mut().collect());
|
|
|
|
// Redo Operation 3: Create hole and add value
|
|
let _ = vec.deref_mut().take(5);
|
|
vec.push(30);
|
|
vec.deref_mut().stamped_write_with_changes(Stamp::new(5))?;
|
|
println!("✓ Redo operation 3: Removed index 5, added value 30 (stamp 5)");
|
|
println!(" Current data: {:?}", vec.deref_mut().collect());
|
|
println!(" Data with holes: {:?}", vec.deref_mut().collect_holed());
|
|
|
|
// Step 11: Verify in-memory data matches checkpoint2
|
|
println!("\n--- Verification 4: After redo (in-memory) ---");
|
|
let after_redo_data = vec.deref_mut().collect_holed();
|
|
let after_redo_stamp = vec.deref_mut().stamp();
|
|
println!("In-memory state after redo:");
|
|
println!(
|
|
" Stamp: {:?} (expected: {:?})",
|
|
after_redo_stamp, checkpoint2_stamp
|
|
);
|
|
println!(" Data: {:?}", after_redo_data);
|
|
|
|
assert_eq!(
|
|
after_redo_stamp, checkpoint2_stamp,
|
|
"Stamp mismatch after redo!"
|
|
);
|
|
assert_eq!(
|
|
after_redo_data, checkpoint2_data,
|
|
"In-memory data mismatch after redo!"
|
|
);
|
|
println!("✓ PASS: In-memory data matches checkpoint2 after redo");
|
|
|
|
// Flush and close
|
|
println!("\n--- Step 12: Flush, close, and reopen (after redo) ---");
|
|
vec.deref_mut()
|
|
.stamped_write_with_changes(checkpoint2_stamp)?;
|
|
let after_redo_flush_hash = compute_directory_hash(test_path)?;
|
|
println!("✓ Flushed to disk");
|
|
println!(" File hash: {}", after_redo_flush_hash);
|
|
if after_redo_flush_hash == checkpoint2_hash {
|
|
println!(" ✓ Hash matches checkpoint2 (operations were deterministic)");
|
|
} else {
|
|
println!(
|
|
" Note: Hash differs from checkpoint2 ({})",
|
|
checkpoint2_hash
|
|
);
|
|
}
|
|
|
|
// Drop and reopen
|
|
drop(vec);
|
|
println!("✓ Closed vecdb");
|
|
|
|
let (mut vec, _options) = V::import_with_changes(&database, "vec", 10)?;
|
|
println!("✓ Reopened vecdb");
|
|
println!(" Stamp after reopen: {:?}", vec.deref_mut().stamp());
|
|
println!(" Length after reopen: {}", vec.len());
|
|
|
|
// Verify using individual gets
|
|
println!("\n--- Verification 5: After reopen (using gets) ---");
|
|
let reader = vec.deref_mut().reader();
|
|
let mut data_via_gets = Vec::new();
|
|
for i in 0..vec.len() {
|
|
let value = vec.deref_mut().get_with_reader(i, &reader);
|
|
data_via_gets.push(value);
|
|
}
|
|
drop(reader);
|
|
|
|
println!("Data read via gets: {:?}", data_via_gets);
|
|
assert_eq!(
|
|
data_via_gets, checkpoint2_data,
|
|
"Data mismatch reading via gets after reopen!"
|
|
);
|
|
println!("✓ PASS: Data correct when reading via gets");
|
|
|
|
// Verify using iterator
|
|
println!("\n--- Verification 6: After reopen (using iterator) ---");
|
|
let data_via_iter = vec.deref_mut().collect_holed();
|
|
println!("Data read via iterator: {:?}", data_via_iter);
|
|
assert_eq!(
|
|
data_via_iter, checkpoint2_data,
|
|
"Data mismatch reading via iterator after reopen!"
|
|
);
|
|
println!("✓ PASS: Data correct when reading via iterator");
|
|
|
|
// Also test the clean iterator (non-holed)
|
|
let data_via_clean_iter: Vec<u32> = vec.deref_mut().collect();
|
|
let expected_clean: Vec<u32> = checkpoint2_data.iter().filter_map(|x| *x).collect();
|
|
println!("Data via clean iterator: {:?}", data_via_clean_iter);
|
|
assert_eq!(
|
|
data_via_clean_iter, expected_clean,
|
|
"Data mismatch via clean iterator!"
|
|
);
|
|
println!("✓ PASS: Data correct when reading via clean iterator");
|
|
|
|
println!("\n✓ ALL VERIFICATION PASSED: Redo + flush + reopen preserved data correctly!");
|
|
|
|
println!("\n=== Test Results ===");
|
|
println!("\nData integrity:");
|
|
println!("✓ Rollback + flush + reopen: Data correctly preserved");
|
|
println!("✓ Redo + flush + reopen: Data correctly preserved");
|
|
println!("✓ Gets work correctly after reopen");
|
|
println!("✓ Iterators (holed and clean) work correctly after reopen");
|
|
println!("\nFile layout:");
|
|
println!(" Checkpoint1 hash: {}", checkpoint1_hash);
|
|
println!(" After rollback: {}", after_flush_hash);
|
|
if after_flush_hash != checkpoint1_hash {
|
|
println!(" ⚠ Hashes differ due to region allocation bug (regions not rolled back)");
|
|
}
|
|
println!(" Checkpoint2 hash: {}", checkpoint2_hash);
|
|
println!(" After redo: {}", after_redo_flush_hash);
|
|
if after_redo_flush_hash == checkpoint2_hash {
|
|
println!(" ✓ Hashes match (redo is deterministic)");
|
|
}
|
|
println!("\n✓ ALL TESTS PASSED - Data integrity maintained across rollback/redo cycle!");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// ============================================================================
|
|
// Test instantiation for BytesVec (no feature flag needed)
|
|
// ============================================================================
|
|
|
|
mod bytes {
|
|
use super::*;
|
|
use vecdb::BytesVec;
|
|
|
|
impl IntegrityVec for BytesVec<usize, u32> {
|
|
fn import_with_changes<'a>(
|
|
db: &'a Database,
|
|
name: &'a str,
|
|
changes: u16,
|
|
) -> Result<(Self, ImportOptions<'a>)> {
|
|
let mut options: ImportOptions = (db, name, Version::TWO).into();
|
|
options = options.with_saved_stamped_changes(changes);
|
|
let vec = Self::forced_import_with(options)?;
|
|
Ok((vec, options))
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn data_integrity_rollback_flush_reopen() -> Result<(), Box<dyn std::error::Error>> {
|
|
run_data_integrity_rollback_flush_reopen::<BytesVec<usize, u32>>()
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Test instantiation for ZeroCopyVec (with zerocopy feature)
|
|
// ============================================================================
|
|
|
|
#[cfg(feature = "zerocopy")]
|
|
mod zerocopy {
|
|
use super::*;
|
|
use vecdb::ZeroCopyVec;
|
|
|
|
impl IntegrityVec for ZeroCopyVec<usize, u32> {
|
|
fn import_with_changes<'a>(
|
|
db: &'a Database,
|
|
name: &'a str,
|
|
changes: u16,
|
|
) -> Result<(Self, ImportOptions<'a>)> {
|
|
let mut options: ImportOptions = (db, name, Version::TWO).into();
|
|
options = options.with_saved_stamped_changes(changes);
|
|
let vec = Self::forced_import_with(options)?;
|
|
Ok((vec, options))
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn data_integrity_rollback_flush_reopen() -> Result<(), Box<dyn std::error::Error>> {
|
|
run_data_integrity_rollback_flush_reopen::<ZeroCopyVec<usize, u32>>()
|
|
}
|
|
}
|