Initial commit

This commit is contained in:
2025-11-06 22:34:14 +01:00
commit ebdbe60e04
21 changed files with 3449 additions and 0 deletions

91
tests/analyzer_tests.rs Normal file
View File

@@ -0,0 +1,91 @@
use anagram_generator::{
analyzer::PronounceabilityAnalyzer,
scorer::{CharacterClassifier, ConsonantClusterRules, PronounceabilityScorer},
};
#[test]
fn test_empty_string_scores_zero() {
let analyzer = PronounceabilityAnalyzer::with_defaults();
assert_eq!(analyzer.score("").value(), 0);
}
#[test]
fn test_good_pronounceable_words() {
let analyzer = PronounceabilityAnalyzer::with_defaults();
assert!(analyzer.score("hello").value() > 60);
assert!(analyzer.score("world").value() > 50);
assert!(analyzer.score("create").value() > 60);
}
#[test]
fn test_poor_pronounceable_words() {
let analyzer = PronounceabilityAnalyzer::with_defaults();
assert!(analyzer.score("bcdfg").value() < 50);
assert!(analyzer.score("xzqwk").value() < 50);
}
#[test]
fn test_many_consecutive_consonants() {
let analyzer = PronounceabilityAnalyzer::with_defaults();
assert!(analyzer.score("strngth").value() < 60);
}
#[test]
fn test_character_classification() {
let classifier = CharacterClassifier::default_french();
assert!(classifier.is_vowel('a'));
assert!(classifier.is_vowel('e'));
assert!(classifier.is_vowel('A'));
assert!(!classifier.is_vowel('b'));
assert!(classifier.is_consonant('b'));
assert!(classifier.is_consonant('B'));
}
#[test]
fn test_common_clusters() {
let rules = ConsonantClusterRules::default_french();
assert!(rules.is_common_cluster("th"));
assert!(rules.is_common_cluster("st"));
assert!(rules.is_common_cluster("TH"));
assert!(!rules.is_common_cluster("xz"));
}
#[test]
fn test_alternating_patterns_get_bonus() {
let analyzer = PronounceabilityAnalyzer::with_defaults();
// "banana" has good alternation (b-a-n-a-n-a)
let score_good_alternation = analyzer.score("banana").value();
// "strength" has poor alternation
let score_poor_alternation = analyzer.score("strength").value();
assert!(score_good_alternation > score_poor_alternation);
}
#[test]
fn test_words_starting_with_consonants() {
let analyzer = PronounceabilityAnalyzer::with_defaults();
let score_consonant = analyzer.score("banana").value();
let score_vowel = analyzer.score("ananas").value();
// Words starting with consonants should get a small bonus
assert!(score_consonant >= score_vowel);
}
#[test]
fn test_only_vowels_penalized() {
let analyzer = PronounceabilityAnalyzer::with_defaults();
assert!(analyzer.score("aeiou").value() < 80);
}
#[test]
fn test_no_vowels_heavily_penalized() {
let analyzer = PronounceabilityAnalyzer::with_defaults();
assert!(analyzer.score("bcdfg").value() < 50);
}
#[test]
fn test_common_consonant_clusters_not_penalized() {
let analyzer = PronounceabilityAnalyzer::with_defaults();
// "three" has "th" which is a common cluster
let score_with_common = analyzer.score("three").value();
// Should have a decent score despite consecutive consonants
assert!(score_with_common > 60);
}

150
tests/generator_tests.rs Normal file
View File

