-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Expand file tree
/
Copy pathtest_search_replace.py
More file actions
373 lines (301 loc) · 13 KB
/
test_search_replace.py
File metadata and controls
373 lines (301 loc) · 13 KB
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
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
# flake8: noqa: E501
import unittest
from aider.coders.search_replace import (
RelativeIndenter,
dmp_apply,
line_pad,
line_unpad,
reverse_lines,
search_and_replace,
strip_blank_lines,
flexible_search_and_replace,
try_strategy,
)
class TestSearchAndReplace(unittest.TestCase):
"""Tests for the search_and_replace() function."""
def test_exact_match_single_occurrence(self):
texts = ("hello world\n", "hello universe\n", "hello world\n")
result = search_and_replace(texts)
self.assertEqual(result, "hello universe\n")
def test_exact_match_multiple_occurrences(self):
original = "foo\nbar\nfoo\n"
texts = ("foo\n", "baz\n", original)
result = search_and_replace(texts)
# replaces ALL occurrences
self.assertEqual(result, "baz\nbar\nbaz\n")
def test_no_match_returns_none(self):
texts = ("missing\n", "replacement\n", "hello world\n")
result = search_and_replace(texts)
self.assertIsNone(result)
def test_empty_search_in_nonempty_original(self):
# empty string is contained 0 times? Actually "".count("") returns len+1
# but the function checks num == 0, so let's see
texts = ("", "added\n", "original\n")
result = search_and_replace(texts)
# "original\n".count("") == 10, which is > 0, so it proceeds
self.assertIsNotNone(result)
def test_multiline_search_replace(self):
search = "line1\nline2\n"
replace = "new1\nnew2\nnew3\n"
original = "before\nline1\nline2\nafter\n"
texts = (search, replace, original)
result = search_and_replace(texts)
self.assertEqual(result, "before\nnew1\nnew2\nnew3\nafter\n")
def test_unicode_content(self):
search = "caf\u00e9\n"
replace = "coffee\n"
original = "I like caf\u00e9\n"
texts = (search, replace, original)
result = search_and_replace(texts)
self.assertEqual(result, "I like coffee\n")
def test_special_characters(self):
search = "a+b*c\n"
replace = "x+y*z\n"
original = "result = a+b*c\n"
texts = (search, replace, original)
result = search_and_replace(texts)
self.assertEqual(result, "result = x+y*z\n")
class TestRelativeIndenter(unittest.TestCase):
"""Tests for the RelativeIndenter class."""
def test_basic_roundtrip(self):
text = " foo\n bar\n baz\n qux\n"
ri = RelativeIndenter([text])
relative = ri.make_relative(text)
absolute = ri.make_absolute(relative)
self.assertEqual(absolute, text)
def test_no_indentation(self):
text = "foo\nbar\nbaz\n"
ri = RelativeIndenter([text])
relative = ri.make_relative(text)
absolute = ri.make_absolute(relative)
self.assertEqual(absolute, text)
def test_increasing_indent(self):
text = "a\n b\n c\n"
ri = RelativeIndenter([text])
relative = ri.make_relative(text)
# make_relative should produce relative changes
# round trip should recover original
absolute = ri.make_absolute(relative)
self.assertEqual(absolute, text)
def test_decreasing_indent_uses_marker(self):
text = " foo\n bar\n"
ri = RelativeIndenter([text])
relative = ri.make_relative(text)
# bar is outdented by 4, so marker should appear
self.assertIn(ri.marker, relative)
absolute = ri.make_absolute(relative)
self.assertEqual(absolute, text)
def test_mixed_indent_roundtrip(self):
text = "def foo():\n if True:\n pass\n else:\n return\n"
ri = RelativeIndenter([text])
relative = ri.make_relative(text)
absolute = ri.make_absolute(relative)
self.assertEqual(absolute, text)
def test_default_marker_is_arrow(self):
ri = RelativeIndenter(["hello\n"])
self.assertEqual(ri.marker, "\u2190")
def test_custom_marker_when_arrow_present(self):
# text contains the arrow character, so a different marker should be chosen
text = "hello \u2190 world\n"
ri = RelativeIndenter([text])
self.assertNotEqual(ri.marker, "\u2190")
def test_blank_lines_preserved(self):
text = "foo\n\n bar\n"
ri = RelativeIndenter([text])
relative = ri.make_relative(text)
absolute = ri.make_absolute(relative)
self.assertEqual(absolute, text)
def test_make_relative_raises_if_marker_present(self):
ri = RelativeIndenter(["hello\n"])
text_with_marker = f"some {ri.marker} text\n"
with self.assertRaises(ValueError):
ri.make_relative(text_with_marker)
def test_make_absolute_odd_number_of_lines_raises(self):
# make_absolute expects pairs of lines (indent + content), so an odd
# count should cause an IndexError when accessing lines[i+1]
ri = RelativeIndenter(["hello\n"])
bad_relative = "\n" + "foo\n" + "extra\n" # 3 lines = odd, will go out of bounds
with self.assertRaises(IndexError):
ri.make_absolute(bad_relative)
def test_multiple_texts_in_constructor(self):
texts = ["foo\n bar\n", " baz\n qux\n"]
ri = RelativeIndenter(texts)
for text in texts:
relative = ri.make_relative(text)
absolute = ri.make_absolute(relative)
self.assertEqual(absolute, text)
class TestLinePadUnpad(unittest.TestCase):
"""Tests for line_pad() and line_unpad()."""
def test_pad_unpad_roundtrip(self):
text = "hello world\n"
padded = line_pad(text)
unpadded = line_unpad(padded)
self.assertEqual(unpadded, text)
def test_pad_adds_newlines(self):
text = "content"
padded = line_pad(text)
self.assertTrue(padded.startswith("\n"))
self.assertTrue(padded.endswith("\n"))
def test_unpad_returns_none_on_invalid(self):
result = line_unpad("not padded text")
self.assertIsNone(result)
def test_empty_string_roundtrip(self):
text = ""
padded = line_pad(text)
unpadded = line_unpad(padded)
self.assertEqual(unpadded, text)
class TestReverseLines(unittest.TestCase):
"""Tests for reverse_lines()."""
def test_reverse_simple(self):
text = "a\nb\nc\n"
result = reverse_lines(text)
# splitlines(keepends=True) gives ["a\n", "b\n", "c\n"]
# reversed: ["c\n", "b\n", "a\n"]
self.assertEqual(result, "c\nb\na\n")
def test_reverse_single_line(self):
text = "only\n"
result = reverse_lines(text)
self.assertEqual(result, "only\n")
def test_reverse_double_reverse_is_identity(self):
text = "first\nsecond\nthird\n"
self.assertEqual(reverse_lines(reverse_lines(text)), text)
class TestStripBlankLines(unittest.TestCase):
"""Tests for strip_blank_lines()."""
def test_strips_leading_trailing_newlines(self):
texts = ["\n\nhello\nworld\n\n\n"]
result = strip_blank_lines(texts)
self.assertEqual(result, ["hello\nworld\n"])
def test_preserves_internal_blank_lines(self):
texts = ["hello\n\nworld\n"]
result = strip_blank_lines(texts)
# internal blank line between hello and world should remain
self.assertEqual(result, ["hello\n\nworld\n"])
def test_multiple_texts(self):
texts = ["\nfoo\n\n", "\nbar\n\n"]
result = strip_blank_lines(texts)
self.assertEqual(result, ["foo\n", "bar\n"])
def test_ensures_trailing_newline(self):
texts = ["hello"]
result = strip_blank_lines(texts)
self.assertTrue(result[0].endswith("\n"))
class TestFlexibleSearchAndReplace(unittest.TestCase):
"""Tests for flexible_search_and_replace() with various strategies."""
def test_exact_match_strategy(self):
search = "old line\n"
replace = "new line\n"
original = "before\nold line\nafter\n"
texts = (search, replace, original)
# Use only the simplest strategy
strategies = [
(search_and_replace, [(False, False, False)]),
]
result = flexible_search_and_replace(texts, strategies)
self.assertEqual(result, "before\nnew line\nafter\n")
def test_returns_none_when_no_strategy_works(self):
texts = ("not found\n", "replacement\n", "original text\n")
strategies = [
(search_and_replace, [(False, False, False)]),
]
result = flexible_search_and_replace(texts, strategies)
self.assertIsNone(result)
def test_strip_blank_lines_preproc_helps(self):
search = "\nold line\n\n"
replace = "\nnew line\n\n"
original = "old line\n"
texts = (search, replace, original)
# Without strip_blank_lines, exact match fails
strategies_no_strip = [
(search_and_replace, [(False, False, False)]),
]
result = flexible_search_and_replace(texts, strategies_no_strip)
self.assertIsNone(result)
# With strip_blank_lines, it should work
strategies_with_strip = [
(search_and_replace, [(True, False, False)]),
]
result = flexible_search_and_replace(texts, strategies_with_strip)
self.assertIsNotNone(result)
class TestTryStrategy(unittest.TestCase):
"""Tests for try_strategy()."""
def test_basic_no_preproc(self):
texts = ("old\n", "new\n", "old\n")
preproc = (False, False, False)
result = try_strategy(texts, search_and_replace, preproc)
self.assertEqual(result, "new\n")
def test_with_strip_blank_lines(self):
texts = ("\nold\n\n", "\nnew\n\n", "old\n")
preproc = (True, False, False)
result = try_strategy(texts, search_and_replace, preproc)
self.assertIsNotNone(result)
def test_with_relative_indent(self):
texts = (" foo\n bar\n", " foo\n baz\n", " foo\n bar\n")
preproc = (False, True, False)
result = try_strategy(texts, search_and_replace, preproc)
self.assertIsNotNone(result)
def test_returns_none_on_failure(self):
texts = ("missing\n", "replacement\n", "original\n")
preproc = (False, False, False)
result = try_strategy(texts, search_and_replace, preproc)
self.assertIsNone(result)
class TestDmpApply(unittest.TestCase):
"""Tests for dmp_apply() - the diff-match-patch based approach."""
def test_identical_search_and_original(self):
text = "hello world\nfoo bar\n"
replace = "hello universe\nfoo bar\n"
texts = (text, replace, text)
result = dmp_apply(texts)
self.assertIsNotNone(result)
self.assertIn("universe", result)
def test_returns_none_on_complete_mismatch(self):
texts = ("aaa\n", "bbb\n", "completely different text that has nothing in common\n")
result = dmp_apply(texts, remap=False)
# May or may not return None depending on DMP tolerance, but shouldn't crash
# Just verify it doesn't raise
class TestEdgeCases(unittest.TestCase):
"""Edge case tests across search_replace functions."""
def test_search_and_replace_whitespace_only(self):
texts = (" \n", " \n", " \n")
result = search_and_replace(texts)
self.assertEqual(result, " \n")
def test_search_and_replace_empty_strings(self):
texts = ("", "", "")
result = search_and_replace(texts)
# "".count("") == 1, not 0, so it proceeds
self.assertEqual(result, "")
def test_search_and_replace_newlines_only(self):
texts = ("\n\n", "\n", "\n\n\n\n")
result = search_and_replace(texts)
self.assertIsNotNone(result)
def test_relativeindenter_tabs(self):
text = "foo\n\tbar\n\t\tbaz\n"
ri = RelativeIndenter([text])
relative = ri.make_relative(text)
absolute = ri.make_absolute(relative)
self.assertEqual(absolute, text)
def test_relativeindenter_empty_text(self):
text = ""
ri = RelativeIndenter([text])
relative = ri.make_relative(text)
absolute = ri.make_absolute(relative)
self.assertEqual(absolute, text)
def test_relativeindenter_single_line_no_newline(self):
text = "hello"
ri = RelativeIndenter([text])
relative = ri.make_relative(text)
# single line without newline - just verify no crash
self.assertIsNotNone(relative)
def test_search_replace_preserves_surrounding_content(self):
original = "line1\nline2\nline3\nline4\nline5\n"
search = "line3\n"
replace = "LINE_THREE\n"
texts = (search, replace, original)
result = search_and_replace(texts)
self.assertEqual(result, "line1\nline2\nLINE_THREE\nline4\nline5\n")
def test_reverse_lines_empty(self):
self.assertEqual(reverse_lines(""), "")
def test_strip_blank_lines_only_newlines(self):
texts = ["\n\n\n"]
result = strip_blank_lines(texts)
self.assertEqual(result, ["\n"])
if __name__ == "__main__":
unittest.main()