|
1 | 1 | import os
|
2 |
| -import shutil |
3 | 2 | import tempfile
|
4 | 3 | import unittest
|
| 4 | +from unittest.mock import patch |
5 | 5 |
|
6 |
| -from seer.automation.codebase.code_search import CodeSearcher, SearchResult |
| 6 | +from seer.automation.codebase.code_search import CodeSearcher |
| 7 | +from seer.automation.codebase.models import SearchResult |
7 | 8 |
|
8 | 9 |
|
9 | 10 | class TestCodeSearcher(unittest.TestCase):
|
10 | 11 | def setUp(self):
|
11 |
| - # Create a temporary directory structure for testing |
12 |
| - self.test_dir = tempfile.mkdtemp() |
13 |
| - self.create_test_files() |
| 12 | + self.temp_dir = tempfile.mkdtemp() |
| 13 | + self.supported_extensions = {".py", ".js", ".ts"} |
14 | 14 | self.code_searcher = CodeSearcher(
|
15 |
| - self.test_dir, {".txt"}, start_path=os.path.join(self.test_dir, "folder1") |
| 15 | + directory=self.temp_dir, |
| 16 | + supported_extensions=self.supported_extensions, |
| 17 | + max_results=5, |
16 | 18 | )
|
17 | 19 |
|
18 |
| - def tearDown(self): |
19 |
| - # Remove the temporary directory after tests |
20 |
| - shutil.rmtree(self.test_dir) |
21 |
| - |
22 |
| - def create_test_files(self): |
23 |
| - # Create a simple directory structure with test files |
24 |
| - os.makedirs(os.path.join(self.test_dir, "folder1")) |
25 |
| - os.makedirs(os.path.join(self.test_dir, "folder2")) |
26 |
| - os.makedirs(os.path.join(self.test_dir, "folder1", "subfolder")) |
27 |
| - |
28 |
| - with open(os.path.join(self.test_dir, "folder1", "file1.txt"), "w") as f: |
29 |
| - f.write("This is file1 with keyword") |
30 |
| - |
31 |
| - with open(os.path.join(self.test_dir, "folder2", "file2.txt"), "w") as f: |
32 |
| - f.write("This is file2 with keyword") |
33 |
| - |
34 |
| - with open(os.path.join(self.test_dir, "root_file.txt"), "w") as f: |
35 |
| - f.write("This is root_file with keyword") |
36 |
| - |
37 |
| - with open(os.path.join(self.test_dir, "folder1", "subfolder", "file3.txt"), "w") as f: |
38 |
| - f.write("This is file3 with keyword") |
39 |
| - |
40 |
| - with open(os.path.join(self.test_dir, "folder1", "subfolder", "file4.txt"), "w") as f: |
41 |
| - f.write("This is file4 without") |
42 |
| - |
43 |
| - def test_calculate_proximity_score(self): |
44 |
| - score1 = self.code_searcher.calculate_proximity_score( |
45 |
| - os.path.join(self.test_dir, "folder1", "file1.txt") |
46 |
| - ) |
47 |
| - score2 = self.code_searcher.calculate_proximity_score( |
48 |
| - os.path.join(self.test_dir, "folder1", "subfolder", "file3.txt") |
49 |
| - ) |
50 |
| - score3 = self.code_searcher.calculate_proximity_score( |
51 |
| - os.path.join(self.test_dir, "folder2", "file2.txt") |
52 |
| - ) |
53 |
| - score4 = self.code_searcher.calculate_proximity_score( |
54 |
| - os.path.join(self.test_dir, "root_file.txt") |
55 |
| - ) |
56 |
| - |
57 |
| - assert score1 >= score2 >= score3 >= score4, "Scores should be in descending order" |
58 |
| - |
59 |
| - def test_calculate_proximity_score_for_file_proximity(self): |
60 |
| - self.code_searcher.start_path = os.path.join(self.test_dir, "folder1", "file1.txt") |
61 |
| - score1 = self.code_searcher.calculate_proximity_score( |
62 |
| - os.path.join(self.test_dir, "folder1", "file1.txt") |
63 |
| - ) |
64 |
| - score2 = self.code_searcher.calculate_proximity_score( |
65 |
| - os.path.join(self.test_dir, "root_file.txt") |
66 |
| - ) |
67 |
| - score3 = self.code_searcher.calculate_proximity_score( |
68 |
| - os.path.join(self.test_dir, "folder1", "subfolder", "file3.txt") |
69 |
| - ) |
70 |
| - score4 = self.code_searcher.calculate_proximity_score( |
71 |
| - os.path.join(self.test_dir, "folder2", "file2.txt") |
72 |
| - ) |
73 |
| - |
74 |
| - assert score1 >= score2 >= score3 >= score4, "Scores should be in descending order" |
75 |
| - |
76 |
| - def test_search_file(self): |
77 |
| - result = self.code_searcher.search_file( |
78 |
| - os.path.join(self.test_dir, "folder1", "file1.txt"), "keyword" |
79 |
| - ) |
80 |
| - assert isinstance(result, SearchResult), "Should return a SearchResult object" |
81 |
| - assert result.relative_path == os.path.join( |
82 |
| - "folder1", "file1.txt" |
83 |
| - ), "Relative path should be correct" |
84 |
| - assert len(result.matches) == 1, "Should find one match" |
85 |
| - assert result.matches[0].line_number == 1, "Match should be on the first line" |
86 |
| - assert "keyword" in result.matches[0].context, "Context should contain the keyword" |
87 |
| - |
88 |
| - def test_search_file_max_size(self): |
89 |
| - self.code_searcher.max_file_size_bytes = 1 |
90 |
| - result = self.code_searcher.search_file( |
91 |
| - os.path.join(self.test_dir, "folder1", "file1.txt"), "keyword" |
92 |
| - ) |
93 |
| - assert result is None, "Should return None for files exceeding max size" |
94 |
| - |
95 |
| - def test_search(self): |
96 |
| - results = self.code_searcher.search("keyword") |
97 |
| - assert len(results) == 4, "Should find keyword in four files but not the fifth" |
98 |
| - assert ( |
99 |
| - results[0].score >= results[1].score >= results[2].score |
100 |
| - ), "Results should be sorted by score" |
| 20 | + # Create a test file |
| 21 | + self.test_file_path = os.path.join(self.temp_dir, "test_file.py") |
| 22 | + with open(self.test_file_path, "w") as f: |
| 23 | + f.write("def test_function():\n print('Hello, World!')\n") |
101 | 24 |
|
102 |
| - def test_max_results(self): |
103 |
| - code_searcher = CodeSearcher( |
104 |
| - self.test_dir, |
105 |
| - {".txt"}, |
106 |
| - max_results=2, |
107 |
| - start_path=os.path.join(self.test_dir, "folder1"), |
108 |
| - ) |
109 |
| - results = code_searcher.search("keyword") |
110 |
| - assert len(results) == 2, "Should return only max_results number of results" |
111 |
| - |
112 |
| - def test_supported_extensions(self): |
113 |
| - # Create a file with unsupported extension |
114 |
| - with open(os.path.join(self.test_dir, "unsupported.dat"), "w") as f: |
115 |
| - f.write("This file has keyword but unsupported extension") |
116 |
| - |
117 |
| - results = self.code_searcher.search("keyword") |
118 |
| - unsupported_found = any("unsupported.dat" in result.relative_path for result in results) |
119 |
| - assert not unsupported_found, "Should not find keyword in unsupported file types" |
120 |
| - |
121 |
| - def test_no_start_path(self): |
122 |
| - code_searcher_no_start = CodeSearcher(self.test_dir, {".txt"}) |
123 |
| - results = code_searcher_no_start.search("keyword") |
124 |
| - assert all( |
125 |
| - result.score == 1.0 for result in results |
126 |
| - ), "All scores should be 1.0 when no start_path is provided" |
127 |
| - |
128 |
| - def test_keyword_not_found(self): |
129 |
| - results = self.code_searcher.search("nonexistent") |
130 |
| - assert len(results) == 0, "Should return empty list when keyword is not found" |
131 |
| - |
132 |
| - def test_read_file_with_encoding(self): |
133 |
| - # Test UTF-8 file |
134 |
| - with open(os.path.join(self.test_dir, "utf8.txt"), "w", encoding="utf-8") as f: |
135 |
| - f.write("Hello in UTF-8 🌍") |
136 |
| - |
137 |
| - # Test different encoding |
138 |
| - with open(os.path.join(self.test_dir, "latin1.txt"), "wb") as f: |
139 |
| - f.write("Hello in Latin-1 é".encode("latin-1")) |
140 |
| - |
141 |
| - # Test file with different encoding than default |
142 |
| - result1 = self.code_searcher.search_file(os.path.join(self.test_dir, "utf8.txt"), "Hello") |
143 |
| - result2 = self.code_searcher.search_file(os.path.join(self.test_dir, "latin1.txt"), "Hello") |
144 |
| - |
145 |
| - assert result1 is not None, "Should read UTF-8 file" |
146 |
| - assert result2 is not None, "Should read Latin-1 file" |
147 |
| - assert "Hello in UTF-8" in result1.matches[0].context |
148 |
| - assert "Hello in Latin-1" in result2.matches[0].context |
149 |
| - |
150 |
| - def test_read_file_with_invalid_encoding(self): |
151 |
| - # Create a binary file that's not valid in any text encoding |
152 |
| - with open(os.path.join(self.test_dir, "binary.txt"), "wb") as f: |
153 |
| - f.write(bytes([0xFF, 0xFE, 0x00, 0x00])) # Invalid UTF-8 |
154 |
| - |
155 |
| - result = self.code_searcher.search_file( |
156 |
| - os.path.join(self.test_dir, "binary.txt"), "keyword" |
157 |
| - ) |
158 |
| - assert result is None, "Should return None for unreadable files" |
159 |
| - |
160 |
| - def test_custom_default_encoding(self): |
161 |
| - # Create a file with specific encoding |
162 |
| - test_text = "Test with special char é" |
163 |
| - with open(os.path.join(self.test_dir, "special.txt"), "w", encoding="latin-1") as f: |
164 |
| - f.write(test_text) |
165 |
| - |
166 |
| - # Create searcher with latin-1 as default encoding |
167 |
| - latin1_searcher = CodeSearcher(self.test_dir, {".txt"}, default_encoding="latin-1") |
168 |
| - |
169 |
| - result = latin1_searcher.search_file(os.path.join(self.test_dir, "special.txt"), "special") |
170 |
| - |
171 |
| - assert result is not None, "Should read file with custom default encoding" |
172 |
| - assert "special char" in result.matches[0].context |
173 |
| - |
174 |
| - def test_empty_file(self): |
175 |
| - # Test handling of empty files |
176 |
| - empty_file = os.path.join(self.test_dir, "empty.txt") |
177 |
| - with open(empty_file, "w"): |
178 |
| - pass |
179 |
| - |
180 |
| - result = self.code_searcher.search_file(empty_file, "keyword") |
181 |
| - assert result is None, "Should handle empty files gracefully" |
182 |
| - |
183 |
| - def test_max_context_characters(self): |
184 |
| - # Create a file with a long content |
185 |
| - long_content = "prefix " * 100 + "keyword" + " suffix" * 100 |
186 |
| - test_file = os.path.join(self.test_dir, "long.txt") |
187 |
| - with open(test_file, "w") as f: |
188 |
| - f.write(long_content) |
189 |
| - |
190 |
| - # Create searcher with limited context |
191 |
| - limited_searcher = CodeSearcher(self.test_dir, {".txt"}, max_context_characters=20) |
192 |
| - |
193 |
| - result = limited_searcher.search_file(test_file, "keyword") |
194 |
| - assert result is not None, "Should find the keyword" |
195 |
| - assert len(result.matches) == 1, "Should have one match" |
196 |
| - assert ( |
197 |
| - len(result.matches[0].context) <= 23 |
198 |
| - ), "Context should be limited to max_context_characters + '...'" |
199 |
| - assert result.matches[0].context.endswith("..."), "Truncated context should end with ..." |
200 |
| - |
201 |
| - # Test with larger limit |
202 |
| - larger_searcher = CodeSearcher(self.test_dir, {".txt"}, max_context_characters=1000) |
203 |
| - result = larger_searcher.search_file(test_file, "keyword") |
204 |
| - assert result is not None, "Should find the keyword" |
205 |
| - assert ( |
206 |
| - len(result.matches[0].context) <= 1003 |
207 |
| - ), "Context should be limited to max_context_characters + '...'" |
208 |
| - assert "keyword" in result.matches[0].context, "Context should contain the keyword" |
| 25 | + def tearDown(self): |
| 26 | + # Clean up temporary files |
| 27 | + if os.path.exists(self.test_file_path): |
| 28 | + os.remove(self.test_file_path) |
| 29 | + os.rmdir(self.temp_dir) |
| 30 | + |
| 31 | + def test_search_file_exists(self): |
| 32 | + # Test searching for a keyword in an existing file |
| 33 | + result = self.code_searcher.search_file(self.test_file_path, "Hello") |
| 34 | + self.assertIsInstance(result, SearchResult) |
| 35 | + self.assertEqual(result.relative_path, "test_file.py") |
| 36 | + self.assertEqual(len(result.matches), 1) |
| 37 | + |
| 38 | + def test_search_file_does_not_exist(self): |
| 39 | + # Test searching for a keyword in a non-existent file |
| 40 | + non_existent_file = os.path.join(self.temp_dir, "non_existent.py") |
| 41 | + result = self.code_searcher.search_file(non_existent_file, "keyword") |
| 42 | + self.assertIsNone(result) |
| 43 | + |
| 44 | + def test_search_with_multiple_files(self): |
| 45 | + # Create another test file |
| 46 | + test_file2_path = os.path.join(self.temp_dir, "test_file2.py") |
| 47 | + with open(test_file2_path, "w") as f: |
| 48 | + f.write("# This is another test file\ndef another_function():\n return 'Another Test'") |
| 49 | + |
| 50 | + try: |
| 51 | + # Test searching for a keyword across multiple files |
| 52 | + results = self.code_searcher.search("function") |
| 53 | + self.assertEqual(len(results), 2) # Should find matches in both files |
| 54 | + finally: |
| 55 | + # Clean up |
| 56 | + if os.path.exists(test_file2_path): |
| 57 | + os.remove(test_file2_path) |
| 58 | + |
| 59 | + @patch('os.path.exists') |
| 60 | + @patch('os.path.getsize') |
| 61 | + def test_missing_file_handling(self, mock_getsize, mock_exists): |
| 62 | + # Simulate a file that doesn't exist |
| 63 | + mock_exists.return_value = False |
| 64 | + |
| 65 | + # This should not raise an error even though getsize would raise FileNotFoundError |
| 66 | + result = self.code_searcher.search_file("/path/to/nonexistent/file.py", "keyword") |
| 67 | + |
| 68 | + # Verify the result is None |
| 69 | + self.assertIsNone(result) |
| 70 | + |
| 71 | + # Verify getsize was never called |
| 72 | + mock_getsize.assert_not_called() |
0 commit comments