use geographdb_core::storage::sectioned::{SectionedStorage, FILE_MAGIC, HEADER_SIZE};
use std::io::{Read, Seek, SeekFrom, Write};
use tempfile::NamedTempFile;
#[test]
fn test_create_creates_valid_header() {
let temp = NamedTempFile::new().unwrap();
let path = temp.path();
let storage = SectionedStorage::create(path).unwrap();
let mut buf = [0u8; 128];
let mut file = std::fs::File::open(path).unwrap();
file.read_exact(&mut buf).unwrap();
assert_eq!(&buf[0..8], &FILE_MAGIC[..]);
assert_eq!(u32::from_le_bytes(buf[8..12].try_into().unwrap()), 1);
assert_eq!(storage.section_count(), 0);
}
#[test]
fn test_create_open_roundtrip() {
let temp = NamedTempFile::new().unwrap();
let path = temp.path();
{
let mut storage = SectionedStorage::create(path).unwrap();
storage.create_section("data", 1000, 0).unwrap();
storage.write_section("data", b"hello").unwrap();
storage.flush().unwrap();
}
{
let mut storage = SectionedStorage::open(path).unwrap();
assert_eq!(storage.section_count(), 1);
let data = storage.read_section("data").unwrap();
assert_eq!(data, b"hello");
}
}
#[test]
fn test_write_read_exact_bytes_no_padding() {
let temp = NamedTempFile::new().unwrap();
let path = temp.path();
let mut storage = SectionedStorage::create(path).unwrap();
storage.create_section("test", 1000, 0).unwrap();
storage.write_section("test", b"exact").unwrap();
let read = storage.read_section("test").unwrap();
assert_eq!(read.len(), 5); assert_eq!(read, b"exact");
}
#[test]
fn test_write_exceeding_capacity_fails() {
let temp = NamedTempFile::new().unwrap();
let path = temp.path();
let mut storage = SectionedStorage::create(path).unwrap();
storage.create_section("small", 10, 0).unwrap();
let result = storage.write_section("small", &[0u8; 100]);
assert!(result.is_err());
}
#[test]
fn test_duplicate_section_rejected() {
let temp = NamedTempFile::new().unwrap();
let path = temp.path();
let mut storage = SectionedStorage::create(path).unwrap();
storage.create_section("dup", 100, 0).unwrap();
let result = storage.create_section("dup", 200, 0);
assert!(result.is_err());
}
#[test]
fn test_validate_required_sections() {
let temp = NamedTempFile::new().unwrap();
let path = temp.path();
let mut storage = SectionedStorage::create(path).unwrap();
storage.create_section("required1", 100, 0).unwrap();
storage.create_section("required2", 100, 0).unwrap();
storage.flush().unwrap();
storage
.validate_required_sections(&["required1", "required2"])
.unwrap();
let result = storage.validate_required_sections(&["required1", "missing"]);
assert!(result.is_err());
}
#[test]
fn test_open_fails_on_invalid_magic() {
let temp = NamedTempFile::new().unwrap();
let mut file = std::fs::File::create(temp.path()).unwrap();
let mut fake_header = [0u8; 128];
fake_header[0..8].copy_from_slice(b"BADMAGIC"); fake_header[8..12].copy_from_slice(&1u32.to_le_bytes()); fake_header[16..24].copy_from_slice(&128u64.to_le_bytes()); fake_header[32..40].copy_from_slice(&128u64.to_le_bytes()); file.write_all(&fake_header).unwrap();
let result = SectionedStorage::open(temp.path());
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("magic") || err.contains("MAGIC"),
"Error should mention invalid magic, got: {}",
err
);
}
#[test]
fn test_checksum_mismatch_detected() {
let temp = NamedTempFile::new().unwrap();
let path = temp.path();
{
let mut storage = SectionedStorage::create(path).unwrap();
storage.create_section("data", 100, 0).unwrap();
storage.write_section("data", b"original").unwrap();
storage.flush().unwrap();
}
{
let mut file = std::fs::OpenOptions::new().write(true).open(path).unwrap();
file.seek(SeekFrom::Start(HEADER_SIZE)).unwrap();
file.write_all(b"corrupted!").unwrap();
}
let result = SectionedStorage::open(temp.path());
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("Checksum mismatch") || err.contains("checksum"),
"Error should mention checksum, got: {}",
err
);
}
#[test]
fn test_create_section_physically_reserves_space() {
let temp = NamedTempFile::new().unwrap();
let mut storage = SectionedStorage::create(temp.path()).unwrap();
let initial_len = temp.path().metadata().unwrap().len();
assert_eq!(initial_len, 128);
storage.create_section("test", 1000, 0).unwrap();
let after_len = temp.path().metadata().unwrap().len();
assert_eq!(after_len, 1128);
assert!(storage.header().next_data_offset <= after_len);
}
#[test]
fn test_flush_table_after_all_data() {
let temp = NamedTempFile::new().unwrap();
let mut storage = SectionedStorage::create(temp.path()).unwrap();
storage.create_section("s1", 200, 0).unwrap();
storage.create_section("s2", 300, 0).unwrap();
let before_flush_len = temp.path().metadata().unwrap().len();
assert_eq!(before_flush_len, 628);
storage.flush().unwrap();
let after_flush_len = temp.path().metadata().unwrap().len();
assert_eq!(after_flush_len, 756);
assert_eq!(storage.header().section_table_offset, 628);
}
#[test]
fn test_validate_detects_missing_physical_reservation() {
let temp = NamedTempFile::new().unwrap();
{
let mut storage = SectionedStorage::create(temp.path()).unwrap();
storage.create_section("data", 1000, 0).unwrap();
storage.write_section("data", b"test data").unwrap();
storage.flush().unwrap();
}
{
let file = std::fs::OpenOptions::new()
.write(true)
.open(temp.path())
.unwrap();
file.set_len(200).unwrap(); }
let result = SectionedStorage::open(temp.path());
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("Failed to read section entry")
|| err.contains("truncated")
|| err.contains("next_data_offset"),
"Error should mention read failure or truncation, got: {}",
err
);
}
#[test]
fn test_create_section_overflow_detected() {
let temp = NamedTempFile::new().unwrap();
let mut storage = SectionedStorage::create(temp.path()).unwrap();
let huge_capacity = u64::MAX - 100;
let result = storage.create_section("huge", huge_capacity, 0);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("overflow") || err.contains("reserve"),
"Error should mention overflow or reserve failure, got: {}",
err
);
}
#[test]
fn test_create_section_after_flush_does_not_overlap_live_table() {
let temp = NamedTempFile::new().unwrap();
let mut storage = SectionedStorage::create(temp.path()).unwrap();
storage.create_section("s1", 200, 0).unwrap();
storage.write_section("s1", b"data1").unwrap();
let s1_offset = storage.get_section("s1").unwrap().offset;
assert_eq!(s1_offset, 128);
storage.flush().unwrap();
let table_offset = storage.header().section_table_offset;
let file_len_after_flush = temp.path().metadata().unwrap().len();
assert_eq!(table_offset, 128 + 200);
assert_eq!(file_len_after_flush, table_offset + 64);
storage.create_section("s2", 150, 0).unwrap();
let s2 = storage.get_section("s2").unwrap();
assert!(
s2.offset >= file_len_after_flush,
"s2 offset {} must be >= old file len {} to avoid table overlap",
s2.offset,
file_len_after_flush
);
let file_len_after_create = temp.path().metadata().unwrap().len();
assert!(
file_len_after_create >= file_len_after_flush,
"File should not shrink during create_section"
);
assert_eq!(file_len_after_create, file_len_after_flush + 150);
storage.flush().unwrap();
{
let mut storage2 = SectionedStorage::open(temp.path()).unwrap();
assert_eq!(storage2.section_count(), 2);
let s1_data = storage2.read_section("s1").unwrap();
assert_eq!(s1_data, b"data1");
let s2_data = storage2.read_section("s2").unwrap();
assert_eq!(s2_data.len(), 0);
let new_table_offset = storage2.header().section_table_offset;
assert!(
new_table_offset > table_offset,
"New table should be after old table"
);
}
storage.write_section("s2", b"data2").unwrap();
storage.flush().unwrap();
{
let mut storage3 = SectionedStorage::open(temp.path()).unwrap();
let s2_data = storage3.read_section("s2").unwrap();
assert_eq!(s2_data, b"data2");
}
}
#[test]
fn test_multiple_flush_cycles_preserve_data() {
let temp = NamedTempFile::new().unwrap();
{
let mut storage = SectionedStorage::create(temp.path()).unwrap();
storage.create_section("s1", 100, 0).unwrap();
storage.write_section("s1", b"cycle1").unwrap();
storage.flush().unwrap();
}
let len_after_cycle1 = temp.path().metadata().unwrap().len();
{
let mut storage = SectionedStorage::open(temp.path()).unwrap();
storage.create_section("s2", 100, 0).unwrap();
storage.write_section("s2", b"cycle2").unwrap();
storage.flush().unwrap();
}
let len_after_cycle2 = temp.path().metadata().unwrap().len();
assert!(len_after_cycle2 > len_after_cycle1);
{
let mut storage = SectionedStorage::open(temp.path()).unwrap();
storage.create_section("s3", 100, 0).unwrap();
storage.write_section("s3", b"cycle3").unwrap();
storage.flush().unwrap();
}
let len_after_cycle3 = temp.path().metadata().unwrap().len();
assert!(len_after_cycle3 > len_after_cycle2);
{
let mut storage = SectionedStorage::open(temp.path()).unwrap();
assert_eq!(storage.section_count(), 3);
assert_eq!(storage.read_section("s1").unwrap(), b"cycle1");
assert_eq!(storage.read_section("s2").unwrap(), b"cycle2");
assert_eq!(storage.read_section("s3").unwrap(), b"cycle3");
}
}
#[test]
fn test_allocation_base_uses_max_of_next_offset_and_file_len() {
let temp = NamedTempFile::new().unwrap();
let mut storage = SectionedStorage::create(temp.path()).unwrap();
storage.create_section("s1", 100, 0).unwrap();
storage.flush().unwrap();
let file_len = temp.path().metadata().unwrap().len();
let next_offset = storage.header().next_data_offset;
assert!(file_len > next_offset);
storage.create_section("s2", 50, 0).unwrap();
let s2 = storage.get_section("s2").unwrap();
assert_eq!(
s2.offset, file_len,
"Section should be allocated at EOF, not at next_data_offset"
);
}
#[test]
fn test_empty_section_name_rejected() {
let temp = NamedTempFile::new().unwrap();
let mut storage = SectionedStorage::create(temp.path()).unwrap();
let result = storage.create_section("", 100, 0);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("empty"));
}
#[test]
fn test_section_name_32_bytes_accepted() {
let name_32_bytes = "12345678901234567890123456789012";
assert_eq!(name_32_bytes.len(), 32);
let temp = NamedTempFile::new().unwrap();
let mut storage = SectionedStorage::create(temp.path()).unwrap();
storage.create_section(name_32_bytes, 100, 0).unwrap();
assert_eq!(storage.section_count(), 1);
assert!(storage.get_section(name_32_bytes).is_some());
}
#[test]
fn test_section_name_33_bytes_rejected() {
let name_33_bytes = "123456789012345678901234567890123";
assert_eq!(name_33_bytes.len(), 33);
let temp = NamedTempFile::new().unwrap();
let mut storage = SectionedStorage::create(temp.path()).unwrap();
let result = storage.create_section(name_33_bytes, 100, 0);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("too long"));
}
#[test]
fn test_section_name_multibyte_utf8() {
let name_with_emoji = "a🔥"; assert_eq!(name_with_emoji.len(), 5);
assert_eq!(name_with_emoji.chars().count(), 2);
let temp = NamedTempFile::new().unwrap();
let mut storage = SectionedStorage::create(temp.path()).unwrap();
storage.create_section(name_with_emoji, 100, 0).unwrap();
}
#[test]
fn test_validate_dirty_state_rejected() {
let temp = NamedTempFile::new().unwrap();
let mut storage = SectionedStorage::create(temp.path()).unwrap();
storage.create_section("data", 100, 0).unwrap();
let result = storage.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("dirty"));
}
#[test]
fn test_validate_after_flush_succeeds() {
let temp = NamedTempFile::new().unwrap();
let mut storage = SectionedStorage::create(temp.path()).unwrap();
storage.create_section("data", 100, 0).unwrap();
storage.write_section("data", b"test").unwrap();
storage.flush().unwrap();
storage.validate().unwrap();
}
#[test]
fn test_empty_section_can_be_read() {
let temp = NamedTempFile::new().unwrap();
let mut storage = SectionedStorage::create(temp.path()).unwrap();
storage.create_section("empty", 100, 0).unwrap();
let data = storage.read_section("empty").unwrap();
assert_eq!(data.len(), 0);
}
#[test]
fn test_get_section() {
let temp = NamedTempFile::new().unwrap();
let mut storage = SectionedStorage::create(temp.path()).unwrap();
storage.create_section("test", 100, 0).unwrap();
let section = storage.get_section("test");
assert!(section.is_some());
assert_eq!(section.unwrap().capacity, 100);
assert!(storage.get_section("nonexistent").is_none());
}
#[test]
fn test_list_sections() {
let temp = NamedTempFile::new().unwrap();
let mut storage = SectionedStorage::create(temp.path()).unwrap();
storage.create_section("s1", 100, 0).unwrap();
storage.create_section("s2", 200, 0).unwrap();
storage.create_section("s3", 300, 0).unwrap();
let sections = storage.list_sections();
assert_eq!(sections.len(), 3);
assert_eq!(sections[0].name, "s1");
assert_eq!(sections[1].name, "s2");
assert_eq!(sections[2].name, "s3");
}
#[test]
fn test_path_method() {
let temp = NamedTempFile::new().unwrap();
let storage = SectionedStorage::create(temp.path()).unwrap();
assert_eq!(storage.path(), temp.path());
}
#[test]
fn test_header_access() {
let temp = NamedTempFile::new().unwrap();
let storage = SectionedStorage::create(temp.path()).unwrap();
let header = storage.header();
assert_eq!(header.version, 1);
assert_eq!(&header.magic[..], &FILE_MAGIC[..]);
}