use super::*;
fn a_report() -> RepairReport {
RepairReport {
recovered: 3,
salvaged: 1,
unreadable: 1,
unreadable_files: vec![(PathBuf::from("/db/tables/7"), "bad footer".into())],
excluded_files: vec![(PathBuf::from("/db/tables/4"), "covered by 9".into())],
lost_coverage: vec![
(
PathBuf::from("/db/tables/7"),
UserKey::from(&b"a"[..]),
UserKey::from(&b"m"[..]),
Some(42),
),
(
PathBuf::from("/db/tables/8"),
UserKey::from(&b"n"[..]),
UserKey::from(&b"z"[..]),
None,
),
],
unknowable_losses: vec![PathBuf::from("/db/tables/11")],
blob_files_salvaged: vec![(PathBuf::from("/db/blobs/2"), "salvaged".into())],
damaged_dictionaries: vec![(PathBuf::from("/db/dicts/5.damaged"), "moved".into())],
method: crate::repair::REPAIR_METHOD,
warnings: vec![
crate::repair::WARN_ALL_AT_L0,
crate::repair::WARN_BLOB_FRAGMENTATION,
],
id: 0x1234_5678_9abc_def0_0fed_cba9_8765_4321,
includes: vec![7, 9],
}
}
#[test]
fn an_encoded_report_decodes_as_itself() {
let report = a_report();
let back = decode_owed(&encode(&report), Path::new("/db"));
assert_eq!(format!("{back:?}"), format!("{report:?}"));
}
#[test]
fn a_damaged_report_leaves_the_loss_unscoped() {
let mut bytes = encode(&a_report());
if let Some(byte) = bytes.get_mut(12) {
*byte ^= 0xFF;
}
let back = decode_owed(&bytes, Path::new("/db"));
assert_eq!(back.wal_replay_scope(), crate::WalReplayScope::FullHistory);
assert_eq!(back.unknowable_losses, vec![PathBuf::from("/db")]);
assert_eq!(back.warnings, vec![crate::repair::WARN_REPORT_COPY_DAMAGED]);
}
#[test]
fn damaged_reports_keep_distinct_ids() {
let damaged = |flip: usize| {
let mut bytes = encode(&a_report());
if let Some(byte) = bytes.get_mut(flip) {
*byte ^= 0xFF;
}
bytes
};
let first = decode_owed(&damaged(12), Path::new("/db"));
let other = decode_owed(&damaged(13), Path::new("/db"));
let repeat = decode_owed(&damaged(12), Path::new("/db"));
assert_ne!(first.id, other.id);
assert_eq!(first.id, repeat.id);
}
fn sealed(mut body: Vec<u8>) -> Vec<u8> {
let checksum = crate::hash::hash64(&body);
put_u64(&mut body, checksum);
body
}
fn body_of(report: &RepairReport) -> Vec<u8> {
let mut encoded = encode(report);
encoded.truncate(encoded.len() - CHECKSUM_LEN);
encoded
}
#[test]
fn a_checksummed_report_of_another_shape_is_refused() {
let report = RepairReport {
includes: Vec::new(),
..a_report()
};
assert!(
decode(&sealed(body_of(&report))).is_some(),
"self-check: the untouched body decodes"
);
let mut other_magic = b"LSMRPRT2".to_vec();
other_magic.extend(body_of(&report).iter().skip(MAGIC.len()));
assert!(decode(&sealed(other_magic)).is_none(), "another magic");
let mut trailing = body_of(&report);
trailing.push(0);
assert!(
decode(&sealed(trailing)).is_none(),
"bytes after the last field"
);
let mut unknown_warning = body_of(&report);
let last_warning = unknown_warning.len() - 16 - 8 - 1;
let Some(last) = unknown_warning.get_mut(last_warning) else {
panic!("self-check: the body holds the warnings");
};
*last = u8::MAX;
assert!(
decode(&sealed(unknown_warning)).is_none(),
"an unknown warning"
);
let one_loss = RepairReport {
recovered: 0,
salvaged: 0,
unreadable: 0,
unreadable_files: Vec::new(),
excluded_files: Vec::new(),
lost_coverage: vec![(
PathBuf::from("/p"),
UserKey::from(&b"a"[..]),
UserKey::from(&b"b"[..]),
None,
)],
unknowable_losses: Vec::new(),
blob_files_salvaged: Vec::new(),
damaged_dictionaries: Vec::new(),
method: crate::repair::REPAIR_METHOD,
warnings: Vec::new(),
id: 0,
includes: Vec::new(),
};
let mut bad_tag = body_of(&one_loss);
let tag_at = MAGIC.len() + 8 + 8 + 8 + 8 + 8 + (8 + 2) + (8 + 1) + (8 + 1);
let Some(tag) = bad_tag.get_mut(tag_at) else {
panic!("self-check: the body holds the bound tag");
};
assert_eq!(*tag, 0, "self-check: the bound tag");
*tag = 2;
assert!(
decode(&sealed(bad_tag)).is_none(),
"a bound tag outside 0/1"
);
}
#[test]
fn reports_of_two_versions_get_two_ids() {
let report = a_report();
assert_ne!(id_for(&report, 9, None), id_for(&report, 10, None));
}
#[test]
#[expect(clippy::expect_used, reason = "test code")]
fn folding_keeps_every_distinct_loss_under_one_path() {
let narrower = (
PathBuf::from("/db/tables/7"),
UserKey::from(&b"c"[..]),
UserKey::from(&b"d"[..]),
Some(10),
);
let mut later = RepairReport {
recovered: 1,
salvaged: 1,
unreadable: 0,
unreadable_files: Vec::new(),
excluded_files: Vec::new(),
lost_coverage: vec![narrower.clone()],
unknowable_losses: Vec::new(),
blob_files_salvaged: vec![(PathBuf::from("/db/blobs/2"), "again".into())],
damaged_dictionaries: Vec::new(),
method: crate::repair::REPAIR_METHOD,
warnings: Vec::new(),
id: 1,
includes: Vec::new(),
};
let earlier = a_report();
let wider = earlier
.lost_coverage
.first()
.cloned()
.expect("the earlier report names /db/tables/7");
crate::repair::fold_report(&mut later, earlier);
assert!(later.lost_coverage.contains(&narrower));
assert!(
later.lost_coverage.contains(&wider),
"the earlier, wider loss of the same path survives"
);
assert_eq!(
later.blob_files_salvaged,
vec![
(PathBuf::from("/db/blobs/2"), "again".into()),
(PathBuf::from("/db/blobs/2"), "salvaged".into()),
],
);
let mut twice = a_report();
crate::repair::fold_report(&mut twice, a_report());
assert_eq!(twice.lost_coverage, a_report().lost_coverage);
assert_eq!(twice.unreadable, 1);
}
#[test]
fn folding_adds_the_earlier_losses_once() {
let mut later = RepairReport {
recovered: 5,
salvaged: 0,
unreadable: 0,
unreadable_files: Vec::new(),
excluded_files: Vec::new(),
lost_coverage: Vec::new(),
unknowable_losses: vec![PathBuf::from("/db/tables/11")],
blob_files_salvaged: vec![(PathBuf::from("/db/blobs/3"), "later".into())],
damaged_dictionaries: Vec::new(),
method: crate::repair::REPAIR_METHOD,
warnings: vec![crate::repair::WARN_ALL_AT_L0],
id: 1,
includes: vec![9],
};
let earlier = a_report();
let earlier_id = earlier.id;
crate::repair::fold_report(&mut later, earlier);
assert_eq!(later.recovered, 5);
assert_eq!(later.salvaged, 0);
assert_eq!(later.unreadable, 1);
assert_eq!(later.lost_coverage.len(), 2);
assert_eq!(
later.unknowable_losses,
vec![PathBuf::from("/db/tables/11")],
"a path named twice is listed once"
);
assert_eq!(
later
.blob_files_salvaged
.iter()
.map(|(p, _)| p.clone())
.collect::<Vec<_>>(),
vec![PathBuf::from("/db/blobs/3"), PathBuf::from("/db/blobs/2")],
);
assert_eq!(
later.warnings,
vec![
crate::repair::WARN_ALL_AT_L0,
crate::repair::WARN_BLOB_FRAGMENTATION
],
);
assert_eq!(later.id, 1, "the later report keeps its own id");
assert_eq!(later.includes, vec![9, earlier_id, 7]);
}