use std::sync::Arc;
use super::backup_check::{
Findings, check_continuity, check_graph_files, check_marks, check_segment, check_snapshot,
};
use super::files::{DirPath, GRAPH_COPY, GRAPH_FILE, read_graph_file};
use super::*;
use crate::persist::MemDir;
use crate::persist::manifest::FIXED_BYTES;
fn root(dir: &MemDir) -> DirPath {
DirPath::root(Arc::new(dir.clone()))
}
fn store() -> MemDir {
let dir = MemDir::new();
let options = StoreOptions::new()
.with_wal_segment_bytes(150)
.with_checkpoint_after_bytes(0);
let store = Store::create(&dir, options).unwrap();
for round in 0..12 {
store
.graph()
.write()
.traversal_mut()
.add_v("person")
.property("name", format!("p{round}"))
.to_list()
.unwrap();
if round == 2 {
store.checkpoint(None).unwrap();
}
}
store.close().unwrap();
dir
}
fn snapshots(dir: &MemDir) -> Vec<String> {
let mut names: Vec<String> = dir
.file_names()
.into_iter()
.filter(|n| n.starts_with("snapshots/") && n.ends_with("/manifest"))
.map(|n| n.trim_end_matches("/manifest").to_string())
.collect();
names.sort();
names
}
fn wal(dir: &MemDir) -> Vec<(u64, String)> {
let mut out: Vec<(u64, String)> = dir
.file_names()
.into_iter()
.filter_map(|n| {
let first = n.strip_prefix("wal/")?.strip_suffix(".wal")?.parse().ok()?;
Some((first, n))
})
.collect();
out.sort();
out
}
#[test]
fn a_manifest_fixed_copy_is_healed_in_the_store_and_damage_in_a_copy() {
let dir = store();
let newest = snapshots(&dir).pop().unwrap();
let name = format!("{newest}/manifest");
let bytes = dir.file(&name).unwrap();
let tail_at = bytes.len() - FIXED_BYTES;
for (label, at) in [("head", 40), ("tail", tail_at + 40)] {
let image = dir.deep_copy();
let mut damaged = bytes.clone();
damaged[at] ^= 1;
image.put_file(&name, damaged);
let snapshot = root(&image).join(&newest);
let mut findings = Findings::default();
let healed = check_snapshot(&root(&image), &snapshot, true, &mut findings);
assert!(findings.is_ok(), "{label}: {:?}", findings.problems);
assert_eq!(findings.warnings.len(), 1, "{label}");
assert_eq!(healed.as_deref(), Some(bytes.as_slice()), "{label}");
let mut strict = Findings::default();
assert!(check_snapshot(&root(&image), &snapshot, false, &mut strict).is_none());
assert_eq!(strict.problems.len(), 1, "{label}");
assert!(
strict.problems[0].reason.starts_with(label),
"{:?}",
strict.problems
);
}
let older = snapshots(&dir)[0].clone();
let other = dir.file(&format!("{older}/manifest")).unwrap();
let mut mixed = bytes.clone();
mixed[tail_at..].copy_from_slice(&other[..FIXED_BYTES]);
let image = dir.deep_copy();
image.put_file(&name, mixed);
let mut findings = Findings::default();
let healed = check_snapshot(
&root(&image),
&root(&image).join(&newest),
true,
&mut findings,
);
assert!(healed.is_none());
assert!(
findings.problems[0].reason.contains("differ"),
"{:?}",
findings.problems
);
let image = dir.deep_copy();
let segment = image
.file_names()
.into_iter()
.find(|n| n.starts_with(&newest) && n.ends_with(".seg"))
.unwrap();
let kept = MemDir::new();
for (n, b) in super::merge_tests::listing(&image) {
if n != segment {
kept.put_file(&n, b);
}
}
let mut findings = Findings::default();
check_snapshot(
&root(&kept),
&root(&kept).join(&newest),
true,
&mut findings,
);
assert_eq!(findings.problems.len(), 1);
}
#[test]
fn wal_segments_under_another_name_of_another_lineage_or_with_early_commits_are_damage() {
let dir = store();
let graph = read_graph_file(&root(&dir)).unwrap().file;
let segments = wal(&dir);
assert!(segments.len() >= 3, "{segments:?}");
let (first, name) = segments[1].clone();
let path = root(&dir).join(&name);
let mut findings = Findings::default();
let check = check_segment(
&root(&dir),
first,
&path,
None,
false,
Some(&graph),
&mut findings,
)
.unwrap();
assert!(findings.is_ok() && !check.commits.is_empty());
let (later, _) = segments[2].clone();
let mut findings = Findings::default();
assert!(
check_segment(
&root(&dir),
later,
&path,
None,
false,
Some(&graph),
&mut findings
)
.is_none()
);
let reasons: Vec<&str> = findings
.problems
.iter()
.map(|p| p.reason.as_str())
.collect();
assert!(
reasons
.iter()
.any(|r| r.contains("its header says first commit")),
"{reasons:?}"
);
assert!(
reasons
.iter()
.any(|r| r.contains("before the segment's first commit")),
"{reasons:?}"
);
let mut foreign = graph.clone();
foreign.lineage[0].graph_id ^= 1;
foreign.graph_id ^= 1;
let mut findings = Findings::default();
assert!(
check_segment(
&root(&dir),
first,
&path,
None,
false,
Some(&foreign),
&mut findings
)
.is_none()
);
assert!(
findings.problems[0].reason.contains("lineage"),
"{:?}",
findings.problems
);
let mut findings = Findings::default();
let missing = root(&dir).join("wal/00000000000000099999.wal");
assert!(
check_segment(
&root(&dir),
99_999,
&missing,
None,
false,
None,
&mut findings
)
.is_none()
);
assert_eq!(findings.problems.len(), 1);
let image = dir.deep_copy();
image.put_file(&name, dir.file(&name).unwrap()[..10].to_vec());
let mut findings = Findings::default();
let short = root(&image).join(&name);
assert!(
check_segment(
&root(&image),
first,
&short,
None,
false,
None,
&mut findings
)
.is_none()
);
assert!(
findings.problems[0].reason.contains("truncated"),
"{:?}",
findings.problems
);
let mut torn = dir.file(&name).unwrap();
let complete = torn.len() as u64;
torn.extend_from_slice(&[0x57, 0x53, 0x52]);
image.put_file(&name, torn);
let mut findings = Findings::default();
assert!(
check_segment(
&root(&image),
first,
&short,
None,
false,
None,
&mut findings
)
.is_none()
);
let mut findings = Findings::default();
let check = check_segment(
&root(&image),
first,
&short,
None,
true,
None,
&mut findings,
)
.unwrap();
assert_eq!(check.end, complete);
}
#[test]
fn a_gap_in_the_copied_commits_is_damage() {
let dir = store();
let graph = read_graph_file(&root(&dir)).unwrap().file;
let segments = wal(&dir);
let mut checks = Vec::new();
for (first, name) in &segments {
let mut findings = Findings::default();
let path = root(&dir).join(name);
checks.push((
*first,
check_segment(
&root(&dir),
*first,
&path,
None,
false,
Some(&graph),
&mut findings,
)
.unwrap(),
));
}
let all: Vec<(u64, &super::backup_check::SegmentCheck)> =
checks.iter().map(|(f, c)| (*f, c)).collect();
let mut findings = Findings::default();
check_continuity(&all, Some(0), &mut findings);
assert!(findings.is_ok(), "{:?}", findings.problems);
let gap: Vec<(u64, &super::backup_check::SegmentCheck)> = all
.iter()
.enumerate()
.filter(|(i, _)| *i != 1)
.map(|(_, c)| *c)
.collect();
let mut findings = Findings::default();
check_continuity(&gap, Some(0), &mut findings);
assert_eq!(findings.problems.len(), 1, "{:?}", findings.problems);
let mut findings = Findings::default();
check_continuity(&all[1..], Some(0), &mut findings);
assert_eq!(findings.problems.len(), 1, "{:?}", findings.problems);
}
#[test]
fn graph_files_and_marks_damage_and_warnings() {
let dir = store();
let image = dir.deep_copy();
for name in [GRAPH_FILE, GRAPH_COPY] {
let mut bytes = image.file(name).unwrap();
bytes[20] ^= 1;
image.put_file(name, bytes);
}
let mut findings = Findings::default();
assert!(
check_graph_files(&root(&image), &mut findings)
.unwrap()
.is_none()
);
assert_eq!(findings.problems.len(), 2);
let image = dir.deep_copy();
let mut longer = read_graph_file(&root(&image)).unwrap().file;
let mut entry = longer.lineage[0];
entry.branched_at = 3;
entry.graph_id ^= 7;
longer.lineage.insert(0, entry);
longer.graph_id = entry.graph_id;
image.put_file(GRAPH_FILE, longer.encode());
let mut findings = Findings::default();
let used = check_graph_files(&root(&image), &mut findings)
.unwrap()
.unwrap();
assert_eq!(used.lineage.len(), 2);
let image = dir.deep_copy();
image.put_file(GRAPH_COPY, b"garbage".to_vec());
let mut findings = Findings::default();
assert!(
check_graph_files(&root(&image), &mut findings)
.unwrap()
.is_some()
);
assert!(findings.is_ok() && findings.warnings.len() == 1);
#[cfg(not(target_arch = "wasm32"))]
{
let temp = std::env::temp_dir().join(format!("bk1-marks-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&temp);
std::fs::create_dir_all(temp.join("marks")).unwrap();
let disk = DirPath::root(crate::persist::store_dir_at(&temp));
let mut findings = Findings::default();
assert!(check_marks(&disk, &mut findings).is_none());
assert!(
findings.is_ok() && findings.warnings.len() == 1,
"{findings:?}"
);
std::fs::remove_dir_all(&temp).unwrap();
}
let image = dir.deep_copy();
image.put_file("marks", vec![9, 9, 9]);
let mut findings = Findings::default();
assert!(check_marks(&root(&image), &mut findings).is_none());
assert_eq!(findings.warnings.len(), 1);
}