| // 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/public/pw_bluetooth_sapphire/internal/host/gap/low_energy_discovery_manager.h" |
| |
| #include <unordered_set> |
| #include <vector> |
| |
| #include <gmock/gmock.h> |
| |
| #include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/common/advertising_data.h" |
| #include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/common/assert.h" |
| #include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/common/macros.h" |
| #include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/gap/peer.h" |
| #include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/gap/peer_cache.h" |
| #include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/hci/fake_local_address_delegate.h" |
| #include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/hci/legacy_low_energy_scanner.h" |
| #include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/testing/controller_test.h" |
| #include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/testing/fake_controller.h" |
| #include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/testing/fake_peer.h" |
| #include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/testing/inspect.h" |
| #include "src/connectivity/bluetooth/core/bt-host/public/pw_bluetooth_sapphire/internal/host/testing/test_helpers.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::FakeDispatcherControllerTest<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 uint16_t kServiceDataUuid = 0x1234; |
| |
| constexpr pw::chrono::SystemClock::duration kTestScanPeriod = |
| std::chrono::seconds(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()->GetWeakPtr(), dispatcher()); |
| discovery_manager_ = std::make_unique<LowEnergyDiscoveryManager>( |
| scanner_.get(), &peer_cache_, dispatcher()); |
| discovery_manager_->AttachInspect(inspector_.GetRoot(), kInspectNodeName); |
| |
| test_device()->set_scan_state_callback( |
| std::bind(&LowEnergyDiscoveryManagerTest::OnScanStateChanged, |
| this, |
| std::placeholders::_1)); |
| } |
| |
| 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; } |
| |
| #ifndef NINSPECT |
| 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(); |
| BT_ASSERT(children.size() == 1u); |
| return children.front().node_ptr()->take_properties(); |
| } |
| #endif // NINSPECT |
| |
| 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 == pw::bluetooth::emboss::LEScanType::ACTIVE ? "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; |
| // - Service Data UUIDs: kServiceDataUuid; |
| // - 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 StaticByteBuffer kAdvData0( |
| // Flags |
| 0x02, |
| 0x01, |
| 0x02, |
| |
| // Complete 16-bit service UUIDs |
| 0x05, |
| 0x03, |
| 0x0d, |
| 0x18, |
| 0x0f, |
| 0x18, |
| |
| // 16-bit service data UUID |
| 0x03, |
| DataType::kServiceData16Bit, |
| LowerBits(kServiceDataUuid), |
| UpperBits(kServiceDataUuid), |
| |
| // Complete local name |
| 0x09, |
| 0x09, |
| 'D', |
| 'e', |
| 'v', |
| 'i', |
| 'c', |
| 'e', |
| ' ', |
| '0'); |
| auto fake_peer = |
| std::make_unique<FakePeer>(kAddress0, dispatcher(), true, true); |
| fake_peer->set_advertising_data(kAdvData0); |
| test_device()->AddPeer(std::move(fake_peer)); |
| |
| // Peer 1 |
| const StaticByteBuffer kAdvData1( |
| // Flags |
| 0x02, |
| 0x01, |
| 0x01, |
| |
| // Complete 16-bit service UUIDs |
| 0x03, |
| 0x03, |
| 0x0d, |
| 0x18); |
| fake_peer = std::make_unique<FakePeer>(kAddress1, dispatcher(), true, true); |
| fake_peer->set_advertising_data(kAdvData1); |
| test_device()->AddPeer(std::move(fake_peer)); |
| |
| // Peer 2 |
| const StaticByteBuffer kAdvData2( |
| // Flags |
| 0x02, |
| 0x01, |
| 0x02, |
| |
| // Complete local name |
| 0x09, |
| 0x09, |
| 'D', |
| 'e', |
| 'v', |
| 'i', |
| 'c', |
| 'e', |
| ' ', |
| '2'); |
| fake_peer = |
| std::make_unique<FakePeer>(kAddress2, dispatcher(), false, false); |
| fake_peer->set_advertising_data(kAdvData2); |
| test_device()->AddPeer(std::move(fake_peer)); |
| |
| // Peer 3 |
| const StaticByteBuffer kAdvData3( |
| // Flags |
| 0x02, |
| 0x01, |
| 0x00, |
| |
| // Complete local name |
| 0x09, |
| 0x09, |
| 'D', |
| 'e', |
| 'v', |
| 'i', |
| 'c', |
| 'e', |
| ' ', |
| '3'); |
| fake_peer = |
| std::make_unique<FakePeer>(kAddress3, dispatcher(), false, false); |
| fake_peer->set_advertising_data(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) { |
| BT_ASSERT(cb_session); |
| session = std::move(cb_session); |
| }); |
| |
| RunUntilIdle(); |
| BT_ASSERT(session); |
| return session; |
| } |
| |
| private: |
| PeerCache peer_cache_{dispatcher()}; |
| hci::FakeLocalAddressDelegate fake_address_delegate_{dispatcher()}; |
| 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_; |
| |
| BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(LowEnergyDiscoveryManagerTest); |
| }; |
| |
| using GAP_LowEnergyDiscoveryManagerTest = LowEnergyDiscoveryManagerTest; |
| |
| TEST_F(LowEnergyDiscoveryManagerTest, StartDiscoveryAndStop) { |
| std::unique_ptr<LowEnergyDiscoverySession> session; |
| discovery_manager()->StartDiscovery( |
| /*active=*/true, |
| [&session](auto cb_session) { session = std::move(cb_session); }); |
| |
| RunUntilIdle(); |
| |
| // The test fixture will be notified of the change in scan state before we |
| // receive the session. |
| EXPECT_TRUE(scan_enabled()); |
| RunUntilIdle(); |
| |
| ASSERT_TRUE(session); |
| EXPECT_TRUE(session->alive()); |
| |
| session->Stop(); |
| EXPECT_FALSE(session->alive()); |
| |
| RunUntilIdle(); |
| EXPECT_FALSE(scan_enabled()); |
| } |
| |
| TEST_F(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); }); |
| |
| RunUntilIdle(); |
| |
| // The test fixture will be notified of the change in scan state before we |
| // receive the session. |
| EXPECT_TRUE(scan_enabled()); |
| RunUntilIdle(); |
| |
| ASSERT_TRUE(session); |
| EXPECT_TRUE(session->alive()); |
| |
| session = nullptr; |
| |
| RunUntilIdle(); |
| EXPECT_FALSE(scan_enabled()); |
| } |
| |
| TEST_F(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); }); |
| |
| RunUntilIdle(); |
| |
| 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(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) {}); |
| |
| RunUntilIdle(); |
| ASSERT_EQ(2u, scan_states().size()); |
| EXPECT_TRUE(scan_states()[0]); |
| EXPECT_FALSE(scan_states()[1]); |
| } |
| |
| TEST_F(LowEnergyDiscoveryManagerTest, StartDiscoveryFailure) { |
| test_device()->SetDefaultResponseStatus( |
| hci_spec::kLESetScanEnable, |
| pw::bluetooth::emboss::StatusCode::COMMAND_DISALLOWED); |
| |
| // |session| should contain nullptr. |
| discovery_manager()->StartDiscovery( |
| /*active=*/true, [](auto session) { EXPECT_FALSE(session); }); |
| |
| RunUntilIdle(); |
| EXPECT_FALSE(scan_enabled()); |
| } |
| |
| TEST_F(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); |
| |
| RunUntilIdle(); |
| 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); |
| } |
| |
| RunUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| EXPECT_EQ(kExpectedSessionCount, sessions.size()); |
| |
| // Remove one session from the list. Scan should continue. |
| sessions.pop_back(); |
| RunUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| |
| // Remove all but one session from the list. Scan should continue. |
| sessions.erase(sessions.begin() + 1, sessions.end()); |
| RunUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| EXPECT_EQ(1u, sessions.size()); |
| |
| // Remove the last session. |
| sessions.clear(); |
| RunUntilIdle(); |
| EXPECT_FALSE(scan_enabled()); |
| } |
| |
| TEST_F(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); |
| } |
| |
| RunUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| EXPECT_EQ(kExpectedSessionCount, sessions.size()); |
| |
| // Remove all sessions. This should stop the scan. |
| sessions.clear(); |
| RunUntilIdle(); |
| EXPECT_FALSE(scan_enabled()); |
| } |
| |
| TEST_F(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); |
| } |
| |
| RunUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| EXPECT_TRUE(session); |
| |
| RunUntilIdle(); |
| EXPECT_EQ(kExpectedSessionCount, cb_count); |
| EXPECT_TRUE(scan_enabled()); |
| |
| // Deleting the only remaning session should stop the scan. |
| session = nullptr; |
| RunUntilIdle(); |
| EXPECT_FALSE(scan_enabled()); |
| } |
| |
| TEST_F(LowEnergyDiscoveryManagerTest, StartDiscoveryWhilePendingStop) { |
| std::unique_ptr<LowEnergyDiscoverySession> session; |
| |
| discovery_manager()->StartDiscovery( |
| /*active=*/true, |
| [&session](auto cb_session) { session = std::move(cb_session); }); |
| |
| RunUntilIdle(); |
| 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. |
| RunUntilIdle(); |
| ASSERT_EQ(3u, scan_states().size()); |
| EXPECT_TRUE(scan_states()[0]); |
| EXPECT_FALSE(scan_states()[1]); |
| EXPECT_TRUE(scan_states()[2]); |
| } |
| |
| TEST_F(LowEnergyDiscoveryManagerTest, StartDiscoveryFailureManyPending) { |
| test_device()->SetDefaultResponseStatus( |
| hci_spec::kLESetScanEnable, |
| pw::bluetooth::emboss::StatusCode::COMMAND_DISALLOWED); |
| |
| 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); |
| } |
| |
| RunUntilIdle(); |
| EXPECT_FALSE(scan_enabled()); |
| } |
| |
| TEST_F(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. |
| RunUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| |
| // End the scan period. |
| RunFor(kTestScanPeriod); |
| ASSERT_EQ(kNumScanStates, scan_states().size()); |
| EXPECT_TRUE(scan_states()[0]); |
| EXPECT_FALSE(scan_states()[1]); |
| EXPECT_TRUE(scan_states()[2]); |
| } |
| |
| TEST_F(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_spec::kLESetScanEnable, |
| pw::bluetooth::emboss::StatusCode::COMMAND_DISALLOWED); |
| }); |
| |
| RunUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| |
| // End the scan period. The scan should not restart. |
| RunFor(kTestScanPeriod); |
| |
| ASSERT_EQ(kNumScanStates, scan_states().size()); |
| EXPECT_TRUE(scan_states()[0]); |
| EXPECT_FALSE(scan_states()[1]); |
| EXPECT_TRUE(session_error); |
| } |
| |
| TEST_F(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(); |
| }); |
| |
| RunUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| |
| // End the scan period. |
| RunFor(kTestScanPeriod); |
| EXPECT_THAT(scan_states(), ::testing::ElementsAre(true, false, true, false)); |
| } |
| |
| TEST_F(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(); |
| }); |
| |
| RunUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| |
| // End the scan period. |
| RunFor(kTestScanPeriod); |
| EXPECT_THAT(scan_states(), ::testing::ElementsAre(true, false)); |
| } |
| |
| TEST_F(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); |
| }); |
| |
| RunUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| |
| // End the scan period. |
| RunFor(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(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(std::chrono::milliseconds(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)); |
| |
| // Session 5 is interested in peers with UUID 0x180d and service data UUID |
| // 0x1234. |
| std::unordered_set<DeviceAddress> peers_session5; |
| result_cb = [&peers_session5](const auto& peer) { |
| peers_session5.insert(peer.address()); |
| }; |
| sessions.push_back(StartDiscoverySession()); |
| |
| sessions[5]->filter()->set_service_uuids({UUID(uuid)}); |
| sessions[5]->filter()->set_service_data_uuids({UUID(kServiceDataUuid)}); |
| sessions[5]->SetResultCallback(std::move(result_cb)); |
| |
| RunUntilIdle(); |
| |
| EXPECT_EQ(6u, sessions.size()); |
| |
| // 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_THAT(peers_session0, ::testing::Contains(kAddress0)); |
| EXPECT_THAT(peers_session0, ::testing::Contains(kAddress1)); |
| EXPECT_THAT(peers_session0, ::testing::Contains(kAddress2)); |
| |
| // Session 1: Should have only seen peer 1. |
| EXPECT_EQ(1u, peers_session1.size()); |
| EXPECT_THAT(peers_session1, ::testing::Contains(kAddress1)); |
| |
| // Session 2: Should have only seen peers 0 and 1 |
| EXPECT_EQ(2u, peers_session2.size()); |
| EXPECT_THAT(peers_session2, ::testing::Contains(kAddress0)); |
| EXPECT_THAT(peers_session2, ::testing::Contains(kAddress1)); |
| |
| // Session 3: Should have only seen peers 0, 2, and 3 |
| EXPECT_EQ(3u, peers_session3.size()); |
| EXPECT_THAT(peers_session3, ::testing::Contains(kAddress0)); |
| EXPECT_THAT(peers_session3, ::testing::Contains(kAddress2)); |
| EXPECT_THAT(peers_session3, ::testing::Contains(kAddress3)); |
| |
| // Session 4: Should have seen peers 2 and 3 |
| EXPECT_EQ(2u, peers_session4.size()); |
| EXPECT_THAT(peers_session4, ::testing::Contains(kAddress2)); |
| EXPECT_THAT(peers_session4, ::testing::Contains(kAddress3)); |
| |
| // Session 5: Should only see peer 0. |
| EXPECT_EQ(1u, peers_session5.size()); |
| EXPECT_THAT(peers_session5, ::testing::Contains(kAddress0)); |
| } |
| |
| TEST_F(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(std::chrono::seconds(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)); |
| |
| RunUntilIdle(); |
| 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(LowEnergyDiscoveryManagerTest, DirectedAdvertisingEventFromUnknownPeer) { |
| auto fake_peer = std::make_unique<FakePeer>(kAddress0, |
| dispatcher(), |
| /*connectable=*/true, |
| /*scannable=*/false); |
| fake_peer->set_directed_advertising_enabled(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++; }); |
| |
| RunUntilIdle(); |
| 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(LowEnergyDiscoveryManagerTest, |
| DirectedAdvertisingEventFromKnownNonConnectablePeer) { |
| auto fake_peer = std::make_unique<FakePeer>(kAddress0, |
| dispatcher(), |
| /*connectable=*/false, |
| /*scannable=*/false); |
| fake_peer->set_directed_advertising_enabled(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++; }); |
| |
| RunFor(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(LowEnergyDiscoveryManagerTest, |
| DirectedAdvertisingEventFromKnownConnectablePeer) { |
| auto fake_peer = std::make_unique<FakePeer>(kAddress0, |
| dispatcher(), |
| /*connectable=*/true, |
| /*scannable=*/false); |
| fake_peer->set_directed_advertising_enabled(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++; }); |
| |
| RunFor(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(LowEnergyDiscoveryManagerTest, |
| ScanResultUpgradesKnownBrEdrPeerToDualMode) { |
| Peer* peer = peer_cache()->NewPeer(kAddrAlias0, /*connectable=*/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)); |
| |
| RunUntilIdle(); |
| |
| ASSERT_EQ(3u, addresses_found.size()); |
| EXPECT_TRUE(addresses_found.find(kAddrAlias0) != addresses_found.end()); |
| EXPECT_EQ(TechnologyType::kDualMode, peer->technology()); |
| } |
| |
| TEST_F(LowEnergyDiscoveryManagerTest, StartAndDisablePassiveScan) { |
| ASSERT_FALSE(test_device()->le_scan_state().enabled); |
| |
| auto session = StartDiscoverySession(/*active=*/false); |
| RunUntilIdle(); |
| EXPECT_TRUE(test_device()->le_scan_state().enabled); |
| EXPECT_EQ(pw::bluetooth::emboss::LEScanType::PASSIVE, |
| test_device()->le_scan_state().scan_type); |
| EXPECT_FALSE(discovery_manager()->discovering()); |
| |
| session.reset(); |
| RunUntilIdle(); |
| EXPECT_FALSE(test_device()->le_scan_state().enabled); |
| } |
| |
| TEST_F(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) { BT_ASSERT(cb_session); }); |
| RunUntilIdle(); |
| |
| 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) { BT_ASSERT(cb_session); }); |
| std::unique_ptr<LowEnergyDiscoverySession> session; |
| discovery_manager()->StartDiscovery(/*active=*/false, [&](auto cb_session) { |
| BT_ASSERT(cb_session); |
| session = std::move(cb_session); |
| }); |
| RunUntilIdle(); |
| EXPECT_EQ(3u, scan_states().size()); |
| |
| EXPECT_TRUE(test_device()->le_scan_state().enabled); |
| } |
| |
| TEST_F(LowEnergyDiscoveryManagerTest, |
| EnablePassiveScanDuringActiveScanAndDisableActiveScanCausesDowngrade) { |
| auto active_session = StartDiscoverySession(); |
| ASSERT_TRUE(active_session); |
| ASSERT_TRUE(test_device()->le_scan_state().enabled); |
| ASSERT_EQ(pw::bluetooth::emboss::LEScanType::ACTIVE, |
| 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); |
| RunUntilIdle(); |
| ASSERT_EQ(pw::bluetooth::emboss::LEScanType::ACTIVE, |
| 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; |
| RunUntilIdle(); |
| EXPECT_TRUE(test_device()->le_scan_state().enabled); |
| EXPECT_EQ(pw::bluetooth::emboss::LEScanType::PASSIVE, |
| test_device()->le_scan_state().scan_type); |
| EXPECT_THAT(scan_states(), ::testing::ElementsAre(true, false, true)); |
| } |
| |
| TEST_F(LowEnergyDiscoveryManagerTest, DisablePassiveScanDuringActiveScan) { |
| auto active_session = StartDiscoverySession(); |
| ASSERT_TRUE(active_session); |
| ASSERT_TRUE(test_device()->le_scan_state().enabled); |
| ASSERT_EQ(pw::bluetooth::emboss::LEScanType::ACTIVE, |
| 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); |
| RunUntilIdle(); |
| ASSERT_EQ(pw::bluetooth::emboss::LEScanType::ACTIVE, |
| 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(); |
| RunUntilIdle(); |
| ASSERT_EQ(pw::bluetooth::emboss::LEScanType::ACTIVE, |
| 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; |
| RunUntilIdle(); |
| EXPECT_FALSE(test_device()->le_scan_state().enabled); |
| EXPECT_THAT(scan_states(), ::testing::ElementsAre(true, false)); |
| } |
| |
| TEST_F(LowEnergyDiscoveryManagerTest, StartActiveScanDuringPassiveScan) { |
| auto passive_session = StartDiscoverySession(false); |
| RunUntilIdle(); |
| ASSERT_TRUE(test_device()->le_scan_state().enabled); |
| ASSERT_EQ(pw::bluetooth::emboss::LEScanType::PASSIVE, |
| 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(pw::bluetooth::emboss::LEScanType::ACTIVE, |
| test_device()->le_scan_state().scan_type); |
| EXPECT_THAT(scan_states(), ::testing::ElementsAre(true, false, true)); |
| } |
| |
| TEST_F(LowEnergyDiscoveryManagerTest, StartActiveScanWhileStartingPassiveScan) { |
| std::unique_ptr<LowEnergyDiscoverySession> passive_session; |
| discovery_manager()->StartDiscovery(/*active=*/false, [&](auto cb_session) { |
| BT_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) { |
| BT_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) |
| RunUntilIdle(); |
| ASSERT_TRUE(test_device()->le_scan_state().enabled); |
| EXPECT_EQ(pw::bluetooth::emboss::LEScanType::ACTIVE, |
| 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(LowEnergyDiscoveryManagerTest, |
| PeerConnectableCallbackOnlyHandlesEventsFromKnownConnectableDevices) { |
| // Address 0: undirected connectable; added to cache below |
| { |
| auto peer = std::make_unique<FakePeer>(kAddress0, |
| dispatcher(), |
| /*connectable=*/true, |
| /*scannable=*/true); |
| test_device()->AddPeer(std::move(peer)); |
| } |
| // Address 1: undirected connectable; NOT in cache |
| { |
| auto peer = std::make_unique<FakePeer>(kAddress1, |
| dispatcher(), |
| /*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, |
| dispatcher(), |
| /*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, |
| dispatcher(), |
| /*connectable=*/false, |
| /*scannable=*/false); |
| peer->set_directed_advertising_enabled(true); |
| test_device()->AddPeer(std::move(peer)); |
| } |
| // Address 4: directed connectable; added to cache below |
| { |
| auto peer = std::make_unique<FakePeer>(kAddress4, |
| dispatcher(), |
| /*connectable=*/true, |
| /*scannable=*/false); |
| peer->set_directed_advertising_enabled(true); |
| test_device()->AddPeer(std::move(peer)); |
| } |
| // Address 5: directed connectable; NOT in cache |
| { |
| auto peer = std::make_unique<FakePeer>(kAddress5, |
| dispatcher(), |
| /*connectable=*/true, |
| /*scannable=*/false); |
| peer->set_directed_advertising_enabled(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); |
| RunUntilIdle(); |
| EXPECT_EQ(2, count); |
| |
| // No new remote peer cache entries should have been created. |
| EXPECT_EQ(4u, peer_cache()->count()); |
| } |
| |
| TEST_F(LowEnergyDiscoveryManagerTest, PassiveScanPeriodRestart) { |
| discovery_manager()->set_scan_period(kTestScanPeriod); |
| auto session = StartDiscoverySession(/*active=*/false); |
| |
| // The scan state should transition to enabled. |
| RunUntilIdle(); |
| 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. |
| RunFor(kTestScanPeriod); |
| EXPECT_TRUE(test_device()->le_scan_state().enabled); |
| EXPECT_EQ(pw::bluetooth::emboss::LEScanType::PASSIVE, |
| test_device()->le_scan_state().scan_type); |
| EXPECT_THAT(scan_states(), ::testing::ElementsAre(true, false, true)); |
| } |
| |
| TEST_F( |
| LowEnergyDiscoveryManagerTest, |
| PauseActiveDiscoveryTwiceKeepsScanningDisabledUntilBothPauseTokensDestroyed) { |
| auto session = StartDiscoverySession(); |
| EXPECT_TRUE(scan_enabled()); |
| |
| std::optional<PauseToken> pause_0 = discovery_manager()->PauseDiscovery(); |
| RunUntilIdle(); |
| EXPECT_FALSE(scan_enabled()); |
| EXPECT_TRUE(discovery_manager()->discovering()); |
| |
| std::optional<PauseToken> pause_1 = discovery_manager()->PauseDiscovery(); |
| RunUntilIdle(); |
| EXPECT_FALSE(scan_enabled()); |
| EXPECT_TRUE(discovery_manager()->discovering()); |
| |
| pause_0.reset(); |
| RunUntilIdle(); |
| EXPECT_FALSE(scan_enabled()); |
| EXPECT_TRUE(discovery_manager()->discovering()); |
| |
| pause_1.reset(); |
| RunUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| EXPECT_TRUE(discovery_manager()->discovering()); |
| } |
| |
| TEST_F(LowEnergyDiscoveryManagerTest, EnablePassiveScanAfterPausing) { |
| std::optional<PauseToken> pause = discovery_manager()->PauseDiscovery(); |
| RunUntilIdle(); |
| EXPECT_FALSE(scan_enabled()); |
| |
| std::unique_ptr<LowEnergyDiscoverySession> session; |
| discovery_manager()->StartDiscovery(/*active=*/false, [&](auto cb_session) { |
| session = std::move(cb_session); |
| }); |
| RunUntilIdle(); |
| EXPECT_FALSE(scan_enabled()); |
| EXPECT_FALSE(session); |
| |
| pause.reset(); |
| RunUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| } |
| |
| TEST_F(LowEnergyDiscoveryManagerTest, StartActiveScanAfterPausing) { |
| std::optional<PauseToken> pause = discovery_manager()->PauseDiscovery(); |
| RunUntilIdle(); |
| EXPECT_FALSE(scan_enabled()); |
| |
| std::unique_ptr<LowEnergyDiscoverySession> session; |
| discovery_manager()->StartDiscovery(/*active=*/true, [&](auto cb_session) { |
| session = std::move(cb_session); |
| }); |
| RunUntilIdle(); |
| EXPECT_FALSE(scan_enabled()); |
| EXPECT_FALSE(session); |
| |
| pause.reset(); |
| RunUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| EXPECT_TRUE(session); |
| } |
| |
| TEST_F(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(); }); |
| |
| RunFor(kTestScanPeriod); |
| EXPECT_TRUE(pause.has_value()); |
| EXPECT_EQ(scan_states().size(), 2u); |
| EXPECT_FALSE(scan_enabled()); |
| } |
| |
| TEST_F(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(); |
| RunUntilIdle(); |
| EXPECT_TRUE(pause.has_value()); |
| EXPECT_EQ(scan_states().size(), 2u); |
| EXPECT_FALSE(scan_enabled()); |
| } |
| |
| TEST_F(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. |
| RunUntilIdle(); |
| 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(); |
| RunUntilIdle(); |
| EXPECT_TRUE(session); |
| EXPECT_TRUE(scan_enabled()); |
| EXPECT_TRUE(discovery_manager()->discovering()); |
| } |
| |
| TEST_F(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. |
| RunUntilIdle(); |
| EXPECT_FALSE(session); |
| EXPECT_TRUE(pause.has_value()); |
| EXPECT_EQ(scan_states().size(), 2u); |
| EXPECT_FALSE(scan_enabled()); |
| |
| // Resume scan. |
| pause.reset(); |
| RunUntilIdle(); |
| EXPECT_TRUE(scan_enabled()); |
| } |
| |
| TEST_F(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); |
| }); |
| }); |
| RunFor(kTestScanPeriod); |
| EXPECT_TRUE(test_device()->le_scan_state().enabled); |
| EXPECT_EQ(pw::bluetooth::emboss::LEScanType::ACTIVE, |
| test_device()->le_scan_state().scan_type); |
| EXPECT_THAT(scan_states(), ::testing::ElementsAre(true, false, true)); |
| } |
| |
| TEST_F(LowEnergyDiscoveryManagerTest, |
| StopSessionInsideOfResultCallbackDoesNotCrash) { |
| auto session = StartDiscoverySession(/*active=*/false); |
| auto result_cb = [&session](const auto& peer) { session->Stop(); }; |
| session->SetResultCallback(std::move(result_cb)); |
| RunUntilIdle(); |
| |
| AddFakePeers(); |
| RunUntilIdle(); |
| } |
| |
| TEST_F(LowEnergyDiscoveryManagerTest, |
| PeerChangesFromNonConnectableToConnectable) { |
| test_device()->AddPeer(std::make_unique<FakePeer>( |
| kAddress0, dispatcher(), /*connectable=*/false)); |
| |
| std::unique_ptr<LowEnergyDiscoverySession> session; |
| discovery_manager()->StartDiscovery( |
| /*active=*/true, |
| [&session](auto cb_session) { session = std::move(cb_session); }); |
| |
| RunUntilIdle(); |
| 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, dispatcher(), /*connectable=*/true)); |
| |
| RunUntilIdle(); |
| 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, dispatcher(), /*connectable=*/false)); |
| |
| RunUntilIdle(); |
| peer = peer_cache()->FindByAddress(kAddress0); |
| ASSERT_TRUE(peer); |
| EXPECT_TRUE(peer->connectable()); |
| } |
| |
| #ifndef NINSPECT |
| TEST_F(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) { |
| BT_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))})); |
| |
| RunUntilIdle(); |
| 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")})); |
| RunUntilIdle(); |
| EXPECT_THAT(InspectProperties(), |
| ::testing::IsSupersetOf({StringIs("state", "Idle")})); |
| |
| // Cause discovery to fail. |
| test_device()->SetDefaultResponseStatus( |
| hci_spec::kLESetScanEnable, |
| pw::bluetooth::emboss::StatusCode::COMMAND_DISALLOWED); |
| discovery_manager()->StartDiscovery( |
| /*active=*/true, [](auto session) { EXPECT_FALSE(session); }); |
| RunUntilIdle(); |
| EXPECT_THAT(InspectProperties(), |
| ::testing::IsSupersetOf({UintIs("failed_count", 1u)})); |
| } |
| #endif // NINSPECT |
| |
| TEST_F(LowEnergyDiscoveryManagerTest, SetResultCallbackIgnoresRemovedPeers) { |
| auto fake_peer_0 = std::make_unique<FakePeer>(kAddress0, dispatcher()); |
| 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, dispatcher()); |
| 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()]++; }); |
| RunUntilIdle(); |
| 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()]++; }); |
| RunUntilIdle(); |
| EXPECT_EQ(result_counts[peer_id_0], 1); |
| EXPECT_EQ(result_counts[peer_id_1], 2); |
| } |
| |
| } // namespace |
| } // namespace bt::gap |