"""Tests for select_locale in the archsetup installer. select_locale resolves the system locale during pre-flight. Three inputs, in precedence order: an existing LANG= in locale.conf wins outright, then LOCALE from the config file, then a numbered menu. That menu is why these tests exist. It carries its own "[1]" default, LOCALE is optional in archsetup.conf.example, and archsetup does not require an archangel install -- so an unattended run (--config-file) can reach the menu's bare read with nobody there to answer it. configure_build_environment's own "no LANG= in locale.conf" branch shows archsetup expects that state to occur. Unattended therefore takes the menu's default, the same ruling nvidia_preflight and configure_autologin make for their prompts. These tests exercise the REAL function body, extracted from the `archsetup` script at run time (not a copy), against a fixture locale.conf. Run from repo root: python3 -m unittest tests.installer-steps.test_select_locale """ import os import shutil import subprocess import tempfile import unittest REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) ARCHSETUP = os.path.join(REPO_ROOT, "archsetup") class SelectLocaleHarness(unittest.TestCase): """Source select_locale out of the real archsetup script.""" def setUp(self): self.tmp = tempfile.mkdtemp(prefix="select-locale-test-") self.conf = os.path.join(self.tmp, "locale.conf") self.wrapper = os.path.join(self.tmp, "run.sh") with open(self.wrapper, "w") as f: f.write( "#!/bin/bash\n" 'ARCHSETUP="$1"; shift\n' "source <(sed -n " "'/^select_locale() {/,/^}/p' \"$ARCHSETUP\")\n" 'select_locale "$LOCALE_CONF"\n' 'echo "RESOLVED=$locale"\n' # Whatever the prompt did not consume is still on stdin. 'if IFS= read -r leftover; then echo "LEFTOVER:$leftover"; ' 'else echo "LEFTOVER:"; fi\n' ) os.chmod(self.wrapper, 0o755) def tearDown(self): shutil.rmtree(self.tmp, ignore_errors=True) def write_conf(self, body): with open(self.conf, "w") as f: f.write(body) def run_select(self, locale="", config_file="", stdin=""): env = dict(os.environ) env["locale"] = locale env["config_file"] = config_file env["LOCALE_CONF"] = self.conf return subprocess.run( ["bash", self.wrapper, ARCHSETUP], input=stdin, capture_output=True, text=True, env=env, ) def resolved(self, r): for line in r.stdout.splitlines(): if line.startswith("RESOLVED="): return line.split("=", 1)[1] self.fail("no RESOLVED line; stdout=%r stderr=%r" % (r.stdout, r.stderr)) # ---------------------------------------------------------- normal ---- def test_existing_locale_conf_wins_and_does_not_prompt(self): self.write_conf("LANG=de_DE.UTF-8\n") r = self.run_select(stdin="sentinel\n") self.assertIn("[OK] Locale: de_DE.UTF-8", r.stdout) # locale.conf is authoritative, so the global is deliberately left # unset -- configure_build_environment reads LANG out of the file. self.assertEqual(self.resolved(r), "") self.assertIn("LEFTOVER:sentinel", r.stdout) def test_config_file_locale_used_when_conf_has_no_lang(self): self.write_conf("") r = self.run_select(locale="fr_FR.UTF-8", stdin="sentinel\n") self.assertIn("(from config)", r.stdout) self.assertEqual(self.resolved(r), "fr_FR.UTF-8") self.assertIn("LEFTOVER:sentinel", r.stdout) def test_menu_choice_selects_that_locale(self): self.write_conf("") r = self.run_select(stdin="3\nsentinel\n") self.assertEqual(self.resolved(r), "de_DE.UTF-8") self.assertIn("LEFTOVER:sentinel", r.stdout) # -------------------------------------------------------- boundary ---- def test_missing_locale_conf_falls_through_to_the_menu(self): # No file at all, not just no LANG= line. r = self.run_select(stdin="2\nsentinel\n") self.assertEqual(self.resolved(r), "en_GB.UTF-8") self.assertIn("LEFTOVER:sentinel", r.stdout) def test_bare_enter_takes_choice_one(self): self.write_conf("") r = self.run_select(stdin="\nsentinel\n") self.assertEqual(self.resolved(r), "en_US.UTF-8") self.assertIn("LEFTOVER:sentinel", r.stdout) def test_unattended_config_file_takes_the_default_without_reading(self): # THE BUG. --config-file is the documented unattended mode and LOCALE # is optional, so this is an ordinary unattended install on a machine # whose locale.conf carries no LANG=. self.write_conf("") r = self.run_select(config_file="/etc/archsetup.conf", stdin="sentinel\n") self.assertEqual(self.resolved(r), "en_US.UTF-8") self.assertIn("LEFTOVER:sentinel", r.stdout) def test_unattended_still_prefers_an_existing_locale_conf(self): self.write_conf("LANG=ja_JP.UTF-8\n") r = self.run_select(config_file="/etc/archsetup.conf", stdin="sentinel\n") self.assertIn("[OK] Locale: ja_JP.UTF-8", r.stdout) self.assertIn("LEFTOVER:sentinel", r.stdout) def test_unattended_still_prefers_the_configured_locale(self): # Unattended does not mean "always en_US" -- LOCALE is an answer, and # the menu default must not override it. self.write_conf("") r = self.run_select(locale="pt_BR.UTF-8", config_file="/etc/archsetup.conf", stdin="sentinel\n") self.assertEqual(self.resolved(r), "pt_BR.UTF-8") self.assertIn("LEFTOVER:sentinel", r.stdout) # ----------------------------------------------------------- error ---- def test_unrecognised_choice_falls_back_to_en_us(self): self.write_conf("") r = self.run_select(stdin="42\nsentinel\n") self.assertEqual(self.resolved(r), "en_US.UTF-8") self.assertIn("LEFTOVER:sentinel", r.stdout) def test_manual_entry_reads_a_second_line(self): self.write_conf("") r = self.run_select(stdin="9\nnl_NL.UTF-8\nsentinel\n") self.assertEqual(self.resolved(r), "nl_NL.UTF-8") self.assertIn("LEFTOVER:sentinel", r.stdout) def test_manual_entry_left_empty_falls_back_to_en_us(self): self.write_conf("") r = self.run_select(stdin="9\n\nsentinel\n") self.assertEqual(self.resolved(r), "en_US.UTF-8") self.assertIn("LEFTOVER:sentinel", r.stdout) if __name__ == "__main__": unittest.main()