use std::sync::Arc;
use aok::{OK, Void};
use compio::runtime::Runtime;
use tempfile::tempdir;
use wcompact::{CompactionType, LogCompactor};
use super::support::FixtureStore;
#[test]
fn until_mid_record_aligns_to_record_end() -> Void {
let rt = Runtime::new()?;
rt.block_on(async {
let dir = tempdir()?;
let store = FixtureStore::open(dir.path().join("align.db"))?;
let s = store.session()?;
store.put(&s, b"key:first", b"v1").await?;
let addr_k2 = store.put(&s, b"key:second", b"v2").await?;
let tail = store.hlog.tail_address();
store.seal_read_only(tail);
let compactor = LogCompactor::new(Arc::clone(&store));
let stats = compactor
.compact(addr_k2 + 3, CompactionType::Lookup)
.await?;
assert_eq!(
stats.scanned_records, 2,
"落点内侧的两条记录都必须处理: {stats:?}"
);
assert_eq!(stats.live_copied, 2);
assert_eq!(
stats.new_begin_address, tail,
"截断点必须对齐到末记录结束边界(即 tail)"
);
assert_eq!(
store.get(&s, b"key:first").await?.as_deref(),
Some(b"v1".as_slice())
);
assert_eq!(
store.get(&s, b"key:second").await?.as_deref(),
Some(b"v2".as_slice())
);
aok::Result::<()>::Ok(())
})?;
OK
}
#[test]
fn record_crossing_read_only_falls_back_to_start() -> Void {
let rt = Runtime::new()?;
rt.block_on(async {
let dir = tempdir()?;
let store = FixtureStore::open(dir.path().join("crossing.db"))?;
let s = store.session()?;
store.put(&s, b"key:first", b"v1").await?;
let addr_k2 = store.put(&s, b"key:second", b"v2").await?;
store.seal_read_only(addr_k2 + 2);
let compactor = LogCompactor::new(Arc::clone(&store));
let stats = compactor
.compact(addr_k2 + 2, CompactionType::Lookup)
.await?;
assert_eq!(stats.scanned_records, 1, "跨界记录不得计入扫描: {stats:?}");
assert_eq!(stats.live_copied, 1, "区间内记录照常迁移");
assert_eq!(
stats.new_begin_address, addr_k2,
"截断点必须回退至跨界记录起始边界"
);
assert_eq!(
store.get(&s, b"key:first").await?.as_deref(),
Some(b"v1".as_slice())
);
assert_eq!(
store.get(&s, b"key:second").await?.as_deref(),
Some(b"v2".as_slice())
);
aok::Result::<()>::Ok(())
})?;
OK
}
#[test]
fn zero_retry_budget_retains_and_rolls_back_truncation() -> Void {
let rt = Runtime::new()?;
rt.block_on(async {
let dir = tempdir()?;
let store = FixtureStore::open(dir.path().join("retain.db"))?;
let s = store.session()?;
let addr_k1 = store.put(&s, b"key:a", b"va").await?;
store.put(&s, b"key:b", b"vb").await?;
store.put(&s, b"key:c", b"vc").await?;
let tail = store.hlog.tail_address();
store.seal_read_only(tail);
let compactor = LogCompactor::with_cas_retries(Arc::clone(&store), 0);
let stats = compactor.compact(tail, CompactionType::Lookup).await?;
assert_eq!(stats.scanned_records, 3);
assert_eq!(stats.retained, 3, "全部存活记录必须保守保留: {stats:?}");
assert_eq!(stats.live_copied, 0);
assert_eq!(stats.superseded, 0);
assert!(
store.reviv_puts_snapshot().is_empty(),
"保守保留路径不得产生任何孤儿副本"
);
assert_eq!(
stats.new_begin_address, addr_k1,
"截断点必须回退至最早存活记录起始边界"
);
assert_eq!(
store.get(&s, b"key:a").await?.as_deref(),
Some(b"va".as_slice())
);
assert_eq!(
store.get(&s, b"key:b").await?.as_deref(),
Some(b"vb".as_slice())
);
assert_eq!(
store.get(&s, b"key:c").await?.as_deref(),
Some(b"vc".as_slice())
);
aok::Result::<()>::Ok(())
})?;
OK
}
#[test]
fn lazy_compaction_window_bounded_by_budget() -> Void {
let rt = Runtime::new()?;
rt.block_on(async {
let dir = tempdir()?;
let store = FixtureStore::open(dir.path().join("lazy.db"))?;
let s = store.session()?;
store.put(&s, b"key:a", b"va").await?;
store.put(&s, b"key:b", b"vb").await?;
let tail = store.hlog.tail_address();
store.seal_read_only(tail);
let compactor = LogCompactor::new(Arc::clone(&store));
let stats = compactor.compact_lazy(0).await?;
assert!(stats.is_empty(), "预算 0 必须空转: {stats:?}");
assert_eq!(stats.new_begin_address, store.hlog.begin_address());
let stats = compactor.compact_lazy(u64::MAX).await?;
assert_eq!(stats.scanned_records, 2);
assert_eq!(stats.live_copied, 2);
assert_eq!(stats.new_begin_address, tail, "全量预算必须推进至只读区");
assert_eq!(
store.get(&s, b"key:a").await?.as_deref(),
Some(b"va".as_slice())
);
assert_eq!(
store.get(&s, b"key:b").await?.as_deref(),
Some(b"vb".as_slice())
);
aok::Result::<()>::Ok(())
})?;
OK
}