252 lines
9.9 KiB
Python
252 lines
9.9 KiB
Python
"""STEP19C : compilation pure du sous-ensemble d'arbre Sway prouvé."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import importlib.util
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
import unittest
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
MODULE = ROOT / "quickshell/.config/quickshell/labfy-sway/session/session_tree_restore.py"
|
|
|
|
|
|
def load_module():
|
|
spec = importlib.util.spec_from_file_location("session_tree_restore_test", MODULE)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
tree = load_module()
|
|
|
|
|
|
def load_executor():
|
|
path = ROOT / "quickshell/.config/quickshell/labfy-sway/session/session_executor.py"
|
|
spec = importlib.util.spec_from_file_location("session_tree_executor_test", path)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
executor = load_executor()
|
|
|
|
|
|
def make_snapshot(
|
|
*,
|
|
live=False,
|
|
layout="splith",
|
|
order=(1, 2, 3),
|
|
nested=False,
|
|
workspace_layout="splith",
|
|
floating_four=False,
|
|
):
|
|
prefix = "l" if live else "w"
|
|
con_base = 100 if live else 900
|
|
windows = []
|
|
for number in range(1, 5 if floating_four else 4):
|
|
window_id = f"{prefix}{number}"
|
|
parent = f"{prefix}root"
|
|
branch = "tiling"
|
|
index = order.index(number) if number in order else 0
|
|
floating = False
|
|
if number == 4:
|
|
parent = None
|
|
branch = "floating"
|
|
index = 0
|
|
floating = True
|
|
elif nested and number in {2, 3}:
|
|
parent = f"{prefix}nested"
|
|
index = number - 2
|
|
windows.append({
|
|
"window_id": window_id,
|
|
"runtime": {"con_id": con_base + number, "pid": 200 + number},
|
|
"workspace": "8",
|
|
"floating": floating,
|
|
"percent": 0.5 if nested and number in {2, 3} else 1 / 3,
|
|
"tree_position": {"parent_container_id": parent, "branch": branch, "index": index},
|
|
})
|
|
children = []
|
|
for number in order:
|
|
if nested and number == 2:
|
|
children.append({"kind": "container", "id": f"{prefix}nested"})
|
|
continue
|
|
if nested and number == 3:
|
|
continue
|
|
children.append({"kind": "window", "id": f"{prefix}{number}"})
|
|
containers = [{
|
|
"container_id": f"{prefix}root",
|
|
"layout": layout,
|
|
"percent": 1.0,
|
|
"children": children,
|
|
"floating_children": [],
|
|
}]
|
|
if nested:
|
|
containers.append({
|
|
"container_id": f"{prefix}nested",
|
|
"layout": "splitv" if layout == "splith" else "splith",
|
|
"percent": 2 / 3,
|
|
"children": [
|
|
{"kind": "window", "id": f"{prefix}2"},
|
|
{"kind": "window", "id": f"{prefix}3"},
|
|
],
|
|
"floating_children": [],
|
|
})
|
|
roots = {
|
|
"tiling": [{"kind": "container", "id": f"{prefix}root"}],
|
|
"floating": ([{"kind": "window", "id": f"{prefix}4"}] if floating_four else []),
|
|
}
|
|
return {
|
|
"workspaces": [{"name": "8", "layout": workspace_layout, "roots": roots}],
|
|
"containers": containers,
|
|
"windows": windows,
|
|
}
|
|
|
|
|
|
def make_plan(source, live, *, omit=()):
|
|
actions = []
|
|
for number in range(1, len(source["windows"]) + 1):
|
|
if number in omit:
|
|
continue
|
|
live_window = live["windows"][number - 1]
|
|
actions.append({
|
|
"action": "reuse-window",
|
|
"source_window": f"w{number}",
|
|
"live_window": copy.deepcopy(live_window),
|
|
})
|
|
actions.append({"action": "restore-tree-position", "source_window": "w1"})
|
|
return {"actions": actions}
|
|
|
|
|
|
class TreeCompilerTest(unittest.TestCase):
|
|
def compile(self, source, live, **kwargs):
|
|
return tree.compile_tree_restore(source, live, make_plan(source, live, **kwargs))
|
|
|
|
def test_identical_tree_has_no_operations(self):
|
|
result = self.compile(make_snapshot(), make_snapshot(live=True))
|
|
self.assertEqual(result["operations"], [])
|
|
|
|
def test_root_splith(self):
|
|
result = self.compile(make_snapshot(layout="splith"), make_snapshot(live=True, layout="tabbed"))
|
|
self.assertEqual(result["operations"][-1]["layout"], "splith")
|
|
|
|
def test_root_splitv(self):
|
|
result = self.compile(make_snapshot(layout="splitv"), make_snapshot(live=True, layout="splith"))
|
|
self.assertEqual(result["operations"][-1]["layout"], "splitv")
|
|
|
|
def test_root_tabbed(self):
|
|
result = self.compile(make_snapshot(layout="tabbed"), make_snapshot(live=True, layout="splith"))
|
|
self.assertEqual(result["operations"][-1]["layout"], "tabbed")
|
|
|
|
def test_root_stacked(self):
|
|
result = self.compile(make_snapshot(layout="stacked"), make_snapshot(live=True, layout="splith"))
|
|
self.assertEqual(result["operations"][-1]["layout"], "stacked")
|
|
|
|
def test_child_order_diff_uses_typed_relative_moves(self):
|
|
result = self.compile(make_snapshot(order=(1, 2, 3)), make_snapshot(live=True, order=(3, 1, 2)))
|
|
self.assertEqual([item["operation"] for item in result["operations"]], ["move-relative"])
|
|
self.assertEqual(result["operations"][0]["relation"], "after")
|
|
|
|
def test_nested_supported_tree(self):
|
|
result = self.compile(make_snapshot(nested=True), make_snapshot(live=True))
|
|
self.assertEqual(
|
|
[item["operation"] for item in result["operations"]],
|
|
["set-split-orientation", "move-relative"],
|
|
)
|
|
|
|
def test_nested_unsupported_tree(self):
|
|
source = make_snapshot(nested=True)
|
|
nested = source["containers"][1]
|
|
nested["children"] = [{"kind": "container", "id": "wdeep"}]
|
|
source["containers"].append({
|
|
"container_id": "wdeep", "layout": "splith", "percent": 1.0,
|
|
"children": [{"kind": "window", "id": "w2"}, {"kind": "window", "id": "w3"}],
|
|
"floating_children": [],
|
|
})
|
|
with self.assertRaisesRegex(tree.TreeRestoreError, "unsupported-nested-tree"):
|
|
self.compile(source, make_snapshot(live=True))
|
|
|
|
def test_missing_target_window(self):
|
|
with self.assertRaisesRegex(tree.TreeRestoreError, "missing-target-window"):
|
|
self.compile(make_snapshot(), make_snapshot(live=True), omit=(3,))
|
|
|
|
def test_unexpected_live_window(self):
|
|
source = make_snapshot(floating_four=True)
|
|
live = make_snapshot(live=True, floating_four=True)
|
|
live["workspaces"][0]["roots"]["floating"] = []
|
|
live["workspaces"][0]["roots"]["tiling"].append({"kind": "window", "id": "l4"})
|
|
live["windows"][3]["floating"] = False
|
|
with self.assertRaisesRegex(tree.TreeRestoreError, "unexpected-live-window"):
|
|
self.compile(source, live)
|
|
|
|
def test_floating_branch_is_preserved_and_excluded(self):
|
|
result = self.compile(make_snapshot(floating_four=True), make_snapshot(live=True, floating_four=True))
|
|
self.assertEqual(result["operations"], [])
|
|
self.assertNotIn("w4", repr(result["workspaces"][0]["target_tree"]))
|
|
|
|
def test_percent_is_explicitly_unsupported(self):
|
|
source = make_snapshot()
|
|
live = make_snapshot(live=True)
|
|
live["windows"][0]["percent"] = 0.9
|
|
result = self.compile(source, live)
|
|
self.assertEqual(result["percent_capability"], "unsupported")
|
|
self.assertEqual(result["operations"], [])
|
|
|
|
def test_determinism(self):
|
|
source = make_snapshot(layout="stacked", order=(1, 2, 3))
|
|
live = make_snapshot(live=True, layout="splitv", order=(3, 2, 1))
|
|
self.assertEqual(self.compile(source, live), self.compile(copy.deepcopy(source), copy.deepcopy(live)))
|
|
|
|
def test_hostile_values_are_rejected(self):
|
|
with self.assertRaisesRegex(tree.TreeRestoreError, "invalid-operation-layout"):
|
|
tree.validate_operation({
|
|
"operation": "set-container-layout", "workspace": "8", "source_window": "w1",
|
|
"layout": "tabbed; exec evil",
|
|
})
|
|
with self.assertRaisesRegex(tree.TreeRestoreError, "invalid-operation-workspace"):
|
|
tree.validate_operation({
|
|
"operation": "move-relative", "workspace": "8; exec evil", "source_window": "w1",
|
|
"relation": "after", "reference_window": "w2",
|
|
})
|
|
|
|
def test_runtime_tree_drift_stops_before_second_command(self):
|
|
source = make_snapshot(layout="tabbed")
|
|
live_before = make_snapshot(live=True, layout="splith")
|
|
live_after_first = make_snapshot(live=True, layout="tabbed")
|
|
live_drifted = make_snapshot(live=True, layout="tabbed", order=(2, 1, 3))
|
|
states = iter((live_before, live_after_first, live_drifted))
|
|
commands = []
|
|
|
|
class Planner:
|
|
@staticmethod
|
|
def match_windows(saved, candidates):
|
|
return {str(saved[0]["window_id"]): {"confidence": "exact", "live_index": 0}}, []
|
|
|
|
compilation = {
|
|
"operations": [
|
|
{"operation": "set-container-layout", "workspace": "8", "source_window": "w1", "layout": "tabbed"},
|
|
{"operation": "set-container-layout", "workspace": "8", "source_window": "w1", "layout": "stacked"},
|
|
]
|
|
}
|
|
with self.assertRaisesRegex(executor.RestoreExecutionError, "RESTORE_TREE_DRIFTED"):
|
|
executor._execute_tree_compilation(
|
|
source,
|
|
make_plan(source, live_before),
|
|
compilation,
|
|
{"w1": 101, "w2": 102, "w3": 103},
|
|
collector=lambda: copy.deepcopy(next(states)),
|
|
planner_module=Planner,
|
|
tree_module=tree,
|
|
runner=lambda argv, _timeout: commands.append(list(argv)) or SimpleNamespace(
|
|
returncode=0, stdout='[{"success":true}]', stderr=""
|
|
),
|
|
monotonic=lambda: 0.0,
|
|
sleeper=lambda _delay: None,
|
|
)
|
|
self.assertEqual(len(commands), 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|