Description
Problem
The CoreBluetoothMock library suffers from periodic crashes due to a number of errors related to concurrent access. While most internal CBMCentralManagerMock
instances are protected from concurrent access with DispatchQueue(label: "Mutex")
, neither the mutable advertisement timers in CBMCentralMock
nor the list of peripherals in CBMCentralManagerNative
are thread-safe.
Example
Here is a basic unit test that sets up a mock peripheral and simulates it
import XCTest
import CoreBluetoothMock
@testable import BluetoothMockExample
final class BluetoothMockExampleTests: XCTestCase {
let device = CBMPeripheralSpec
.simulatePeripheral(proximity: CBMProximity.near)
.advertising(
advertisementData: [
CBMAdvertisementDataLocalNameKey: "Test_Device",
CBMAdvertisementDataServiceUUIDsKey: CBMUUID(),
CBMAdvertisementDataIsConnectable: true as NSNumber
]
)
.connectable(
name: "Test_Device",
services: [
CBMServiceMock(
type: CBMUUID(),
primary: true,
characteristics: [CBMCharacteristicMock(type: CBMUUID(), properties: [.read])]
)
],
delegate: nil
)
.build()
override func tearDown() async throws {
CBMCentralManagerMock.tearDownSimulation()
}
func testSimulation() throws {
CBMCentralManagerMock.simulatePeripherals([device])
}
}
When run a single time, this test passes ~99.5% of the time. When tested repeatedly (1000x), however, the tests begin crashing with various errors including:
-
"-[__NSCFNumber countByEnumeratingWithState:objects:count:]: unrecognized selector sent to instance ...
-
malloc: double free for ptr ...
-
EXC_BAD_ACCESS (code=1, address=...)
RepeatedTestCrash-CoreBluetoothMock.mp4
Recommended Solution
Instead of wrapping every access to the advertisement timers or native peripherals in a sync or async block with a local queue, I recommend implementing a thread-safe wrapper similar to what @wcharysz-allegion has done in his fork - see main...wcharysz-allegion:IOS-CoreBluetooth-Mock:main