@@ -0,0 +1,150 @@
use anagram_generator::{
analyzer::PronounceabilityAnalyzer, generator::AnagramGenerator, generator::GenerationConfig,
};
use rand::SeedableRng;
use rand::rngs::StdRng;
use std::collections::HashSet;
#[test]
fn test_generation_produces_valid_anagrams() {
let rng = StdRng::seed_from_u64(42);
let scorer = PronounceabilityAnalyzer::with_defaults();
let mut generator = AnagramGenerator::new(rng, scorer);
let config = GenerationConfig::new(0, 100);
let anagrams = generator.generate("test", 5, &config);
assert!(!anagrams.is_empty());
// Verify all anagrams have the same letters as source
for anagram in &anagrams {
let mut source_chars: Vec<char> = "test".chars().collect();
let mut anagram_chars: Vec<char> = anagram.text().chars().collect();
source_chars.sort();
anagram_chars.sort();
assert_eq!(source_chars, anagram_chars);
}
}
#[test]
fn test_generation_respects_min_score() {
let rng = StdRng::seed_from_u64(42);
let scorer = PronounceabilityAnalyzer::with_defaults();
let mut generator = AnagramGenerator::new(rng, scorer);
let min_score = 70;
let config = GenerationConfig::new(min_score, 10000);
let anagrams = generator.generate("example", 10, &config);
for anagram in &anagrams {
assert!(anagram.score().value() >= min_score);
}
}
#[test]
fn test_generation_produces_unique_anagrams() {
let rng = StdRng::seed_from_u64(42);
let scorer = PronounceabilityAnalyzer::with_defaults();
let mut generator = AnagramGenerator::new(rng, scorer);
let config = GenerationConfig::new(0, 10000);
let anagrams = generator.generate("hello", 5, &config);
let unique_texts: HashSet<&str> = anagrams.iter().map(|a| a.text()).collect();
assert_eq!(unique_texts.len(), anagrams.len());
}
#[test]
fn test_anagrams_sorted_by_score() {
let rng = StdRng::seed_from_u64(42);
let scorer = PronounceabilityAnalyzer::with_defaults();
let mut generator = AnagramGenerator::new(rng, scorer);
let config = GenerationConfig::new(0, 10000);
let anagrams = generator.generate("world", 10, &config);
for i in 1..anagrams.len() {
assert!(anagrams[i - 1].score() >= anagrams[i].score());
}
}
#[test]
fn test_generation_config_builder() {
let config = GenerationConfig::default()
.with_min_score(60)
.with_max_attempts(5000);
assert_eq!(config.min_score.value(), 60);
assert_eq!(config.max_attempts_per_anagram, 5000);
}
#[test]
fn test_generation_excludes_original_word() {
let rng = StdRng::seed_from_u64(42);
let scorer = PronounceabilityAnalyzer::with_defaults();
let mut generator = AnagramGenerator::new(rng, scorer);
let source = "test";
let config = GenerationConfig::new(0, 10000);
let anagrams = generator.generate(source, 20, &config);
// None of the anagrams should be the original word
assert!(anagrams.iter().all(|a| a.text() != source));
}
#[test]
fn test_generation_normalizes_input() {
let rng = StdRng::seed_from_u64(42);
let scorer = PronounceabilityAnalyzer::with_defaults();
let mut generator = AnagramGenerator::new(rng, scorer);
let config = GenerationConfig::new(0, 100);
let anagrams = generator.generate("TeSt", 5, &config);
// All anagrams should be lowercase
for anagram in &anagrams {
assert_eq!(anagram.text(), anagram.text().to_lowercase());
// Verify the anagram contains the same letters as the normalized input
let mut source_chars: Vec<char> = "test".chars().collect();
let mut anagram_chars: Vec<char> = anagram.text().chars().collect();
source_chars.sort();
anagram_chars.sort();
assert_eq!(source_chars, anagram_chars);
}
}
#[test]
fn test_generation_with_long_word() {
let rng = StdRng::seed_from_u64(42);
let scorer = PronounceabilityAnalyzer::with_defaults();
let mut generator = AnagramGenerator::new(rng, scorer);
let config = GenerationConfig::new(50, 5000);
let anagrams = generator.generate("complexity", 10, &config);
assert!(!anagrams.is_empty());
for anagram in &anagrams {
assert_eq!(anagram.text().len(), "complexity".len());
}
}
#[test]
fn test_generation_with_repeated_letters() {
let rng = StdRng::seed_from_u64(42);
let scorer = PronounceabilityAnalyzer::with_defaults();
let mut generator = AnagramGenerator::new(rng, scorer);
let config = GenerationConfig::new(0, 1000);
let anagrams = generator.generate("balloon", 5, &config);
assert!(!anagrams.is_empty());
// Verify the letter counts are preserved
for anagram in &anagrams {
let mut source_chars: Vec<char> = "balloon".chars().collect();
let mut anagram_chars: Vec<char> = anagram.text().chars().collect();
source_chars.sort();
anagram_chars.sort();
assert_eq!(source_chars, anagram_chars);
}
}

