blob: 2bcd05d5dca624063d60db0d76c31d1871f51072 [file] [log] [blame] [edit]
// Copyright 2025 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_proxy/internal/recombiner.h"
#include <cstdint>
#include <mutex>
#include <optional>
#include "pw_bluetooth_proxy/internal/locked_l2cap_channel.h"
#include "pw_bluetooth_proxy/proxy_host.h"
#include "pw_bluetooth_proxy_private/test_utils.h"
#include "pw_containers/to_array.h"
#include "pw_span/cast.h"
#include "pw_status/status.h"
#include "pw_unit_test/framework.h"
namespace pw::bluetooth::proxy {
namespace {
using pw::containers::to_array;
class RecombinerTest : public ProxyHostTest {};
TEST_F(RecombinerTest, InactiveAtCreation) {
Recombiner recombiner{Direction::kFromHost};
EXPECT_FALSE(recombiner.IsActive());
}
TEST_F(RecombinerTest, Start) {
ProxyHost proxy_{[]([[maybe_unused]] H4PacketWithHci&& packet) {},
[]([[maybe_unused]] H4PacketWithH4&& packet) {},
0,
0};
BasicL2capChannel channel = BuildBasicL2capChannel(proxy_, {});
pw::sync::Mutex mutex;
LockedL2capChannel locked_channel{channel, std::unique_lock(mutex)};
Recombiner recombiner{Direction::kFromHost};
PW_TEST_EXPECT_OK(recombiner.StartRecombination(locked_channel, 8u));
EXPECT_TRUE(recombiner.IsActive());
EXPECT_FALSE(recombiner.IsComplete());
}
TEST_F(RecombinerTest, GetLocalCid) {
constexpr uint16_t kLocalCid = 0x20;
ProxyHost proxy_{[]([[maybe_unused]] H4PacketWithHci&& packet) {},
[]([[maybe_unused]] H4PacketWithH4&& packet) {},
0,
0};
BasicL2capChannel channel =
BuildBasicL2capChannel(proxy_, {.local_cid = kLocalCid});
pw::sync::Mutex mutex;
LockedL2capChannel locked_channel{channel, std::unique_lock(mutex)};
Recombiner recombiner{Direction::kFromController};
PW_TEST_EXPECT_OK(recombiner.StartRecombination(locked_channel, 8u));
EXPECT_EQ(recombiner.local_cid(), kLocalCid);
}
TEST_F(RecombinerTest, EndWithChannel) {
ProxyHost proxy_{[]([[maybe_unused]] H4PacketWithHci&& packet) {},
[]([[maybe_unused]] H4PacketWithH4&& packet) {},
0,
0};
BasicL2capChannel channel = BuildBasicL2capChannel(proxy_, {});
pw::sync::Mutex mutex;
std::optional<LockedL2capChannel> locked_channel{
LockedL2capChannel{channel, std::unique_lock(mutex)}};
Recombiner recombiner{Direction::kFromController};
PW_TEST_EXPECT_OK(recombiner.StartRecombination(*locked_channel, 8u));
EXPECT_TRUE(recombiner.IsActive());
EXPECT_FALSE(recombiner.IsComplete());
recombiner.EndRecombination(locked_channel);
EXPECT_FALSE(recombiner.IsActive());
}
TEST_F(RecombinerTest, EndWithoutChannel) {
ProxyHost proxy_{[]([[maybe_unused]] H4PacketWithHci&& packet) {},
[]([[maybe_unused]] H4PacketWithH4&& packet) {},
0,
0};
Recombiner recombiner{Direction::kFromController};
{
BasicL2capChannel channel = BuildBasicL2capChannel(proxy_, {});
pw::sync::Mutex mutex;
std::optional<LockedL2capChannel> locked_channel{
LockedL2capChannel{channel, std::unique_lock(mutex)}};
PW_TEST_EXPECT_OK(recombiner.StartRecombination(*locked_channel, 8u));
}
EXPECT_TRUE(recombiner.IsActive());
EXPECT_FALSE(recombiner.IsComplete());
std::optional<LockedL2capChannel> null_channel = std::nullopt;
recombiner.EndRecombination(null_channel);
EXPECT_FALSE(recombiner.IsActive());
}
TEST_F(RecombinerTest, WriteThenTake) {
ProxyHost proxy_{[]([[maybe_unused]] H4PacketWithHci&& packet) {},
[]([[maybe_unused]] H4PacketWithH4&& packet) {},
0,
0};
Direction kDirection = Direction::kFromController;
Recombiner recombiner{kDirection};
BasicL2capChannel channel = BuildBasicL2capChannel(proxy_, {});
pw::sync::Mutex mutex;
std::optional<LockedL2capChannel> locked_channel{
LockedL2capChannel{channel, std::unique_lock(mutex)}};
PW_TEST_EXPECT_OK(recombiner.StartRecombination(*locked_channel, 8u));
EXPECT_TRUE(recombiner.IsActive());
EXPECT_FALSE(recombiner.IsComplete());
static constexpr std::array<uint8_t, 8> kExpectedData = {
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88};
// Write first chunk
PW_TEST_EXPECT_OK(recombiner.RecombineFragment(
locked_channel, to_array<uint8_t>({0x11, 0x22, 0x33, 0x44})));
EXPECT_TRUE(recombiner.IsActive());
EXPECT_FALSE(recombiner.IsComplete());
// Write second chunk
PW_TEST_EXPECT_OK(recombiner.RecombineFragment(
locked_channel, to_array<uint8_t>({0x55, 0x66, 0x77, 0x88})));
// We have read all expected bytes.
EXPECT_TRUE(recombiner.IsComplete());
// We are no longer recombining.
EXPECT_FALSE(recombiner.IsActive());
multibuf::MultiBuf mbuf = Recombiner::TakeBuf(locked_channel, kDirection);
EXPECT_TRUE(mbuf.IsContiguous());
pw::span<uint8_t> span =
pw::span_cast<uint8_t>(mbuf.ContiguousSpan().value());
EXPECT_TRUE(std::equal(
span.begin(), span.end(), kExpectedData.begin(), kExpectedData.end()));
}
TEST_F(RecombinerTest, WriteCompleteWithoutChannel) {
ProxyHost proxy_{[]([[maybe_unused]] H4PacketWithHci&& packet) {},
[]([[maybe_unused]] H4PacketWithH4&& packet) {},
0,
0};
Recombiner recombiner{Direction::kFromController};
{
BasicL2capChannel channel = BuildBasicL2capChannel(proxy_, {});
pw::sync::Mutex mutex;
std::optional<LockedL2capChannel> locked_channel{
LockedL2capChannel{channel, std::unique_lock(mutex)}};
PW_TEST_EXPECT_OK(recombiner.StartRecombination(*locked_channel, 8u));
EXPECT_TRUE(recombiner.IsActive());
EXPECT_FALSE(recombiner.IsComplete());
// Write first chunk
PW_TEST_EXPECT_OK(recombiner.RecombineFragment(
locked_channel, to_array<uint8_t>({0x11, 0x22, 0x33, 0x44})));
EXPECT_TRUE(recombiner.IsActive());
EXPECT_FALSE(recombiner.IsComplete());
}
std::optional<LockedL2capChannel> null_channel = std::nullopt;
// Write second chunk
PW_TEST_EXPECT_OK(recombiner.RecombineFragment(
null_channel, to_array<uint8_t>({0x55, 0x66, 0x77, 0x88})));
// We have read all expected bytes.
EXPECT_TRUE(recombiner.IsComplete());
// We are no longer recombining.
EXPECT_FALSE(recombiner.IsActive());
recombiner.EndRecombination(null_channel);
EXPECT_FALSE(recombiner.IsActive());
}
TEST_F(RecombinerTest, RecombinedPduIsLargerThanSpecified) {
ProxyHost proxy_{[]([[maybe_unused]] H4PacketWithHci&& packet) {},
[]([[maybe_unused]] H4PacketWithH4&& packet) {},
0,
0};
Recombiner recombiner{Direction::kFromController};
BasicL2capChannel channel = BuildBasicL2capChannel(proxy_, {});
pw::sync::Mutex mutex;
std::optional<LockedL2capChannel> locked_channel{
LockedL2capChannel{channel, std::unique_lock(mutex)}};
PW_TEST_EXPECT_OK(recombiner.StartRecombination(*locked_channel, 8u));
EXPECT_TRUE(recombiner.IsActive());
EXPECT_FALSE(recombiner.IsComplete());
// Write first chunk
PW_TEST_EXPECT_OK(recombiner.RecombineFragment(
locked_channel, to_array<uint8_t>({0x11, 0x22, 0x33, 0x44})));
EXPECT_TRUE(recombiner.IsActive());
EXPECT_FALSE(recombiner.IsComplete());
// Try to write too large a second chunk.
EXPECT_EQ(
recombiner.RecombineFragment(
locked_channel, to_array<uint8_t>({0x55, 0x66, 0x77, 0x88, 0x99})),
pw::Status::ResourceExhausted());
// Should still not be complete.
EXPECT_FALSE(recombiner.IsComplete());
// Still waiting for a packet of the right size.
EXPECT_TRUE(recombiner.IsActive());
// Client ends recombination at this point.
recombiner.EndRecombination(locked_channel);
EXPECT_FALSE(recombiner.IsActive());
}
} // namespace
} // namespace pw::bluetooth::proxy