Repository navigation
Mock Bluetooth in-process
BLESwift 3.1.0 ships the virtual radio as its own product, BLESwiftVirtualRadio. It builds for every platform, and VirtualRadio.install(_:) routes every Central() and PeripheralHost() to it, with no TCP connection and no provider. Virtual devices can now inject faults, set per-operation delays, and run on a ManualClock that a test advances by hand. The same clock drives Central's timeouts. One change is source-breaking, for a narrow set of callers: VirtualRadio.register(_:advertising:) is now async. Code that already awaits it is unaffected.
Added
BLESwiftVirtualRadio, a new product that builds for every platform: the in-process
virtual radio (VirtualRadio,VirtualDevice,VirtualCentralBackend,
VirtualPeripheralManagerBackend, …) for mocking Bluetooth in an app or test process,
with a "Mocking Bluetooth in-process" DocC article. (#65)VirtualRadio.install(_:),uninstall()andinstalled, which route every
Central()/PeripheralHost()to a radio throughBackendRegistry, with no TCP and no
provider. (#65)VirtualDeviceDescriptor.maximumWriteWithoutResponseLength, a per-device one-packet
length (20...512, default 182) that a connectedCentralreports for.withoutResponse,
past which the radio drops a write without response and truncates a notification.
VirtualRadio.register(_:advertising:)traps on a value outside that range. (#66)- Fault injection on
VirtualDeviceHandle:failConnections(with:times:)and
clearConnectionFailures(),disconnect(_:error:)for a peripheral-initiated drop, and
setATTError(_:for:operations:)withVirtualGATTOperationsto answer reads, writes
with response, and subscribes with a chosenATTError; plus
VirtualRadio.peripheralDisconnectedError(CBErrorDomaincode 7). (#67) Configuration.clock, theClock<Duration>everyCentraltimer sleeps on — operation
and connect timeouts, the scan timeout and loss timer, the reconnect backoff — defaulting
toContinuousClock(); andManualClockinBLESwiftTestSupport, a clock a test advances
by hand. (#68)- Per-device delays on the virtual radio:
VirtualRadio(clock:),VirtualLatency(connect,
discovery, read, write, subscribe),VirtualDeviceDescriptor.latency, and
VirtualDeviceHandle.setLatency(_:). Delays default to zero, so existing behavior is
unchanged; on aManualClocka 60-second connect delay against a 30-second
connect(timeout:)is driven to its deadline without waiting. (#68) PeripheralIdentifier.cachedName: the peripheral's name exactly as CoreBluetooth reported
it,nilwhen it has none.nameis unchanged — it still returns"No Name"for an
unnamed peripheral — but it now comes fromcachedName, so an unnamed peripheral can be
told apart from one actually named "No Name". (#63)BackendRegistry.setFactories(central:peripheralManager:), which replaces both factories
under one lock;VirtualRadio.install(_:)/uninstall()andSimulatorLink.install/
uninstall()go through it. (#70)
Changed
- The virtual radio types moved out of
BLESwiftProviderintoBLESwiftVirtualRadio
and are no longer macOS-only.BLESwiftProviderre-exports the new module, so existing
import BLESwiftProvidercode compiles unchanged. (#65) - The virtual radio's logger label is
BLESwiftVirtualRadio.VirtualRadio; it was
BLESwiftProvider.VirtualRadioin 3.0.0, on macOS. TheNSErrordomain is unchanged. (#71) - Source-breaking for synchronous in-actor callers only:
VirtualRadio.register(_:advertising:)isasync. Callers outside the actor already
awaitit and are unaffected; a synchronous method in anextension VirtualRadiothat
callsregistermust becomeasync. (#73)
Fixed
- On the virtual radio, a connect issued while a cancel was in progress could report its
didFailToConnect(code 11) before or after the cancel'sdidDisconnect, depending on
thread scheduling. The failure is now delivered behind the cancel'sdidDisconnect,
always. (#89) - A virtual device's
subscriptionChanged(isSubscribed: true)report still in flight could
be overtaken by the unsubscribe report of a forced disconnect, removal, re-registration,
setServices, or detach that dropped the same subscription, so a handler that awaits
inside the report finished the two out of order. Reports for a device now reach its
handler one at a time, in the order the radio applied the changes, with each
didDisconnectbehind the unsubscribes it follows. The entry points still return once
their reports are delivered — except when called from inside a report (a handler's
subscriptionChanged, or a task it spawned there), where they return first and the
reports follow once the handler returns, since waiting would deadlock. A report that
never returns now blocks every later report for its device and every call waiting on
them — other centrals'setNotifyValue, a disconnect that drops a subscription,
remove(),setServices(_:), a re-register, a detach — so handlers must return
promptly. A teardown with no report in flight still delivers itsdidDisconnectin the
same actor turn. (#76) - Re-registering an identifier that still had connections left them, and their
subscriptions, attached to the new registration: the new handler inherited subscribers it
was never told about, the old one never heard the unsubscribes, and the centrals stayed
connected without hearing a disconnect. The old registration is now retired the way a
removal retires one — the old handler hears each unsubscribe, then each connected central
hearsdidDisconnectwithdeviceRemovedError— while the identifier never leaves the
radio.VirtualDeviceHandle.remove()now uses that same order too: it used to deliver the
didDisconnects before telling the handler about the dropped subscriptions, so with a
handler that awaits insubscriptionChanged, a central now hears the disconnect only once
the handler has returned. (#73) VirtualRadio.install(_:)andSimulatorLink.installset the two registry factories and
their owninstalled/endpointunder separate locks, so aCentral()and a
PeripheralHost()built on another thread in between could come from different installs,
andinstalled/endpointcould lag the registry. Both now change together. (#70)ManualClock.sleep(until:)returned normally for a deadline already reached on a task
that had been cancelled, whereContinuousClockthrowsCancellationError, so a test on
ManualClockcould not exercise a sleep's cancellation path for a zero delay.
Cancellation is now checked first, matchingContinuousClock. (#81)ScanFilter'snamePrefixandnameExactmatched an unnamed peripheral against the
"No Name"display fallback, soScanFilter(namePrefix: "No")let unnamed peripherals
through. An unnamed peripheral now fails any name filter. (#63)