use std::collections::BTreeMap;
use dendro::archive::{Archive, ArchiveMut, Depth, Problem, SegmentMeta, SourceMeta, WalRow};
fn meta() -> SourceMeta {
SourceMeta {
labels: BTreeMap::from([("source".to_string(), "a".to_string())]),
metadata: BTreeMap::new(),
clock_anchor_wall_ns: 0,
}
}
fn row(ts: i64) -> WalRow {
WalRow {
stream: "s".to_string(),
ts,
wall_offset: 0,
row: vec![1],
}
}
#[test]
fn a_healthy_archive_is_sound_and_counted() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("v.dendro");
let mut db = ArchiveMut::create(&path).unwrap();
let id = db.insert_source(&meta()).unwrap();
db.insert_segment(
id,
"s",
0,
&SegmentMeta {
rows: 2,
first_ts: 1,
last_ts: 2,
},
b"1,2",
)
.unwrap();
db.insert_wal_rows(id, &[row(3), row(4)]).unwrap();
for depth in [Depth::Quick, Depth::Full] {
let report = db.verify(depth).unwrap();
assert!(report.is_sound(), "{:?}", report.problems);
assert_eq!(
(
report.sources,
report.streams,
report.segments,
report.wal_rows
),
(1, 1, 1, 2)
);
}
}
#[test]
fn wal_rows_no_read_path_can_reach_are_reported() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("v.dendro");
let mut db = ArchiveMut::create(&path).unwrap();
let id = db.insert_source(&meta()).unwrap();
db.insert_wal_rows(id, &[row(1), row(2), row(9)]).unwrap();
db.insert_segment(
id,
"s",
0,
&SegmentMeta {
rows: 2,
first_ts: 1,
last_ts: 2,
},
b"1,2",
)
.unwrap();
let report = db.verify(Depth::Quick).unwrap();
assert_eq!(
report.problems,
vec![Problem::UnreadableWalRows {
source_id: id,
stream: "s".to_string(),
rows: 2,
}]
);
assert_eq!(report.wal_rows, 3, "all three are stored; only one is live");
assert!(report.problems[0]
.to_string()
.contains("no read path can reach"));
db.prune_wal(id, "s", 2).unwrap();
assert!(db.verify(Depth::Quick).unwrap().is_sound());
}
#[test]
fn a_self_contradicting_segment_is_reported() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("v.dendro");
let mut db = ArchiveMut::create(&path).unwrap();
let id = db.insert_source(&meta()).unwrap();
db.insert_segment(
id,
"backwards",
0,
&SegmentMeta {
rows: 1,
first_ts: 100,
last_ts: 1,
},
b"x",
)
.unwrap();
db.insert_segment(
id,
"empty",
0,
&SegmentMeta {
rows: 0,
first_ts: 1,
last_ts: 1,
},
b"x",
)
.unwrap();
let report = db.verify(Depth::Quick).unwrap();
assert_eq!(report.problems.len(), 2, "{:?}", report.problems);
let text: Vec<String> = report.problems.iter().map(|p| p.to_string()).collect();
assert!(
text.iter().any(|t| t.contains("runs backwards")),
"{text:?}"
);
assert!(
text.iter().any(|t| t.contains("claims no rows")),
"{text:?}"
);
}
#[test]
fn damage_inside_the_file_is_found_at_either_depth() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("v.dendro");
{
let mut db = ArchiveMut::create(&path).unwrap();
let id = db.insert_source(&meta()).unwrap();
for seq in 0..40u64 {
db.insert_segment(
id,
"s",
seq,
&SegmentMeta {
rows: 1,
first_ts: seq as i64,
last_ts: seq as i64,
},
&vec![seq as u8; 8192],
)
.unwrap();
}
db.checkpoint_passive().unwrap();
}
let mut bytes = std::fs::read(&path).unwrap();
let middle = bytes.len() / 2;
for b in &mut bytes[middle..middle + 4096] {
*b ^= 0xff;
}
std::fs::write(&path, &bytes).unwrap();
let db = Archive::open(&path).unwrap();
let report = db.verify(Depth::Full).unwrap();
assert!(
report
.problems
.iter()
.any(|p| matches!(p, Problem::Corrupt(_))),
"the deep check must notice a scribbled page: {:?}",
report.problems
);
let quick = db.verify(Depth::Quick).unwrap();
assert!(
quick
.problems
.iter()
.any(|p| matches!(p, Problem::Corrupt(_))),
"the cheap pass walks the database too, and must not miss this: {:?}",
quick.problems
);
}
#[test]
fn damage_on_any_page_is_reported_not_returned_as_an_error() {
let dir = tempfile::tempdir().unwrap();
let clean = dir.path().join("clean.dendro");
{
let mut db = ArchiveMut::create(&clean).unwrap();
let id = db.insert_source(&meta()).unwrap();
for seq in 0..40u64 {
db.insert_segment(
id,
"s",
seq,
&SegmentMeta {
rows: 1,
first_ts: seq as i64,
last_ts: seq as i64,
},
&vec![seq as u8; 8192],
)
.unwrap();
}
db.checkpoint_passive().unwrap();
}
let bytes = std::fs::read(&clean).unwrap();
let pages = bytes.len() / 4096;
let damaged = dir.path().join("damaged.dendro");
for page in 1..pages {
let mut copy = bytes.clone();
for b in &mut copy[page * 4096..(page + 1) * 4096] {
*b ^= 0xff;
}
std::fs::write(&damaged, ©).unwrap();
let Ok(db) = Archive::open(&damaged) else {
continue;
};
for depth in [Depth::Quick, Depth::Full] {
if let Err(e) = db.verify(depth) {
panic!("damage on page {page} of {pages} made verify({depth:?}) fail: {e}");
}
}
}
}