| // Copyright 2017 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "src/connectivity/bluetooth/core/bt-host/gap/low_energy_discovery_manager.h" |
| |
| #include <lib/inspect/testing/cpp/inspect.h> |
| #include <zircon/assert.h> |
| |
| #include <unordered_set> |
| #include <vector> |
| |
| #include <fbl/macros.h> |
| #include <gmock/gmock.h> |
| |
| #include "lib/inspect/cpp/reader.h" |
| #include "src/connectivity/bluetooth/core/bt-host/common/advertising_data.h" |
| #include "src/connectivity/bluetooth/core/bt-host/gap/peer.h" |
| #include "src/connectivity/bluetooth/core/bt-host/gap/peer_cache.h" |
| #include "src/connectivity/bluetooth/core/bt-host/hci/fake_local_address_delegate.h" |
| #include "src/connectivity/bluetooth/core/bt-host/hci/legacy_low_energy_scanner.h" |
| #include "src/connectivity/bluetooth/core/bt-host/testing/controller_test.h" |
| #include "src/connectivity/bluetooth/core/bt-host/testing/fake_controller.h" |
| #include "src/connectivity/bluetooth/core/bt-host/testing/fake_peer.h" |
| |
| namespace bt::gap { |
| namespace { |
| |
| using namespace inspect::testing; |
| using bt::testing::FakeController; |
| using bt::testing::FakePeer; |
| using PauseToken = LowEnergyDiscoveryManager::PauseToken; |
| |
| using TestingBase = bt::testing::ControllerTest<FakeController>; |
| |
| const DeviceAddress kAddress0(DeviceAddress::Type::kLEPublic, {0}); |
| const DeviceAddress kAddrAlias0(DeviceAddress::Type::kBREDR, kAddress0.value()); |
| const DeviceAddress kAddress1(DeviceAddress::Type::kLERandom, {1}); |
| const DeviceAddress kAddress2(DeviceAddress::Type::kLEPublic, {2}); |
| const DeviceAddress kAddress3(DeviceAddress::Type::kLEPublic, {3}); |
| const DeviceAddress kAddress4(DeviceAddress::Type::kLEPublic, {4}); |
| const DeviceAddress kAddress5(DeviceAddress::Type::kLEPublic, {5}); |
| |
| constexpr zx::duration kTestScanPeriod = zx::sec(10); |
| |
| const char* kInspectNodeName = "low_energy_discovery_manager"; |
| |
| class LowEnergyDiscoveryManagerTest : public TestingBase { |
| public: |
| LowEnergyDiscoveryManagerTest() = default; |
| ~LowEnergyDiscoveryManagerTest() override = default; |
| |
| void SetUp() override { |
| TestingBase::SetUp(); |
| |
| scan_enabled_ = false; |
| |
| FakeController::Settings settings; |
| settings.ApplyLegacyLEConfig(); |
| test_device()->set_settings(settings); |
| |
| // TODO(armansito): Now that the hci::LowEnergyScanner is injected into |
| // |discovery_manager_| rather than constructed by it, a fake implementation |
| // could be injected directly. Consider providing fake behavior here in this |
| // harness rather than using a FakeController. |
| scanner_ = std::make_unique<hci::LegacyLowEnergyScanner>(&fake_address_delegate_, |
| transport()->WeakPtr(), dispatcher()); |
| discovery_manager_ = std::make_unique<LowEnergyDiscoveryManager>(transport()->WeakPtr(), |
| scanner_.get(), &peer_cache_); |
| discovery_manager_->AttachInspect(inspector_.GetRoot(), kInspectNodeName); |
| |
| test_device()->set_scan_state_callback( |
| std::bind(&LowEnergyDiscoveryManagerTest::OnScanStateChanged, this, std::placeholders::_1)); |
| test_device()->StartCmdChannel(test_cmd_chan()); |
| test_device()->StartAclChannel(test_acl_chan()); |
| } |
| |
| void TearDown() override { |
| if (discovery_manager_) { |
| discovery_manager_ = nullptr; |
| } |
| scanner_ = nullptr; |
| test_device()->Stop(); |
| TestingBase::TearDown(); |
| } |
| |
| protected: |
| LowEnergyDiscoveryManager* discovery_manager() const { return discovery_manager_.get(); } |
| |
| // Deletes |discovery_manager_|. |
| void DeleteDiscoveryManager() { discovery_manager_ = nullptr; } |
| |
| inspect::Hierarchy InspectHierarchy() const { |
| return inspect::ReadFromVmo(inspector_.DuplicateVmo()).take_value(); |
| } |
| |
| std::vector<inspect::PropertyValue> InspectProperties() const { |
| auto hierarchy = InspectHierarchy(); |
| auto children = hierarchy.take_children(); |
| ZX_ASSERT(children.size() == 1u); |
| return children.front().node_ptr()->take_properties(); |
| } |
| |
| PeerCache* peer_cache() { return &peer_cache_; } |
| |
| // Returns the last reported scan state of the FakeController. |
| bool scan_enabled() const { return scan_enabled_; } |
| |
| // The scan states that the FakeController has transitioned through. |
| const std::vector<bool> scan_states() const { return scan_states_; } |
| |
| // Sets a callback that will run when the scan state transitions |count| |
| // times. |
| void set_scan_state_handler(size_t count, fit::closure callback) { |
| scan_state_callbacks_[count] = std::move(callback); |
| } |
| |
| // Called by FakeController when the scan state changes. |
| void OnScanStateChanged(bool enabled) { |
| auto scan_type = test_device()->le_scan_state().scan_type; |
| bt_log(DEBUG, "gap-test", "FakeController scan state: %s %s", enabled ? "enabled" : "disabled", |
| scan_type == hci::LEScanType::kActive ? "active" : "passive"); |
| scan_enabled_ = enabled; |
| scan_states_.push_back(enabled); |
| |
| auto iter = scan_state_callbacks_.find(scan_states_.size()); |
| if (iter != scan_state_callbacks_.end()) { |
| iter->second(); |
| } |
| } |
| |
| // Registers the following fake peers with the FakeController: |
| // |
| // Peer 0: |
| // - Connectable, not scannable; |
| // - General discoverable; |
| // - UUIDs: 0x180d, 0x180f; |
| // - has name: "Device 0" |
| // |
| // Peer 1: |
| // - Connectable, not scannable; |
| // - Limited discoverable; |
| // - UUIDs: 0x180d; |
| // - has name: "Device 1" |
| // |
| // Peer 2: |
| // - Not connectable, not scannable; |
| // - General discoverable; |
| // - UUIDs: none; |
| // - has name: "Device 2" |
| // |
| // Peer 3: |
| // - Not discoverable; |
| void AddFakePeers() { |
| // Peer 0 |
| const auto kAdvData0 = CreateStaticByteBuffer( |
| // Flags |
| 0x02, 0x01, 0x02, |
| |
| // Complete 16-bit service UUIDs |
| 0x05, 0x03, 0x0d, 0x18, 0x0f, 0x18, |
| |
| // Complete local name |
| 0x09, 0x09, 'D', 'e', 'v', 'i', 'c', 'e', ' ', '0'); |
| auto fake_peer = std::make_unique<FakePeer>(kAddress0, true, true); |
| fake_peer->SetAdvertisingData(kAdvData0); |
| test_device()->AddPeer(std::move(fake_peer)); |
| |
| // Peer 1 |
| const auto kAdvData1 = CreateStaticByteBuffer( |
| // Flags |
| 0x02, 0x01, 0x01, |
| |
| // Complete 16-bit service UUIDs |
| 0x03, 0x03, 0x0d, 0x18); |
| fake_peer = std::make_unique<FakePeer>(kAddress1, true, true); |
| fake_peer->SetAdvertisingData(kAdvData1); |
| test_device()->AddPeer(std::move(fake_peer)); |
| |
| // Peer 2 |
| const auto kAdvData2 = CreateStaticByteBuffer( |
| // Flags |
| 0x02, 0x01, 0x02, |
| |
| // Complete local name |
| 0x09, 0x09, 'D', 'e', 'v', 'i', 'c', 'e', ' ', '2'); |
| fake_peer = std::make_unique<FakePeer>(kAddress2, false, false); |
| fake_peer->SetAdvertisingData(kAdvData2); |
| test_device()->AddPeer(std::move(fake_peer)); |
| |
| // Peer 3 |
| const auto kAdvData3 = CreateStaticByteBuffer( |
| // Flags |
| 0x02, 0x01, 0x00, |
| |
| // Complete local name |
| 0x09, 0x09, 'D', 'e', 'v', 'i', 'c', 'e', ' ', '3'); |
| fake_peer = std::make_unique<FakePeer>(kAddress3, false, false); |
| fake_peer->SetAdvertisingData(kAdvData3); |
| test_device()->AddPeer(std::move(fake_peer)); |
| } |
| |
| // Creates and returns a discovery session. |
| std::unique_ptr<LowEnergyDiscoverySession> StartDiscoverySession(bool active = true) { |
| std::unique_ptr<LowEnergyDiscoverySession> session; |
| discovery_manager()->StartDiscovery(active, [&](auto cb_session) { |
| ZX_ASSERT(cb_session); |
| session = std::move(cb_session); |
| }); |
| |
| RunLoopUntilIdle(); |
| ZX_ASSERT(session); |
| return session; |
| } |
| |
| private: |
| PeerCache peer_cache_; |
| hci::FakeLocalAddressDelegate fake_address_delegate_; |
| std::unique_ptr<hci::LegacyLowEnergyScanner> scanner_; |
| std::unique_ptr<LowEnergyDiscoveryManager> discovery_manager_; |
| |
| bool scan_enabled_; |
| std::vector<bool> scan_states_; |
| std::unordered_map<size_t, fit::closure> scan_state_callbacks_; |
| |
| inspect::Inspector inspector_; |
| |
| DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(LowEnergyDiscoveryManagerTest); |
| }; |
| |
| using GAP_LowEnergyDiscoveryManagerTest = LowEnergyDiscoveryManagerTest; |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, StartDiscoveryAndStop) { |
| std::unique_ptr<LowEnergyDiscoverySession> session; |
| discovery_manager()->StartDiscovery( |
| /*active=*/true, [&session](auto cb_session) { session = std::move(cb_session); }); |
| |
| RunLoopUntilIdle(); |
| |
| // The test fixture will be notified of the change in scan state before we |
| // receive the session. |
| EXPECT_TRUE(scan_enabled()); |
| RunLoopUntilIdle(); |
| |
| ASSERT_TRUE(session); |
| EXPECT_TRUE(session->alive()); |
| |
| session->Stop(); |
| EXPECT_FALSE(session->alive()); |
| |
| RunLoopUntilIdle(); |
| EXPECT_FALSE(scan_enabled()); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, StartDiscoveryAndStopByDeleting) { |
| // Start discovery but don't acquire ownership of the received session. This |
| // should immediately terminate the session. |
| std::unique_ptr<LowEnergyDiscoverySession> session; |
| discovery_manager()->StartDiscovery( |
| /*active=*/true, [&session](auto cb_session) { session = std::move(cb_session); }); |
| |
| RunLoopUntilIdle(); |
| |
| // The test fixture will be notified of the change in scan state before we |
| // receive the session. |
| EXPECT_TRUE(scan_enabled()); |
| RunLoopUntilIdle(); |
| |
| ASSERT_TRUE(session); |
| EXPECT_TRUE(session->alive()); |
| |
| session = nullptr; |
| |
| RunLoopUntilIdle(); |
| EXPECT_FALSE(scan_enabled()); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, Destructor) { |
| // Start discovery with a session, delete the manager and ensure that the |
| // session is inactive with the error callback called. |
| std::unique_ptr<LowEnergyDiscoverySession> session; |
| discovery_manager()->StartDiscovery( |
| /*active=*/true, [&session](auto cb_session) { session = std::move(cb_session); }); |
| |
| RunLoopUntilIdle(); |
| |
| EXPECT_TRUE(scan_enabled()); |
| |
| ASSERT_TRUE(session); |
| EXPECT_TRUE(session->alive()); |
| |
| size_t num_errors = 0u; |
| session->set_error_callback([&num_errors]() { num_errors++; }); |
| |
| EXPECT_EQ(0u, num_errors); |
| DeleteDiscoveryManager(); |
| EXPECT_EQ(1u, num_errors); |
| EXPECT_FALSE(session->alive()); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, StartDiscoveryAndStopInCallback) { |
| // Start discovery but don't acquire ownership of the received session. This |
| // should terminate the session when |session| goes out of scope. |
| discovery_manager()->StartDiscovery(/*active=*/true, [](auto session) {}); |
| |
| RunLoopUntilIdle(); |
| ASSERT_EQ(2u, scan_states().size()); |
| EXPECT_TRUE(scan_states()[0]); |
| EXPECT_FALSE(scan_states()[1]); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, StartDiscoveryFailure) { |
| test_device()->SetDefaultResponseStatus(hci::kLESetScanEnable, |
| hci::StatusCode::kCommandDisallowed); |
| |
| // |session| should contain nullptr. |
| discovery_manager()->StartDiscovery(/*active=*/true, [](auto session) { EXPECT_FALSE(session); }); |
| |
| RunLoopUntilIdle(); |
| EXPECT_FALSE(scan_enabled()); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, StartDiscoveryWhileScanning) { |
| std::vector<std::unique_ptr<LowEnergyDiscoverySession>> sessions; |
| |
| constexpr size_t kExpectedSessionCount = 5; |
| size_t cb_count = 0u; |
| auto cb = [&cb_count, &sessions](auto session) { |
| sessions.push_back(std::move(session)); |
| cb_count++; |
| }; |
| |
| discovery_manager()->StartDiscovery(/*active=*/true, cb); |
| |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| EXPECT_EQ(1u, sessions.size()); |
| |
| // Add the rest of the sessions. These are expected to succeed immediately but |
| // the callbacks should be called asynchronously. |
| for (size_t i = 1u; i < kExpectedSessionCount; i++) { |
| discovery_manager()->StartDiscovery(/*active=*/true, cb); |
| } |
| |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| EXPECT_EQ(kExpectedSessionCount, sessions.size()); |
| |
| // Remove one session from the list. Scan should continue. |
| sessions.pop_back(); |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| |
| // Remove all but one session from the list. Scan should continue. |
| sessions.erase(sessions.begin() + 1, sessions.end()); |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| EXPECT_EQ(1u, sessions.size()); |
| |
| // Remove the last session. |
| sessions.clear(); |
| RunLoopUntilIdle(); |
| EXPECT_FALSE(scan_enabled()); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, StartDiscoveryWhilePendingStart) { |
| std::vector<std::unique_ptr<LowEnergyDiscoverySession>> sessions; |
| |
| constexpr size_t kExpectedSessionCount = 5; |
| size_t cb_count = 0u; |
| auto cb = [&cb_count, &sessions](auto session) { |
| sessions.push_back(std::move(session)); |
| cb_count++; |
| }; |
| |
| for (size_t i = 0u; i < kExpectedSessionCount; i++) { |
| discovery_manager()->StartDiscovery(/*active=*/true, cb); |
| } |
| |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| EXPECT_EQ(kExpectedSessionCount, sessions.size()); |
| |
| // Remove all sessions. This should stop the scan. |
| sessions.clear(); |
| RunLoopUntilIdle(); |
| EXPECT_FALSE(scan_enabled()); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, StartDiscoveryWhilePendingStartAndStopInCallback) { |
| constexpr size_t kExpectedSessionCount = 5; |
| size_t cb_count = 0u; |
| std::unique_ptr<LowEnergyDiscoverySession> session; |
| auto cb = [&cb_count, &session](auto cb_session) { |
| cb_count++; |
| if (cb_count == kExpectedSessionCount) { |
| // Hold on to only the last session object. The rest should get deleted |
| // within the callback. |
| session = std::move(cb_session); |
| } |
| }; |
| |
| for (size_t i = 0u; i < kExpectedSessionCount; i++) { |
| discovery_manager()->StartDiscovery(/*active=*/true, cb); |
| } |
| |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| EXPECT_TRUE(session); |
| |
| RunLoopUntilIdle(); |
| EXPECT_EQ(kExpectedSessionCount, cb_count); |
| EXPECT_TRUE(scan_enabled()); |
| |
| // Deleting the only remaning session should stop the scan. |
| session = nullptr; |
| RunLoopUntilIdle(); |
| EXPECT_FALSE(scan_enabled()); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, StartDiscoveryWhilePendingStop) { |
| std::unique_ptr<LowEnergyDiscoverySession> session; |
| |
| discovery_manager()->StartDiscovery( |
| /*active=*/true, [&session](auto cb_session) { session = std::move(cb_session); }); |
| |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| EXPECT_TRUE(session); |
| |
| // Stop the session. This should issue a request to stop the ongoing scan but |
| // the request will remain pending until we run the message loop. |
| session = nullptr; |
| |
| // Request a new session. The discovery manager should restart the scan after |
| // the ongoing one stops. |
| discovery_manager()->StartDiscovery( |
| /*active=*/true, [&session](auto cb_session) { session = std::move(cb_session); }); |
| |
| // Discovery should stop and start again. |
| RunLoopUntilIdle(); |
| ASSERT_EQ(3u, scan_states().size()); |
| EXPECT_TRUE(scan_states()[0]); |
| EXPECT_FALSE(scan_states()[1]); |
| EXPECT_TRUE(scan_states()[2]); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, StartDiscoveryFailureManyPending) { |
| test_device()->SetDefaultResponseStatus(hci::kLESetScanEnable, |
| hci::StatusCode::kCommandDisallowed); |
| |
| constexpr size_t kExpectedSessionCount = 5; |
| size_t cb_count = 0u; |
| auto cb = [&cb_count](auto session) { |
| // |session| should contain nullptr as the request will fail. |
| EXPECT_FALSE(session); |
| cb_count++; |
| }; |
| |
| for (size_t i = 0u; i < kExpectedSessionCount; i++) { |
| discovery_manager()->StartDiscovery(/*active=*/true, cb); |
| } |
| |
| RunLoopUntilIdle(); |
| EXPECT_FALSE(scan_enabled()); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, ScanPeriodRestart) { |
| constexpr size_t kNumScanStates = 3; |
| |
| discovery_manager()->set_scan_period(kTestScanPeriod); |
| |
| std::unique_ptr<LowEnergyDiscoverySession> session; |
| discovery_manager()->StartDiscovery( |
| /*active=*/true, [&session](auto cb_session) { session = std::move(cb_session); }); |
| |
| // We should observe the scan state become enabled -> disabled -> enabled. |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| |
| // End the scan period. |
| RunLoopFor(kTestScanPeriod); |
| ASSERT_EQ(kNumScanStates, scan_states().size()); |
| EXPECT_TRUE(scan_states()[0]); |
| EXPECT_FALSE(scan_states()[1]); |
| EXPECT_TRUE(scan_states()[2]); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, ScanPeriodRestartFailure) { |
| constexpr size_t kNumScanStates = 2; |
| |
| discovery_manager()->set_scan_period(kTestScanPeriod); |
| |
| std::unique_ptr<LowEnergyDiscoverySession> session; |
| bool session_error = false; |
| discovery_manager()->StartDiscovery(/*active=*/true, [&](auto cb_session) { |
| session = std::move(cb_session); |
| session->set_error_callback([&session_error] { session_error = true; }); |
| }); |
| |
| // The controller will fail to restart scanning after scanning stops at the |
| // end of the period. The scan state will transition twice (-> enabled -> |
| // disabled). |
| set_scan_state_handler(kNumScanStates, [this] { |
| test_device()->SetDefaultResponseStatus(hci::kLESetScanEnable, |
| hci::StatusCode::kCommandDisallowed); |
| }); |
| |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| |
| // End the scan period. The scan should not restart. |
| RunLoopFor(kTestScanPeriod); |
| |
| ASSERT_EQ(kNumScanStates, scan_states().size()); |
| EXPECT_TRUE(scan_states()[0]); |
| EXPECT_FALSE(scan_states()[1]); |
| EXPECT_TRUE(session_error); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, ScanPeriodRestartRemoveSession) { |
| constexpr size_t kNumScanStates = 4; |
| |
| discovery_manager()->set_scan_period(kTestScanPeriod); |
| |
| std::unique_ptr<LowEnergyDiscoverySession> session; |
| discovery_manager()->StartDiscovery( |
| /*active=*/true, [&session](auto cb_session) { session = std::move(cb_session); }); |
| |
| // We should observe 3 scan state transitions (-> enabled -> disabled -> |
| // enabled). |
| set_scan_state_handler(kNumScanStates - 1, [this, &session] { |
| ASSERT_TRUE(session); |
| EXPECT_TRUE(scan_enabled()); |
| |
| // At this point the fake controller has updated its state but the discovery |
| // manager has not processed the restarted scan. We should be able to remove |
| // the current session and the state should ultimately become disabled. |
| session->Stop(); |
| }); |
| |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| |
| // End the scan period. |
| RunLoopFor(kTestScanPeriod); |
| EXPECT_THAT(scan_states(), ::testing::ElementsAre(true, false, true, false)); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, ScanPeriodRemoveSessionDuringRestart) { |
| constexpr size_t kNumScanStates = 2; |
| |
| // Set a very short scan period for the sake of the test. |
| discovery_manager()->set_scan_period(kTestScanPeriod); |
| |
| std::unique_ptr<LowEnergyDiscoverySession> session; |
| discovery_manager()->StartDiscovery( |
| /*active=*/true, [&session](auto cb_session) { session = std::move(cb_session); }); |
| |
| // The controller will fail to restart scanning after scanning stops at the |
| // end of the period. The scan state will transition twice (-> enabled -> |
| // disabled). |
| set_scan_state_handler(kNumScanStates, [this, &session] { |
| ASSERT_TRUE(session); |
| EXPECT_FALSE(scan_enabled()); |
| |
| // Stop the session before the discovery manager processes the event. It |
| // should detect this and discontinue the scan. |
| session->Stop(); |
| }); |
| |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| |
| // End the scan period. |
| RunLoopFor(kTestScanPeriod); |
| EXPECT_THAT(scan_states(), ::testing::ElementsAre(true, false)); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, ScanPeriodRestartRemoveAndAddSession) { |
| constexpr size_t kNumScanPeriodRestartStates = 3; |
| constexpr size_t kTotalNumStates = 5; |
| |
| // Set a very short scan period for the sake of the test. |
| discovery_manager()->set_scan_period(kTestScanPeriod); |
| |
| std::unique_ptr<LowEnergyDiscoverySession> session; |
| auto cb = [&session](auto cb_session) { session = std::move(cb_session); }; |
| discovery_manager()->StartDiscovery(/*active=*/true, cb); |
| |
| // We should observe 3 scan state transitions (-> enabled -> disabled -> |
| // enabled). |
| set_scan_state_handler(kNumScanPeriodRestartStates, [this, &session, cb] { |
| ASSERT_TRUE(session); |
| EXPECT_TRUE(scan_enabled()); |
| |
| // At this point the fake controller has updated its state but the discovery |
| // manager has not processed the restarted scan. We should be able to remove |
| // the current session and create a new one and the state should update |
| // accordingly. |
| session->Stop(); |
| discovery_manager()->StartDiscovery(/*active=*/true, cb); |
| }); |
| |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| |
| // End the scan period. |
| RunLoopFor(kTestScanPeriod); |
| |
| // Scan should have been disabled and re-enabled. |
| ASSERT_EQ(kTotalNumStates, scan_states().size()); |
| EXPECT_TRUE(scan_states()[0]); |
| EXPECT_FALSE(scan_states()[1]); |
| EXPECT_TRUE(scan_states()[2]); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, StartDiscoveryWithFilters) { |
| AddFakePeers(); |
| |
| std::vector<std::unique_ptr<LowEnergyDiscoverySession>> sessions; |
| |
| // Set a short scan period so that we that we process events for multiple scan |
| // periods during the test. |
| discovery_manager()->set_scan_period(zx::msec(200)); |
| |
| // Session 0 is interested in performing general discovery. |
| std::unordered_set<DeviceAddress> peers_session0; |
| LowEnergyDiscoverySession::PeerFoundCallback result_cb = [&peers_session0](const auto& peer) { |
| peers_session0.insert(peer.address()); |
| }; |
| sessions.push_back(StartDiscoverySession()); |
| sessions[0]->filter()->SetGeneralDiscoveryFlags(); |
| sessions[0]->SetResultCallback(std::move(result_cb)); |
| |
| // Session 1 is interested in performing limited discovery. |
| std::unordered_set<DeviceAddress> peers_session1; |
| result_cb = [&peers_session1](const auto& peer) { peers_session1.insert(peer.address()); }; |
| sessions.push_back(StartDiscoverySession()); |
| sessions[1]->filter()->set_flags(static_cast<uint8_t>(AdvFlag::kLELimitedDiscoverableMode)); |
| sessions[1]->SetResultCallback(std::move(result_cb)); |
| |
| // Session 2 is interested in peers with UUID 0x180d. |
| std::unordered_set<DeviceAddress> peers_session2; |
| result_cb = [&peers_session2](const auto& peer) { peers_session2.insert(peer.address()); }; |
| sessions.push_back(StartDiscoverySession()); |
| |
| uint16_t uuid = 0x180d; |
| sessions[2]->filter()->set_service_uuids({UUID(uuid)}); |
| sessions[2]->SetResultCallback(std::move(result_cb)); |
| |
| // Session 3 is interested in peers whose names contain "Device". |
| std::unordered_set<DeviceAddress> peers_session3; |
| result_cb = [&peers_session3](const auto& peer) { peers_session3.insert(peer.address()); }; |
| sessions.push_back(StartDiscoverySession()); |
| sessions[3]->filter()->set_name_substring("Device"); |
| sessions[3]->SetResultCallback(std::move(result_cb)); |
| |
| // Session 4 is interested in non-connectable peers. |
| std::unordered_set<DeviceAddress> peers_session4; |
| result_cb = [&peers_session4](const auto& peer) { peers_session4.insert(peer.address()); }; |
| sessions.push_back(StartDiscoverySession()); |
| sessions[4]->filter()->set_connectable(false); |
| sessions[4]->SetResultCallback(std::move(result_cb)); |
| |
| RunLoopUntilIdle(); |
| |
| EXPECT_EQ(5u, sessions.size()); |
| |
| #define EXPECT_CONTAINS(addr, dev_list) EXPECT_TRUE(dev_list.find(addr) != dev_list.end()) |
| // At this point all sessions should have processed all peers at least once. |
| |
| // Session 0: Should have seen all peers except for peer 3, which is |
| // non-discoverable. |
| EXPECT_EQ(3u, peers_session0.size()); |
| EXPECT_CONTAINS(kAddress0, peers_session0); |
| EXPECT_CONTAINS(kAddress1, peers_session0); |
| EXPECT_CONTAINS(kAddress2, peers_session0); |
| |
| // Session 1: Should have only seen peer 1. |
| EXPECT_EQ(1u, peers_session1.size()); |
| EXPECT_CONTAINS(kAddress1, peers_session1); |
| |
| // Session 2: Should have only seen peers 0 and 1 |
| EXPECT_EQ(2u, peers_session2.size()); |
| EXPECT_CONTAINS(kAddress0, peers_session2); |
| EXPECT_CONTAINS(kAddress1, peers_session2); |
| |
| // Session 3: Should have only seen peers 0, 2, and 3 |
| EXPECT_EQ(3u, peers_session3.size()); |
| EXPECT_CONTAINS(kAddress0, peers_session3); |
| EXPECT_CONTAINS(kAddress2, peers_session3); |
| EXPECT_CONTAINS(kAddress3, peers_session3); |
| |
| // Session 4: Should have seen peers 2 and 3 |
| EXPECT_EQ(2u, peers_session4.size()); |
| EXPECT_CONTAINS(kAddress2, peers_session4); |
| EXPECT_CONTAINS(kAddress3, peers_session4); |
| |
| #undef EXPECT_CONTAINS |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, StartDiscoveryWithFiltersCachedPeerNotifications) { |
| AddFakePeers(); |
| |
| std::vector<std::unique_ptr<LowEnergyDiscoverySession>> sessions; |
| |
| // Set a long scan period to make sure that the FakeController sends |
| // advertising reports only once. |
| discovery_manager()->set_scan_period(zx::sec(20)); |
| |
| // Session 0 is interested in performing general discovery. |
| std::unordered_set<DeviceAddress> peers_session0; |
| LowEnergyDiscoverySession::PeerFoundCallback result_cb = [&peers_session0](const auto& peer) { |
| peers_session0.insert(peer.address()); |
| }; |
| sessions.push_back(StartDiscoverySession()); |
| sessions[0]->filter()->SetGeneralDiscoveryFlags(); |
| sessions[0]->SetResultCallback(std::move(result_cb)); |
| |
| RunLoopUntilIdle(); |
| ASSERT_EQ(3u, peers_session0.size()); |
| |
| // Session 1 is interested in performing limited discovery. |
| std::unordered_set<DeviceAddress> peers_session1; |
| result_cb = [&peers_session1](const auto& peer) { peers_session1.insert(peer.address()); }; |
| sessions.push_back(StartDiscoverySession()); |
| sessions[1]->filter()->set_flags(static_cast<uint8_t>(AdvFlag::kLELimitedDiscoverableMode)); |
| sessions[1]->SetResultCallback(std::move(result_cb)); |
| |
| // Session 2 is interested in peers with UUID 0x180d. |
| std::unordered_set<DeviceAddress> peers_session2; |
| result_cb = [&peers_session2](const auto& peer) { peers_session2.insert(peer.address()); }; |
| sessions.push_back(StartDiscoverySession()); |
| |
| uint16_t uuid = 0x180d; |
| sessions[2]->filter()->set_service_uuids({UUID(uuid)}); |
| sessions[2]->SetResultCallback(std::move(result_cb)); |
| |
| // Session 3 is interested in peers whose names contain "Device". |
| std::unordered_set<DeviceAddress> peers_session3; |
| result_cb = [&peers_session3](const auto& peer) { peers_session3.insert(peer.address()); }; |
| sessions.push_back(StartDiscoverySession()); |
| sessions[3]->filter()->set_name_substring("Device"); |
| sessions[3]->SetResultCallback(std::move(result_cb)); |
| |
| // Session 4 is interested in non-connectable peers. |
| std::unordered_set<DeviceAddress> peers_session4; |
| result_cb = [&peers_session4](const auto& peer) { peers_session4.insert(peer.address()); }; |
| sessions.push_back(StartDiscoverySession()); |
| sessions[4]->filter()->set_connectable(false); |
| sessions[4]->SetResultCallback(std::move(result_cb)); |
| |
| EXPECT_EQ(5u, sessions.size()); |
| |
| #define EXPECT_CONTAINS(addr, dev_list) EXPECT_TRUE(dev_list.find(addr) != dev_list.end()) |
| // At this point all sessions should have processed all peers at least once |
| // without running the message loop; results for Sessions 1, 2, 3, and 4 |
| // should have come from the cache. |
| |
| // Session 0: Should have seen all peers except for peer 3, which is |
| // non-discoverable. |
| EXPECT_EQ(3u, peers_session0.size()); |
| EXPECT_CONTAINS(kAddress0, peers_session0); |
| EXPECT_CONTAINS(kAddress1, peers_session0); |
| EXPECT_CONTAINS(kAddress2, peers_session0); |
| |
| // Session 1: Should have only seen peer 1. |
| EXPECT_EQ(1u, peers_session1.size()); |
| EXPECT_CONTAINS(kAddress1, peers_session1); |
| |
| // Session 2: Should have only seen peers 0 and 1 |
| EXPECT_EQ(2u, peers_session2.size()); |
| EXPECT_CONTAINS(kAddress0, peers_session2); |
| EXPECT_CONTAINS(kAddress1, peers_session2); |
| |
| // Session 3: Should have only seen peers 0, 2, and 3 |
| EXPECT_EQ(3u, peers_session3.size()); |
| EXPECT_CONTAINS(kAddress0, peers_session3); |
| EXPECT_CONTAINS(kAddress2, peers_session3); |
| EXPECT_CONTAINS(kAddress3, peers_session3); |
| |
| // Session 4: Should have seen peers 2 and 3 |
| EXPECT_EQ(2u, peers_session4.size()); |
| EXPECT_CONTAINS(kAddress2, peers_session4); |
| EXPECT_CONTAINS(kAddress3, peers_session4); |
| |
| #undef EXPECT_CONTAINS |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, DirectedAdvertisingEventFromUnknownPeer) { |
| auto fake_peer = std::make_unique<FakePeer>(kAddress0, /*connectable=*/true, /*scannable=*/false); |
| fake_peer->enable_directed_advertising(true); |
| test_device()->AddPeer(std::move(fake_peer)); |
| |
| int connectable_count = 0; |
| discovery_manager()->set_peer_connectable_callback([&](auto) { connectable_count++; }); |
| discovery_manager()->set_scan_period(kTestScanPeriod); |
| |
| auto active_session = StartDiscoverySession(); |
| int active_count = 0; |
| active_session->SetResultCallback([&](auto& peer) { active_count++; }); |
| |
| auto passive_session = StartDiscoverySession(/*active=*/false); |
| int passive_count = 0; |
| passive_session->SetResultCallback([&](auto& peer) { passive_count++; }); |
| |
| RunLoopUntilIdle(); |
| ASSERT_TRUE(active_session); |
| ASSERT_TRUE(passive_session); |
| EXPECT_EQ(0, connectable_count); |
| EXPECT_EQ(0, active_count); |
| EXPECT_EQ(0, passive_count); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, DirectedAdvertisingEventFromKnownNonConnectablePeer) { |
| auto fake_peer = |
| std::make_unique<FakePeer>(kAddress0, /*connectable=*/false, /*scannable=*/false); |
| fake_peer->enable_directed_advertising(true); |
| test_device()->AddPeer(std::move(fake_peer)); |
| Peer* peer = peer_cache()->NewPeer(kAddress0, /*connectable=*/false); |
| ASSERT_TRUE(peer); |
| |
| int connectable_count = 0; |
| discovery_manager()->set_peer_connectable_callback([&](auto) { connectable_count++; }); |
| discovery_manager()->set_scan_period(kTestScanPeriod); |
| |
| auto active_session = StartDiscoverySession(); |
| int active_count = 0; |
| active_session->SetResultCallback([&](auto& peer) { active_count++; }); |
| |
| auto passive_session = StartDiscoverySession(/*active=*/false); |
| int passive_count = 0; |
| passive_session->SetResultCallback([&](auto& peer) { passive_count++; }); |
| |
| RunLoopFor(kTestScanPeriod); |
| ASSERT_TRUE(active_session); |
| ASSERT_TRUE(passive_session); |
| EXPECT_EQ(0, connectable_count); |
| EXPECT_EQ(0, active_count); |
| EXPECT_EQ(1, passive_count); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, DirectedAdvertisingEventFromKnownConnectablePeer) { |
| auto fake_peer = std::make_unique<FakePeer>(kAddress0, /*connectable=*/true, /*scannable=*/false); |
| fake_peer->enable_directed_advertising(true); |
| test_device()->AddPeer(std::move(fake_peer)); |
| Peer* peer = peer_cache()->NewPeer(kAddress0, /*connectable=*/true); |
| ASSERT_TRUE(peer); |
| |
| int connectable_count = 0; |
| discovery_manager()->set_peer_connectable_callback([&](Peer* callback_peer) { |
| ASSERT_TRUE(callback_peer); |
| EXPECT_TRUE(callback_peer->le()); |
| EXPECT_EQ(peer, callback_peer); |
| connectable_count++; |
| }); |
| discovery_manager()->set_scan_period(kTestScanPeriod); |
| |
| auto active_session = StartDiscoverySession(); |
| int active_count = 0; |
| active_session->SetResultCallback([&](auto& peer) { active_count++; }); |
| |
| auto passive_session = StartDiscoverySession(/*active=*/false); |
| int passive_count = 0; |
| passive_session->SetResultCallback([&](auto& peer) { passive_count++; }); |
| |
| RunLoopFor(kTestScanPeriod); |
| ASSERT_TRUE(active_session); |
| ASSERT_TRUE(passive_session); |
| // Connectable callback will be notified at the start of each scan period. |
| EXPECT_EQ(2, connectable_count); |
| EXPECT_EQ(0, active_count); |
| EXPECT_EQ(1, passive_count); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, ScanResultUpgradesKnownBrEdrPeerToDualMode) { |
| Peer* peer = peer_cache()->NewPeer(kAddrAlias0, true); |
| ASSERT_TRUE(peer); |
| ASSERT_EQ(peer, peer_cache()->FindByAddress(kAddress0)); |
| ASSERT_EQ(TechnologyType::kClassic, peer->technology()); |
| |
| AddFakePeers(); |
| |
| discovery_manager()->set_scan_period(kTestScanPeriod); |
| |
| std::unordered_set<DeviceAddress> addresses_found; |
| LowEnergyDiscoverySession::PeerFoundCallback result_cb = [&addresses_found](const auto& peer) { |
| addresses_found.insert(peer.address()); |
| }; |
| auto session = StartDiscoverySession(); |
| session->filter()->SetGeneralDiscoveryFlags(); |
| session->SetResultCallback(std::move(result_cb)); |
| |
| RunLoopUntilIdle(); |
| |
| ASSERT_EQ(3u, addresses_found.size()); |
| EXPECT_TRUE(addresses_found.find(kAddrAlias0) != addresses_found.end()); |
| EXPECT_EQ(TechnologyType::kDualMode, peer->technology()); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, StartAndDisablePassiveScan) { |
| ASSERT_FALSE(test_device()->le_scan_state().enabled); |
| |
| auto session = StartDiscoverySession(/*active=*/false); |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(test_device()->le_scan_state().enabled); |
| EXPECT_EQ(hci::LEScanType::kPassive, test_device()->le_scan_state().scan_type); |
| EXPECT_FALSE(discovery_manager()->discovering()); |
| |
| session.reset(); |
| RunLoopUntilIdle(); |
| EXPECT_FALSE(test_device()->le_scan_state().enabled); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, StartAndDisablePassiveScanQuickly) { |
| ASSERT_FALSE(test_device()->le_scan_state().enabled); |
| |
| // Session will be destroyed in callback, stopping scan. |
| discovery_manager()->StartDiscovery(/*active=*/false, |
| [&](auto cb_session) { ZX_ASSERT(cb_session); }); |
| RunLoopUntilIdle(); |
| |
| EXPECT_FALSE(test_device()->le_scan_state().enabled); |
| EXPECT_EQ(2u, scan_states().size()); |
| |
| // This should not result in a request to stop scan because both pending requests will be |
| // processed at the same time, and second call to StartDiscovery() retains its session. |
| discovery_manager()->StartDiscovery(/*active=*/false, |
| [&](auto cb_session) { ZX_ASSERT(cb_session); }); |
| std::unique_ptr<LowEnergyDiscoverySession> session; |
| discovery_manager()->StartDiscovery(/*active=*/false, [&](auto cb_session) { |
| ZX_ASSERT(cb_session); |
| session = std::move(cb_session); |
| }); |
| RunLoopUntilIdle(); |
| EXPECT_EQ(3u, scan_states().size()); |
| |
| EXPECT_TRUE(test_device()->le_scan_state().enabled); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, |
| EnablePassiveScanDuringActiveScanAndDisableActiveScanCausesDowngrade) { |
| auto active_session = StartDiscoverySession(); |
| ASSERT_TRUE(active_session); |
| ASSERT_TRUE(test_device()->le_scan_state().enabled); |
| ASSERT_EQ(hci::LEScanType::kActive, test_device()->le_scan_state().scan_type); |
| |
| // The scan state should transition to enabled. |
| ASSERT_EQ(1u, scan_states().size()); |
| EXPECT_TRUE(scan_states()[0]); |
| |
| // Enabling passive scans should not disable the active scan. |
| auto passive_session = StartDiscoverySession(false); |
| RunLoopUntilIdle(); |
| ASSERT_EQ(hci::LEScanType::kActive, test_device()->le_scan_state().scan_type); |
| EXPECT_TRUE(test_device()->le_scan_state().enabled); |
| EXPECT_EQ(1u, scan_states().size()); |
| |
| // Stopping the active session should fall back to passive scan. |
| active_session = nullptr; |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(test_device()->le_scan_state().enabled); |
| EXPECT_EQ(hci::LEScanType::kPassive, test_device()->le_scan_state().scan_type); |
| EXPECT_THAT(scan_states(), ::testing::ElementsAre(true, false, true)); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, DisablePassiveScanDuringActiveScan) { |
| auto active_session = StartDiscoverySession(); |
| ASSERT_TRUE(active_session); |
| ASSERT_TRUE(test_device()->le_scan_state().enabled); |
| ASSERT_EQ(hci::LEScanType::kActive, test_device()->le_scan_state().scan_type); |
| |
| // The scan state should transition to enabled. |
| ASSERT_EQ(1u, scan_states().size()); |
| EXPECT_TRUE(scan_states()[0]); |
| |
| // Enabling passive scans should not disable the active scan. |
| auto passive_session = StartDiscoverySession(false); |
| RunLoopUntilIdle(); |
| ASSERT_EQ(hci::LEScanType::kActive, test_device()->le_scan_state().scan_type); |
| EXPECT_TRUE(test_device()->le_scan_state().enabled); |
| EXPECT_EQ(1u, scan_states().size()); |
| |
| // Disabling the passive scan should not disable the active scan. |
| passive_session.reset(); |
| RunLoopUntilIdle(); |
| ASSERT_EQ(hci::LEScanType::kActive, test_device()->le_scan_state().scan_type); |
| EXPECT_TRUE(test_device()->le_scan_state().enabled); |
| EXPECT_EQ(1u, scan_states().size()); |
| |
| // Stopping the active session should stop scans. |
| active_session = nullptr; |
| RunLoopUntilIdle(); |
| EXPECT_FALSE(test_device()->le_scan_state().enabled); |
| EXPECT_THAT(scan_states(), ::testing::ElementsAre(true, false)); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, StartActiveScanDuringPassiveScan) { |
| auto passive_session = StartDiscoverySession(false); |
| RunLoopUntilIdle(); |
| ASSERT_TRUE(test_device()->le_scan_state().enabled); |
| ASSERT_EQ(hci::LEScanType::kPassive, test_device()->le_scan_state().scan_type); |
| |
| // The scan state should transition to enabled. |
| ASSERT_EQ(1u, scan_states().size()); |
| EXPECT_TRUE(scan_states()[0]); |
| |
| // Starting discovery should turn off the passive scan and initiate an active |
| // scan. |
| auto active_session = StartDiscoverySession(); |
| EXPECT_TRUE(active_session); |
| EXPECT_TRUE(test_device()->le_scan_state().enabled); |
| EXPECT_EQ(hci::LEScanType::kActive, test_device()->le_scan_state().scan_type); |
| EXPECT_THAT(scan_states(), ::testing::ElementsAre(true, false, true)); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, StartActiveScanWhileStartingPassiveScan) { |
| std::unique_ptr<LowEnergyDiscoverySession> passive_session; |
| discovery_manager()->StartDiscovery(/*active=*/false, [&](auto cb_session) { |
| ZX_ASSERT(cb_session); |
| passive_session = std::move(cb_session); |
| }); |
| ASSERT_FALSE(passive_session); |
| |
| std::unique_ptr<LowEnergyDiscoverySession> active_session; |
| discovery_manager()->StartDiscovery(/*active=*/true, [&](auto cb_session) { |
| ZX_ASSERT(cb_session); |
| active_session = std::move(cb_session); |
| }); |
| ASSERT_FALSE(active_session); |
| |
| // Scan should not be enabled yet. |
| EXPECT_FALSE(test_device()->le_scan_state().enabled); |
| EXPECT_TRUE(scan_states().empty()); |
| |
| // Process all the requests. We should observe multiple state transitions: |
| // -> enabled (passive) -> disabled -> enabled (active) |
| RunLoopUntilIdle(); |
| ASSERT_TRUE(test_device()->le_scan_state().enabled); |
| EXPECT_EQ(hci::LEScanType::kActive, test_device()->le_scan_state().scan_type); |
| EXPECT_THAT(scan_states(), ::testing::ElementsAre(true, false, true)); |
| } |
| |
| // Emulate a number of connectable and non-connectable advertisers in both undirected connectable |
| // and directed connectable modes. This test is to ensure that the only peers notified during a |
| // passive scan are from connectable peers that are already in the cache. |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, |
| PeerConnectableCallbackOnlyHandlesEventsFromKnownConnectableDevices) { |
| // Address 0: undirected connectable; added to cache below |
| { |
| auto peer = std::make_unique<FakePeer>(kAddress0, /*connectable=*/true, /*scannable=*/true); |
| test_device()->AddPeer(std::move(peer)); |
| } |
| // Address 1: undirected connectable; NOT in cache |
| { |
| auto peer = std::make_unique<FakePeer>(kAddress1, /*connectable=*/true, /*scannable=*/true); |
| test_device()->AddPeer(std::move(peer)); |
| } |
| // Address 2: not connectable; added to cache below |
| { |
| auto peer = std::make_unique<FakePeer>(kAddress2, /*connectable=*/false, /*scannable=*/false); |
| test_device()->AddPeer(std::move(peer)); |
| } |
| // Address 3: not connectable but directed advertising (NOTE: although a directed advertising PDU |
| // is inherently connectable, it is theoretically possible for the peer_cache() to be in this |
| // state, even if unlikely in practice). |
| // |
| // added to cache below |
| { |
| auto peer = std::make_unique<FakePeer>(kAddress3, /*connectable=*/false, /*scannable=*/false); |
| peer->enable_directed_advertising(true); |
| test_device()->AddPeer(std::move(peer)); |
| } |
| // Address 4: directed connectable; added to cache below |
| { |
| auto peer = std::make_unique<FakePeer>(kAddress4, /*connectable=*/true, /*scannable=*/false); |
| peer->enable_directed_advertising(true); |
| test_device()->AddPeer(std::move(peer)); |
| } |
| // Address 5: directed connectable; NOT in cache |
| { |
| auto peer = std::make_unique<FakePeer>(kAddress5, /*connectable=*/true, /*scannable=*/false); |
| peer->enable_directed_advertising(true); |
| test_device()->AddPeer(std::move(peer)); |
| } |
| |
| // Add cache entries for addresses 0, 2, 3, and 4. The callback should only run for addresses 0 |
| // and 4 as the only known connectable peers. All other advertisements should be ignored. |
| auto address0_id = peer_cache()->NewPeer(kAddress0, /*connectable=*/true)->identifier(); |
| peer_cache()->NewPeer(kAddress2, /*connectable=*/false); |
| peer_cache()->NewPeer(kAddress3, /*connectable=*/false); |
| auto address4_id = peer_cache()->NewPeer(kAddress4, /*connectable=*/true)->identifier(); |
| EXPECT_EQ(4u, peer_cache()->count()); |
| |
| int count = 0; |
| discovery_manager()->set_peer_connectable_callback([&](Peer* peer) { |
| ASSERT_TRUE(peer); |
| auto id = peer->identifier(); |
| count++; |
| EXPECT_TRUE(id == address0_id || id == address4_id) << id.ToString(); |
| }); |
| auto session = StartDiscoverySession(/*active=*/false); |
| RunLoopUntilIdle(); |
| EXPECT_EQ(2, count); |
| |
| // No new remote peer cache entries should have been created. |
| EXPECT_EQ(4u, peer_cache()->count()); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, PassiveScanPeriodRestart) { |
| discovery_manager()->set_scan_period(kTestScanPeriod); |
| auto session = StartDiscoverySession(/*active=*/false); |
| |
| // The scan state should transition to enabled. |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| ASSERT_EQ(1u, scan_states().size()); |
| EXPECT_TRUE(test_device()->le_scan_state().enabled); |
| |
| // End the scan period by advancing time. |
| RunLoopFor(kTestScanPeriod); |
| EXPECT_TRUE(test_device()->le_scan_state().enabled); |
| EXPECT_EQ(hci::LEScanType::kPassive, test_device()->le_scan_state().scan_type); |
| EXPECT_THAT(scan_states(), ::testing::ElementsAre(true, false, true)); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, |
| PauseActiveDiscoveryTwiceKeepsScanningDisabledUntilBothPauseTokensDestroyed) { |
| auto session = StartDiscoverySession(); |
| EXPECT_TRUE(scan_enabled()); |
| |
| std::optional<PauseToken> pause_0 = discovery_manager()->PauseDiscovery(); |
| RunLoopUntilIdle(); |
| EXPECT_FALSE(scan_enabled()); |
| EXPECT_TRUE(discovery_manager()->discovering()); |
| |
| std::optional<PauseToken> pause_1 = discovery_manager()->PauseDiscovery(); |
| RunLoopUntilIdle(); |
| EXPECT_FALSE(scan_enabled()); |
| EXPECT_TRUE(discovery_manager()->discovering()); |
| |
| pause_0.reset(); |
| RunLoopUntilIdle(); |
| EXPECT_FALSE(scan_enabled()); |
| EXPECT_TRUE(discovery_manager()->discovering()); |
| |
| pause_1.reset(); |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| EXPECT_TRUE(discovery_manager()->discovering()); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, EnablePassiveScanAfterPausing) { |
| std::optional<PauseToken> pause = discovery_manager()->PauseDiscovery(); |
| RunLoopUntilIdle(); |
| EXPECT_FALSE(scan_enabled()); |
| |
| std::unique_ptr<LowEnergyDiscoverySession> session; |
| discovery_manager()->StartDiscovery(/*active=*/false, |
| [&](auto cb_session) { session = std::move(cb_session); }); |
| RunLoopUntilIdle(); |
| EXPECT_FALSE(scan_enabled()); |
| EXPECT_FALSE(session); |
| |
| pause.reset(); |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, StartActiveScanAfterPausing) { |
| std::optional<PauseToken> pause = discovery_manager()->PauseDiscovery(); |
| RunLoopUntilIdle(); |
| EXPECT_FALSE(scan_enabled()); |
| |
| std::unique_ptr<LowEnergyDiscoverySession> session; |
| discovery_manager()->StartDiscovery(/*active=*/true, |
| [&](auto cb_session) { session = std::move(cb_session); }); |
| RunLoopUntilIdle(); |
| EXPECT_FALSE(scan_enabled()); |
| EXPECT_FALSE(session); |
| |
| pause.reset(); |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| EXPECT_TRUE(session); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, PauseDiscoveryJustBeforeScanComplete) { |
| discovery_manager()->set_scan_period(kTestScanPeriod); |
| |
| auto session = StartDiscoverySession(); |
| EXPECT_TRUE(scan_enabled()); |
| |
| // Pause discovery in FakeController scan state callback to ensure it is called just before |
| // kComplete status is received. This will be the 2nd scan state change because it is started |
| // above and then stopped by the scan period ending below. |
| std::optional<PauseToken> pause; |
| set_scan_state_handler(2, [this, &pause]() { pause = discovery_manager()->PauseDiscovery(); }); |
| |
| RunLoopFor(kTestScanPeriod); |
| EXPECT_TRUE(pause.has_value()); |
| EXPECT_EQ(scan_states().size(), 2u); |
| EXPECT_FALSE(scan_enabled()); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, PauseDiscoveryJustBeforeScanStopped) { |
| auto session = StartDiscoverySession(); |
| EXPECT_TRUE(scan_enabled()); |
| |
| // Pause discovery in FakeController scan state callback to ensure it is called just before |
| // kStopped status is received. This will be the 2nd scan state change because it is started |
| // above and then stopped by the session being destroyed below. |
| std::optional<PauseToken> pause; |
| set_scan_state_handler(2, [this, &pause]() { pause = discovery_manager()->PauseDiscovery(); }); |
| |
| session.reset(); |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(pause.has_value()); |
| EXPECT_EQ(scan_states().size(), 2u); |
| EXPECT_FALSE(scan_enabled()); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, PauseJustBeforeScanActive) { |
| // Pause discovery in FakeController scan state callback to ensure it is called just before |
| // kActive status is received. This will be the first scan state change. |
| std::optional<PauseToken> pause; |
| set_scan_state_handler(1, [this, &pause]() { pause = discovery_manager()->PauseDiscovery(); }); |
| |
| std::unique_ptr<LowEnergyDiscoverySession> session; |
| discovery_manager()->StartDiscovery(/*active=*/true, |
| [&](auto cb_session) { session = std::move(cb_session); }); |
| |
| // The scan should be canceled. |
| RunLoopUntilIdle(); |
| EXPECT_FALSE(session); |
| EXPECT_TRUE(pause.has_value()); |
| EXPECT_EQ(scan_states().size(), 2u); |
| EXPECT_FALSE(scan_enabled()); |
| EXPECT_FALSE(discovery_manager()->discovering()); |
| |
| // Resume discovery. |
| pause.reset(); |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(session); |
| EXPECT_TRUE(scan_enabled()); |
| EXPECT_TRUE(discovery_manager()->discovering()); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, PauseJustBeforeScanPassive) { |
| // Pause discovery in FakeController scan state callback to ensure it is called just before |
| // kPassive status is received. This will be the first scan state change. |
| std::optional<PauseToken> pause; |
| set_scan_state_handler(1, [this, &pause]() { pause = discovery_manager()->PauseDiscovery(); }); |
| |
| std::unique_ptr<LowEnergyDiscoverySession> session; |
| discovery_manager()->StartDiscovery(/*active=*/false, |
| [&](auto cb_session) { session = std::move(cb_session); }); |
| |
| // The scan should be canceled. |
| RunLoopUntilIdle(); |
| EXPECT_FALSE(session); |
| EXPECT_TRUE(pause.has_value()); |
| EXPECT_EQ(scan_states().size(), 2u); |
| EXPECT_FALSE(scan_enabled()); |
| |
| // Resume scan. |
| pause.reset(); |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, |
| StartActiveScanWhilePassiveScanStoppingBetweenScanPeriods) { |
| discovery_manager()->set_scan_period(kTestScanPeriod); |
| |
| auto passive_session = StartDiscoverySession(/*active=*/false); |
| |
| std::unique_ptr<LowEnergyDiscoverySession> active_session; |
| set_scan_state_handler(2, [this, &active_session]() { |
| discovery_manager()->StartDiscovery( |
| /*active=*/true, [&active_session](auto session) { active_session = std::move(session); }); |
| }); |
| RunLoopFor(kTestScanPeriod); |
| EXPECT_TRUE(test_device()->le_scan_state().enabled); |
| EXPECT_EQ(hci::LEScanType::kActive, test_device()->le_scan_state().scan_type); |
| EXPECT_THAT(scan_states(), ::testing::ElementsAre(true, false, true)); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, StopSessionInsideOfResultCallbackDoesNotCrash) { |
| auto session = StartDiscoverySession(/*active=*/false); |
| auto result_cb = [&session](const auto& peer) { session->Stop(); }; |
| session->SetResultCallback(std::move(result_cb)); |
| RunLoopUntilIdle(); |
| |
| AddFakePeers(); |
| RunLoopUntilIdle(); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, PeerChangesFromNonConnectableToConnectable) { |
| test_device()->AddPeer(std::make_unique<FakePeer>(kAddress0, /*connectable=*/false)); |
| |
| std::unique_ptr<LowEnergyDiscoverySession> session; |
| discovery_manager()->StartDiscovery( |
| /*active=*/true, [&session](auto cb_session) { session = std::move(cb_session); }); |
| |
| RunLoopUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| auto peer = peer_cache()->FindByAddress(kAddress0); |
| ASSERT_TRUE(peer); |
| EXPECT_FALSE(peer->connectable()); |
| |
| // Make peer connectable. |
| test_device()->RemovePeer(kAddress0); |
| test_device()->AddPeer(std::make_unique<FakePeer>(kAddress0, /*connectable=*/true)); |
| |
| RunLoopUntilIdle(); |
| peer = peer_cache()->FindByAddress(kAddress0); |
| ASSERT_TRUE(peer); |
| EXPECT_TRUE(peer->connectable()); |
| |
| // Ensure peer stays connectable after non-connectable advertisement. |
| test_device()->RemovePeer(kAddress0); |
| test_device()->AddPeer(std::make_unique<FakePeer>(kAddress0, /*connectable=*/false)); |
| |
| RunLoopUntilIdle(); |
| peer = peer_cache()->FindByAddress(kAddress0); |
| ASSERT_TRUE(peer); |
| EXPECT_TRUE(peer->connectable()); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, Inspect) { |
| // Ensure node exists before testing properties. |
| ASSERT_THAT(InspectHierarchy(), AllOf(ChildrenMatch(ElementsAre(NodeMatches( |
| AllOf(NameMatches(std::string(kInspectNodeName)))))))); |
| EXPECT_THAT(InspectProperties(), |
| UnorderedElementsAre(StringIs("state", "Idle"), IntIs("paused", 0), |
| UintIs("failed_count", 0u), DoubleIs("scan_interval_ms", 0.0), |
| DoubleIs("scan_window_ms", 0.0))); |
| |
| std::unique_ptr<LowEnergyDiscoverySession> passive_session; |
| discovery_manager()->StartDiscovery(/*active=*/false, [&](auto cb_session) { |
| ZX_ASSERT(cb_session); |
| passive_session = std::move(cb_session); |
| }); |
| EXPECT_THAT(InspectProperties(), |
| ::testing::IsSupersetOf({StringIs("state", "Starting"), |
| DoubleIs("scan_interval_ms", ::testing::Gt(0.0)), |
| DoubleIs("scan_window_ms", ::testing::Gt(0.0))})); |
| |
| RunLoopUntilIdle(); |
| EXPECT_THAT(InspectProperties(), |
| ::testing::IsSupersetOf({StringIs("state", "Passive"), |
| DoubleIs("scan_interval_ms", ::testing::Gt(0.0)), |
| DoubleIs("scan_window_ms", ::testing::Gt(0.0))})); |
| |
| { |
| auto pause_token = discovery_manager()->PauseDiscovery(); |
| EXPECT_THAT(InspectProperties(), |
| ::testing::IsSupersetOf({StringIs("state", "Stopping"), IntIs("paused", 1)})); |
| } |
| |
| auto active_session = StartDiscoverySession(); |
| EXPECT_THAT(InspectProperties(), |
| ::testing::IsSupersetOf({StringIs("state", "Active"), |
| DoubleIs("scan_interval_ms", ::testing::Gt(0.0)), |
| DoubleIs("scan_window_ms", ::testing::Gt(0.0))})); |
| |
| passive_session.reset(); |
| active_session.reset(); |
| EXPECT_THAT(InspectProperties(), ::testing::IsSupersetOf({StringIs("state", "Stopping")})); |
| RunLoopUntilIdle(); |
| EXPECT_THAT(InspectProperties(), ::testing::IsSupersetOf({StringIs("state", "Idle")})); |
| |
| // Cause discovery to fail. |
| test_device()->SetDefaultResponseStatus(hci::kLESetScanEnable, |
| hci::StatusCode::kCommandDisallowed); |
| discovery_manager()->StartDiscovery(/*active=*/true, [](auto session) { EXPECT_FALSE(session); }); |
| RunLoopUntilIdle(); |
| EXPECT_THAT(InspectProperties(), ::testing::IsSupersetOf({UintIs("failed_count", 1u)})); |
| } |
| |
| TEST_F(GAP_LowEnergyDiscoveryManagerTest, SetResultCallbackIgnoresRemovedPeers) { |
| auto fake_peer_0 = std::make_unique<FakePeer>(kAddress0); |
| test_device()->AddPeer(std::move(fake_peer_0)); |
| Peer* peer_0 = peer_cache()->NewPeer(kAddress0, /*connectable=*/true); |
| PeerId peer_id_0 = peer_0->identifier(); |
| |
| auto fake_peer_1 = std::make_unique<FakePeer>(kAddress1); |
| test_device()->AddPeer(std::move(fake_peer_1)); |
| Peer* peer_1 = peer_cache()->NewPeer(kAddress1, /*connectable=*/true); |
| PeerId peer_id_1 = peer_1->identifier(); |
| |
| // Start active session so that results get cached. |
| auto session = StartDiscoverySession(/*active=*/true); |
| |
| std::unordered_map<PeerId, int> result_counts; |
| session->SetResultCallback([&](const Peer& peer) { result_counts[peer.identifier()]++; }); |
| RunLoopUntilIdle(); |
| EXPECT_EQ(result_counts[peer_id_0], 1); |
| EXPECT_EQ(result_counts[peer_id_1], 1); |
| |
| // Remove peer_0 to make the cached result stale. The result callback should not be called again |
| // for peer_0. |
| ASSERT_TRUE(peer_cache()->RemoveDisconnectedPeer(peer_0->identifier())); |
| session->SetResultCallback([&](const Peer& peer) { result_counts[peer.identifier()]++; }); |
| RunLoopUntilIdle(); |
| EXPECT_EQ(result_counts[peer_id_0], 1); |
| EXPECT_EQ(result_counts[peer_id_1], 2); |
| } |
| |
| } // namespace |
| } // namespace bt::gap |