Skip to content

Commit 96f825a

Browse files
committed
Add skipped unit tests for BLE support (nvaccess#19122)
TDD approach: introduce all tests with skip decorators ahead of implementation. - tests/unit/test_hwIo_ble.py: BLE scanner, BLE I/O, and findDeviceByAddress - tests/unit/brailleDisplayDrivers/test_dotPad.py: buffered receive logic - tests/unit/test_bdDetect.py: BLE device registration and matching
1 parent 70370f4 commit 96f825a

5 files changed

Lines changed: 631 additions & 0 deletions

File tree

tests/unit/brailleDisplayDrivers/__init__.py

Whitespace-only changes.
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# A part of NonVisual Desktop Access (NVDA)
2+
# This file is covered by the GNU General Public License.
3+
# See the file COPYING for more details.
4+
# Copyright (C) 2025-2026 NV Access Limited, Dot Incorporated, Bram Duvigneau
5+
6+
"""Unit tests for the dotPad braille display driver.
7+
8+
These tests cover the buffered receive logic that supports both serial (byte-at-a-time)
9+
and BLE (complete packet) receive modes. The implementation is part of #19122.
10+
"""
11+
12+
import unittest
13+
from unittest.mock import MagicMock
14+
import struct
15+
import functools
16+
import operator
17+
18+
19+
@unittest.skip("Requires buffered receive implementation from #19122")
20+
class TestDotPadBufferedReceive(unittest.TestCase):
21+
"""Tests for the buffered receive logic in the DotPad driver."""
22+
23+
def setUp(self):
24+
"""Set up test fixtures."""
25+
from brailleDisplayDrivers.dotPad.driver import BrailleDisplayDriver
26+
from brailleDisplayDrivers.dotPad.defs import (
27+
DP_Command,
28+
DP_PacketSyncByte,
29+
DP_CHECKSUM_BASE,
30+
)
31+
32+
self.BrailleDisplayDriver = BrailleDisplayDriver
33+
self.DP_Command = DP_Command
34+
self.DP_PacketSyncByte = DP_PacketSyncByte
35+
self.DP_CHECKSUM_BASE = DP_CHECKSUM_BASE
36+
37+
# Create a minimal driver instance for testing receive logic
38+
self.driver = MagicMock(spec=BrailleDisplayDriver)
39+
self.driver._receiveBuffer = bytearray()
40+
self.driver.MAX_PACKET_SIZE = 512
41+
self.driver._lastResponse = {}
42+
43+
# Track processed packets
44+
self.processedPackets = []
45+
46+
def mockProcessPacket(packetBody):
47+
self.processedPackets.append(bytes(packetBody))
48+
49+
self.driver._processPacket = mockProcessPacket
50+
51+
# Bind the actual _onReceive method
52+
self.driver._onReceive = BrailleDisplayDriver._onReceive.__get__(self.driver, type(self.driver))
53+
54+
def _createPacket(self, dest=0, cmd=0x0101, seqNum=0, data=b""):
55+
"""Helper to create a valid DotPad packet.
56+
57+
:param dest: Destination address
58+
:param cmd: Command code
59+
:param seqNum: Sequence number
60+
:param data: Packet data payload
61+
:return: Complete packet as bytes
62+
"""
63+
packetBody = bytearray([dest])
64+
packetBody.extend(struct.pack(">H", cmd))
65+
packetBody.append(seqNum)
66+
packetBody.extend(data)
67+
68+
checksum = functools.reduce(operator.xor, packetBody, self.DP_CHECKSUM_BASE)
69+
packetBody.append(checksum)
70+
71+
packet = bytearray(
72+
[
73+
self.DP_PacketSyncByte.SYNC1,
74+
self.DP_PacketSyncByte.SYNC2,
75+
],
76+
)
77+
packet.extend(struct.pack(">H", len(packetBody)))
78+
packet.extend(packetBody)
79+
80+
return bytes(packet)
81+
82+
def test_completePacketAtOnce(self):
83+
"""Test receiving a complete packet in a single call (BLE behavior)."""
84+
packet = self._createPacket(dest=0, cmd=0x0101, seqNum=1, data=b"test")
85+
86+
self.driver._onReceive(packet)
87+
88+
self.assertEqual(len(self.processedPackets), 1)
89+
self.assertEqual(len(self.driver._receiveBuffer), 0)
90+
91+
def test_byteAtATime(self):
92+
"""Test receiving a packet one byte at a time (Serial behavior)."""
93+
packet = self._createPacket(dest=0, cmd=0x0101, seqNum=1, data=b"AB")
94+
95+
for byte in packet:
96+
self.driver._onReceive(bytes([byte]))
97+
98+
self.assertEqual(len(self.processedPackets), 1)
99+
self.assertEqual(len(self.driver._receiveBuffer), 0)
100+
101+
def test_partialPacket(self):
102+
"""Test receiving a packet in multiple chunks."""
103+
packet = self._createPacket(dest=0, cmd=0x0101, seqNum=1, data=b"test data")
104+
105+
chunk1 = packet[: len(packet) // 2]
106+
chunk2 = packet[len(packet) // 2 :]
107+
108+
self.driver._onReceive(chunk1)
109+
self.assertEqual(len(self.processedPackets), 0)
110+
self.assertGreater(len(self.driver._receiveBuffer), 0)
111+
112+
self.driver._onReceive(chunk2)
113+
self.assertEqual(len(self.processedPackets), 1)
114+
self.assertEqual(len(self.driver._receiveBuffer), 0)
115+
116+
def test_multiplePacketsAtOnce(self):
117+
"""Test receiving multiple complete packets in a single call."""
118+
packet1 = self._createPacket(dest=0, cmd=0x0101, seqNum=1, data=b"A")
119+
packet2 = self._createPacket(dest=0, cmd=0x0102, seqNum=2, data=b"B")
120+
packet3 = self._createPacket(dest=0, cmd=0x0103, seqNum=3, data=b"C")
121+
122+
allPackets = packet1 + packet2 + packet3
123+
124+
self.driver._onReceive(allPackets)
125+
126+
self.assertEqual(len(self.processedPackets), 3)
127+
self.assertEqual(len(self.driver._receiveBuffer), 0)
128+
129+
def test_badSyncByte_resynchronize(self):
130+
"""Test that bad sync bytes are discarded and driver resynchronizes."""
131+
badData = b"\x00\x11\x22"
132+
goodPacket = self._createPacket(dest=0, cmd=0x0101, seqNum=1, data=b"OK")
133+
134+
self.driver._onReceive(badData + goodPacket)
135+
136+
self.assertEqual(len(self.processedPackets), 1)
137+
self.assertEqual(len(self.driver._receiveBuffer), 0)
138+
139+
def test_bufferOverflow_cleared(self):
140+
"""Test that buffer is cleared when it exceeds MAX_PACKET_SIZE."""
141+
garbageData = b"\xff" * (self.driver.MAX_PACKET_SIZE + 10)
142+
143+
self.driver._onReceive(garbageData)
144+
145+
self.assertEqual(len(self.processedPackets), 0)
146+
self.assertEqual(len(self.driver._receiveBuffer), 0)
147+
148+
def test_incompletePacketInBuffer(self):
149+
"""Test that incomplete packet stays in buffer."""
150+
packet = self._createPacket(dest=0, cmd=0x0101, seqNum=1, data=b"test")
151+
152+
partialData = packet[:6]
153+
self.driver._onReceive(partialData)
154+
155+
self.assertEqual(len(self.processedPackets), 0)
156+
self.assertEqual(len(self.driver._receiveBuffer), 6)
157+
self.assertEqual(bytes(self.driver._receiveBuffer), partialData)
158+
159+
def test_emptyData(self):
160+
"""Test receiving empty data doesn't cause errors."""
161+
self.driver._onReceive(b"")
162+
163+
self.assertEqual(len(self.processedPackets), 0)
164+
self.assertEqual(len(self.driver._receiveBuffer), 0)
165+
166+
def test_partialHeaderOnly(self):
167+
"""Test receiving only partial header (less than 4 bytes)."""
168+
self.driver._onReceive(
169+
bytes(
170+
[
171+
self.DP_PacketSyncByte.SYNC1,
172+
self.DP_PacketSyncByte.SYNC2,
173+
0x00,
174+
],
175+
),
176+
)
177+
178+
self.assertEqual(len(self.processedPackets), 0)
179+
self.assertEqual(len(self.driver._receiveBuffer), 3)

tests/unit/test_bdDetect.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,47 @@ def matchFunc(match: bdDetect.DeviceMatch) -> bool:
9999

100100
registrar.addBluetoothDevices(matchFunc)
101101
self.assertEqual(registrar._getDriverDict().get(bdDetect.CommunicationType.BLUETOOTH), matchFunc)
102+
103+
@unittest.skip("Requires BLE support in bdDetect from #19122")
104+
def test_addBleDevices(self):
105+
"""Test adding a BLE match function."""
106+
from brailleDisplayDrivers import dotPad
107+
108+
registrar = bdDetect.DriverRegistrar(dotPad.BrailleDisplayDriver.name)
109+
110+
def matchFunc(match: bdDetect.DeviceMatch) -> bool:
111+
return match.id.startswith("DotPad")
112+
113+
registrar.addBleDevices(matchFunc)
114+
115+
stored_match_func = registrar._getDriverDict().get(bdDetect.CommunicationType.BLE)
116+
self.assertEqual(stored_match_func, matchFunc)
117+
self.assertTrue(callable(stored_match_func))
118+
119+
@unittest.skip("Requires BLE support in bdDetect from #19122")
120+
def test_bleDeviceMatching(self):
121+
"""Test that BLE device matching works correctly."""
122+
from brailleDisplayDrivers import dotPad
123+
124+
registrar = bdDetect.DriverRegistrar(dotPad.BrailleDisplayDriver.name)
125+
126+
registrar.addBleDevices(dotPad.BrailleDisplayDriver._isBleDotPad)
127+
128+
matching_device = bdDetect.DeviceMatch(
129+
type=bdDetect.ProtocolType.BLE,
130+
id="DotPad320",
131+
port="AA:BB:CC:DD:EE:FF",
132+
deviceInfo={"name": "DotPad320", "address": "AA:BB:CC:DD:EE:FF"},
133+
)
134+
135+
non_matching_device = bdDetect.DeviceMatch(
136+
type=bdDetect.ProtocolType.BLE,
137+
id="SomeOtherDevice",
138+
port="11:22:33:44:55:66",
139+
deviceInfo={"name": "SomeOtherDevice", "address": "11:22:33:44:55:66"},
140+
)
141+
142+
match_func = registrar._getDriverDict().get(bdDetect.CommunicationType.BLE)
143+
144+
self.assertTrue(match_func(matching_device))
145+
self.assertFalse(match_func(non_matching_device))

0 commit comments

Comments
 (0)