"""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()