#!/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()