Dotfiles_ArchAP/tests/test_session_snapshot.py

278 lines
12 KiB
Python

"""Contrat Session Snapshot V1 sur arbres Sway synthétiques."""
import copy
import importlib.util
from pathlib import Path
import tempfile
import unittest
from unittest import mock
SOURCE = (
Path(__file__).resolve().parents[1]
/ "quickshell/.config/quickshell/labfy-sway/session/session_snapshot.py"
)
SPEC = importlib.util.spec_from_file_location("session_snapshot", SOURCE)
snapshot_module = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(snapshot_module)
def leaf(
con_id,
app_id,
*,
focused=False,
floating="auto_off",
fullscreen=0,
properties=None,
scratchpad_state="none",
):
return {
"id": con_id,
"type": "con",
"name": f"titre {con_id}",
"app_id": app_id,
"window": con_id if properties else None,
"window_properties": properties,
"pid": 90000 + con_id,
"nodes": [],
"floating_nodes": [],
"layout": "none",
"orientation": "none",
"percent": 0.5,
"rect": {"x": con_id, "y": 2, "width": 300, "height": 200},
"focused": focused,
"urgent": False,
"marks": [f"mark-{con_id}"],
"floating": floating,
"fullscreen_mode": fullscreen,
"scratchpad_state": scratchpad_state,
}
def container(con_id, layout, children, floating_nodes=None):
return {
"id": con_id,
"type": "con",
"name": None,
"nodes": children,
"floating_nodes": floating_nodes or [],
"layout": layout,
"orientation": "horizontal" if layout in {"splith", "tabbed"} else "vertical",
"percent": 1.0,
"rect": {"x": 0, "y": 0, "width": 1200, "height": 800},
"focused": False,
"urgent": False,
"marks": [],
}
def fixture():
firefox_one = leaf(11, "firefox")
firefox_two = leaf(12, "firefox", fullscreen=1)
kitty = leaf(13, "kitty", focused=True)
xwayland = leaf(14, None, properties={"class": "LegacyApp", "instance": "legacy"})
unknown = leaf(15, "not-catalogued")
floating = leaf(16, "float-app", floating="user_on")
tabbed = container(21, "tabbed", [firefox_one, firefox_two])
stacked = container(22, "stacked", [kitty, xwayland])
splitv = container(23, "splitv", [tabbed, stacked])
splith = container(24, "splith", [splitv, unknown], [floating])
workspace = {
"id": 4,
"type": "workspace",
"name": "dev",
"nodes": [splith],
"floating_nodes": [],
"layout": "splith",
"orientation": "horizontal",
}
scratch = leaf(30, "scratch-app", floating="user_on", scratchpad_state="changed")
tree = {
"id": 1,
"type": "root",
"nodes": [
{
"id": 2,
"type": "output",
"name": "__i3",
"nodes": [{"id": 3, "type": "workspace", "name": "__i3_scratch", "nodes": [], "floating_nodes": [scratch]}],
"floating_nodes": [],
},
{"id": 5, "type": "output", "name": "DP-1", "nodes": [workspace], "floating_nodes": []},
],
}
outputs = [{
"name": "DP-1",
"active": True,
"focused": True,
"rect": {"x": 0, "y": 0, "width": 1200, "height": 800},
"scale": 1.25,
"transform": "normal",
"current_mode": {"width": 1920, "height": 1080, "refresh": 60000},
}]
workspaces = [{
"name": "dev",
"num": -1,
"output": "DP-1",
"focused": True,
"visible": True,
"urgent": False,
"rect": {"x": 0, "y": 0, "width": 1200, "height": 800},
"layout": "splith",
"orientation": "horizontal",
}]
version = {"variant": "sway", "human_readable": "0.6", "sway_original_version": "1.12.0"}
entries = [
{"desktop_entry": "firefox.desktop", "stem": "firefox", "startup_wm_class": "firefox", "name": "Firefox"},
{"desktop_entry": "kitty.desktop", "stem": "kitty", "startup_wm_class": "kitty", "name": "Kitty"},
{"desktop_entry": "legacy.desktop", "stem": "legacy-desktop", "startup_wm_class": "LegacyApp", "name": "Legacy"},
]
return version, outputs, workspaces, tree, entries
class SessionSnapshotTest(unittest.TestCase):
def build(self):
version, outputs, workspaces, tree, entries = fixture()
with mock.patch.object(snapshot_module, "_basename_from_pid", return_value=None):
return snapshot_module.build_snapshot(
version,
outputs,
workspaces,
tree,
captured_at="2026-01-01T00:00:00.000Z",
environment={"XDG_SESSION_TYPE": "wayland", "XDG_CURRENT_DESKTOP": "sway"},
desktop_entries=entries,
managed_autostart={"float-app"},
)
def test_layout_tree_order_and_simple_workspace(self):
snapshot = self.build()
self.assertEqual(snapshot["workspaces"][0]["name"], "dev")
self.assertEqual(snapshot["workspaces"][0]["roots"]["tiling"], [{"kind": "container", "id": "c1"}])
layouts = {item["layout"] for item in snapshot["containers"]}
self.assertTrue({"splith", "splitv", "tabbed", "stacked"}.issubset(layouts))
top = snapshot["containers"][0]
self.assertEqual([ref["kind"] for ref in top["children"]], ["container", "window"])
def test_simple_direct_window_and_empty_workspace(self):
version, outputs, workspaces, tree, entries = fixture()
direct = leaf(40, "firefox")
real_output = tree["nodes"][1]
real_output["nodes"][0]["nodes"] = [direct]
empty = {
"id": 41, "type": "workspace", "name": "2", "nodes": [],
"floating_nodes": [], "layout": "splith", "orientation": "horizontal",
}
real_output["nodes"].append(empty)
workspaces.append({
"name": "2", "num": 2, "output": "DP-1", "focused": False,
"visible": False, "urgent": False,
"rect": {"x": 0, "y": 0, "width": 1200, "height": 800},
"layout": "splith", "orientation": "horizontal",
})
with mock.patch.object(snapshot_module, "_basename_from_pid", return_value=None):
snapshot = snapshot_module.build_snapshot(version, outputs, workspaces, tree, desktop_entries=entries)
self.assertEqual(snapshot["workspaces"][0]["roots"]["tiling"][0]["kind"], "window")
self.assertEqual(snapshot["workspaces"][1]["roots"], {"tiling": [], "floating": []})
def test_floating_fullscreen_focus_and_marks(self):
snapshot = self.build()
floating = next(window for window in snapshot["windows"] if window["app_id"] == "float-app")
fullscreen = next(window for window in snapshot["windows"] if window["runtime"]["con_id"] == 12)
kitty = next(window for window in snapshot["windows"] if window["app_id"] == "kitty")
self.assertTrue(floating["floating"])
self.assertEqual(floating["classification"]["managed_by"], "sway-autostart")
self.assertEqual(fullscreen["fullscreen_mode"], 1)
self.assertEqual(snapshot["focus"]["window_id"], kitty["window_id"])
self.assertEqual(kitty["marks"], ["mark-13"])
def test_duplicate_app_ids_keep_distinct_snapshot_ordinals(self):
windows = [window for window in self.build()["windows"] if window["app_id"] == "firefox"]
self.assertEqual([window["snapshot_identity"] for window in windows], ["firefox#1", "firefox#2"])
self.assertNotEqual(windows[0]["window_id"], windows[1]["window_id"])
def test_desktop_identity_exact_xwayland_and_unresolved(self):
snapshot = self.build()
firefox = next(window for window in snapshot["windows"] if window["app_id"] == "firefox")
legacy = next(window for window in snapshot["windows"] if window["xwayland"]["class"] == "LegacyApp")
unknown = next(window for window in snapshot["windows"] if window["app_id"] == "not-catalogued")
self.assertEqual(firefox["restore_identity"]["desktop_entry"], "firefox.desktop")
self.assertEqual(legacy["restore_identity"]["matched_by"], "StartupWMClass")
self.assertEqual(legacy["restore_identity"]["confidence"], "exact")
self.assertEqual(unknown["restore_identity"]["confidence"], "unresolved")
def test_hidden_scratchpad_is_not_a_user_workspace(self):
snapshot = self.build()
scratch = next(window for window in snapshot["windows"] if window["app_id"] == "scratch-app")
self.assertIsNone(scratch["workspace"])
self.assertTrue(scratch["scratchpad"]["member"])
self.assertEqual(scratch["scratchpad"]["visibility"], "hidden")
self.assertEqual(snapshot["restore_hints"]["scratchpad_roots"], [{"kind": "window", "id": scratch["window_id"]}])
def test_visible_scratchpad_member_stays_on_real_workspace(self):
version, outputs, workspaces, tree, entries = fixture()
visible = leaf(42, "scratch-visible", scratchpad_state="fresh")
tree["nodes"][1]["nodes"][0]["floating_nodes"] = [visible]
with mock.patch.object(snapshot_module, "_basename_from_pid", return_value=None):
snapshot = snapshot_module.build_snapshot(version, outputs, workspaces, tree, desktop_entries=entries)
window = next(item for item in snapshot["windows"] if item["app_id"] == "scratch-visible")
self.assertEqual(window["workspace"], "dev")
self.assertEqual(window["scratchpad"]["visibility"], "visible")
def test_unique_executable_basename_is_only_heuristic(self):
entries = [{"desktop_entry": "odd-name.desktop", "stem": "tool", "startup_wm_class": None, "name": "Tool"}]
identity = snapshot_module.resolve_desktop_entry("unrelated", {}, "tool", entries)
self.assertEqual(identity, {
"desktop_entry": "odd-name.desktop",
"confidence": "heuristic",
"matched_by": "executable-basename",
})
def test_title_and_timestamp_do_not_affect_structural_projection(self):
first = self.build()
second = copy.deepcopy(first)
second["metadata"]["captured_at"] = "2027-02-02T00:00:00Z"
second["windows"][0]["title_hint"] = "titre dynamique différent"
self.assertEqual(
snapshot_module.structural_projection(first),
snapshot_module.structural_projection(second),
)
def test_validator_rejects_broken_tree_relation(self):
snapshot = self.build()
snapshot["workspaces"][0]["roots"]["tiling"][0]["id"] = "absent"
with self.assertRaises(snapshot_module.SnapshotError):
snapshot_module.validate_snapshot(snapshot)
def test_validator_rejects_duplicate_reference(self):
snapshot = self.build()
root = snapshot["workspaces"][0]["roots"]["tiling"][0]
snapshot["workspaces"][0]["roots"]["tiling"].append(copy.deepcopy(root))
with self.assertRaises(snapshot_module.SnapshotError):
snapshot_module.validate_snapshot(snapshot)
def test_autostart_parser_excludes_shell_wrappers_and_keeps_direct_apps(self):
with tempfile.TemporaryDirectory() as folder:
path = Path(folder) / "autostart"
path.write_text(
"exec limusic-app\n"
"exec python3 ~/.config/sway/scripts/inactive-windows-transparency.py\n"
"exec sh -c 'private command'\n",
encoding="utf-8",
)
self.assertEqual(
snapshot_module.parse_sway_autostart(path),
{"limusic-app", "inactive-windows-transparency.py"},
)
def test_no_command_line_or_environment_field_exists(self):
serialized_keys = repr(self.build()).casefold()
self.assertNotIn("cmdline", serialized_keys)
self.assertNotIn("command_line", serialized_keys)
self.assertNotIn("/proc/", serialized_keys)
self.assertNotIn("environment", serialized_keys)
if __name__ == "__main__":
unittest.main()