mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-08-11 09:53:04 -07:00
2110 lines
57 KiB
Rust
2110 lines
57 KiB
Rust
use rawdb::{Database, PAGE_SIZE, Result};
|
|
use std::sync::Arc;
|
|
use std::thread;
|
|
use tempfile::TempDir;
|
|
|
|
/// Helper to create a temporary test database
|
|
fn setup_test_db() -> Result<(Database, TempDir)> {
|
|
let temp_dir = TempDir::new()?;
|
|
let db = Database::open(temp_dir.path())?;
|
|
Ok((db, temp_dir))
|
|
}
|
|
|
|
#[test]
|
|
fn test_database_creation() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
// Database should start empty
|
|
assert_eq!(db.regions().index_to_region().len(), 0);
|
|
assert_eq!(db.layout().start_to_region().len(), 0);
|
|
assert_eq!(db.layout().start_to_hole().len(), 0);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_create_single_region() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region = db.create_region_if_needed("test_region")?;
|
|
|
|
// Verify region properties
|
|
let meta = region.meta();
|
|
assert_eq!(meta.start(), 0);
|
|
assert_eq!(meta.len(), 0);
|
|
assert_eq!(meta.reserved(), PAGE_SIZE);
|
|
drop(meta);
|
|
|
|
// Verify it's tracked in regions
|
|
let regions = db.regions();
|
|
assert_eq!(regions.index_to_region().len(), 1);
|
|
assert!(regions.get_from_id("test_region").is_some());
|
|
|
|
// Verify it's tracked in layout
|
|
let layout = db.layout();
|
|
assert_eq!(layout.start_to_region().len(), 1);
|
|
assert!(layout.start_to_hole().is_empty());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_create_region_idempotent() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region1 = db.create_region_if_needed("test")?;
|
|
let region2 = db.create_region_if_needed("test")?;
|
|
|
|
// Should return same region
|
|
assert_eq!(region1.index(), region2.index());
|
|
assert_eq!(db.regions().index_to_region().len(), 1);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_reserve_region_capacity_preserves_data() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region = db.create_region_if_needed("first")?;
|
|
region.write(b"preserved")?;
|
|
let blocker = db.create_region_if_needed("blocker")?;
|
|
let initial_start = region.meta().start();
|
|
|
|
region.reserve_capacity(PAGE_SIZE * 3 + 1)?;
|
|
|
|
let meta = region.meta();
|
|
assert_ne!(meta.start(), initial_start);
|
|
assert_eq!(meta.len(), b"preserved".len());
|
|
assert_eq!(meta.reserved(), PAGE_SIZE * 4);
|
|
drop(meta);
|
|
assert_eq!(region.create_reader().read_all(), b"preserved");
|
|
assert_eq!(blocker.meta().start(), PAGE_SIZE);
|
|
|
|
db.flush()?;
|
|
drop(blocker);
|
|
drop(region);
|
|
drop(db);
|
|
|
|
let reopened = Database::open(_temp.path())?;
|
|
let region = reopened.get_region("first").unwrap();
|
|
assert_eq!(region.meta().reserved(), PAGE_SIZE * 4);
|
|
assert_eq!(region.create_reader().read_all(), b"preserved");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_write_to_region_within_reserved() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region = db.create_region_if_needed("test")?;
|
|
let data = b"Hello, World!";
|
|
|
|
region.write(data)?;
|
|
|
|
// Verify data was written
|
|
let meta = region.meta();
|
|
assert_eq!(meta.len(), data.len());
|
|
assert_eq!(meta.reserved(), PAGE_SIZE);
|
|
let start = meta.start();
|
|
drop(meta);
|
|
|
|
let mmap = db.mmap();
|
|
assert_eq!(&mmap[start..start + data.len()], data);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_write_append() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region = db.create_region_if_needed("test")?;
|
|
|
|
region.write(b"Hello")?;
|
|
region.write(b", World!")?;
|
|
|
|
let meta = region.meta();
|
|
assert_eq!(meta.len(), 13);
|
|
let start = meta.start();
|
|
drop(meta);
|
|
|
|
let mmap = db.mmap();
|
|
assert_eq!(&mmap[start..(start + 13)], b"Hello, World!");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_write_at_position() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region = db.create_region_if_needed("test")?;
|
|
|
|
region.write(b"Hello, World!")?;
|
|
region.write_at(b"Rust!", 7)?;
|
|
|
|
let meta = region.meta();
|
|
let start = meta.start();
|
|
drop(meta);
|
|
|
|
let mmap = db.mmap();
|
|
assert_eq!(&mmap[start..(start + 13)], b"Hello, Rust!!");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_write_exceeds_reserved() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region = db.create_region_if_needed("test")?;
|
|
|
|
// Write more than PAGE_SIZE to trigger expansion
|
|
let large_data = vec![1u8; PAGE_SIZE + 100];
|
|
region.write(&large_data)?;
|
|
|
|
let meta = region.meta();
|
|
assert_eq!(meta.len(), large_data.len());
|
|
assert!(meta.reserved() >= PAGE_SIZE * 2);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_truncate_region() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region = db.create_region_if_needed("test")?;
|
|
|
|
region.write(b"Hello, World!")?;
|
|
|
|
let meta_before = region.meta();
|
|
assert_eq!(meta_before.len(), 13);
|
|
drop(meta_before);
|
|
|
|
region.truncate(5)?;
|
|
|
|
let meta_after = region.meta();
|
|
assert_eq!(meta_after.len(), 5);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_truncate_errors() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region = db.create_region_if_needed("test")?;
|
|
region.write(b"Hello")?;
|
|
|
|
// Truncating beyond length should error
|
|
let result = region.truncate(10);
|
|
assert!(result.is_err());
|
|
|
|
// Truncating to same length should be OK
|
|
let result = region.truncate(5);
|
|
assert!(result.is_ok());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_remove_region() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region = db.create_region_if_needed("test")?;
|
|
let index = region.index();
|
|
|
|
region.write(b"Hello")?;
|
|
|
|
// Remove region
|
|
region.remove()?;
|
|
|
|
// Verify removal
|
|
let regions = db.regions();
|
|
assert!(regions.get_from_id("test").is_none());
|
|
assert!(regions.get_from_index(index).is_none());
|
|
|
|
db.flush()?; // Make hole available
|
|
|
|
// Layout should have a hole now
|
|
let layout = db.layout();
|
|
assert_eq!(layout.start_to_region().len(), 0);
|
|
assert_eq!(layout.start_to_hole().len(), 1);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_multiple_regions() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region1 = db.create_region_if_needed("region1")?;
|
|
let region2 = db.create_region_if_needed("region2")?;
|
|
let region3 = db.create_region_if_needed("region3")?;
|
|
|
|
// Write different data to each
|
|
region1.write(b"First")?;
|
|
region2.write(b"Second")?;
|
|
region3.write(b"Third")?;
|
|
|
|
// Verify all exist
|
|
assert_eq!(db.regions().index_to_region().len(), 3);
|
|
assert_eq!(db.layout().start_to_region().len(), 3);
|
|
|
|
// Verify data integrity
|
|
let mmap = db.mmap();
|
|
|
|
let meta1 = region1.meta();
|
|
assert_eq!(&mmap[meta1.start()..(meta1.start() + 5)], b"First");
|
|
drop(meta1);
|
|
|
|
let meta2 = region2.meta();
|
|
assert_eq!(&mmap[meta2.start()..(meta2.start() + 6)], b"Second");
|
|
drop(meta2);
|
|
|
|
let meta3 = region3.meta();
|
|
assert_eq!(&mmap[meta3.start()..(meta3.start() + 5)], b"Third");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_region_reuse_after_removal() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region1 = db.create_region_if_needed("region1")?;
|
|
let _region2 = db.create_region_if_needed("region2")?;
|
|
let index1 = region1.index();
|
|
|
|
// Remove first region
|
|
region1.remove()?;
|
|
|
|
// Create a new region - should reuse the slot
|
|
let region3 = db.create_region_if_needed("region3")?;
|
|
assert_eq!(region3.index(), index1);
|
|
|
|
// Verify only 2 regions exist
|
|
let regions = db.regions();
|
|
assert_eq!(regions.id_to_index().len(), 2);
|
|
assert!(regions.get_from_id("region1").is_none());
|
|
assert!(regions.get_from_id("region2").is_some());
|
|
assert!(regions.get_from_id("region3").is_some());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_hole_filling() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let _region1 = db.create_region_if_needed("region1")?;
|
|
let region2 = db.create_region_if_needed("region2")?;
|
|
let _region3 = db.create_region_if_needed("region3")?;
|
|
|
|
// Remove middle region to create a hole
|
|
region2.remove()?;
|
|
db.flush()?; // Make hole available for reuse
|
|
|
|
let layout = db.layout();
|
|
assert_eq!(layout.start_to_hole().len(), 1);
|
|
drop(layout);
|
|
|
|
// Create new region - should fill the hole
|
|
let _region4 = db.create_region_if_needed("region4")?;
|
|
|
|
let layout = db.layout();
|
|
// Hole should be gone since new region takes PAGE_SIZE which fills it exactly
|
|
assert_eq!(layout.start_to_hole().len(), 0);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_persistence() -> Result<()> {
|
|
let temp = TempDir::new()?;
|
|
let path = temp.path();
|
|
dbg!(&path);
|
|
|
|
// Create and populate database
|
|
{
|
|
let db = Database::open(path)?;
|
|
let region = db.create_region_if_needed("persistent")?;
|
|
region.write(b"Persisted data")?;
|
|
db.flush()?;
|
|
}
|
|
|
|
// Reopen and verify
|
|
{
|
|
let db = Database::open(path)?;
|
|
let regions = db.regions();
|
|
let region = regions
|
|
.get_from_id("persistent")
|
|
.expect("Region should persist");
|
|
|
|
let meta = region.meta();
|
|
assert_eq!(meta.len(), 14);
|
|
let start = meta.start();
|
|
drop(meta);
|
|
|
|
let mmap = db.mmap();
|
|
assert_eq!(&mmap[start..(start + 14)], b"Persisted data");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_reader() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region = db.create_region_if_needed("test")?;
|
|
region.write(b"Hello, World!")?;
|
|
|
|
let reader = region.create_reader();
|
|
assert_eq!(reader.read_all(), b"Hello, World!");
|
|
assert_eq!(reader.read(0, 5), b"Hello");
|
|
assert_eq!(reader.read(7, 5), b"World");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_retain_regions() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let _ = db.create_region_if_needed("keep1")?;
|
|
let _ = db.create_region_if_needed("remove1")?;
|
|
let _ = db.create_region_if_needed("keep2")?;
|
|
let _ = db.create_region_if_needed("remove2")?;
|
|
|
|
let mut keep_set = std::collections::HashSet::new();
|
|
keep_set.insert("keep1".to_string());
|
|
keep_set.insert("keep2".to_string());
|
|
|
|
db.retain_regions(keep_set)?;
|
|
|
|
let regions = db.regions();
|
|
assert_eq!(regions.id_to_index().len(), 2);
|
|
assert!(regions.get_from_id("keep1").is_some());
|
|
assert!(regions.get_from_id("keep2").is_some());
|
|
assert!(regions.get_from_id("remove1").is_none());
|
|
assert!(regions.get_from_id("remove2").is_none());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_region_defragmentation() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region1 = db.create_region_if_needed("region1")?;
|
|
let region2 = db.create_region_if_needed("region2")?;
|
|
|
|
dbg!(0);
|
|
|
|
// Write small data first
|
|
region1.write(b"small")?;
|
|
|
|
dbg!(1);
|
|
// Write large data to region1 - should move it to end
|
|
let large_data = vec![1u8; PAGE_SIZE * 2];
|
|
region1.write(&large_data)?;
|
|
db.flush()?; // Make hole available
|
|
|
|
dbg!(2);
|
|
// region1 should have moved, leaving a hole
|
|
let layout = db.layout();
|
|
assert!(layout.start_to_hole().len() == 1);
|
|
|
|
dbg!(3);
|
|
// region2 should still be at its original position
|
|
let meta2 = region2.meta();
|
|
assert_eq!(meta2.start(), PAGE_SIZE);
|
|
dbg!(4);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_concurrent_region_creation() -> Result<()> {
|
|
let temp = TempDir::new()?;
|
|
let db = Arc::new(Database::open(temp.path())?);
|
|
|
|
let handles: Vec<_> = (0..10)
|
|
.map(|i| {
|
|
let db = Arc::clone(&db);
|
|
thread::spawn(move || {
|
|
let region_name = format!("region_{}", i);
|
|
db.create_region_if_needed(®ion_name)
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
// Wait for all threads
|
|
for handle in handles {
|
|
let _ = handle.join().unwrap()?;
|
|
}
|
|
|
|
// Verify all regions created
|
|
let regions = db.regions();
|
|
assert_eq!(regions.id_to_index().len(), 10);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_set_min_regions() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
db.set_min_regions(100)?;
|
|
|
|
// File should be large enough for 100 regions
|
|
let file_len = db.file_len();
|
|
assert!(file_len >= 100 * PAGE_SIZE);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_large_write() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region = db.create_region_if_needed("large")?;
|
|
|
|
// Write 1MB of data
|
|
let large_data = vec![42u8; 1024 * 1024];
|
|
region.write(&large_data)?;
|
|
|
|
let meta = region.meta();
|
|
assert_eq!(meta.len(), large_data.len());
|
|
let start = meta.start();
|
|
drop(meta);
|
|
|
|
// Verify data
|
|
let mmap = db.mmap();
|
|
assert_eq!(&mmap[start..(start + large_data.len())], &large_data[..]);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_truncate_write() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region = db.create_region_if_needed("test")?;
|
|
|
|
region.write(b"Hello, World!")?;
|
|
|
|
let meta_before = region.meta();
|
|
assert_eq!(meta_before.len(), 13);
|
|
drop(meta_before);
|
|
|
|
// Truncate write - should set length to exactly the written data
|
|
region.truncate_write(7, b"Rust")?;
|
|
|
|
let meta_after = region.meta();
|
|
assert_eq!(meta_after.len(), 11); // 7 + 4
|
|
let start = meta_after.start();
|
|
drop(meta_after);
|
|
|
|
let mmap = db.mmap();
|
|
assert_eq!(&mmap[start..(start + 11)], b"Hello, Rust");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_punch_holes() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region = db.create_region_if_needed("test")?;
|
|
|
|
// Write large data then truncate
|
|
let large_data = vec![1u8; PAGE_SIZE * 2];
|
|
region.write(&large_data)?;
|
|
region.truncate(100)?;
|
|
|
|
// Flush and punch holes
|
|
db.compact()?;
|
|
|
|
// Should still be able to read the data
|
|
let meta = region.meta();
|
|
assert_eq!(meta.len(), 100);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_write_at_invalid_position() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region = db.create_region_if_needed("test")?;
|
|
region.write(b"Hello")?;
|
|
|
|
// Writing beyond length should fail
|
|
let result = region.write_at(b"World", 10);
|
|
assert!(result.is_err());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_empty_region_operations() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region = db.create_region_if_needed("empty")?;
|
|
|
|
// Reading empty region
|
|
let reader = region.create_reader();
|
|
assert_eq!(reader.read_all(), b"");
|
|
drop(reader);
|
|
|
|
// Truncating empty region to 0 should work
|
|
region.truncate(0)?;
|
|
|
|
let meta = region.meta();
|
|
assert_eq!(meta.len(), 0);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_region_metadata_updates() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region = db.create_region_if_needed("test")?;
|
|
|
|
// Initial state
|
|
{
|
|
let meta = region.meta();
|
|
assert_eq!(meta.start(), 0);
|
|
assert_eq!(meta.len(), 0);
|
|
assert_eq!(meta.reserved(), PAGE_SIZE);
|
|
}
|
|
|
|
// After first write
|
|
region.write(b"Hello")?;
|
|
{
|
|
let meta = region.meta();
|
|
assert_eq!(meta.len(), 5);
|
|
assert_eq!(meta.reserved(), PAGE_SIZE);
|
|
}
|
|
|
|
// After expansion
|
|
let large = vec![1u8; PAGE_SIZE * 3];
|
|
region.write(&large)?;
|
|
{
|
|
let meta = region.meta();
|
|
assert_eq!(meta.len(), 5 + large.len());
|
|
assert!(meta.reserved() >= PAGE_SIZE * 4);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// ============================================================================
|
|
// Complex Integration Tests
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_complex_region_lifecycle() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
// Create multiple regions
|
|
let r1 = db.create_region_if_needed("region1")?;
|
|
let r2 = db.create_region_if_needed("region2")?;
|
|
let r3 = db.create_region_if_needed("region3")?;
|
|
|
|
// Write to all regions
|
|
r1.write(b"Data for region 1")?;
|
|
r2.write(b"Data for region 2")?;
|
|
r3.write(b"Data for region 3")?;
|
|
|
|
// Remove middle region
|
|
r2.remove()?;
|
|
db.flush()?; // Make hole available
|
|
|
|
// Verify hole exists
|
|
{
|
|
let layout = db.layout();
|
|
assert_eq!(layout.start_to_hole().len(), 1);
|
|
}
|
|
|
|
// Create new region that should reuse the hole
|
|
let r4 = db.create_region_if_needed("region4")?;
|
|
r4.write(b"Fills the hole")?;
|
|
|
|
// Verify hole was filled
|
|
{
|
|
let layout = db.layout();
|
|
assert_eq!(layout.start_to_hole().len(), 0);
|
|
}
|
|
|
|
// Write large data to trigger region movement (overwrite from start)
|
|
let large = vec![42u8; PAGE_SIZE * 3];
|
|
r4.write_at(&large, 0)?;
|
|
db.flush()?; // Make hole available
|
|
|
|
// Verify r4 moved and created a hole
|
|
{
|
|
let layout = db.layout();
|
|
assert!(!layout.start_to_hole().is_empty());
|
|
}
|
|
|
|
// Verify all data is still correct
|
|
{
|
|
let reader = r1.create_reader();
|
|
assert_eq!(reader.read_all(), b"Data for region 1");
|
|
drop(reader);
|
|
}
|
|
|
|
{
|
|
let reader = r3.create_reader();
|
|
assert_eq!(reader.read_all(), b"Data for region 3");
|
|
drop(reader);
|
|
}
|
|
|
|
{
|
|
let reader = r4.create_reader();
|
|
assert_eq!(reader.read_all(), &large[..]);
|
|
drop(reader);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_many_small_regions() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
// Create 50 small regions
|
|
let mut regions = Vec::new();
|
|
for i in 0..50 {
|
|
let name = format!("region_{}", i);
|
|
let region = db.create_region_if_needed(&name)?;
|
|
let data = format!("Data for region {}", i);
|
|
region.write(data.as_bytes())?;
|
|
regions.push(Some(region));
|
|
}
|
|
|
|
// Verify all regions
|
|
for (i, region_opt) in regions.iter().enumerate() {
|
|
let region = region_opt.as_ref().unwrap();
|
|
let reader = region.create_reader();
|
|
let expected = format!("Data for region {}", i);
|
|
assert_eq!(reader.read_all(), expected.as_bytes());
|
|
drop(reader);
|
|
}
|
|
|
|
// Remove every other region
|
|
for i in (0..50).step_by(2) {
|
|
let region = regions[i].take().unwrap();
|
|
region.remove()?;
|
|
}
|
|
|
|
// Verify remaining regions
|
|
for i in (1..50).step_by(2) {
|
|
let region = regions[i].as_ref().unwrap();
|
|
let reader = region.create_reader();
|
|
let expected = format!("Data for region {}", i);
|
|
assert_eq!(reader.read_all(), expected.as_bytes());
|
|
drop(reader);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_interleaved_operations() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let r1 = db.create_region_if_needed("r1")?;
|
|
let r2 = db.create_region_if_needed("r2")?;
|
|
let r3 = db.create_region_if_needed("r3")?;
|
|
|
|
// Interleave writes
|
|
r1.write(b"Start1")?;
|
|
r2.write(b"Start2")?;
|
|
r3.write(b"Start3")?;
|
|
|
|
r1.write(b" More1")?;
|
|
r2.write(b" More2")?;
|
|
|
|
// Truncate one
|
|
r3.truncate(3)?;
|
|
|
|
// Continue writing
|
|
r1.write(b" End1")?;
|
|
r2.write_at(b"X", 0)?;
|
|
|
|
// Verify results
|
|
{
|
|
let reader = r1.create_reader();
|
|
assert_eq!(reader.read_all(), b"Start1 More1 End1");
|
|
drop(reader);
|
|
}
|
|
|
|
{
|
|
let reader = r2.create_reader();
|
|
assert_eq!(reader.read_all(), b"Xtart2 More2");
|
|
drop(reader);
|
|
}
|
|
|
|
{
|
|
let meta = r3.meta();
|
|
assert_eq!(meta.len(), 3);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_persistence_with_holes() -> Result<()> {
|
|
let temp = TempDir::new()?;
|
|
let path = temp.path();
|
|
|
|
// Create database with regions and holes
|
|
{
|
|
let db = Database::open(path)?;
|
|
|
|
let r1 = db.create_region_if_needed("keep1")?;
|
|
let r2 = db.create_region_if_needed("remove")?;
|
|
let r3 = db.create_region_if_needed("keep2")?;
|
|
|
|
r1.write(b"Keep this 1")?;
|
|
r2.write(b"Remove this")?;
|
|
r3.write(b"Keep this 2")?;
|
|
|
|
r2.remove()?;
|
|
db.flush()?;
|
|
}
|
|
|
|
// Reopen and verify
|
|
{
|
|
let db = Database::open(path)?;
|
|
|
|
let regions = db.regions();
|
|
assert!(regions.get_from_id("keep1").is_some());
|
|
assert!(regions.get_from_id("remove").is_none());
|
|
assert!(regions.get_from_id("keep2").is_some());
|
|
|
|
let r1 = regions.get_from_id("keep1").unwrap();
|
|
let r3 = regions.get_from_id("keep2").unwrap();
|
|
|
|
let reader1 = r1.create_reader();
|
|
assert_eq!(reader1.read_all(), b"Keep this 1");
|
|
drop(reader1);
|
|
|
|
let reader3 = r3.create_reader();
|
|
assert_eq!(reader3.read_all(), b"Keep this 2");
|
|
drop(reader3);
|
|
|
|
// Verify hole still exists
|
|
let layout = db.layout();
|
|
assert!(!layout.start_to_hole().is_empty());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_region_growth_patterns() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region = db.create_region_if_needed("growing")?;
|
|
|
|
// Grow gradually
|
|
for i in 0..10 {
|
|
let data = vec![i as u8; 1000];
|
|
region.write(&data)?;
|
|
}
|
|
|
|
let meta = region.meta();
|
|
assert_eq!(meta.len(), 10_000);
|
|
|
|
// Verify all data
|
|
let reader = region.create_reader();
|
|
let all_data = reader.read_all();
|
|
for i in 0..10 {
|
|
let chunk = &all_data[i * 1000..(i + 1) * 1000];
|
|
assert!(chunk.iter().all(|&b| b == i as u8));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_write_at_boundary_conditions() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region = db.create_region_if_needed("boundary")?;
|
|
|
|
// Write at start
|
|
region.write(b"0123456789")?;
|
|
|
|
// Write at exact length boundary
|
|
region.write_at(b"ABC", 10)?;
|
|
|
|
// Write at position 0
|
|
region.write_at(b"X", 0)?;
|
|
|
|
let reader = region.create_reader();
|
|
assert_eq!(reader.read_all(), b"X123456789ABC");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_multiple_flushes() -> Result<()> {
|
|
let temp = TempDir::new()?;
|
|
let path = temp.path();
|
|
|
|
{
|
|
let db = Database::open(path)?;
|
|
let r = db.create_region_if_needed("test")?;
|
|
|
|
r.write(b"Version 1")?;
|
|
db.flush()?;
|
|
|
|
r.write(b" Version 2")?;
|
|
db.flush()?;
|
|
|
|
r.write(b" Version 3")?;
|
|
db.flush()?;
|
|
}
|
|
|
|
{
|
|
let db = Database::open(path)?;
|
|
let regions = db.regions();
|
|
let r = regions.get_from_id("test").unwrap();
|
|
|
|
let reader = r.create_reader();
|
|
assert_eq!(reader.read_all(), b"Version 1 Version 2 Version 3");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_hole_coalescing() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
// Create 5 regions
|
|
let mut regions: Vec<_> = (0..5)
|
|
.map(|i| db.create_region_if_needed(&format!("r{}", i)))
|
|
.collect::<Result<Vec<_>>>()?
|
|
.into_iter()
|
|
.map(Some)
|
|
.collect();
|
|
|
|
// Write small data to each
|
|
for r in ®ions {
|
|
r.as_ref().unwrap().write(b"data")?;
|
|
}
|
|
|
|
// Remove regions 1, 2, 3 to create adjacent holes
|
|
regions[1].take().unwrap().remove()?;
|
|
regions[2].take().unwrap().remove()?;
|
|
regions[3].take().unwrap().remove()?;
|
|
db.flush()?; // Make holes available and coalesce
|
|
|
|
// Check that holes were coalesced
|
|
let layout = db.layout();
|
|
// Should have 1 large hole, not 3 separate ones
|
|
let holes = layout.start_to_hole();
|
|
assert_eq!(holes.len(), 1);
|
|
|
|
// The single hole should span all 3 removed regions
|
|
let hole_size = holes.values().next().unwrap();
|
|
assert_eq!(*hole_size, PAGE_SIZE * 3);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_stress_region_creation_and_removal() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
// Create and remove regions in a cycle
|
|
for cycle in 0..5 {
|
|
// Create 20 regions
|
|
let regions: Vec<_> = (0..20)
|
|
.map(|i| {
|
|
let name = format!("cycle_{}_region_{}", cycle, i);
|
|
db.create_region_if_needed(&name)
|
|
})
|
|
.collect::<Result<Vec<_>>>()?;
|
|
|
|
// Write to each
|
|
for (i, r) in regions.iter().enumerate() {
|
|
let data = format!("Cycle {} Region {}", cycle, i);
|
|
r.write(data.as_bytes())?;
|
|
}
|
|
|
|
// Verify
|
|
for (i, r) in regions.iter().enumerate() {
|
|
let reader = r.create_reader();
|
|
let expected = format!("Cycle {} Region {}", cycle, i);
|
|
assert_eq!(reader.read_all(), expected.as_bytes());
|
|
drop(reader);
|
|
}
|
|
|
|
// Remove all
|
|
for r in regions {
|
|
r.remove()?;
|
|
}
|
|
|
|
// Verify all gone
|
|
let reg = db.regions();
|
|
assert_eq!(reg.id_to_index().len(), 0);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_mixed_size_writes() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region = db.create_region_if_needed("mixed")?;
|
|
|
|
// Write various sizes
|
|
region.write(b"tiny")?;
|
|
region.write(&[1u8; 100])?;
|
|
region.write(&[2u8; 1000])?;
|
|
region.write(&[3u8; 10000])?;
|
|
|
|
let meta = region.meta();
|
|
assert_eq!(meta.len(), 4 + 100 + 1000 + 10000);
|
|
|
|
// Verify each section
|
|
let reader = region.create_reader();
|
|
assert_eq!(&reader.read(0, 4), b"tiny");
|
|
assert!(reader.read(4, 100).iter().all(|&b| b == 1));
|
|
assert!(reader.read(104, 1000).iter().all(|&b| b == 2));
|
|
assert!(reader.read(1104, 10000).iter().all(|&b| b == 3));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// ============================================================================
|
|
// Concurrent Operations Tests
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_concurrent_writes_to_different_regions() -> Result<()> {
|
|
let temp = TempDir::new()?;
|
|
let db = Arc::new(Database::open(temp.path())?);
|
|
|
|
// Create regions upfront
|
|
let regions: Vec<_> = (0..10)
|
|
.map(|i| db.create_region_if_needed(&format!("region_{}", i)))
|
|
.collect::<Result<Vec<_>>>()?;
|
|
|
|
// Write to different regions concurrently
|
|
let handles: Vec<_> = regions
|
|
.into_iter()
|
|
.enumerate()
|
|
.map(|(i, region)| {
|
|
thread::spawn(move || {
|
|
let data = vec![i as u8; 1000];
|
|
region.write(&data)
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
// Wait for all writes
|
|
for handle in handles {
|
|
handle.join().unwrap()?;
|
|
}
|
|
|
|
// Verify all data
|
|
for i in 0..10 {
|
|
let regions = db.regions();
|
|
let region = regions.get_from_id(&format!("region_{}", i)).unwrap();
|
|
let reader = region.create_reader();
|
|
let data = reader.read_all();
|
|
assert_eq!(data.len(), 1000);
|
|
assert!(data.iter().all(|&b| b == i as u8));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_concurrent_reads() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
let db = Arc::new(db);
|
|
|
|
let region = db.create_region_if_needed("shared")?;
|
|
let data = b"Shared data for concurrent reads";
|
|
region.write(data)?;
|
|
|
|
// Multiple threads reading simultaneously
|
|
let handles: Vec<_> = (0..20)
|
|
.map(|_| {
|
|
let db = Arc::clone(&db);
|
|
thread::spawn(move || {
|
|
let regions = db.regions();
|
|
let region = regions.get_from_id("shared").unwrap();
|
|
let reader = region.create_reader();
|
|
assert_eq!(reader.read_all(), b"Shared data for concurrent reads");
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
for handle in handles {
|
|
handle.join().unwrap();
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// ============================================================================
|
|
// Reader Edge Cases
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_reader_prefixed() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region = db.create_region_if_needed("test")?;
|
|
region.write(b"0123456789ABCDEF")?;
|
|
|
|
let reader = region.create_reader();
|
|
|
|
// Test prefixed reads
|
|
let prefixed = reader.prefixed(5);
|
|
assert!(prefixed.starts_with(b"56789ABCDEF"));
|
|
|
|
let prefixed_at_start = reader.prefixed(0);
|
|
assert!(prefixed_at_start.starts_with(b"0123456789ABCDEF"));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_reader_unchecked_read() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region = db.create_region_if_needed("test")?;
|
|
region.write(b"Hello World")?;
|
|
|
|
let reader = region.create_reader();
|
|
|
|
// Unchecked read within bounds should work
|
|
let data = reader.unchecked_read(0, 5);
|
|
assert_eq!(data, b"Hello");
|
|
|
|
let data = reader.unchecked_read(6, 5);
|
|
assert_eq!(data, b"World");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic]
|
|
fn test_reader_bounds_check() {
|
|
let (db, _temp) = setup_test_db().unwrap();
|
|
|
|
let region = db.create_region_if_needed("test").unwrap();
|
|
region.write(b"Short").unwrap();
|
|
|
|
let reader = region.create_reader();
|
|
|
|
// This should panic due to bounds check
|
|
let _ = reader.read(0, 100);
|
|
}
|
|
|
|
// Test that Reader is self-contained and safe to use after dropping original references
|
|
#[test]
|
|
fn test_reader_outlives_region_variable() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let reader = {
|
|
let region = db.create_region_if_needed("test")?;
|
|
region.write(b"Hello from dropped region")?;
|
|
region.create_reader()
|
|
// region is dropped here, but reader should still be valid
|
|
};
|
|
|
|
// Reader should still work because it holds its own Arc references
|
|
assert_eq!(reader.read_all(), b"Hello from dropped region");
|
|
assert_eq!(reader.read(0, 5), b"Hello");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_reader_outlives_database_variable() -> Result<()> {
|
|
let (_temp, reader) = {
|
|
let (db, temp) = setup_test_db()?;
|
|
let region = db.create_region_if_needed("test")?;
|
|
region.write(b"Persisted data")?;
|
|
let reader = region.create_reader();
|
|
// Both db and region go out of scope, but reader holds Arc clones
|
|
(temp, reader)
|
|
};
|
|
|
|
// Reader should still work
|
|
assert_eq!(reader.read_all(), b"Persisted data");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_reader_can_be_stored_in_struct() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
struct DataHolder {
|
|
reader: rawdb::Reader,
|
|
}
|
|
|
|
let region = db.create_region_if_needed("test")?;
|
|
region.write(b"Stored in struct")?;
|
|
|
|
let holder = DataHolder {
|
|
reader: region.create_reader(),
|
|
};
|
|
|
|
// Can use reader through struct
|
|
assert_eq!(holder.reader.read_all(), b"Stored in struct");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_multiple_readers_from_same_region() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region = db.create_region_if_needed("test")?;
|
|
region.write(b"Shared data")?;
|
|
|
|
// Create multiple readers
|
|
let reader1 = region.create_reader();
|
|
let reader2 = region.create_reader();
|
|
let reader3 = region.create_reader();
|
|
|
|
// All should read the same data
|
|
assert_eq!(reader1.read_all(), b"Shared data");
|
|
assert_eq!(reader2.read_all(), b"Shared data");
|
|
assert_eq!(reader3.read_all(), b"Shared data");
|
|
|
|
// Drop in different order
|
|
drop(reader2);
|
|
assert_eq!(reader1.read_all(), b"Shared data");
|
|
assert_eq!(reader3.read_all(), b"Shared data");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// ============================================================================
|
|
// Extreme Cases
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_very_long_region_names() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
// Create regions with very long names
|
|
let long_name = "a".repeat(1000);
|
|
let region = db.create_region_if_needed(&long_name)?;
|
|
region.write(b"data")?;
|
|
|
|
// Verify it persists
|
|
db.flush()?;
|
|
|
|
let regions = db.regions();
|
|
let retrieved = regions.get_from_id(&long_name);
|
|
assert!(retrieved.is_some());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_zero_byte_writes() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region = db.create_region_if_needed("empty_writes")?;
|
|
|
|
// Write zero bytes
|
|
region.write(b"")?;
|
|
|
|
let meta = region.meta();
|
|
assert_eq!(meta.len(), 0);
|
|
drop(meta);
|
|
|
|
// Write some data, then write zero bytes again
|
|
region.write(b"Hello")?;
|
|
region.write(b"")?;
|
|
|
|
let meta = region.meta();
|
|
assert_eq!(meta.len(), 5);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_alternating_write_and_truncate() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region = db.create_region_if_needed("oscillating")?;
|
|
|
|
for cycle in 0..10 {
|
|
// Grow
|
|
let data = vec![cycle as u8; 1000];
|
|
region.write(&data)?;
|
|
|
|
let meta = region.meta();
|
|
let expected_len = if cycle == 0 { 1000 } else { 100 + 1000 };
|
|
assert_eq!(meta.len(), expected_len);
|
|
drop(meta);
|
|
|
|
// Shrink
|
|
region.truncate(100)?;
|
|
|
|
let meta = region.meta();
|
|
assert_eq!(meta.len(), 100);
|
|
drop(meta);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// ============================================================================
|
|
// Persistence Edge Cases
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_retain_regions_edge_cases() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
// Create some regions
|
|
let _ = db.create_region_if_needed("keep1")?;
|
|
let _ = db.create_region_if_needed("keep2")?;
|
|
let _ = db.create_region_if_needed("remove1")?;
|
|
|
|
// Retain with empty set - should remove all
|
|
let empty_set = std::collections::HashSet::new();
|
|
db.retain_regions(empty_set)?;
|
|
|
|
let regions = db.regions();
|
|
assert_eq!(regions.id_to_index().len(), 0);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// ============================================================================
|
|
// Layout and Hole Management
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_complex_fragmentation_scenario() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
// Create pattern: region, region, region, region, region
|
|
let r0 = db.create_region_if_needed("r0")?;
|
|
let r1 = db.create_region_if_needed("r1")?;
|
|
let r2 = db.create_region_if_needed("r2")?;
|
|
let r3 = db.create_region_if_needed("r3")?;
|
|
let r4 = db.create_region_if_needed("r4")?;
|
|
|
|
for r in [&r0, &r1, &r2, &r3, &r4] {
|
|
r.write(b"data")?;
|
|
}
|
|
|
|
// Remove pattern: keep, remove, keep, remove, keep
|
|
// This creates 2 separate holes
|
|
r1.remove()?;
|
|
r3.remove()?;
|
|
db.flush()?; // Make holes available
|
|
|
|
let layout = db.layout();
|
|
assert_eq!(layout.start_to_hole().len(), 2);
|
|
drop(layout);
|
|
|
|
// Create a new region - should fill one of the holes
|
|
let r5 = db.create_region_if_needed("r5")?;
|
|
r5.write(b"fills hole")?;
|
|
|
|
let layout = db.layout();
|
|
assert_eq!(layout.start_to_hole().len(), 1); // One hole filled, one remains
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_set_min_len_preallocate() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
// Preallocate large file
|
|
let large_size = PAGE_SIZE * 1000;
|
|
db.set_min_len(large_size)?;
|
|
|
|
let file_len = db.file_len();
|
|
assert!(file_len >= large_size);
|
|
|
|
// Should still be able to write
|
|
let region = db.create_region_if_needed("test")?;
|
|
region.write(b"After preallocation")?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// ============================================================================
|
|
// Data Integrity Tests
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_partial_overwrites_data_integrity() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region = db.create_region_if_needed("partial")?;
|
|
|
|
// Write initial pattern
|
|
let initial = b"AAAAAAAAAA";
|
|
region.write(initial)?;
|
|
|
|
// Overwrite middle
|
|
region.write_at(b"BBB", 3)?;
|
|
|
|
// Overwrite start
|
|
region.write_at(b"CC", 0)?;
|
|
|
|
// Overwrite end
|
|
region.write_at(b"DD", 8)?;
|
|
|
|
let reader = region.create_reader();
|
|
assert_eq!(reader.read_all(), b"CCABBBAADD");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_write_at_exact_reserved_boundary() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region = db.create_region_if_needed("boundary")?;
|
|
|
|
// Fill exactly to PAGE_SIZE
|
|
let data = vec![42u8; PAGE_SIZE];
|
|
region.write(&data)?;
|
|
|
|
let meta = region.meta();
|
|
assert_eq!(meta.len(), PAGE_SIZE);
|
|
assert_eq!(meta.reserved(), PAGE_SIZE);
|
|
drop(meta);
|
|
|
|
// Writing one more byte should trigger expansion
|
|
region.write(b"X")?;
|
|
|
|
let meta = region.meta();
|
|
assert_eq!(meta.len(), PAGE_SIZE + 1);
|
|
assert!(meta.reserved() > PAGE_SIZE);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// ============================================================================
|
|
// Comprehensive Integration Test
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_comprehensive_db_operations() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region1 = db.create_region_if_needed("region1")?;
|
|
|
|
{
|
|
let layout = db.layout();
|
|
|
|
assert!(layout.start_to_region().len() == 1);
|
|
|
|
assert!(layout.start_to_hole().is_empty());
|
|
|
|
let regions = db.regions();
|
|
|
|
assert!(
|
|
regions
|
|
.get_from_id("region1")
|
|
.is_some_and(|r| r.ptr_eq(®ion1))
|
|
);
|
|
|
|
let region1_meta = region1.meta();
|
|
assert!(region1_meta.start() == 0);
|
|
assert!(region1_meta.len() == 0);
|
|
assert!(region1_meta.reserved() == PAGE_SIZE);
|
|
}
|
|
|
|
region1.write(&[0, 1, 2, 3, 4])?;
|
|
|
|
{
|
|
let region1_meta = region1.meta();
|
|
assert!(region1_meta.start() == 0);
|
|
assert!(region1_meta.len() == 5);
|
|
assert!(region1_meta.reserved() == PAGE_SIZE);
|
|
assert!(db.mmap()[0..10] == [0, 1, 2, 3, 4, 0, 0, 0, 0, 0]);
|
|
}
|
|
|
|
region1.write(&[5, 6, 7, 8, 9])?;
|
|
|
|
{
|
|
let region1_meta = region1.meta();
|
|
assert!(region1_meta.start() == 0);
|
|
assert!(region1_meta.len() == 10);
|
|
assert!(region1_meta.reserved() == PAGE_SIZE);
|
|
assert!(db.mmap()[0..10] == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
|
}
|
|
|
|
region1.write_at(&[1, 2], 0)?;
|
|
|
|
{
|
|
let region1_meta = region1.meta();
|
|
assert!(region1_meta.start() == 0);
|
|
assert!(region1_meta.len() == 10);
|
|
assert!(region1_meta.reserved() == PAGE_SIZE);
|
|
assert!(db.mmap()[0..10] == [1, 2, 2, 3, 4, 5, 6, 7, 8, 9]);
|
|
}
|
|
|
|
region1.write_at(&[10, 11, 12, 13, 14, 15, 16, 17, 18], 4)?;
|
|
|
|
{
|
|
let region1_meta = region1.meta();
|
|
assert!(region1_meta.start() == 0);
|
|
assert!(region1_meta.len() == 13);
|
|
assert!(region1_meta.reserved() == PAGE_SIZE);
|
|
assert!(
|
|
db.mmap()[0..20]
|
|
== [
|
|
1, 2, 2, 3, 10, 11, 12, 13, 14, 15, 16, 17, 18, 0, 0, 0, 0, 0, 0, 0
|
|
]
|
|
);
|
|
}
|
|
|
|
region1.write_at(&[0, 0, 0, 0, 0, 1], 13)?;
|
|
|
|
{
|
|
let region1_meta = region1.meta();
|
|
assert!(region1_meta.start() == 0);
|
|
assert!(region1_meta.len() == 19);
|
|
assert!(region1_meta.reserved() == PAGE_SIZE);
|
|
assert!(
|
|
db.mmap()[0..20]
|
|
== [
|
|
1, 2, 2, 3, 10, 11, 12, 13, 14, 15, 16, 17, 18, 0, 0, 0, 0, 0, 1, 0
|
|
]
|
|
);
|
|
}
|
|
|
|
region1.write_at(&[1; 8000], 0)?;
|
|
|
|
{
|
|
let region1_meta = region1.meta();
|
|
assert!(region1_meta.start() == 0);
|
|
assert!(region1_meta.len() == 8000);
|
|
assert!(region1_meta.reserved() == PAGE_SIZE * 2);
|
|
assert!(db.mmap()[0..8000] == [1; 8000]);
|
|
assert!(db.mmap()[8000..8001] == [0]);
|
|
}
|
|
|
|
db.flush()?;
|
|
|
|
region1.truncate(10)?;
|
|
db.compact()?;
|
|
|
|
{
|
|
let region1_meta = region1.meta();
|
|
assert!(region1_meta.start() == 0);
|
|
assert!(region1_meta.len() == 10);
|
|
assert!(region1_meta.reserved() == PAGE_SIZE * 2);
|
|
|
|
// We only punch a hole in whole pages (4096 bytes)
|
|
// Thus the last byte of the page where the is still data wasn't overwritten when truncating
|
|
// And the first byte of the punched page was set to 0
|
|
assert!(db.mmap()[4095..=4096] == [1, 0]);
|
|
}
|
|
|
|
db.flush()?;
|
|
|
|
region1.truncate(10)?;
|
|
db.compact()?;
|
|
|
|
{
|
|
let region1_meta = region1.meta();
|
|
assert!(region1_meta.start() == 0);
|
|
assert!(region1_meta.len() == 10);
|
|
assert!(region1_meta.reserved() == PAGE_SIZE * 2);
|
|
// We only punch a hole in whole pages (4096 bytes)
|
|
// Thus the last byte of the page where the is still data wasn't overwritten when truncating
|
|
// And the first byte of the punched page was set to 0
|
|
assert!(db.mmap()[4095..=4096] == [1, 0]);
|
|
}
|
|
|
|
db.flush()?;
|
|
|
|
region1.remove()?;
|
|
db.compact()?;
|
|
|
|
{
|
|
let regions = db.regions();
|
|
let index_to_region = regions.index_to_region();
|
|
assert!(index_to_region.len() == 1);
|
|
assert!(index_to_region[0].is_none());
|
|
assert!(regions.id_to_index().is_empty());
|
|
|
|
let layout = db.layout();
|
|
assert!(layout.start_to_region().is_empty());
|
|
assert!(layout.start_to_hole().len() == 1);
|
|
}
|
|
|
|
let region1 = db.create_region_if_needed("region1")?;
|
|
let region2 = db.create_region_if_needed("region2")?;
|
|
let region3 = db.create_region_if_needed("region3")?;
|
|
|
|
{
|
|
let regions = db.regions();
|
|
let index_to_region = regions.index_to_region();
|
|
assert!(index_to_region.len() == 3);
|
|
|
|
let region1_meta = region1.meta();
|
|
assert!(region1_meta.start() == 0);
|
|
assert!(region1_meta.len() == 0);
|
|
assert!(region1_meta.reserved() == PAGE_SIZE);
|
|
|
|
let region2_meta = region2.meta();
|
|
assert!(region2_meta.start() == PAGE_SIZE);
|
|
assert!(region2_meta.len() == 0);
|
|
assert!(region2_meta.reserved() == PAGE_SIZE);
|
|
|
|
let region3_meta = region3.meta();
|
|
assert!(region3_meta.start() == PAGE_SIZE * 2);
|
|
assert!(region3_meta.len() == 0);
|
|
assert!(region3_meta.reserved() == PAGE_SIZE);
|
|
|
|
let id_to_index = regions.id_to_index();
|
|
assert!(id_to_index.len() == 3);
|
|
assert!(id_to_index.get("region1") == Some(&0));
|
|
assert!(id_to_index.get("region2") == Some(&1));
|
|
assert!(id_to_index.get("region3") == Some(&2));
|
|
|
|
let layout = db.layout();
|
|
let start_to_index = layout.start_to_region();
|
|
assert!(start_to_index.len() == 3);
|
|
|
|
assert!(start_to_index.get(&0).unwrap().ptr_eq(®ion1));
|
|
assert!(start_to_index.get(&PAGE_SIZE).unwrap().ptr_eq(®ion2));
|
|
assert!(
|
|
start_to_index
|
|
.get(&(PAGE_SIZE * 2))
|
|
.unwrap()
|
|
.ptr_eq(®ion3)
|
|
);
|
|
assert!(layout.start_to_hole().is_empty());
|
|
}
|
|
|
|
region2.remove()?;
|
|
db.compact()?;
|
|
|
|
{
|
|
let regions = db.regions();
|
|
let index_to_region = regions.index_to_region();
|
|
assert!(index_to_region.len() == 3);
|
|
|
|
let region1_meta = region1.meta();
|
|
assert!(region1_meta.start() == 0);
|
|
assert!(region1_meta.len() == 0);
|
|
assert!(region1_meta.reserved() == PAGE_SIZE);
|
|
assert!(index_to_region.get(1).is_some_and(|opt| opt.is_none()));
|
|
|
|
let region3_meta = region3.meta();
|
|
assert!(region3_meta.start() == PAGE_SIZE * 2);
|
|
assert!(region3_meta.len() == 0);
|
|
assert!(region3_meta.reserved() == PAGE_SIZE);
|
|
let id_to_index = regions.id_to_index();
|
|
assert!(id_to_index.len() == 2);
|
|
assert!(id_to_index.get("region1") == Some(&0));
|
|
assert!(id_to_index.get("region2").is_none());
|
|
assert!(id_to_index.get("region3") == Some(&2));
|
|
|
|
let layout = db.layout();
|
|
let start_to_index = layout.start_to_region();
|
|
assert!(start_to_index.len() == 2);
|
|
assert!(start_to_index.get(&0).unwrap().ptr_eq(®ion1));
|
|
assert!(
|
|
start_to_index
|
|
.get(&(PAGE_SIZE * 2))
|
|
.unwrap()
|
|
.ptr_eq(®ion3)
|
|
);
|
|
let start_to_hole = layout.start_to_hole();
|
|
assert!(start_to_hole.len() == 1);
|
|
assert!(start_to_hole.get(&PAGE_SIZE) == Some(&PAGE_SIZE));
|
|
}
|
|
|
|
let region2 = db.create_region_if_needed("region2")?;
|
|
let region2_i = region2.index();
|
|
assert!(region2_i == 1);
|
|
|
|
region2.remove()?;
|
|
db.compact()?;
|
|
|
|
{
|
|
let regions = db.regions();
|
|
let index_to_region = regions.index_to_region();
|
|
assert!(index_to_region.len() == 3);
|
|
|
|
let region1_meta = region1.meta();
|
|
assert!(region1_meta.start() == 0);
|
|
assert!(region1_meta.len() == 0);
|
|
assert!(region1_meta.reserved() == PAGE_SIZE);
|
|
assert!(
|
|
index_to_region
|
|
.get(region2_i)
|
|
.is_some_and(|opt| opt.is_none())
|
|
);
|
|
|
|
let region3_meta = region3.meta();
|
|
assert!(region3_meta.start() == PAGE_SIZE * 2);
|
|
assert!(region3_meta.len() == 0);
|
|
assert!(region3_meta.reserved() == PAGE_SIZE);
|
|
|
|
let id_to_index = regions.id_to_index();
|
|
assert!(id_to_index.len() == 2);
|
|
assert!(id_to_index.get("region1") == Some(&0));
|
|
assert!(id_to_index.get("region2").is_none());
|
|
assert!(id_to_index.get("region3") == Some(&2));
|
|
|
|
let layout = db.layout();
|
|
let start_to_index = layout.start_to_region();
|
|
assert!(start_to_index.len() == 2);
|
|
assert!(start_to_index.get(&0).unwrap().ptr_eq(®ion1));
|
|
assert!(
|
|
start_to_index
|
|
.get(&(PAGE_SIZE * 2))
|
|
.unwrap()
|
|
.ptr_eq(®ion3)
|
|
);
|
|
|
|
let start_to_hole = layout.start_to_hole();
|
|
assert!(start_to_hole.len() == 1);
|
|
assert!(start_to_hole.get(&PAGE_SIZE) == Some(&PAGE_SIZE));
|
|
}
|
|
|
|
region1.write_at(&[1; 8000], 0)?;
|
|
|
|
{
|
|
let regions = db.regions();
|
|
let index_to_region = regions.index_to_region();
|
|
assert!(index_to_region.len() == 3);
|
|
|
|
let region1_meta = region1.meta();
|
|
assert!(region1_meta.start() == 0);
|
|
assert!(region1_meta.len() == 8000);
|
|
assert!(region1_meta.reserved() == 2 * PAGE_SIZE);
|
|
assert!(
|
|
index_to_region
|
|
.get(region2_i)
|
|
.is_some_and(|opt| opt.is_none())
|
|
);
|
|
|
|
let region3_meta = region3.meta();
|
|
assert!(region3_meta.start() == PAGE_SIZE * 2);
|
|
assert!(region3_meta.len() == 0);
|
|
assert!(region3_meta.reserved() == PAGE_SIZE);
|
|
let id_to_index = regions.id_to_index();
|
|
assert!(id_to_index.len() == 2);
|
|
assert!(id_to_index.get("region1") == Some(&0));
|
|
assert!(id_to_index.get("region2").is_none());
|
|
assert!(id_to_index.get("region3") == Some(&2));
|
|
|
|
let layout = db.layout();
|
|
let start_to_index = layout.start_to_region();
|
|
assert!(start_to_index.len() == 2);
|
|
assert!(start_to_index.get(&0).unwrap().ptr_eq(®ion1));
|
|
assert!(
|
|
start_to_index
|
|
.get(&(PAGE_SIZE * 2))
|
|
.unwrap()
|
|
.ptr_eq(®ion3)
|
|
);
|
|
let start_to_hole = layout.start_to_hole();
|
|
assert!(start_to_hole.is_empty());
|
|
}
|
|
|
|
let region2 = db.create_region_if_needed("region2")?;
|
|
|
|
{
|
|
let regions = db.regions();
|
|
let index_to_region = regions.index_to_region();
|
|
assert!(index_to_region.len() == 3);
|
|
|
|
let region1_meta = region1.meta();
|
|
assert!(region1_meta.start() == 0);
|
|
assert!(region1_meta.len() == 8000);
|
|
assert!(region1_meta.reserved() == 2 * PAGE_SIZE);
|
|
|
|
let region2_meta = region2.meta();
|
|
assert!(region2_meta.start() == PAGE_SIZE * 3);
|
|
assert!(region2_meta.len() == 0);
|
|
assert!(region2_meta.reserved() == PAGE_SIZE);
|
|
|
|
let region3_meta = region3.meta();
|
|
assert!(region3_meta.start() == PAGE_SIZE * 2);
|
|
assert!(region3_meta.len() == 0);
|
|
assert!(region3_meta.reserved() == PAGE_SIZE);
|
|
let id_to_index = regions.id_to_index();
|
|
assert!(id_to_index.len() == 3);
|
|
assert!(id_to_index.get("region1") == Some(&0));
|
|
assert!(id_to_index.get("region2") == Some(&1));
|
|
assert!(id_to_index.get("region3") == Some(&2));
|
|
|
|
let layout = db.layout();
|
|
let start_to_index = layout.start_to_region();
|
|
assert!(start_to_index.len() == 3);
|
|
assert!(start_to_index.get(&0).unwrap().ptr_eq(®ion1));
|
|
assert!(
|
|
start_to_index
|
|
.get(&(PAGE_SIZE * 2))
|
|
.unwrap()
|
|
.ptr_eq(®ion3)
|
|
);
|
|
assert!(
|
|
start_to_index
|
|
.get(&(PAGE_SIZE * 3))
|
|
.unwrap()
|
|
.ptr_eq(®ion2)
|
|
);
|
|
let start_to_hole = layout.start_to_hole();
|
|
assert!(start_to_hole.is_empty());
|
|
}
|
|
|
|
region3.remove()?;
|
|
db.compact()?;
|
|
|
|
{
|
|
let regions = db.regions();
|
|
let index_to_region = regions.index_to_region();
|
|
assert!(index_to_region.len() == 3);
|
|
|
|
let region1_meta = region1.meta();
|
|
assert!(region1_meta.start() == 0);
|
|
assert!(region1_meta.len() == 8000);
|
|
assert!(region1_meta.reserved() == 2 * PAGE_SIZE);
|
|
|
|
let region2_meta = region2.meta();
|
|
assert!(region2_meta.start() == PAGE_SIZE * 3);
|
|
assert!(region2_meta.len() == 0);
|
|
assert!(region2_meta.reserved() == PAGE_SIZE);
|
|
|
|
let id_to_index = regions.id_to_index();
|
|
assert!(id_to_index.len() == 2);
|
|
assert!(id_to_index.get("region1") == Some(&0));
|
|
assert!(id_to_index.get("region2") == Some(&1));
|
|
assert!(id_to_index.get("region3").is_none());
|
|
|
|
let layout = db.layout();
|
|
let start_to_index = layout.start_to_region();
|
|
assert!(start_to_index.len() == 2);
|
|
assert!(start_to_index.get(&0).unwrap().ptr_eq(®ion1));
|
|
assert!(
|
|
start_to_index
|
|
.get(&(PAGE_SIZE * 3))
|
|
.unwrap()
|
|
.ptr_eq(®ion2)
|
|
);
|
|
let start_to_hole = layout.start_to_hole();
|
|
assert!(start_to_hole.get(&(PAGE_SIZE * 2)) == Some(&PAGE_SIZE));
|
|
}
|
|
|
|
region1.write(&[1; 8000])?;
|
|
db.compact()?;
|
|
|
|
{
|
|
let regions = db.regions();
|
|
let index_to_region = regions.index_to_region();
|
|
assert!(index_to_region.len() == 3);
|
|
|
|
let region1_meta = region1.meta();
|
|
assert!(region1_meta.start() == PAGE_SIZE * 4);
|
|
assert!(region1_meta.len() == 16_000);
|
|
assert!(region1_meta.reserved() == 4 * PAGE_SIZE);
|
|
|
|
let region2_meta = region2.meta();
|
|
assert!(region2_meta.start() == PAGE_SIZE * 3);
|
|
assert!(region2_meta.len() == 0);
|
|
assert!(region2_meta.reserved() == PAGE_SIZE);
|
|
|
|
let id_to_index = regions.id_to_index();
|
|
assert!(id_to_index.len() == 2);
|
|
assert!(id_to_index.get("region1") == Some(&0));
|
|
assert!(id_to_index.get("region2") == Some(&1));
|
|
assert!(id_to_index.get("region3").is_none());
|
|
|
|
let layout = db.layout();
|
|
let start_to_index = layout.start_to_region();
|
|
assert!(start_to_index.len() == 2);
|
|
assert!(
|
|
start_to_index
|
|
.get(&(PAGE_SIZE * 4))
|
|
.unwrap()
|
|
.ptr_eq(®ion1)
|
|
);
|
|
assert!(
|
|
start_to_index
|
|
.get(&(PAGE_SIZE * 3))
|
|
.unwrap()
|
|
.ptr_eq(®ion2)
|
|
);
|
|
let start_to_hole = layout.start_to_hole();
|
|
assert!(start_to_hole.get(&0) == Some(&(PAGE_SIZE * 3)));
|
|
}
|
|
|
|
region2.write(&[1; 6000])?;
|
|
|
|
let region4 = db.create_region_if_needed("region4")?;
|
|
region2.remove()?;
|
|
region4.remove()?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// ============================================================================
|
|
// Region Rename Tests
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
fn test_basic_region_rename() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region = db.create_region_if_needed("old_name")?;
|
|
region.write(b"Test data")?;
|
|
|
|
// Verify old name exists
|
|
{
|
|
let regions = db.regions();
|
|
assert!(regions.get_from_id("old_name").is_some());
|
|
assert!(regions.get_from_id("new_name").is_none());
|
|
}
|
|
|
|
// Rename the region
|
|
region.rename("new_name")?;
|
|
|
|
// Verify new name exists and old name doesn't
|
|
{
|
|
let regions = db.regions();
|
|
assert!(regions.get_from_id("old_name").is_none());
|
|
assert!(regions.get_from_id("new_name").is_some());
|
|
}
|
|
|
|
// Verify data is still intact
|
|
let reader = region.create_reader();
|
|
assert_eq!(reader.read_all(), b"Test data");
|
|
drop(reader);
|
|
|
|
// Verify metadata was updated
|
|
let meta = region.meta();
|
|
assert_eq!(meta.id(), "new_name");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_rename_with_persistence() -> Result<()> {
|
|
let temp = TempDir::new()?;
|
|
let path = temp.path();
|
|
|
|
// Create and rename region
|
|
{
|
|
let db = Database::open(path)?;
|
|
let region = db.create_region_if_needed("original")?;
|
|
region.write(b"Persistent data")?;
|
|
region.rename("renamed")?;
|
|
db.flush()?;
|
|
}
|
|
|
|
// Reopen and verify rename persisted
|
|
{
|
|
let db = Database::open(path)?;
|
|
let regions = db.regions();
|
|
|
|
assert!(regions.get_from_id("original").is_none());
|
|
let renamed = regions.get_from_id("renamed");
|
|
assert!(renamed.is_some());
|
|
|
|
let reader = renamed.unwrap().create_reader();
|
|
assert_eq!(reader.read_all(), b"Persistent data");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_rename_to_existing_name_fails() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region1 = db.create_region_if_needed("region1")?;
|
|
let _region2 = db.create_region_if_needed("region2")?;
|
|
|
|
// Trying to rename region1 to region2 should fail
|
|
let result = region1.rename("region2");
|
|
assert!(result.is_err());
|
|
|
|
// Verify region1 still has its original name
|
|
let regions = db.regions();
|
|
assert!(regions.get_from_id("region1").is_some());
|
|
assert!(regions.get_from_id("region2").is_some());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_rename_after_remove_and_recreate() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
// Create a region, write some data, then remove it
|
|
let region1 = db.create_region_if_needed("temp")?;
|
|
region1.write(b"Old data")?;
|
|
region1.remove()?;
|
|
|
|
// Create a new region with the same name
|
|
let region2 = db.create_region_if_needed("temp")?;
|
|
region2.write(b"New data")?;
|
|
|
|
// Rename the new region
|
|
region2.rename("renamed")?;
|
|
|
|
// Verify the rename worked
|
|
let regions = db.regions();
|
|
assert!(regions.get_from_id("temp").is_none());
|
|
assert!(regions.get_from_id("renamed").is_some());
|
|
|
|
// Verify it has the new data (not old)
|
|
let reader = region2.create_reader();
|
|
assert_eq!(reader.read_all(), b"New data");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_multiple_renames() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region = db.create_region_if_needed("name1")?;
|
|
region.write(b"Data")?;
|
|
|
|
// Rename multiple times
|
|
region.rename("name2")?;
|
|
region.rename("name3")?;
|
|
region.rename("name4")?;
|
|
|
|
// Verify final name
|
|
let regions = db.regions();
|
|
assert!(regions.get_from_id("name1").is_none());
|
|
assert!(regions.get_from_id("name2").is_none());
|
|
assert!(regions.get_from_id("name3").is_none());
|
|
assert!(regions.get_from_id("name4").is_some());
|
|
|
|
// Verify data is still intact
|
|
let reader = region.create_reader();
|
|
assert_eq!(reader.read_all(), b"Data");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_rename_preserves_region_metadata() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region = db.create_region_if_needed("original")?;
|
|
|
|
// Write large data to trigger expansion
|
|
let large_data = vec![42u8; PAGE_SIZE * 2];
|
|
region.write(&large_data)?;
|
|
|
|
// Capture metadata before rename
|
|
let (start_before, len_before, reserved_before, index_before) = {
|
|
let meta = region.meta();
|
|
(meta.start(), meta.len(), meta.reserved(), region.index())
|
|
};
|
|
|
|
// Rename
|
|
region.rename("renamed")?;
|
|
|
|
// Verify metadata preserved (except id)
|
|
{
|
|
let meta = region.meta();
|
|
assert_eq!(meta.id(), "renamed");
|
|
assert_eq!(meta.start(), start_before);
|
|
assert_eq!(meta.len(), len_before);
|
|
assert_eq!(meta.reserved(), reserved_before);
|
|
assert_eq!(region.index(), index_before);
|
|
}
|
|
|
|
// Verify data is still intact
|
|
let reader = region.create_reader();
|
|
assert_eq!(reader.read_all(), &large_data[..]);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_rename_with_special_characters() -> Result<()> {
|
|
let (db, _temp) = setup_test_db()?;
|
|
|
|
let region = db.create_region_if_needed("simple")?;
|
|
|
|
// Rename with special characters (but not control characters)
|
|
region.rename("name-with-dashes")?;
|
|
assert!(db.regions().get_from_id("name-with-dashes").is_some());
|
|
|
|
region.rename("name_with_underscores")?;
|
|
assert!(db.regions().get_from_id("name_with_underscores").is_some());
|
|
|
|
region.rename("name.with.dots")?;
|
|
assert!(db.regions().get_from_id("name.with.dots").is_some());
|
|
|
|
region.rename("name:with:colons")?;
|
|
assert!(db.regions().get_from_id("name:with:colons").is_some());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_concurrent_renames() -> Result<()> {
|
|
let temp = TempDir::new()?;
|
|
let db = Arc::new(Database::open(temp.path())?);
|
|
|
|
// Create regions upfront
|
|
let regions: Vec<_> = (0..10)
|
|
.map(|i| db.create_region_if_needed(&format!("region_{}", i)))
|
|
.collect::<Result<Vec<_>>>()?;
|
|
|
|
// Rename different regions concurrently
|
|
let handles: Vec<_> = regions
|
|
.into_iter()
|
|
.enumerate()
|
|
.map(|(i, region)| thread::spawn(move || region.rename(&format!("renamed_{}", i))))
|
|
.collect();
|
|
|
|
// Wait for all renames
|
|
for handle in handles {
|
|
handle.join().unwrap()?;
|
|
}
|
|
|
|
// Verify all renames succeeded
|
|
let regions_lock = db.regions();
|
|
for i in 0..10 {
|
|
assert!(regions_lock.get_from_id(&format!("region_{}", i)).is_none());
|
|
assert!(
|
|
regions_lock
|
|
.get_from_id(&format!("renamed_{}", i))
|
|
.is_some()
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|