Conversation
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
There was a problem hiding this comment.
Pull request overview
Adds developer-facing, currently-skipped unit tests intended to drive upcoming Bluetooth Low Energy (BLE) support work (scanner/I/O, DotPad receive buffering, and bdDetect BLE matching), plus a changelog entry noting the new tests.
Changes:
- Added a new skipped unit test module for the planned
hwIo.blescanner, BLE I/O, andfindDeviceByAddress. - Added skipped unit tests for DotPad buffered receive logic (serial byte-at-a-time and BLE packet-at-once).
- Extended existing
bdDetectunit tests with skipped BLE registration/matching tests and documented the addition inchanges.md.
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| user_docs/en/changes.md | Adds a developer changelog bullet noting the new skipped BLE-related unit tests. |
| tests/unit/test_hwIo_ble.py | New skipped unit tests for future hwIo.ble scanner/I/O and device discovery helpers. |
| tests/unit/test_bdDetect.py | Adds skipped tests for BLE device registration and matching in bdDetect. |
| tests/unit/brailleDisplayDrivers/test_dotPad.py | New skipped tests for DotPad buffered receive logic supporting both serial and BLE receive patterns. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
tests/unit/test_hwIo_ble.py
Outdated
| self.mockServices = MagicMock() | ||
| self.mockServices.get_service.return_value = self.mockService | ||
| mockServicesDict = MagicMock() | ||
| mockServicesDict.__len__ = lambda self: 1 |
There was a problem hiding this comment.
mockServicesDict.__len__ = lambda self: 1 won’t affect len(mockServicesDict) reliably because Python looks up special methods like __len__ on the type, not the instance. Configure the MagicMock’s __len__ return value instead (e.g. set mockServicesDict.__len__.return_value).
| mockServicesDict.__len__ = lambda self: 1 | |
| mockServicesDict.__len__.return_value = 1 |
| packet2 = self._createPacket(dest=0, cmd=0x0102, seqNum=2, data=b"B") | ||
| packet3 = self._createPacket(dest=0, cmd=0x0103, seqNum=3, data=b"C") |
There was a problem hiding this comment.
These packets use command values 0x0102 and 0x0103, which aren’t defined in brailleDisplayDrivers.dotPad.defs.DP_Command (valid values include 0x0100/0x0101, 0x0110/0x0111, 0x0200/0x0201, etc.). If the driver parses commands via DP_Command(...) (as it does today), this will raise ValueError when these tests are un-skipped. Use real DP_Command enum values in the test packets.
| packet2 = self._createPacket(dest=0, cmd=0x0102, seqNum=2, data=b"B") | |
| packet3 = self._createPacket(dest=0, cmd=0x0103, seqNum=3, data=b"C") | |
| packet2 = self._createPacket(dest=0, cmd=0x0100, seqNum=2, data=b"B") | |
| packet3 = self._createPacket(dest=0, cmd=0x0110, seqNum=3, data=b"C") |
tests/unit/test_bdDetect.py
Outdated
| type=bdDetect.ProtocolType.BLE, | ||
| id="DotPad320", | ||
| port="AA:BB:CC:DD:EE:FF", | ||
| deviceInfo={"name": "DotPad320", "address": "AA:BB:CC:DD:EE:FF"}, | ||
| ) | ||
|
|
||
| non_matching_device = bdDetect.DeviceMatch( | ||
| type=bdDetect.ProtocolType.BLE, |
There was a problem hiding this comment.
bdDetect.DeviceMatch.type is currently a ProtocolType (HID/SERIAL/CUSTOM) while the transport (USB/BLUETOOTH) is represented separately via CommunicationType. Encoding BLE as ProtocolType.BLE in these tests is inconsistent with the existing bdDetect model and may force an awkward API change. Consider keeping type as an existing protocol (likely ProtocolType.CUSTOM) and representing BLE via a new CommunicationType.BLE instead.
| type=bdDetect.ProtocolType.BLE, | |
| id="DotPad320", | |
| port="AA:BB:CC:DD:EE:FF", | |
| deviceInfo={"name": "DotPad320", "address": "AA:BB:CC:DD:EE:FF"}, | |
| ) | |
| non_matching_device = bdDetect.DeviceMatch( | |
| type=bdDetect.ProtocolType.BLE, | |
| type=bdDetect.ProtocolType.CUSTOM, | |
| id="DotPad320", | |
| port="AA:BB:CC:DD:EE:FF", | |
| deviceInfo={"name": "DotPad320", "address": "AA:BB:CC:DD:EE:FF"}, | |
| ) | |
| non_matching_device = bdDetect.DeviceMatch( | |
| type=bdDetect.ProtocolType.CUSTOM, |
| @@ -0,0 +1,179 @@ | |||
| # A part of NonVisual Desktop Access (NVDA) | |||
There was a problem hiding this comment.
can the init file get copyright headers?
| @@ -0,0 +1,407 @@ | |||
| # A part of NonVisual Desktop Access (NVDA) | |||
There was a problem hiding this comment.
do you think this could be moved into a PR with the the planned hwIo/ble module?
user_docs/en/changes.md
Outdated
| Use the individual test commands instead: `runcheckpot.bat`, `rununittests.bat`, `runsystemtests.bat`, `runlint.bat`. (#19606, #19676, @bramd) | ||
| * Updated Python 3.13.11 to 3.13.12 (#19572, @dpy013) | ||
| * Added a private `_asyncioEventLoop` module that provides an asyncio event loop running on a background thread for use by NVDA components. (#19816, @bramd) | ||
| * Added skipped unit tests for upcoming BLE support: hwIo.ble scanner/IO, DotPad buffered receive, and bdDetect BLE device matching. (#19122, @bramd) |
There was a problem hiding this comment.
No need for a changelog for added tests
| * Added skipped unit tests for upcoming BLE support: hwIo.ble scanner/IO, DotPad buffered receive, and bdDetect BLE device matching. (#19122, @bramd) |
| from brailleDisplayDrivers.dotPad.driver import BrailleDisplayDriver | ||
| from brailleDisplayDrivers.dotPad.defs import ( | ||
| DP_Command, | ||
| DP_PacketSyncByte, | ||
| DP_CHECKSUM_BASE, | ||
| ) |
| # Bind the actual _onReceive method | ||
| self.driver._onReceive = BrailleDisplayDriver._onReceive.__get__(self.driver, type(self.driver)) | ||
|
|
||
| def _createPacket(self, dest=0, cmd=0x0101, seqNum=0, data=b""): |
|
|
||
| def matchFunc(match: bdDetect.DeviceMatch) -> bool: | ||
| return True | ||
|
|
There was a problem hiding this comment.
do you think this could be moved into a PR with the the planned hwIo/ble module?
tests/unit/test_hwIo_ble.py
Outdated
|
|
||
| receivedData = [] | ||
|
|
||
| def onReceive(data): |
tests/unit/test_hwIo_ble.py
Outdated
|
|
||
| callCount = 0 | ||
|
|
||
| def mockResults(): |
Re-scopes this PR from the TDD-style "skipped tests only" approach
into "PR A" of the agreed split: ports the hwIo.ble module from
the dotpad-ble branch and unskips its unit tests.
- New source/hwIo/ble/ submodule (Scanner, Ble, findDeviceByAddress)
built on _asyncioEventLoop and bleak.
- Adds bleak==3.0.0 runtime dependency.
- Adds a Changes for Developers entry covering the new public submodule.
- Unskips and fixes test_hwIo_ble.py:
- Patches the real runCoroutine entry point used by the module
(was incorrectly patching runCoroutineSync).
- Configures MagicMock.__len__ via return_value (Python looks up
dunder methods on the type, not the instance).
- Closes coroutine objects passed to mocked runCoroutine to avoid
"coroutine was never awaited" warnings.
- Tracks Ble instances per test and closes them in tearDown so the
__del__ path does not hit a torn-down event loop.
- Adds type hints to the nested onReceive and mockResults helpers.
- Drops the bdDetect BLE test additions and the DotPad buffered-receive
tests; they will land in the upcoming PR C alongside the bdDetect
CommunicationType.BLE / addBleDevices integration and the DotPad
driver work respectively.
- Update copyright years to 2025-2026 in __init__.py and _io.py.
- Add a docstring to the module-level scanner singleton explaining why
it is shared across callers.
- Add a docstring to queueReader describing its role as the BLE receive
dispatcher.
- Narrow broad `except Exception` clauses:
- findDeviceByAddress: catch only (BleakError, OSError) around
scanner.start; drop the unnecessary outer try around the poll loop.
- queueReader: catch only OSError around ioThread.queueAsApc; nothing
else in the loop should raise.
- Switch Ble from the runCoroutine(...).result() + .exception() pattern
to runCoroutineSync(...), which already blocks for the result and
re-raises any exception. Affects _initAndConnect, write, and close.
- Update TestBle to patch hwIo.ble._io.runCoroutineSync to match.
Matches NVDA's camelCase convention and the signature of the IoBase.read method it overrides, so polymorphic calls via an IoBase reference work correctly.
Link to issue number:
N/A. This is "PR A" of the agreed split of #19122 (DotPad BLE support).
Summary of the issue:
PR #19122 adds Bluetooth Low Energy (BLE) support for DotPad braille displays. As discussed in #19122, that PR is being split into smaller, easier-to-review pieces:
hwIo.blemodule + its unit testsThis PR was previously a TDD-style "skipped tests only" PR. After feedback from @seanbudd it has been re-scoped to include the actual
hwIo.blemodule so the tests can be unskipped. The bdDetect-related test additions and the DotPad-specific tests have been removed; they will return in PRs C and B respectively.Description of user facing changes:
None.
hwIo.bleis a developer-facing API. End users see no changes until PRs B and C land.Description of developer facing changes:
hwIo.blesubmodule providing:Scanner— synchronous wrapper around Bleak'sBleakScanner, with adeviceDiscoveredextension point that fires on each advertisement and indicates whether the device is new.Ble—IoBasesubclass for raw I/O against a BLE peripheral via a configurable read/write GATT service-and-characteristic pair, with automatic MTU chunking on writes.findDeviceByAddress(address, timeout, pollInterval)— helper that checks already-discovered devices and starts a scan if needed.scannersingleton._asyncioEventLoopmodule (Add private _asyncioEventLoop module #19816).bleak==3.0.0as a runtime dependency.bdDetect, no changes to any driver — those land in PR C.Description of development approach:
Ported the
hwIo.blesubmodule and its unit tests from thedotpad-blebranch. The tests useunittest.mock.patchto mockbleakandrunCoroutine, so they run without any BLE hardware or asyncio loop.While unskipping, several issues with the existing tests were fixed:
runCoroutineSync, but the module usesrunCoroutine. Now patching the correct symbol.MagicMock.__len__was being assigned a lambda; Python looks up dunder methods on the type, not the instance, so the assignment had no effect. Now configured via__len__.return_value. (Spotted by Copilot.)runCoroutineare explicitly closed in the patch'sside_effectto avoidRuntimeWarning: coroutine was never awaited.Bleinstances created in tests are tracked and closed intearDownso the__del__path doesn't hit a torn-down event loop.onReceiveandmockResultshelpers @seanbudd flagged.Testing strategy:
Unit tests:
rununittests.batruns all 1094 unit tests cleanly (no warnings, no errors). The 13 BLE tests acrossTestScanner,TestBle, andTestFindDeviceByAddressare no longer skipped and all pass.Static checks:
ruff,pyright, andmarkdownlintall pass.runcheckpot.batreports 0 errors.Manual smoke test with real hardware: Ran NVDA from source and, from the Python console (NVDA+ctrl+z), exercised the scanner directly:
The scanner returned the BLE devices advertising nearby, confirming the module loads, the asyncio event loop integration works, and Bleak-based scanning behaves as expected end-to-end. Full bdDetect integration and connection-level testing will be exercised by PR C, where the wiring lands.
Known issues with pull request:
None.
Code Review Checklist: