Merge branch 'review/pr-894'

Closes #894
This commit is contained in:
Ian McEwen
2026-05-31 18:10:59 -07:00
4 changed files with 268 additions and 11 deletions

View File

@@ -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

View File

@@ -11,6 +11,7 @@ from types import SimpleNamespace
from unittest.mock import mock_open, MagicMock, patch
import pytest
import meshtastic.__main__ as mt_main
from meshtastic.__main__ import (
export_config,
@@ -27,6 +28,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
@@ -261,6 +263,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 <name_or_address>", 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):