- hypr/apps.lua - hypr/autostart.lua - hypr/envs.lua - hypr/hyprland.lua - hypr/hyprsunset.conf - hypr/input.lua - hypr/looknfeel.lua - hypr/omasettings.lua - hypr/xdph.conf - omarchy/branding/about.txt - omarchy/branding/screensaver.txt - omarchy/extensions/omarchy-menu.jsonc - omarchy/hooks/battery-low.d/play-warning-sound.sample - omarchy/hooks/font-set.d/show-font-notification.sample - omarchy/hooks/post-boot.d/weather.sample - omarchy/hooks/post-update.d/install-voxtype.hook - omarchy/hooks/post-update.d/setup-agent.hook - omarchy/hooks/post-update.d/setup-fingerprint.hook - omarchy/hooks/post-update.d/show-update-notification.sample - omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample - omarchy/hooks/theme-set.d/show-theme-notification.sample - omarchy/shell.json - omarchy/shell.toml - omarchy/theme.name - omarchy/themes/azure-glow/README.md - omarchy/themes/azure-glow/alacritty.toml - omarchy/themes/azure-glow/btop.theme - omarchy/themes/azure-glow/hyprland.conf - omarchy/themes/azure-glow/hyprlock.conf - omarchy/themes/azure-glow/icons.theme - … 269 more
181 lines
7.6 KiB
Python
181 lines
7.6 KiB
Python
"""Tests for state.py and paths.py — atomic writes, bounded reads, where the files live.
|
|
|
|
Run: python3 -m unittest discover -s test
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
from nexthopd.state import write_atomic, read_json # noqa: E402
|
|
|
|
|
|
class AtomicState(unittest.TestCase):
|
|
def test_write_read(self):
|
|
with tempfile.TemporaryDirectory() as d:
|
|
p = Path(d) / "live.json"
|
|
write_atomic(p, {"x": 1})
|
|
self.assertEqual(read_json(p), {"x": 1})
|
|
self.assertEqual(read_json(Path(d) / "missing.json", 42), 42)
|
|
# No temp files left behind.
|
|
self.assertEqual([f.name for f in Path(d).iterdir()], ["live.json"])
|
|
|
|
|
|
class StateReadSafety(unittest.TestCase):
|
|
"""The read the QML side consumes: bounded, non-blocking, no-follow,
|
|
regular files only. Every property is enforced on the fd actually read,
|
|
so a state file swapped at its predictable path can neither redirect
|
|
the read, stall it, nor allocate without limit."""
|
|
|
|
def setUp(self):
|
|
from nexthopd.state import read_text_bounded
|
|
self.read = read_text_bounded
|
|
self.tmp = tempfile.TemporaryDirectory()
|
|
self.d = Path(self.tmp.name)
|
|
|
|
def tearDown(self):
|
|
self.tmp.cleanup()
|
|
|
|
def test_regular_file_reads(self):
|
|
(self.d / "live.json").write_text('{"a":1}')
|
|
got = self.read(self.d / "live.json", 1024)
|
|
self.assertIsNotNone(got)
|
|
self.assertEqual(got[0], '{"a":1}')
|
|
|
|
def test_symlink_refused_and_target_unread(self):
|
|
secret = self.d / "secret"
|
|
secret.write_text("SENSITIVE")
|
|
(self.d / "live.json").symlink_to(secret)
|
|
self.assertIsNone(self.read(self.d / "live.json", 1024))
|
|
|
|
def test_fifo_refused_without_blocking(self):
|
|
import os as _os
|
|
_os.mkfifo(self.d / "live.json")
|
|
# No writer will ever open this; a blocking open would hang here.
|
|
self.assertIsNone(self.read(self.d / "live.json", 1024))
|
|
|
|
def test_oversized_refused_by_the_read_itself(self):
|
|
(self.d / "live.json").write_text("x" * 5000)
|
|
self.assertIsNone(self.read(self.d / "live.json", 1024))
|
|
|
|
def test_exactly_at_the_cap_is_allowed(self):
|
|
(self.d / "live.json").write_text("y" * 1024)
|
|
got = self.read(self.d / "live.json", 1024)
|
|
self.assertIsNotNone(got)
|
|
self.assertEqual(len(got[0]), 1024)
|
|
|
|
def test_directory_refused(self):
|
|
self.assertIsNone(self.read(self.d, 1024))
|
|
|
|
def test_missing_file_is_none(self):
|
|
self.assertIsNone(self.read(self.d / "nope.json", 1024))
|
|
|
|
def test_stamp_changes_only_when_the_file_does(self):
|
|
p = self.d / "live.json"
|
|
p.write_text('{"a":1}')
|
|
first = self.read(p, 1024)[1]
|
|
self.assertEqual(self.read(p, 1024)[1], first)
|
|
import os as _os
|
|
p.write_text('{"a":2}')
|
|
_os.utime(p, ns=(0, 12345))
|
|
self.assertNotEqual(self.read(p, 1024)[1], first)
|
|
|
|
def test_stream_keys_are_a_closed_set(self):
|
|
from nexthopd.cli import STREAMABLE
|
|
# The QML side names a key, never a path — nothing it passes can
|
|
# widen what gets opened.
|
|
self.assertEqual(sorted(STREAMABLE),
|
|
["apps", "live", "manifest", "recent"])
|
|
|
|
def test_indented_json_still_streams_as_one_line(self):
|
|
# manifest.json is pretty-printed. Forwarding it verbatim would
|
|
# emit several lines and the shell service would never learn the
|
|
# version, silently disabling the update handover.
|
|
import json as _json
|
|
p = self.d / "manifest.json"
|
|
p.write_text(_json.dumps({"version": "9.9.9", "kinds": ["service"]},
|
|
indent=2))
|
|
text = self.read(p, 1024)[0]
|
|
self.assertIn("\n", text)
|
|
line = _json.dumps(_json.loads(text), separators=(",", ":"))
|
|
self.assertNotIn("\n", line)
|
|
self.assertEqual(_json.loads(line)["version"], "9.9.9")
|
|
|
|
def test_embedded_newline_cannot_break_framing(self):
|
|
import json as _json
|
|
p = self.d / "live.json"
|
|
p.write_text(_json.dumps({"note": "one\ntwo", "v": 1}))
|
|
text = self.read(p, 4096)[0]
|
|
line = _json.dumps(_json.loads(text), separators=(",", ":"))
|
|
self.assertNotIn("\n", line)
|
|
self.assertEqual(_json.loads(line)["note"], "one\ntwo")
|
|
|
|
|
|
class VolatileSnapshotsLiveInTheRuntimeDir(unittest.TestCase):
|
|
def test_runtime_dir_prefers_xdg_runtime_dir(self):
|
|
from nexthopd import paths
|
|
saved = dict(os.environ)
|
|
self.addCleanup(lambda: (os.environ.clear(), os.environ.update(saved)))
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
os.environ["XDG_RUNTIME_DIR"] = tmp
|
|
self.assertEqual(paths.runtime_dir(), Path(tmp) / "nexthop")
|
|
for fn in (paths.live_path, paths.recent_path, paths.apps_path):
|
|
self.assertEqual(fn().parent, Path(tmp) / "nexthop")
|
|
# History, settings and the lock never move.
|
|
for fn in (paths.db_path, paths.lock_path):
|
|
self.assertEqual(fn().parent, paths.state_dir())
|
|
# No usable runtime dir: everything stays together in the
|
|
# state dir, so a reader and a writer in the same environment
|
|
# always agree.
|
|
os.environ["XDG_RUNTIME_DIR"] = str(Path(tmp) / "missing")
|
|
self.assertEqual(paths.runtime_dir(), paths.state_dir())
|
|
os.environ.pop("XDG_RUNTIME_DIR")
|
|
self.assertEqual(paths.runtime_dir(), paths.state_dir())
|
|
|
|
def test_volatile_writes_do_not_fsync(self):
|
|
from nexthopd import state
|
|
calls = []
|
|
self.addCleanup(setattr, state.os, "fsync", state.os.fsync)
|
|
state.os.fsync = lambda fd: calls.append(fd)
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
write_atomic(Path(tmp) / "a.json", {"x": 1})
|
|
self.assertEqual(calls, [])
|
|
self.assertEqual(read_json(Path(tmp) / "a.json", None), {"x": 1})
|
|
write_atomic(Path(tmp) / "b.json", {"x": 1}, durable=True)
|
|
self.assertEqual(len(calls), 1)
|
|
|
|
def test_legacy_snapshots_are_retired_with_a_tombstone(self):
|
|
from nexthopd import paths, state
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
old = Path(tmp) / "state"
|
|
old.mkdir()
|
|
for name in (paths.RECENT, paths.APPS):
|
|
(old / name).write_text("{}")
|
|
write_atomic(old / paths.LIVE, {"v": 1, "state": "online", "index": 93,
|
|
"t": 1.0, "pid": 42, "pid_start": 7,
|
|
"daemon_version": "0.2.21",
|
|
"link": {"ssid": "Home"}})
|
|
state.retire_legacy_snapshots(old, Path(tmp) / "runtime", now=2.0)
|
|
self.assertFalse((old / paths.RECENT).exists())
|
|
self.assertFalse((old / paths.APPS).exists())
|
|
tomb = read_json(old / paths.LIVE, None)
|
|
# An old reader shows "no data", not the last number it saw...
|
|
self.assertEqual(tomb["state"], "no-daemon")
|
|
self.assertIsNone(tomb["index"])
|
|
self.assertEqual(tomb["t"], 2.0)
|
|
# ...and an old version watch finds no pid to retire.
|
|
for key in ("pid", "pid_start", "daemon_version"):
|
|
self.assertNotIn(key, tomb)
|
|
self.assertEqual(tomb["link"], {"ssid": "Home"}) # shape kept
|
|
# Same directory (no runtime dir available): nothing to retire.
|
|
state.retire_legacy_snapshots(old, old, now=3.0)
|
|
self.assertEqual(read_json(old / paths.LIVE, None)["t"], 2.0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|