Files
2026-09-06 01:06:40 +08:00

184 lines
8.5 KiB
Python

#!/usr/bin/env python3
"""Офлайн-тесты команды apply: YAML-конфиг → argv существующих команд (без железа).
Запуск: python3 tests/test_apply.py (или pytest tests/test_apply.py).
Эталон — katana.yaml.example: конфиг должен разбираться и превращаться
в валидные argv для build_parser().parse_args() (валидация значений —
та же, что у обычных команд).
"""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from katana import (APPLY_SECTION_ORDER, CONFIG_KEYS, _build_macro_slots,
build_parser, config_argv, load_config, macro_argv)
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
EXAMPLE = os.path.join(ROOT, "katana.yaml.example")
def parse_ok(argv):
"""parse_args без запуска команды: проверяет валидность значений."""
return build_parser().parse_args(argv)
def test_example_config_loads():
"""Пример конфига читается, секции — только разрешённые."""
cfg = load_config(EXAMPLE)
assert set(cfg) <= set(APPLY_SECTION_ORDER), set(cfg)
assert {"macros", "remap", "paint", "lighting"} <= set(cfg)
# YAML-ключи слотов макросов парсятся как int
assert sorted(cfg["macros"]) == [0, 1]
def test_example_config_argv_valid():
"""Все секции примера превращаются в валидные argv существующих команд.
save=False для всех секций, кроме последней — так делает cmd_apply
(save коммитит всю RAM, поэтому он только у последней команды).
"""
cfg = load_config(EXAMPLE)
for slot, spec in cfg["macros"].items():
ns = parse_ok(macro_argv(slot, spec, save=False))
assert ns.cmd == "macro" and ns.macro_cmd == "set" and not ns.save
ns = parse_ok(config_argv("remap", cfg["remap"], save=False))
assert ns.cmd == "remap" and "caps=macro0" in ns.key and ns.no_save
ns = parse_ok(config_argv("paint", cfg["paint"], save=False))
assert ns.cmd == "paint" and ns.all == (0, 0, 0) and ns.no_save
assert ns.wasd == (30, 144, 255) # dodgerblue через parse_color
assert "esc=ff0000" in ns.key
ns = parse_ok(config_argv("lighting", cfg["lighting"], save=True))
assert ns.cmd == "mode" and ns.n == 1 # static (последняя секция — с save)
assert ns.color == (255, 0, 0) and not ns.no_save
def test_remap_argv():
"""[remap]: keys → --key, save управляется cmd_apply (не ключом секции)."""
argv = config_argv("remap", {"keys": {"caps": "copy", "menu": "meta+e"}},
save=False)
assert argv == ["remap", "--key", "caps=copy", "--key", "menu=meta+e",
"--no-save"]
argv = config_argv("remap", {"keys": {"caps": "copy"}}, save=True)
assert argv == ["remap", "--key", "caps=copy"] # save=True — без --no-save
argv = config_argv("remap", {"clear": True}, save=False)
assert "--clear" in argv
parse_ok(argv)
def test_lighting_argv():
"""[lighting]: имена/номера режимов, off/default, опциональные поля."""
ns = parse_ok(config_argv("lighting", {"mode": "static", "speed": 5},
save=False))
assert (ns.cmd, ns.n, ns.speed) == ("mode", 1, 5) and ns.no_save
ns = parse_ok(config_argv("lighting", {"mode": 1, "color": "ff8800"},
save=True))
assert (ns.cmd, ns.n, ns.color) == ("mode", 1, (255, 136, 0))
assert config_argv("lighting", {"mode": "off"}, save=False) == \
["off", "--no-save"]
assert config_argv("lighting", {"mode": "off"}, save=True) == ["off"]
# default сам делает save, флаг не нужен
assert config_argv("lighting", {"mode": "default"}, save=False) == ["default"]
def test_paint_argv():
"""[paint]: all/keys/rows/shorthand-алиасы/black/keep/brightness."""
argv = config_argv("paint", {
"all": "black", "keys": {"esc": "red"}, "rows": {1: "202020"},
"wasd": "dodgerblue", "black": True, "keep": True, "brightness": 10,
}, save=False)
ns = parse_ok(argv)
assert ns.all == (0, 0, 0) and ns.black and ns.keep and ns.no_save
assert ns.brightness == 10
assert "esc=red" in ns.key
assert ns.row1 == (32, 32, 32) # --row1 202020
assert ns.wasd == (30, 144, 255)
def test_macro_argv():
"""[macros]: tokens через '--', xml, save управляется cmd_apply."""
argv = macro_argv(0, {"tokens": ["+lctrl", "a@50", "-lctrl"]}, save=False)
assert argv == ["macro", "set", "0", "--", "+lctrl", "a@50", "-lctrl"]
ns = parse_ok(argv) # токены с '-' не съедаются как опции
assert ns.index == 0 and ns.tokens == ["+lctrl", "a@50", "-lctrl"] and not ns.save
argv = macro_argv(1, {"xml": "test.xml"}, save=True)
# --save строго ДО '--': после '--' argparse считает всё позиционным
ns = parse_ok(argv)
assert ns.xml == "test.xml" and ns.save
for spec in ({"tokens": ["a"], "xml": "t.xml"}, {}):
try:
macro_argv(0, spec, save=False)
except SystemExit:
pass
else:
raise AssertionError(f"spec {spec} must fail")
def test_build_macro_slots():
"""[macros] → слоты одним блобом: несколько слотов, пропуски, ошибки."""
slots = _build_macro_slots({
0: {"tokens": ["+lctrl", "a@50", "-lctrl"]},
2: {"tokens": ["b"]},
})
# слот 1 — пропуск (пустой), слоты 0 и 2 заполнены
assert len(slots) == 3 and len(slots[0]) == 4 and slots[1] == [] \
and len(slots[2]) == 2
slots = _build_macro_slots({5: {"xml": "raw-data/katana-v1/test.xml"}})
assert len(slots) == 6 and slots[0] == [] and len(slots[5]) == 8
for slots_cfg in ({"0": {"tokens": ["a"], "xml": "t.xml"}}, # оба поля
{"0": {}}, # ни одного
{"x": {"tokens": ["a"]}}, # не число
{"0": {"tokens": []}}, # пустой список
{"0": {"tokens": ["a"], "save": True}}): # save не поддержан
try:
_build_macro_slots(slots_cfg)
except SystemExit:
pass
else:
raise AssertionError(f"{slots_cfg} must fail")
def test_unknown_keys_and_sections():
"""Опечатки в ключах секций и неизвестные секции отклоняются.
Ключ 'save' в секциях не поддерживается (apply управляет save сам)."""
for section, values in (
("remap", {"key": {"caps": "copy"}}), # опечатка keys
("paint", {"colour": "red"}), # опечатка color
("lighting", {"mode": "static", "spead": 5}), # опечатка speed
("remap", {"keys": {"caps": "copy"}, "save": True}), # save не поддержан
("paint", {"all": "black", "save": False}), # save не поддержан
("lighting", {"mode": "off", "save": True}), # save не поддержан
):
try:
config_argv(section, values, save=False)
except SystemExit as e:
assert "unknown keys" in str(e), e
else:
raise AssertionError(f"{section} {values} must fail")
try:
load_config("/nonexistent/katana.yaml")
except SystemExit as e:
assert "not found" in str(e), e
else:
raise AssertionError("missing config must fail")
def test_section_order():
"""Порядок применения: macros последними — запись макросов сбивает
режим подсветки, а save коммитит всю RAM (см. katana.py)."""
assert APPLY_SECTION_ORDER == ("remap", "paint", "lighting", "raw", "macros")
assert set(CONFIG_KEYS) == {"remap", "paint", "lighting"}
def main():
tests = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
for test in tests:
test()
print(f"ok: {test.__name__}")
print(f"\n{len(tests)} tests passed")
if __name__ == "__main__":
main()