From de29cab82e7f54202312f5b246c0116b8b87aa51 Mon Sep 17 00:00:00 2001 From: Aryadev Chavali Date: Tue, 7 Apr 2026 12:30:00 +0100 Subject: [PATCH] cards:tests: implement tests for card API --- src/card/mod.rs | 3 + src/card/tests.rs | 436 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 439 insertions(+) create mode 100644 src/card/tests.rs diff --git a/src/card/mod.rs b/src/card/mod.rs index d4c0ba4..4778d82 100644 --- a/src/card/mod.rs +++ b/src/card/mod.rs @@ -5,6 +5,9 @@ mod impls; mod numerics; mod ord; +#[cfg(test)] +mod tests; + #[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Copy, Clone)] pub enum Rank { Three = 0, diff --git a/src/card/tests.rs b/src/card/tests.rs new file mode 100644 index 0000000..1c1a25c --- /dev/null +++ b/src/card/tests.rs @@ -0,0 +1,436 @@ +#[cfg(test)] +mod test_numerics { + use std::collections::HashSet; + + use crate::card::{Card, PlayingCard, Rank, Suit}; + + #[test] + fn rank() { + // TEST: Negative numbers cannot be ranks + assert!(matches!(Rank::try_from(-1), Err(_))); + + // TEST: Numbers >= 13 cannot be a rank. + for i in 13..1000 { + assert!( + matches!(Rank::try_from(i), Err(_)), + "Expected {i} not to match a rank" + ); + } + + // TEST: Numbers in [0, 12] are mapped to ranks + let set = (0..13) + .map(|i| { + let rank = Rank::try_from(i); + assert!(rank.is_ok(), "Expected {i} to map to a valid rank"); + rank.unwrap() + }) + .collect::>(); + + assert_eq!( + set.len(), + 13, + "Expected 13 unique ranks from Rank::try_from(0..13)" + ); + } + + #[test] + fn suit() { + // TEST: Negative numbers cannot be suits + assert!(matches!(Suit::try_from(-1), Err(_))); + + // TEST: Numbers >= 4 cannot be a suit. + for i in 4..1000 { + assert!( + matches!(Suit::try_from(i), Err(_)), + "Expected {i} not to match a suit" + ); + } + + // TEST: Numbers in [0, 3] are mapped to suits + let set = (0..4) + .map(|i| { + let suit = Suit::try_from(i); + assert!(suit.is_ok(), "Expected {i} to map to a valid suit"); + suit.unwrap() + }) + .collect::>(); + + assert_eq!( + set.len(), + 4, + "Expected 4 unique Suits from Suit::try_from(0..4)" + ); + } + + #[test] + fn playing_card() { + // TEST: Negative numbers cannot be playing cards + assert!(PlayingCard::try_from(-1).is_err()); + + // TEST: PlayingCard derived from 0 should be the 3 of Diamonds of the + // 0th deck. + { + let expected = PlayingCard::new(0, Rank::Three, Suit::Diamond); + let card = PlayingCard::try_from(0); + assert!(card.is_ok()); + let card = card.unwrap(); + assert_eq!(card.rank, expected.rank); + assert_eq!(card.suit, expected.suit); + } + + fn range_to_playing_card(deck: i64) -> HashSet { + ((deck * 52)..((deck + 1) * 52)) + .map(|n| { + let res = PlayingCard::try_from(n); + assert!( + res.is_ok(), + "Expected {n} to map to a PlayingCard" + ); + res.unwrap() + }) + .map(|card| { + // TEST: Playing cards derived from 52d..52(d + 1) all are + // in deck d. + assert_eq!(card.deck, deck); + card + }) + .collect::>() + } + + // 0..51 should map to the 0th deck of playing cards. + let first_deck = range_to_playing_card(0); + // 52..103 should map to the 1st deck of playing cards. + let second_deck = range_to_playing_card(1); + + // TEST: Expect 52 unique playing cards from + // PlayingCard::try_from(0..52) or PlayingCard::try_from(52..104). + assert_eq!(first_deck.len(), 52, "Expected 52 unique cards"); + assert_eq!(second_deck.len(), 52, "Expected 52 unique cards"); + + // There are 52 unique cards in PlayingCard::try_from(0..52). With deck + // being fixed to one number, pigeonhole principle suggests all Playing + // Cards really must be there (13 ranks, 4 suits => 52 combinations). + // To really prove it we can check all combinations of rank and suit and + // observe that they are present. + + for rank in (0..13i64).map(|n| Rank::try_from(n).unwrap()) { + for suit in (0..4i64).map(|n| Suit::try_from(n).unwrap()) { + let playing_card = PlayingCard::new(0, rank, suit); + // TEST: Expected combination of rank and suit to be present in + // first deck. + assert!( + first_deck.contains(&playing_card), + "Expected {playing_card} to be present in the map of 0th deck" + ); + + // TEST: Expected combination of rank and suit to be present in + // second deck. + let playing_card = PlayingCard::new(1, rank, suit); + assert!( + second_deck.contains(&playing_card), + "Expected {playing_card} to be present in the map of 1st deck" + ); + } + } + + // To test i64::from on PlayingCard, we can just do a map over a big + // range and see that PlayingCard::try_from is the inverse. This range + // should cover an inordinate number of Cards for a single game so for + // our purposes it's just fine. + for i in 0..1000 { + let pc = PlayingCard::try_from(i).unwrap(); + let ret = i64::from(pc); + // TEST: i64::from is the exact inverse of PlayingCard::try_from. + assert_eq!( + i, ret, + "Expected i64::from(PlayingCard::try_from(x)) = x" + ); + } + } + + #[test] + fn card() { + // You can make Cards from negative numbers. + for i in -100..0 { + let card = Card::from(i); + + // TEST: Card::from(negative number) makes jokers. + assert!( + matches!(card, Card::Joker(_)), + "Expected Card::from({i}) makes jokers" + ); + } + + // Card::from should defer to PlayingCard::try_from for positive + // integers. We may test this with a ridiculous range. + let range = 0..1000; + + let cards = range.clone().map(Card::from).collect::>(); + + let playing_cards = range + .clone() + .map(|x| PlayingCard::try_from(x).unwrap()) + .collect::>(); + + assert_eq!(cards.len(), playing_cards.len()); + assert_eq!(cards.len(), 1000); + + for (card, pc) in cards.iter().zip(playing_cards.iter()) { + if let Card::PlayingCard(c) = card { + assert_eq!(c.deck, pc.deck); + assert_eq!(c.rank, pc.rank); + assert_eq!(c.suit, pc.suit); + } else { + unreachable!("All of cards should be playing cards"); + } + } + + // Thus the test on PlayingCard::from applies here. Great! + + // To test i64::from on Card, we'll do what the test on PlayingCard did + // and use a massive range. + for i in -1000..1000 { + let pc = Card::from(i); + let ret = i64::from(pc); + // TEST: i64::from is the exact inverse of Card::from. + assert_eq!(i, ret, "Expected i64::from(Card::from({i})) = {i}"); + } + } +} + +#[cfg(test)] +mod test_ord { + use crate::card::Card; + + #[test] + fn jokers() { + // All jokers are equivalent, regardless of the i64 they originate from. + let joker = Card::from(-1); + for i in -1000..-1 { + let other = Card::from(i); + assert_eq!(joker, other); + } + + // All playing cards are "better" than Jokers. + for i in 0..1000 { + let other = Card::from(i); + assert!(other > joker, "{other} > {joker}"); + assert!(joker < other, "{joker} < {other}"); + } + } + + #[test] + fn natural_ordering() { + // The cards Card::from(0..52) preserve the ordering relationship of the + // underlying numbers. In other words, Cards and 0..52 are order + // isomorphic. + for i in 0..52 { + let card = Card::from(i); + // We don't need to do a lower numbers check because of this. + for hi in (i + 1)..52 { + let hi_card = Card::from(hi); + // TEST: Higher numbers lead to higher cards. + assert!(card < hi_card, "{card} < {hi_card}"); + assert!(hi_card > card, "{hi_card} > {card}"); + } + } + } + + #[test] + fn deck_irrelevance() { + // We'll do a similar check as natural_ordering, but we'll be using + // cards from the 1st deck rather than the 0th deck. + for i in 0..52 { + let j = i + 52; + let card_0 = Card::from(i); + let card_1 = Card::from(j); + + // TEST: Two numbers equivalent mod 52 are "equivalent" cards under + // Card::from. + assert_eq!(card_0, card_1); + + // We don't need to do a lower numbers check because of this. + for hi in (i + 1)..52 { + let hi_card_0 = Card::from(hi); // deck 0 + let hi_card_1 = Card::from(hi + 52); // deck 1 + + // TEST: Higher cards are still ordered better than another + // deck's cards. + assert!(card_1 < hi_card_0, "{card_1} < {hi_card_0}"); + assert!(card_0 < hi_card_1, "{card_0} < {hi_card_1}"); + assert!(hi_card_0 > card_1, "{hi_card_0} > {card_1}"); + assert!(hi_card_1 > card_0, "{hi_card_1} > {card_0}"); + } + } + } +} + +#[cfg(test)] +mod test_impls { + use std::collections::{HashMap, HashSet}; + + use crate::{ + card::{Card, PlayingCard, Rank, Suit}, + helper::ExactSizedArr, + }; + + #[test] + fn rank() { + let ranks = Rank::iter_all().collect::>(); + // TEST: Rank::iter_all produces all 13 unique ranks. + assert_eq!(ranks.len(), 13); + + for rank in Rank::iter_all() { + let cards = rank.cards().collect::>(); + assert_eq!(cards.len(), 4, "Expected 4 cards per rank"); + for c in cards { + // TEST: rank.cards() generates Playing Cards of the same rank + // as the input in deck 0. + assert!(matches!(c, Card::PlayingCard(_))); + let Card::PlayingCard(c) = c else { + unreachable!("See above"); + }; + assert_eq!(c.deck, 0); + assert_eq!(c.rank, rank); + } + + // Since there are 4 unique cards generated by rank.cards(), and + // they all have the same deck (0) and rank (rank), they must differ + // by Suit by virtue of ord. 4 suits suggests (by pigeonhole + // principle) that all suits must have been used. + + // So rank.cards() generates all cards of deck 0 that have that rank + // (which is precisely 4 cards). QED. + } + } + + #[test] + fn suit() { + let suits = Suit::iter_all().collect::>(); + // TEST: Suit::iter_all produces all 4 unique suits. + assert_eq!(suits.len(), 4); + + for suit in Suit::iter_all() { + let cards = suit.cards().collect::>(); + assert_eq!(cards.len(), 13, "Expected 13 cards per suit"); + + for c in cards { + // TEST: suit.cards() generates Playing Cards of the same suit + // as the input in deck 0. + assert!(matches!(c, Card::PlayingCard(_))); + let Card::PlayingCard(c) = c else { + unreachable!("See above"); + }; + assert_eq!(c.deck, 0); + assert_eq!(c.suit, suit); + } + + // Similar to rank, pigeonhole principle suggests we must have all + // 13 ranks of cards in the suit expected for suit.cards(). + } + } + + #[test] + fn playing_card() { + for deck in 0..10 { + let playing_cards = + PlayingCard::iter_all(deck).collect::>(); + + // TEST: Expected 52 cards to be generated by PlayingCard::iter_all. + assert_eq!( + playing_cards.len(), + 52, + "Expected 52 cards in a playing card deck" + ); + + for card in PlayingCard::iter_all(deck) + .into_array::<52>() + .unwrap_or_else(|_| { + unreachable!( + "Look at previous assertion; there must be 52 cards in the iterator." + ) + }) { + // TEST: card.deck must match the input deck + assert_eq!(card.deck, deck); + let numeral = i64::from(card); + + // TEST: Expect the card from playing_cards to be bounded by the + // limits generated by i64::from. + assert!( + numeral >= 52 * deck, + "Expected i64::from({}) to be bounded below by {}", + card, + 52 * deck + ); + + assert!( + numeral <= (52 * deck) + 51, + "Expected i64::from({}) to be bounded above by {}", + card, + (52 * deck) + 51 + ); + } + } + } + + #[test] + fn card() { + for decks in 1..10 { + let cards = Card::iter_all(decks).collect::>(); + + // TEST: Expected there to be `decks` number of decks of cards (2 + // jokers + 52 playing cards) present in Card::iter_all(decks). + assert_eq!( + cards.len(), + (54 * decks) as usize, + "Expected {} cards in a playing card deck", + 54 * decks, + ); + + // TEST: Expect there to be 2 jokers per deck of cards input. + assert_eq!( + cards.iter().filter(|n| matches!(n, Card::Joker(_))).count(), + (2 * decks) as usize, + "Expected there to be {} jokers in Card::iter_all({})", + 2 * decks, + decks, + ); + + // Construct a counter which maps each unique (rank, suit) + // combination to the count of that combination in cards. + let counter = { + let mut counter: HashMap<(Rank, Suit), i64> = HashMap::new(); + for card in &cards { + let Card::PlayingCard(card) = card else { + continue; + }; + + if let Some(count) = + counter.get_mut(&(card.rank, card.suit)) + { + *count += 1; + } else { + counter.insert((card.rank, card.suit), 1); + } + } + counter + }; + + for (rank, suit) in Rank::iter_all() + .flat_map(|r| Suit::iter_all().map(move |s| (r, s))) + { + // TEST: We expect `decks` instances of a (rank, suit) + // combination in Card::iter_all(decks). + assert_eq!( + counter[&(rank, suit)], + decks, + "{} of {} doesn't have {} copies in Card::iter_all({})", + rank, + suit, + decks, + decks + ); + } + } + } +}