// Copyright 2024 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/l2cap/le_dynamic_channel.h" #include #include "pw_async/fake_dispatcher_fixture.h" #include "pw_bluetooth_sapphire/internal/host/l2cap/dynamic_channel_registry.h" #include "pw_bluetooth_sapphire/internal/host/l2cap/fake_signaling_channel.h" #include "pw_bluetooth_sapphire/internal/host/l2cap/l2cap_defs.h" #include "pw_bluetooth_sapphire/internal/host/l2cap/signaling_channel.h" #include "pw_bluetooth_sapphire/internal/host/l2cap/types.h" #include "pw_bluetooth_sapphire/internal/host/testing/test_helpers.h" #include "pw_unit_test/framework.h" namespace bt::l2cap::internal { namespace { constexpr ChannelId DynamicCid(int16_t channel_number = 0) { return kFirstDynamicChannelId + channel_number; } auto LeConnReq(const LECreditBasedConnectionRequestPayload& payload) { return StaticByteBuffer(LowerBits(payload.le_psm), UpperBits(payload.le_psm), LowerBits(payload.src_cid), UpperBits(payload.src_cid), LowerBits(payload.mtu), UpperBits(payload.mtu), LowerBits(payload.mps), UpperBits(payload.mps), LowerBits(payload.initial_credits), UpperBits(payload.initial_credits)); } auto LeConnRsp(const LECreditBasedConnectionResponsePayload& payload) { return StaticByteBuffer(LowerBits(payload.dst_cid), UpperBits(payload.dst_cid), LowerBits(payload.mtu), UpperBits(payload.mtu), LowerBits(payload.mps), UpperBits(payload.mps), LowerBits(payload.initial_credits), UpperBits(payload.initial_credits), LowerBits(static_cast(payload.result)), UpperBits(static_cast(payload.result))); } auto DisconnReq(const DisconnectionRequestPayload& payload) { return StaticByteBuffer(LowerBits(payload.dst_cid), UpperBits(payload.dst_cid), LowerBits(payload.src_cid), UpperBits(payload.src_cid)); } auto DisconnRsp(const DisconnectionResponsePayload& payload) { return StaticByteBuffer(LowerBits(payload.dst_cid), UpperBits(payload.dst_cid), LowerBits(payload.src_cid), UpperBits(payload.src_cid)); } class LeDynamicChannelTest : public pw::async::test::FakeDispatcherFixture { public: LeDynamicChannelTest() = default; ~LeDynamicChannelTest() override = default; protected: // Import types for brevity. using DynamicChannelCallback = DynamicChannelRegistry::DynamicChannelCallback; using ServiceRequestCallback = DynamicChannelRegistry::ServiceRequestCallback; // TestLoopFixture overrides void SetUp() override { channel_close_cb_ = nullptr; inbound_open_cb_ = nullptr; signaling_channel_ = std::make_unique(dispatcher()); registry_ = std::make_unique( sig(), fit::bind_member<&LeDynamicChannelTest::OnChannelClose>(this), fit::bind_member<&LeDynamicChannelTest::OnServiceRequest>(this), /*random_channel_ids=*/false); } void TearDown() override { RunUntilIdle(); services_.clear(); registry_ = nullptr; signaling_channel_ = nullptr; inbound_open_cb_ = nullptr; channel_close_cb_ = nullptr; } const DynamicChannel* DoOpenOutbound( const LECreditBasedConnectionRequestPayload& request, const LECreditBasedConnectionResponsePayload& response, ChannelParameters params, SignalingChannel::Status response_status = SignalingChannel::Status::kSuccess) { auto req = LeConnReq(request); auto rsp = LeConnRsp(response); EXPECT_OUTBOUND_REQ(*sig(), kLECreditBasedConnectionRequest, req.view(), {response_status, rsp.view()}); auto channel = std::make_shared>(); registry()->OpenOutbound( request.le_psm, params, [weak = std::weak_ptr(channel)](auto chan) { if (auto channel = weak.lock()) { *channel = chan; } }); RunUntilIdle(); if (HasFatalFailure()) { return nullptr; } // We should always get a result, whether it is a success or failure. EXPECT_TRUE(*channel); if (!*channel) { return nullptr; } return **channel; } bool DoOpenInbound( const LECreditBasedConnectionRequestPayload& request, const LECreditBasedConnectionResponsePayload& expected_response) { auto req = LeConnReq(request); auto rsp = LeConnRsp(expected_response); sig()->ReceiveExpect( kLECreditBasedConnectionRequest, req.view(), rsp.view()); RunUntilIdle(); if (HasFatalFailure()) { return false; } return true; } bool DoCloseOutbound(const DisconnectionRequestPayload& request, const DisconnectionResponsePayload& response) { auto req = DisconnReq(request); auto rsp = DisconnRsp(response); EXPECT_OUTBOUND_REQ(*sig(), kDisconnectionRequest, req.view(), {SignalingChannel::Status::kSuccess, rsp.view()}); bool channel_close_cb_called = false; registry()->CloseChannel(request.src_cid, [&] { channel_close_cb_called = true; }); RunUntilIdle(); if (HasFatalFailure()) { return false; } return channel_close_cb_called; } bool DoCloseOutbound(const DisconnectionRequestPayload& request) { DisconnectionResponsePayload response = {request.dst_cid, request.src_cid}; return DoCloseOutbound(request, response); } testing::FakeSignalingChannel* sig() const { return signaling_channel_.get(); } LeDynamicChannelRegistry* registry() const { return registry_.get(); } void set_channel_close_cb(DynamicChannelCallback close_cb) { channel_close_cb_ = std::move(close_cb); } bool RegisterService(Psm psm, ChannelParameters params) { auto [_, result] = services_.insert( {psm, DynamicChannelRegistry::ServiceInfo{ params, fit::bind_member(this, &LeDynamicChannelTest::OnInboundChannel)}}); return result; } void SetNextInboundChannel(std::optional* out) { ASSERT_EQ(inbound_open_cb_, nullptr); inbound_open_cb_ = [out](const DynamicChannel* channel) { *out = channel; }; } private: void OnChannelClose(const DynamicChannel* channel) { if (channel_close_cb_) { channel_close_cb_(channel); } } void OnInboundChannel(const DynamicChannel* channel) { if (inbound_open_cb_) { std::move(inbound_open_cb_)(channel); } } std::optional OnServiceRequest(Psm psm) { auto iter = services_.find(psm); if (iter == services_.end()) { return std::nullopt; } return DynamicChannelRegistry::ServiceInfo(iter->second.channel_params, iter->second.channel_cb.share()); } DynamicChannelCallback channel_close_cb_; DynamicChannelCallback inbound_open_cb_; std::unordered_map services_; std::unique_ptr signaling_channel_; std::unique_ptr registry_; BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(LeDynamicChannelTest); }; TEST_F(LeDynamicChannelTest, StateToString) { constexpr std::array, 4> kCases{{ {{.exchanged_connection_request = false, .exchanged_connection_response = false, .exchanged_disconnect_request = false}, "{exchanged_connection_request: false, " "exchanged_connection_response: false, " "exchanged_disconnect_request: false}"}, {{.exchanged_connection_request = false, .exchanged_connection_response = false, .exchanged_disconnect_request = true}, "{exchanged_connection_request: false, " "exchanged_connection_response: false, " "exchanged_disconnect_request: true}"}, {{.exchanged_connection_request = false, .exchanged_connection_response = true, .exchanged_disconnect_request = false}, "{exchanged_connection_request: false, " "exchanged_connection_response: true, " "exchanged_disconnect_request: false}"}, {{.exchanged_connection_request = true, .exchanged_connection_response = false, .exchanged_disconnect_request = false}, "{exchanged_connection_request: true, " "exchanged_connection_response: false, " "exchanged_disconnect_request: false}"}, }}; for (const auto& [state, expected] : kCases) { EXPECT_EQ(state.ToString(), expected); } } TEST_F(LeDynamicChannelTest, OpenOutboundDefaultParametersCloseOutbound) { static constexpr auto kMode = CreditBasedFlowControlMode::kLeCreditBasedFlowControl; static constexpr ChannelParameters kChannelParams{ .mode = kMode, .max_rx_sdu_size = std::nullopt, .flush_timeout = std::nullopt, }; static constexpr LECreditBasedConnectionRequestPayload kLeConnReqPayload{ .le_psm = 0x0015, .src_cid = DynamicCid(), .mtu = kDefaultMTU, .mps = kMaxInboundPduPayloadSize, .initial_credits = 0x0000, }; static constexpr LECreditBasedConnectionResponsePayload kLeConnRspPayload{ .dst_cid = DynamicCid(), .mtu = 0x0064, .mps = 0x0032, .initial_credits = 0x0050, .result = LECreditBasedConnectionResult::kSuccess, }; static constexpr DisconnectionRequestPayload kDisconnReqPayload{ .dst_cid = kLeConnRspPayload.dst_cid, .src_cid = kLeConnReqPayload.src_cid, }; auto chan = DoOpenOutbound(kLeConnReqPayload, kLeConnRspPayload, kChannelParams); ASSERT_TRUE(chan); EXPECT_TRUE(chan->IsOpen()); EXPECT_TRUE(chan->IsConnected()); EXPECT_EQ(kLeConnReqPayload.src_cid, chan->local_cid()); EXPECT_EQ(kLeConnRspPayload.dst_cid, chan->remote_cid()); ChannelInfo expected_info = ChannelInfo::MakeCreditBasedFlowControlMode( kMode, kDefaultMTU, kLeConnRspPayload.mtu, kLeConnRspPayload.mps, kLeConnRspPayload.initial_credits); ChannelInfo actual_info = chan->info(); EXPECT_EQ(expected_info.mode, actual_info.mode); EXPECT_EQ(expected_info.max_rx_sdu_size, actual_info.max_rx_sdu_size); EXPECT_EQ(expected_info.max_tx_sdu_size, actual_info.max_tx_sdu_size); EXPECT_EQ(expected_info.n_frames_in_tx_window, actual_info.n_frames_in_tx_window); EXPECT_EQ(expected_info.max_transmissions, actual_info.max_transmissions); EXPECT_EQ(expected_info.max_tx_pdu_payload_size, actual_info.max_tx_pdu_payload_size); EXPECT_EQ(expected_info.psm, actual_info.psm); EXPECT_EQ(expected_info.flush_timeout, actual_info.flush_timeout); EXPECT_EQ(expected_info.remote_initial_credits, actual_info.remote_initial_credits); EXPECT_TRUE(DoCloseOutbound(kDisconnReqPayload)); } TEST_F(LeDynamicChannelTest, OpenOutboundSpecificParametersCloseOutbound) { static constexpr auto kMode = CreditBasedFlowControlMode::kLeCreditBasedFlowControl; static constexpr ChannelParameters kChannelParams{ .mode = kMode, .max_rx_sdu_size = 0x0023, .flush_timeout = std::nullopt, }; static constexpr LECreditBasedConnectionRequestPayload kLeConnReqPayload{ .le_psm = 0x0015, .src_cid = DynamicCid(), .mtu = kChannelParams.max_rx_sdu_size.value_or(0), .mps = kMaxInboundPduPayloadSize, .initial_credits = 0x0000, }; static constexpr LECreditBasedConnectionResponsePayload kLeConnRspPayload{ .dst_cid = DynamicCid(), .mtu = 0x0064, .mps = 0x0032, .initial_credits = 0x0050, .result = LECreditBasedConnectionResult::kSuccess, }; static constexpr DisconnectionRequestPayload kDisconnReqPayload{ .dst_cid = kLeConnRspPayload.dst_cid, .src_cid = kLeConnReqPayload.src_cid, }; auto chan = DoOpenOutbound(kLeConnReqPayload, kLeConnRspPayload, kChannelParams); ASSERT_TRUE(chan); EXPECT_TRUE(chan->IsOpen()); EXPECT_TRUE(chan->IsConnected()); EXPECT_EQ(kLeConnReqPayload.src_cid, chan->local_cid()); EXPECT_EQ(kLeConnRspPayload.dst_cid, chan->remote_cid()); ChannelInfo expected_info = ChannelInfo::MakeCreditBasedFlowControlMode( kMode, *kChannelParams.max_rx_sdu_size, kLeConnRspPayload.mtu, kLeConnRspPayload.mps, kLeConnRspPayload.initial_credits); ChannelInfo actual_info = chan->info(); EXPECT_EQ(expected_info.mode, actual_info.mode); EXPECT_EQ(expected_info.max_rx_sdu_size, actual_info.max_rx_sdu_size); EXPECT_EQ(expected_info.max_tx_sdu_size, actual_info.max_tx_sdu_size); EXPECT_EQ(expected_info.n_frames_in_tx_window, actual_info.n_frames_in_tx_window); EXPECT_EQ(expected_info.max_transmissions, actual_info.max_transmissions); EXPECT_EQ(expected_info.max_tx_pdu_payload_size, actual_info.max_tx_pdu_payload_size); EXPECT_EQ(expected_info.psm, actual_info.psm); EXPECT_EQ(expected_info.flush_timeout, actual_info.flush_timeout); EXPECT_EQ(expected_info.remote_initial_credits, actual_info.remote_initial_credits); EXPECT_TRUE(DoCloseOutbound(kDisconnReqPayload)); } TEST_F(LeDynamicChannelTest, OpenOutboundBadChannel) { static constexpr auto kMode = CreditBasedFlowControlMode::kLeCreditBasedFlowControl; static constexpr ChannelParameters kChannelParams{ .mode = kMode, .max_rx_sdu_size = std::nullopt, .flush_timeout = std::nullopt, }; static constexpr LECreditBasedConnectionRequestPayload kLeConnReqPayload{ .le_psm = 0x0015, .src_cid = DynamicCid(), .mtu = kDefaultMTU, .mps = kMaxInboundPduPayloadSize, .initial_credits = 0x0000, }; static constexpr LECreditBasedConnectionResponsePayload kLeConnRspPayload{ .dst_cid = DynamicCid(-1), .mtu = 0x0064, .mps = 0x0032, .initial_credits = 0x0050, .result = LECreditBasedConnectionResult::kSuccess, }; auto chan = DoOpenOutbound(kLeConnReqPayload, kLeConnRspPayload, kChannelParams); EXPECT_FALSE(chan); } TEST_F(LeDynamicChannelTest, OpenOutboundRejected) { static constexpr auto kMode = CreditBasedFlowControlMode::kLeCreditBasedFlowControl; static constexpr ChannelParameters kChannelParams{ .mode = kMode, .max_rx_sdu_size = std::nullopt, .flush_timeout = std::nullopt, }; static constexpr LECreditBasedConnectionRequestPayload kLeConnReqPayload{ .le_psm = 0x0015, .src_cid = DynamicCid(), .mtu = kDefaultMTU, .mps = kMaxInboundPduPayloadSize, .initial_credits = 0x0000, }; static constexpr LECreditBasedConnectionResponsePayload kLeConnRspPayload{ .dst_cid = DynamicCid(), .mtu = 0x0064, .mps = 0x0032, .initial_credits = 0x0050, .result = LECreditBasedConnectionResult::kSuccess, }; auto chan = DoOpenOutbound(kLeConnReqPayload, kLeConnRspPayload, kChannelParams, SignalingChannel::Status::kReject); EXPECT_FALSE(chan); } TEST_F(LeDynamicChannelTest, OpenOutboundPsmNotSupported) { static constexpr auto kMode = CreditBasedFlowControlMode::kLeCreditBasedFlowControl; static constexpr ChannelParameters kChannelParams{ .mode = kMode, .max_rx_sdu_size = std::nullopt, .flush_timeout = std::nullopt, }; static constexpr LECreditBasedConnectionRequestPayload kLeConnReqPayload{ .le_psm = 0x0015, .src_cid = DynamicCid(), .mtu = kDefaultMTU, .mps = kMaxInboundPduPayloadSize, .initial_credits = 0x0000, }; static constexpr LECreditBasedConnectionResponsePayload kLeConnRspPayload{ .dst_cid = DynamicCid(), .mtu = 0x0064, .mps = 0x0032, .initial_credits = 0x0050, .result = LECreditBasedConnectionResult::kPsmNotSupported, }; auto chan = DoOpenOutbound(kLeConnReqPayload, kLeConnRspPayload, kChannelParams); EXPECT_FALSE(chan); } TEST_F(LeDynamicChannelTest, OpenInboundDefaultParamsCloseInbound) { static constexpr auto kMode = CreditBasedFlowControlMode::kLeCreditBasedFlowControl; static constexpr Psm kPsm = 0x0015; static constexpr ChannelParameters kChannelParams{ .mode = kMode, .max_rx_sdu_size = std::nullopt, .flush_timeout = std::nullopt, }; static constexpr LECreditBasedConnectionRequestPayload kLeConnReqPayload{ .le_psm = kPsm, .src_cid = DynamicCid(), .mtu = 0x0064, .mps = 0x0032, .initial_credits = 0x0032, }; static constexpr LECreditBasedConnectionResponsePayload kLeConnRspPayload{ .dst_cid = DynamicCid(), .mtu = kDefaultMTU, .mps = kMaxInboundPduPayloadSize, .initial_credits = 0x0000, .result = LECreditBasedConnectionResult::kSuccess, }; static constexpr DisconnectionRequestPayload kDisconnReqPayload{ .dst_cid = kLeConnRspPayload.dst_cid, .src_cid = kLeConnReqPayload.src_cid, }; std::optional channel; SetNextInboundChannel(&channel); ASSERT_TRUE(RegisterService(kPsm, kChannelParams)); EXPECT_TRUE(DoOpenInbound(kLeConnReqPayload, kLeConnRspPayload)); ASSERT_TRUE(channel); ASSERT_TRUE(*channel); EXPECT_TRUE((*channel)->IsOpen()); EXPECT_TRUE((*channel)->IsConnected()); EXPECT_EQ(kLeConnReqPayload.src_cid, (*channel)->remote_cid()); EXPECT_EQ(kLeConnRspPayload.dst_cid, (*channel)->local_cid()); ChannelInfo expected_info = ChannelInfo::MakeCreditBasedFlowControlMode( kMode, kLeConnRspPayload.mtu, kLeConnReqPayload.mtu, kLeConnReqPayload.mps, kLeConnReqPayload.initial_credits); ChannelInfo actual_info = (*channel)->info(); EXPECT_EQ(expected_info.mode, actual_info.mode); EXPECT_EQ(expected_info.max_rx_sdu_size, actual_info.max_rx_sdu_size); EXPECT_EQ(expected_info.max_tx_sdu_size, actual_info.max_tx_sdu_size); EXPECT_EQ(expected_info.n_frames_in_tx_window, actual_info.n_frames_in_tx_window); EXPECT_EQ(expected_info.max_transmissions, actual_info.max_transmissions); EXPECT_EQ(expected_info.max_tx_pdu_payload_size, actual_info.max_tx_pdu_payload_size); EXPECT_EQ(expected_info.psm, actual_info.psm); EXPECT_EQ(expected_info.flush_timeout, actual_info.flush_timeout); EXPECT_EQ(expected_info.remote_initial_credits, actual_info.remote_initial_credits); EXPECT_TRUE(DoCloseOutbound(kDisconnReqPayload)); } TEST_F(LeDynamicChannelTest, OpenInboundUnsupportedPsm) { static constexpr Psm kPsm = 0x0015; static constexpr LECreditBasedConnectionRequestPayload kLeConnReqPayload{ .le_psm = kPsm, .src_cid = DynamicCid(), .mtu = 0x0064, .mps = 0x0032, .initial_credits = 0x0032, }; static constexpr LECreditBasedConnectionResponsePayload kLeConnRspPayload{ .dst_cid = kInvalidChannelId, .mtu = 0, .mps = 0, .initial_credits = 0, .result = LECreditBasedConnectionResult::kPsmNotSupported, }; std::optional channel; SetNextInboundChannel(&channel); EXPECT_TRUE(DoOpenInbound(kLeConnReqPayload, kLeConnRspPayload)); EXPECT_FALSE(channel); } TEST_F(LeDynamicChannelTest, OpenInboundBadChannel) { static constexpr LECreditBasedConnectionRequestPayload kLeConnReqPayload{ .le_psm = 0x0015, .src_cid = DynamicCid(-1), .mtu = 0x0064, .mps = 0x0032, .initial_credits = 0x0050, }; static constexpr LECreditBasedConnectionResponsePayload kLeConnRspPayload{ .dst_cid = kInvalidChannelId, .mtu = 0, .mps = 0, .initial_credits = 0, .result = LECreditBasedConnectionResult::kInvalidSourceCID, }; std::optional channel; SetNextInboundChannel(&channel); EXPECT_TRUE(DoOpenInbound(kLeConnReqPayload, kLeConnRspPayload)); EXPECT_FALSE(channel); } } // namespace } // namespace bt::l2cap::internal