blob: f7ab2767da6fb0ffda4e41f93cf310dde3fc74e1 [file] [edit]
// 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 <cstdint>
#include <mutex>
#include "pw_allocator/libc_allocator.h"
#include "pw_allocator/testing.h"
#include "pw_assert/check.h"
#include "pw_bluetooth_proxy/h4_packet.h"
#include "pw_bluetooth_proxy/l2cap_channel_common.h"
#include "pw_bluetooth_proxy_private/test_utils.h"
#include "pw_containers/flat_map.h"
#include "pw_multibuf/from_span.h"
#include "pw_multibuf/multibuf.h"
#include "pw_sync/mutex.h"
#include "pw_thread/test_thread_context.h"
#include "pw_thread/thread.h"
#include "pw_unit_test/framework.h"
namespace pw::bluetooth::proxy {
namespace {
constexpr uint16_t kConnectionHandle = 123;
// ########## L2capCocTest
class L2capCocTest : public ProxyHostTest {};
TEST_F(L2capCocTest, CannotCreateChannelWithInvalidArgs) {
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[](H4PacketWithHci&&) {});
pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn(
[](H4PacketWithH4&&) {});
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/0,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
// Connection handle too large by 1.
EXPECT_EQ(BuildCocWithResult(proxy, CocParameters{.handle = 0x0FFF}).status(),
PW_STATUS_INVALID_ARGUMENT);
// Local CID invalid (0).
EXPECT_EQ(BuildCocWithResult(proxy, CocParameters{.local_cid = 0}).status(),
PW_STATUS_INVALID_ARGUMENT);
// Remote CID invalid (0).
EXPECT_EQ(BuildCocWithResult(proxy, CocParameters{.remote_cid = 0}).status(),
PW_STATUS_INVALID_ARGUMENT);
}
TEST_F(L2capCocTest, CanDeleteChannelInEventCallback) {
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn(
[]([[maybe_unused]] H4PacketWithH4&& packet) {});
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/1,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
// Allow proxy to reserve 1 credit.
PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 1));
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
std::optional<L2capCoc> channel;
channel = BuildCoc(
proxy,
CocParameters{
.handle = kConnectionHandle,
.tx_credits = 1,
.event_fn = [&channel]([[maybe_unused]] L2capChannelEvent event) {
channel.reset();
}});
channel->Close();
EXPECT_FALSE(channel.has_value());
}
// ########## L2capCocWriteTest
class L2capCocWriteTest : public ProxyHostTest {};
// Size of sdu_length field in first K-frames.
constexpr uint8_t kSduLengthFieldSize = 2;
// Size of a K-Frame over Acl packet with no payload.
constexpr uint8_t kFirstKFrameOverAclMinSize =
emboss::AclDataFrameHeader::IntrinsicSizeInBytes() +
emboss::FirstKFrame::MinSizeInBytes();
TEST_F(L2capCocWriteTest, BasicWrite) {
struct {
int sends_called = 0;
// First four bits 0x0 encode PB & BC flags
uint16_t handle = 0x0ACB;
// Length of L2CAP PDU
uint16_t acl_data_total_length = 0x0009;
// L2CAP header PDU length field
uint16_t pdu_length = 0x0005;
// Random CID
uint16_t channel_id = 0x1234;
// Length of L2CAP SDU
uint16_t sdu_length = 0x0003;
// L2CAP information payload
std::array<uint8_t, 3> payload = {0xAB, 0xCD, 0xEF};
// Built from the preceding values in little endian order (except payload in
// big endian).
std::array<uint8_t, 13> expected_hci_packet = {0xCB,
0x0A,
0x09,
0x00,
0x05,
0x00,
0x34,
0x12,
0x03,
0x00,
0xAB,
0xCD,
0xEF};
} capture;
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn(
[&capture](H4PacketWithH4&& packet) {
++capture.sends_called;
EXPECT_EQ(packet.GetH4Type(), emboss::H4PacketType::ACL_DATA);
EXPECT_EQ(packet.GetHciSpan().size(),
capture.expected_hci_packet.size());
EXPECT_TRUE(std::equal(packet.GetHciSpan().begin(),
packet.GetHciSpan().end(),
capture.expected_hci_packet.begin(),
capture.expected_hci_packet.end()));
PW_TEST_ASSERT_OK_AND_ASSIGN(
auto acl,
MakeEmbossView<emboss::AclDataFrameView>(packet.GetHciSpan()));
EXPECT_EQ(acl.header().handle().Read(), capture.handle);
EXPECT_EQ(acl.header().packet_boundary_flag().Read(),
emboss::AclDataPacketBoundaryFlag::FIRST_NON_FLUSHABLE);
EXPECT_EQ(acl.header().broadcast_flag().Read(),
emboss::AclDataPacketBroadcastFlag::POINT_TO_POINT);
EXPECT_EQ(acl.data_total_length().Read(),
capture.acl_data_total_length);
emboss::FirstKFrameView kframe = emboss::MakeFirstKFrameView(
acl.payload().BackingStorage().data(), acl.SizeInBytes());
EXPECT_EQ(kframe.pdu_length().Read(), capture.pdu_length);
EXPECT_EQ(kframe.channel_id().Read(), capture.channel_id);
EXPECT_EQ(kframe.sdu_length().Read(), capture.sdu_length);
for (size_t i = 0; i < 3; ++i) {
EXPECT_EQ(kframe.payload()[i].Read(), capture.payload[i]);
}
});
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/1,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
// Allow proxy to reserve 1 credit.
PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 1));
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, capture.handle, emboss::StatusCode::SUCCESS));
L2capCoc channel = BuildCoc(proxy,
CocParameters{.handle = capture.handle,
.remote_cid = capture.channel_id});
auto mbuf_result = multibuf::FromSpan(
*allocator, as_writable_bytes(span(capture.payload)), [](ByteSpan) {});
ASSERT_TRUE(mbuf_result.has_value());
multibuf::MultiBuf mbuf = std::move(*mbuf_result);
PW_TEST_EXPECT_OK(channel.Write(std::move(mbuf)).status);
RunDispatcher();
EXPECT_EQ(capture.sends_called, 1);
}
TEST_F(L2capCocWriteTest, ErrorOnWriteToStoppedChannel) {
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn(
[]([[maybe_unused]] H4PacketWithH4&& packet) {});
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/1,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
// Allow proxy to reserve 1 credit.
PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 1));
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
L2capCoc channel = BuildCoc(
proxy,
CocParameters{.handle = kConnectionHandle,
.tx_credits = 1,
.event_fn = []([[maybe_unused]] L2capChannelEvent event) {
FAIL();
}});
channel.Stop();
EXPECT_EQ(channel.IsWriteAvailable(), PW_STATUS_FAILED_PRECONDITION);
multibuf::MultiBuf empty = MakeEmptyMultiBuf();
EXPECT_EQ(channel.Write(std::move(empty)).status,
Status::FailedPrecondition());
RunDispatcher();
}
TEST_F(L2capCocWriteTest, WriteExceedingMtuFails) {
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn(
[]([[maybe_unused]] H4PacketWithH4&& packet) { FAIL(); });
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/1,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
// Allow proxy to reserve 1 credit.
PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 1));
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
// Payload size exceeds MTU.
L2capCoc small_mtu_channel = BuildCoc(proxy, CocParameters{.tx_mtu = 1});
std::array<uint8_t, 24> payload;
auto mbuf_result = multibuf::FromSpan(
*allocator, as_writable_bytes(span(payload)), [](ByteSpan) {});
ASSERT_TRUE(mbuf_result.has_value());
multibuf::MultiBuf mbuf = std::move(*mbuf_result);
EXPECT_EQ(small_mtu_channel.Write(std::move(mbuf)).status,
Status::InvalidArgument());
RunDispatcher();
}
TEST_F(L2capCocWriteTest, MultipleWritesSameChannel) {
struct {
int sends_called = 0;
std::array<uint8_t, 3> payload = {0xAB, 0xCD, 0xEF};
} capture;
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn(
[&capture](H4PacketWithH4&& packet) {
++capture.sends_called;
PW_TEST_ASSERT_OK_AND_ASSIGN(
auto acl,
MakeEmbossView<emboss::AclDataFrameView>(packet.GetHciSpan()));
emboss::FirstKFrameView kframe = emboss::MakeFirstKFrameView(
acl.payload().BackingStorage().data(), acl.SizeInBytes());
for (size_t i = 0; i < 3; ++i) {
EXPECT_EQ(kframe.payload()[i].Read(), capture.payload[i]);
}
});
auto* allocator = GetProxyHostAllocator();
uint16_t num_writes = 5;
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/num_writes,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(
proxy,
/*num_credits_to_reserve=*/num_writes));
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
L2capCoc channel = BuildCoc(proxy, CocParameters{.tx_credits = num_writes});
for (int i = 0; i < num_writes; ++i) {
auto mbuf_result = multibuf::FromSpan(
*allocator, as_writable_bytes(span(capture.payload)), [](ByteSpan) {});
ASSERT_TRUE(mbuf_result.has_value());
multibuf::MultiBuf mbuf = std::move(*mbuf_result);
PW_TEST_EXPECT_OK(channel.Write(std::move(mbuf)).status);
RunDispatcher();
std::for_each(capture.payload.begin(),
capture.payload.end(),
[](uint8_t& byte) { ++byte; });
}
EXPECT_EQ(capture.sends_called, num_writes);
}
// Verify we get unavailable when send queue is full due to running out
// of ACL credits. And that it reports available again once send queue is no
// longer full.
// TODO: https://pwbug.dev/380299794 - Add equivalent test for other channel
// types.
TEST_F(L2capCocWriteTest, FlowControlDueToAclCredits) {
uint16_t kHandle = 123;
// We will send enough packets to use up ACL LE credits and to fill the
// queue. And then send one more to verify we get an unavailable.
const uint16_t kAclLeCredits = 2;
const uint16_t kExpectedSuccessfulWrites =
kAclLeCredits + L2capChannel::QueueCapacity() - NumBufferedPayloads();
// Set plenty of kL2capTxCredits to ensure that isn't the bottleneck.
const uint16_t kL2capTxCredits = kExpectedSuccessfulWrites + 1;
struct {
int write_available_events = 0;
} capture;
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn(
[]([[maybe_unused]] H4PacketWithH4&& packet) {});
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/kAclLeCredits,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(
proxy,
/*num_credits_to_reserve=*/kAclLeCredits));
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
ChannelEventCallback&& kEventFn{[&capture](L2capChannelEvent event) {
if (event == L2capChannelEvent::kWriteAvailable) {
capture.write_available_events++;
}
}};
L2capCoc channel = BuildCoc(proxy,
CocParameters{.handle = kHandle,
.tx_credits = kL2capTxCredits,
.event_fn = std::move(kEventFn)});
// Use up the ACL credits and fill up the send queue.
for (int i = 0; i < kExpectedSuccessfulWrites; ++i) {
EXPECT_EQ(channel.IsWriteAvailable(), PW_STATUS_OK);
multibuf::MultiBuf empty = MakeEmptyMultiBuf();
EXPECT_EQ(channel.Write(std::move(empty)).status, PW_STATUS_OK);
RunDispatcher();
}
EXPECT_EQ(0, capture.write_available_events);
// Send queue is full, so Write should get unavailable.
multibuf::MultiBuf empty1 = MakeEmptyMultiBuf();
EXPECT_EQ(channel.Write(std::move(empty1)).status, PW_STATUS_UNAVAILABLE);
RunDispatcher();
// Release a ACL credit, so even should trigger and write should be available
// again.
EXPECT_EQ(0, capture.write_available_events);
PW_TEST_EXPECT_OK(SendNumberOfCompletedPackets(proxy, {{kHandle, 1}}));
RunDispatcher();
EXPECT_EQ(1, capture.write_available_events);
EXPECT_EQ(channel.IsWriteAvailable(), PW_STATUS_OK);
multibuf::MultiBuf empty2 = MakeEmptyMultiBuf();
EXPECT_EQ(channel.Write(std::move(empty2)).status, PW_STATUS_OK);
RunDispatcher();
// Verify event on just IsWriteAvailable
EXPECT_EQ(channel.IsWriteAvailable(), PW_STATUS_UNAVAILABLE);
PW_TEST_EXPECT_OK(SendNumberOfCompletedPackets(proxy, {{kHandle, 1}}));
RunDispatcher();
EXPECT_EQ(2, capture.write_available_events);
}
// Verify we get unavailable when send queue is full due to running out of L2cap
// CoC credit.
// TODO: https://pwbug.dev/380299794 - Add equivalent test for other channel
// types (where appropriate).
TEST_F(L2capCocWriteTest, UnavailableWhenSendQueueIsFullDueToL2capCocCredits) {
// We will send enough packets to use up L2CAP CoC credits and to fill the
// queue. And then send one more to verify we get an unavailable.
const uint16_t kL2capTxCredits = 2;
const uint16_t kExpectedSuccessfulWrites =
kL2capTxCredits + L2capChannel::QueueCapacity();
// Set plenty of kAclLeCredits to ensure that isn't the bottleneck.
const uint16_t kAclLeCredits = kExpectedSuccessfulWrites + 1;
struct {
int write_available_events = 0;
} capture;
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn(
[]([[maybe_unused]] H4PacketWithH4&& packet) {});
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/kAclLeCredits,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(
proxy,
/*num_credits_to_reserve=*/kAclLeCredits));
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
ChannelEventCallback&& kEventFn{[&capture](L2capChannelEvent event) {
if (event == L2capChannelEvent::kWriteAvailable) {
capture.write_available_events++;
}
}};
L2capCoc channel = BuildCoc(proxy,
CocParameters{.tx_credits = kL2capTxCredits,
.event_fn = std::move(kEventFn)});
// Use up the CoC credits and fill up the send queue.
for (int i = 0; i < kExpectedSuccessfulWrites; ++i) {
EXPECT_EQ(channel.IsWriteAvailable(), PW_STATUS_OK);
multibuf::MultiBuf empty = MakeEmptyMultiBuf();
EXPECT_EQ(channel.Write(std::move(empty)).status, PW_STATUS_OK);
RunDispatcher();
}
EXPECT_EQ(0, capture.write_available_events);
// Send queue is full, so client should now get unavailable.
EXPECT_EQ(channel.IsWriteAvailable(), PW_STATUS_UNAVAILABLE);
multibuf::MultiBuf empty = MakeEmptyMultiBuf();
EXPECT_EQ(channel.Write(std::move(empty)).status, PW_STATUS_UNAVAILABLE);
RunDispatcher();
EXPECT_EQ(0, capture.write_available_events);
// TODO: https://pwbug.dev/380299794 - Verify we properly show available once
// Write is available again.
}
TEST_F(L2capCocWriteTest, MultipleWritesMultipleChannels) {
struct {
int sends_called = 0;
std::array<uint8_t, 3> payload = {0xAB, 0xCD, 0xEF};
} capture;
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn(
[&capture](H4PacketWithH4&& packet) {
++capture.sends_called;
PW_TEST_ASSERT_OK_AND_ASSIGN(
auto acl,
MakeEmbossView<emboss::AclDataFrameView>(packet.GetHciSpan()));
emboss::FirstKFrameView kframe = emboss::MakeFirstKFrameView(
acl.payload().BackingStorage().data(), acl.SizeInBytes());
for (size_t i = 0; i < 3; ++i) {
EXPECT_EQ(kframe.payload()[i].Read(), capture.payload[i]);
}
});
constexpr uint16_t kNumChannels = 5;
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/kNumChannels,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(
proxy,
/*num_credits_to_reserve=*/kNumChannels));
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
uint16_t local_cid = 123;
uint16_t remote_cid = 456;
Vector<L2capCoc, kNumChannels> channels;
for (uint16_t i = 0; i < kNumChannels; ++i) {
channels.emplace_back(
BuildCoc(proxy,
CocParameters{
.local_cid = static_cast<uint16_t>(local_cid + i),
.remote_cid = static_cast<uint16_t>(remote_cid + i),
}));
}
for (int i = 0; i < kNumChannels; ++i) {
auto mbuf_result = multibuf::FromSpan(
*allocator, as_writable_bytes(span(capture.payload)), [](ByteSpan) {});
ASSERT_TRUE(mbuf_result.has_value());
multibuf::MultiBuf mbuf = std::move(*mbuf_result);
PW_TEST_EXPECT_OK(channels[i].Write(std::move(mbuf)).status);
RunDispatcher();
std::for_each(capture.payload.begin(),
capture.payload.end(),
[](uint8_t& byte) { ++byte; });
}
EXPECT_EQ(capture.sends_called, kNumChannels);
}
// TODO: https://pwbug.dev/365161669 - Disable test at build-level once
// joinability is a build-system constraint.
#if PW_THREAD_JOINING_ENABLED
// Have multiple threads write to a L2capCoc channel. Verify all resulting ACL
// packets are sent towards controller in the correct order per channel.
// This test be run repetitively with googletest by using:
// clang-format off
// bazelisk --config=googletest //pw_bluetooth_proxy:pw_bluetooth_proxy_test --
// --gtest_filter=L2capCocWriteTest.MultithreadedWrite --gtest_repeat=1000
// clang-format on
// TODO: https://pwbug.dev/441841355 - Update test to send larger packets and
// verify segment order is correct.
TEST_F(L2capCocWriteTest, MultithreadedWrite) {
constexpr unsigned int kNumThreads = 40;
constexpr unsigned int kPacketsPerThread = L2capChannel::QueueCapacity() - 1;
constexpr uint16_t kBaseLocalCid = 0xb000; // 176
constexpr uint16_t kBaseRemoteCid = 0xc000; // 192
constexpr uint16_t kPayloadSize = 10;
struct {
const uint16_t kExpectedSduLength = kPayloadSize;
const uint16_t kExpectedPduLength =
kSduLengthFieldSize + kExpectedSduLength;
const uint16_t kExpectedAclDataTotalLength =
emboss::BasicL2capHeader::IntrinsicSizeInBytes() + kExpectedPduLength;
sync::Mutex sends_by_channel_mutex;
std::array<unsigned int, kNumThreads> sends_by_channel
PW_GUARDED_BY(sends_by_channel_mutex){};
} capture;
pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn(
[&capture](H4PacketWithH4&& packet) {
EXPECT_EQ(packet.GetH4Type(), emboss::H4PacketType::ACL_DATA);
EXPECT_EQ(packet.GetHciSpan().size(),
static_cast<unsigned long>(
emboss::AclDataFrameHeader::IntrinsicSizeInBytes() +
capture.kExpectedAclDataTotalLength));
PW_TEST_ASSERT_OK_AND_ASSIGN(
auto acl,
MakeEmbossView<emboss::AclDataFrameView>(packet.GetHciSpan()));
EXPECT_EQ(acl.header().handle().Read(), kConnectionHandle);
EXPECT_EQ(acl.header().packet_boundary_flag().Read(),
emboss::AclDataPacketBoundaryFlag::FIRST_NON_FLUSHABLE);
EXPECT_EQ(acl.header().broadcast_flag().Read(),
emboss::AclDataPacketBroadcastFlag::POINT_TO_POINT);
EXPECT_EQ(acl.data_total_length().Read(),
capture.kExpectedAclDataTotalLength);
PW_TEST_ASSERT_OK_AND_ASSIGN(
emboss::FirstKFrameView kframe,
MakeEmbossView<emboss::FirstKFrameView>(
acl.payload().BackingStorage().data(), acl.SizeInBytes()));
EXPECT_EQ(kframe.pdu_length().Read(), capture.kExpectedPduLength);
EXPECT_EQ(kframe.sdu_length().Read(), capture.kExpectedSduLength);
// Each channel's remote cid has the thread index as its LSB.
uint16_t current_remote_cid = kframe.channel_id().Read();
unsigned int current_thread_id = current_remote_cid & ~kBaseRemoteCid;
{
std::lock_guard lock(capture.sends_by_channel_mutex);
// Each payload byte should match the send count (to verify ordering).
for (size_t i = 0; i < kPayloadSize; ++i) {
EXPECT_EQ(kframe.payload()[i].Read(),
capture.sends_by_channel[current_thread_id]);
}
capture.sends_by_channel[current_thread_id]++;
}
});
allocator::test::AllocatorForTest<65536> allocator;
ProxyHost proxy =
ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/kNumThreads * kPacketsPerThread,
/*br_edr_acl_credits_to_reserve=*/0,
&allocator);
// If using async, start a dispatcher thread.
StartDispatcherOnNewThread(proxy);
PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(
proxy, kNumThreads * kPacketsPerThread));
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
RunDispatcher();
// Use libc allocators so msan can detect use after frees.
std::array<std::byte, 200 * 1024> packet_buffer{};
pw::multibuf::SimpleAllocator multibuf_allocator{
/*data_area=*/packet_buffer,
/*metadata_alloc=*/allocator::GetLibCAllocator()};
struct ThreadCapture {
L2capCoc channel;
multibuf::MultiBufAllocator& packet_allocator;
};
pw::Vector<ThreadCapture, kNumThreads> captures;
pw::thread::test::TestThreadContext context;
pw::Vector<pw::Thread, kNumThreads> threads;
for (unsigned int i = 0; i < kNumThreads; ++i) {
uint16_t local_cid = kBaseLocalCid + i;
// Each channel's remote cid has the thread index its LSB. That is
// used to track packet ordering per channel.
uint16_t remote_cid = kBaseRemoteCid + i;
// TODO: https://pwbug.dev/422222575 - Move channel creation, close, and
// destruction inside each thread once we have proper channel lifecycle
// locking.
ThreadCapture thread_capture{
BuildCoc(proxy,
CocParameters{.handle = kConnectionHandle,
.local_cid = local_cid,
.remote_cid = remote_cid,
.tx_credits = kPacketsPerThread}),
multibuf_allocator,
};
captures.emplace_back(std::move(thread_capture));
}
for (unsigned int thread_numb = 0; thread_numb < kNumThreads; ++thread_numb) {
ThreadCapture& thread_capture = captures[thread_numb];
threads.emplace_back(context.options(), [&thread_capture]() {
for (unsigned int packet_numb = 0; packet_numb < kPacketsPerThread;
++packet_numb) {
auto mbuf_result =
thread_capture.packet_allocator.AllocateContiguous(kPayloadSize);
ASSERT_TRUE(mbuf_result.has_value());
multibuf::MultiBuf mbuf = std::move(*mbuf_result);
std::fill(
mbuf.begin(), mbuf.end(), static_cast<std::byte>(packet_numb));
PW_TEST_EXPECT_OK(thread_capture.channel.Write(std::move(mbuf)).status);
}
});
}
// Ensure the writer threads complete, drop the channels, and ensure the
// dispatcher thread completes.
for (auto& t : threads) {
t.join();
}
captures.clear();
JoinDispatcherThread();
{
std::lock_guard lock(capture.sends_by_channel_mutex);
for (unsigned int i = 0; i < kNumThreads; ++i) {
EXPECT_EQ(capture.sends_by_channel[i], kPacketsPerThread);
}
}
}
#endif // PW_THREAD_JOINING_ENABLED
// ########## L2capCocReadTest
class L2capCocReadTest : public ProxyHostTest {};
TEST_F(L2capCocReadTest, BasicRead) {
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn(
[]([[maybe_unused]] H4PacketWithH4&& packet) {});
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/0,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
struct {
int receives_called = 0;
std::array<uint8_t, 3> expected_payload = {0xAB, 0xCD, 0xEF};
} capture;
uint16_t local_cid = 234;
L2capCoc channel = BuildCoc(
proxy,
CocParameters{.handle = kConnectionHandle,
.local_cid = local_cid,
.receive_fn = [&capture](multibuf::MultiBuf&& payload) {
++capture.receives_called;
ASSERT_FALSE(payload.empty());
ConstByteSpan rx_sdu = *payload.ConstChunks().begin();
ConstByteSpan expected_sdu =
as_bytes(span(capture.expected_payload.data(),
capture.expected_payload.size()));
EXPECT_TRUE(std::equal(rx_sdu.begin(),
rx_sdu.end(),
expected_sdu.begin(),
expected_sdu.end()));
}});
std::array<uint8_t,
kFirstKFrameOverAclMinSize + capture.expected_payload.size()>
hci_arr;
hci_arr.fill(0);
H4PacketWithHci h4_packet{emboss::H4PacketType::ACL_DATA, hci_arr};
Result<emboss::AclDataFrameWriter> acl =
MakeEmbossWriter<emboss::AclDataFrameWriter>(hci_arr);
acl->header().handle().Write(kConnectionHandle);
acl->data_total_length().Write(emboss::FirstKFrame::MinSizeInBytes() +
capture.expected_payload.size());
emboss::FirstKFrameWriter kframe = emboss::MakeFirstKFrameView(
acl->payload().BackingStorage().data(), acl->data_total_length().Read());
kframe.pdu_length().Write(kSduLengthFieldSize +
capture.expected_payload.size());
kframe.channel_id().Write(local_cid);
kframe.sdu_length().Write(capture.expected_payload.size());
std::copy(capture.expected_payload.begin(),
capture.expected_payload.end(),
hci_arr.begin() + kFirstKFrameOverAclMinSize);
// Send ACL data packet destined for the CoC we registered.
proxy.HandleH4HciFromController(std::move(h4_packet));
EXPECT_EQ(capture.receives_called, 1);
}
TEST_F(L2capCocReadTest, RxCreditsAreReplenished) {
const uint16_t kRxCredits = 10;
// Corresponds to kRxCreditReplenishThreshold in l2cap_coc.cc times
// kRxCredits.
// TODO: b/353734827 - Update test once client can to determine this constant.
const uint16_t kRxThreshold = 3;
struct {
uint16_t local_cid = 234;
uint16_t tx_packets_sent = 0;
// We expect when we reach threshold to replenish exactly that amount.
uint16_t expected_additional_credits = kRxThreshold;
} capture;
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn(
[&capture]([[maybe_unused]] H4PacketWithH4&& packet) {
capture.tx_packets_sent++;
// Verify packet is properly formed FLOW_CONTROL_CREDIT_IND with the
// expected credits.
PW_TEST_ASSERT_OK_AND_ASSIGN(
auto acl,
MakeEmbossView<emboss::AclDataFrameView>(packet.GetHciSpan()));
EXPECT_EQ(acl.header().handle().Read(), kConnectionHandle);
EXPECT_EQ(
acl.data_total_length().Read(),
emboss::BasicL2capHeader::IntrinsicSizeInBytes() +
emboss::L2capFlowControlCreditInd::IntrinsicSizeInBytes());
emboss::CFrameView cframe = emboss::MakeCFrameView(
acl.payload().BackingStorage().data(), acl.payload().SizeInBytes());
EXPECT_EQ(cframe.pdu_length().Read(),
emboss::L2capFlowControlCreditInd::IntrinsicSizeInBytes());
// 0x0005 = LE-U fixed signaling channel ID.
EXPECT_EQ(cframe.channel_id().Read(), 0x0005);
emboss::L2capFlowControlCreditIndView ind =
emboss::MakeL2capFlowControlCreditIndView(
cframe.payload().BackingStorage().data(),
cframe.payload().SizeInBytes());
EXPECT_EQ(ind.command_header().code().Read(),
emboss::L2capSignalingPacketCode::FLOW_CONTROL_CREDIT_IND);
EXPECT_EQ(
ind.command_header().data_length().Read(),
emboss::L2capFlowControlCreditInd::IntrinsicSizeInBytes() -
emboss::L2capSignalingCommandHeader::IntrinsicSizeInBytes());
EXPECT_EQ(ind.cid().Read(), capture.local_cid);
EXPECT_EQ(ind.credits().Read(), capture.expected_additional_credits);
});
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/10,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, 12));
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
L2capCoc channel = BuildCoc(proxy,
CocParameters{.handle = kConnectionHandle,
.local_cid = capture.local_cid,
.rx_credits = kRxCredits});
auto SendRxH4Packet = [this, &capture, &proxy]() {
std::array<uint8_t, 3> expected_payload = {0xAB, 0xCD, 0xEF};
std::array<uint8_t, kFirstKFrameOverAclMinSize + expected_payload.size()>
hci_arr;
hci_arr.fill(0);
H4PacketWithHci h4_packet{emboss::H4PacketType::ACL_DATA, hci_arr};
Result<emboss::AclDataFrameWriter> acl =
MakeEmbossWriter<emboss::AclDataFrameWriter>(hci_arr);
acl->header().handle().Write(kConnectionHandle);
acl->data_total_length().Write(emboss::FirstKFrame::MinSizeInBytes() +
expected_payload.size());
emboss::FirstKFrameWriter kframe =
emboss::MakeFirstKFrameView(acl->payload().BackingStorage().data(),
acl->data_total_length().Read());
kframe.pdu_length().Write(kSduLengthFieldSize + expected_payload.size());
kframe.channel_id().Write(capture.local_cid);
kframe.sdu_length().Write(expected_payload.size());
std::copy(expected_payload.begin(),
expected_payload.end(),
hci_arr.begin() + kFirstKFrameOverAclMinSize);
proxy.HandleH4HciFromController(std::move(h4_packet));
RunDispatcher();
};
// Rx packets before threshold should not trigger a credit packet.
EXPECT_EQ(0, capture.tx_packets_sent);
for (int i = 0; i < kRxThreshold - 1; i++) {
// Send ACL data packet destined for the CoC we registered.
SendRxH4Packet();
EXPECT_EQ(0, capture.tx_packets_sent);
}
// RX packet at threshold should trigger exactly one credit packet with
// threshold credits.
SendRxH4Packet();
EXPECT_EQ(1, capture.tx_packets_sent);
// Send just up to threshold again.
for (int i = 0; i < kRxThreshold - 1; i++) {
SendRxH4Packet();
EXPECT_EQ(1, capture.tx_packets_sent);
}
// RX packet at threshold should once again trigger exactly one credit packet
// with threshold credits.
SendRxH4Packet();
EXPECT_EQ(2, capture.tx_packets_sent);
}
TEST_F(L2capCocReadTest, ChannelHandlesReadWithNullReceiveFn) {
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {
EXPECT_EQ(packet.GetH4Type(), emboss::H4PacketType::EVENT);
});
pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn(
[]([[maybe_unused]] H4PacketWithH4&& packet) {});
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/0,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
uint16_t local_cid = 234;
L2capCoc channel = BuildCoc(
proxy,
CocParameters{.handle = kConnectionHandle,
.local_cid = local_cid,
.rx_credits = 1,
.receive_fn = nullptr,
.event_fn = []([[maybe_unused]] L2capChannelEvent event) {
FAIL();
}});
std::array<uint8_t, kFirstKFrameOverAclMinSize> hci_arr;
hci_arr.fill(0);
H4PacketWithHci h4_packet{emboss::H4PacketType::ACL_DATA, hci_arr};
Result<emboss::AclDataFrameWriter> acl =
MakeEmbossWriter<emboss::AclDataFrameWriter>(hci_arr);
acl->header().handle().Write(kConnectionHandle);
acl->data_total_length().Write(emboss::FirstKFrame::MinSizeInBytes());
emboss::FirstKFrameWriter kframe = emboss::MakeFirstKFrameView(
acl->payload().BackingStorage().data(), acl->payload().SizeInBytes());
kframe.pdu_length().Write(kSduLengthFieldSize);
kframe.channel_id().Write(local_cid);
kframe.sdu_length().Write(0);
proxy.HandleH4HciFromController(std::move(h4_packet));
}
TEST_F(L2capCocReadTest, ErrorOnRxToStoppedChannel) {
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn(
[]([[maybe_unused]] H4PacketWithH4&& packet) {});
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/0,
/*br_edr_acl_credits_to_reserve*/ 0,
allocator);
StartDispatcherOnCurrentThread(proxy);
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
int events_received = 0;
uint16_t num_invalid_rx = 3;
uint16_t local_cid = 234;
L2capCoc channel = BuildCoc(
proxy,
CocParameters{.handle = kConnectionHandle,
.local_cid = local_cid,
.rx_credits = num_invalid_rx,
.receive_fn = [](multibuf::MultiBuf&&) { FAIL(); },
.event_fn =
[&events_received](L2capChannelEvent event) {
++events_received;
EXPECT_EQ(event, L2capChannelEvent::kRxWhileStopped);
}});
std::array<uint8_t, kFirstKFrameOverAclMinSize> hci_arr;
hci_arr.fill(0);
Result<emboss::AclDataFrameWriter> acl =
MakeEmbossWriter<emboss::AclDataFrameWriter>(hci_arr);
acl->header().handle().Write(kConnectionHandle);
acl->data_total_length().Write(emboss::FirstKFrame::MinSizeInBytes());
emboss::FirstKFrameWriter kframe = emboss::MakeFirstKFrameView(
acl->payload().BackingStorage().data(), acl->payload().SizeInBytes());
kframe.pdu_length().Write(kSduLengthFieldSize);
kframe.channel_id().Write(local_cid);
kframe.sdu_length().Write(0);
channel.Stop();
for (int i = 0; i < num_invalid_rx; ++i) {
H4PacketWithHci h4_packet{emboss::H4PacketType::ACL_DATA, hci_arr};
proxy.HandleH4HciFromController(std::move(h4_packet));
}
EXPECT_EQ(events_received, num_invalid_rx);
}
TEST_F(L2capCocReadTest, TooShortAclPassedToHost) {
int sends_called = 0;
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[&sends_called]([[maybe_unused]] H4PacketWithHci&& packet) {
if (packet.GetH4Type() == emboss::H4PacketType::ACL_DATA) {
++sends_called;
}
});
pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn(
[]([[maybe_unused]] H4PacketWithH4&& packet) {});
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/0,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
uint16_t local_cid = 234;
L2capCoc channel = BuildCoc(
proxy,
CocParameters{.handle = kConnectionHandle,
.local_cid = local_cid,
.receive_fn = [](multibuf::MultiBuf&&) { FAIL(); }});
std::array<uint8_t, kFirstKFrameOverAclMinSize> hci_arr;
hci_arr.fill(0);
H4PacketWithHci h4_packet{emboss::H4PacketType::ACL_DATA, hci_arr};
Result<emboss::AclDataFrameWriter> acl =
MakeEmbossWriter<emboss::AclDataFrameWriter>(hci_arr);
acl->header().handle().Write(kConnectionHandle);
// Write size larger than buffer size.
acl->data_total_length().Write(emboss::FirstKFrame::MinSizeInBytes() + 5);
proxy.HandleH4HciFromController(std::move(h4_packet));
EXPECT_EQ(sends_called, 1);
}
TEST_F(L2capCocReadTest, ChannelClosedWithErrorIfMtuExceeded) {
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn(
[]([[maybe_unused]] H4PacketWithH4&& packet) {});
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/0,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
uint16_t local_cid = 234;
constexpr uint16_t kRxMtu = 5;
int events_received = 0;
L2capCoc channel = BuildCoc(
proxy,
CocParameters{.handle = kConnectionHandle,
.local_cid = local_cid,
.rx_mtu = kRxMtu,
.receive_fn = [](multibuf::MultiBuf&&) { FAIL(); },
.event_fn =
[&events_received](L2capChannelEvent event) {
++events_received;
EXPECT_EQ(event, L2capChannelEvent::kRxInvalid);
}});
constexpr uint16_t kPayloadSize = kRxMtu + 1;
std::array<uint8_t, kFirstKFrameOverAclMinSize + kPayloadSize> hci_arr;
hci_arr.fill(0);
H4PacketWithHci h4_packet{emboss::H4PacketType::ACL_DATA, hci_arr};
Result<emboss::AclDataFrameWriter> acl =
MakeEmbossWriter<emboss::AclDataFrameWriter>(hci_arr);
acl->header().handle().Write(kConnectionHandle);
acl->data_total_length().Write(emboss::FirstKFrame::MinSizeInBytes() +
kPayloadSize);
emboss::FirstKFrameWriter kframe = emboss::MakeFirstKFrameView(
acl->payload().BackingStorage().data(), acl->data_total_length().Read());
kframe.pdu_length().Write(kSduLengthFieldSize + kPayloadSize);
kframe.channel_id().Write(local_cid);
kframe.sdu_length().Write(kPayloadSize);
proxy.HandleH4HciFromController(std::move(h4_packet));
EXPECT_EQ(events_received, 1);
}
TEST_F(L2capCocReadTest, ChannelClosedWithErrorIfMpsExceeded) {
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn(
[]([[maybe_unused]] H4PacketWithH4&& packet) {});
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/0,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
uint16_t local_cid = 234;
constexpr uint16_t kRxMps = 5;
int events_received = 0;
L2capCoc channel = BuildCoc(
proxy,
CocParameters{.handle = kConnectionHandle,
.local_cid = local_cid,
.rx_mps = kRxMps,
.receive_fn = [](multibuf::MultiBuf&&) { FAIL(); },
.event_fn =
[&events_received](L2capChannelEvent event) {
++events_received;
EXPECT_EQ(event, L2capChannelEvent::kRxInvalid);
}});
constexpr uint16_t kPayloadSize = kRxMps + 1;
std::array<uint8_t, kFirstKFrameOverAclMinSize + kPayloadSize> hci_arr;
hci_arr.fill(0);
H4PacketWithHci h4_packet{emboss::H4PacketType::ACL_DATA, hci_arr};
Result<emboss::AclDataFrameWriter> acl =
MakeEmbossWriter<emboss::AclDataFrameWriter>(hci_arr);
acl->header().handle().Write(kConnectionHandle);
acl->data_total_length().Write(emboss::FirstKFrame::MinSizeInBytes() +
kPayloadSize);
emboss::FirstKFrameWriter kframe = emboss::MakeFirstKFrameView(
acl->payload().BackingStorage().data(), acl->data_total_length().Read());
kframe.pdu_length().Write(kSduLengthFieldSize + kPayloadSize);
kframe.channel_id().Write(local_cid);
kframe.sdu_length().Write(kPayloadSize);
proxy.HandleH4HciFromController(std::move(h4_packet));
EXPECT_EQ(events_received, 1);
}
TEST_F(L2capCocReadTest, ChannelClosedWithErrorIfPayloadsExceedSduLength) {
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn(
[]([[maybe_unused]] H4PacketWithH4&& packet) {});
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/0,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
uint16_t local_cid = 234;
int events_received = 0;
L2capCoc channel = BuildCoc(
proxy,
CocParameters{.handle = kConnectionHandle,
.local_cid = local_cid,
.receive_fn = [](multibuf::MultiBuf&&) { FAIL(); },
.event_fn =
[&events_received](L2capChannelEvent event) {
++events_received;
EXPECT_EQ(event, L2capChannelEvent::kRxInvalid);
}});
constexpr uint16_t k1stPayloadSize = 1;
constexpr uint16_t k2ndPayloadSize = 3;
ASSERT_GT(k2ndPayloadSize, k1stPayloadSize + 1);
// Indicate SDU length that does not account for the 2nd payload size.
constexpr uint16_t kSduLength = k1stPayloadSize + 1;
std::array<uint8_t,
kFirstKFrameOverAclMinSize +
std::max(k1stPayloadSize, k2ndPayloadSize)>
hci_arr;
hci_arr.fill(0);
H4PacketWithHci h4_1st_segment{
emboss::H4PacketType::ACL_DATA,
pw::span<uint8_t>(hci_arr.data(),
kFirstKFrameOverAclMinSize + k1stPayloadSize)};
Result<emboss::AclDataFrameWriter> acl =
MakeEmbossWriter<emboss::AclDataFrameWriter>(hci_arr);
acl->header().handle().Write(kConnectionHandle);
acl->data_total_length().Write(emboss::FirstKFrame::MinSizeInBytes() +
k1stPayloadSize);
emboss::FirstKFrameWriter kframe = emboss::MakeFirstKFrameView(
acl->payload().BackingStorage().data(), acl->data_total_length().Read());
kframe.pdu_length().Write(kSduLengthFieldSize + k1stPayloadSize);
kframe.channel_id().Write(local_cid);
kframe.sdu_length().Write(kSduLength);
proxy.HandleH4HciFromController(std::move(h4_1st_segment));
// Send 2nd segment.
acl->data_total_length().Write(emboss::SubsequentKFrame::MinSizeInBytes() +
k2ndPayloadSize);
kframe.pdu_length().Write(k2ndPayloadSize);
H4PacketWithHci h4_2nd_segment{
emboss::H4PacketType::ACL_DATA,
pw::span<uint8_t>(hci_arr.data(),
emboss::AclDataFrame::MinSizeInBytes() +
emboss::SubsequentKFrame::MinSizeInBytes() +
k2ndPayloadSize)};
proxy.HandleH4HciFromController(std::move(h4_2nd_segment));
EXPECT_EQ(events_received, 1);
}
TEST_F(L2capCocReadTest, NoReadOnStoppedChannel) {
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn(
[]([[maybe_unused]] H4PacketWithH4&& packet) {});
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/0,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
uint16_t local_cid = 234;
L2capCoc channel = BuildCoc(
proxy,
CocParameters{.handle = kConnectionHandle,
.local_cid = local_cid,
.receive_fn = [](multibuf::MultiBuf&&) { FAIL(); }});
std::array<uint8_t, kFirstKFrameOverAclMinSize> hci_arr;
hci_arr.fill(0);
H4PacketWithHci h4_packet{emboss::H4PacketType::ACL_DATA, hci_arr};
Result<emboss::AclDataFrameWriter> acl =
MakeEmbossWriter<emboss::AclDataFrameWriter>(hci_arr);
acl->header().handle().Write(kConnectionHandle);
acl->data_total_length().Write(emboss::FirstKFrame::MinSizeInBytes());
emboss::FirstKFrameWriter kframe = emboss::MakeFirstKFrameView(
acl->payload().BackingStorage().data(), acl->data_total_length().Read());
kframe.pdu_length().Write(kSduLengthFieldSize);
kframe.channel_id().Write(local_cid);
channel.Stop();
proxy.HandleH4HciFromController(std::move(h4_packet));
}
TEST_F(L2capCocReadTest, NoReadOnSameCidDifferentConnectionHandle) {
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn(
[]([[maybe_unused]] H4PacketWithH4&& packet) {});
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/0,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
uint16_t local_cid = 234;
L2capCoc channel = BuildCoc(
proxy,
CocParameters{.handle = kConnectionHandle,
.local_cid = local_cid,
.receive_fn = [](multibuf::MultiBuf&&) { FAIL(); }});
std::array<uint8_t, kFirstKFrameOverAclMinSize> hci_arr;
hci_arr.fill(0);
H4PacketWithHci h4_packet{emboss::H4PacketType::ACL_DATA, hci_arr};
Result<emboss::AclDataFrameWriter> acl =
MakeEmbossWriter<emboss::AclDataFrameWriter>(hci_arr);
acl->header().handle().Write(444);
acl->data_total_length().Write(emboss::FirstKFrame::MinSizeInBytes());
emboss::FirstKFrameWriter kframe = emboss::MakeFirstKFrameView(
acl->payload().BackingStorage().data(), acl->data_total_length().Read());
kframe.pdu_length().Write(kSduLengthFieldSize);
kframe.channel_id().Write(local_cid);
proxy.HandleH4HciFromController(std::move(h4_packet));
}
TEST_F(L2capCocReadTest, MultipleReadsSameChannel) {
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn(
[]([[maybe_unused]] H4PacketWithH4&& packet) {});
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/0,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
struct {
int sends_called = 0;
std::array<uint8_t, 3> payload = {0xAB, 0xCD, 0xEF};
} capture;
uint16_t local_cid = 234;
L2capCoc channel = BuildCoc(
proxy,
CocParameters{.handle = kConnectionHandle,
.local_cid = local_cid,
.receive_fn = [&capture](multibuf::MultiBuf&& payload) {
++capture.sends_called;
ASSERT_FALSE(payload.empty());
ConstByteSpan rx_sdu = *payload.ConstChunks().begin();
ConstByteSpan expected_sdu = as_bytes(
span(capture.payload.data(), capture.payload.size()));
EXPECT_TRUE(std::equal(rx_sdu.begin(),
rx_sdu.end(),
expected_sdu.begin(),
expected_sdu.end()));
}});
std::array<uint8_t, kFirstKFrameOverAclMinSize + capture.payload.size()>
hci_arr;
hci_arr.fill(0);
Result<emboss::AclDataFrameWriter> acl =
MakeEmbossWriter<emboss::AclDataFrameWriter>(hci_arr);
acl->header().handle().Write(kConnectionHandle);
acl->data_total_length().Write(emboss::FirstKFrame::MinSizeInBytes() +
capture.payload.size());
emboss::FirstKFrameWriter kframe = emboss::MakeFirstKFrameView(
acl->payload().BackingStorage().data(), acl->data_total_length().Read());
kframe.pdu_length().Write(capture.payload.size() + kSduLengthFieldSize);
kframe.channel_id().Write(local_cid);
kframe.sdu_length().Write(capture.payload.size());
int num_reads = 10;
for (int i = 0; i < num_reads; ++i) {
std::copy(capture.payload.begin(),
capture.payload.end(),
hci_arr.begin() + kFirstKFrameOverAclMinSize);
H4PacketWithHci h4_packet{emboss::H4PacketType::ACL_DATA, hci_arr};
proxy.HandleH4HciFromController(std::move(h4_packet));
std::for_each(capture.payload.begin(),
capture.payload.end(),
[](uint8_t& byte) { ++byte; });
}
EXPECT_EQ(capture.sends_called, num_reads);
}
TEST_F(L2capCocReadTest, MultipleReadsMultipleChannels) {
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn(
[]([[maybe_unused]] H4PacketWithH4&& packet) {});
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/0,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
struct {
int sends_called = 0;
std::array<uint8_t, 3> payload = {0xAB, 0xCD, 0xEF};
} capture;
constexpr int kNumChannels = 5;
uint16_t local_cid = 123;
uint16_t remote_cid = 456;
auto receive_fn = [&capture](multibuf::MultiBuf&& payload) {
++capture.sends_called;
ASSERT_FALSE(payload.empty());
ConstByteSpan rx_sdu = *payload.ConstChunks().begin();
ConstByteSpan expected_sdu =
as_bytes(span(capture.payload.data(), capture.payload.size()));
EXPECT_TRUE(std::equal(rx_sdu.begin(),
rx_sdu.end(),
expected_sdu.begin(),
expected_sdu.end()));
};
Vector<L2capCoc, kNumChannels> channels;
for (uint16_t i = 0; i < kNumChannels; ++i) {
channels.emplace_back(BuildCoc(
proxy,
CocParameters{.handle = kConnectionHandle,
.local_cid = static_cast<uint16_t>(local_cid + i),
.remote_cid = static_cast<uint16_t>(remote_cid + i),
.receive_fn = receive_fn}));
}
std::array<uint8_t, kFirstKFrameOverAclMinSize + capture.payload.size()>
hci_arr;
hci_arr.fill(0);
Result<emboss::AclDataFrameWriter> acl =
MakeEmbossWriter<emboss::AclDataFrameWriter>(hci_arr);
acl->header().handle().Write(kConnectionHandle);
acl->data_total_length().Write(emboss::FirstKFrame::MinSizeInBytes() +
capture.payload.size());
emboss::FirstKFrameWriter kframe = emboss::MakeFirstKFrameView(
acl->payload().BackingStorage().data(), acl->data_total_length().Read());
kframe.pdu_length().Write(capture.payload.size() + kSduLengthFieldSize);
kframe.sdu_length().Write(capture.payload.size());
for (int i = 0; i < kNumChannels; ++i) {
kframe.channel_id().Write(local_cid + i);
std::copy(capture.payload.begin(),
capture.payload.end(),
hci_arr.begin() + kFirstKFrameOverAclMinSize);
H4PacketWithHci h4_packet{emboss::H4PacketType::ACL_DATA, hci_arr};
proxy.HandleH4HciFromController(std::move(h4_packet));
std::for_each(capture.payload.begin(),
capture.payload.end(),
[](uint8_t& byte) { ++byte; });
}
EXPECT_EQ(capture.sends_called, kNumChannels);
}
TEST_F(L2capCocReadTest, ChannelStoppageDoNotAffectOtherChannels) {
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn(
[]([[maybe_unused]] H4PacketWithH4&& packet) {});
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/0,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
struct {
int sends_called = 0;
std::array<uint8_t, 3> payload = {0xAB, 0xCD, 0xEF};
} capture;
constexpr int kNumChannels = 5;
uint16_t local_cid = 123;
uint16_t remote_cid = 456;
auto receive_fn = [&capture](multibuf::MultiBuf&& payload) {
++capture.sends_called;
ASSERT_FALSE(payload.empty());
ConstByteSpan rx_sdu = *payload.ConstChunks().begin();
ConstByteSpan expected_sdu =
as_bytes(span(capture.payload.data(), capture.payload.size()));
EXPECT_TRUE(std::equal(rx_sdu.begin(),
rx_sdu.end(),
expected_sdu.begin(),
expected_sdu.end()));
};
Vector<L2capCoc, kNumChannels> channels;
for (uint16_t i = 0; i < kNumChannels; ++i) {
channels.emplace_back(BuildCoc(
proxy,
CocParameters{.handle = kConnectionHandle,
.local_cid = static_cast<uint16_t>(local_cid + i),
.remote_cid = static_cast<uint16_t>(remote_cid + i),
.receive_fn = receive_fn}));
}
// Stop the 2nd and 4th of the 5 channels.
channels[1].Stop();
channels[3].Stop();
std::array<uint8_t, kFirstKFrameOverAclMinSize + capture.payload.size()>
hci_arr;
hci_arr.fill(0);
Result<emboss::AclDataFrameWriter> acl =
MakeEmbossWriter<emboss::AclDataFrameWriter>(hci_arr);
acl->header().handle().Write(kConnectionHandle);
acl->data_total_length().Write(emboss::FirstKFrame::MinSizeInBytes() +
capture.payload.size());
emboss::FirstKFrameWriter kframe = emboss::MakeFirstKFrameView(
acl->payload().BackingStorage().data(), acl->data_total_length().Read());
kframe.pdu_length().Write(capture.payload.size() + kSduLengthFieldSize);
kframe.sdu_length().Write(capture.payload.size());
for (int i = 0; i < kNumChannels; ++i) {
// Still send packets to the stopped channels, so we can validate that it
// does not cause issues.
kframe.channel_id().Write(local_cid + i);
std::copy(capture.payload.begin(),
capture.payload.end(),
hci_arr.begin() + kFirstKFrameOverAclMinSize);
H4PacketWithHci h4_packet{emboss::H4PacketType::ACL_DATA, hci_arr};
proxy.HandleH4HciFromController(std::move(h4_packet));
std::for_each(capture.payload.begin(),
capture.payload.end(),
[](uint8_t& byte) { ++byte; });
}
EXPECT_EQ(capture.sends_called, kNumChannels - 2);
}
TEST_F(L2capCocReadTest, NonCocAclPacketPassesThroughToHost) {
struct {
int sends_called = 0;
std::array<uint8_t, 3> expected_payload = {0xAB, 0xCD, 0xEF};
} capture;
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[&capture](H4PacketWithHci&& packet) {
if (packet.GetH4Type() == emboss::H4PacketType::EVENT) {
return;
}
++capture.sends_called;
Result<emboss::AclDataFrameWriter> acl =
MakeEmbossWriter<emboss::AclDataFrameWriter>(packet.GetHciSpan());
EXPECT_EQ(acl->header().handle().Read(), kConnectionHandle);
emboss::BFrameView bframe =
emboss::MakeBFrameView(acl->payload().BackingStorage().data(),
acl->data_total_length().Read());
for (size_t i = 0; i < capture.expected_payload.size(); ++i) {
EXPECT_EQ(bframe.payload()[i].Read(), capture.expected_payload[i]);
}
});
pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn(
[]([[maybe_unused]] H4PacketWithH4&& packet) {});
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/0,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
// Acquire unused CoC to validate that doing so does not interfere.
L2capCoc channel = BuildCoc(
proxy,
CocParameters{.handle = kConnectionHandle,
.receive_fn = [](multibuf::MultiBuf&&) { FAIL(); }});
std::array<uint8_t,
emboss::AclDataFrameHeader::IntrinsicSizeInBytes() +
emboss::BasicL2capHeader::IntrinsicSizeInBytes() +
capture.expected_payload.size()>
hci_arr;
hci_arr.fill(0);
H4PacketWithHci h4_packet{emboss::H4PacketType::ACL_DATA, hci_arr};
Result<emboss::AclDataFrameWriter> acl =
MakeEmbossWriter<emboss::AclDataFrameWriter>(hci_arr);
acl->header().handle().Write(kConnectionHandle);
acl->data_total_length().Write(
emboss::BasicL2capHeader::IntrinsicSizeInBytes() +
capture.expected_payload.size());
emboss::BFrameWriter bframe = emboss::MakeBFrameView(
acl->payload().BackingStorage().data(), acl->data_total_length().Read());
bframe.pdu_length().Write(capture.expected_payload.size());
bframe.channel_id().Write(111);
std::copy(capture.expected_payload.begin(),
capture.expected_payload.end(),
hci_arr.begin() +
emboss::AclDataFrameHeader::IntrinsicSizeInBytes() +
emboss::BasicL2capHeader::IntrinsicSizeInBytes());
// Send ACL packet that should be forwarded to host.
proxy.HandleH4HciFromController(std::move(h4_packet));
EXPECT_EQ(capture.sends_called, 1);
}
TEST_F(L2capCocReadTest, AclFrameWithIncompleteL2capHeaderForwardedToHost) {
int sends_to_host_called = 0;
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[&sends_to_host_called]([[maybe_unused]] H4PacketWithHci&& packet) {
if (packet.GetH4Type() == emboss::H4PacketType::ACL_DATA) {
++sends_to_host_called;
}
});
pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn(
[]([[maybe_unused]] H4PacketWithH4&& packet) {});
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/0,
/*br_edr_acl_credits_to_reserve=*/0,
GetProxyHostAllocator());
StartDispatcherOnCurrentThread(proxy);
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
L2capCoc channel =
BuildCoc(proxy, CocParameters{.handle = kConnectionHandle});
std::array<uint8_t, emboss::AclDataFrameHeader::IntrinsicSizeInBytes()>
hci_arr;
hci_arr.fill(0);
H4PacketWithHci h4_packet{emboss::H4PacketType::ACL_DATA, hci_arr};
Result<emboss::AclDataFrameWriter> acl =
MakeEmbossWriter<emboss::AclDataFrameWriter>(hci_arr);
acl->header().handle().Write(kConnectionHandle);
acl->data_total_length().Write(0);
proxy.HandleH4HciFromController(std::move(h4_packet));
EXPECT_EQ(sends_to_host_called, 1);
}
TEST_F(L2capCocReadTest, FragmentedPduDoesNotInterfereWithOtherChannels) {
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn(
[]([[maybe_unused]] H4PacketWithH4&& packet) {});
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/0,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
uint16_t handle_frag = 0x123, handle_fine = 0x234;
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, handle_frag, emboss::StatusCode::SUCCESS));
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, handle_fine, emboss::StatusCode::SUCCESS));
uint16_t cid_frag = 0x345, cid_fine = 0x456;
int packets_received = 0;
auto receive_fn = [&packets_received](multibuf::MultiBuf&&) {
++packets_received;
};
L2capCoc frag_channel = BuildCoc(proxy,
CocParameters{
.handle = handle_frag,
.local_cid = cid_frag,
.receive_fn = receive_fn,
});
L2capCoc fine_channel = BuildCoc(proxy,
CocParameters{
.handle = handle_fine,
.local_cid = cid_fine,
.receive_fn = receive_fn,
});
// Order of receptions:
// 1. 1st of 3 fragments to frag_channel.
// 2. Non-fragmented PDU to fine_channel.
// 3. 2nd of 3 fragments to frag_channel.
// 4. Non-fragmented PDU to fine_channel.
// 5. 3rd of 3 fragments to frag_channel.
// 6. Non-fragmented PDU to fine_channel.
constexpr uint8_t kPduLength = 14;
ASSERT_GT(kPduLength, kSduLengthFieldSize);
constexpr uint8_t kSduLength = kPduLength - kSduLengthFieldSize;
// 1. 1st of 3 fragments to frag_channel.
std::array<uint8_t, emboss::AclDataFrame::MinSizeInBytes() + kSduLength>
frag_hci_arr;
frag_hci_arr.fill(0);
H4PacketWithHci h4_1st_fragment{
emboss::H4PacketType::ACL_DATA,
pw::span(frag_hci_arr.data(), kFirstKFrameOverAclMinSize)};
Result<emboss::AclDataFrameWriter> acl_frag =
MakeEmbossWriter<emboss::AclDataFrameWriter>(frag_hci_arr);
acl_frag->header().handle().Write(handle_frag);
acl_frag->data_total_length().Write(emboss::FirstKFrame::MinSizeInBytes());
emboss::FirstKFrameWriter kframe_frag =
emboss::MakeFirstKFrameView(acl_frag->payload().BackingStorage().data(),
acl_frag->data_total_length().Read());
kframe_frag.pdu_length().Write(kPduLength);
kframe_frag.channel_id().Write(cid_frag);
kframe_frag.sdu_length().Write(kSduLength);
proxy.HandleH4HciFromController(std::move(h4_1st_fragment));
// 2. Non-fragmented PDU to fine_channel.
std::array<uint8_t, kFirstKFrameOverAclMinSize> fine_hci_arr;
fine_hci_arr.fill(0);
H4PacketWithHci h4_packet{emboss::H4PacketType::ACL_DATA,
pw::span(fine_hci_arr)};
Result<emboss::AclDataFrameWriter> acl_fine =
MakeEmbossWriter<emboss::AclDataFrameWriter>(fine_hci_arr);
acl_fine->header().handle().Write(handle_fine);
acl_fine->data_total_length().Write(emboss::FirstKFrame::MinSizeInBytes());
emboss::FirstKFrameWriter kframe_fine =
emboss::MakeFirstKFrameView(acl_fine->payload().BackingStorage().data(),
acl_fine->data_total_length().Read());
kframe_fine.pdu_length().Write(kSduLengthFieldSize);
kframe_fine.channel_id().Write(cid_fine);
kframe_fine.sdu_length().Write(0);
proxy.HandleH4HciFromController(std::move(h4_packet));
// 3. 2nd of 3 fragments to frag_channel.
acl_frag->header().packet_boundary_flag().Write(
emboss::AclDataPacketBoundaryFlag::CONTINUING_FRAGMENT);
acl_frag->header().broadcast_flag().Write(
emboss::AclDataPacketBroadcastFlag::POINT_TO_POINT);
acl_frag->data_total_length().Write(kSduLength / 2);
H4PacketWithHci h4_2nd_fragment{
emboss::H4PacketType::ACL_DATA,
pw::span<uint8_t>(
frag_hci_arr.data(),
emboss::AclDataFrame::MinSizeInBytes() + kSduLength / 2)};
proxy.HandleH4HciFromController(std::move(h4_2nd_fragment));
// 4. Non-fragmented PDU to fine_channel.
H4PacketWithHci h4_packet_2{emboss::H4PacketType::ACL_DATA,
pw::span(fine_hci_arr)};
proxy.HandleH4HciFromController(std::move(h4_packet_2));
// 5. 3rd of 3 fragments to frag_channel.
if (kSduLength % 2 == 1) {
acl_frag->data_total_length().Write(kSduLength / 2 + 1);
}
H4PacketWithHci h4_3rd_fragment{
emboss::H4PacketType::ACL_DATA,
pw::span<uint8_t>(frag_hci_arr.data(),
emboss::AclDataFrame::MinSizeInBytes() +
kSduLength / 2 + (kSduLength % 2))};
proxy.HandleH4HciFromController(std::move(h4_3rd_fragment));
// 6. Non-fragmented PDU to fine_channel.
H4PacketWithHci h4_packet_3{emboss::H4PacketType::ACL_DATA,
pw::span(fine_hci_arr)};
proxy.HandleH4HciFromController(std::move(h4_packet_3));
// 3 non-fragmented PDUs plus 1 recombined PDU
EXPECT_EQ(packets_received, 3 + 1);
}
// ########## L2capCocQueueTest
class L2capCocQueueTest : public ProxyHostTest {};
TEST_F(L2capCocQueueTest, ReadBufferResponseDrainsQueue) {
const uint16_t kNumSends = L2capCoc::QueueCapacity() - NumBufferedPayloads();
size_t sends_called = 0;
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn(
[&sends_called]([[maybe_unused]] H4PacketWithH4&& packet) {
++sends_called;
});
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/kNumSends,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
L2capCoc channel = BuildCoc(
proxy,
CocParameters{.handle = kConnectionHandle, .tx_credits = kNumSends});
EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 0);
for (size_t i = 0; i < kNumSends; ++i) {
multibuf::MultiBuf empty = MakeEmptyMultiBuf();
PW_TEST_EXPECT_OK(channel.Write(std::move(empty)).status);
RunDispatcher();
}
multibuf::MultiBuf empty = MakeEmptyMultiBuf();
EXPECT_EQ(channel.Write(std::move(empty)).status, PW_STATUS_UNAVAILABLE);
RunDispatcher();
EXPECT_EQ(sends_called, 0u);
PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, kNumSends));
EXPECT_EQ(sends_called, kNumSends);
}
TEST_F(L2capCocQueueTest, NocpEventDrainsQueue) {
const uint16_t kNumSends = L2capCoc::QueueCapacity() - NumBufferedPayloads();
size_t sends_called = 0;
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn(
[&sends_called]([[maybe_unused]] H4PacketWithH4&& packet) {
++sends_called;
});
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/kNumSends,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, kNumSends));
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
L2capCoc channel =
BuildCoc(proxy,
CocParameters{.handle = kConnectionHandle,
.tx_credits = uint16_t(kNumSends * 2)});
for (size_t i = 0; i < kNumSends; ++i) {
multibuf::MultiBuf empty = MakeEmptyMultiBuf();
PW_TEST_EXPECT_OK(channel.Write(std::move(empty)).status);
RunDispatcher();
}
EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), 0);
for (size_t i = 0; i < kNumSends; ++i) {
multibuf::MultiBuf empty = MakeEmptyMultiBuf();
PW_TEST_EXPECT_OK(channel.Write(std::move(empty)).status);
RunDispatcher();
}
multibuf::MultiBuf empty = MakeEmptyMultiBuf();
EXPECT_EQ(channel.Write(std::move(empty)).status, PW_STATUS_UNAVAILABLE);
RunDispatcher();
EXPECT_EQ(sends_called, kNumSends);
PW_TEST_EXPECT_OK(
SendNumberOfCompletedPackets(proxy, {{kConnectionHandle, kNumSends}}));
EXPECT_EQ(sends_called, kNumSends * 2u);
}
TEST_F(L2capCocQueueTest, RemovingLrdChannelDoesNotInvalidateRoundRobin) {
const uint16_t kNumSends = L2capCoc::QueueCapacity() - NumBufferedPayloads();
size_t sends_called = 0;
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn(
[&sends_called]([[maybe_unused]] H4PacketWithH4&& packet) {
++sends_called;
});
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/kNumSends,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, kNumSends));
EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), kNumSends);
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
std::array<uint16_t, 3> local_cids = {101, 102, 103};
std::array<uint16_t, 3> remote_cids = {104, 105, 106};
L2capCoc chan_left = BuildCoc(proxy,
CocParameters{.handle = kConnectionHandle,
.local_cid = local_cids[0],
.remote_cid = remote_cids[0],
.tx_credits = 1});
std::optional<L2capCoc> chan_middle =
BuildCoc(proxy,
CocParameters{.handle = kConnectionHandle,
.local_cid = local_cids[1],
.remote_cid = remote_cids[1],
.tx_credits = uint16_t(kNumSends + 1)});
L2capCoc chan_right = BuildCoc(proxy,
CocParameters{.handle = kConnectionHandle,
.local_cid = local_cids[2],
.remote_cid = remote_cids[2],
.tx_credits = 1});
// We have 3 channels. Make it so LRD channel iterator is on the middle
// channel, then release that channel and ensure the other two are still
// reached in the round robin.
// Queue a packet in middle channel.
for (size_t i = 0; i < kNumSends; ++i) {
multibuf::MultiBuf empty = MakeEmptyMultiBuf();
PW_TEST_EXPECT_OK(chan_middle->Write(std::move(empty)).status);
RunDispatcher();
}
EXPECT_EQ(sends_called, kNumSends);
// Make middle channel the LRD channel.
PW_TEST_EXPECT_OK(
SendNumberOfCompletedPackets(proxy, {{kConnectionHandle, 1}}));
EXPECT_EQ(sends_called, kNumSends);
// Queue a packet each in left and right channels.
multibuf::MultiBuf empty1 = MakeEmptyMultiBuf();
PW_TEST_EXPECT_OK(chan_left.Write(std::move(empty1)).status);
multibuf::MultiBuf empty2 = MakeEmptyMultiBuf();
PW_TEST_EXPECT_OK(chan_right.Write(std::move(empty2)).status);
RunDispatcher();
EXPECT_EQ(sends_called, kNumSends + 1u);
// Drop middle channel. LRD write iterator should still be valid.
chan_middle.reset();
// Confirm packets in remaining two channels are sent in round robin.
PW_TEST_EXPECT_OK(
SendNumberOfCompletedPackets(proxy, {{kConnectionHandle, 2}}));
EXPECT_EQ(sends_called, kNumSends + 2u);
}
TEST_F(L2capCocQueueTest, H4BufferReleaseTriggersQueueDrain) {
constexpr uint8_t kAclLeCredits = 255;
struct {
size_t sends_called = 0;
pw::Vector<H4PacketWithH4, 50> packet_store;
} capture;
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn(
[&capture](H4PacketWithH4&& packet) {
++capture.sends_called;
capture.packet_store.push_back(std::move(packet));
});
allocator::test::AllocatorForTest<4096> allocator;
ProxyHost proxy(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/kAclLeCredits,
/*br_edr_acl_credits_to_reserve=*/0,
&allocator);
StartDispatcherOnCurrentThread(proxy);
PW_TEST_EXPECT_OK(
SendLeReadBufferResponseFromController(proxy, kAclLeCredits));
EXPECT_EQ(proxy.GetNumFreeLeAclPackets(), kAclLeCredits);
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
constexpr uint16_t kRemoteCid = 0x456;
L2capCoc channel = BuildCoc(proxy,
CocParameters{
.handle = kConnectionHandle,
.remote_cid = kRemoteCid,
.tx_mtu = 1000,
.tx_mps = 1000,
.tx_credits = kAclLeCredits,
});
EXPECT_EQ(capture.sends_called, 0u);
// Occupy all buffers. Final Write should queue and not send.
std::array<uint8_t, 240> payload = {};
size_t num_writes = 0;
do {
auto mbuf_result = multibuf::FromSpan(
allocator, as_writable_bytes(span(payload)), [](ByteSpan) {});
ASSERT_TRUE(mbuf_result.has_value());
multibuf::MultiBuf mbuf = std::move(*mbuf_result);
PW_TEST_EXPECT_OK(channel.IsWriteAvailable());
PW_TEST_EXPECT_OK(channel.Write(std::move(mbuf)).status);
RunDispatcher();
++num_writes;
} while (capture.sends_called == num_writes);
// The final write should be queued and not sent.
EXPECT_EQ(capture.sends_called, num_writes - 1);
// Sending should have stopped because the allocator is full, not because ACL
// credits ran out.
EXPECT_LT(capture.sends_called, kAclLeCredits);
// Destroying an H4 packet will send the next packet in the destructor, so we
// need to first extract the packet from the vector to avoid appending to
// the vector during pop_back.
ASSERT_FALSE(capture.packet_store.empty());
std::optional<H4PacketWithH4> packet = std::move(capture.packet_store.back());
capture.packet_store.pop_back();
// Release a buffer. Queued packet should then send.
packet.reset();
RunDispatcher();
EXPECT_EQ(capture.sends_called, num_writes);
// Free all buffers before the allocator is destroyed.
capture.packet_store.clear();
}
TEST_F(L2capCocQueueTest, RoundRobinHandlesMultiplePasses) {
const uint16_t kNumSends = L2capCoc::QueueCapacity() - NumBufferedPayloads();
struct {
size_t sends_called = 0;
Vector<H4PacketWithH4, kNumSends> packet_store;
} capture;
pw::Function<void(H4PacketWithHci && packet)>&& send_to_host_fn(
[](H4PacketWithHci&&) {});
pw::Function<void(H4PacketWithH4 && packet)>&& send_to_controller_fn(
[&capture](H4PacketWithH4&& packet) {
++capture.sends_called;
// We prevent packets from being released in this test because each
// packet release triggers a round robin.
capture.packet_store.push_back(std::move(packet));
});
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/kNumSends,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
L2capCoc channel = BuildCoc(
proxy,
CocParameters{.handle = kConnectionHandle, .tx_credits = kNumSends});
// Occupy all queue slots.
for (size_t i = 0; i < kNumSends; ++i) {
multibuf::MultiBuf empty = MakeEmptyMultiBuf();
PW_TEST_EXPECT_OK(channel.Write(std::move(empty)).status);
RunDispatcher();
}
EXPECT_EQ(capture.sends_called, 0ul);
// This will provide enough credits for all queued packets. So they all should
// be drained and sent.
PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(proxy, kNumSends));
EXPECT_EQ(capture.sends_called, kNumSends);
capture.packet_store.clear();
}
// ########## L2capCocReassemblyTest
class L2capCocReassemblyTest : public ProxyHostTest {};
TEST_F(L2capCocReassemblyTest, OneSegmentRx) {
pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn(
[](H4PacketWithH4&&) {});
pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn(
[](H4PacketWithHci&&) {});
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/0,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
uint16_t local_cid = 0x234;
struct {
int sdus_received = 0;
std::array<uint8_t, 10> expected_payload = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
} capture;
L2capCoc channel = BuildCoc(
proxy,
{.handle = kConnectionHandle,
.local_cid = local_cid,
.receive_fn = [&capture](multibuf::MultiBuf&& payload) {
++capture.sdus_received;
ASSERT_FALSE(payload.empty());
ConstByteSpan rx_sdu = *payload.ConstChunks().begin();
ConstByteSpan expected_sdu = as_bytes(span(
capture.expected_payload.data(), capture.expected_payload.size()));
EXPECT_TRUE(std::equal(rx_sdu.begin(),
rx_sdu.end(),
expected_sdu.begin(),
expected_sdu.end()));
}});
Result<BFrameWithStorage> bframe =
SetupBFrame(kConnectionHandle,
local_cid,
capture.expected_payload.size() + kSduLengthFieldSize);
H4PacketWithHci h4_packet{emboss::H4PacketType::ACL_DATA,
bframe->acl.hci_span()};
emboss::FirstKFrameWriter kframe = emboss::MakeFirstKFrameView(
bframe->acl.writer.payload().BackingStorage().data(),
bframe->acl.writer.payload().SizeInBytes());
kframe.sdu_length().Write(capture.expected_payload.size());
PW_CHECK(TryToCopyToEmbossStruct(/*emboss_dest=*/kframe.payload(),
/*src=*/capture.expected_payload));
proxy.HandleH4HciFromController(std::move(h4_packet));
EXPECT_EQ(capture.sdus_received, 1);
}
TEST_F(L2capCocReassemblyTest, SduReceivedWhenSegmentedOverFullRangeOfMps) {
pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn(
[](H4PacketWithH4&&) {});
pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn(
[](H4PacketWithHci&&) {});
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/0,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
uint16_t local_cid = 0x234;
struct {
uint16_t sdus_received = 0;
std::array<uint8_t, 19> expected_payload = {
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19};
} capture;
L2capCoc channel = BuildCoc(
proxy,
{.handle = kConnectionHandle,
.local_cid = local_cid,
.receive_fn = [&capture](multibuf::MultiBuf&& payload) {
++capture.sdus_received;
ASSERT_FALSE(payload.empty());
ConstByteSpan rx_sdu = *payload.ConstChunks().begin();
ConstByteSpan expected_sdu = as_bytes(span(
capture.expected_payload.data(), capture.expected_payload.size()));
EXPECT_TRUE(std::equal(rx_sdu.begin(),
rx_sdu.end(),
expected_sdu.begin(),
expected_sdu.end()));
}});
uint16_t sdus_sent = 0;
// Test sending payload segmented in every possible way, from MPS of 2 octets
// to MPS values 5 octets greater than the payload size.
for (uint16_t mps = 2; mps < capture.expected_payload.size() + 5; ++mps) {
for (size_t segment_no = 0;; ++segment_no) {
Result<KFrameWithStorage> kframe =
SetupKFrame(kConnectionHandle,
local_cid,
mps,
segment_no,
span(capture.expected_payload));
if (!kframe.ok()) {
break;
}
H4PacketWithHci h4_packet{emboss::H4PacketType::ACL_DATA,
kframe->acl.hci_span()};
proxy.HandleH4HciFromController(std::move(h4_packet));
}
++sdus_sent;
}
EXPECT_EQ(capture.sdus_received, sdus_sent);
}
TEST_F(L2capCocReassemblyTest, ErrorIfPayloadBytesExceedSduLength) {
pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn(
[](H4PacketWithH4&&) {});
pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn(
[](H4PacketWithHci&&) {});
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/0,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
uint16_t local_cid = 0x234;
int events_received = 0;
L2capCoc channel =
BuildCoc(proxy,
{
.handle = kConnectionHandle,
.local_cid = local_cid,
.receive_fn = [](multibuf::MultiBuf&&) { FAIL(); },
.event_fn =
[&events_received](L2capChannelEvent event) {
++events_received;
EXPECT_EQ(event, L2capChannelEvent::kRxInvalid);
},
});
constexpr uint16_t kIndicatedSduLength = 5;
// First PDU will be 2 bytes for SDU length field + 2 payload bytes
// Second PDU will have 4 payload bytes, which will exceed SDU length by 1.
constexpr uint16_t kFirstPayloadLength = 2;
std::array<uint8_t, kFirstKFrameOverAclMinSize + kFirstPayloadLength> hci_arr;
hci_arr.fill(0);
H4PacketWithHci first_h4_packet{emboss::H4PacketType::ACL_DATA, hci_arr};
Result<emboss::AclDataFrameWriter> acl =
MakeEmbossWriter<emboss::AclDataFrameWriter>(hci_arr);
acl->header().handle().Write(kConnectionHandle);
acl->data_total_length().Write(emboss::FirstKFrame::MinSizeInBytes() +
kFirstPayloadLength);
emboss::FirstKFrameWriter kframe = emboss::MakeFirstKFrameView(
acl->payload().BackingStorage().data(), acl->data_total_length().Read());
kframe.pdu_length().Write(kSduLengthFieldSize + kFirstPayloadLength);
kframe.channel_id().Write(local_cid);
kframe.sdu_length().Write(kIndicatedSduLength);
proxy.HandleH4HciFromController(std::move(first_h4_packet));
H4PacketWithHci second_h4_packet{emboss::H4PacketType::ACL_DATA, hci_arr};
proxy.HandleH4HciFromController(std::move(second_h4_packet));
EXPECT_EQ(events_received, 1);
}
TEST_F(L2capCocReassemblyTest, ErrorIfRxBufferTooSmallForFirstKFrame) {
pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn(
[](H4PacketWithH4&&) {});
pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn(
[](H4PacketWithHci&&) {});
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/0,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
uint16_t local_cid = 0x234;
int events_received = 0;
L2capCoc channel =
BuildCoc(proxy,
{
.handle = kConnectionHandle,
.local_cid = local_cid,
.receive_fn = [](multibuf::MultiBuf&&) { FAIL(); },
.event_fn =
[&events_received](L2capChannelEvent event) {
++events_received;
EXPECT_EQ(event, L2capChannelEvent::kRxInvalid);
},
});
std::array<uint8_t, kFirstKFrameOverAclMinSize - 1> hci_arr;
hci_arr.fill(0);
H4PacketWithHci h4_packet{emboss::H4PacketType::ACL_DATA, hci_arr};
Result<emboss::AclDataFrameWriter> acl =
MakeEmbossWriter<emboss::AclDataFrameWriter>(hci_arr);
acl->header().handle().Write(kConnectionHandle);
acl->data_total_length().Write(emboss::FirstKFrame::MinSizeInBytes() - 1);
emboss::FirstKFrameWriter kframe = emboss::MakeFirstKFrameView(
acl->payload().BackingStorage().data(), acl->data_total_length().Read());
EXPECT_TRUE(!kframe.IsComplete());
kframe.pdu_length().Write(1);
kframe.channel_id().Write(local_cid);
proxy.HandleH4HciFromController(std::move(h4_packet));
EXPECT_EQ(events_received, 1);
}
// ########## L2capCocSegmentation
class L2capCocSegmentationTest : public ProxyHostTest {};
TEST_F(L2capCocSegmentationTest, SduSentWhenSegmentedOverFullRangeOfMps) {
constexpr size_t kPayloadSize = 312;
struct {
uint16_t remote_cid = 0x456;
uint16_t sdus_received = 0;
uint16_t mps;
std::array<uint8_t, kPayloadSize> expected_payload;
} capture;
for (size_t i = 0; i < capture.expected_payload.size(); ++i) {
capture.expected_payload[i] = i % UINT8_MAX;
}
pw::Function<void(H4PacketWithH4 && packet)> send_to_controller_fn(
[&capture](H4PacketWithH4&& tx_kframe) {
static uint16_t segment_no = 0;
static uint16_t pdu_bytes_received = 0;
PW_TEST_ASSERT_OK_AND_ASSIGN(
KFrameWithStorage expected_kframe,
SetupKFrame(kConnectionHandle,
capture.remote_cid,
capture.mps,
segment_no,
span(capture.expected_payload)));
EXPECT_TRUE(std::equal(tx_kframe.GetHciSpan().begin(),
tx_kframe.GetHciSpan().end(),
expected_kframe.acl.hci_span().begin(),
expected_kframe.acl.hci_span().end()));
pdu_bytes_received +=
expected_kframe.acl.writer.data_total_length().Read() -
emboss::BasicL2capHeader::IntrinsicSizeInBytes();
if (pdu_bytes_received ==
capture.expected_payload.size() + kSduLengthFieldSize) {
++capture.sdus_received;
segment_no = 0;
pdu_bytes_received = 0;
} else {
++segment_no;
}
});
pw::Function<void(H4PacketWithHci && packet)> send_to_host_fn(
[](H4PacketWithHci&&) {});
auto* allocator = GetProxyHostAllocator();
ProxyHost proxy = ProxyHost(std::move(send_to_host_fn),
std::move(send_to_controller_fn),
/*le_acl_credits_to_reserve=*/UINT8_MAX,
/*br_edr_acl_credits_to_reserve=*/0,
allocator);
StartDispatcherOnCurrentThread(proxy);
PW_TEST_EXPECT_OK(SendLeReadBufferResponseFromController(
proxy,
/*num_credits_to_reserve=*/UINT8_MAX,
/*le_acl_data_packet_length=*/UINT16_MAX));
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kConnectionHandle, emboss::StatusCode::SUCCESS));
uint16_t sdus_sent = 0;
// Test sending payload segmented in every possible way, from MPS of 23 octets
// to MPS values 5 octets greater than the payload size. 23 bytes is the
// minimum MPS supported for L2CAP channels.
for (capture.mps = 23; capture.mps < kPayloadSize + 5; ++capture.mps) {
L2capCoc channel = BuildCoc(proxy,
{.handle = kConnectionHandle,
.remote_cid = capture.remote_cid,
.tx_mtu = capture.expected_payload.size(),
.tx_mps = capture.mps,
.tx_credits = UINT8_MAX});
auto mbuf_result =
multibuf::FromSpan(*allocator,
as_writable_bytes(span(capture.expected_payload)),
[](ByteSpan) {});
ASSERT_TRUE(mbuf_result.has_value());
multibuf::MultiBuf mbuf = std::move(*mbuf_result);
PW_TEST_EXPECT_OK(channel.Write(std::move(mbuf)).status);
RunDispatcher();
++sdus_sent;
// Replenish proxy's LE ACL send credits, or else only UINT8_MAX PDUs could
// be sent in this test.
PW_TEST_EXPECT_OK(SendNumberOfCompletedPackets(
proxy,
{{kConnectionHandle,
static_cast<uint8_t>(UINT8_MAX - proxy.GetNumFreeLeAclPackets())}}));
}
EXPECT_EQ(capture.sdus_received, sdus_sent);
}
} // namespace
} // namespace pw::bluetooth::proxy