Files

220 lines
9.6 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Офлайн-тесты парсеров и низкоуровневых функций katana.py (без железа).
Запуск: python3 tests/test_parsers.py (или pytest tests/test_parsers.py).
Покрывает: parse_color, parse_mode, check_range, resolve_key, build_payload,
номера hidraw ioctl, статические проверки Katana (_check, _check_checksum)
и целостность keymap.py (карта, слепые слоты, алиасы, группы клавиш).
"""
import argparse
import contextlib
import io
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import misc.keymap as keymap
from katana import (ALPHA_KEYS, ARROW_KEYS, BIND_COUNT, DIGITS_KEYS, KEYS_COUNT,
MODES, NUMPAD_KEYS, PUNCT_KEYS, ROW_KEYS, Katana, _hid_ioc,
_hid_iocgfeature, _hid_iocsfeature, build_payload,
check_range, parse_color, parse_mode, resolve_key)
def expect_systemexit(func, *args, **kwargs):
"""Вызвать func и вернуть текст исключения SystemExit (или зафейлить)."""
try:
func(*args, **kwargs)
except SystemExit as e:
return str(e)
raise AssertionError(f"{func.__name__}{args} must fail with SystemExit")
def test_parse_color():
"""parse_color: hex, '#hex', именованные цвета, регистр; мусор отклоняется."""
assert parse_color("ff0000") == (255, 0, 0)
assert parse_color("#FF0000") == (255, 0, 0) # решётка и верхний регистр
assert parse_color("red") == (255, 0, 0)
assert parse_color("BLACK") == (0, 0, 0)
assert parse_color("dodgerblue") == (30, 144, 255)
for bad in ("ff00", "zzzzzz", "red!", "12345", ""):
try:
parse_color(bad)
except (argparse.ArgumentTypeError, ValueError):
# ValueError для 6-символьного не-hex перехватывает argparse
pass
else:
raise AssertionError(f"color {bad!r} must fail")
def test_parse_mode():
"""parse_mode: номер (в т.ч. 0x-форма), алиас, off; мусор отклоняется."""
assert parse_mode("1") == 1
assert parse_mode("19") == 19
assert parse_mode("0x10") == 16
assert parse_mode("static") == 1
assert parse_mode("SHUTTLE") == 19 # регистр не важен
assert parse_mode("off") == 0 and parse_mode("OFF") == 0
# все алиасы MODES распознаются и дают свои номера
for num, (name, _) in MODES.items():
assert parse_mode(name) == num, name
try:
parse_mode("nosuch")
except argparse.ArgumentTypeError:
pass
else:
raise AssertionError("unknown mode must fail")
def test_check_range():
"""check_range: значение в диапазоне возвращается, вне — SystemExit."""
assert check_range(5, 1, 15, "brightness") == 5
msg = expect_systemexit(check_range, 0, 1, 15, "brightness")
assert "brightness must be 1..15" in msg, msg
msg = expect_systemexit(check_range, 16, 1, 15, "brightness")
assert "got 16" in msg, msg
def test_resolve_key():
"""resolve_key: имена keymap, символы, алиасы, hex-индексы; цифры — имена."""
assert resolve_key("esc") == 0x01
assert resolve_key("Esc") == 0x01 # регистр не важен
assert resolve_key("caps") == 0x37
assert resolve_key("space") == 0x5E
# символы-псевдонимы (базовые имена из KEYMAP до скобки)
assert resolve_key("[") == 0x30
assert resolve_key("~") == 0x13 # shifted-вариант backquote
assert resolve_key(";") == 0x41
assert resolve_key(chr(92)) == 0x43
# синонимы из KEY_SYMBOL_ALIASES
assert resolve_key("tilde") == 0x13
assert resolve_key("lbracket") == 0x30
assert resolve_key("capslock") == 0x37
assert resolve_key("ctrl") == 0x5B
# голые цифры — это ИМЕНА клавиш цифрового ряда, а не индексы
assert resolve_key("1") == 0x14
assert resolve_key("0") == 0x1D
# hex-индекс передаётся напрямую
assert resolve_key("0x25") == 0x25
assert resolve_key("0x8e") == 0x8E
# нераспознанное — None
assert resolve_key("0xzz") is None
assert resolve_key("nosuchkey") is None
assert resolve_key("") is None
def test_build_payload():
"""build_payload: раскладка 64-байтного lighting payload."""
p = build_payload(7, (0x11, 0x22, 0x33), flag=0, brightness=0x0C,
speed=0x05, submode=0x01)
assert len(p) == 64
assert p[0] == 7 # режим
assert p[1:4] == bytes([0x11, 0x22, 0x33]) # RGB
assert p[8] == 0 # flag
assert p[9] == 0x0C # яркость
assert p[10] == 0x05 # скорость
assert p[11] == 0x01 # подрежим/направление
assert p[14:16] == b"\xaa\x55" # магия
assert p[4:8] == bytes(4) and p[12:14] == bytes(2) # остальное — нули
def test_hid_ioc_numbers():
"""Номера ioctl _IOWR('H', nr, size): размер в байтах 16–29, 'H' в 8–15."""
expected = (3 << 30) | (65 << 16) | (0x48 << 8) | 0x06
assert _hid_ioc(0x06, 65) == expected
assert _hid_iocsfeature(65) == _hid_ioc(0x06, 65) # SET_REPORT
assert _hid_iocgfeature(65) == _hid_ioc(0x07, 65) # GET_REPORT
def test_katana_check():
"""Katana._check: ACK с правильной командой и статусом 01 проходит."""
ok = bytes([0x04, 0x18, 0x00, 0x01]) + bytes(60)
Katana._check(ok, 0x18) # не должно бросать
# чужой opcode в ACK
msg = expect_systemexit(Katana._check, ok, 0x13)
assert "bad ACK" in msg, msg
# короткий ACK
msg = expect_systemexit(Katana._check, b"\x04\x18", 0x18)
assert "bad ACK" in msg, msg
# отказ прошивки (статус 00/ff)
bad = bytes([0x04, 0x18, 0x00, 0x00]) + bytes(60)
msg = expect_systemexit(Katana._check, bad, 0x18)
assert "rejected" in msg and "reset" in msg, msg
def test_katana_check_checksum():
"""Katana._check_checksum: совпадение молчит, расхождение — warning в stderr."""
data = bytes(range(64))
ack_ok = bytearray(64)
ack_ok[4:6] = (sum(data) & 0xFFFF).to_bytes(2, "little")
err = io.StringIO()
with contextlib.redirect_stderr(err):
Katana._check_checksum(bytes(ack_ok), data)
assert err.getvalue() == "", "matching checksum must not warn"
ack_bad = bytearray(ack_ok)
ack_bad[4] ^= 0xFF
with contextlib.redirect_stderr(err):
Katana._check_checksum(bytes(ack_bad), data)
assert "warning" in err.getvalue() and "checksum" in err.getvalue()
def test_keymap_integrity():
"""keymap.py: индексы в диапазоне слотов, карта не пересекается со слепыми."""
for idx in keymap.KEYMAP:
assert 0 <= idx < KEYS_COUNT, f"index {idx:#x} out of range"
assert not set(keymap.KEYMAP) & keymap.BLIND, "keymap overlaps blind slots"
# name(): клавиша, слепой слот, неизвестный слот
assert keymap.name(0x01) == "Esc"
assert keymap.name(0x00) == "(blind)"
unknown = next(iter(keymap.UNKNOWN), None)
if unknown is not None:
assert keymap.name(unknown).endswith("?")
def test_keymap_aliases_resolve():
"""Каждый алиас keymap указывает на тот же индекс, что и каноническое имя."""
for alias, canonical in keymap.KEY_SYMBOL_ALIASES.items():
idx = resolve_key(canonical)
if idx is None:
continue # каноническое имя не в KEYMAP — алиас просто не заведётся
assert resolve_key(alias) == idx, f"{alias} -> {canonical} != {idx:#x}"
def test_paint_groups_resolve():
"""Все клавиши shorthand-групп paint (--wasd/--numpad/--alpha/...) резолвятся."""
groups = {"numpad": NUMPAD_KEYS, "alpha": ALPHA_KEYS, "punct": PUNCT_KEYS,
"digits": DIGITS_KEYS, "arrows": ARROW_KEYS}
for name, keys in groups.items():
for key in keys:
idx = resolve_key(key)
assert idx is not None, f"{name}: key {key!r} does not resolve"
assert 0 <= idx < KEYS_COUNT
for row_n, keys in ROW_KEYS.items():
assert 1 <= row_n <= 6
for key in keys:
idx = resolve_key(key)
assert idx is not None, f"row{row_n}: key {key!r} does not resolve"
# группа wasd — просто буквы из alpha
for key in ("w", "a", "s", "d"):
assert resolve_key(key) is not None
def test_counts():
"""Константы размеров согласованы: 143 слота в обеих таблицах."""
assert KEYS_COUNT == BIND_COUNT == 143
assert len(keymap.KEYMAP) + len(keymap.BLIND) + len(keymap.UNKNOWN) \
== 0x8F # слоты 0x00..0x8e покрыты тремя списками без пересечений
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()