113
tests/integration_tests.rs Normal file
View File

@@ -0,0 +1,113 @@
use anagram_generator::{
analyzer::PronounceabilityAnalyzer, generator::AnagramGenerator, generator::GenerationConfig,
scorer::PronounceabilityScorer,
};
use rand::thread_rng;
#[test]
fn test_end_to_end_anagram_generation() {
let rng = thread_rng();
let scorer = PronounceabilityAnalyzer::with_defaults();
let mut generator = AnagramGenerator::new(rng, scorer);
let config = GenerationConfig::new(50, 1000);
let anagrams = generator.generate("example", 5, &config);
assert!(!anagrams.is_empty());
assert!(anagrams.len() <= 5);
for anagram in &anagrams {
assert!(anagram.score().value() >= 50);
assert_eq!(anagram.text().len(), "example".len());
}
}
#[test]
fn test_high_quality_anagrams_only() {
let rng = thread_rng();
let scorer = PronounceabilityAnalyzer::with_defaults();
let mut generator = AnagramGenerator::new(rng, scorer);
let config = GenerationConfig::new(80, 5000);
let anagrams = generator.generate("lawrence", 10, &config);
for anagram in &anagrams {
assert!(anagram.score().value() >= 80);
}
}
#[test]
fn test_generation_with_difficult_word() {
let rng = thread_rng();
let scorer = PronounceabilityAnalyzer::with_defaults();
let mut generator = AnagramGenerator::new(rng, scorer);
// A word with few vowels is harder to generate pronounceable anagrams from
let config = GenerationConfig::new(30, 5000);
let anagrams = generator.generate("rhythm", 3, &config);
// Should still be able to generate some anagrams
assert!(!anagrams.is_empty());
}
#[test]
fn test_customizable_scoring_via_trait() {
// Test that we can use the trait abstraction
struct SimpleScorer;
impl PronounceabilityScorer for SimpleScorer {
fn score(&self, text: &str) -> anagram_generator::PronouncabilityScore {
// Simple scorer: just count vowels
let vowel_count = text
.chars()
.filter(|c| matches!(c, 'a' | 'e' | 'i' | 'o' | 'u'))
.count();
anagram_generator::PronouncabilityScore::new((vowel_count * 20) as u32)
}
}
let rng = thread_rng();
let scorer = SimpleScorer;
let mut generator = AnagramGenerator::new(rng, scorer);
let config = GenerationConfig::new(0, 100);
let anagrams = generator.generate("aeiou", 5, &config);
assert!(!anagrams.is_empty());
}
#[test]
fn test_analyzer_with_custom_configuration() {
use anagram_generator::scorer::{
CharacterClassifier, ConsonantClusterRules, PatternAnalysisConfig, ScoringPenalties,
};
let classifier = CharacterClassifier::default_french();
let cluster_rules = ConsonantClusterRules::default_french();
let penalties = ScoringPenalties::default();
let config = PatternAnalysisConfig::default();
let analyzer = PronounceabilityAnalyzer::new(classifier, cluster_rules, penalties, config);
let score = analyzer.score("hello");
assert!(score.value() > 0);
}
#[test]
fn test_generation_config_builder_pattern() {
let config = GenerationConfig::default()
.with_min_score(70)
.with_max_attempts(3000);
assert_eq!(config.min_score.value(), 70);
assert_eq!(config.max_attempts_per_anagram, 3000);
let rng = thread_rng();
let scorer = PronounceabilityAnalyzer::with_defaults();
let mut generator = AnagramGenerator::new(rng, scorer);
let anagrams = generator.generate("test", 5, &config);
for anagram in &anagrams {
assert!(anagram.score().value() >= 70);
}
}

View File

