做完rust全链路

This commit is contained in:
LIm1tZz7 committed 2026-09-24 21:28:49 +08:00
1 parent e428265357
commit 2a4b446252
34 files changed
+1756 -1031

No files matched your search

+26 -21
View File
@@ -1,10 +1,10 @@
use chusql_storage::btree::DiskBTree;
use chusql_storage::config::{DEFAULT_BTREE_ORDER, DEFAULT_PAGE_SIZE};
use chusql_storage::page::DEFAULT_POOL_SIZE;
// B+ 树测试:空树查询、插入后查找、重开文件数据仍在、覆盖同一个键、升序遍历、
// 倒序插入 500 个键、页大小 / order 可以配置、配置和文件头对不上时报错。
// B+ 树测试:插查、重开、分裂、页大小与 order 校验。
/// 空树的 get 返回 None,遍历返回空。
/// 空树查不到,遍历为空
#[test]
fn empty_tree_get() {
let dir = tempfile::tempdir().unwrap();
@@ -12,13 +12,14 @@ fn empty_tree_get() {
dir.path().join("t.idx"),
DEFAULT_PAGE_SIZE,
DEFAULT_BTREE_ORDER,
DEFAULT_POOL_SIZE,
)
.unwrap();
assert_eq!(t.get(1).unwrap(), None);
assert_eq!(t.iter_all().unwrap(), vec![]);
}
/// 插 100 个键,每个都能按 key 查回来。
/// 插 100 个键都能查回
#[test]
fn insert_then_get() {
let dir = tempfile::tempdir().unwrap();
@@ -26,6 +27,7 @@ fn insert_then_get() {
dir.path().join("t.idx"),
DEFAULT_PAGE_SIZE,
DEFAULT_BTREE_ORDER,
DEFAULT_POOL_SIZE,
)
.unwrap();
for i in 0..100 {
@@ -36,24 +38,24 @@ fn insert_then_get() {
}
}
/// 关掉再打开同一个索引文件,数据还在。
/// 重开文件数据还在
#[test]
fn reopen_persists() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("t.idx");
{
let mut t = DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER).unwrap();
let mut t = DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).unwrap();
for i in 0..50 {
t.insert(i, i as u64).unwrap();
}
}
let mut t = DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER).unwrap();
let mut t = DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).unwrap();
for i in 0..50 {
assert_eq!(t.get(i).unwrap(), Some(i as u64));
}
}
/// 同一个键插两次,后写的值生效。
/// 同一个键后写的值生效
#[test]
fn update_existing() {
let dir = tempfile::tempdir().unwrap();
@@ -61,6 +63,7 @@ fn update_existing() {
dir.path().join("t.idx"),
DEFAULT_PAGE_SIZE,
DEFAULT_BTREE_ORDER,
DEFAULT_POOL_SIZE,
)
.unwrap();
t.insert(5, 100).unwrap();
@@ -68,7 +71,7 @@ fn update_existing() {
assert_eq!(t.get(5).unwrap(), Some(200));
}
/// 倒序插入后遍历,结果仍按 key 升序。
/// 倒序插入后遍历仍升序
#[test]
fn iter_all_sorted() {
let dir = tempfile::tempdir().unwrap();
@@ -76,6 +79,7 @@ fn iter_all_sorted() {
dir.path().join("t.idx"),
DEFAULT_PAGE_SIZE,
DEFAULT_BTREE_ORDER,
DEFAULT_POOL_SIZE,
)
.unwrap();
for i in (0..50).rev() {
@@ -88,7 +92,7 @@ fn iter_all_sorted() {
}
}
/// 倒序插入 500 个键(必然多次分裂),每个键都能查到。
/// 倒序插 500 个键都能查到
#[test]
fn reverse_order_insert() {
let dir = tempfile::tempdir().unwrap();
@@ -96,6 +100,7 @@ fn reverse_order_insert() {
dir.path().join("t.idx"),
DEFAULT_PAGE_SIZE,
DEFAULT_BTREE_ORDER,
DEFAULT_POOL_SIZE,
)
.unwrap();
for i in (0..500).rev() {
@@ -106,11 +111,11 @@ fn reverse_order_insert() {
}
}
/// 页大小和 order 都可以配置:512 字节的页 + 8 个孩子照样能插能查。
/// 512 字节页 + 8 个孩子可用
#[test]
fn custom_page_size_and_order() {
let dir = tempfile::tempdir().unwrap();
let mut t = DiskBTree::open(dir.path().join("t.idx"), 512, 8).unwrap();
let mut t = DiskBTree::open(dir.path().join("t.idx"), 512, 8, DEFAULT_POOL_SIZE).unwrap();
for i in 0..200 {
t.insert(i, i as u64 + 1).unwrap();
}
@@ -119,32 +124,32 @@ fn custom_page_size_and_order() {
}
}
/// 文件头里记着页大小和 order:换配置再打开要报错,而不是读出乱七八糟的东西。
/// 页大小或 order 不符要报错
#[test]
fn config_mismatch_errors() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("t.idx");
{
let mut t = DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER).unwrap();
let mut t = DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).unwrap();
t.insert(1, 1).unwrap();
}
assert!(DiskBTree::open(&path, DEFAULT_PAGE_SIZE, 8).is_err());
assert!(DiskBTree::open(&path, 8192, DEFAULT_BTREE_ORDER).is_err());
assert!(DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER).is_ok());
assert!(DiskBTree::open(&path, DEFAULT_PAGE_SIZE, 8, DEFAULT_POOL_SIZE).is_err());
assert!(DiskBTree::open(&path, 8192, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).is_err());
assert!(DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).is_ok());
}
/// 不是本项目的索引文件(比如随便一个空文件)要报错。
/// 不是索引文件要报错
#[test]
fn not_an_index_file_errors() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("junk.idx");
std::fs::write(&path, vec![7u8; DEFAULT_PAGE_SIZE]).unwrap();
assert!(DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER).is_err());
assert!(DiskBTree::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_BTREE_ORDER, DEFAULT_POOL_SIZE).is_err());
}
/// order 放不进一页时直接拒绝。
/// order 放不进一页要报错
#[test]
fn order_too_large_for_page_errors() {
let dir = tempfile::tempdir().unwrap();
assert!(DiskBTree::open(dir.path().join("t.idx"), 512, 300).is_err());
assert!(DiskBTree::open(dir.path().join("t.idx"), 512, 300, DEFAULT_POOL_SIZE).is_err());
}
+8
View File
@@ -2,6 +2,10 @@ use chusql_storage::catalog::Catalog;
use chusql_storage::protocol::{ColumnType, Row};
use serde_json::json;
// 数据字典测试:补列、去重、未知表。
fn row(pairs: &[(&str, serde_json::Value)]) -> Row {
let mut m = Row::new();
for (k, v) in pairs {
@@ -10,6 +14,7 @@ fn row(pairs: &[(&str, serde_json::Value)]) -> Row {
m
}
/// 空文件得到空字典
#[test]
fn empty_file_gives_empty_catalog() {
let dir = tempfile::tempdir().unwrap();
@@ -18,6 +23,7 @@ fn empty_file_gives_empty_catalog() {
assert_eq!(c.table_names().len(), 0);
}
/// 补列之后能查到 schema
#[test]
fn ensure_then_describe() {
let dir = tempfile::tempdir().unwrap();
@@ -39,6 +45,7 @@ fn ensure_then_describe() {
assert_eq!(name_col.ty, ColumnType::Str);
}
/// 再插入不会重复列
#[test]
fn second_insert_does_not_duplicate_columns() {
let dir = tempfile::tempdir().unwrap();
@@ -50,6 +57,7 @@ fn second_insert_does_not_duplicate_columns() {
assert_eq!(c.describe("users").unwrap().columns.len(), 1);
}
/// 未知表返回 None
#[test]
fn describe_unknown_returns_none() {
let dir = tempfile::tempdir().unwrap();
+13 -13
View File
@@ -6,10 +6,9 @@ use chusql_storage::config::{
};
use chusql_storage::log::Level;
// 配置测试:没有文件时全默认、文件里写了才覆盖、环境变量优先于文件、
// 非法值 / 未知键要报错、每个选项的来源都记得下来。
// 配置测试:默认值、文件覆盖、环境变量优先、非法值报错。
/// 没有配置文件、没有环境变量:全部走内置默认值。
/// 无文件时全默认
#[test]
fn defaults_without_file() {
let loaded = config::resolve(None, None, &|_| None).unwrap();
@@ -23,7 +22,7 @@ fn defaults_without_file() {
assert_eq!(loaded.config.log_level, Level::Info);
}
/// 文件里只写了一项:这一项生效,其余回退默认(并且来源记得清清楚楚)。
/// 只覆盖文件里写了的项
#[test]
fn file_overrides_only_written_keys() {
let path = PathBuf::from("t.toml");
@@ -41,15 +40,16 @@ fn file_overrides_only_written_keys() {
assert_eq!(order.2, Origin::Default);
}
/// 五项全写:值都生效,来源都是文件。
/// 六项全写都生效
#[test]
fn file_all_keys() {
let text = "[page]\nsize = 8192\n[btree]\norder = 32\n[storage]\ndata_dir = \"mydata\"\n[server]\npipe_name = \"mypipe\"\n[log]\nlevel = \"debug\"\n";
let text = "[page]\nsize = 8192\n[btree]\norder = 32\n[buffer]\npool_size = 128\n[storage]\ndata_dir = \"mydata\"\n[server]\npipe_name = \"mypipe\"\n[log]\nlevel = \"debug\"\n";
let path = PathBuf::from("t.toml");
let loaded = config::resolve(Some(text), Some(path.clone()), &|_| None).unwrap();
assert_eq!(loaded.config.page_size, 8192);
assert_eq!(loaded.config.btree_order, 32);
assert_eq!(loaded.config.pool_size, 128);
assert_eq!(loaded.config.data_dir, PathBuf::from("mydata"));
assert_eq!(loaded.config.pipe_name, "mypipe");
assert_eq!(loaded.config.log_level, Level::Debug);
@@ -61,7 +61,7 @@ fn file_all_keys() {
);
}
/// 环境变量优先于文件。
/// 环境变量优先于文件
#[test]
fn env_beats_file() {
let text = "[storage]\ndata_dir = \"from-file\"\n";
@@ -77,14 +77,14 @@ fn env_beats_file() {
assert_eq!(dir.2, Origin::Env("CHUSQL_DATA_DIR"));
}
/// 日志等级写错要报错,而不是悄悄按默认跑。
/// 日志等级非法要报错
#[test]
fn rejects_bad_log_level() {
let err = config::resolve(Some("[log]\nlevel = \"loud\"\n"), None, &|_| None).unwrap_err();
assert!(err.contains("log.level"), "got {}", err);
}
/// 页大小必须是 512..65536 之间的 2 的幂。
/// 页大小非法要报错
#[test]
fn rejects_bad_page_size() {
let err = config::resolve(Some("[page]\nsize = 3000\n"), None, &|_| None).unwrap_err();
@@ -94,7 +94,7 @@ fn rejects_bad_page_size() {
assert!(err.contains("page.size"), "got {}", err);
}
/// order 太小、或者放不进一页,都要报错。
/// order 非法要报错
#[test]
fn rejects_bad_btree_order() {
let err = config::resolve(Some("[btree]\norder = 2\n"), None, &|_| None).unwrap_err();
@@ -104,14 +104,14 @@ fn rejects_bad_btree_order() {
assert!(err.contains("does not fit"), "got {}", err);
}
/// 配置里出现认不出来的键要报错(拼错了不能当没看见)。
/// 未知配置键要报错
#[test]
fn rejects_unknown_key() {
let err = config::resolve(Some("[page]\nsizes = 4096\n"), None, &|_| None).unwrap_err();
assert!(err.contains("bad config"), "got {}", err);
}
/// 环境变量给了非法值也要报错。
/// 环境变量非法要报错
#[test]
fn rejects_bad_env_value() {
let env = |k: &str| (k == "CHUSQL_PAGE_SIZE").then(|| "big".to_string());
@@ -119,7 +119,7 @@ fn rejects_bad_env_value() {
assert!(err.contains("CHUSQL_PAGE_SIZE"), "got {}", err);
}
/// 来源文案要能说清"哪来的"。
/// 来源文案正确
#[test]
fn origin_description() {
assert_eq!(Origin::Default.describe(), "default");
+17 -17
View File
@@ -1,11 +1,11 @@
use chusql_storage::config::DEFAULT_PAGE_SIZE;
use chusql_storage::heap::HeapTable;
use chusql_storage::page::DEFAULT_POOL_SIZE;
use chusql_storage::protocol::Row;
use serde_json::json;
// 堆表测试:插入 + 全表扫描、跨页多行、大行、超大行报错、重开文件后数据还在、自定义页大小。
// 堆表测试:插查、跨页、大行、重开、索引重建。
/// 造一行测试数据(列名 → JSON 值)。
fn row(pairs: &[(&str, serde_json::Value)]) -> Row {
let mut m = Row::new();
for (k, v) in pairs {
@@ -14,12 +14,12 @@ fn row(pairs: &[(&str, serde_json::Value)]) -> Row {
m
}
/// 插一行再扫描,能原样读回来。
/// 插一行再扫描读回
#[test]
fn insert_and_scan_one_row() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("t.db");
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE).unwrap();
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
t.insert(&row(&[("id", json!(1)), ("name", json!("Alice"))]))
.unwrap();
@@ -30,12 +30,12 @@ fn insert_and_scan_one_row() {
assert_eq!(rows[0]["name"], json!("Alice"));
}
/// 插 500 行(必然跨多页),扫描回来的行数和顺序都要对。
/// 500 行跨页顺序不变
#[test]
fn insert_many_rows_cross_pages() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("t.db");
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE).unwrap();
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
for i in 0..500 {
t.insert(&row(&[("id", json!(i))])).unwrap();
@@ -43,18 +43,17 @@ fn insert_many_rows_cross_pages() {
let rows = t.scan().unwrap();
assert_eq!(rows.len(), 500);
// 顺序保持
for (i, r) in rows.iter().enumerate() {
assert_eq!(r["id"], json!(i as i64));
}
}
/// 约 2KB 的大行也要能放进一页。
/// 2KB 大行放得下
#[test]
fn large_row_still_fits() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("t.db");
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE).unwrap();
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
let big = "x".repeat(2000);
t.insert(&row(&[("data", json!(big.clone()))])).unwrap();
@@ -64,43 +63,43 @@ fn large_row_still_fits() {
assert_eq!(rows[0]["data"], json!(big));
}
/// 超过一页容量的行要报错,而不是悄悄写坏。
/// 超页的行要报错
#[test]
fn row_too_large_errors() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("t.db");
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE).unwrap();
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
let huge = "x".repeat(10_000);
let err = t.insert(&row(&[("data", json!(huge))]));
assert!(err.is_err());
}
/// 关掉再重新打开同一个文件,之前插入的数据还在。
/// 重开文件数据还在
#[test]
fn reopen_and_scan() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("t.db");
{
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE).unwrap();
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
t.insert(&row(&[("id", json!(1))])).unwrap();
t.insert(&row(&[("id", json!(2))])).unwrap();
}
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE).unwrap();
let mut t = HeapTable::open(&path, DEFAULT_PAGE_SIZE, DEFAULT_POOL_SIZE).unwrap();
let rows = t.scan().unwrap();
assert_eq!(rows.len(), 2);
assert_eq!(rows[0]["id"], json!(1));
assert_eq!(rows[1]["id"], json!(2));
}
/// 页大小可以配置:512 字节的页装得下小行、放不下大行,行数不变。
/// 512 字节页可用
#[test]
fn custom_page_size() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("small.db");
let mut t = HeapTable::open(&path, 512).unwrap();
let mut t = HeapTable::open(&path, 512, DEFAULT_POOL_SIZE).unwrap();
for i in 0..50 {
t.insert(&row(&[("id", json!(i))])).unwrap();
@@ -111,7 +110,7 @@ fn custom_page_size() {
assert!(t.insert(&row(&[("data", json!(big))])).is_err());
}
/// 整表改写之后索引必须跟着重建:旧键查不到,新行按 id 能查到。
/// 整表改写后索引重建
#[test]
fn replace_all_rebuilds_index() {
let dir = tempfile::tempdir().unwrap();
@@ -120,6 +119,7 @@ fn replace_all_rebuilds_index() {
dir.path().join("t.idx"),
DEFAULT_PAGE_SIZE,
chusql_storage::config::DEFAULT_BTREE_ORDER,
DEFAULT_POOL_SIZE,
)
.unwrap();
+52 -18
View File
@@ -1,10 +1,9 @@
use chusql_storage::config::DEFAULT_PAGE_SIZE;
use chusql_storage::page::{Page, PageFile};
// 页文件测试:一页的读写、页号越界报错、追加页让文件变长、多页互不干扰、覆写同一页、
// 自定义页大小、页大小和文件对不上时报错。
// 页文件测试:读写、追加、覆写、页大小校验与页缓存。
/// 写入一页再读回来,内容和长度都应该一致。
/// 写一页再读回一致
#[test]
fn write_then_read_page() {
let dir = tempfile::tempdir().unwrap();
@@ -23,7 +22,7 @@ fn write_then_read_page() {
assert_eq!(read.data.len(), DEFAULT_PAGE_SIZE);
}
/// 空文件读第 0 页应该报错。
/// 空文件读第 0 页报错
#[test]
fn read_out_of_range_returns_error() {
let dir = tempfile::tempdir().unwrap();
@@ -33,7 +32,7 @@ fn read_out_of_range_returns_error() {
assert!(pf.read_page(0).is_err());
}
/// 每 append 一页,num_pages 就加一。
/// 追加让页数加一
#[test]
fn append_grows_file() {
let dir = tempfile::tempdir().unwrap();
@@ -50,7 +49,7 @@ fn append_grows_file() {
assert_eq!(pf.num_pages().unwrap(), 3);
}
/// 页 0 和页 1 的内容互不影响。
/// 两页内容互不影响
#[test]
fn two_pages_independent() {
let dir = tempfile::tempdir().unwrap();
@@ -69,7 +68,7 @@ fn two_pages_independent() {
assert_eq!(pf.read_page(1).unwrap().data[0], 2);
}
/// 覆写同一页:读到的是新内容,页数不变。
/// 覆写同页读到新内容
#[test]
fn overwrite_page() {
let dir = tempfile::tempdir().unwrap();
@@ -89,25 +88,20 @@ fn overwrite_page() {
assert_eq!(pf.num_pages().unwrap(), 1);
}
/// 页大小可以配置:512 字节的页照样读写,文件按 512 字节对齐。
/// 512 字节页可用
#[test]
fn custom_page_size() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("small.db");
let path = dir.path().join("t.db");
let mut pf = PageFile::open(&path, 512).unwrap();
assert_eq!(pf.page_size(), 512);
let mut p = Page::new(0, 512);
p.data[511] = 9;
pf.write_page(&p).unwrap();
pf.write_page(&Page::new(0, 512)).unwrap();
pf.flush().unwrap();
assert_eq!(pf.read_page(0).unwrap().data[511], 9);
assert_eq!(pf.num_pages().unwrap(), 1);
assert_eq!(std::fs::metadata(&path).unwrap().len(), 512);
}
/// 文件长度不是页大小整数倍(比如页大小被改过)要报错,而不是算出错误的页数。
/// 长度不是整数倍要报错
#[test]
fn mismatched_file_length_errors() {
let dir = tempfile::tempdir().unwrap();
@@ -118,7 +112,7 @@ fn mismatched_file_length_errors() {
assert!(pf.num_pages().is_err());
}
/// 页大小对不上的页不许写进去。
/// 页大小不符拒绝写入
#[test]
fn write_wrong_sized_page_errors() {
let dir = tempfile::tempdir().unwrap();
@@ -127,3 +121,43 @@ fn write_wrong_sized_page_errors() {
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);
}
+71 -19
View File
@@ -9,9 +9,9 @@ use interprocess::local_socket::{
};
use interprocess::TryClone;
// 命名管道集成测试:起 server 子进程,用唯一管道名,测试之间互不干扰。
// 命名管道集成测试:起真 server,跑协议、索引与配置。
/// 生成唯一管道名,避免测试之间抢占。
/// 唯一管道名,避免测试互抢
fn unique_pipe_name() -> String {
let id = std::process::id();
let ns = std::time::SystemTime::now()
@@ -21,18 +21,18 @@ fn unique_pipe_name() -> String {
format!("chusql-test-{}-{}", id, ns)
}
/// 持有 server 子进程,Drop 时杀掉。
/// 持有 server 子进程,退出时杀掉
struct ServerProc(Child);
impl Drop for ServerProc {
/// 测试结束时杀掉子进程并回收,避免留下孤儿进程。
/// 测试结束杀掉子进程
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
/// 启动 server 子进程,并等管道可连(最多 5 秒)。
/// 起 server,等管道可连
fn start_server(pipe: &str) -> (ServerProc, tempfile::TempDir) {
let data = tempfile::tempdir().unwrap();
@@ -57,7 +57,7 @@ fn start_server(pipe: &str) -> (ServerProc, tempfile::TempDir) {
panic!("server did not start within 5s");
}
/// 连到指定管道。
/// 连到指定管道
fn connect(pipe: &str) -> std::io::Result<LocalSocketStream> {
let name = pipe
.to_ns_name::<GenericNamespaced>()
@@ -65,7 +65,7 @@ fn connect(pipe: &str) -> std::io::Result<LocalSocketStream> {
LocalSocketStream::connect(name)
}
/// 发一行请求,读一行响应。
/// 发一行请求读一行响应
fn send(stream: &mut LocalSocketStream, line: &str) -> String {
stream.write_all(line.as_bytes()).unwrap();
stream.write_all(b"\n").unwrap();
@@ -77,7 +77,7 @@ fn send(stream: &mut LocalSocketStream, line: &str) -> String {
buf.trim().to_string()
}
/// ping 应该回 pong。
/// ping 回 pong
#[test]
fn ping_pong() {
let pipe = unique_pipe_name();
@@ -87,7 +87,7 @@ fn ping_pong() {
assert_eq!(send(&mut c, r#"{"method":"ping"}"#), r#"{"status":"pong"}"#);
}
/// 插一行再 scan,能扫到刚插进去的数据。
/// 插入后能扫到
#[test]
fn insert_then_scan() {
let pipe = unique_pipe_name();
@@ -104,6 +104,7 @@ fn insert_then_scan() {
assert!(r2.contains("Alice"), "scan response: {}", r2);
}
/// 不存在的表报错
#[test]
fn scan_unknown_table_errors() {
let pipe = unique_pipe_name();
@@ -115,6 +116,7 @@ fn scan_unknown_table_errors() {
assert!(r.contains("unknown table"), "got {}", r);
}
/// 列出已建的表
#[test]
fn list_tables_returns_created_tables() {
let pipe = unique_pipe_name();
@@ -129,6 +131,7 @@ fn list_tables_returns_created_tables() {
assert!(r.contains("orders"), "got {}", r);
}
/// 整表替换只剩新行
#[test]
fn replace_all_replaces_rows() {
let pipe = unique_pipe_name();
@@ -149,13 +152,13 @@ fn replace_all_replaces_rows() {
assert!(!r.contains("Alice"), "old row should be gone: {}", r);
}
/// 带 key 插入后能按下标查
#[test]
fn insert_with_key_then_lookup() {
let pipe = unique_pipe_name();
let (_srv, _data) = start_server(&pipe);
let mut c = connect(&pipe).unwrap();
// 带索引键插三行
for (id, name) in [(1, "Alice"), (2, "Bob"), (3, "Carol")] {
let req = format!(
r#"{{"method":"insert","table":"idx_users","row":{{"id":{},"name":"{}"}},"key":{}}}"#,
@@ -165,15 +168,14 @@ fn insert_with_key_then_lookup() {
assert!(r.contains(r#""status":"ok""#), "insert: {}", r);
}
// 命中
let r = send(&mut c, r#"{"method":"lookup_by_index","table":"idx_users","key":2}"#);
assert!(r.contains("Bob"), "lookup: {}", r);
// 未命中
let r = send(&mut c, r#"{"method":"lookup_by_index","table":"idx_users","key":99}"#);
assert!(r.contains(r#""rows":[]"#), "miss: {}", r);
}
/// 不带 key 不建索引
#[test]
fn insert_without_key_does_not_index() {
let pipe = unique_pipe_name();
@@ -185,12 +187,12 @@ fn insert_without_key_does_not_index() {
assert!(r.contains(r#""rows":[]"#), "got: {}", r);
}
/// 重开服务后索引仍在
#[test]
fn lookup_survives_reopen() {
let pipe = unique_pipe_name();
let data = tempfile::tempdir().unwrap();
// 第一次:写 3 行带 key
{
let exe = env!("CARGO_BIN_EXE_server");
let child = Command::new(exe)
@@ -213,13 +215,11 @@ fn lookup_survives_reopen() {
);
send(&mut c, &req);
}
drop(srv); // 杀 server
drop(srv);
}
// 等一下,让文件句柄真正释放
thread::sleep(Duration::from_millis(200));
// 第二次:重开 server,从索引查到
let (_srv, _data) = {
let child = Command::new(env!("CARGO_BIN_EXE_server"))
.env("CHUSQL_PIPE", &pipe)
@@ -241,7 +241,7 @@ fn lookup_survives_reopen() {
assert!(r.contains("Bob"), "persist lookup: {}", r);
}
/// 配置文件里的管道名和数据目录要真的生效(这里不设 CHUSQL_PIPE / CHUSQL_DATA_DIR)。
/// 配置文件真的生效
#[test]
fn config_file_is_used() {
let dir = tempfile::tempdir().unwrap();
@@ -249,7 +249,6 @@ fn config_file_is_used() {
let data_dir = dir.path().join("data");
let config_path = dir.path().join("chusql-storage.toml");
// TOML 里写正斜杠,省得转义 Windows 路径
let text = format!(
"[server]\npipe_name = \"{}\"\n[storage]\ndata_dir = \"{}\"\n[log]\nlevel = \"debug\"\n",
pipe,
@@ -276,9 +275,62 @@ fn config_file_is_used() {
let r = send(&mut c, r#"{"method":"insert","table":"cfg","row":{"id":1}}"#);
assert!(r.contains(r#""status":"ok""#), "insert: {}", r);
// 数据真的落在配置文件指的那个目录里
assert!(
data_dir.join("cfg.db").exists(),
"data dir from config file was not used"
);
}
/// 建表后能查 schema
#[test]
fn create_table_then_describe() {
let pipe = unique_pipe_name();
let (_srv, _data) = start_server(&pipe);
let mut c = connect(&pipe).unwrap();
let r = send(
&mut c,
r#"{"method":"create_table","table":"ct_users","columns":[{"name":"id","ty":"int"},{"name":"name","ty":"str"}]}"#,
);
assert!(r.contains(r#""status":"ok""#), "create: {}", r);
let r = send(&mut c, r#"{"method":"describe_table","table":"ct_users"}"#);
assert!(r.contains("id"), "describe: {}", r);
assert!(r.contains("name"), "describe: {}", r);
assert!(r.contains(r#""ty":"int""#), "describe: {}", r);
assert!(r.contains(r#""ty":"str""#), "describe: {}", r);
}
/// 重复建表报错
#[test]
fn create_table_twice_errors() {
let pipe = unique_pipe_name();
let (_srv, _data) = start_server(&pipe);
let mut c = connect(&pipe).unwrap();
let req = r#"{"method":"create_table","table":"dup","columns":[{"name":"id","ty":"int"}]}"#;
let r1 = send(&mut c, req);
assert!(r1.contains(r#""status":"ok""#), "first: {}", r1);
let r2 = send(&mut c, req);
assert!(r2.contains(r#""status":"error""#), "second: {}", r2);
assert!(r2.contains("already exists"), "second: {}", r2);
}
/// 新建空表能扫描
#[test]
fn scan_empty_table_after_create() {
let pipe = unique_pipe_name();
let (_srv, _data) = start_server(&pipe);
let mut c = connect(&pipe).unwrap();
let r = send(
&mut c,
r#"{"method":"create_table","table":"empty_t","columns":[{"name":"id","ty":"int"}]}"#,
);
assert!(r.contains(r#""status":"ok""#), "create: {}", r);
let r = send(&mut c, r#"{"method":"scan","table":"empty_t"}"#);
assert!(r.contains(r#""status":"rows""#), "scan: {}", r);
assert!(r.contains(r#""rows":[]"#), "scan: {}", r);
}
+10 -5
View File
@@ -2,6 +2,9 @@ use chusql_storage::protocol::Row;
use chusql_storage::wal::{Wal, WalOp};
use serde_json::json;
// WAL 测试:追加、读取、清空、截断尾巴。
fn row(pairs: &[(&str, serde_json::Value)]) -> Row {
let mut m = Row::new();
for (k, v) in pairs {
@@ -10,6 +13,7 @@ fn row(pairs: &[(&str, serde_json::Value)]) -> Row {
m
}
/// 写读 Insert 操作
#[test]
fn append_and_read_insert() {
let dir = tempfile::tempdir().unwrap();
@@ -26,6 +30,7 @@ fn append_and_read_insert() {
assert_eq!(back, op);
}
/// 写读 ReplaceAll 操作
#[test]
fn append_and_read_replace_all() {
let dir = tempfile::tempdir().unwrap();
@@ -44,6 +49,7 @@ fn append_and_read_replace_all() {
assert_eq!(back, op);
}
/// 文件不存在返回 None
#[test]
fn missing_file_returns_none() {
let dir = tempfile::tempdir().unwrap();
@@ -51,6 +57,7 @@ fn missing_file_returns_none() {
assert!(wal.read().unwrap().is_none());
}
/// 空文件返回 None
#[test]
fn empty_file_returns_none() {
let dir = tempfile::tempdir().unwrap();
@@ -60,6 +67,7 @@ fn empty_file_returns_none() {
assert!(wal.read().unwrap().is_none());
}
/// 清空后读不到
#[test]
fn clear_truncates() {
let dir = tempfile::tempdir().unwrap();
@@ -79,6 +87,7 @@ fn clear_truncates() {
assert_eq!(std::fs::metadata(&path).unwrap().len(), 0);
}
/// 尾巴截断被忽略
#[test]
fn truncated_tail_is_ignored() {
let dir = tempfile::tempdir().unwrap();
@@ -92,19 +101,15 @@ fn truncated_tail_is_ignored() {
})
.unwrap();
// 砍掉最后 5 个字节,模拟"写一半断电"
let full = std::fs::read(&path).unwrap();
std::fs::write(&path, &full[..full.len() - 5]).unwrap();
// 读回来应该当作"没有这条"
assert!(wal.read().unwrap().is_none());
}
/// 第二次追加会覆盖
#[test]
fn overwrite_on_second_append_is_visible() {
// 简化版一次只允许一条,append 两次会叠在文件里——
// 但正常路径下 clear 先跑,所以实际不会叠加。
// 这个测试只验证 read 能解析最前面那一条。
let dir = tempfile::tempdir().unwrap();
let wal = Wal::new(dir.path().join("wal.log"));