#![forbid(unsafe_code)]
use std::path::Path;
use crate::evidence::corpus::source_tree_files;
use crate::optimizer::policy::OptimizeOptions;
use crate::store::transaction::CrashHooks;
use crate::store::{NewEntry, Store, StoreConfig};
use tempfile::TempDir;
fn create_store(dir: &TempDir) -> Store {
let cfg = StoreConfig {
segment_size: 4 * 1024 * 1024,
..Default::default()
};
Store::create(dir.path(), &cfg, [0x7b; 16]).unwrap()
}
fn write_tree(store: &Store, files: &[(String, Vec<u8>)]) -> Vec<(u64, u64, Vec<u8>)> {
use std::collections::HashMap;
let mut dir_cache: HashMap<String, u64> = HashMap::new();
dir_cache.insert(String::new(), store.current_root().root_dir_ino);
let mut out: Vec<(u64, u64, Vec<u8>)> = Vec::new();
for (rel, bytes) in files {
let (dir_part, name) = match rel.rsplit_once('/') {
Some((d, n)) => (d.to_string(), n.to_string()),
None => (String::new(), rel.clone()),
};
if !dir_cache.contains_key(&dir_part) {
let mut cur = String::new();
let mut cur_ino = store.current_root().root_dir_ino;
for comp in dir_part.split('/') {
if comp.is_empty() {
continue;
}
let next_path = if cur.is_empty() {
comp.to_string()
} else {
format!("{cur}/{comp}")
};
let ino = match dir_cache.get(&next_path) {
Some(&c) => c,
None => {
let existing = store.dir_lookup(cur_ino, comp.as_bytes()).unwrap();
let ino = match existing {
Some(e) => e.ino,
None => store
.create_entry(
cur_ino,
comp.as_bytes(),
NewEntry::dir(0o755, 1000, 1000),
&CrashHooks::none(),
)
.unwrap(),
};
dir_cache.insert(next_path.clone(), ino);
ino
}
};
cur = next_path;
cur_ino = ino;
}
dir_cache.insert(dir_part.clone(), cur_ino);
}
let ino = store
.create_entry(
dir_cache[&dir_part],
name.as_bytes(),
NewEntry::file(0o644, 1000, 1000),
&CrashHooks::none(),
)
.unwrap();
let mut writes: Vec<(u64, Vec<u8>)> = Vec::new();
let mut off = 0u64;
while off < bytes.len() as u64 {
let len = 65536u64.min(bytes.len() as u64 - off);
writes.push((off, bytes[off as usize..(off + len) as usize].to_vec()));
off += len;
}
store
.write_region_batch(ino, &writes, OptimizeOptions::default())
.unwrap();
out.push((ino, dir_cache[&dir_part], bytes.clone()));
}
out
}
fn reachable(store: &Store) -> u64 {
let unreachable = crate::store::gc::unreachable_bytes(store).unwrap();
let records_total: u64 = store
.object_index()
.iter()
.into_iter()
.map(|(_, loc)| loc.total_size())
.sum();
records_total.saturating_sub(unreachable)
}
fn verify_bytes(store: &Store, files: &[(u64, u64, Vec<u8>)]) {
for (ino, _dir, bytes) in files {
let mut got = Vec::new();
let mut off = 0u64;
while off < bytes.len() as u64 {
let len = 65536u64.min(bytes.len() as u64 - off);
got.extend_from_slice(&store.read_file(*ino, off, len).unwrap());
off += len;
}
assert_eq!(got, *bytes, "byte-exactness violated for ino {ino}");
}
}
#[test]
fn print_physical_reconciliation_real_tree() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let files = source_tree_files(root).unwrap();
let dir = TempDir::new().unwrap();
let store = create_store(&dir);
let tree = write_tree(&store, &files);
crate::optimizer::background::shared_dict_pass(&store, OptimizeOptions::default(), None)
.unwrap();
crate::optimizer::background::model_bundle_pass(&store, OptimizeOptions::default(), None)
.unwrap();
println!(
"segments before GC: {:?}",
crate::store::segment::list_segments(store.dir()).unwrap()
);
let live0 = crate::store::gc::mark_live(&store).unwrap();
let ratios = crate::store::gc::physical_ratios(&store, &live0).unwrap();
for (seq, (l, t)) in &ratios {
println!(
" pre-GC seg {seq}: live {l} / {t} = {:.3}",
*l as f64 / *t as f64
);
}
let reclaimed = crate::store::gc::collect(&store, &CrashHooks::none()).unwrap();
println!(
"collect reclaimed {reclaimed}; segments now: {:?}",
crate::store::segment::list_segments(store.dir()).unwrap()
);
let by_tag = crate::store::gc::unreachable_bytes_by_record_tag(&store).unwrap();
println!("unreachable by tag post-GC: {by_tag:?}");
verify_bytes(&store, &tree);
let report = crate::store::physical::physical_report(&store).unwrap();
println!("\n==== Phase-9H physical reconciliation (real tree) ====");
println!(
"logical {} B reachable {} B",
store.logical_bytes().unwrap(),
reachable(&store)
);
println!("{}", crate::store::physical::render(&report));
println!(
"segments: {} ({} with index-hidden bytes, {} with dead indexed bytes)",
report.segments.len(),
report
.segments
.iter()
.filter(|s| s.index_hidden_bytes > 0)
.count(),
report
.segments
.iter()
.filter(|s| s.dead_indexed_bytes > 0)
.count(),
);
for s in &report.segments {
if s.index_hidden_bytes > 0 || s.dead_indexed_bytes > 0 {
println!(
" seg {:>3}: file {:>10} live {:>10} dead_idx {:>10} hidden {:>10} unindexed {:>10} phys_ratio {:.2} idx_ratio {:.2}",
s.seq,
s.file_bytes,
s.live_bytes,
s.dead_indexed_bytes,
s.index_hidden_bytes,
s.unindexed_bytes,
s.physical_live_ratio(),
s.index_live_ratio(),
);
}
}
}
#[test]
fn full_compaction_converges_real_tree() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let files = source_tree_files(root).unwrap();
let dir = TempDir::new().unwrap();
let store = create_store(&dir);
let tree = write_tree(&store, &files);
crate::optimizer::background::shared_dict_pass(&store, OptimizeOptions::default(), None)
.unwrap();
crate::optimizer::background::model_bundle_pass(&store, OptimizeOptions::default(), None)
.unwrap();
verify_bytes(&store, &tree);
let logical = store.logical_bytes().unwrap();
let report_pre = crate::store::physical::physical_report(&store).unwrap();
let backing_pre = report_pre.file_bytes;
let reclaimed_gc = crate::store::gc::collect(&store, &CrashHooks::none()).unwrap();
let backing_after_gc = crate::store::physical::physical_report(&store)
.unwrap()
.file_bytes;
verify_bytes(&store, &tree);
println!(
"gc: reclaimed {reclaimed_gc} B; backing {} -> {} B",
backing_pre, backing_after_gc
);
assert!(
backing_after_gc < backing_pre,
"threshold GC must reclaim physical garbage: {backing_after_gc} >= {backing_pre}"
);
let reachable_before = reachable(&store);
let reclaimed = crate::store::gc::compact_full(&store, &CrashHooks::none()).unwrap();
let report_after = crate::store::physical::physical_report(&store).unwrap();
let backing_after = report_after.file_bytes;
let reachable_after = reachable(&store);
verify_bytes(&store, &tree);
println!(
"compact: reclaimed {reclaimed} B; backing {} -> {} B; reachable {} -> {} B",
backing_after_gc, backing_after, reachable_before, reachable_after
);
assert_eq!(
reachable_before, reachable_after,
"compaction must not change the reachable set"
);
assert!(
backing_after <= backing_after_gc,
"full compaction must not grow the backing store: {backing_after} > {backing_after_gc}"
);
let overhead = backing_after.saturating_sub(reachable_after);
let logical_fraction = overhead as f64 / logical.max(1) as f64;
println!(
"post-compact overhead: {overhead} B = {:.2}% of logical ({logical} B)",
100.0 * logical_fraction
);
assert!(
overhead < 192 * 1024,
"post-compaction backing must converge to reachable + bounded overhead: {overhead} B"
);
let report_second = crate::store::physical::physical_report(&store).unwrap();
let reclaimed2 = crate::store::gc::compact_full(&store, &CrashHooks::none()).unwrap();
let report_second2 = crate::store::physical::physical_report(&store).unwrap();
println!(
"second compact reclaimed {reclaimed2} B; backing {} -> {} B",
report_second.file_bytes, report_second2.file_bytes
);
assert!(
reclaimed2 < 4096,
"second compaction must be essentially idempotent: reclaimed {reclaimed2}"
);
assert_eq!(
report_second2.file_bytes, report_second.file_bytes,
"backing must be stable after the second compaction"
);
assert_eq!(report_second2.unexplained(), 0);
drop(store);
let store2 = Store::open(dir.path(), &StoreConfig::default()).unwrap();
verify_bytes(&store2, &tree);
let report = crate::fsck::fsck(dir.path(), &crate::fsck::FsckOptions::default()).unwrap();
assert!(report.is_clean(), "fsck: {}", report.render());
}
#[test]
fn compact_preserves_snapshot_roots() {
let dir = TempDir::new().unwrap();
let store = create_store(&dir);
let d = store
.create_entry(
store.current_root().root_dir_ino,
b"docs",
NewEntry::dir(0o755, 1000, 1000),
&CrashHooks::none(),
)
.unwrap();
let ino = store
.create_entry(
d,
b"v1.txt",
NewEntry::file(0o644, 1000, 1000),
&CrashHooks::none(),
)
.unwrap();
let mut body = Vec::new();
for i in 0..3000u64 {
body.extend_from_slice(format!("record {i}: value {i:x}\n").as_bytes());
}
store.write_region(ino, 0, &body).unwrap();
let _snap1 = store.create_snapshot(b"v1", &CrashHooks::none()).unwrap();
let mut body2 = Vec::new();
for i in 0..3000u64 {
body2.extend_from_slice(format!("entry {i}: value {:x}\n", i * 7).as_bytes());
}
store.write_region(ino, 0, &body2).unwrap();
crate::store::gc::compact_full(&store, &CrashHooks::none()).unwrap();
store
.restore_snapshot(b"v1", &CrashHooks::none())
.expect("snapshot must survive compaction");
let back = store.read_file(ino, 0, body.len() as u64).unwrap();
assert_eq!(back, body, "snapshot restore must be byte-exact");
drop(store);
let store2 = Store::open(dir.path(), &StoreConfig::default()).unwrap();
assert!(
store2
.list_snapshots()
.unwrap()
.iter()
.any(|(name, _)| name == b"v1")
);
let report = crate::fsck::fsck(dir.path(), &crate::fsck::FsckOptions::default()).unwrap();
assert!(report.is_clean(), "fsck: {}", report.render());
}