From 0bef1370d7bd9c8eb5ee7c287008982e72f0bb11 Mon Sep 17 00:00:00 2001 From: Ian McEwen Date: Sun, 31 May 2026 18:10:30 -0700 Subject: [PATCH] tests of new BLEError functionality --- meshtastic/tests/test_ble_interface.py | 69 ++++++++++++++++ meshtastic/tests/test_main.py | 109 +++++++++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 meshtastic/tests/test_ble_interface.py diff --git a/meshtastic/tests/test_ble_interface.py b/meshtastic/tests/test_ble_interface.py new file mode 100644 index 0000000..0b725e6 --- /dev/null +++ b/meshtastic/tests/test_ble_interface.py @@ -0,0 +1,69 @@ +"""Meshtastic unit tests for ble_interface.py""" + +from unittest.mock import MagicMock, patch + +import pytest +from bleak.exc import BleakError + +from ..ble_interface import BLEInterface + + +@pytest.mark.unit +def test_ble_error_default_kind_unknown(): + """BLEError defaults to UNKNOWN kind.""" + error = BLEInterface.BLEError("test") + assert error.kind == BLEInterface.BLEError.UNKNOWN + + +@pytest.mark.unit +def test_ble_find_device_not_found_sets_kind(): + """find_device emits DEVICE_NOT_FOUND for no scan results.""" + iface = object.__new__(BLEInterface) + with patch("meshtastic.ble_interface.BLEInterface.scan", return_value=[]): + with pytest.raises(BLEInterface.BLEError) as excinfo: + iface.find_device("missing") + assert excinfo.value.kind == BLEInterface.BLEError.DEVICE_NOT_FOUND + + +@pytest.mark.unit +def test_ble_find_device_multiple_sets_kind(): + """find_device emits MULTIPLE_DEVICES for ambiguous matches.""" + iface = object.__new__(BLEInterface) + first = MagicMock() + first.name = "dup" + first.address = "AA:AA:AA:AA:AA:01" + second = MagicMock() + second.name = "dup" + second.address = "AA:AA:AA:AA:AA:02" + with patch( + "meshtastic.ble_interface.BLEInterface.scan", return_value=[first, second] + ): + with pytest.raises(BLEInterface.BLEError) as excinfo: + iface.find_device("dup") + assert excinfo.value.kind == BLEInterface.BLEError.MULTIPLE_DEVICES + + +@pytest.mark.unit +def test_ble_send_to_radio_wraps_write_errors_with_kind(): + """_sendToRadioImpl wraps write failures with WRITE_ERROR.""" + iface = object.__new__(BLEInterface) + iface.client = MagicMock() + iface.client.write_gatt_char.side_effect = RuntimeError("boom") + to_radio = MagicMock() + to_radio.SerializeToString.return_value = b"\x01" + with pytest.raises(BLEInterface.BLEError) as excinfo: + iface._sendToRadioImpl(to_radio) + assert excinfo.value.kind == BLEInterface.BLEError.WRITE_ERROR + + +@pytest.mark.unit +def test_ble_receive_wraps_unexpected_bleak_error_with_kind(): + """_receiveFromRadioImpl wraps unexpected BleakError with READ_ERROR.""" + iface = object.__new__(BLEInterface) + iface.should_read = True + iface._want_receive = True + iface.client = MagicMock() + iface.client.read_gatt_char.side_effect = BleakError("some other BLE failure") + with pytest.raises(BLEInterface.BLEError) as excinfo: + iface._receiveFromRadioImpl() + assert excinfo.value.kind == BLEInterface.BLEError.READ_ERROR diff --git a/meshtastic/tests/test_main.py b/meshtastic/tests/test_main.py index 251de98..ca1eace 100644 --- a/meshtastic/tests/test_main.py +++ b/meshtastic/tests/test_main.py @@ -9,6 +9,7 @@ import sys from unittest.mock import mock_open, MagicMock, patch import pytest +import meshtastic.__main__ as mt_main from meshtastic.__main__ import ( export_config, @@ -25,6 +26,7 @@ from meshtastic import mt_config from ..protobuf.channel_pb2 import Channel # pylint: disable=E0611 # from ..ble_interface import BLEInterface +from ..mesh_interface import MeshInterface from ..node import Node # from ..radioconfig_pb2 import UserPreferences @@ -259,6 +261,113 @@ def test_main_info_with_permission_error(patched_getlogin, capsys, caplog): assert err == "" +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +def test_main_ble_device_not_found_message(capsys): + """Test BLE device-not-found help text.""" + sys.argv = ["", "--info", "--ble", "any"] + mt_config.args = sys.argv + + with patch("meshtastic.__main__.BLEInterface.__init__") as mock_ble_init: + mock_ble_init.side_effect = mt_main.BLEInterface.BLEError( + "missing", + mt_main.BLEInterface.BLEError.DEVICE_NOT_FOUND, + ) + with pytest.raises(SystemExit) as excinfo: + main() + + out, err = capsys.readouterr() + assert excinfo.value.code == 1 + assert re.search(r"BLE device not found", out, re.MULTILINE) + assert re.search(r"--ble-scan", out, re.MULTILINE) + assert err == "" + + +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +def test_main_ble_multiple_devices_message(capsys): + """Test BLE multiple-devices help text.""" + sys.argv = ["", "--info", "--ble", "any"] + mt_config.args = sys.argv + + with patch("meshtastic.__main__.BLEInterface.__init__") as mock_ble_init: + mock_ble_init.side_effect = mt_main.BLEInterface.BLEError( + "multiple", + mt_main.BLEInterface.BLEError.MULTIPLE_DEVICES, + ) + with pytest.raises(SystemExit) as excinfo: + main() + + out, err = capsys.readouterr() + assert excinfo.value.code == 1 + assert re.search(r"Multiple Meshtastic BLE devices found", out, re.MULTILINE) + assert re.search(r"meshtastic --ble ", out, re.MULTILINE) + assert err == "" + + +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +def test_main_ble_write_error_message(capsys): + """Test BLE write-error help text.""" + sys.argv = ["", "--info", "--ble", "any"] + mt_config.args = sys.argv + + with patch("meshtastic.__main__.BLEInterface.__init__") as mock_ble_init: + mock_ble_init.side_effect = mt_main.BLEInterface.BLEError( + "write fail", + mt_main.BLEInterface.BLEError.WRITE_ERROR, + ) + with pytest.raises(SystemExit) as excinfo: + main() + + out, err = capsys.readouterr() + assert excinfo.value.code == 1 + assert re.search(r"Failed to write to BLE device", out, re.MULTILINE) + assert re.search(r"user not in 'bluetooth' group", out, re.MULTILINE) + assert err == "" + + +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +def test_main_ble_read_error_message(capsys): + """Test BLE read-error help text.""" + sys.argv = ["", "--info", "--ble", "any"] + mt_config.args = sys.argv + + with patch("meshtastic.__main__.BLEInterface.__init__") as mock_ble_init: + mock_ble_init.side_effect = mt_main.BLEInterface.BLEError( + "read fail", + mt_main.BLEInterface.BLEError.READ_ERROR, + ) + with pytest.raises(SystemExit) as excinfo: + main() + + out, err = capsys.readouterr() + assert excinfo.value.code == 1 + assert re.search(r"Failed to read from BLE device", out, re.MULTILINE) + assert re.search(r"Move closer to the device", out, re.MULTILINE) + assert err == "" + + +@pytest.mark.unit +@pytest.mark.usefixtures("reset_mt_config") +def test_main_serial_timeout_message(capsys): + """Test serial timeout help text.""" + sys.argv = ["", "--info"] + mt_config.args = sys.argv + + with patch("meshtastic.serial_interface.SerialInterface") as mock_serial: + mock_serial.side_effect = MeshInterface.MeshInterfaceError("Timed out waiting") + with pytest.raises(SystemExit) as excinfo: + main() + + out, err = capsys.readouterr() + assert excinfo.value.code == 1 + assert re.search(r"Connection timed out", out, re.MULTILINE) + assert re.search(r"Device is rebooting", out, re.MULTILINE) + assert err == "" + + @pytest.mark.unit @pytest.mark.usefixtures("reset_mt_config") def test_main_info_with_tcp_interface(capsys):