|
| 1 | +@preconcurrency import CoreBluetoothMock |
| 2 | +import Foundation |
| 3 | +import Testing |
| 4 | + |
| 5 | +@testable import AsyncCoreBluetooth |
| 6 | + |
| 7 | +@Suite(.serialized) struct ReadRSSITests { |
| 8 | + let mockPeripheralDelegate = MockPeripheral.Delegate() |
| 9 | + let rssiDeviation = 15 |
| 10 | + |
| 11 | + func setup(proximity: CBMProximity, connect: Bool = true) async throws -> (Peripheral, CBMPeripheralSpec, CentralManager) { |
| 12 | + CBMCentralManagerMock.simulateInitialState(.poweredOff) |
| 13 | + |
| 14 | + // Create a mutable mockPeripheral |
| 15 | + let mockPeripheral = MockPeripheral.makeDevice( |
| 16 | + delegate: mockPeripheralDelegate, |
| 17 | + isKnown: true, |
| 18 | + proximity: proximity |
| 19 | + ) |
| 20 | + |
| 21 | + CBMCentralManagerMock.simulatePeripherals([mockPeripheral]) |
| 22 | + CBMCentralManagerMock.simulateInitialState(.poweredOn) |
| 23 | + |
| 24 | + let centralManager = CentralManager(forceMock: true) |
| 25 | + _ = await centralManager.start().first(where: { $0 == .poweredOn }) |
| 26 | + |
| 27 | + let peripheral = await centralManager.retrievePeripherals(withIdentifiers: [ |
| 28 | + mockPeripheral.identifier |
| 29 | + ])[0] |
| 30 | + if connect { |
| 31 | + _ = await centralManager.connect(peripheral).first(where: { $0 == .connected }) |
| 32 | + } |
| 33 | + |
| 34 | + return (peripheral, mockPeripheral, centralManager) |
| 35 | + } |
| 36 | + |
| 37 | + @Test("Read RSSI returns near proximity value") |
| 38 | + func test_readRSSI_near() async throws { |
| 39 | + let (peripheral, _, _) = try await setup(proximity: .near) |
| 40 | + |
| 41 | + try await peripheral.readRSSI() |
| 42 | + guard let rssiValue = await peripheral.rssi.stream.first(where: { _ in true }) else { |
| 43 | + Issue.record("No RSSI value received") |
| 44 | + return |
| 45 | + } |
| 46 | + // Near proximity base RSSI is -40, but can vary by ±15 (rssiDeviation) |
| 47 | + #expect(rssiValue >= -40 - rssiDeviation && rssiValue <= -40 + rssiDeviation) |
| 48 | + } |
| 49 | + |
| 50 | + @Test("Read RSSI returns immediate proximity value") |
| 51 | + func test_readRSSI_immediate() async throws { |
| 52 | + let (peripheral, _, _) = try await setup(proximity: .immediate) |
| 53 | + |
| 54 | + try await peripheral.readRSSI() |
| 55 | + guard let rssiValue = await peripheral.rssi.stream.first(where: { _ in true }) else { |
| 56 | + Issue.record("No RSSI value received") |
| 57 | + return |
| 58 | + } |
| 59 | + // Immediate proximity base RSSI is -70, but can vary by ±15 (rssiDeviation) |
| 60 | + #expect(rssiValue >= -70 - rssiDeviation && rssiValue <= -70 + rssiDeviation) |
| 61 | + } |
| 62 | + |
| 63 | + @Test("Read RSSI returns far proximity value") |
| 64 | + func test_readRSSI_far() async throws { |
| 65 | + let (peripheral, _, _) = try await setup(proximity: .far) |
| 66 | + |
| 67 | + try await peripheral.readRSSI() |
| 68 | + guard let rssiValue = await peripheral.rssi.stream.first(where: { _ in true }) else { |
| 69 | + Issue.record("No RSSI value received") |
| 70 | + return |
| 71 | + } |
| 72 | + // Far proximity base RSSI is -100, but can vary by ±15 (rssiDeviation) |
| 73 | + #expect(rssiValue >= -100 - rssiDeviation && rssiValue <= -100 + rssiDeviation) |
| 74 | + } |
| 75 | + |
| 76 | + @Test("RSSI Observable is initially nil") |
| 77 | + func test_rssi_initially_nil() async throws { |
| 78 | + let (peripheral, _, _) = try await setup(proximity: .near) |
| 79 | + |
| 80 | + // Before reading RSSI, the observable should be nil |
| 81 | + let initialValue = await peripheral._rssi.current |
| 82 | + #expect(initialValue == nil) |
| 83 | + } |
| 84 | + |
| 85 | + @Test("Multiple sequential RSSI reads return consistent values for the same proximity") |
| 86 | + func test_sequential_rssi_reads_consistent() async throws { |
| 87 | + let (peripheral, _, _) = try await setup(proximity: .near) |
| 88 | + |
| 89 | + // Read RSSI multiple times with the same proximity |
| 90 | + let rssi1 = try await peripheral.readRSSI() |
| 91 | + let rssi2 = try await peripheral.readRSSI() |
| 92 | + let rssi3 = try await peripheral.readRSSI() |
| 93 | + |
| 94 | + // All values should be near proximity (-40 ± deviation) |
| 95 | + #expect(rssi1 >= -40 - rssiDeviation && rssi1 <= -40 + rssiDeviation) |
| 96 | + #expect(rssi2 >= -40 - rssiDeviation && rssi2 <= -40 + rssiDeviation) |
| 97 | + #expect(rssi3 >= -40 - rssiDeviation && rssi3 <= -40 + rssiDeviation) |
| 98 | + |
| 99 | + // The observable should have the latest value |
| 100 | + let currentValue = await peripheral._rssi.current |
| 101 | + #expect(currentValue == rssi3) |
| 102 | + } |
| 103 | + |
| 104 | + @Test("RSSI observable updates when proximity changes") |
| 105 | + func test_rssi_updates_with_proximity_changes() async throws { |
| 106 | + // Start with near proximity |
| 107 | + let (peripheral, mockPeripheral, _) = try await setup(proximity: .near) |
| 108 | + |
| 109 | + // Read initial RSSI |
| 110 | + _ = try await peripheral.readRSSI() |
| 111 | + let initialRssi = await peripheral._rssi.current |
| 112 | + #expect(initialRssi != nil) |
| 113 | + #expect(initialRssi! >= -40 - rssiDeviation && initialRssi! <= -40 + rssiDeviation) |
| 114 | + |
| 115 | + // Change proximity to far and read again |
| 116 | + mockPeripheral.simulateProximityChange(.far) |
| 117 | + _ = try await peripheral.readRSSI() |
| 118 | + |
| 119 | + // RSSI should update to far proximity value |
| 120 | + let farRssi = await peripheral._rssi.current |
| 121 | + #expect(farRssi != nil) |
| 122 | + #expect(farRssi! >= -100 - rssiDeviation && farRssi! <= -100 + rssiDeviation) |
| 123 | + } |
| 124 | + |
| 125 | + @Test("RSSI stream receives updates") |
| 126 | + func test_rssi_stream_receives_updates() async throws { |
| 127 | + let (peripheral, mockPeripheral, _) = try await setup(proximity: .near) |
| 128 | + |
| 129 | + // Get initial RSSI value |
| 130 | + _ = try await peripheral.readRSSI() |
| 131 | + let firstValue = await peripheral._rssi.current |
| 132 | + #expect(firstValue != nil) |
| 133 | + #expect(firstValue! >= -40 - rssiDeviation && firstValue! <= -40 + rssiDeviation) |
| 134 | + |
| 135 | + // Change to immediate proximity and read |
| 136 | + mockPeripheral.simulateProximityChange(.immediate) |
| 137 | + _ = try await peripheral.readRSSI() |
| 138 | + let secondValue = await peripheral._rssi.current |
| 139 | + #expect(secondValue != nil) |
| 140 | + #expect(secondValue! >= -70 - rssiDeviation && secondValue! <= -70 + rssiDeviation) |
| 141 | + |
| 142 | + // Change to far proximity and read |
| 143 | + mockPeripheral.simulateProximityChange(.far) |
| 144 | + _ = try await peripheral.readRSSI() |
| 145 | + let thirdValue = await peripheral._rssi.current |
| 146 | + #expect(thirdValue != nil) |
| 147 | + #expect(thirdValue! >= -100 - rssiDeviation && thirdValue! <= -100 + rssiDeviation) |
| 148 | + } |
| 149 | + |
| 150 | + @Test("RSSI values can be collected with AsyncStream") |
| 151 | + func test_rssi_values_collected_with_stream() async throws { |
| 152 | + let (peripheral, mockPeripheral, _) = try await setup(proximity: .near) |
| 153 | + |
| 154 | + // Create a task to collect RSSI values from the stream |
| 155 | + let streamTask = Task { |
| 156 | + var values = [Int]() |
| 157 | + var count = 0 |
| 158 | + for await value in await peripheral.rssi.stream { |
| 159 | + values.append(value) |
| 160 | + count += 1 |
| 161 | + if count >= 3 { |
| 162 | + // We'll collect 3 values |
| 163 | + break |
| 164 | + } |
| 165 | + } |
| 166 | + return values |
| 167 | + } |
| 168 | + |
| 169 | + // Now perform real RSSI reads with different proximities to generate values |
| 170 | + _ = try await peripheral.readRSSI() // Read with near proximity |
| 171 | + |
| 172 | + // Change to immediate proximity and read again |
| 173 | + mockPeripheral.simulateProximityChange(.immediate) |
| 174 | + _ = try await peripheral.readRSSI() |
| 175 | + |
| 176 | + // Change to far proximity and read again |
| 177 | + mockPeripheral.simulateProximityChange(.far) |
| 178 | + _ = try await peripheral.readRSSI() |
| 179 | + |
| 180 | + // Get the collected values from our stream task |
| 181 | + let collectedValues = await streamTask.value |
| 182 | + |
| 183 | + // Verify the stream collected all three values |
| 184 | + #expect(collectedValues.count == 3) |
| 185 | + |
| 186 | + // Verify values match the expected proximity ranges |
| 187 | + #expect(collectedValues[0] >= -40 - rssiDeviation && collectedValues[0] <= -40 + rssiDeviation) // Near |
| 188 | + #expect(collectedValues[1] >= -70 - rssiDeviation && collectedValues[1] <= -70 + rssiDeviation) // Immediate |
| 189 | + #expect(collectedValues[2] >= -100 - rssiDeviation && collectedValues[2] <= -100 + rssiDeviation) // Far |
| 190 | + } |
| 191 | + |
| 192 | + // @Test("Disconnected peripheral throws error on RSSI read") |
| 193 | + // func test_disconnected_peripheral_throws_error() async throws { |
| 194 | + // let (peripheral, _, centralManager) = try await setup(proximity: .near) |
| 195 | + // await centralManager.cancelPeripheralConnection(peripheral) |
| 196 | + |
| 197 | + // // Attempt to read RSSI, should throw an error |
| 198 | + // do { |
| 199 | + // _ = try await peripheral.readRSSI() |
| 200 | + // Issue.record("Expected an error when reading RSSI from disconnected peripheral") |
| 201 | + // } catch { |
| 202 | + // // Expected to throw an error |
| 203 | + // #expect(error is PeripheralConnectionError) |
| 204 | + // } |
| 205 | + // } |
| 206 | +} |
0 commit comments