289 lines
9.2 KiB
Rust
289 lines
9.2 KiB
Rust
use chusql_core_storage::config::DEFAULT_PAGE_SIZE;
|
|
use chusql_core_storage::page::{Page, PageFile};
|
|
|
|
// 页文件测试:读写、追加、覆写、页大小校验与页缓存。
|
|
|
|
/// 写一页再读回一致
|
|
#[test]
|
|
fn write_then_read_page() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let path = dir.path().join("test.db");
|
|
|
|
let mut pf = PageFile::open(&path, DEFAULT_PAGE_SIZE).unwrap();
|
|
|
|
let mut p = Page::new(0, DEFAULT_PAGE_SIZE);
|
|
p.data[0] = 42;
|
|
p.data[DEFAULT_PAGE_SIZE - 1] = 7;
|
|
pf.write_page(&p).unwrap();
|
|
|
|
let read = pf.read_page(0).unwrap();
|
|
assert_eq!(read.data[0], 42);
|
|
assert_eq!(read.data[DEFAULT_PAGE_SIZE - 1], 7);
|
|
assert_eq!(read.data.len(), DEFAULT_PAGE_SIZE);
|
|
}
|
|
|
|
/// 空文件读第 0 页报错
|
|
#[test]
|
|
fn read_out_of_range_returns_error() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let path = dir.path().join("test.db");
|
|
|
|
let mut pf = PageFile::open(&path, DEFAULT_PAGE_SIZE).unwrap();
|
|
assert!(pf.read_page(0).is_err());
|
|
}
|
|
|
|
/// 追加让页数加一
|
|
#[test]
|
|
fn append_grows_file() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let path = dir.path().join("test.db");
|
|
|
|
let mut pf = PageFile::open(&path, DEFAULT_PAGE_SIZE).unwrap();
|
|
assert_eq!(pf.num_pages().unwrap(), 0);
|
|
|
|
pf.append_page().unwrap();
|
|
assert_eq!(pf.num_pages().unwrap(), 1);
|
|
|
|
pf.append_page().unwrap();
|
|
pf.append_page().unwrap();
|
|
assert_eq!(pf.num_pages().unwrap(), 3);
|
|
}
|
|
|
|
/// 覆写同页读到新内容
|
|
#[test]
|
|
fn overwrite_page() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let path = dir.path().join("test.db");
|
|
|
|
let mut pf = PageFile::open(&path, DEFAULT_PAGE_SIZE).unwrap();
|
|
|
|
let mut a = Page::new(0, DEFAULT_PAGE_SIZE);
|
|
a.data[0] = 1;
|
|
pf.write_page(&a).unwrap();
|
|
|
|
let mut b = Page::new(0, DEFAULT_PAGE_SIZE);
|
|
b.data[0] = 99;
|
|
pf.write_page(&b).unwrap();
|
|
|
|
assert_eq!(pf.read_page(0).unwrap().data[0], 99);
|
|
assert_eq!(pf.num_pages().unwrap(), 1);
|
|
}
|
|
|
|
/// 512 字节页可用
|
|
#[test]
|
|
fn custom_page_size() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let path = dir.path().join("t.db");
|
|
let mut pf = PageFile::open(&path, 512).unwrap();
|
|
|
|
pf.write_page(&Page::new(0, 512)).unwrap();
|
|
pf.flush().unwrap();
|
|
|
|
assert_eq!(std::fs::metadata(&path).unwrap().len(), 512);
|
|
}
|
|
|
|
/// 长度不是整数倍要报错
|
|
#[test]
|
|
fn mismatched_file_length_errors() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let path = dir.path().join("odd.db");
|
|
std::fs::write(&path, vec![0u8; 100]).unwrap();
|
|
|
|
let mut pf = PageFile::open(&path, DEFAULT_PAGE_SIZE).unwrap();
|
|
assert!(pf.num_pages().is_err());
|
|
}
|
|
|
|
/// 页大小不符拒绝写入
|
|
#[test]
|
|
fn write_wrong_sized_page_errors() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let path = dir.path().join("test.db");
|
|
|
|
let mut pf = PageFile::open(&path, 512).unwrap();
|
|
assert!(pf.write_page(&Page::new(0, 1024)).is_err());
|
|
}
|
|
|
|
/// 第二次读命中缓存
|
|
#[test]
|
|
fn cache_hit_avoids_second_read() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let path = dir.path().join("t.db");
|
|
|
|
{
|
|
let mut pf = PageFile::open(&path, 4096).unwrap();
|
|
pf.write_page(&Page::new(0, 4096)).unwrap();
|
|
pf.flush().unwrap();
|
|
}
|
|
|
|
let mut pf = PageFile::open(&path, 4096).unwrap();
|
|
let _ = pf.read_page(0).unwrap();
|
|
let _ = pf.read_page(0).unwrap();
|
|
let _ = pf.read_page(0).unwrap();
|
|
|
|
assert_eq!(pf.hits(), 2);
|
|
assert_eq!(pf.misses(), 1);
|
|
assert!(pf.hit_rate() > 0.6);
|
|
}
|
|
|
|
/// 退出时脏页写回
|
|
#[test]
|
|
fn dirty_page_flushed_on_drop() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let path = dir.path().join("t.db");
|
|
|
|
{
|
|
let mut pf = PageFile::open(&path, DEFAULT_PAGE_SIZE).unwrap();
|
|
let mut p = Page::new(0, DEFAULT_PAGE_SIZE);
|
|
p.data[0] = 42;
|
|
pf.write_page(&p).unwrap();
|
|
}
|
|
|
|
let mut pf2 = PageFile::open(&path, DEFAULT_PAGE_SIZE).unwrap();
|
|
let p = pf2.read_page(0).unwrap();
|
|
assert_eq!(p.data[0], 42);
|
|
}
|
|
|
|
/// 已同步镜像恢复撕裂页且页 LSN 持久保留
|
|
#[test]
|
|
fn full_page_image_repairs_torn_write_before_read() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let path = dir.path().join("torn.db");
|
|
let metadata = dir.path().join("torn.db.pages");
|
|
let journal = dir.path().join("torn.db.page-wal");
|
|
let mut file = PageFile::with_options(&path, 512, 1).unwrap();
|
|
file.set_lsn(7);
|
|
file.write_page(&Page::new(0, 512)).unwrap();
|
|
file.flush().unwrap();
|
|
let old_metadata = std::fs::read(&metadata).unwrap();
|
|
file.set_lsn(13);
|
|
let mut changed = Page::new(0, 512);
|
|
changed.data.fill(0x5a);
|
|
file.write_page(&changed).unwrap();
|
|
file.append_page().unwrap(); // 淘汰页零前镜像已同步
|
|
let image = std::fs::read(&journal).unwrap();
|
|
assert!(!image.is_empty());
|
|
drop(file);
|
|
std::fs::write(&metadata, old_metadata).unwrap();
|
|
std::fs::write(&journal, &image).unwrap();
|
|
std::fs::write(&path, [0x11; 213]).unwrap();
|
|
let mut reopened = PageFile::open(&path, 512).unwrap();
|
|
assert_eq!(reopened.read_page(0).unwrap().data, changed.data);
|
|
assert_eq!(reopened.page_lsn(0), Some(13));
|
|
drop(reopened);
|
|
assert_eq!(std::fs::metadata(&journal).unwrap().len(), 0);
|
|
let mut reopened = PageFile::open(&path, 512).unwrap();
|
|
assert_eq!(reopened.read_page(0).unwrap().data, changed.data);
|
|
assert_eq!(reopened.page_lsn(0), Some(13));
|
|
}
|
|
|
|
/// 撕裂日志尾不发布,完整损坏帧明确拒绝
|
|
#[test]
|
|
fn page_image_tail_and_corruption_are_distinguished() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let path = dir.path().join("tail.db");
|
|
let metadata = dir.path().join("tail.db.pages");
|
|
let journal = dir.path().join("tail.db.page-wal");
|
|
let mut file = PageFile::with_options(&path, 512, 1).unwrap();
|
|
file.write_page(&Page::new(0, 512)).unwrap();
|
|
file.flush().unwrap();
|
|
let old_metadata = std::fs::read(&metadata).unwrap();
|
|
let old_data = std::fs::read(&path).unwrap();
|
|
file.set_lsn(20);
|
|
let mut changed = Page::new(0, 512);
|
|
changed.data.fill(9);
|
|
file.write_page(&changed).unwrap();
|
|
file.append_page().unwrap();
|
|
let image = std::fs::read(&journal).unwrap();
|
|
drop(file);
|
|
for end in [1, 39, image.len() - 1] {
|
|
std::fs::write(&metadata, &old_metadata).unwrap();
|
|
std::fs::write(&path, &old_data).unwrap();
|
|
std::fs::write(&journal, &image[..end]).unwrap();
|
|
let mut reopened = PageFile::open(&path, 512).unwrap();
|
|
assert_eq!(reopened.read_page(0).unwrap().data, vec![0; 512]);
|
|
}
|
|
let mut corrupt = image;
|
|
corrupt[40] ^= 1;
|
|
std::fs::write(&metadata, &old_metadata).unwrap();
|
|
std::fs::write(&journal, &corrupt).unwrap();
|
|
assert!(
|
|
PageFile::open(&path, 512)
|
|
.unwrap_err()
|
|
.to_string()
|
|
.contains("checksum")
|
|
);
|
|
assert_eq!(std::fs::read(&journal).unwrap(), corrupt);
|
|
}
|
|
|
|
/// 截断标记阻止旧页重放后复活
|
|
#[test]
|
|
fn truncate_clears_page_versions_and_survives_reopen() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let path = dir.path().join("reset.db");
|
|
let mut file = PageFile::open(&path, 512).unwrap();
|
|
file.set_lsn(5);
|
|
for _ in 0..3 {
|
|
file.append_page().unwrap();
|
|
}
|
|
file.flush().unwrap();
|
|
file.set_lsn(8);
|
|
file.truncate().unwrap();
|
|
file.append_page().unwrap();
|
|
file.flush().unwrap();
|
|
assert_eq!(file.page_lsn(0), Some(8));
|
|
assert_eq!(file.page_lsn(2), None);
|
|
drop(file);
|
|
let mut file = PageFile::open(&path, 512).unwrap();
|
|
assert_eq!(file.num_pages().unwrap(), 1);
|
|
assert_eq!(file.page_lsn(0), Some(8));
|
|
assert_eq!(file.page_lsn(2), None);
|
|
}
|
|
|
|
/// 镜像写入失败后禁止析构或重试绕过恢复
|
|
#[test]
|
|
fn page_journal_error_fences_further_flushes() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let path = dir.path().join("fenced.db");
|
|
let journal = dir.path().join("fenced.db.page-wal");
|
|
std::fs::write(&path, [0; 512]).unwrap();
|
|
let mut file = PageFile::open(&path, 512).unwrap();
|
|
let mut page = Page::new(0, 512);
|
|
page.data.fill(7);
|
|
file.write_page(&page).unwrap();
|
|
std::fs::create_dir(&journal).unwrap();
|
|
assert!(file.flush().is_err());
|
|
std::fs::remove_dir(&journal).unwrap();
|
|
assert!(
|
|
file.flush()
|
|
.unwrap_err()
|
|
.to_string()
|
|
.contains("requires recovery")
|
|
);
|
|
drop(file);
|
|
let mut file = PageFile::open(&path, 512).unwrap();
|
|
assert_eq!(file.read_page(0).unwrap().data, vec![0; 512]);
|
|
}
|
|
|
|
/// 小缓存淘汰不会反复记录同一批未改页
|
|
#[test]
|
|
fn eviction_batches_images_without_pool_sized_write_amplification() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let path = dir.path().join("bounded.db");
|
|
let journal = dir.path().join("bounded.db.page-wal");
|
|
let mut file = PageFile::with_options(&path, 512, 2).unwrap();
|
|
file.set_lsn(9);
|
|
for id in 0..100 {
|
|
let mut page = Page::new(id, 512);
|
|
page.data.fill(id as u8);
|
|
file.write_page(&page).unwrap();
|
|
}
|
|
assert!(std::fs::metadata(&journal).unwrap().len() <= 100 * (512 + 40));
|
|
file.flush().unwrap();
|
|
drop(file);
|
|
let mut file = PageFile::open(&path, 512).unwrap();
|
|
for id in 0..100 {
|
|
assert_eq!(file.read_page(id).unwrap().data, vec![id as u8; 512]);
|
|
assert_eq!(file.page_lsn(id), Some(9));
|
|
}
|
|
}
|