use pith_digest::sha256;
use pith_text::{canonicalize, jaccard_estimate, signature};
const FIXTURE_SHA256: [(&str, &str); 8] = [
(
"doc_a",
"c1eb9f012dcb813e212beb49012408fb26b4bf3b086c0c81c56a4a89ec7977c9",
),
(
"doc_a_edit",
"4593fba5e4513fdc84591ef31deabea139481252abdc71ec2f14de056f22e773",
),
(
"doc_b",
"5e3f18ad6bf8f6f2a5ebaf63e747d2b95af712439b80348c241c093b3b06e65d",
),
(
"prose",
"754d411b311d9243901a0b45c1fea81368a2b252d906fdcf1b6b882f1d7e16ee",
),
(
"punct_tags",
"24538fa0872b9c19f005c765fb3b37215d87ee67b43c2cbabff5aad89c7efbe3",
),
(
"short_two",
"aaf3183a9fd3134e00b0fdcd1d5fde6394526d371b77827185e328c5d13ee42c",
),
(
"vi_nfc",
"dafaca378c617539d844564c9f22743845feb1ed11ca959bc161167b82a62562",
),
(
"vi_nfd",
"b6d2a058afc1e9e0ce834a1f97bdae5fdd93f74f9ffeae2bc5d8d9c5d45e98b2",
),
];
const FIXTURES: [&str; 8] = [
"doc_a",
"doc_a_edit",
"doc_b",
"prose",
"punct_tags",
"short_two",
"vi_nfc",
"vi_nfd",
];
fn fixture(name: &str) -> &'static str {
match name {
"doc_a" => include_str!("fixtures/doc_a.txt"),
"doc_a_edit" => include_str!("fixtures/doc_a_edit.txt"),
"doc_b" => include_str!("fixtures/doc_b.txt"),
"prose" => include_str!("fixtures/prose.txt"),
"punct_tags" => include_str!("fixtures/punct_tags.txt"),
"short_two" => include_str!("fixtures/short_two.txt"),
"vi_nfc" => include_str!("fixtures/vi_nfc.txt"),
"vi_nfd" => include_str!("fixtures/vi_nfd.txt"),
other => unreachable!("unknown fixture {other}"),
}
}
#[test]
fn fixtures_match_pinned_sha256() {
for (name, expected) in FIXTURE_SHA256 {
let bytes = fixture(name).as_bytes();
assert!(!bytes.is_empty(), "fixture {name} is empty");
let got = hex_lower(sha256(bytes).expect("sha256 of fixture").as_bytes());
assert_eq!(got, expected, "fixture {name} drifted from its pin");
}
}
fn hex_lower(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
s.push_str(&format!("{b:02x}"));
}
s
}
#[test]
fn fixtures_are_deterministic_and_idempotent() {
for name in FIXTURES {
let text = fixture(name);
let sig = signature(text);
assert_eq!(sig.len(), 128, "{name} signature length");
assert_eq!(signature(text), sig, "{name} must be deterministic");
let canon = canonicalize(text);
assert_eq!(canonicalize(&canon), canon, "{name} must be idempotent");
}
}
#[test]
fn vietnamese_nfc_and_nfd_fixtures_agree() {
let nfc_text = fixture("vi_nfc");
let nfd_text = fixture("vi_nfd");
assert_ne!(nfc_text, nfd_text, "the two spellings must differ as bytes");
assert_eq!(canonicalize(nfc_text), canonicalize(nfd_text));
assert_eq!(signature(nfc_text), signature(nfd_text));
}
#[test]
fn near_duplicate_fixture_outscores_unrelated_pair() {
let a = signature(fixture("doc_a"));
let a_edit = signature(fixture("doc_a_edit"));
let b = signature(fixture("doc_b"));
let near = jaccard_estimate(&a, &a_edit);
let far = jaccard_estimate(&a, &b);
assert!(
near > far,
"near-duplicate {near} must beat unrelated {far}"
);
assert!(near > 0.5, "one-edit near-duplicate estimated {near}");
assert_eq!(jaccard_estimate(&a, &a), 1.0);
}
#[test]
fn adversarial_and_tiny_fixtures_stay_distinct() {
let punct = signature(fixture("punct_tags"));
let tiny = signature(fixture("short_two"));
let empty = signature("");
assert_ne!(punct, tiny);
assert_ne!(punct, empty);
assert_ne!(tiny, empty, "a two-word document is not the empty set");
assert_ne!(
tiny,
vec![u64::MAX; 128],
"a two-word document is not empty"
);
}