// Copyright 2023 The Pigweed Authors // // Licensed under the Apache License, Version 2.0 (the "License"); you may not // use this file except in compliance with the License. You may obtain a copy of // the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations under // the License. #include "pw_bluetooth_sapphire/internal/host/gap/bredr_connection_request.h" #include #include #include #include #include #include "pw_bluetooth_sapphire/internal/host/common/device_address.h" #include "pw_bluetooth_sapphire/internal/host/testing/controller_test.h" #include "pw_bluetooth_sapphire/internal/host/testing/inspect.h" #include "pw_bluetooth_sapphire/internal/host/testing/mock_controller.h" #include "pw_bluetooth_sapphire/internal/host/testing/test_helpers.h" #include "pw_bluetooth_sapphire/internal/host/testing/test_packets.h" namespace bt::gap { namespace { using namespace inspect::testing; const DeviceAddress kTestAddr(DeviceAddress::Type::kBREDR, {1}); const PeerId kPeerId; constexpr hci::Error RetryableError = ToResult(pw::bluetooth::emboss::StatusCode::PAGE_TIMEOUT).error_value(); const StaticByteBuffer kCreateConnectionRsp( hci_spec::kCommandStatusEventCode, // event code 0x04, pw::bluetooth::emboss::StatusCode::SUCCESS, // status 0xF0, // num_hci_command_packets LowerBits(hci_spec::kCreateConnection), UpperBits(hci_spec::kCreateConnection)); using BrEdrConnectionRequestTests = pw::async::test::FakeDispatcherFixture; TEST_F(BrEdrConnectionRequestTests, IncomingRequestStatusTracked) { // A freshly created request is not yet incoming auto req = BrEdrConnectionRequest(dispatcher(), kTestAddr, kPeerId, Peer::InitializingConnectionToken([] {})); EXPECT_FALSE(req.HasIncoming()); req.BeginIncoming(); // We should now have an incoming request, but still not an outgoing EXPECT_TRUE(req.HasIncoming()); EXPECT_FALSE(req.AwaitingOutgoing()); // A completed request is no longer incoming req.CompleteIncoming(); EXPECT_FALSE(req.HasIncoming()); } TEST_F(BrEdrConnectionRequestTests, CallbacksExecuted) { bool callback_called = false; bool token_destroyed = false; auto req = BrEdrConnectionRequest( dispatcher(), kTestAddr, kPeerId, Peer::InitializingConnectionToken( [&token_destroyed] { token_destroyed = true; }), [&callback_called](auto, auto) { callback_called = true; }); // A freshly created request with a callback is awaiting outgoing EXPECT_TRUE(req.AwaitingOutgoing()); // Notifying callbacks triggers the callback req.NotifyCallbacks(fit::ok(), [&]() { EXPECT_TRUE(token_destroyed); return nullptr; }); EXPECT_TRUE(token_destroyed); EXPECT_TRUE(callback_called); } #ifndef NINSPECT TEST_F(BrEdrConnectionRequestTests, Inspect) { // inspector must outlive request inspect::Inspector inspector; BrEdrConnectionRequest req(dispatcher(), kTestAddr, kPeerId, Peer::InitializingConnectionToken([] {}), [](auto, auto) {}); req.BeginIncoming(); req.AttachInspect(inspector.GetRoot(), "request_name"); auto hierarchy = inspect::ReadFromVmo(inspector.DuplicateVmo()); EXPECT_THAT( hierarchy.value(), ChildrenMatch(ElementsAre(NodeMatches(AllOf( NameMatches("request_name"), PropertyList(UnorderedElementsAre( StringIs("peer_id", kPeerId.ToString()), UintIs("callbacks", 1u), BoolIs("has_incoming", true), IntIs("first_create_connection_request_timestamp", -1)))))))); } #endif // NINSPECT using TestingBase = testing::FakeDispatcherControllerTest; class BrEdrConnectionRequestLoopTest : public TestingBase { protected: BrEdrConnectionRequestLoopTest() : req_(dispatcher(), kTestAddr, kPeerId, Peer::InitializingConnectionToken([] {}), [this](hci::Result<> res, BrEdrConnection* conn) { if (handler_) { handler_(res, conn); } }) { TestingBase::SetUp(); // By default, an outbound ConnectionRequest with a complete handler that // just logs the result. handler_ = [](hci::Result<> res, auto /*ignore*/) { bt_log(INFO, "gap-bredr-test", "outbound connection request complete: %s", bt_str(res)); }; } BrEdrConnectionRequest& connection_req() { return req_; } private: BrEdrConnectionRequest req_; BrEdrConnectionRequest::OnComplete handler_; }; TEST_F(BrEdrConnectionRequestLoopTest, CreateHciConnectionRequest) { EXPECT_CMD_PACKET_OUT(test_device(), testing::CreateConnectionPacket(kTestAddr), &kCreateConnectionRsp); bool failure = false; auto failure_cb = [&failure](hci::Result<> result, PeerId) { if (result.is_error()) { failure = true; } }; connection_req().CreateHciConnectionRequest( cmd_channel(), /*clock_offset=*/std::nullopt, /*page_scan_repetition_mode=*/std::nullopt, /*timeout_cb=*/[]() {}, std::move(failure_cb), dispatcher()); RunUntilIdle(); ASSERT_FALSE(failure); } TEST_F(BrEdrConnectionRequestLoopTest, ShouldntRetryBeforeFirstCreateConnection) { EXPECT_FALSE(connection_req().ShouldRetry(RetryableError)); } TEST_F(BrEdrConnectionRequestLoopTest, RetryableErrorCodeShouldRetryAfterFirstCreateConnection) { EXPECT_CMD_PACKET_OUT(test_device(), testing::CreateConnectionPacket(kTestAddr), &kCreateConnectionRsp); bool failure = false; auto failure_cb = [&failure](hci::Result<> result, PeerId) { if (result.is_error()) { failure = true; } }; connection_req().CreateHciConnectionRequest( cmd_channel(), /*clock_offset=*/std::nullopt, /*page_scan_repetition_mode=*/std::nullopt, /*timeout_cb=*/[]() {}, std::move(failure_cb), dispatcher()); RunFor(std::chrono::seconds(1)); ASSERT_FALSE(failure); EXPECT_TRUE(connection_req().ShouldRetry(RetryableError)); } TEST_F(BrEdrConnectionRequestLoopTest, ShouldntRetryWithNonRetriableErrorCode) { EXPECT_CMD_PACKET_OUT(test_device(), testing::CreateConnectionPacket(kTestAddr), &kCreateConnectionRsp); bool failure = false; auto failure_cb = [&failure](hci::Result<> result, PeerId) { if (result.is_error()) { failure = true; } }; connection_req().CreateHciConnectionRequest( cmd_channel(), /*clock_offset=*/std::nullopt, /*page_scan_repetition_mode=*/std::nullopt, /*timeout_cb=*/[]() {}, std::move(failure_cb), dispatcher()); RunFor(std::chrono::seconds(1)); ASSERT_FALSE(failure); EXPECT_FALSE(connection_req().ShouldRetry(hci::Error(HostError::kCanceled))); } TEST_F(BrEdrConnectionRequestLoopTest, ShouldntRetryAfterThirtySeconds) { EXPECT_CMD_PACKET_OUT(test_device(), testing::CreateConnectionPacket(kTestAddr), &kCreateConnectionRsp); bool failure = false; auto failure_cb = [&failure](hci::Result<> result, PeerId) { if (result.is_error()) { failure = true; } }; connection_req().CreateHciConnectionRequest( cmd_channel(), /*clock_offset=*/std::nullopt, /*page_scan_repetition_mode=*/std::nullopt, /*timeout_cb=*/[]() {}, std::move(failure_cb), dispatcher()); RunFor(std::chrono::seconds(15)); ASSERT_FALSE(failure); // Should be OK to retry after 15 seconds EXPECT_TRUE(connection_req().ShouldRetry(RetryableError)); EXPECT_CMD_PACKET_OUT(test_device(), testing::CreateConnectionPacket(kTestAddr), &kCreateConnectionRsp); connection_req().CreateHciConnectionRequest( cmd_channel(), /*clock_offset=*/std::nullopt, /*page_scan_repetition_mode=*/std::nullopt, /*timeout_cb=*/[]() {}, std::move(failure_cb), dispatcher()); // Should still be OK to retry, even though we've already retried RunFor(std::chrono::seconds(14)); EXPECT_TRUE(connection_req().ShouldRetry(RetryableError)); EXPECT_CMD_PACKET_OUT(test_device(), testing::CreateConnectionPacket(kTestAddr), &kCreateConnectionRsp); connection_req().CreateHciConnectionRequest( cmd_channel(), /*clock_offset=*/std::nullopt, /*page_scan_repetition_mode=*/std::nullopt, /*timeout_cb=*/[]() {}, std::move(failure_cb), dispatcher()); RunFor(std::chrono::seconds(1)); EXPECT_FALSE(connection_req().ShouldRetry(RetryableError)); } } // namespace } // namespace bt::gap