diff options
Diffstat (limited to 'tests/installer-steps')
| -rw-r--r-- | tests/installer-steps/test_configure_hibernate_delay.py | 131 | ||||
| -rw-r--r-- | tests/installer-steps/test_orchestrators.py | 2 |
2 files changed, 132 insertions, 1 deletions
diff --git a/tests/installer-steps/test_configure_hibernate_delay.py b/tests/installer-steps/test_configure_hibernate_delay.py new file mode 100644 index 0000000..95f2dcb --- /dev/null +++ b/tests/installer-steps/test_configure_hibernate_delay.py @@ -0,0 +1,131 @@ +"""Test configure_hibernate_delay — the suspend-to-hibernate handoff. + +The desktop panel can upgrade its idle suspend stage to +suspend-then-hibernate, but the delay between the two is systemd's +HibernateDelaySec, which lives in a root-owned drop-in and so cannot be a +panel setting. Left unset the delay depends on the hardware: a machine +with a battery hibernates on a low-battery alarm at an hour nobody chose, +and one without falls back to systemd's 2h default. + +90 minutes is the seeded value: long enough that stepping out of a meeting +costs a screen unlock rather than a passphrase and a resume, short enough +that a laptop in a bag is not holding its encryption keys in RAM all +afternoon (a suspended machine is; a hibernated one is not). + +Method: sed-extract configure_hibernate_delay from the real `archsetup`, +point it at a temp config dir, and assert on the file it writes. + + python3 -m unittest tests.installer-steps.test_configure_hibernate_delay +""" + +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(confdir): + script = textwrap.dedent(f"""\ + logfile=/dev/null + action="" + display() {{ :; }} + error_warn() {{ echo "WARN: $1"; return 1; }} + source <(sed -n '/^configure_hibernate_delay() {{/,/^}}/p' "{ARCHSETUP}") + configure_hibernate_delay "{confdir}" + 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)) + + +def body(confdir): + with open(os.path.join(confdir, "60-hibernate-delay.conf")) as f: + return f.read() + + +class ConfigureHibernateDelay(unittest.TestCase): + # ------------------------------------------------------------ normal ---- + def test_writes_a_sleep_drop_in_with_the_delay(self): + with tempfile.TemporaryDirectory() as d: + r = run(d) + self.assertEqual(rc_of(r), 0) + text = body(d) + self.assertIn("[Sleep]", text) + self.assertIn("HibernateDelaySec=90min", text) + + def test_drop_in_is_world_readable_not_writable(self): + with tempfile.TemporaryDirectory() as d: + run(d) + mode = stat.S_IMODE( + os.stat(os.path.join(d, "60-hibernate-delay.conf")).st_mode) + self.assertEqual(mode, 0o644) + + def test_explains_why_the_value_is_not_left_to_systemd(self): + # The number is a judgement call, so the file has to carry its + # reasoning: whoever changes it next needs the tradeoff, not just + # a number to overwrite. Assert on the tradeoff's own terms — + # "contains a hash" would pass on any comment at all. + with tempfile.TemporaryDirectory() as d: + run(d) + text = body(d).lower() + self.assertIn("2h default", text) # what unset would give + self.assertIn("encryption keys", text) # why not longer + self.assertIn("passphrase", text) # why not shorter + + def test_drop_in_sorts_above_vendor_drop_ins(self): + # systemd reserves 10-40 for /usr and 60-90 for /etc. A 10- file + # in /etc sorts below a vendor 20- file and silently loses to it. + with tempfile.TemporaryDirectory() as d: + run(d) + name = os.listdir(d)[0] + prefix = int(name.split("-")[0]) + self.assertGreaterEqual(prefix, 60) + self.assertLessEqual(prefix, 90) + + # ---------------------------------------------------------- boundary ---- + def test_running_twice_leaves_one_correct_drop_in(self): + with tempfile.TemporaryDirectory() as d: + run(d) + first = body(d) + r = run(d) + self.assertEqual(rc_of(r), 0) + self.assertEqual(first, body(d)) + + def test_absent_directory_is_created(self): + with tempfile.TemporaryDirectory() as d: + nested = os.path.join(d, "etc", "systemd", "sleep.conf.d") + r = run(nested) + self.assertEqual(rc_of(r), 0) + self.assertIn("HibernateDelaySec=90min", body(nested)) + + # ------------------------------------------------------------- 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: + confdir = os.path.join(d, "ro") + os.mkdir(confdir) + os.chmod(confdir, 0o500) + try: + r = run(confdir) + self.assertIn("WARN:", r.stdout) + self.assertNotEqual(rc_of(r), 0) + finally: + os.chmod(confdir, 0o700) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/installer-steps/test_orchestrators.py b/tests/installer-steps/test_orchestrators.py index 34c46d1..c2eb710 100644 --- a/tests/installer-steps/test_orchestrators.py +++ b/tests/installer-steps/test_orchestrators.py @@ -28,7 +28,7 @@ ARCHSETUP = os.path.join(REPO_ROOT, "archsetup") ORCHESTRATORS = { "essential_services": [ "configure_randomness", "configure_networking", "configure_power", - "configure_backlight_access", + "configure_hibernate_delay", "configure_backlight_access", "configure_ssh_server", "configure_fail2ban", "configure_firewall", "configure_service_discovery", "configure_job_scheduling", "configure_package_cache", "configure_snapshots", |
