File size: 12,805 Bytes
38c016b |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 |
"""
Specific unit tests to verify the list index out of range bug is completely fixed.
These tests reproduce the exact conditions that were causing the crash.
"""
import pytest
import asyncio
import sys
from pathlib import Path
from unittest.mock import Mock, patch
# Add project root to path
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from src.services.crossword_generator import CrosswordGenerator
class TestIndexBugFix:
"""Test cases specifically for the index out of range bug."""
@pytest.fixture
def real_vector_words(self):
"""Real word data that was causing the crash - from the actual logs."""
return [
{'word': 'ZOOLOGY', 'clue': 'zoology (animal)', 'similarity': 0.6106429100036621, 'source': 'vector_search', 'crossword_score': 16},
{'word': 'NATURE', 'clue': 'nature (animal)', 'similarity': 0.5933953523635864, 'source': 'vector_search', 'crossword_score': 18},
{'word': 'VETERINARY', 'clue': 'veterinary (animal)', 'similarity': 0.7589661479, 'source': 'vector_search', 'crossword_score': 25},
{'word': 'ZOOLOGICAL', 'clue': 'zoological (animal)', 'similarity': 0.668032, 'source': 'vector_search', 'crossword_score': 22},
{'word': 'MAMMALIAN', 'clue': 'mammalian (animal)', 'similarity': 0.6375998, 'source': 'vector_search', 'crossword_score': 20},
{'word': 'CHILDREN', 'clue': 'children (animal)', 'similarity': 0.6281173, 'source': 'vector_search', 'crossword_score': 19},
{'word': 'ELEPHANT', 'clue': 'elephant (animal)', 'similarity': 0.6157694, 'source': 'vector_search', 'crossword_score': 18},
{'word': 'FAUNA', 'clue': 'fauna (animal)', 'similarity': 0.5890194177627563, 'source': 'vector_search', 'crossword_score': 16},
{'word': 'ORGANISM', 'clue': 'organism (animal)', 'similarity': 0.58123, 'source': 'vector_search', 'crossword_score': 19},
{'word': 'MAMMAL', 'clue': 'mammal (animal)', 'similarity': 0.57892, 'source': 'vector_search', 'crossword_score': 17},
{'word': 'CREATURE', 'clue': 'creature (animal)', 'similarity': 0.57654, 'source': 'vector_search', 'crossword_score': 18},
{'word': 'SPECIES', 'clue': 'species (animal)', 'similarity': 0.57432, 'source': 'vector_search', 'crossword_score': 16}
]
def test_calculate_placement_score_bounds_checking(self):
"""Test that _calculate_placement_score handles out-of-bounds access correctly."""
generator = CrosswordGenerator()
# Create a small 5x5 grid
grid = [["." for _ in range(5)] for _ in range(5)]
# Test cases that should NOT crash
test_cases = [
# Horizontal placement that would go out of bounds
{"row": 2, "col": 3, "direction": "horizontal", "word": "ELEPHANT"}, # 8 letters, would go to col 10
{"row": 4, "col": 0, "direction": "horizontal", "word": "VETERINARY"}, # 10 letters, would go to col 9
# Vertical placement that would go out of bounds
{"row": 3, "col": 2, "direction": "vertical", "word": "ZOOLOGICAL"}, # 10 letters, would go to row 12
{"row": 1, "col": 4, "direction": "vertical", "word": "MAMMALIAN"}, # 9 letters, would go to row 9
# Edge cases
{"row": 0, "col": 0, "direction": "horizontal", "word": "SUPERLONGWORD"},
{"row": 0, "col": 0, "direction": "vertical", "word": "SUPERLONGWORD"},
{"row": 4, "col": 4, "direction": "horizontal", "word": "TEST"},
{"row": 4, "col": 4, "direction": "vertical", "word": "TEST"},
]
for i, test_case in enumerate(test_cases):
placement = {
"row": test_case["row"],
"col": test_case["col"],
"direction": test_case["direction"]
}
word = test_case["word"]
try:
# This should NOT raise IndexError
score = generator._calculate_placement_score(grid, word, placement, [])
print(f"β
Test case {i+1}: {word} at ({test_case['row']},{test_case['col']}) {test_case['direction']} -> score: {score}")
assert isinstance(score, int), f"Score should be integer, got {type(score)}"
except IndexError as e:
pytest.fail(f"β IndexError in test case {i+1}: {word} at ({test_case['row']},{test_case['col']}) {test_case['direction']} - {e}")
except Exception as e:
pytest.fail(f"β Unexpected error in test case {i+1}: {e}")
def test_word_sorting_alignment(self, real_vector_words):
"""Test that word sorting maintains alignment between word_list and word_objs."""
generator = CrosswordGenerator()
# This is the exact code path that was causing the index error
word_pairs = []
for i, w in enumerate(real_vector_words):
if isinstance(w, dict) and "word" in w:
word_pairs.append((w["word"].upper(), w))
else:
pytest.fail(f"Invalid word format at index {i}: {w}")
# Sort pairs by word length (longest first)
word_pairs.sort(key=lambda pair: len(pair[0]), reverse=True)
# Extract sorted lists
word_list = [pair[0] for pair in word_pairs]
sorted_word_objs = [pair[1] for pair in word_pairs]
# Verify alignment
assert len(word_list) == len(sorted_word_objs), "Array lengths must match"
for i, (word, word_obj) in enumerate(zip(word_list, sorted_word_objs)):
assert word == word_obj["word"].upper(), f"Mismatch at index {i}: {word} != {word_obj['word'].upper()}"
print(f"β
Word sorting alignment verified for {len(word_list)} words")
def test_grid_creation_with_real_data(self, real_vector_words):
"""Test grid creation with the exact data that was causing crashes."""
generator = CrosswordGenerator()
try:
# This should NOT crash
result = generator._create_grid(real_vector_words)
if result is None:
print("β οΈ Grid creation returned None (no successful placement)")
else:
print(f"β
Grid creation succeeded with {len(result['placed_words'])} placed words")
assert "grid" in result
assert "clues" in result
assert "placed_words" in result
except IndexError as e:
pytest.fail(f"β IndexError in grid creation: {e}")
except Exception as e:
# Other exceptions are okay (e.g., timeout, no intersections found)
print(f"βΉοΈ Grid creation failed with non-index error: {e}")
def test_backtrack_placement_bounds(self, real_vector_words):
"""Test that backtracking placement handles bounds correctly."""
generator = CrosswordGenerator()
# Create grid
grid = [["." for _ in range(15)] for _ in range(15)]
placed_words = []
# Extract word list
word_list = [w["word"].upper() for w in real_vector_words]
word_list.sort(key=len, reverse=True)
try:
# Test backtracking - should not crash even if no solution found
result = generator._backtrack_placement(
grid, word_list, real_vector_words, 0, placed_words,
start_time=0, timeout=1.0 # Short timeout
)
print(f"β
Backtrack placement completed without IndexError, result: {result}")
except IndexError as e:
pytest.fail(f"β IndexError in backtrack placement: {e}")
except Exception as e:
# Other exceptions are okay (timeout, etc.)
print(f"βΉοΈ Backtrack placement failed with non-index error: {e}")
def test_intersection_placement_edge_cases(self):
"""Test intersection placement calculations with edge cases."""
generator = CrosswordGenerator()
# Create grid with a word already placed
grid = [["." for _ in range(10)] for _ in range(10)]
# Place "TEST" horizontally at (5, 2)
for i, letter in enumerate("TEST"):
grid[5][2 + i] = letter
placed_words = [{
"word": "TEST",
"row": 5,
"col": 2,
"direction": "horizontal",
"number": 1
}]
# Test words that might cause out-of-bounds access
test_words = ["VETERINARY", "ZOOLOGICAL", "ELEPHANT", "T", "AT", "STRESS"]
for word in test_words:
try:
placements = generator._find_all_intersection_placements(grid, word, placed_words)
print(f"β
Found {len(placements)} intersection placements for '{word}'")
# Test each placement
for placement in placements:
try:
score = generator._calculate_placement_score(grid, word, placement, placed_words)
print(f" - Placement at ({placement['row']},{placement['col']}) {placement['direction']}: score {score}")
except IndexError as e:
pytest.fail(f"β IndexError calculating score for {word}: {e}")
except IndexError as e:
pytest.fail(f"β IndexError finding intersections for {word}: {e}")
@pytest.mark.asyncio
async def test_full_puzzle_generation_stress(self, real_vector_words):
"""Stress test full puzzle generation with problematic data."""
generator = CrosswordGenerator()
# Mock vector service
mock_vector_service = Mock()
mock_vector_service.find_similar_words = Mock(return_value=real_vector_words)
generator.vector_service = mock_vector_service
try:
# This should complete without IndexError
result = await generator.generate_puzzle(["Animals"], "medium", True)
if result is None:
print("β οΈ Puzzle generation returned None")
else:
print(f"β
Full puzzle generation succeeded!")
assert "grid" in result
assert "clues" in result
assert "metadata" in result
except IndexError as e:
pytest.fail(f"β IndexError in full puzzle generation: {e}")
except Exception as e:
# Other exceptions might be okay
print(f"βΉοΈ Puzzle generation failed with non-index error: {e}")
def test_edge_case_grids(self):
"""Test edge cases with different grid sizes and word combinations."""
generator = CrosswordGenerator()
edge_cases = [
# Very small grid
{"grid_size": 3, "words": ["CAT", "DOG"]},
# Single cell grid
{"grid_size": 1, "words": ["A"]},
# Large grid with short words
{"grid_size": 20, "words": ["A", "I", "IT", "AT"]},
# Small grid with long words
{"grid_size": 5, "words": ["SUPERCALIFRAGILISTICEXPIALIDOCIOUS"]},
]
for case in edge_cases:
grid = [["." for _ in range(case["grid_size"])] for _ in range(case["grid_size"])]
placed_words = []
for word in case["words"]:
try:
# Test various placement attempts
for row in range(case["grid_size"]):
for col in range(case["grid_size"]):
for direction in ["horizontal", "vertical"]:
placement = {"row": row, "col": col, "direction": direction}
# These should not crash
can_place = generator._can_place_word(grid, word, row, col, direction)
score = generator._calculate_placement_score(grid, word, placement, placed_words)
assert isinstance(can_place, bool)
assert isinstance(score, int)
except IndexError as e:
pytest.fail(f"β IndexError with grid_size={case['grid_size']}, word='{word}': {e}")
if __name__ == "__main__":
# Run just these specific tests
pytest.main([__file__, "-v", "--tb=short"]) |