From 1307215b8fdea01b249473bf4c314a6da48d2252 Mon Sep 17 00:00:00 2001 From: "Paul Gauthier (aider)" Date: Thu, 8 May 2025 09:36:15 -0700 Subject: [PATCH] test: Test io.format_files_for_input pretty output --- tests/basic/test_io.py | 119 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/tests/basic/test_io.py b/tests/basic/test_io.py index e925ef66d..f590194ab 100644 --- a/tests/basic/test_io.py +++ b/tests/basic/test_io.py @@ -7,6 +7,7 @@ from prompt_toolkit.completion import CompleteEvent from prompt_toolkit.document import Document from aider.dump import dump # noqa: F401 +from rich.text import Text from aider.io import AutoCompleter, ConfirmGroup, InputOutput from aider.utils import ChdirTemporaryDirectory @@ -476,5 +477,123 @@ class TestInputOutputMultilineMode(unittest.TestCase): mock_print.assert_called_once() +class TestInputOutputFormatFiles(unittest.TestCase): + def test_format_files_for_input_pretty_false(self): + io = InputOutput(pretty=False, fancy_input=False) + rel_fnames = ["file1.txt", "file[markup].txt", "ro_file.txt"] + rel_read_only_fnames = ["ro_file.txt"] + + expected_output = ( + "file1.txt\n" + "file[markup].txt\n" + "ro_file.txt (read only)\n" + ) + # Sort the expected lines because the order of editable vs read-only might vary + # depending on internal sorting, but the content should be the same. + # The method sorts editable_files and read_only_files separately. + # The final output joins sorted(read_only_files) + sorted(editable_files) + + # Based on current implementation: + # read_only_files = ["ro_file.txt (read only)"] + # editable_files = ["file1.txt", "file[markup].txt"] + # output = "\n".join(read_only_files + editable_files) + "\n" + + # Correct expected output based on implementation: + expected_output_lines = sorted([ + "ro_file.txt (read only)", + "file1.txt", + "file[markup].txt", + ]) + expected_output = "\n".join(expected_output_lines) + "\n" + + actual_output = io.format_files_for_input(rel_fnames, rel_read_only_fnames) + + # Normalizing actual output by splitting, sorting, and rejoining + actual_output_lines = sorted(filter(None, actual_output.splitlines())) + normalized_actual_output = "\n".join(actual_output_lines) + "\n" + + self.assertEqual(normalized_actual_output, expected_output) + + @patch("rich.columns.Columns") + @patch("os.path.abspath") + @patch("os.path.join") + def test_format_files_for_input_pretty_true_no_files(self, mock_join, mock_abspath, mock_columns): + io = InputOutput(pretty=True, root="test_root") + io.format_files_for_input([], []) + mock_columns.assert_not_called() + + @patch("rich.columns.Columns") + @patch("os.path.abspath") + @patch("os.path.join") + def test_format_files_for_input_pretty_true_editable_only(self, mock_join, mock_abspath, mock_columns): + io = InputOutput(pretty=True, root="test_root") + rel_fnames = ["edit1.txt", "edit[markup].txt"] + + io.format_files_for_input(rel_fnames, []) + + mock_columns.assert_called_once() + args, _ = mock_columns.call_args + renderables = args[0] + + self.assertEqual(len(renderables), 2) + self.assertIsInstance(renderables[0], Text) + self.assertEqual(renderables[0].plain, "edit1.txt") + self.assertIsInstance(renderables[1], Text) + self.assertEqual(renderables[1].plain, "edit[markup].txt") + + @patch("rich.columns.Columns") + @patch("os.path.abspath") + @patch("os.path.join") + def test_format_files_for_input_pretty_true_readonly_only(self, mock_join, mock_abspath, mock_columns): + io = InputOutput(pretty=True, root="test_root") + + # Mock path functions to ensure rel_path is chosen by the shortener logic + mock_join.side_effect = lambda *args: "/".join(args) + mock_abspath.side_effect = lambda p: "/ABS_PREFIX_VERY_LONG/" + os.path.normpath(p) + + rel_read_only_fnames = ["ro1.txt", "ro[markup].txt"] + # When all files in chat are read-only + rel_fnames = list(rel_read_only_fnames) + + io.format_files_for_input(rel_fnames, rel_read_only_fnames) + + mock_columns.assert_called_once() + args, _ = mock_columns.call_args + renderables = args[0] + + self.assertEqual(len(renderables), 3) # Readonly: + 2 files + self.assertIsInstance(renderables[0], Text) + self.assertEqual(renderables[0].plain, "Readonly:") + self.assertIsInstance(renderables[1], Text) + self.assertEqual(renderables[1].plain, "ro1.txt") + self.assertIsInstance(renderables[2], Text) + self.assertEqual(renderables[2].plain, "ro[markup].txt") + + @patch("rich.columns.Columns") + @patch("os.path.abspath") + @patch("os.path.join") + def test_format_files_for_input_pretty_true_mixed_files(self, mock_join, mock_abspath, mock_columns): + io = InputOutput(pretty=True, root="test_root") + + mock_join.side_effect = lambda *args: "/".join(args) + mock_abspath.side_effect = lambda p: "/ABS_PREFIX_VERY_LONG/" + os.path.normpath(p) + + rel_fnames = ["edit1.txt", "edit[markup].txt", "ro1.txt", "ro[markup].txt"] + rel_read_only_fnames = ["ro1.txt", "ro[markup].txt"] + + io.format_files_for_input(rel_fnames, rel_read_only_fnames) + + self.assertEqual(mock_columns.call_count, 2) + + # First call for read-only files + args_ro, _ = mock_columns.call_args_list[0] + renderables_ro = args_ro[0] + self.assertEqual(renderables_ro, [Text("Readonly:"), Text("ro1.txt"), Text("ro[markup].txt")]) + + # Second call for editable files + args_ed, _ = mock_columns.call_args_list[1] + renderables_ed = args_ed[0] + self.assertEqual(renderables_ed, [Text("Editable:"), Text("edit1.txt"), Text("edit[markup].txt")]) + if __name__ == "__main__": unittest.main()