diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 278c780f6..4c7324ac7 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -1103,7 +1103,7 @@ class Coder: if lang: return self.normalize_language(lang) except Exception: - pass # pragma: no cover + pass # Environment variables for env_var in ("LANG", "LANGUAGE", "LC_ALL", "LC_MESSAGES"): diff --git a/tests/basic/test_coder.py b/tests/basic/test_coder.py index 22739ac35..dbc8cb9d1 100644 --- a/tests/basic/test_coder.py +++ b/tests/basic/test_coder.py @@ -1,5 +1,6 @@ import os import tempfile +import locale import unittest from pathlib import Path from unittest.mock import MagicMock, patch @@ -1181,6 +1182,112 @@ This command will print 'Hello, World!' to the console.""" sanity_check_messages(coder.cur_messages) self.assertEqual(coder.cur_messages[-1]["role"], "assistant") + def test_normalize_language(self): + coder = Coder.create(self.GPT35, None, io=InputOutput()) + + # Test None and empty + self.assertIsNone(coder.normalize_language(None)) + self.assertIsNone(coder.normalize_language("")) + + # Test "C" and "POSIX" + self.assertIsNone(coder.normalize_language("C")) + self.assertIsNone(coder.normalize_language("POSIX")) + + # Test already formatted names + self.assertEqual(coder.normalize_language("English"), "English") + self.assertEqual(coder.normalize_language("French"), "French") + + # Test common locale codes (fallback map, assuming babel is not installed or fails) + with patch("aider.coders.base_coder.Locale", None): + self.assertEqual(coder.normalize_language("en_US"), "English") + self.assertEqual(coder.normalize_language("fr_FR"), "French") + self.assertEqual(coder.normalize_language("es"), "Spanish") + self.assertEqual(coder.normalize_language("de_DE.UTF-8"), "German") + self.assertEqual(coder.normalize_language("ja"), "Japanese") + self.assertEqual(coder.normalize_language("unknown_code"), "unknown_code") # Fallback to original + + # Test with babel.Locale mocked (available) + mock_babel_locale = MagicMock() + mock_locale_instance = MagicMock() + mock_locale_instance.get_display_name.return_value = "english" # babel returns lowercase + mock_babel_locale.parse.return_value = mock_locale_instance + + with patch("aider.coders.base_coder.Locale", mock_babel_locale): + self.assertEqual(coder.normalize_language("en_US"), "English") + mock_babel_locale.parse.assert_called_with("en_US") + mock_locale_instance.get_display_name.assert_called_with("en") + + self.assertEqual(coder.normalize_language("fr-FR"), "French") # Test with hyphen + mock_babel_locale.parse.assert_called_with("fr_FR") # Hyphen replaced + + # Test with babel.Locale raising an exception (simulating parse failure) + mock_babel_locale_error = MagicMock() + mock_babel_locale_error.parse.side_effect = Exception("Babel parse error") + with patch("aider.coders.base_coder.Locale", mock_babel_locale_error): + self.assertEqual(coder.normalize_language("en_US"), "English") # Falls back to map + + def test_get_user_language(self): + io = InputOutput() + coder = Coder.create(self.GPT35, None, io=io) + + # 1. Test with self.chat_language set + coder.chat_language = "fr_CA" + with patch.object(coder, "normalize_language", return_value="French Canadian") as mock_norm: + self.assertEqual(coder.get_user_language(), "French Canadian") + mock_norm.assert_called_once_with("fr_CA") + coder.chat_language = None # Reset + + # 2. Test with locale.getlocale() + with patch("locale.getlocale", return_value=("en_GB", "UTF-8")) as mock_getlocale: + with patch.object(coder, "normalize_language", return_value="British English") as mock_norm: + self.assertEqual(coder.get_user_language(), "British English") + mock_getlocale.assert_called_once() + mock_norm.assert_called_once_with("en_GB") + + # Test with locale.getlocale() returning None or empty + with patch("locale.getlocale", return_value=(None, None)) as mock_getlocale: + with patch("os.environ.get") as mock_env_get: # Ensure env vars are not used yet + mock_env_get.return_value = None + self.assertIsNone(coder.get_user_language()) # Should be None if nothing found + + # 3. Test with environment variables + env_vars_to_test = ["LANG", "LANGUAGE", "LC_ALL", "LC_MESSAGES"] + + # Test LANG + with patch("locale.getlocale", side_effect=Exception("locale error")): # Mock locale to fail + with patch("os.environ.get") as mock_env_get: + mock_env_get.side_effect = lambda key: "de_DE.UTF-8" if key == "LANG" else None + with patch.object(coder, "normalize_language", return_value="German") as mock_norm: + self.assertEqual(coder.get_user_language(), "German") + mock_env_get.assert_any_call("LANG") + mock_norm.assert_called_once_with("de_DE") + + # Test LANGUAGE (takes precedence over LANG if both were hypothetically checked by os.environ.get) + # but our code checks in order, so we only need to mock the first one it finds. + with patch("locale.getlocale", side_effect=Exception("locale error")): + with patch("os.environ.get") as mock_env_get: + mock_env_get.side_effect = lambda key: "es_ES" if key == "LANGUAGE" else None + with patch.object(coder, "normalize_language", return_value="Spanish") as mock_norm: + self.assertEqual(coder.get_user_language(), "Spanish") + mock_env_get.assert_any_call("LANGUAGE") # LANG would be called first + mock_norm.assert_called_once_with("es_ES") + + # 4. Test priority: chat_language > locale > env + coder.chat_language = "it_IT" + with patch("locale.getlocale", return_value=("en_US", "UTF-8")) as mock_getlocale: + with patch("os.environ.get", return_value="de_DE") as mock_env_get: + with patch.object(coder, "normalize_language", side_effect=lambda x: x.upper()) as mock_norm: + self.assertEqual(coder.get_user_language(), "IT_IT") # From chat_language + mock_norm.assert_called_once_with("it_IT") + mock_getlocale.assert_not_called() + mock_env_get.assert_not_called() + coder.chat_language = None + + # 5. Test when no language is found + with patch("locale.getlocale", side_effect=Exception("locale error")): + with patch("os.environ.get", return_value=None) as mock_env_get: + self.assertIsNone(coder.get_user_language()) + def test_architect_coder_auto_accept_true(self): with GitTemporaryDirectory(): io = InputOutput(yes=True)