aboutsummaryrefslogtreecommitdiff
path: root/tests/installer-steps/test_configure_hibernate_delay.py
blob: 95f2dcb76a642cbdcbe04dec4c5300b6599076b3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
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()