use super::*;
use std::fs;
use tempfile::TempDir;
struct Fixed(u64);
impl Rng for Fixed {
fn next_u64(&mut self) -> u64 {
self.0
}
}
fn set(names: &[&str]) -> HashSet<String> {
names.iter().map(|s| (*s).to_owned()).collect()
}
const ABC: &[&str] = &["a", "b", "c"];
type Case = (
&'static [&'static str],
u64,
&'static [&'static str],
Result<&'static str, MintError>,
);
#[test]
fn mint_scans_from_the_draw_and_retries_past_collisions() {
let cases: &[Case] = &[
(ABC, 0, &[], Ok("a-b")),
(ABC, 4, &[], Ok("c-a")),
(ABC, 10, &[], Ok("c-a")),
(ABC, 0, &["a-b"], Ok("a-c")),
(ABC, 5, &["c-b", "a-b"], Ok("a-c")),
(
&["a", "b"],
0,
&["a-b", "b-a"],
Err(MintError::Exhausted(2)),
),
(&["solo"], 0, &[], Err(MintError::Exhausted(0))),
];
for (words, draw, taken, expect) in cases {
let got = mint_from(words, &mut Fixed(*draw), &set(taken));
let want = expect.clone().map(str::to_owned);
assert_eq!(got, want, "words={words:?} draw={draw} taken={taken:?}");
}
}
#[test]
fn exhaustion_error_names_the_pool_size() {
let err = MintError::Exhausted(2);
assert_eq!(
err.to_string(),
"name pool exhausted: all 2 two-word combinations are occupied"
);
}
#[test]
fn embedded_wordlist_holds_its_invariants() {
let words = wordlist();
assert!(!words.iter().any(|w| w.starts_with('#') || w.is_empty()));
for w in &words {
assert!(
(3..=9).contains(&w.len()) && w.chars().all(|c| c.is_ascii_lowercase()),
"{w:?} violates ^[a-z]{{3,9}}$"
);
}
let unique: HashSet<&&str> = words.iter().collect();
assert_eq!(unique.len(), words.len(), "duplicate word in words.txt");
assert!(!unique.contains(&"unknown"));
assert_eq!(words.len(), 7395);
assert_eq!(words.len() * (words.len() - 1), 54_678_630);
}
#[test]
fn pair_indexing_is_a_bijection_onto_the_pool() {
for words in [ABC, &["a", "b", "c", "d"][..]] {
let pool = words.len() * (words.len() - 1);
let all: HashSet<String> = (0..pool).map(|i| pair(words, i)).collect();
assert_eq!(all.len(), pool, "pair() is not injective over {words:?}");
for name in &all {
let (a, b) = name.split_once('-').unwrap();
assert_ne!(a, b, "a word paired with itself");
assert!(words.contains(&a) && words.contains(&b), "{name}");
}
}
let words = wordlist();
let pool = words.len() * (words.len() - 1);
for idx in [0, 1, pool / 2, pool - 1] {
let name = pair(&words, idx);
let (a, b) = name.split_once('-').unwrap();
assert_ne!(a, b);
assert!(words.contains(&a) && words.contains(&b), "{name} at {idx}");
}
}
#[test]
fn mint_over_the_embedded_list_avoids_the_occupied_set() {
let name = mint(&mut SplitMix64::from_seed(7), &HashSet::new()).unwrap();
let again = mint(&mut SplitMix64::from_seed(7), &HashSet::new()).unwrap();
assert_eq!(name, again);
assert!(name.contains('-'));
let next = mint(&mut SplitMix64::from_seed(7), &set(&[&name])).unwrap();
assert_ne!(next, name);
}
#[test]
fn splitmix64_advances_and_seeds_from_entropy() {
let mut rng = SplitMix64::from_seed(0);
let draws: HashSet<u64> = (0..64).map(|_| rng.next_u64()).collect();
assert_eq!(draws.len(), 64, "generator repeated within 64 draws");
assert_ne!(
SplitMix64::from_entropy().next_u64(),
SplitMix64::from_seed(0).next_u64()
);
}
#[test]
fn occupied_is_dir_leaves_plus_claimants() {
let root = TempDir::new().unwrap();
fs::create_dir(root.path().join("cobalt-gecko")).unwrap();
fs::create_dir(root.path().join("amber-wren")).unwrap();
fs::write(root.path().join("notes.txt"), "x").unwrap();
let taken = occupied(root.path(), &["slate-heron".to_owned()]);
assert_eq!(taken, set(&["cobalt-gecko", "amber-wren", "slate-heron"]));
}
#[test]
fn occupied_over_a_missing_root_is_just_the_claimants() {
let missing = TempDir::new().unwrap().path().join("gone");
assert_eq!(occupied(&missing, &[]), HashSet::new());
assert_eq!(
occupied(&missing, &["ochre-lynx".to_owned()]),
set(&["ochre-lynx"])
);
}