From f353079f6f3c62debadf3f27d0af90d175455a1d Mon Sep 17 00:00:00 2001 From: Edwin Wu Date: Fri, 31 Oct 2025 12:14:36 -0700 Subject: [PATCH] [NC Apple coverage] Add more comprehensive tests for BLE medium connections. PiperOrigin-RevId: 826582061 --- .../Mediums/BLE/Tests/GNCFakeBLEMedium.h | 3 + .../Mediums/BLE/Tests/GNCFakeBLEMedium.m | 42 ++-- .../platform/implementation/apple/Tests/BUILD | 4 + .../apple/Tests/ble_medium_test.mm | 201 +++++++++++++++++- .../implementation/apple/ble_medium.h | 5 +- .../implementation/apple/ble_medium.mm | 26 +-- 6 files changed, 243 insertions(+), 38 deletions(-) diff --git a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeBLEMedium.h b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeBLEMedium.h index a8af868f..7eeec84e 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeBLEMedium.h +++ b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeBLEMedium.h @@ -45,6 +45,9 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, nullable) GNCAdvertisementFoundHandler advertisementFoundHandler; +/** When NO, the completion for opening an L2CAP channel will not be invoked. Default is YES. */ +@property(nonatomic) BOOL openL2CAPChannelShouldComplete; + @end NS_ASSUME_NONNULL_END diff --git a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeBLEMedium.m b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeBLEMedium.m index 1a38817a..f2d81269 100644 --- a/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeBLEMedium.m +++ b/internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeBLEMedium.m @@ -28,6 +28,14 @@ NS_ASSUME_NONNULL_BEGIN @implementation GNCFakeBLEMedium +- (instancetype)init { + self = [super init]; + if (self) { + _openL2CAPChannelShouldComplete = YES; + } + return self; +} + - (void)startAdvertisingData:(NSDictionary *)advertisementData completionHandler:(nullable GNCStartAdvertisingCompletionHandler)completionHandler { if (completionHandler) { @@ -35,7 +43,8 @@ NS_ASSUME_NONNULL_BEGIN } } -- (void)stopAdvertisingWithCompletionHandler:(nullable GNCStopAdvertisingCompletionHandler)completionHandler { +- (void)stopAdvertisingWithCompletionHandler: + (nullable GNCStopAdvertisingCompletionHandler)completionHandler { if (completionHandler) { completionHandler(self.stopAdvertisingError); } @@ -51,15 +60,17 @@ NS_ASSUME_NONNULL_BEGIN } - (void)startScanningForMultipleServices:(NSArray *)serviceUUIDs - advertisementFoundHandler:(GNCAdvertisementFoundHandler)advertisementFoundHandler - completionHandler:(nullable GNCStartScanningCompletionHandler)completionHandler { + advertisementFoundHandler:(GNCAdvertisementFoundHandler)advertisementFoundHandler + completionHandler: + (nullable GNCStartScanningCompletionHandler)completionHandler { self.advertisementFoundHandler = advertisementFoundHandler; if (completionHandler) { completionHandler(self.startScanningError); } } -- (void)stopScanningWithCompletionHandler:(nullable GNCStopScanningCompletionHandler)completionHandler { +- (void)stopScanningWithCompletionHandler: + (nullable GNCStopScanningCompletionHandler)completionHandler { if (completionHandler) { completionHandler(self.stopScanningError); } @@ -71,15 +82,17 @@ NS_ASSUME_NONNULL_BEGIN } } -- (void)startGATTServerWithCompletionHandler:(nullable GNCGATTServerCompletionHandler)completionHandler { +- (void)startGATTServerWithCompletionHandler: + (nullable GNCGATTServerCompletionHandler)completionHandler { if (completionHandler) { completionHandler(self.fakeGATTServer, self.startGATTServerError); } } - (void)connectToGATTServerForPeripheral:(id)peripheral - disconnectionHandler:(nullable GNCGATTDisconnectionHandler)disconnectionHandler - completionHandler:(nullable GNCGATTConnectionCompletionHandler)completionHandler { + disconnectionHandler:(nullable GNCGATTDisconnectionHandler)disconnectionHandler + completionHandler: + (nullable GNCGATTConnectionCompletionHandler)completionHandler { self.lastConnectedPeripheral = peripheral; self.lastDisconnectionHandler = disconnectionHandler; if (completionHandler) { @@ -87,8 +100,8 @@ NS_ASSUME_NONNULL_BEGIN if (!self.fakeGATTClient) { // Create a default fake client if one isn't provided. self.fakeGATTClient = [[GNCBLEGATTClient alloc] initWithPeripheral:peripheral - requestDisconnectionHandler:^(id p) { - // Do nothing in fake. + requestDisconnectionHandler:^(id p){ + // Do nothing in fake. }]; } completionHandler(self.fakeGATTClient, nil); @@ -100,10 +113,11 @@ NS_ASSUME_NONNULL_BEGIN - (void)openL2CAPServerWithPSMPublishedCompletionHandler: (GNCOpenL2CAPServerPSMPublishedCompletionHandler)psmPublishedCompletionHandler - channelOpenedCompletionHandler: - (GNCOpenL2CAPServerChannelOpendCompletionHandler)channelOpenedCompletionHandler - peripheralManager: - (nullable id)peripheralManager { + channelOpenedCompletionHandler: + (GNCOpenL2CAPServerChannelOpendCompletionHandler) + channelOpenedCompletionHandler + peripheralManager: + (nullable id)peripheralManager { if (psmPublishedCompletionHandler) { psmPublishedCompletionHandler(self.fakePSM, self.openL2CAPServerSocketError); } @@ -114,7 +128,7 @@ NS_ASSUME_NONNULL_BEGIN peripheral:(id)peripheral completionHandler:(nullable GNCOpenL2CAPStreamCompletionHandler)completionHandler { self.lastConnectedPeripheral = peripheral; - if (completionHandler) { + if (self.openL2CAPChannelShouldComplete && completionHandler) { completionHandler(self.fakeL2CAPStream, self.openL2CAPChannelError); } } diff --git a/internal/platform/implementation/apple/Tests/BUILD b/internal/platform/implementation/apple/Tests/BUILD index c62951bf..c39d81ed 100644 --- a/internal/platform/implementation/apple/Tests/BUILD +++ b/internal/platform/implementation/apple/Tests/BUILD @@ -62,6 +62,9 @@ objc_library( "//internal/platform/implementation/apple:network_utils", "//internal/platform/implementation/apple/Flags", "//internal/platform/implementation/apple/Mediums/BLE", + "//internal/platform/implementation/apple/Mediums/BLE/Sockets:Central", + "//internal/platform/implementation/apple/Mediums/BLE/Sockets:Peripheral", + "//internal/platform/implementation/apple/Mediums/BLE/Sockets:Shared", "//internal/platform/implementation/apple/Mediums/BLE/Tests:BLETestsLib", "//internal/platform/implementation/apple/Mediums/CoreLocation/CLLocationManager/Fake", "//internal/platform/implementation/apple/Mediums/Hotspot", @@ -70,6 +73,7 @@ objc_library( "//third_party/apple_frameworks:CommonCrypto", "//third_party/apple_frameworks:Foundation", "//third_party/apple_frameworks:XCTest", + "//third_party/objective_c/ocmock/v3:OCMock", "@com_google_absl//absl/time", "@com_google_googletest//:gtest", "@nlohmann_json//:json", diff --git a/internal/platform/implementation/apple/Tests/ble_medium_test.mm b/internal/platform/implementation/apple/Tests/ble_medium_test.mm index f6012f87..b7fca780 100644 --- a/internal/platform/implementation/apple/Tests/ble_medium_test.mm +++ b/internal/platform/implementation/apple/Tests/ble_medium_test.mm @@ -28,11 +28,33 @@ #import "internal/platform/implementation/apple/Mediums/BLE/GNCBLEL2CAPClient.h" #import "internal/platform/implementation/apple/Mediums/BLE/GNCBLEMedium.h" #import "internal/platform/implementation/apple/Mediums/BLE/GNCPeripheral.h" +#import "internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Central/GNSCentralManager.h" +#import "internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Central/GNSCentralPeerManager.h" +#import "internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Peripheral/GNSPeripheralManager.h" +#import "internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Peripheral/GNSPeripheralServiceManager.h" +#import "internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Shared/GNSSocket.h" #import "internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeBLEGATTServer.h" #import "internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakeBLEMedium.h" #import "internal/platform/implementation/apple/Mediums/BLE/Tests/GNCFakePeripheral.h" #include "internal/platform/implementation/apple/ble_utils.h" #include "internal/platform/implementation/ble.h" +#import "third_party/objective_c/ocmock/v3/Source/OCMock/OCMock.h" + +namespace nearby { +namespace apple { + +class BleMediumPeer { + public: + static void SetSocketCentralManager(BleMedium *ble_medium, GNSCentralManager *manager) { + ble_medium->socketCentralManager_ = manager; + } + static void SetSocketPeripheralManager(BleMedium *ble_medium, GNSPeripheralManager *manager) { + ble_medium->socketPeripheralManager_ = manager; + } +}; + +} // namespace apple +} // namespace nearby // TODO(b/293336684): Add tests for Weave sockets, AdvertisementFoundHandler, and more edge cases. @@ -53,6 +75,10 @@ static const char *const kTestServiceID = "TestServiceID"; _medium = std::make_unique((GNCBLEMedium *)_fakeGNCBLEMedium); } +- (void)tearDown { + [super tearDown]; +} + #pragma mark - Advertising Tests - (void)testStartAdvertising_Success { @@ -308,17 +334,186 @@ static const char *const kTestServiceID = "TestServiceID"; XCTAssertEqual(l2cap_socket.get(), nullptr); } +- (void)testConnectOverL2cap_OpenChannelError { + GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init]; + NSDictionary *serviceData = + @{[CBUUID UUIDWithString:kTestServiceUUIDString] : [NSData dataWithBytes:"test" length:4]}; + XCTestExpectation *expectation = + [self expectationWithDescription:@"Advertisement found callback should be called."]; + nearby::api::ble::BleMedium::ScanCallback callback = { + .advertisement_found_cb = std::function( + [expectation](nearby::api::ble::BlePeripheral::UniqueId peripheral_id, + const nearby::api::ble::BleAdvertisementData &advertisement) { + [expectation fulfill]; + })}; + _medium->StartScanning(nearby::Uuid(0, 0), nearby::api::ble::TxPowerLevel::kUltraLow, + std::move(callback)); + if (_fakeGNCBLEMedium.advertisementFoundHandler) { + _fakeGNCBLEMedium.advertisementFoundHandler(fakePeripheral, serviceData); + } + [self waitForExpectations:@[ expectation ] timeout:1.0]; + _fakeGNCBLEMedium.openL2CAPChannelError = [NSError errorWithDomain:@"test" code:0 userInfo:nil]; + + auto l2cap_socket = + _medium->ConnectOverL2cap(123, kTestServiceID, nearby::api::ble::TxPowerLevel::kUltraLow, + fakePeripheral.identifier.hash, nullptr); + + XCTAssertEqual(l2cap_socket.get(), nullptr); +} + +- (void)testConnectOverL2cap_Timeout { + GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init]; + NSDictionary *serviceData = + @{[CBUUID UUIDWithString:kTestServiceUUIDString] : [NSData dataWithBytes:"test" length:4]}; + XCTestExpectation *expectation = + [self expectationWithDescription:@"Advertisement found callback should be called."]; + nearby::api::ble::BleMedium::ScanCallback callback = { + .advertisement_found_cb = std::function( + [expectation](nearby::api::ble::BlePeripheral::UniqueId peripheral_id, + const nearby::api::ble::BleAdvertisementData &advertisement) { + [expectation fulfill]; + })}; + _medium->StartScanning(nearby::Uuid(0, 0), nearby::api::ble::TxPowerLevel::kUltraLow, + std::move(callback)); + if (_fakeGNCBLEMedium.advertisementFoundHandler) { + _fakeGNCBLEMedium.advertisementFoundHandler(fakePeripheral, serviceData); + } + [self waitForExpectations:@[ expectation ] timeout:1.0]; + _fakeGNCBLEMedium.openL2CAPChannelShouldComplete = NO; + + auto l2cap_socket = + _medium->ConnectOverL2cap(123, kTestServiceID, nearby::api::ble::TxPowerLevel::kUltraLow, + fakePeripheral.identifier.hash, nullptr); + + XCTAssertEqual(l2cap_socket.get(), nullptr); +} + +#pragma mark - Connect Tests + +- (void)testConnect_PeripheralNotFound { + auto socket = + _medium->Connect(kTestServiceID, nearby::api::ble::TxPowerLevel::kUltraLow, 99999, nullptr); + + XCTAssertEqual(socket.get(), nullptr); +} + +- (void)testConnect_CentralPeerNotFound { + GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init]; + NSDictionary *serviceData = + @{[CBUUID UUIDWithString:kTestServiceUUIDString] : [NSData dataWithBytes:"test" length:4]}; + XCTestExpectation *expectation = + [self expectationWithDescription:@"Advertisement found callback should be called."]; + nearby::api::ble::BleMedium::ScanCallback callback = { + .advertisement_found_cb = std::function( + [expectation](nearby::api::ble::BlePeripheral::UniqueId peripheral_id, + const nearby::api::ble::BleAdvertisementData &advertisement) { + [expectation fulfill]; + })}; + _medium->StartScanning(nearby::Uuid(0, 0), nearby::api::ble::TxPowerLevel::kUltraLow, + std::move(callback)); + if (_fakeGNCBLEMedium.advertisementFoundHandler) { + _fakeGNCBLEMedium.advertisementFoundHandler(fakePeripheral, serviceData); + } + [self waitForExpectations:@[ expectation ] timeout:1.0]; + + id mockCentralManager = OCMClassMock([GNSCentralManager class]); + OCMStub([mockCentralManager retrieveCentralPeerWithIdentifier:fakePeripheral.identifier]) + .andReturn(nil); + nearby::apple::BleMediumPeer::SetSocketCentralManager(_medium.get(), mockCentralManager); + + auto socket = _medium->Connect(kTestServiceID, nearby::api::ble::TxPowerLevel::kUltraLow, + fakePeripheral.identifier.hash, nullptr); + + XCTAssertEqual(socket.get(), nullptr); +} + +- (void)testConnect_SocketError { + GNCFakePeripheral *fakePeripheral = [[GNCFakePeripheral alloc] init]; + NSDictionary *serviceData = + @{[CBUUID UUIDWithString:kTestServiceUUIDString] : [NSData dataWithBytes:"test" length:4]}; + XCTestExpectation *expectation = + [self expectationWithDescription:@"Advertisement found callback should be called."]; + nearby::api::ble::BleMedium::ScanCallback callback = { + .advertisement_found_cb = std::function( + [expectation](nearby::api::ble::BlePeripheral::UniqueId peripheral_id, + const nearby::api::ble::BleAdvertisementData &advertisement) { + [expectation fulfill]; + })}; + _medium->StartScanning(nearby::Uuid(0, 0), nearby::api::ble::TxPowerLevel::kUltraLow, + std::move(callback)); + if (_fakeGNCBLEMedium.advertisementFoundHandler) { + _fakeGNCBLEMedium.advertisementFoundHandler(fakePeripheral, serviceData); + } + [self waitForExpectations:@[ expectation ] timeout:1.0]; + + id mockCentralPeerManager = OCMClassMock([GNSCentralPeerManager class]); + OCMStub([mockCentralPeerManager socketWithPairingCharacteristic:NO completion:[OCMArg any]]) + .andDo(^(GNSCentralPeerManager *localSelf, BOOL pairing, + void (^completion)(GNSSocket *socket, NSError *error)) { + completion(nil, [NSError errorWithDomain:@"test" code:0 userInfo:nil]); + }); + + id mockCentralManager = OCMClassMock([GNSCentralManager class]); + OCMStub([mockCentralManager retrieveCentralPeerWithIdentifier:fakePeripheral.identifier]) + .andReturn(mockCentralPeerManager); + nearby::apple::BleMediumPeer::SetSocketCentralManager(_medium.get(), mockCentralManager); + + auto socket = _medium->Connect(kTestServiceID, nearby::api::ble::TxPowerLevel::kUltraLow, + fakePeripheral.identifier.hash, nullptr); + + XCTAssertEqual(socket.get(), nullptr); +} + #pragma mark - Server Socket Tests - (void)testOpenServerSocket_Success { - XCTSkip(@"TODO(b/293336684): Requires more capable GNSPeripheralManager fakes for full testing."); + id mockPeripheralManager = OCMClassMock([GNSPeripheralManager class]); + OCMStub([mockPeripheralManager addPeripheralServiceManager:[OCMArg any] + bleServiceAddedCompletion:[OCMArg any]]) + .andDo(^(GNSPeripheralManager *localSelf, GNSPeripheralServiceManager *manager, + void (^completion)(NSError *error)) { + completion(nil); + }); + nearby::apple::BleMediumPeer::SetSocketPeripheralManager(_medium.get(), mockPeripheralManager); + auto server_socket = _medium->OpenServerSocket(kTestServiceID); XCTAssertNotEqual(server_socket.get(), nullptr); } -// TODO(b/293336684): Add failure test case for OpenServerSocket when GNSPeripheralManager fakes -// are more capable. +- (void)testOpenServerSocket_Failure { + id mockPeripheralManager = OCMClassMock([GNSPeripheralManager class]); + OCMStub([mockPeripheralManager addPeripheralServiceManager:[OCMArg any] + bleServiceAddedCompletion:[OCMArg any]]) + .andDo(^(GNSPeripheralManager *localSelf, GNSPeripheralServiceManager *manager, + void (^completion)(NSError *error)) { + completion([NSError errorWithDomain:@"test" code:0 userInfo:nil]); + }); + nearby::apple::BleMediumPeer::SetSocketPeripheralManager(_medium.get(), mockPeripheralManager); + + auto server_socket = _medium->OpenServerSocket(kTestServiceID); + + XCTAssertEqual(server_socket.get(), nullptr); +} + +- (void)testOpenServerSocket_Timeout { + id mockPeripheralManager = OCMClassMock([GNSPeripheralManager class]); + OCMStub([mockPeripheralManager addPeripheralServiceManager:[OCMArg any] + bleServiceAddedCompletion:[OCMArg any]]) + .andDo(^(GNSPeripheralManager *localSelf, GNSPeripheralServiceManager *manager, + void (^completion)(NSError *error)){ + // Do not call completion to simulate timeout. + }); + nearby::apple::BleMediumPeer::SetSocketPeripheralManager(_medium.get(), mockPeripheralManager); + + auto server_socket = _medium->OpenServerSocket(kTestServiceID); + + XCTAssertEqual(server_socket.get(), nullptr); +} #pragma mark - Other Tests diff --git a/internal/platform/implementation/apple/ble_medium.h b/internal/platform/implementation/apple/ble_medium.h index 5efac96d..7c052c43 100644 --- a/internal/platform/implementation/apple/ble_medium.h +++ b/internal/platform/implementation/apple/ble_medium.h @@ -42,9 +42,13 @@ namespace nearby { namespace apple { +class BleMediumPeer; + // The main BLE medium used inside of Nearby. This serves as the entry point for all BLE and GATT // related operations. class BleMedium : public api::ble::BleMedium { + friend class BleMediumPeer; + public: BleMedium(); // For testing only. @@ -197,7 +201,6 @@ class BleMedium : public api::ble::BleMedium { void HandleAdvertisementFound(id peripheral, NSDictionary *serviceData); void ClearAdvertisementPacketsMap(); - void CleanUpExpiredAdvertisementPackets(NSDate *now); bool ShouldReportAdvertisement(NSDate *now, api::ble::BlePeripheral::UniqueId peripheral_id, NSDictionary *service_data); void AddAdvertisementPacketInfo(api::ble::BlePeripheral::UniqueId peripheral_id, diff --git a/internal/platform/implementation/apple/ble_medium.mm b/internal/platform/implementation/apple/ble_medium.mm index 604844df..dbb3af30 100644 --- a/internal/platform/implementation/apple/ble_medium.mm +++ b/internal/platform/implementation/apple/ble_medium.mm @@ -38,6 +38,7 @@ #import "internal/platform/implementation/apple/Log/GNCLogger.h" // TODO(b/293336684): Old Weave imports that need to be deleted once shared Weave is complete. +#import "internal/platform/implementation/apple/GNCUtils.h" #import "internal/platform/implementation/apple/Mediums/BLE/GNCMBleConnection.h" #import "internal/platform/implementation/apple/Mediums/BLE/GNCMBleUtils.h" #import "internal/platform/implementation/apple/Mediums/BLE/Sockets/Source/Central/GNSCentralManager.h" @@ -52,7 +53,6 @@ #import "internal/platform/implementation/apple/ble_socket.h" #import "internal/platform/implementation/apple/bluetooth_adapter_v2.h" #import "internal/platform/implementation/apple/utils.h" -#import "internal/platform/implementation/apple/GNCUtils.h" static NSString *const kWeaveServiceUUID = @"FEF3"; static const char *const kConnectionCallbackQueueLabel = @@ -225,7 +225,8 @@ std::unique_ptr BleMedium::StartScanning( if (blockError) { GNCLoggerError(@"Failed to start scanning: %@", blockError); - // The start_scanning_result callback was already called in the completionHandler with the error. + // The start_scanning_result callback was already called in the completionHandler with the + // error. return nullptr; } @@ -449,13 +450,11 @@ std::unique_ptr BleMedium::OpenServerSocket( auto server_socket = std::make_unique(); __block auto server_socket_ptr = server_socket.get(); - if (socketPeripheralManager_) { - [socketPeripheralManager_ stop]; + if (socketPeripheralManager_ == nil) { + socketPeripheralManager_ = [[GNSPeripheralManager alloc] initWithAdvertisedName:nil + restoreIdentifier:nil]; } - socketPeripheralManager_ = [[GNSPeripheralManager alloc] initWithAdvertisedName:nil - restoreIdentifier:nil]; - if (socketPeripheralManager_ == nil) { GNCLoggerError(@"Failed to create peripheral manager."); return nullptr; @@ -726,19 +725,6 @@ NSDate *BleMedium::GetLastTimestampToCleanExpiredAdvertisementPackets() { return last_timestamp_to_clean_expired_advertisement_packets_; } -void BleMedium::CleanUpExpiredAdvertisementPackets(NSDate *now) { - absl::MutexLock lock(&advertisement_packets_mutex_); - for (auto it = advertisement_packets_map_.begin(); it != advertisement_packets_map_.end();) { - if ([now timeIntervalSinceDate:it->second.last_timestamp] > - kAdvertisementPacketsMapExpirationTimeInterval) { - advertisement_packets_map_.erase(it++); - } else { - ++it; - } - } - last_timestamp_to_clean_expired_advertisement_packets_ = now; -} - bool BleMedium::ShouldReportAdvertisement(NSDate *now, api::ble::BlePeripheral::UniqueId peripheral_id, NSDictionary *service_data) {