|
|
""" |
|
|
Unit tests for CrosswordGenerator to ensure robust crossword generation. |
|
|
""" |
|
|
|
|
|
import pytest |
|
|
import asyncio |
|
|
from unittest.mock import Mock, patch |
|
|
import sys |
|
|
from pathlib import Path |
|
|
|
|
|
|
|
|
project_root = Path(__file__).parent.parent |
|
|
sys.path.insert(0, str(project_root)) |
|
|
|
|
|
from src.services.crossword_generator import CrosswordGenerator |
|
|
|
|
|
|
|
|
@pytest.fixture |
|
|
def sample_words(): |
|
|
"""Sample word data for testing.""" |
|
|
return [ |
|
|
{"word": "DOG", "clue": "Man's best friend", "similarity": 0.8, "source": "test"}, |
|
|
{"word": "ELEPHANT", "clue": "Large mammal with trunk", "similarity": 0.7, "source": "test"}, |
|
|
{"word": "CAT", "clue": "Feline pet", "similarity": 0.9, "source": "test"}, |
|
|
{"word": "BUTTERFLY", "clue": "Colorful flying insect", "similarity": 0.6, "source": "test"}, |
|
|
{"word": "TIGER", "clue": "Striped big cat", "similarity": 0.75, "source": "test"}, |
|
|
{"word": "WHALE", "clue": "Largest marine mammal", "similarity": 0.65, "source": "test"}, |
|
|
] |
|
|
|
|
|
|
|
|
@pytest.fixture |
|
|
def mock_vector_service(): |
|
|
"""Mock vector search service for testing.""" |
|
|
mock_service = Mock() |
|
|
mock_service.is_initialized = True |
|
|
return mock_service |
|
|
|
|
|
|
|
|
class TestCrosswordGenerator: |
|
|
"""Test cases for CrosswordGenerator.""" |
|
|
|
|
|
def test_init(self): |
|
|
"""Test generator initialization.""" |
|
|
generator = CrosswordGenerator() |
|
|
assert generator.max_attempts == 100 |
|
|
assert generator.min_words == 6 |
|
|
assert generator.max_words == 10 |
|
|
assert generator.vector_service is None |
|
|
|
|
|
def test_init_with_vector_service(self, mock_vector_service): |
|
|
"""Test generator initialization with vector service.""" |
|
|
generator = CrosswordGenerator(vector_service=mock_vector_service) |
|
|
assert generator.vector_service == mock_vector_service |
|
|
|
|
|
def test_sort_words_for_crossword(self, sample_words): |
|
|
"""Test word sorting by crossword suitability.""" |
|
|
generator = CrosswordGenerator() |
|
|
sorted_words = generator._sort_words_for_crossword(sample_words) |
|
|
|
|
|
|
|
|
assert len(sorted_words) == len(sample_words) |
|
|
assert all(isinstance(w, dict) for w in sorted_words) |
|
|
assert all("crossword_score" in w for w in sorted_words) |
|
|
|
|
|
|
|
|
scores = [w["crossword_score"] for w in sorted_words] |
|
|
|
|
|
assert len(scores) > 0 |
|
|
|
|
|
def test_filter_by_difficulty(self, sample_words): |
|
|
"""Test difficulty filtering.""" |
|
|
generator = CrosswordGenerator() |
|
|
|
|
|
|
|
|
easy_words = generator._filter_by_difficulty(sample_words, "easy") |
|
|
easy_lengths = [len(w["word"]) for w in easy_words] |
|
|
assert all(3 <= length <= 8 for length in easy_lengths) |
|
|
|
|
|
|
|
|
medium_words = generator._filter_by_difficulty(sample_words, "medium") |
|
|
medium_lengths = [len(w["word"]) for w in medium_words] |
|
|
assert all(4 <= length <= 10 for length in medium_lengths) |
|
|
|
|
|
|
|
|
hard_words = generator._filter_by_difficulty(sample_words, "hard") |
|
|
hard_lengths = [len(w["word"]) for w in hard_words] |
|
|
assert all(5 <= length <= 15 for length in hard_lengths) |
|
|
|
|
|
def test_calculate_grid_size(self): |
|
|
"""Test grid size calculation.""" |
|
|
generator = CrosswordGenerator() |
|
|
|
|
|
|
|
|
short_words = ["DOG", "CAT", "BAT"] |
|
|
size = generator._calculate_grid_size(short_words) |
|
|
assert size >= 8 |
|
|
assert size >= 3 |
|
|
|
|
|
|
|
|
long_words = ["ELEPHANT", "BUTTERFLY", "HIPPOPOTAMUS"] |
|
|
size = generator._calculate_grid_size(long_words) |
|
|
assert size >= 12 |
|
|
|
|
|
def test_create_grid_word_processing(self, sample_words): |
|
|
"""Test the critical word processing logic that was causing index errors.""" |
|
|
generator = CrosswordGenerator() |
|
|
|
|
|
|
|
|
result = generator._create_grid(sample_words) |
|
|
|
|
|
|
|
|
assert result is None or isinstance(result, dict) |
|
|
|
|
|
|
|
|
if result: |
|
|
assert "grid" in result |
|
|
assert "clues" in result |
|
|
assert "placed_words" in result |
|
|
|
|
|
def test_create_grid_empty_words(self): |
|
|
"""Test grid creation with empty word list.""" |
|
|
generator = CrosswordGenerator() |
|
|
result = generator._create_grid([]) |
|
|
assert result is None |
|
|
|
|
|
def test_create_grid_malformed_words(self): |
|
|
"""Test grid creation with malformed word data.""" |
|
|
generator = CrosswordGenerator() |
|
|
|
|
|
|
|
|
malformed_words = [ |
|
|
"just_string", |
|
|
{"no_word_key": "value"}, |
|
|
{"word": ""}, |
|
|
None, |
|
|
123, |
|
|
] |
|
|
|
|
|
|
|
|
result = generator._create_grid(malformed_words) |
|
|
assert result is None or isinstance(result, dict) |
|
|
|
|
|
def test_can_place_word_horizontal(self): |
|
|
"""Test horizontal word placement validation.""" |
|
|
generator = CrosswordGenerator() |
|
|
grid = [["." for _ in range(10)] for _ in range(10)] |
|
|
|
|
|
|
|
|
assert generator._can_place_word(grid, "TEST", 5, 3, "horizontal") |
|
|
|
|
|
|
|
|
assert not generator._can_place_word(grid, "TOOLONG", 5, 7, "horizontal") |
|
|
assert not generator._can_place_word(grid, "TEST", 5, -1, "horizontal") |
|
|
assert not generator._can_place_word(grid, "TEST", -1, 3, "horizontal") |
|
|
|
|
|
def test_can_place_word_vertical(self): |
|
|
"""Test vertical word placement validation.""" |
|
|
generator = CrosswordGenerator() |
|
|
grid = [["." for _ in range(10)] for _ in range(10)] |
|
|
|
|
|
|
|
|
assert generator._can_place_word(grid, "TEST", 3, 5, "vertical") |
|
|
|
|
|
|
|
|
assert not generator._can_place_word(grid, "TOOLONG", 7, 5, "vertical") |
|
|
assert not generator._can_place_word(grid, "TEST", -1, 5, "vertical") |
|
|
assert not generator._can_place_word(grid, "TEST", 3, -1, "vertical") |
|
|
|
|
|
def test_place_and_remove_word(self): |
|
|
"""Test word placement and removal.""" |
|
|
generator = CrosswordGenerator() |
|
|
grid = [["." for _ in range(10)] for _ in range(10)] |
|
|
|
|
|
|
|
|
original_state = generator._place_word(grid, "TEST", 5, 3, "horizontal") |
|
|
|
|
|
|
|
|
assert grid[5][3] == "T" |
|
|
assert grid[5][4] == "E" |
|
|
assert grid[5][5] == "S" |
|
|
assert grid[5][6] == "T" |
|
|
|
|
|
|
|
|
generator._remove_word(grid, original_state) |
|
|
|
|
|
|
|
|
assert grid[5][3] == "." |
|
|
assert grid[5][4] == "." |
|
|
assert grid[5][5] == "." |
|
|
assert grid[5][6] == "." |
|
|
|
|
|
def test_find_word_intersections(self): |
|
|
"""Test finding intersections between words.""" |
|
|
generator = CrosswordGenerator() |
|
|
|
|
|
|
|
|
intersections = generator._find_word_intersections("CAT", "DOG") |
|
|
assert len(intersections) == 0 |
|
|
|
|
|
intersections = generator._find_word_intersections("CAT", "ACE") |
|
|
assert len(intersections) >= 1 |
|
|
|
|
|
|
|
|
for intersection in intersections: |
|
|
assert "word_pos" in intersection |
|
|
assert "placed_pos" in intersection |
|
|
assert isinstance(intersection["word_pos"], int) |
|
|
assert isinstance(intersection["placed_pos"], int) |
|
|
|
|
|
def test_create_simple_cross(self, sample_words): |
|
|
"""Test simple cross creation as fallback.""" |
|
|
generator = CrosswordGenerator() |
|
|
|
|
|
|
|
|
words_with_intersection = [ |
|
|
{"word": "CAT", "clue": "Feline"}, |
|
|
{"word": "ACE", "clue": "Playing card"}, |
|
|
] |
|
|
|
|
|
word_list = ["CAT", "ACE"] |
|
|
result = generator._create_simple_cross(word_list, words_with_intersection) |
|
|
|
|
|
if result: |
|
|
assert "grid" in result |
|
|
assert "clues" in result |
|
|
assert "placed_words" in result |
|
|
assert len(result["placed_words"]) == 2 |
|
|
|
|
|
def test_generate_clues(self, sample_words): |
|
|
"""Test clue generation for placed words.""" |
|
|
generator = CrosswordGenerator() |
|
|
|
|
|
placed_words = [ |
|
|
{"word": "DOG", "row": 0, "col": 0, "direction": "horizontal", "number": 1}, |
|
|
{"word": "CAT", "row": 0, "col": 0, "direction": "vertical", "number": 2}, |
|
|
] |
|
|
|
|
|
clues = generator._generate_clues(sample_words, placed_words) |
|
|
|
|
|
assert len(clues) == 2 |
|
|
for clue in clues: |
|
|
assert "number" in clue |
|
|
assert "word" in clue |
|
|
assert "text" in clue |
|
|
assert "direction" in clue |
|
|
assert clue["direction"] in ["across", "down"] |
|
|
assert "position" in clue |
|
|
|
|
|
@pytest.mark.asyncio |
|
|
async def test_select_words_with_vector_service(self, mock_vector_service, sample_words): |
|
|
"""Test word selection with vector service.""" |
|
|
|
|
|
mock_vector_service.find_similar_words.return_value = sample_words |
|
|
|
|
|
generator = CrosswordGenerator(vector_service=mock_vector_service) |
|
|
|
|
|
words = await generator._select_words(["Animals"], "medium", True) |
|
|
|
|
|
assert len(words) <= generator.max_words |
|
|
assert all(isinstance(w, dict) for w in words) |
|
|
mock_vector_service.find_similar_words.assert_called_once() |
|
|
|
|
|
@pytest.mark.asyncio |
|
|
async def test_select_words_without_vector_service(self): |
|
|
"""Test word selection without vector service.""" |
|
|
generator = CrosswordGenerator() |
|
|
|
|
|
|
|
|
words = await generator._select_words(["Animals"], "medium", True) |
|
|
|
|
|
|
|
|
assert isinstance(words, list) |
|
|
|
|
|
@pytest.mark.asyncio |
|
|
async def test_generate_puzzle_success(self, mock_vector_service, sample_words): |
|
|
"""Test successful puzzle generation.""" |
|
|
mock_vector_service.find_similar_words.return_value = sample_words |
|
|
|
|
|
generator = CrosswordGenerator(vector_service=mock_vector_service) |
|
|
|
|
|
|
|
|
with patch.object(generator, '_create_grid') as mock_create_grid: |
|
|
mock_create_grid.return_value = { |
|
|
"grid": [["T", "E", "S", "T"], [".", ".", ".", "."]], |
|
|
"placed_words": [{"word": "TEST", "row": 0, "col": 0, "direction": "horizontal", "number": 1}], |
|
|
"clues": [{"number": 1, "word": "TEST", "text": "A test", "direction": "across", "position": {"row": 0, "col": 0}}] |
|
|
} |
|
|
|
|
|
result = await generator.generate_puzzle(["Animals"], "medium", True) |
|
|
|
|
|
assert result is not None |
|
|
assert "grid" in result |
|
|
assert "clues" in result |
|
|
assert "metadata" in result |
|
|
assert result["metadata"]["topics"] == ["Animals"] |
|
|
assert result["metadata"]["difficulty"] == "medium" |
|
|
assert result["metadata"]["aiGenerated"] is True |
|
|
|
|
|
@pytest.mark.asyncio |
|
|
async def test_generate_puzzle_insufficient_words(self, mock_vector_service): |
|
|
"""Test puzzle generation with insufficient words.""" |
|
|
|
|
|
mock_vector_service.find_similar_words.return_value = [ |
|
|
{"word": "CAT", "clue": "Feline", "similarity": 0.8, "source": "test"} |
|
|
] |
|
|
|
|
|
generator = CrosswordGenerator(vector_service=mock_vector_service) |
|
|
|
|
|
with pytest.raises(Exception, match="Not enough words generated"): |
|
|
await generator.generate_puzzle(["Animals"], "medium", True) |
|
|
|
|
|
@pytest.mark.asyncio |
|
|
async def test_generate_puzzle_grid_creation_fails(self, mock_vector_service, sample_words): |
|
|
"""Test puzzle generation when grid creation fails.""" |
|
|
mock_vector_service.find_similar_words.return_value = sample_words |
|
|
|
|
|
generator = CrosswordGenerator(vector_service=mock_vector_service) |
|
|
|
|
|
|
|
|
with patch.object(generator, '_create_grid', return_value=None): |
|
|
with pytest.raises(Exception, match="Could not create crossword grid"): |
|
|
await generator.generate_puzzle(["Animals"], "medium", True) |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
pytest.main([__file__, "-v"]) |