[NC Apple coverage] Add more comprehensive tests for BLE medium connections.

PiperOrigin-RevId: 826582061
This commit is contained in:
Edwin Wu
2025-10-31 12:15:56 -07:00
committed by Copybara-Service
parent 3cdee32f14
commit f353079f6f
6 changed files with 243 additions and 38 deletions
@@ -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
@@ -28,6 +28,14 @@ NS_ASSUME_NONNULL_BEGIN
@implementation GNCFakeBLEMedium
- (instancetype)init {
self = [super init];
if (self) {
_openL2CAPChannelShouldComplete = YES;
}
return self;
}
- (void)startAdvertisingData:(NSDictionary<CBUUID *, NSData *> *)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<CBUUID *> *)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<GNCPeripheral>)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<GNCPeripheral> p) {
// Do nothing in fake.
requestDisconnectionHandler:^(id<GNCPeripheral> 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<GNCPeripheralManager>)peripheralManager {
channelOpenedCompletionHandler:
(GNCOpenL2CAPServerChannelOpendCompletionHandler)
channelOpenedCompletionHandler
peripheralManager:
(nullable id<GNCPeripheralManager>)peripheralManager {
if (psmPublishedCompletionHandler) {
psmPublishedCompletionHandler(self.fakePSM, self.openL2CAPServerSocketError);
}
@@ -114,7 +128,7 @@ NS_ASSUME_NONNULL_BEGIN
peripheral:(id<GNCPeripheral>)peripheral
completionHandler:(nullable GNCOpenL2CAPStreamCompletionHandler)completionHandler {
self.lastConnectedPeripheral = peripheral;
if (completionHandler) {
if (self.openL2CAPChannelShouldComplete && completionHandler) {
completionHandler(self.fakeL2CAPStream, self.openL2CAPChannelError);
}
}
@@ -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",
@@ -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<nearby::apple::BleMedium>((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<CBUUID *, NSData *> *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<void(nearby::api::ble::BlePeripheral::UniqueId,
nearby::api::ble::BleAdvertisementData)>(
[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<CBUUID *, NSData *> *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<void(nearby::api::ble::BlePeripheral::UniqueId,
nearby::api::ble::BleAdvertisementData)>(
[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<CBUUID *, NSData *> *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<void(nearby::api::ble::BlePeripheral::UniqueId,
nearby::api::ble::BleAdvertisementData)>(
[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<CBUUID *, NSData *> *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<void(nearby::api::ble::BlePeripheral::UniqueId,
nearby::api::ble::BleAdvertisementData)>(
[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
@@ -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<GNCPeripheral> peripheral,
NSDictionary<CBUUID *, NSData *> *serviceData);
void ClearAdvertisementPacketsMap();
void CleanUpExpiredAdvertisementPackets(NSDate *now);
bool ShouldReportAdvertisement(NSDate *now, api::ble::BlePeripheral::UniqueId peripheral_id,
NSDictionary<CBUUID *, NSData *> *service_data);
void AddAdvertisementPacketInfo(api::ble::BlePeripheral::UniqueId peripheral_id,
@@ -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<api::ble::BleMedium::ScanningSession> 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<api::ble::BleServerSocket> BleMedium::OpenServerSocket(
auto server_socket = std::make_unique<BleServerSocket>();
__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<CBUUID *, NSData *> *service_data) {