From 55411f879f4a66476dd45782bfb453f015683d27 Mon Sep 17 00:00:00 2001 From: David Rebbe Date: Tue, 15 Sep 2026 23:33:31 -0400 Subject: [PATCH] fix: correct radio message argument parsing --- src/methods.cpp | 16 ++++----- tests/test_radio_message.py | 65 +++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 tests/test_radio_message.py diff --git a/src/methods.cpp b/src/methods.cpp index 1fde87fd..dceaa6cd 100644 --- a/src/methods.cpp +++ b/src/methods.cpp @@ -2445,13 +2445,13 @@ PyObject* meth_write_sdcard( PyObject* meth_create_neovi_radio_message(PyObject* self, PyObject* args, PyObject* keywords) { (void)self; - int relay1 = 0; - int relay2 = 0; - int relay3 = 0; - int relay4 = 0; - int relay5 = 0; - int led5 = 0; - int led6 = 0; + unsigned char relay1 = 0; + unsigned char relay2 = 0; + unsigned char relay3 = 0; + unsigned char relay4 = 0; + unsigned char relay5 = 0; + unsigned char led5 = 0; + unsigned char led6 = 0; int msb = 0; int lsb = 0; int analog = 0; @@ -2461,7 +2461,7 @@ PyObject* meth_create_neovi_radio_message(PyObject* self, PyObject* args, PyObje #endif char* kwords[] = { "Relay1", "Relay2", "Relay3", "Relay4", "Relay5", "LED5", "LED6", "MSB_report_rate", "LSB_report_rate", "analog_change_report_rate", - "relay_timeout" }; + "relay_timeout", NULL }; // Accepts keywords: Relay1-Relay5 (boolean), LED5 (boolean), LED6 (boolean), MSB_report_rate (int), // LSB_report_rate (int), analog_change_report_rate (int), relay_timeout (int). if (!PyArg_ParseTupleAndKeywords(args, diff --git a/tests/test_radio_message.py b/tests/test_radio_message.py new file mode 100644 index 00000000..ce6c1148 --- /dev/null +++ b/tests/test_radio_message.py @@ -0,0 +1,65 @@ +"""Hardware-independent radio message parsing regressions. + +Run each call in a child process so native parser crashes become test failures. +""" + +import os +import subprocess +import sys + +import pytest + + +@pytest.mark.parametrize( + "code", + [ + "assert ics.create_neovi_radio_message() == (0, 0, 0, 0, 0)", + """assert ics.create_neovi_radio_message( + Relay1=1, Relay2=0, Relay3=2, Relay4=0, Relay5=255, + LED5=0, LED6=1, MSB_report_rate=0x123, LSB_report_rate=0x45, + analog_change_report_rate=0x67, relay_timeout=0x89, + ) == (0x55, 0x23, 0x45, 0x67, 0x89)""", + "assert ics.create_neovi_radio_message(0, 1, 0, 1, 0, 1, 0, 255, 128, 1, 2) == (0x2A, 255, 128, 1, 2)", + "assert ics.create_neovi_radio_message(relay_timeout=255) == (0, 0, 0, 0, 255)", + """try: + ics.create_neovi_radio_message(unknown=1) +except TypeError: + pass +else: + raise AssertionError('unknown keyword was accepted')""", + *[ + f"""try: + ics.create_neovi_radio_message(**{{{name!r}: {value!r}}}) +except {error}: + pass +else: + raise AssertionError('invalid argument was accepted')""" + for name in ("Relay1", "Relay2", "Relay3", "Relay4", "Relay5", "LED5", "LED6") + for value, error in ((-1, "OverflowError"), (256, "OverflowError"), ("bad", "TypeError")) + ], + ], + ids=["defaults", "all-keywords", "positional", "last-keyword", "unknown-keyword"] + + [ + f"{name}-{case}" + for name in ("Relay1", "Relay2", "Relay3", "Relay4", "Relay5", "LED5", "LED6") + for case in ("negative", "overflow", "non-integer") + ], +) +def test_create_neovi_radio_message_subprocess(code): + env = os.environ.copy() + # Preserve the test runner's import path, including an in-place extension. + env["PYTHONPATH"] = os.pathsep.join(sys.path) + prelude = "import ics\n" + if sys.platform == "win32": + # Suppress Windows Error Reporting dialogs when testing a broken build. + prelude = "import ctypes\nctypes.windll.kernel32.SetErrorMode(3)\n" + prelude + result = subprocess.run( + [sys.executable, "-c", prelude + code], + env=env, + capture_output=True, + text=True, + timeout=30, + ) + assert result.returncode == 0, ( + f"child exited with {result.returncode}\nstdout: {result.stdout}\nstderr: {result.stderr}" + )