# flake8: noqa: E501 import tempfile import unittest from pathlib import Path from unittest.mock import MagicMock, patch from aider.coders import Coder from aider.coders import editblock_coder as eb from aider.dump import dump # noqa: F401 from aider.io import InputOutput from aider.models import Model from aider.utils import ChdirTemporaryDirectory class TestUtils(unittest.TestCase): def setUp(self): self.GPT35 = Model("gpt-3.5-turbo") def test_find_filename(self): fence = ("```", "```") valid_fnames = ["file1.py", "file2.py", "dir/file3.py", r"\windows\__init__.py"] # Test with filename on a single line lines = ["file1.py", "```"] self.assertEqual(eb.find_filename(lines, fence, valid_fnames), "file1.py") # Test with filename in fence lines = ["```python", "file3.py", "```"] self.assertEqual(eb.find_filename(lines, fence, valid_fnames), "dir/file3.py") # Test with no valid filename lines = ["```", "invalid_file.py", "```"] self.assertEqual("invalid_file.py", eb.find_filename(lines, fence, valid_fnames)) # Test with multiple fences lines = ["```python", "file1.py", "```", "```", "file2.py", "```"] self.assertEqual(eb.find_filename(lines, fence, valid_fnames), "file2.py") # Test with filename having extra characters lines = ["# file1.py", "```"] self.assertEqual(eb.find_filename(lines, fence, valid_fnames), "file1.py") # Test with fuzzy matching lines = ["file1_py", "```"] self.assertEqual(eb.find_filename(lines, fence, valid_fnames), "file1.py") # Test with fuzzy matching lines = [r"\windows__init__.py", "```"] self.assertEqual(eb.find_filename(lines, fence, valid_fnames), r"\windows\__init__.py") # fuzzy logic disabled v0.11.2-dev def __test_replace_most_similar_chunk(self): whole = "This is a sample text.\nAnother line of text.\nYet another line.\n" part = "This is a sample text\n" replace = "This is a replaced text.\n" expected_output = "This is a replaced text.\nAnother line of text.\nYet another line.\n" result = eb.replace_most_similar_chunk(whole, part, replace) self.assertEqual(result, expected_output) # fuzzy logic disabled v0.11.2-dev def __test_replace_most_similar_chunk_not_perfect_match(self): whole = "This is a sample text.\nAnother line of text.\nYet another line.\n" part = "This was a sample text.\nAnother line of txt\n" replace = "This is a replaced text.\nModified line of text.\n" expected_output = "This is a replaced text.\nModified line of text.\nYet another line.\n" result = eb.replace_most_similar_chunk(whole, part, replace) self.assertEqual(result, expected_output) def test_strip_quoted_wrapping(self): input_text = ( "filename.ext\n```\nWe just want this content\nNot the filename and triple quotes\n```" ) expected_output = "We just want this content\nNot the filename and triple quotes\n" result = eb.strip_quoted_wrapping(input_text, "filename.ext") self.assertEqual(result, expected_output) def test_strip_quoted_wrapping_no_filename(self): input_text = "```\nWe just want this content\nNot the triple quotes\n```" expected_output = "We just want this content\nNot the triple quotes\n" result = eb.strip_quoted_wrapping(input_text) self.assertEqual(result, expected_output) def test_strip_quoted_wrapping_no_wrapping(self): input_text = "We just want this content\nNot the triple quotes\n" expected_output = "We just want this content\nNot the triple quotes\n" result = eb.strip_quoted_wrapping(input_text) self.assertEqual(result, expected_output) def test_find_original_update_blocks(self): edit = """ Here's the change: ```text foo.txt <<<<<<< SEARCH Two ======= Tooooo >>>>>>> REPLACE ``` Hope you like it! """ edits = list(eb.find_original_update_blocks(edit)) self.assertEqual(edits, [("foo.txt", "Two\n", "Tooooo\n")]) def test_find_original_update_blocks_quote_below_filename(self): edit = """ Here's the change: foo.txt ```text <<<<<<< SEARCH Two ======= Tooooo >>>>>>> REPLACE ``` Hope you like it! """ edits = list(eb.find_original_update_blocks(edit)) self.assertEqual(edits, [("foo.txt", "Two\n", "Tooooo\n")]) def test_find_original_update_blocks_unclosed(self): edit = """ Here's the change: ```text foo.txt <<<<<<< SEARCH Two ======= Tooooo oops! """ with self.assertRaises(ValueError) as cm: list(eb.find_original_update_blocks(edit)) self.assertIn("Expected `>>>>>>> REPLACE` or `=======`", str(cm.exception)) def test_find_original_update_blocks_missing_filename(self): edit = """ Here's the change: ```text <<<<<<< SEARCH Two ======= Tooooo oops! >>>>>>> REPLACE """ with self.assertRaises(ValueError) as cm: _blocks = list(eb.find_original_update_blocks(edit)) self.assertIn("filename", str(cm.exception)) def test_find_original_update_blocks_no_final_newline(self): edit = """ aider/coder.py <<<<<<< SEARCH self.console.print("[red]^C again to quit") ======= self.io.tool_error("^C again to quit") >>>>>>> REPLACE aider/coder.py <<<<<<< SEARCH self.io.tool_error("Malformed ORIGINAL/UPDATE blocks, retrying...") self.io.tool_error(err) ======= self.io.tool_error("Malformed ORIGINAL/UPDATE blocks, retrying...") self.io.tool_error(str(err)) >>>>>>> REPLACE aider/coder.py <<<<<<< SEARCH self.console.print("[red]Unable to get commit message from gpt-3.5-turbo. Use /commit to try again.\n") ======= self.io.tool_error("Unable to get commit message from gpt-3.5-turbo. Use /commit to try again.") >>>>>>> REPLACE aider/coder.py <<<<<<< SEARCH self.console.print("[red]Skipped commit.") ======= self.io.tool_error("Skipped commit.") >>>>>>> REPLACE""" # Should not raise a ValueError list(eb.find_original_update_blocks(edit)) def test_incomplete_edit_block_missing_filename(self): edit = """ No problem! Here are the changes to patch `subprocess.check_output` instead of `subprocess.run` in both tests: ```python tests/test_repomap.py <<<<<<< SEARCH def test_check_for_ctags_failure(self): with patch("subprocess.run") as mock_run: mock_run.side_effect = Exception("ctags not found") ======= def test_check_for_ctags_failure(self): with patch("subprocess.check_output") as mock_check_output: mock_check_output.side_effect = Exception("ctags not found") >>>>>>> REPLACE <<<<<<< SEARCH def test_check_for_ctags_success(self): with patch("subprocess.run") as mock_run: mock_run.return_value = CompletedProcess(args=["ctags", "--version"], returncode=0, stdout='''{ "_type": "tag", "name": "status", "path": "aider/main.py", "pattern": "/^ status = main()$/", "kind": "variable" }''') ======= def test_check_for_ctags_success(self): with patch("subprocess.check_output") as mock_check_output: mock_check_output.return_value = '''{ "_type": "tag", "name": "status", "path": "aider/main.py", "pattern": "/^ status = main()$/", "kind": "variable" }''' >>>>>>> REPLACE ``` These changes replace the `subprocess.run` patches with `subprocess.check_output` patches in both `test_check_for_ctags_failure` and `test_check_for_ctags_success` tests. """ edit_blocks = list(eb.find_original_update_blocks(edit)) self.assertEqual(len(edit_blocks), 2) # 2 edits self.assertEqual(edit_blocks[0][0], "tests/test_repomap.py") self.assertEqual(edit_blocks[1][0], "tests/test_repomap.py") def test_replace_part_with_missing_varied_leading_whitespace(self): whole = """ line1 line2 line3 line4 """ part = "line2\n line3\n" replace = "new_line2\n new_line3\n" expected_output = """ line1 new_line2 new_line3 line4 """ result = eb.replace_most_similar_chunk(whole, part, replace) self.assertEqual(result, expected_output) def test_replace_part_with_missing_leading_whitespace(self): whole = " line1\n line2\n line3\n" part = "line1\nline2\n" replace = "new_line1\nnew_line2\n" expected_output = " new_line1\n new_line2\n line3\n" result = eb.replace_most_similar_chunk(whole, part, replace) self.assertEqual(result, expected_output) def test_replace_multiple_matches(self): "only replace first occurrence" whole = "line1\nline2\nline1\nline3\n" part = "line1\n" replace = "new_line\n" expected_output = "new_line\nline2\nline1\nline3\n" result = eb.replace_most_similar_chunk(whole, part, replace) self.assertEqual(result, expected_output) def test_replace_multiple_matches_missing_whitespace(self): "only replace first occurrence" whole = " line1\n line2\n line1\n line3\n" part = "line1\n" replace = "new_line\n" expected_output = " new_line\n line2\n line1\n line3\n" result = eb.replace_most_similar_chunk(whole, part, replace) self.assertEqual(result, expected_output) def test_replace_part_with_just_some_missing_leading_whitespace(self): whole = " line1\n line2\n line3\n" part = " line1\n line2\n" replace = " new_line1\n new_line2\n" expected_output = " new_line1\n new_line2\n line3\n" result = eb.replace_most_similar_chunk(whole, part, replace) self.assertEqual(result, expected_output) def test_replace_part_with_missing_leading_whitespace_including_blank_line(self): """ The part has leading whitespace on all lines, so should be ignored. But it has a *blank* line with no whitespace at all, which was causing a bug per issue #25. Test case to repro and confirm fix. """ whole = " line1\n line2\n line3\n" part = "\n line1\n line2\n" replace = " new_line1\n new_line2\n" expected_output = " new_line1\n new_line2\n line3\n" result = eb.replace_most_similar_chunk(whole, part, replace) self.assertEqual(result, expected_output) def test_create_new_file_with_other_file_in_chat(self): # https://github.com/Aider-AI/aider/issues/2258 with ChdirTemporaryDirectory(): # Create a few temporary files file1 = "file.txt" with open(file1, "w", encoding="utf-8") as f: f.write("one\ntwo\nthree\n") files = [file1] # Initialize the Coder object with the mocked IO and mocked repo coder = Coder.create( self.GPT35, "diff", use_git=False, io=InputOutput(yes=True), fnames=files ) def mock_send(*args, **kwargs): coder.partial_response_content = f""" Do this: newfile.txt <<<<<<< SEARCH ======= creating a new file >>>>>>> REPLACE """ coder.partial_response_function_call = dict() return [] coder.send = mock_send coder.run(with_message="hi") content = Path(file1).read_text(encoding="utf-8") self.assertEqual(content, "one\ntwo\nthree\n") content = Path("newfile.txt").read_text(encoding="utf-8") self.assertEqual(content, "creating a new file\n") def test_full_edit(self): # Create a few temporary files _, file1 = tempfile.mkstemp() with open(file1, "w", encoding="utf-8") as f: f.write("one\ntwo\nthree\n") files = [file1] # Initialize the Coder object with the mocked IO and mocked repo coder = Coder.create(self.GPT35, "diff", io=InputOutput(), fnames=files) def mock_send(*args, **kwargs): coder.partial_response_content = f""" Do this: {Path(file1).name} <<<<<<< SEARCH two ======= new >>>>>>> REPLACE """ coder.partial_response_function_call = dict() return [] coder.send = mock_send # Call the run method with a message coder.run(with_message="hi") content = Path(file1).read_text(encoding="utf-8") self.assertEqual(content, "one\nnew\nthree\n") def test_full_edit_dry_run(self): # Create a few temporary files _, file1 = tempfile.mkstemp() orig_content = "one\ntwo\nthree\n" with open(file1, "w", encoding="utf-8") as f: f.write(orig_content) files = [file1] # Initialize the Coder object with the mocked IO and mocked repo coder = Coder.create( self.GPT35, "diff", io=InputOutput(dry_run=True), fnames=files, dry_run=True, ) def mock_send(*args, **kwargs): coder.partial_response_content = f""" Do this: {Path(file1).name} <<<<<<< SEARCH two ======= new >>>>>>> REPLACE """ coder.partial_response_function_call = dict() return [] coder.send = mock_send # Call the run method with a message coder.run(with_message="hi") content = Path(file1).read_text(encoding="utf-8") self.assertEqual(content, orig_content) def test_find_original_update_blocks_mupltiple_same_file(self): edit = """ Here's the change: ```text foo.txt <<<<<<< SEARCH one ======= two >>>>>>> REPLACE ... <<<<<<< SEARCH three ======= four >>>>>>> REPLACE ``` Hope you like it! """ edits = list(eb.find_original_update_blocks(edit)) self.assertEqual( edits, [ ("foo.txt", "one\n", "two\n"), ("foo.txt", "three\n", "four\n"), ], ) def test_deepseek_coder_v2_filename_mangling(self): edit = """ Here's the change: ```python foo.txt ``` ```python <<<<<<< SEARCH one ======= two >>>>>>> REPLACE ``` Hope you like it! """ edits = list(eb.find_original_update_blocks(edit)) self.assertEqual( edits, [ ("foo.txt", "one\n", "two\n"), ], ) def test_new_file_created_in_same_folder(self): edit = """ Here's the change: path/to/a/file2.txt ```python <<<<<<< SEARCH ======= three >>>>>>> REPLACE ``` another change path/to/a/file1.txt ```python <<<<<<< SEARCH one ======= two >>>>>>> REPLACE ``` Hope you like it! """ edits = list(eb.find_original_update_blocks(edit, valid_fnames=["path/to/a/file1.txt"])) self.assertEqual( edits, [ ("path/to/a/file2.txt", "", "three\n"), ("path/to/a/file1.txt", "one\n", "two\n"), ], ) def test_find_original_update_blocks_quad_backticks_with_triples_in_LLM_reply(self): # https://github.com/Aider-AI/aider/issues/2879 edit = """ Here's the change: foo.txt ```text <<<<<<< SEARCH ======= Tooooo >>>>>>> REPLACE ``` Hope you like it! """ quad_backticks = "`" * 4 quad_backticks = (quad_backticks, quad_backticks) edits = list(eb.find_original_update_blocks(edit, fence=quad_backticks)) self.assertEqual(edits, [("foo.txt", "", "Tooooo\n")]) # Test for shell script blocks with sh language identifier (issue #3785) def test_find_original_update_blocks_with_sh_language_identifier(self): # https://github.com/Aider-AI/aider/issues/3785 edit = """ Here's a shell script: ```sh test_hello.sh <<<<<<< SEARCH ======= #!/bin/bash # Check if exactly one argument is provided if [ "$#" -ne 1 ]; then echo "Usage: $0 " >&2 exit 1 fi # Echo the first argument echo "$1" exit 0 >>>>>>> REPLACE ``` """ edits = list(eb.find_original_update_blocks(edit)) # Instead of comparing exact strings, check that we got the right file and structure self.assertEqual(len(edits), 1) self.assertEqual(edits[0][0], "test_hello.sh") self.assertEqual(edits[0][1], "") # Check that the content contains the expected shell script elements result_content = edits[0][2] self.assertIn("#!/bin/bash", result_content) self.assertIn('if [ "$#" -ne 1 ];', result_content) self.assertIn('echo "Usage: $0 "', result_content) self.assertIn("exit 1", result_content) self.assertIn('echo "$1"', result_content) self.assertIn("exit 0", result_content) # Test for C# code blocks with csharp language identifier def test_find_original_update_blocks_with_csharp_language_identifier(self): edit = """ Here's a C# code change: ```csharp Program.cs <<<<<<< SEARCH Console.WriteLine("Hello World!"); ======= Console.WriteLine("Hello, C# World!"); >>>>>>> REPLACE ``` """ edits = list(eb.find_original_update_blocks(edit)) search_text = 'Console.WriteLine("Hello World!");\n' replace_text = 'Console.WriteLine("Hello, C# World!");\n' self.assertEqual(edits, [("Program.cs", search_text, replace_text)]) if __name__ == "__main__": unittest.main()