@@ -0,0 +1,177 @@
use anagram_generator::{
analyzer::PronounceabilityAnalyzer, generator::AnagramGenerator, generator::GenerationConfig,
generator::LetterRemovalStrategy,
};
use rand::SeedableRng;
use rand::rngs::StdRng;
#[test]
fn test_letter_removal_disabled_by_default() {
let config = GenerationConfig::default();
assert_eq!(config.letter_removal, LetterRemovalStrategy::None);
}
#[test]
fn test_letter_removal_can_be_enabled() {
let config = GenerationConfig::default().allow_removing_letters(3);
assert_eq!(
config.letter_removal,
LetterRemovalStrategy::Adaptive { max_removals: 3 }
);
}
#[test]
fn test_generation_without_removal_produces_same_length() {
let rng = StdRng::seed_from_u64(42);
let scorer = PronounceabilityAnalyzer::with_defaults();
let mut generator = AnagramGenerator::new(rng, scorer);
let source = "difficult";
let config = GenerationConfig::new(0, 1000);
let anagrams = generator.generate(source, 5, &config);
for anagram in &anagrams {
assert_eq!(anagram.text().len(), source.len());
}
}
#[test]
fn test_generation_with_removal_may_produce_shorter_words() {
let rng = StdRng::seed_from_u64(42);
let scorer = PronounceabilityAnalyzer::with_defaults();
let mut generator = AnagramGenerator::new(rng, scorer);
let source = "xyzqwbcdfg"; // Very hard to pronounce word
let config = GenerationConfig::new(60, 5000).allow_removing_letters(4);
let anagrams = generator.generate(source, 5, &config);
// With letter removal enabled, some results might be shorter
let has_shorter = anagrams.iter().any(|a| a.text().len() < source.len());
// Due to randomness and the difficult source word, we expect some shorter results
assert!(has_shorter || anagrams.is_empty());
}
#[test]
fn test_letter_removal_improves_pronounceability() {
let rng1 = StdRng::seed_from_u64(42);
let rng2 = StdRng::seed_from_u64(42);
let scorer1 = PronounceabilityAnalyzer::with_defaults();
let scorer2 = PronounceabilityAnalyzer::with_defaults();
let mut generator_without = AnagramGenerator::new(rng1, scorer1);
let mut generator_with = AnagramGenerator::new(rng2, scorer2);
let source = "bcdfghjklm"; // No vowels, very hard to pronounce
let config_without = GenerationConfig::new(40, 10000);
let config_with = GenerationConfig::new(40, 10000).allow_removing_letters(5);
let anagrams_without = generator_without.generate(source, 10, &config_without);
let anagrams_with = generator_with.generate(source, 10, &config_with);
// With letter removal, we should be able to generate more anagrams
// or achieve higher scores on average
if !anagrams_with.is_empty() && !anagrams_without.is_empty() {
let avg_score_without: f32 = anagrams_without
.iter()
.map(|a| a.score().value() as f32)
.sum::<f32>()
/ anagrams_without.len() as f32;
let avg_score_with: f32 = anagrams_with
.iter()
.map(|a| a.score().value() as f32)
.sum::<f32>()
/ anagrams_with.len() as f32;
// Letter removal should help achieve better or equal scores
assert!(
avg_score_with >= avg_score_without || anagrams_with.len() > anagrams_without.len()
);
}
}
#[test]
fn test_letter_removal_respects_max_removals() {
let rng = StdRng::seed_from_u64(42);
let scorer = PronounceabilityAnalyzer::with_defaults();
let mut generator = AnagramGenerator::new(rng, scorer);
let source = "testing";
let max_removals = 2;
let config = GenerationConfig::new(0, 1000).allow_removing_letters(max_removals);
let anagrams = generator.generate(source, 20, &config);
// All anagrams should have at least (source.len() - max_removals) letters
let min_length = source.len() - max_removals;
for anagram in &anagrams {
assert!(
anagram.text().len() >= min_length,
"Anagram '{}' is too short (min: {})",
anagram.text(),
min_length
);
}
}
#[test]
fn test_letter_removal_maintains_min_word_length() {
let rng = StdRng::seed_from_u64(42);
let scorer = PronounceabilityAnalyzer::with_defaults();
let mut generator = AnagramGenerator::new(rng, scorer);
let source = "abc";
let config = GenerationConfig::new(0, 1000).allow_removing_letters(10); // More than word length
let anagrams = generator.generate(source, 10, &config);
// Should maintain at least 2 characters (word length - 1)
for anagram in &anagrams {
assert!(
anagram.text().len() >= 1,
"Anagram '{}' is too short",
anagram.text()
);
}
}
#[test]
fn test_letter_removal_with_good_word() {
let rng = StdRng::seed_from_u64(42);
let scorer = PronounceabilityAnalyzer::with_defaults();
let mut generator = AnagramGenerator::new(rng, scorer);
let source = "example"; // Already pronounceable
let config = GenerationConfig::new(70, 1000).allow_removing_letters(2);
let anagrams = generator.generate(source, 10, &config);
// With an already good word, letter removal might not be necessary
// But it should still work and produce results
assert!(!anagrams.is_empty());
for anagram in &anagrams {
assert!(anagram.score().value() >= 70);
}
}
#[test]
fn test_config_builder_with_letter_removal() {
let config = GenerationConfig::default()
.with_min_score(60)
.with_max_attempts(5000)
.allow_removing_letters(3);
assert_eq!(config.min_score.value(), 60);
assert_eq!(config.max_attempts_per_anagram, 5000);
assert_eq!(
config.letter_removal,
LetterRemovalStrategy::Adaptive { max_removals: 3 }
);
}
#[test]
fn test_letter_removal_strategy_with_method() {
let config = GenerationConfig::default()
.with_letter_removal(LetterRemovalStrategy::Adaptive { max_removals: 5 });
assert_eq!(
config.letter_removal,
LetterRemovalStrategy::Adaptive { max_removals: 5 }
);
}

