use super::*;
use crate::template::{GitRunner, RealGit};
use crate::workspace::fixture;
struct Fixed(u64);
impl Rng for Fixed {
fn next_u64(&self) -> u64 {
self.0
}
}
fn set(names: &[&str]) -> HashSet<String> {
names.iter().map(|s| (*s).to_owned()).collect()
}
const ABC: &[&str] = &["ash", "bay", "cove"];
const ABC_PAIRS: &[&str] = &[
"AshBay", "AshCove", "BayAsh", "BayCove", "CoveAsh", "CoveBay",
];
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("AshBay")),
(ABC, 2, &[], Ok("BayAsh")),
(ABC, 5, &[], Ok("CoveBay")),
(ABC, 9, &[], Ok("BayCove")),
(ABC, 0, &["AshBay"], Ok("AshCove")),
(ABC, 5, &["CoveBay", "AshBay"], Ok("AshCove")),
(
&["ash", "bay"],
0,
&["AshBay", "BayAsh"],
Err(MintError::Exhausted(2)),
),
(&["ash"], 0, &[], Err(MintError::Exhausted(0))),
(&[], 0, &[], Err(MintError::Exhausted(0))),
(&["", "bay"], 0, &[], Ok("Bay")),
];
for (words, draw, taken, expect) in cases {
let got = mint_from(words, &Fixed(*draw), &set(taken));
let want = expect.clone().map(str::to_owned);
assert_eq!(got, want, "words={words:?} draw={draw} taken={taken:?}");
}
}
#[test]
fn the_scan_spells_every_distinct_pair_once_then_exhausts() {
let mut occupied = HashSet::new();
let mut minted = Vec::new();
for _ in 0..ABC_PAIRS.len() {
let name = mint_from(ABC, &Fixed(0), &occupied).expect("a free pair remains");
occupied.insert(name.clone());
minted.push(name);
}
assert_eq!(minted, ABC_PAIRS, "the scan order is the index mapping");
assert_eq!(
mint_from(ABC, &Fixed(0), &occupied),
Err(MintError::Exhausted(6)),
"the pair space is the bound, and it is exact"
);
for word in ABC {
let self_pair = pascal(word, word);
assert!(!occupied.contains(&self_pair), "{self_pair:?} was spelled");
}
}
#[test]
fn exhaustion_is_loud_at_both_altitudes() {
let err = MintError::Exhausted(2);
assert_eq!(
err.to_string(),
"name pool exhausted: all 2 names are occupied"
);
assert_eq!(Unavailable::from(err.clone()).to_string(), err.to_string());
}
mod corpus;
fn split_pascal(name: &str) -> (String, String) {
assert!(
is_minted_shape(name),
"{name:?} is not two PascalCase words with no separator"
);
let cut = name
.char_indices()
.rev()
.find(|(_, c)| c.is_ascii_uppercase())
.map(|(i, _)| i)
.expect("the shape check found two capitals");
let (first, second) = name.split_at(cut);
let (first, second) = (first.to_lowercase(), second.to_lowercase());
let pool = wordlist();
assert!(
pool.contains(&first.as_str()),
"{first:?} is not in the pool"
);
assert!(
pool.contains(&second.as_str()),
"{second:?} is not in the pool"
);
assert_ne!(first, second, "{name:?} pairs a word with itself");
(first, second)
}
#[test]
fn the_minted_shape_predicate_reads_only_two_pascal_words() {
assert!(is_minted_shape("PeachHollow"));
for wrong in [
"",
"peach",
"Peach",
"peachHollow",
"PeachHollowGate",
"Peach-Hollow",
"Peach Hollow",
"peach-hollow",
] {
assert!(!is_minted_shape(wrong), "{wrong:?} read as minted");
}
}
#[test]
fn mint_over_the_embedded_list_is_deterministic_and_avoids_the_occupied_set() {
let name = mint(&SplitMix64::from_seed(7), &HashSet::new()).unwrap();
let again = mint(&SplitMix64::from_seed(7), &HashSet::new()).unwrap();
assert_eq!(name, again);
let (first, second) = split_pascal(&name);
assert_eq!(name, pascal(&first, &second));
let next = mint(&SplitMix64::from_seed(7), &set(&[&name])).unwrap();
assert_ne!(next, name);
split_pascal(&next);
}
#[test]
fn splitmix64_advances_and_seeds_from_entropy() {
let 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 preflight_validates_a_supplied_name_and_mints_on_omission() {
let (_h, ws) = fixture::workspace();
let git = RealGit::new();
let wt = fixture::spawn_root(&ws, "20260101T000000Z-aaaaaaaa");
super::super::settle(&wt, Some("pale-otter"), &git).unwrap();
git.run(&wt, &["commit", "-m", "settle name"]).unwrap();
assert_eq!(
preflight(&ws, Some("brook"), &git, &Fixed(0)).unwrap(),
"brook"
);
assert!(matches!(
preflight(&ws, Some("pale-otter"), &git, &Fixed(0)),
Err(Unavailable::Taken { .. })
));
let minted = preflight(&ws, None, &git, &SplitMix64::from_seed(7)).unwrap();
assert_eq!(
minted,
preflight(&ws, None, &git, &SplitMix64::from_seed(7)).unwrap()
);
split_pascal(&minted);
assert_ne!(minted, "pale-otter");
assert!(matches!(
preflight(Path::new("/nonexistent"), None, &git, &Fixed(0)),
Err(Unavailable::Scan(_))
));
}