use super::*;
use crate::backend::native::v3::{
allocator::PageAllocator, btree::BTreeManager, header::PersistentHeaderV3,
};
use parking_lot::RwLock;
use std::path::PathBuf;
use std::sync::Arc;
use tempfile::TempDir;
#[test]
fn test_page_type_from_u8() {
assert_eq!(PageType::from_u8(0), Some(PageType::Free));
assert_eq!(PageType::from_u8(1), Some(PageType::BTreeIndex));
assert_eq!(PageType::from_u8(2), Some(PageType::NodeData));
assert_eq!(PageType::from_u8(3), Some(PageType::EdgeCluster));
assert_eq!(PageType::from_u8(255), None);
}
#[test]
fn test_direction_conversion() {
assert_eq!(Direction::Outgoing.to_v2(), V2Direction::Outgoing);
assert_eq!(Direction::Incoming.to_v2(), V2Direction::Incoming);
}
#[test]
fn test_v3_edge_cluster_new() {
let cluster = V3EdgeCluster::new(42, Direction::Outgoing, 100);
assert_eq!(cluster.src, 42);
assert!(cluster.edges.is_empty());
assert_eq!(cluster.direction, Direction::Outgoing);
assert_eq!(cluster.page_id, 100);
assert_eq!(cluster.format_version, 3);
}
#[test]
fn test_v3_edge_cluster_add_edge() {
let mut cluster = V3EdgeCluster::new(1, Direction::Outgoing, 1);
cluster.add_edge(2, None);
cluster.add_edge(3, None);
assert_eq!(cluster.dsts(), vec![2, 3]);
}
#[test]
fn test_v3_edge_cluster_serialize_roundtrip() {
let mut cluster = V3EdgeCluster::new(42, Direction::Outgoing, 100);
cluster.add_edge(100, None);
cluster.add_edge(200, None);
let bytes = cluster.serialize().unwrap();
let deserialized = V3EdgeCluster::deserialize(&bytes, 100).unwrap();
assert_eq!(deserialized.format_version, 3);
assert_eq!(deserialized.src, 42, "src should survive roundtrip");
assert_eq!(
deserialized.direction,
Direction::Outgoing,
"direction should survive roundtrip"
);
assert_eq!(deserialized.dsts(), vec![100, 200]);
assert_eq!(deserialized.page_id, 100);
}
#[test]
fn test_v3_edge_cluster_roundtrip_incoming() {
let mut cluster = V3EdgeCluster::new(99, Direction::Incoming, 50);
cluster.add_edge(10, None);
let bytes = cluster.serialize().unwrap();
let deserialized = V3EdgeCluster::deserialize(&bytes, 50).unwrap();
assert_eq!(deserialized.src, 99);
assert_eq!(
deserialized.direction,
Direction::Incoming,
"Incoming direction must survive serialization roundtrip"
);
}
fn create_test_edge_store(db_path: Option<PathBuf>) -> (V3EdgeStore, Arc<RwLock<PageAllocator>>) {
let header = PersistentHeaderV3::new_v3();
let allocator = Arc::new(RwLock::new(PageAllocator::new(&header)));
let btree = if let Some(ref path) = db_path {
BTreeManager::new(allocator.clone(), None, path.clone())
} else {
BTreeManager::new(allocator.clone(), None, None::<PathBuf>)
};
let edge_store = if let Some(ref path) = db_path {
let wal_path = path.with_extension("v3wal");
let writer = WALWriter::new(wal_path, 1).expect("Failed to create WAL writer");
writer.write_header().expect("Failed to write WAL header");
V3EdgeStore::with_path_and_allocator(
btree,
Some(writer),
path.clone(),
allocator.clone(),
header.page_size,
)
} else {
V3EdgeStore::new(btree, None, allocator.clone(), header.page_size)
};
let _ = edge_store.restore_btree_from_metadata();
(edge_store, allocator)
}
#[test]
fn test_edge_insert_creates_wal_record() {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.graph");
let wal_path = db_path.with_extension("v3wal");
let (edge_store, _allocator) = create_test_edge_store(Some(db_path.clone()));
edge_store
.insert_edge(1, 2, Direction::Outgoing, None)
.expect("Insert failed");
edge_store.flush_wal().expect("WAL flush failed");
assert!(
wal_path.exists(),
"NOTE: WAL file should exist after edge insert with WAL enabled"
);
let wal_content = std::fs::read(&wal_path).expect("Failed to read WAL file");
assert!(
wal_content.len() > 64, "NOTE: WAL should contain edge insert record beyond header"
);
}
#[test]
fn test_flush_writes_dirty_clusters_to_pages() {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.graph");
std::fs::write(&db_path, vec![0u8; 4096]).expect("Failed to create db file");
let (edge_store, _allocator) = create_test_edge_store(Some(db_path.clone()));
edge_store
.insert_edge(1, 2, Direction::Outgoing, None)
.expect("Insert 1->2 failed");
edge_store
.insert_edge(1, 3, Direction::Outgoing, None)
.expect("Insert 1->3 failed");
edge_store
.insert_edge(2, 4, Direction::Outgoing, None)
.expect("Insert 2->4 failed");
let result = edge_store.flush(None);
assert!(result.is_ok(), "Flush should succeed");
let file_size = std::fs::metadata(&db_path)
.expect("Failed to read file metadata")
.len();
assert!(
file_size > 4096,
"NOTE: Database file should grow after flush writes dirty clusters"
);
}
#[test]
fn test_flush_updates_btree_index() {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.graph");
std::fs::write(&db_path, vec![0u8; 4096]).expect("Failed to create db file");
let (edge_store, _allocator) = create_test_edge_store(Some(db_path.clone()));
edge_store
.insert_edge(1, 2, Direction::Outgoing, None)
.expect("Insert failed");
edge_store
.insert_edge(1, 3, Direction::Outgoing, None)
.expect("Insert failed");
edge_store.flush(None).expect("Flush failed");
let btree = edge_store.btree.read();
let lookup_key = edge_key(1, Direction::Outgoing);
let lookup_result = btree.lookup(lookup_key);
assert!(lookup_result.is_ok(), "B+Tree lookup should succeed");
assert!(
lookup_result.unwrap().is_some(),
"B+Tree should contain edge page mapping for node 1 after flush"
);
}
#[test]
fn test_wal_checkpoint_after_flush() {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.graph");
let wal_path = db_path.with_extension("v3wal");
std::fs::write(&db_path, vec![0u8; 4096]).expect("Failed to create db file");
let (edge_store, _allocator) = create_test_edge_store(Some(db_path.clone()));
for i in 0..5 {
edge_store
.insert_edge(1, i as i64 + 10, Direction::Outgoing, None)
.unwrap_or_else(|_| panic!("Insert iteration {} failed", i));
edge_store.flush(None).expect("Flush failed");
}
assert!(
!wal_path.exists(),
"WAL file should be truncated (removed) after flush"
);
}
#[test]
fn test_edge_recovery_after_crash() {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.graph");
let wal_path = db_path.with_extension("v3wal");
std::fs::write(&db_path, vec![0u8; 4096]).expect("Failed to create db file");
{
let (edge_store, _allocator) = create_test_edge_store(Some(db_path.clone()));
edge_store
.insert_edge(1, 2, Direction::Outgoing, None)
.expect("Insert failed");
edge_store
.insert_edge(1, 3, Direction::Outgoing, None)
.expect("Insert failed");
edge_store
.insert_edge(2, 4, Direction::Outgoing, None)
.expect("Insert failed");
edge_store.flush(None).expect("Flush failed");
assert!(
!wal_path.exists(),
"WAL file should be truncated (removed) after flush with checkpoint"
);
}
{
let (recovered_store, _allocator) = create_test_edge_store(Some(db_path.clone()));
let neighbors = recovered_store
.outgoing(1)
.expect("Failed to get neighbors");
assert!(
neighbors.len() >= 2,
"After recovery, node 1 should have at least 2 outgoing neighbors"
);
let neighbor_vec: Vec<i64> = neighbors.iter().copied().collect();
assert!(
neighbor_vec.contains(&2),
"Node 1 should have edge to node 2"
);
assert!(
neighbor_vec.contains(&3),
"Node 1 should have edge to node 3"
);
}
}
#[test]
fn test_data_persists_after_multiple_wal_truncations() {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.graph");
let wal_path = db_path.with_extension("v3wal");
std::fs::write(&db_path, vec![0u8; 4096]).expect("Failed to create db file");
{
let (edge_store, _allocator) = create_test_edge_store(Some(db_path.clone()));
for i in 0..5 {
edge_store
.insert_edge(1, i + 10, Direction::Outgoing, None)
.expect("Insert failed");
}
edge_store.flush(None).expect("Flush failed");
assert!(
!wal_path.exists(),
"WAL should be truncated after first flush"
);
for i in 0..5 {
edge_store
.insert_edge(2, i + 20, Direction::Outgoing, None)
.expect("Insert failed");
}
edge_store.flush(None).expect("Flush failed");
assert!(
!wal_path.exists(),
"WAL should be truncated after second flush"
);
}
let (recovered_store, _allocator) = create_test_edge_store(Some(db_path.clone()));
let neighbors1 = recovered_store
.outgoing(1)
.expect("Failed to get node 1 neighbors");
assert_eq!(
neighbors1.len(),
5,
"Node 1 should have 5 outgoing neighbors"
);
let neighbors2 = recovered_store
.outgoing(2)
.expect("Failed to get node 2 neighbors");
assert_eq!(
neighbors2.len(),
5,
"Node 2 should have 5 outgoing neighbors"
);
}
#[test]
fn test_flush_with_no_dirty_clusters() {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.graph");
let (edge_store, _allocator) = create_test_edge_store(Some(db_path));
let result = edge_store.flush(None);
assert!(result.is_ok(), "Flush with empty cache should succeed");
}
#[test]
fn test_multiple_flushes_idempotent() {
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.graph");
std::fs::write(&db_path, vec![0u8; 4096]).expect("Failed to create db file");
let (edge_store, _allocator) = create_test_edge_store(Some(db_path.clone()));
edge_store
.insert_edge(1, 2, Direction::Outgoing, None)
.expect("Insert failed");
for _ in 0..3 {
edge_store.flush(None).expect("Flush failed");
}
}
#[test]
fn test_wal_edge_insert_record_format() {
use crate::backend::native::v3::wal::{V3_WAL_HEADER_SIZE, V3WALRecord, V3WALRecordType};
use std::fs;
let temp_dir = TempDir::new().unwrap();
let db_path = temp_dir.path().join("test.graph");
let wal_path = db_path.with_extension("v3wal");
let (edge_store, _allocator) = create_test_edge_store(Some(db_path.clone()));
edge_store
.insert_edge(1, 2, Direction::Outgoing, None)
.expect("Insert failed");
edge_store.flush_wal().expect("WAL flush failed");
let wal_content = fs::read(&wal_path).expect("Failed to read WAL");
assert!(
wal_content.len() > V3_WAL_HEADER_SIZE,
"WAL should have records beyond header"
);
let mut pos = V3_WAL_HEADER_SIZE;
let mut found_edge_insert = false;
while pos < wal_content.len() - 8 {
if pos + 4 > wal_content.len() {
break;
}
let size = u32::from_le_bytes([
wal_content[pos],
wal_content[pos + 1],
wal_content[pos + 2],
wal_content[pos + 3],
]) as usize;
pos += 4;
if pos + size > wal_content.len() || size == 0 {
break;
}
let record_bytes = &wal_content[pos..pos + size];
if let Ok(record) = V3WALRecord::from_bytes(record_bytes)
&& record.record_type() == V3WALRecordType::EdgeInsert
{
found_edge_insert = true;
break;
}
pos += size;
}
assert!(
found_edge_insert,
"WAL should contain EdgeInsert record (type 12)"
);
}
#[path = "edge_compat_weighted_tests.rs"]
mod edge_compat_weighted_tests;