From 6d19a33f451e3fd1cfe60ba6314f0d1f375ad7a4 Mon Sep 17 00:00:00 2001 From: MCKero Date: Fri, 28 Aug 2026 04:37:32 +0100 Subject: [PATCH] Add key-name validation mirroring the keypad model Rejects unknown names in the server rather than letting them reach QMP as an error. A test parses keypad_key_names out of qemu/py32f071.c and fails if the lists drift, so adding a key to the model cannot silently leave the web UI behind. PTT is deliberately absent: the model wires it separately on GPIOC, so the press property rejects the name. --- tools/test_uvk5_keys.py | 52 +++++++++++++++++++++++++++++++++++++++++ tools/uvk5_keys.py | 24 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 tools/test_uvk5_keys.py create mode 100644 tools/uvk5_keys.py diff --git a/tools/test_uvk5_keys.py b/tools/test_uvk5_keys.py new file mode 100644 index 0000000..5d629ec --- /dev/null +++ b/tools/test_uvk5_keys.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""Unit tests for key-name validation.""" +import os +import re +import unittest + +from uvk5_keys import KEYS, is_valid, normalise + + +class TestKeys(unittest.TestCase): + def test_matches_the_model_key_list(self): + # keypad_key_names in qemu/py32f071.c + self.assertEqual(set(KEYS), { + "MENU", "UP", "DOWN", "EXIT", "F", "STAR", + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", + "SIDE1", "SIDE2", + }) + + def test_stays_in_sync_with_the_machine_model(self): + """Read the real list out of py32f071.c so the two cannot drift. + + If someone adds a key to the model, this fails until KEYS is updated -- + which beats discovering it as a QMP error at runtime. + """ + src = os.path.join(os.path.dirname(os.path.abspath(__file__)), + os.pardir, "qemu", "py32f071.c") + text = open(src).read() + block = re.search( + r"keypad_key_names\[[^\]]*\]\s*=\s*\{(.*?)\};", text, re.S) + self.assertIsNotNone(block, "could not find keypad_key_names in the model") + names = set(re.findall(r'"([^"]+)"', block.group(1))) + self.assertEqual(names, set(KEYS)) + + def test_ptt_is_not_offered(self): + # The keypad model has no PTT line; qom-set would error on it. + self.assertFalse(is_valid("PTT")) + + def test_normalise_is_case_insensitive_and_strips(self): + self.assertEqual(normalise("menu"), "MENU") + self.assertEqual(normalise(" up "), "UP") + self.assertTrue(is_valid("menu")) + + def test_rejects_unknown(self): + self.assertFalse(is_valid("BANANA")) + + def test_rejects_empty_and_none(self): + self.assertFalse(is_valid("")) + self.assertFalse(is_valid(None)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/uvk5_keys.py b/tools/uvk5_keys.py new file mode 100644 index 0000000..7662803 --- /dev/null +++ b/tools/uvk5_keys.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +"""Key names the keypad model accepts. + +Mirrors keypad_key_names in qemu/py32f071.c. test_uvk5_keys.py parses that array +out of the model source and fails if the two drift apart. + +PTT is absent on purpose: the keypad model has no PTT line -- it is wired +separately on GPIOC -- so the "press" property rejects the name. Offering a PTT +button would produce a QMP error rather than a transmission. +""" + +KEYS = ( + "MENU", "UP", "DOWN", "EXIT", "F", "STAR", + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", + "SIDE1", "SIDE2", +) + + +def normalise(name) -> str: + return (name or "").strip().upper() + + +def is_valid(name) -> bool: + return normalise(name) in KEYS