aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/installer-steps/test_configure_backlight_access.py139
-rw-r--r--tests/installer-steps/test_orchestrators.py1
2 files changed, 140 insertions, 0 deletions
diff --git a/tests/installer-steps/test_configure_backlight_access.py b/tests/installer-steps/test_configure_backlight_access.py
new file mode 100644
index 0000000..6fd171a
--- /dev/null
+++ b/tests/installer-steps/test_configure_backlight_access.py
@@ -0,0 +1,139 @@
+"""Test configure_backlight_access — udev rule for brightness writes.
+
+Arch's brightnessctl ships no udev rules: it relies on logind, which grants
+brightness writes only to the *active seat session*. Anything outside that
+session — a script, a remote shell, a panel launched into a different
+session — gets EPERM against root-owned sysfs. Found on velox 2026-08-13:
+on a fresh install the desktop settings panel's screen and keyboard
+brightness sliders were both inert.
+
+The step drops a rule making the brightness attributes group-writable by
+video (the group create_user already adds the user to). leds are granted to
+video as well rather than input, so nobody needs input-group membership —
+and the keylogging surface that carries — just to dim a keyboard.
+
+Method: sed-extract configure_backlight_access from the real `archsetup`,
+point it at a temp rules dir, and assert on the file it writes.
+
+ python3 -m unittest tests.installer-steps.test_configure_backlight_access
+"""
+
+import os
+import re
+import stat
+import subprocess
+import tempfile
+import textwrap
+import unittest
+
+REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
+ARCHSETUP = os.path.join(REPO_ROOT, "archsetup")
+
+
+def run(ruledir, marker=None):
+ # The step sends udevadm's own output to $logfile, so the stub records
+ # its argv to a marker file instead of stdout — writing straight to the
+ # file is unaffected by the caller's redirect.
+ marker = marker or os.devnull
+ script = textwrap.dedent(f"""\
+ logfile=/dev/null
+ action=""
+ display() {{ :; }}
+ error_warn() {{ echo "WARN: $1"; return 1; }}
+ udevadm() {{ echo "UDEVADM: $*" >> "{marker}"; }}
+ source <(sed -n '/^configure_backlight_access() {{/,/^}}/p' "{ARCHSETUP}")
+ configure_backlight_access "{ruledir}"
+ echo "RC=$?"
+ exit 0
+ """)
+ return subprocess.run(
+ ["bash", "-c", script], capture_output=True, text=True, timeout=10,
+ )
+
+
+def rc_of(r):
+ m = re.search(r"^RC=(\d+)$", r.stdout, re.M)
+ assert m, "no RC line in output: %r / %r" % (r.stdout, r.stderr)
+ return int(m.group(1))
+
+
+class ConfigureBacklightAccess(unittest.TestCase):
+ # ------------------------------------------------------------ normal ----
+ def test_writes_a_rule_covering_backlight_and_keyboard_leds(self):
+ with tempfile.TemporaryDirectory() as d:
+ r = run(d)
+ self.assertEqual(rc_of(r), 0)
+ rule = os.path.join(d, "90-backlight.rules")
+ self.assertTrue(os.path.exists(rule))
+ body = open(rule).read()
+ self.assertIn('SUBSYSTEM=="backlight"', body)
+ self.assertIn('SUBSYSTEM=="leds"', body)
+ self.assertIn('KERNEL=="*kbd_backlight"', body)
+
+ def test_grants_the_video_group_write_access(self):
+ with tempfile.TemporaryDirectory() as d:
+ run(d)
+ body = open(os.path.join(d, "90-backlight.rules")).read()
+ self.assertIn("chgrp video", body)
+ self.assertIn("chmod g+w", body)
+
+ def test_does_not_use_the_input_group(self):
+ # Granting leds to input would require input-group membership,
+ # which also confers read access to every input device.
+ with tempfile.TemporaryDirectory() as d:
+ run(d)
+ body = open(os.path.join(d, "90-backlight.rules")).read()
+ self.assertNotIn("chgrp input", body)
+
+ def test_rule_is_world_readable_not_writable(self):
+ with tempfile.TemporaryDirectory() as d:
+ run(d)
+ mode = stat.S_IMODE(os.stat(os.path.join(d, "90-backlight.rules")).st_mode)
+ self.assertEqual(mode, 0o644)
+
+ def test_reloads_udev_so_the_rule_applies_without_a_reboot(self):
+ with tempfile.TemporaryDirectory() as d:
+ marker = os.path.join(d, "udevadm.calls")
+ run(d, marker=marker)
+ calls = open(marker).read()
+ self.assertIn("UDEVADM: control --reload", calls)
+ self.assertRegex(calls, r"UDEVADM: trigger .*backlight")
+ self.assertRegex(calls, r"UDEVADM: trigger .*leds")
+
+ # ---------------------------------------------------------- boundary ----
+ def test_running_twice_leaves_one_correct_rule(self):
+ # Compare whole bodies rather than counting lines: a count encodes
+ # today's line total, so it breaks on a correct edit and passes an
+ # append regression that happens to hit the same number.
+ with tempfile.TemporaryDirectory() as d:
+ run(d)
+ first = open(os.path.join(d, "90-backlight.rules")).read()
+ r = run(d)
+ self.assertEqual(rc_of(r), 0)
+ second = open(os.path.join(d, "90-backlight.rules")).read()
+ self.assertEqual(first, second)
+
+ def test_absent_rules_directory_is_created(self):
+ with tempfile.TemporaryDirectory() as d:
+ nested = os.path.join(d, "etc", "udev", "rules.d")
+ r = run(nested)
+ self.assertEqual(rc_of(r), 0)
+ self.assertTrue(os.path.exists(os.path.join(nested, "90-backlight.rules")))
+
+ # ------------------------------------------------------------- error ----
+ @unittest.skipUnless(os.geteuid() != 0, "root ignores directory write bits")
+ def test_unwritable_directory_warns_and_does_not_crash(self):
+ with tempfile.TemporaryDirectory() as d:
+ ruledir = os.path.join(d, "ro")
+ os.mkdir(ruledir)
+ os.chmod(ruledir, 0o500)
+ try:
+ r = run(ruledir)
+ self.assertIn("WARN:", r.stdout)
+ self.assertNotEqual(rc_of(r), 0)
+ finally:
+ os.chmod(ruledir, 0o700)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/installer-steps/test_orchestrators.py b/tests/installer-steps/test_orchestrators.py
index 395ec61..34c46d1 100644
--- a/tests/installer-steps/test_orchestrators.py
+++ b/tests/installer-steps/test_orchestrators.py
@@ -28,6 +28,7 @@ ARCHSETUP = os.path.join(REPO_ROOT, "archsetup")
ORCHESTRATORS = {
"essential_services": [
"configure_randomness", "configure_networking", "configure_power",
+ "configure_backlight_access",
"configure_ssh_server", "configure_fail2ban", "configure_firewall",
"configure_service_discovery", "configure_job_scheduling",
"configure_package_cache", "configure_snapshots",