quick_search/crates/quicksearch-core/src/verify_tests.rs

282 lines
9.3 KiB
Rust
Raw Normal View History

use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use super::*;
use crate::testutil::{scratch_dir, touch};
/// Run to completion with cancellation switched off, returning every update.
fn run(paths: &[PathBuf]) -> Vec<VerifyUpdate> {
let cancel = AtomicBool::new(false);
let mut seen = Vec::new();
verify_identical(paths, &cancel, &mut |u| seen.push(u));
seen
}
/// The report from a run, asserting it produced exactly one terminal update
/// and that the update was `Done`.
fn report(paths: &[PathBuf]) -> VerifyReport {
let seen = run(paths);
let terminal: Vec<&VerifyUpdate> = seen
.iter()
.filter(|u| !matches!(u, VerifyUpdate::Progress { .. }))
.collect();
assert_eq!(terminal.len(), 1, "expected one terminal update: {seen:?}");
match terminal[0] {
VerifyUpdate::Done(r) => r.clone(),
other => panic!("expected Done, got {other:?}"),
}
}
/// `n` files in a fresh directory, each with the body it is given.
fn files(tag: &str, bodies: &[&[u8]]) -> Vec<PathBuf> {
let dir = scratch_dir(tag);
bodies
.iter()
.enumerate()
.map(|(i, body)| {
let p = dir.join(format!("copy{i}.bin"));
touch(&p, body);
p
})
.collect()
}
#[test]
fn identical_files_all_report_identical() {
for count in [2, 3] {
let bodies = vec![&b"the same bytes in every copy"[..]; count];
let paths = files("verify-same", &bodies);
let r = report(&paths);
assert_eq!(r.reference, Some(0));
assert!(r.all_identical(), "{r:?}");
assert_eq!(r.differing(), 0);
// Every file was read through: the head hash alone would not do.
assert_eq!(r.bytes_read, 28 * count as u64);
}
}
#[test]
fn a_difference_is_reported_at_its_offset() {
// First byte, mid-file, and the very last byte: the last is the one a
// head-only hash can never see, and the reason this module exists.
for (label, a, b, at) in [
("first", &b"Xbcdefgh"[..], &b"abcdefgh"[..], 0),
("middle", &b"abcdefgh"[..], &b"abcXefgh"[..], 3),
("last", &b"abcdefgh"[..], &b"abcdefgX"[..], 7),
] {
let paths = files("verify-diff", &[a, b]);
let r = report(&paths);
assert_eq!(r.verdicts[0], MemberVerdict::Identical, "{label}");
assert_eq!(r.verdicts[1], MemberVerdict::DiffersAt(at), "{label}");
assert!(!r.all_identical(), "{label}");
assert_eq!(r.differing(), 1, "{label}");
}
}
/// The realistic false positive: same size, same head, different tail — a
/// pre-allocated disk image, which is what `[processing] hash_length`
/// documents as the known limitation.
#[test]
fn a_shared_head_with_a_different_tail_is_caught() {
let head = vec![0u8; 64 * 1024];
let mut a = head.clone();
let mut b = head;
a.extend_from_slice(b"footer-a");
b.extend_from_slice(b"footer-b");
let paths = files("verify-tail", &[a.as_slice(), b.as_slice()]);
let r = report(&paths);
assert_eq!(r.verdicts[1], MemberVerdict::DiffersAt(64 * 1024 + 7));
}
/// Bigger than one chunk, so the multi-chunk path and the running offset are
/// both exercised rather than assumed.
#[test]
fn a_difference_past_the_first_chunk_is_found() {
// Two files, so the chunk is the 256 KiB ceiling and the difference sits
// in the second one.
let size = MAX_CHUNK * 2 + 1234;
let a = vec![7u8; size];
let mut b = a.clone();
let at = MAX_CHUNK + 500;
b[at] = 8;
let paths = files("verify-chunks", &[a.as_slice(), b.as_slice()]);
let r = report(&paths);
assert_eq!(r.verdicts[1], MemberVerdict::DiffersAt(at as u64));
// Two chunks out of each file and then it stops: with nothing left to
// compare against, reading the remainder of the reference would be work
// that cannot change the answer.
assert_eq!(
r.bytes_read,
4 * MAX_CHUNK as u64,
"did not stop once the last live file dropped out"
);
}
#[test]
fn different_lengths_are_decided_without_reading() {
let paths = files("verify-len", &[b"abcdefgh", b"abcdefghij"]);
let r = report(&paths);
assert_eq!(
r.verdicts[1],
MemberVerdict::LengthDiffers {
len: 10,
reference_len: 8
}
);
assert_eq!(r.bytes_read, 0, "a length mismatch reads nothing");
}
#[test]
fn empty_files_are_identical() {
let paths = files("verify-empty", &[b"", b""]);
let r = report(&paths);
assert!(r.all_identical(), "{r:?}");
assert_eq!(r.bytes_read, 0);
}
#[test]
fn a_missing_member_is_unreadable_and_the_rest_still_compare() {
let mut paths = files("verify-missing", &[b"same", b"same"]);
paths.insert(1, PathBuf::from("/nonexistent/quicksearch-verify-missing"));
let r = report(&paths);
assert_eq!(r.reference, Some(0));
assert!(matches!(r.verdicts[1], MemberVerdict::Unreadable(_)));
assert_eq!(
r.verdicts[2],
MemberVerdict::Identical,
"an unreadable member stopped the others being compared"
);
assert_eq!(r.differing(), 1);
}
/// The first path is the obvious reference, but not a required one: an
/// unreadable first member must not sink the whole run.
#[test]
fn the_reference_falls_through_to_the_first_readable_member() {
let mut paths = files("verify-refmissing", &[b"same", b"same"]);
paths.insert(0, PathBuf::from("/nonexistent/quicksearch-verify-ref"));
let r = report(&paths);
assert_eq!(r.reference, Some(1));
assert!(matches!(r.verdicts[0], MemberVerdict::Unreadable(_)));
assert_eq!(r.verdicts[1], MemberVerdict::Identical);
assert_eq!(r.verdicts[2], MemberVerdict::Identical);
}
#[test]
fn nothing_readable_reports_no_reference() {
let paths = vec![
PathBuf::from("/nonexistent/quicksearch-verify-a"),
PathBuf::from("/nonexistent/quicksearch-verify-b"),
];
let r = report(&paths);
assert_eq!(r.reference, None);
assert_eq!(r.verdicts.len(), 2);
assert!(r.verdicts.iter().all(|v| !v.is_identical()));
}
#[test]
fn a_single_file_and_an_empty_set_are_vacuously_identical() {
let paths = files("verify-one", &[b"alone"]);
let r = report(&paths);
assert_eq!(r.reference, Some(0));
assert!(r.all_identical());
assert_eq!(r.bytes_read, 0, "nothing to compare it against");
let r = report(&[]);
assert_eq!(r.reference, None);
assert!(r.verdicts.is_empty());
assert!(r.all_identical());
}
#[test]
fn a_run_cancelled_before_it_starts_reports_only_that() {
let paths = files("verify-cancel", &[b"same", b"same"]);
let cancel = AtomicBool::new(true);
let mut seen = Vec::new();
verify_identical(&paths, &cancel, &mut |u| seen.push(u));
assert_eq!(seen, vec![VerifyUpdate::Cancelled]);
}
/// Cancelling part way through ends the run there, with no `Done` claiming a
/// verdict it never reached. The first chunk always reports progress, so the
/// flag goes up between two chunks rather than at a time the test has to race
/// for.
#[test]
fn cancelling_mid_run_ends_it_without_a_verdict() {
let body = vec![3u8; MAX_CHUNK * 4];
let paths = files("verify-cancel-mid", &[body.as_slice(), body.as_slice()]);
let cancel = AtomicBool::new(false);
let mut seen = Vec::new();
verify_identical(&paths, &cancel, &mut |u| {
cancel.store(true, Ordering::Relaxed);
seen.push(u);
});
assert!(
matches!(seen.first(), Some(VerifyUpdate::Progress { .. })),
"the first chunk did not report progress: {seen:?}"
);
assert_eq!(seen.last(), Some(&VerifyUpdate::Cancelled), "{seen:?}");
assert!(
!seen.iter().any(|u| matches!(u, VerifyUpdate::Done(_))),
"a cancelled run still reported a verdict: {seen:?}"
);
}
/// Whatever progress reports, it is a fraction that makes sense: monotonic,
/// and never past its own denominator.
#[test]
fn progress_climbs_and_never_overruns_its_denominator() {
let body = vec![9u8; MAX_CHUNK * 6];
let paths = files(
"verify-progress",
&[body.as_slice(), body.as_slice(), body.as_slice()],
);
let mut last = 0;
let mut count = 0;
for update in run(&paths) {
if let VerifyUpdate::Progress {
bytes_read,
bytes_total,
} = update
{
assert!(
bytes_read <= bytes_total,
"{bytes_read} read of {bytes_total}"
);
assert!(bytes_read >= last, "progress went backwards");
last = bytes_read;
count += 1;
}
}
assert!(count > 0, "a six-chunk comparison reported no progress");
}
/// A directory is not a file this can compare, however the platform refuses
/// it — `File::open` fails outright on Windows, while on Linux it opens and
/// then refuses to be read. Either way it is that member's problem, not the
/// run's.
#[test]
fn an_unreadable_member_does_not_stop_the_run() {
let dir = scratch_dir("verify-dir");
let a = dir.join("a.bin");
let b = dir.join("b.bin");
touch(&a, b"identical bytes");
touch(&b, b"identical bytes");
let sub = dir.join("subdir");
std::fs::create_dir_all(&sub).unwrap();
let r = report(&[a, sub, b]);
assert_eq!(r.reference, Some(0));
assert!(
!r.verdicts[1].is_identical(),
"a directory was called an identical file"
);
assert_eq!(r.verdicts[2], MemberVerdict::Identical);
}