203 lines
6.8 KiB
Python
203 lines
6.8 KiB
Python
"""Unit tests for the KDL parser and writer.
|
|
|
|
Tests the core parse → mutate → write round-trip logic that underpins
|
|
all config changes in NiriMod.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
|
|
from nirimod.kdl_parser import (
|
|
KdlNode,
|
|
KdlRawString,
|
|
find_or_create,
|
|
parse_kdl,
|
|
remove_child,
|
|
set_child_arg,
|
|
set_node_flag,
|
|
write_kdl,
|
|
)
|
|
|
|
|
|
class TestKdlRoundTrip(unittest.TestCase):
|
|
"""parse_kdl → write_kdl should produce semantically equivalent output."""
|
|
|
|
def _roundtrip(self, text: str) -> list[KdlNode]:
|
|
nodes = parse_kdl(text)
|
|
out = write_kdl(nodes)
|
|
return parse_kdl(out)
|
|
|
|
def test_simple_node(self):
|
|
nodes = parse_kdl("prefer-no-csd\n")
|
|
self.assertEqual(len(nodes), 1)
|
|
self.assertEqual(nodes[0].name, "prefer-no-csd")
|
|
|
|
def test_node_with_string_arg(self):
|
|
nodes = parse_kdl('output "eDP-1" {\n scale 2.0\n}\n')
|
|
self.assertEqual(nodes[0].name, "output")
|
|
self.assertEqual(nodes[0].args[0], "eDP-1")
|
|
scale = nodes[0].get_child("scale")
|
|
self.assertIsNotNone(scale)
|
|
self.assertAlmostEqual(scale.args[0], 2.0)
|
|
|
|
def test_boolean_values(self):
|
|
nodes = parse_kdl(
|
|
"input {\n keyboard {\n repeat-rate 30\n xkb-numlock true\n }\n}\n"
|
|
)
|
|
kb = nodes[0].get_child("keyboard")
|
|
self.assertIsNotNone(kb)
|
|
numlock = kb.get_child("xkb-numlock")
|
|
self.assertIsNotNone(numlock)
|
|
self.assertIs(numlock.args[0], True)
|
|
|
|
def test_raw_string(self):
|
|
text = "spawn-at-startup r#\"bash -c 'echo hi'\"#\n"
|
|
nodes = parse_kdl(text)
|
|
self.assertIsInstance(nodes[0].args[0], KdlRawString)
|
|
|
|
def test_raw_string_property_preserves_backslash(self):
|
|
src = 'match app-id="steam" title=r#"^notificationtoasts_\\d+_desktop$"#\n'
|
|
nodes = parse_kdl(src)
|
|
title = nodes[0].props["title"]
|
|
self.assertIsInstance(title, KdlRawString)
|
|
self.assertEqual(title, r"^notificationtoasts_\d+_desktop$")
|
|
self.assertIn(r'title=r"^notificationtoasts_\d+_desktop$"', write_kdl(nodes))
|
|
|
|
def test_write_preserves_children(self):
|
|
src = "layout {\n gaps 16\n border {\n width 2\n }\n}\n"
|
|
nodes = self._roundtrip(src)
|
|
layout = nodes[0]
|
|
self.assertEqual(layout.name, "layout")
|
|
border = layout.get_child("border")
|
|
self.assertIsNotNone(border)
|
|
self.assertEqual(border.get_child("width").args[0], 2)
|
|
|
|
def test_null_value(self):
|
|
nodes = parse_kdl("cursor-warps null\n")
|
|
self.assertIsNone(nodes[0].args[0])
|
|
|
|
def test_props(self):
|
|
nodes = parse_kdl("position x=0 y=1080\n")
|
|
self.assertEqual(nodes[0].props["x"], 0)
|
|
self.assertEqual(nodes[0].props["y"], 1080)
|
|
|
|
def test_empty_input(self):
|
|
nodes = parse_kdl("")
|
|
self.assertEqual(nodes, [])
|
|
self.assertIn("NiriMod", write_kdl(nodes))
|
|
|
|
def test_comments_are_preserved_as_trivia(self):
|
|
src = "// top-level comment\nprefer-no-csd\n"
|
|
out = write_kdl(parse_kdl(src))
|
|
self.assertIn("prefer-no-csd", out)
|
|
|
|
|
|
class TestMutationHelpers(unittest.TestCase):
|
|
"""Tests for find_or_create, set_child_arg, remove_child, set_node_flag."""
|
|
|
|
def setUp(self):
|
|
self.nodes = parse_kdl("layout {\n gaps 8\n}\n")
|
|
|
|
def test_find_existing(self):
|
|
node = find_or_create(self.nodes, "layout")
|
|
self.assertEqual(node.name, "layout")
|
|
|
|
def test_create_missing(self):
|
|
node = find_or_create(self.nodes, "input")
|
|
self.assertEqual(node.name, "input")
|
|
self.assertIn(node, self.nodes)
|
|
|
|
def test_find_or_create_nested(self):
|
|
node = find_or_create(self.nodes, "layout", "struts")
|
|
self.assertEqual(node.name, "struts")
|
|
|
|
def test_set_child_arg_creates(self):
|
|
parent = self.nodes[0]
|
|
set_child_arg(parent, "border-rule", 4)
|
|
child = parent.get_child("border-rule")
|
|
self.assertIsNotNone(child)
|
|
self.assertEqual(child.args[0], 4)
|
|
|
|
def test_set_child_arg_updates(self):
|
|
parent = self.nodes[0]
|
|
set_child_arg(parent, "gaps", 16)
|
|
self.assertEqual(parent.get_child("gaps").args[0], 16)
|
|
|
|
def test_remove_child(self):
|
|
parent = self.nodes[0]
|
|
remove_child(parent, "gaps")
|
|
self.assertIsNone(parent.get_child("gaps"))
|
|
|
|
def test_remove_nonexistent_is_noop(self):
|
|
parent = self.nodes[0]
|
|
remove_child(parent, "nonexistent")
|
|
self.assertEqual(len(parent.children), 1)
|
|
|
|
def test_set_node_flag_add(self):
|
|
parent = KdlNode("input")
|
|
set_node_flag(parent, "warp-mouse-to-focus", True)
|
|
self.assertIsNotNone(parent.get_child("warp-mouse-to-focus"))
|
|
self.assertEqual(parent.get_child("warp-mouse-to-focus").args, [])
|
|
|
|
def test_set_node_flag_serializes_bare_flag(self):
|
|
parent = KdlNode("blur")
|
|
|
|
set_node_flag(parent, "off", True)
|
|
|
|
self.assertIn("off", write_kdl([parent]))
|
|
self.assertNotIn("off true", write_kdl([parent]))
|
|
|
|
def test_set_node_flag_remove(self):
|
|
parent = KdlNode("input")
|
|
parent.children.append(KdlNode("warp-mouse-to-focus"))
|
|
set_node_flag(parent, "warp-mouse-to-focus", False)
|
|
self.assertIsNone(parent.get_child("warp-mouse-to-focus"))
|
|
|
|
def test_set_node_flag_restores_bare_flag(self):
|
|
parent = KdlNode("blur")
|
|
parent.children.append(KdlNode("off"))
|
|
|
|
set_node_flag(parent, "off", False)
|
|
set_node_flag(parent, "off", True)
|
|
|
|
self.assertIn("off", write_kdl([parent]))
|
|
self.assertNotIn("off true", write_kdl([parent]))
|
|
|
|
def test_set_node_flag_idempotent_add(self):
|
|
parent = KdlNode("input")
|
|
set_node_flag(parent, "warp-mouse-to-focus", True)
|
|
set_node_flag(parent, "warp-mouse-to-focus", True)
|
|
count = sum(1 for c in parent.children if c.name == "warp-mouse-to-focus")
|
|
self.assertEqual(count, 1)
|
|
|
|
|
|
class TestWriteKdl(unittest.TestCase):
|
|
"""Tests for the KDL serializer."""
|
|
|
|
def test_write_empty(self):
|
|
out = write_kdl([])
|
|
self.assertIn("NiriMod", out)
|
|
|
|
def test_write_simple(self):
|
|
nodes = [KdlNode("prefer-no-csd")]
|
|
out = write_kdl(nodes)
|
|
self.assertIn("prefer-no-csd", out)
|
|
|
|
def test_write_raw_string(self):
|
|
# A value containing a double-quote triggers the hash-delimited r#"..."# form
|
|
node = KdlNode("env", args=[KdlRawString('value with "double" quotes')])
|
|
out = write_kdl([node])
|
|
self.assertIn('r#"', out)
|
|
|
|
def test_write_nested(self):
|
|
parent = KdlNode("layout")
|
|
parent.children.append(KdlNode("gaps", args=[16]))
|
|
out = write_kdl([parent])
|
|
self.assertIn("gaps", out)
|
|
self.assertIn("16", out)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|