blob: dd2b62b71a0598ce19f8df484d0478bf749451be [file] [edit]
// Copyright 2026 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/config.h"
#if PW_BLUETOOTH_PROXY_CONFIG_ENABLE_RECOVERY
#include <cstdint>
#include <utility>
#include "pw_bluetooth_proxy/acl_snapshot.h"
#include "pw_bluetooth_proxy/h4_packet.h"
#include "pw_bluetooth_proxy/proxy_host.h"
#include "pw_bluetooth_proxy_private/test_utils.h"
#include "pw_function/function.h"
#include "pw_status/status.h"
#include "pw_unit_test/framework.h"
namespace pw::bluetooth::proxy {
namespace {
constexpr uint16_t kLeConnectionHandle1 = 0x123;
constexpr uint16_t kLeConnectionHandle2 = 0x124;
constexpr uint16_t kBrEdrConnectionHandle = 0x456;
constexpr uint16_t kLeMaxAclCredits = 10;
constexpr uint16_t kBrEdrMaxAclCredits = 5;
class AclRecoveryTest : public ProxyHostTest {
protected:
static AclConnectionSnapshot CreateAclConnectionSnapshot(
uint16_t connection_handle = kLeConnectionHandle1,
AclTransportType transport = AclTransportType::kLe,
uint16_t num_proxy_pending = 0,
uint16_t num_host_pending = 0,
uint16_t num_queued_host = 0) {
AclConnectionSnapshot snapshot;
snapshot.connection_handle = connection_handle;
snapshot.transport = transport;
snapshot.num_proxy_pending_packets = num_proxy_pending;
snapshot.num_host_pending_packets = num_host_pending;
snapshot.num_queued_host_packets = num_queued_host;
return snapshot;
}
static AclSnapshot CreateAclSnapshot(
uint16_t le_max = kLeMaxAclCredits,
uint16_t le_pending = 0,
uint16_t br_edr_max = kBrEdrMaxAclCredits,
uint16_t br_edr_pending = 0,
bool incomplete = false) {
AclSnapshot snapshot;
snapshot.snapshot_incomplete = incomplete;
snapshot.le_transport.controller_max_packets = le_max;
snapshot.le_transport.pending = le_pending;
snapshot.br_edr_transport.controller_max_packets = br_edr_max;
snapshot.br_edr_transport.pending = br_edr_pending;
return snapshot;
}
};
TEST_F(AclRecoveryTest, SnapshotCaptureAndRecover) {
constexpr uint16_t kLePendingAclCredits = 2;
constexpr uint16_t kBrEdrPendingAclCredits = 1;
Function<void(H4PacketWithHci && packet)> send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
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=*/2,
/*br_edr_acl_credits_to_reserve=*/2,
GetProxyHostAllocator());
StartDispatcherOnCurrentThread(proxy);
AclSnapshot snapshot = CreateAclSnapshot(kLeMaxAclCredits,
kLePendingAclCredits,
kBrEdrMaxAclCredits,
kBrEdrPendingAclCredits);
snapshot.acl_connections.push_back(
CreateAclConnectionSnapshot(kLeConnectionHandle1,
AclTransportType::kLe,
/*num_proxy_pending=*/kLePendingAclCredits,
/*num_host_pending=*/2,
/*num_queued_host=*/0));
snapshot.acl_connections.push_back(
CreateAclConnectionSnapshot(kBrEdrConnectionHandle,
AclTransportType::kBrEdr,
/*num_proxy_pending=*/kBrEdrPendingAclCredits,
/*num_host_pending=*/1,
/*num_queued_host=*/0));
PW_TEST_ASSERT_OK(proxy.RecoverAclFromSnapshot(snapshot));
std::optional<AclConnectionSnapshot> connection_snapshot;
proxy.RegisterAclStateUpdateCallback(
[&connection_snapshot](const AclStateUpdate& update) {
connection_snapshot = update.connection;
});
Result<AclFrameWithStorage> acl_frame = SetupAcl(kLeConnectionHandle1, 10);
ASSERT_TRUE(acl_frame.ok());
proxy.HandleH4HciFromHost(
H4PacketWithH4(emboss::H4PacketType::ACL_DATA, acl_frame->h4_span()));
// Verify that the first connection was restored correctly. Expect
// num_host_pending_packets to be 3 (2 restored + 1 new).
ASSERT_TRUE(connection_snapshot.has_value());
EXPECT_EQ(connection_snapshot->connection_handle, kLeConnectionHandle1);
EXPECT_EQ(connection_snapshot->transport, AclTransportType::kLe);
EXPECT_EQ(connection_snapshot->num_proxy_pending_packets,
kLePendingAclCredits);
EXPECT_EQ(connection_snapshot->num_host_pending_packets, 3);
EXPECT_EQ(connection_snapshot->num_queued_host_packets, 0);
acl_frame = SetupAcl(kBrEdrConnectionHandle, 10);
ASSERT_TRUE(acl_frame.ok());
connection_snapshot.reset();
proxy.HandleH4HciFromHost(
H4PacketWithH4(emboss::H4PacketType::ACL_DATA, acl_frame->h4_span()));
// Verify that the second connection was restored correctly. Expect
// num_host_pending_packets to be 2 (1 restored + 1 new).
ASSERT_TRUE(connection_snapshot.has_value());
EXPECT_EQ(connection_snapshot->connection_handle, kBrEdrConnectionHandle);
EXPECT_EQ(connection_snapshot->transport, AclTransportType::kBrEdr);
EXPECT_EQ(connection_snapshot->num_proxy_pending_packets,
kBrEdrPendingAclCredits);
EXPECT_EQ(connection_snapshot->num_host_pending_packets, 2);
EXPECT_EQ(connection_snapshot->num_queued_host_packets, 0);
}
TEST_F(AclRecoveryTest, SnapshotRecoverFailsOnIncomplete) {
Function<void(H4PacketWithHci && packet)> send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
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=*/2,
/*br_edr_acl_credits_to_reserve=*/0,
GetProxyHostAllocator());
StartDispatcherOnCurrentThread(proxy);
AclSnapshot snapshot;
snapshot.snapshot_incomplete = true;
EXPECT_EQ(proxy.RecoverAclFromSnapshot(snapshot), Status::DataLoss());
}
TEST_F(AclRecoveryTest, RegisterStateUpdateCallback) {
Function<void(H4PacketWithHci && packet)> send_to_host_fn(
[]([[maybe_unused]] H4PacketWithHci&& packet) {});
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=*/2,
/*br_edr_acl_credits_to_reserve=*/0,
GetProxyHostAllocator());
StartDispatcherOnCurrentThread(proxy);
PW_TEST_ASSERT_OK(SendLeReadBufferResponseFromController(proxy, 10));
struct {
uint32_t updates_sent = 0;
AclStateUpdate last_update;
} update_capture;
proxy.RegisterAclStateUpdateCallback(
[&update_capture](const AclStateUpdate& update) {
update_capture.updates_sent++;
update_capture.last_update = update;
});
// Verify that connection creation triggers a state update callback.
PW_TEST_ASSERT_OK(SendLeConnectionCompleteEvent(
proxy, kLeConnectionHandle1, emboss::StatusCode::SUCCESS));
EXPECT_EQ(update_capture.updates_sent, 1u);
AclConnectionSnapshot connection_snapshot =
update_capture.last_update.connection;
EXPECT_EQ(connection_snapshot.connection_handle, kLeConnectionHandle1);
EXPECT_EQ(connection_snapshot.num_proxy_pending_packets, 0);
EXPECT_EQ(connection_snapshot.num_host_pending_packets, 0);
// Verify that sending a packet from the host triggers a state update
// callback.
Result<AclFrameWithStorage> acl_frame = SetupAcl(kLeConnectionHandle1, 10);
ASSERT_TRUE(acl_frame.ok());
H4PacketWithH4 h4_packet(emboss::H4PacketType::ACL_DATA,
acl_frame->h4_span());
proxy.HandleH4HciFromHost(std::move(h4_packet));
EXPECT_EQ(update_capture.updates_sent, 2u);
connection_snapshot = update_capture.last_update.connection;
EXPECT_EQ(connection_snapshot.connection_handle, kLeConnectionHandle1);
EXPECT_EQ(connection_snapshot.num_host_pending_packets, 1);
// Verify that reclaiming credits via NOCP event triggers a state update
// callback.
PW_TEST_ASSERT_OK(
SendNumberOfCompletedPackets(proxy, {{kLeConnectionHandle1, 1}}));
EXPECT_EQ(update_capture.updates_sent, 3u);
connection_snapshot = update_capture.last_update.connection;
EXPECT_EQ(connection_snapshot.connection_handle, kLeConnectionHandle1);
EXPECT_EQ(connection_snapshot.num_host_pending_packets, 0);
}
TEST_F(AclRecoveryTest, CreditResynchronizationDefersAndSends) {
constexpr uint16_t kQueuedPackets1 = 3;
constexpr uint16_t kQueuedPackets2 = 5;
struct {
std::optional<H4PacketWithHci> captured_packet;
Allocator* allocator_ptr;
} send_capture;
send_capture.allocator_ptr = GetProxyHostAllocator();
Function<void(H4PacketWithHci && packet)> send_to_host_fn(
[&send_capture](H4PacketWithHci&& packet) {
send_capture.captured_packet =
H4PacketWithHci::CopyFrom(*send_capture.allocator_ptr, packet)
.value();
});
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=*/2,
/*br_edr_acl_credits_to_reserve=*/0,
GetProxyHostAllocator());
StartDispatcherOnCurrentThread(proxy);
// Create a snapshot where connections had queued packets that were lost.
AclSnapshot snapshot = CreateAclSnapshot();
snapshot.acl_connections.push_back(
CreateAclConnectionSnapshot(kLeConnectionHandle1,
AclTransportType::kLe,
/*num_proxy_pending=*/0,
/*num_host_pending=*/0,
kQueuedPackets1));
snapshot.acl_connections.push_back(
CreateAclConnectionSnapshot(kLeConnectionHandle2,
AclTransportType::kLe,
/*num_proxy_pending=*/0,
/*num_host_pending=*/0,
kQueuedPackets2));
PW_TEST_ASSERT_OK(proxy.RecoverAclFromSnapshot(snapshot));
// Verify that no refund event is sent to host immediately.
EXPECT_FALSE(send_capture.captured_packet.has_value());
proxy.InitiateAclCreditResynchronization();
// Verify that a NUMBER_OF_COMPLETED_PACKETS event was sent with correct
// refunds.
ASSERT_TRUE(send_capture.captured_packet.has_value());
auto view = MakeEmbossView<emboss::NumberOfCompletedPacketsEventView>(
send_capture.captured_packet->GetHciSpan());
ASSERT_TRUE(view.ok());
EXPECT_EQ(view->header().event_code().Read(),
emboss::EventCode::NUMBER_OF_COMPLETED_PACKETS);
EXPECT_EQ(view->num_handles().Read(), 2);
EXPECT_EQ(view->nocp_data()[0].connection_handle().Read(),
kLeConnectionHandle1);
EXPECT_EQ(view->nocp_data()[0].num_completed_packets().Read(),
kQueuedPackets1);
EXPECT_EQ(view->nocp_data()[1].connection_handle().Read(),
kLeConnectionHandle2);
EXPECT_EQ(view->nocp_data()[1].num_completed_packets().Read(),
kQueuedPackets2);
// Verify that subsequent triggers do not send duplicate refunds.
send_capture.captured_packet.reset();
proxy.InitiateAclCreditResynchronization();
EXPECT_FALSE(send_capture.captured_packet.has_value());
}
} // namespace
} // namespace pw::bluetooth::proxy
#endif // PW_BLUETOOTH_PROXY_CONFIG_ENABLE_RECOVERY