93
tests/types_tests.rs Normal file
View File

@@ -0,0 +1,93 @@
use anagram_generator::types::{Anagram, PronouncabilityScore};
#[test]
fn test_score_value_clamped_to_range() {
let score1 = PronouncabilityScore::new(150);
assert_eq!(score1.value(), 100); // Should clamp to max
let score2 = PronouncabilityScore::new(50);
assert_eq!(score2.value(), 50); // Should stay as is
}
#[test]
fn test_score_saturating_operations() {
let score = PronouncabilityScore::new(80);
let increased = score.saturating_add(30);
assert_eq!(increased.value(), 100); // Should clamp at max
let decreased = score.saturating_sub(90);
assert_eq!(decreased.value(), 0); // Should clamp at min
}
#[test]
fn test_score_default_is_max() {
let score = PronouncabilityScore::default();
assert_eq!(score.value(), 100);
}
#[test]
fn test_score_from_u32() {
let score: PronouncabilityScore = 75u32.into();
assert_eq!(score.value(), 75);
}
#[test]
fn test_score_ordering() {
let score1 = PronouncabilityScore::new(50);
let score2 = PronouncabilityScore::new(80);
assert!(score2 > score1);
assert!(score1 < score2);
assert_eq!(score1, PronouncabilityScore::new(50));
}
#[test]
fn test_anagram_creation() {
let score = PronouncabilityScore::new(75);
let anagram = Anagram::new("hello".to_string(), score);
assert_eq!(anagram.text(), "hello");
assert_eq!(anagram.score(), score);
}
#[test]
fn test_anagram_ordering() {
let anagram1 = Anagram::new("hello".to_string(), PronouncabilityScore::new(50));
let anagram2 = Anagram::new("world".to_string(), PronouncabilityScore::new(80));
// Anagrams with higher scores should come first
assert!(anagram2 < anagram1);
}
#[test]
fn test_anagram_equality() {
let anagram1 = Anagram::new("hello".to_string(), PronouncabilityScore::new(75));
let anagram2 = Anagram::new("hello".to_string(), PronouncabilityScore::new(75));
let anagram3 = Anagram::new("world".to_string(), PronouncabilityScore::new(75));
assert_eq!(anagram1, anagram2);
assert_ne!(anagram1, anagram3);
}
#[test]
fn test_anagram_sorting() {
let mut anagrams = vec![
Anagram::new("a".to_string(), PronouncabilityScore::new(50)),
Anagram::new("b".to_string(), PronouncabilityScore::new(80)),
Anagram::new("c".to_string(), PronouncabilityScore::new(65)),
];
anagrams.sort();
// Should be sorted by score descending
assert_eq!(anagrams[0].score().value(), 80);
assert_eq!(anagrams[1].score().value(), 65);
assert_eq!(anagrams[2].score().value(), 50);
}
#[test]
fn test_score_display() {
let score = PronouncabilityScore::new(75);
assert_eq!(format!("{}", score), "75");
}