aboutsummaryrefslogtreecommitdiff
path: root/tests/airplane-mode/test_airplane_mode.py
blob: 5db0ed17f56681a739564dfd8978422e8f176fd4 (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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
"""Tests for dotfiles/hyprland/.local/bin/airplane-mode.

airplane-mode is a stateful toggle. On engage it RECORDS the current state of
each lever (wifi on/off, CPU EPP value, brightness, which services were
running) to a state file, then applies the low-power settings. On disengage it
reads that file and RESTORES exactly what was recorded — so a lever that was
already in its low-power position before engaging is left untouched on
disengage. That save-and-replay logic is what these tests pin down.

The real script runs against command stubs (sudo / nmcli / brightnessctl /
systemctl / notify / pkill) placed on PATH, plus fake EPP sysfs files in a
temp dir. The stubs log every invocation and report state driven by STUB_*
env vars, so the test controls "what was running" without touching the host.
No reimplementation of the script — the production body executes.

Run from repo root:
    python3 -m unittest tests.airplane-mode.test_airplane_mode
"""

import os
import shutil
import subprocess
import tempfile
import unittest


REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
SCRIPT = os.path.join(REPO_ROOT, "dotfiles/hyprland/.local/bin/airplane-mode")

SYSTEM_SERVICES = "svc-a.service svc-b.service svc-c.service"
USER_SERVICES = "svc-user.service"


class AirplaneModeHarness(unittest.TestCase):

    def setUp(self):
        self.tmp = tempfile.mkdtemp(prefix="airplane-mode-test-")
        self.state_file = os.path.join(self.tmp, "airplane-state")
        self.stub_log = os.path.join(self.tmp, "stub.log")

        # Fake EPP sysfs files for two CPUs, pre-set to the normal value.
        self.epp_dir = os.path.join(self.tmp, "epp")
        self.epp_files = []
        for cpu in ("cpu0", "cpu1"):
            d = os.path.join(self.epp_dir, cpu)
            os.makedirs(d)
            f = os.path.join(d, "energy_performance_preference")
            self._write(f, "balance_performance\n")
            self.epp_files.append(f)
        self.epp_glob = os.path.join(self.epp_dir, "cpu*", "energy_performance_preference")

        self.stub_dir = os.path.join(self.tmp, "stubs")
        os.makedirs(self.stub_dir)
        self._make_stubs()

    def tearDown(self):
        shutil.rmtree(self.tmp, ignore_errors=True)

    # -- helpers --------------------------------------------------------------

    def _write(self, path, contents):
        with open(path, "w") as f:
            f.write(contents)

    def _stub(self, name, body):
        path = os.path.join(self.stub_dir, name)
        self._write(path, "#!/bin/sh\n" + body)
        os.chmod(path, 0o755)

    def _make_stubs(self):
        # sudo just runs the rest of the command line (no privilege needed in
        # the test; the fake EPP files are writable by the test user).
        self._stub("sudo", 'exec "$@"\n')

        self._stub("nmcli", (
            'echo "nmcli $*" >> "$STUB_LOG"\n'
            'if [ "$1" = radio ] && [ "$2" = wifi ] && [ -z "$3" ]; then\n'
            '  echo "${STUB_WIFI:-enabled}"\n'
            'fi\n'
            'exit 0\n'
        ))

        self._stub("brightnessctl", (
            'echo "brightnessctl $*" >> "$STUB_LOG"\n'
            'case "$1" in\n'
            '  get) echo "${STUB_BRIGHTNESS:-96000}" ;;\n'
            '  max) echo 96000 ;;\n'
            'esac\n'
            'exit 0\n'
        ))

        self._stub("systemctl", (
            'echo "systemctl $*" >> "$STUB_LOG"\n'
            'user=0; sub=""; svc=""\n'
            'for a in "$@"; do\n'
            '  case "$a" in\n'
            '    --user) user=1 ;;\n'
            '    --quiet) ;;\n'
            '    is-active|stop|start) sub="$a" ;;\n'
            '    *) svc="$a" ;;\n'
            '  esac\n'
            'done\n'
            'if [ "$sub" = is-active ]; then\n'
            '  if [ "$user" = 1 ]; then list="$STUB_ACTIVE_USER"; else list="$STUB_ACTIVE_SYSTEM"; fi\n'
            '  case " $list " in *" $svc "*) exit 0 ;; *) exit 3 ;; esac\n'
            'fi\n'
            'exit 0\n'
        ))

        self._stub("notify", 'echo "notify $*" >> "$STUB_LOG"\nexit 0\n')
        self._stub("pkill", 'echo "pkill $*" >> "$STUB_LOG"\nexit 0\n')

    def run_toggle(self, wifi="enabled", brightness="96000",
                   active_system=SYSTEM_SERVICES, active_user=USER_SERVICES):
        env = os.environ.copy()
        env["PATH"] = self.stub_dir + os.pathsep + env["PATH"]
        env["XDG_RUNTIME_DIR"] = self.tmp
        env["STUB_LOG"] = self.stub_log
        env["STUB_WIFI"] = wifi
        env["STUB_BRIGHTNESS"] = brightness
        env["STUB_ACTIVE_SYSTEM"] = active_system
        env["STUB_ACTIVE_USER"] = active_user
        env["AIRPLANE_EPP_GLOB"] = self.epp_glob
        env["AIRPLANE_SYSTEM_SERVICES"] = SYSTEM_SERVICES
        env["AIRPLANE_USER_SERVICES"] = USER_SERVICES
        env["AIRPLANE_BRIGHTNESS_LOW"] = "35%"
        return subprocess.run(
            [SCRIPT], env=env, capture_output=True, text=True, timeout=15,
        )

    def state(self):
        out = {}
        with open(self.state_file) as f:
            for line in f:
                if "=" in line:
                    k, v = line.rstrip("\n").split("=", 1)
                    out[k] = v
        return out

    def log(self):
        try:
            with open(self.stub_log) as f:
                return f.read()
        except FileNotFoundError:
            return ""

    def epp_values(self):
        vals = []
        for f in self.epp_files:
            with open(f) as fh:
                vals.append(fh.read().strip())
        return vals


# -----------------------------------------------------------------------------
# Normal cases — engage from a clean (everything-on) state
# -----------------------------------------------------------------------------

class TestEngage(AirplaneModeHarness):

    def test_engage_writes_mode_on(self):
        r = self.run_toggle()
        self.assertEqual(r.returncode, 0, msg=r.stderr)
        self.assertEqual(self.state()["mode"], "on")

    def test_engage_turns_wifi_off(self):
        self.run_toggle(wifi="enabled")
        self.assertIn("nmcli radio wifi off", self.log())

    def test_engage_records_prior_wifi_state(self):
        self.run_toggle(wifi="enabled")
        self.assertEqual(self.state()["wifi"], "enabled")

    def test_engage_sets_epp_to_power_on_all_cpus(self):
        self.run_toggle()
        self.assertEqual(self.epp_values(), ["power", "power"])

    def test_engage_records_prior_epp(self):
        self.run_toggle()
        self.assertEqual(self.state()["epp"], "balance_performance")

    def test_engage_dims_brightness_and_saves_prior(self):
        self.run_toggle(brightness="96000")
        self.assertIn("brightnessctl set 35%", self.log())
        self.assertEqual(self.state()["brightness"], "96000")

    def test_engage_stops_active_services_and_records_them(self):
        self.run_toggle(active_system="svc-a.service svc-c.service",
                        active_user="svc-user.service")
        log = self.log()
        self.assertIn("systemctl stop svc-a.service", log)
        self.assertIn("systemctl stop svc-c.service", log)
        self.assertIn("systemctl --user stop svc-user.service", log)
        self.assertIn("svc-a.service", self.state()["stopped_system"])
        self.assertIn("svc-c.service", self.state()["stopped_system"])
        self.assertIn("svc-user.service", self.state()["stopped_user"])

    def test_engage_does_not_stop_already_inactive_service(self):
        # svc-b is not in the active list → never stopped, never recorded.
        self.run_toggle(active_system="svc-a.service", active_user="")
        log = self.log()
        self.assertNotIn("systemctl stop svc-b.service", log)
        self.assertNotIn("svc-b.service", self.state().get("stopped_system", ""))

    def test_engage_refreshes_waybar(self):
        self.run_toggle()
        self.assertIn("pkill -RTMIN+10 waybar", self.log())


# -----------------------------------------------------------------------------
# Normal cases — disengage restores recorded state
# -----------------------------------------------------------------------------

class TestDisengage(AirplaneModeHarness):

    def seed_on(self, wifi="enabled", epp="balance_performance",
                brightness="96000", stopped_system="svc-a.service svc-c.service",
                stopped_user="svc-user.service"):
        self._write(self.state_file, (
            f"mode=on\nwifi={wifi}\nepp={epp}\nbrightness={brightness}\n"
            f"stopped_system={stopped_system}\nstopped_user={stopped_user}\n"
        ))
        # EPP files are in low-power state while engaged.
        for f in self.epp_files:
            self._write(f, "power\n")

    def test_disengage_writes_mode_off(self):
        self.seed_on()
        r = self.run_toggle()
        self.assertEqual(r.returncode, 0, msg=r.stderr)
        self.assertEqual(self.state()["mode"], "off")

    def test_disengage_restores_wifi_when_it_was_on(self):
        self.seed_on(wifi="enabled")
        self.run_toggle()
        self.assertIn("nmcli radio wifi on", self.log())

    def test_disengage_restores_epp(self):
        self.seed_on(epp="balance_performance")
        self.run_toggle()
        self.assertEqual(self.epp_values(), ["balance_performance", "balance_performance"])

    def test_disengage_restores_brightness_to_saved_value(self):
        self.seed_on(brightness="80000")
        self.run_toggle()
        self.assertIn("brightnessctl set 80000", self.log())

    def test_disengage_restarts_recorded_services(self):
        self.seed_on(stopped_system="svc-a.service svc-c.service",
                     stopped_user="svc-user.service")
        log = self.log()  # before
        self.run_toggle()
        log = self.log()
        self.assertIn("systemctl start svc-a.service", log)
        self.assertIn("systemctl start svc-c.service", log)
        self.assertIn("systemctl --user start svc-user.service", log)


# -----------------------------------------------------------------------------
# Boundary — "leave it as it was" cases
# -----------------------------------------------------------------------------

class TestPreserveExistingState(AirplaneModeHarness):

    def test_engage_with_wifi_already_off_records_disabled(self):
        self.run_toggle(wifi="disabled")
        self.assertEqual(self.state()["wifi"], "disabled")

    def test_disengage_does_not_reenable_wifi_that_was_already_off(self):
        # Seed an engaged state where wifi was already off before engaging.
        self._write(self.state_file, (
            "mode=on\nwifi=disabled\nepp=balance_performance\n"
            "brightness=96000\nstopped_system=\nstopped_user=\n"
        ))
        for f in self.epp_files:
            self._write(f, "power\n")
        self.run_toggle()
        self.assertNotIn("nmcli radio wifi on", self.log())

    def test_disengage_only_restarts_recorded_services_not_all_known(self):
        # Only svc-a was recorded as stopped → svc-b/svc-c must not be started.
        self._write(self.state_file, (
            "mode=on\nwifi=enabled\nepp=balance_performance\n"
            "brightness=96000\nstopped_system=svc-a.service\nstopped_user=\n"
        ))
        for f in self.epp_files:
            self._write(f, "power\n")
        self.run_toggle()
        log = self.log()
        self.assertIn("systemctl start svc-a.service", log)
        self.assertNotIn("systemctl start svc-b.service", log)
        self.assertNotIn("systemctl start svc-c.service", log)

    def test_disengage_with_no_services_recorded_starts_nothing(self):
        self._write(self.state_file, (
            "mode=on\nwifi=enabled\nepp=balance_performance\n"
            "brightness=96000\nstopped_system=\nstopped_user=\n"
        ))
        for f in self.epp_files:
            self._write(f, "power\n")
        self.run_toggle()
        self.assertNotIn("systemctl start", self.log())


# -----------------------------------------------------------------------------
# Boundary — toggle dispatch
# -----------------------------------------------------------------------------

class TestToggleDispatch(AirplaneModeHarness):

    def test_missing_state_file_engages(self):
        # No state file → not engaged → first run turns airplane mode ON.
        self.assertFalse(os.path.exists(self.state_file))
        self.run_toggle()
        self.assertEqual(self.state()["mode"], "on")

    def test_mode_off_file_engages(self):
        self._write(self.state_file, "mode=off\n")
        self.run_toggle()
        self.assertEqual(self.state()["mode"], "on")


if __name__ == "__main__":
    unittest.main()