Skip to content

3.1.0

Latest

Choose a tag to compare

@kylebrowning kylebrowning released this 01 Nov 17:08
b00ece9

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() and installed, which route every
    Central() / PeripheralHost() to a radio through BackendRegistry, with no TCP and no
    provider. (#65)
  • VirtualDeviceDescriptor.maximumWriteWithoutResponseLength, a per-device one-packet
    length (20...512, default 182) that a connected Central reports 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:) with VirtualGATTOperations to answer reads, writes
    with response, and subscribes with a chosen ATTError; plus
    VirtualRadio.peripheralDisconnectedError (CBErrorDomain code 7). (#67)
  • Configuration.clock, the Clock<Duration> every Central timer sleeps on — operation
    and connect timeouts, the scan timeout and loss timer, the reconnect backoff — defaulting
    to ContinuousClock(); and ManualClock in BLESwiftTestSupport, 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 a ManualClock a 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, nil when it has none. name is unchanged — it still returns "No Name" for an
    unnamed peripheral — but it now comes from cachedName, 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() and SimulatorLink.install /
    uninstall() go through it. (#70)

Changed

  • The virtual radio types moved out of BLESwiftProvider into BLESwiftVirtualRadio
    and are no longer macOS-only. BLESwiftProvider re-exports the new module, so existing
    import BLESwiftProvider code compiles unchanged. (#65)
  • The virtual radio's logger label is BLESwiftVirtualRadio.VirtualRadio; it was
    BLESwiftProvider.VirtualRadio in 3.0.0, on macOS. The NSError domain is unchanged. (#71)
  • Source-breaking for synchronous in-actor callers only:
    VirtualRadio.register(_:advertising:) is async. Callers outside the actor already
    await it and are unaffected; a synchronous method in an extension VirtualRadio that
    calls register must become async. (#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's didDisconnect, depending on
    thread scheduling. The failure is now delivered behind the cancel's didDisconnect,
    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
    didDisconnect behind 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 its didDisconnect in 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
    hears didDisconnect with deviceRemovedError — 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 in subscriptionChanged, a central now hears the disconnect only once
    the handler has returned. (#73)
  • VirtualRadio.install(_:) and SimulatorLink.install set the two registry factories and
    their own installed / endpoint under separate locks, so a Central() and a
    PeripheralHost() built on another thread in between could come from different installs,
    and installed / endpoint could 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, where ContinuousClock throws CancellationError, so a test on
    ManualClock could not exercise a sleep's cancellation path for a zero delay.
    Cancellation is now checked first, matching ContinuousClock. (#81)
  • ScanFilter's namePrefix and nameExact matched an unnamed peripheral against the
    "No Name" display fallback, so ScanFilter(namePrefix: "No") let unnamed peripherals
    through. An unnamed peripheral now fails any name filter. (#63)