blob: 1f3bd96f9337428b06be5ad9603b66260cfe8b6a [file]
// 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 <cstddef>
#include <cstring>
#include <optional>
#include <utility>
#include "pw_allocator/testing.h"
#include "pw_async2/dispatcher_for_test.h"
#include "pw_bytes/endian.h"
#include "pw_rpc2/internal/call.h"
#include "pw_rpc2/internal/client_connection_task.h"
#include "pw_rpc2/internal/handshake.h"
#include "pw_rpc2/internal/packet.h"
#include "pw_rpc2/internal/packet_testing.h"
#include "pw_rpc2/internal/server_connection_task.h"
#include "pw_rpc2/internal/test_utils.h"
#include "pw_rpc2/server.h"
#include "pw_transport/transport.h"
#include "pw_unit_test/framework.h"
namespace pw::rpc2::internal {
namespace {
namespace flags = ::pw::rpc2::internal::flags;
// Arms one call's ingress reservation, so that the connection task can deliver
// a message to it without backpressure.
// Acquires a slot in a call's ingress queue, as the connection task
// does when it has a message to deliver.
class ReserveSlotTask : public async2::Task {
public:
explicit ReserveSlotTask(Call& call) : call_(call) {}
async2::Poll<> DoPend(async2::Context& cx) override {
reserved_ = call_.ReserveMessageSlot(cx);
return async2::Ready();
}
bool reserved() const { return reserved_; }
private:
Call& call_;
bool reserved_ = false;
};
TEST(ServerDispatchTest, DispatchesIncomingRequest) {
allocator::test::AllocatorForTest<16384> allocator;
auto [conn, raw_conn] = test::MakeMockConnection(allocator);
async2::DispatcherForTest dispatcher;
Server server(allocator, dispatcher);
auto task = dispatcher.Post<ServerConnectionTask>(
allocator, EstablishedConnection{conn}, allocator, server);
ASSERT_NE(task, nullptr);
std::byte payload[4] = {
std::byte{1}, std::byte{2}, std::byte{3}, std::byte{4}};
auto pkt = PacketFramer::FrameStartUnaryPacket(
allocator, /*call_id=*/10, /*service_id=*/20, /*method_id=*/30, payload);
ASSERT_TRUE(pkt.ok());
raw_conn->SetNextRead(std::move(*pkt));
dispatcher.RunUntilStalled();
// Service 20 is not registered, so the request is rejected. Receiving the
// error at all proves the packet was decoded and routed to the server.
ASSERT_EQ(raw_conn->written_packet_count(), 1u);
auto response =
InboundPacket::Decode(ConstBuf::Unowned(raw_conn->written_packet(0)));
ASSERT_TRUE(response.ok());
EXPECT_EQ(response->type(),
(PacketType::Make<flags::kServer, flags::kErrorTerminal>()));
EXPECT_EQ(response->call_id(), 10u);
EXPECT_EQ(response->server_error(), ServerError::kUnknownService);
// This connection was built by hand rather than accepted, so the server does
// not know to close it. Unpost it here: the dispatcher outlives the server,
// and would otherwise be the one destroying it, after the server it refers
// to is gone.
task->Deregister();
}
TEST(ServerDispatchTest, CallRegistriesAreScopedToTheirConnectionTask) {
allocator::test::AllocatorForTest<16384> allocator;
async2::DispatcherForTest dispatcher;
auto [conn1, raw_conn1] = test::MakeMockConnection(allocator);
auto [conn2, raw_conn2] = test::MakeMockConnection(allocator);
auto task1 = dispatcher.Post<ClientConnectionTask>(
allocator, EstablishedConnection{conn1}, allocator);
auto task2 = dispatcher.Post<ClientConnectionTask>(
allocator, EstablishedConnection{conn2}, allocator);
ASSERT_NE(task1, nullptr);
ASSERT_NE(task2, nullptr);
// The same call ID on two different connections.
auto call1 = internal::ClientCall::Create(*task1, 42u, allocator);
ASSERT_NE(call1, nullptr);
auto call2 = internal::ClientCall::Create(*task2, 42u, allocator);
ASSERT_NE(call2, nullptr);
// Deliver a message for call 42 on the first connection only.
std::byte data[3] = {std::byte{1}, std::byte{2}, std::byte{3}};
auto pkt = PacketFramer::FrameServerMessagePacket(allocator, 42u, data);
ASSERT_TRUE(pkt.ok());
raw_conn1->SetNextRead(std::move(*pkt));
task1->Wake();
dispatcher.RunUntilStalled();
// Only the call on that connection received it: its one-message queue is now
// full, while the identically numbered call on the other connection is
// untouched.
ReserveSlotTask reserve1(*call1);
ReserveSlotTask reserve2(*call2);
dispatcher.Post(reserve1);
dispatcher.Post(reserve2);
dispatcher.RunUntilStalled();
reserve1.Deregister();
reserve2.Deregister();
EXPECT_FALSE(reserve1.reserved());
EXPECT_TRUE(reserve2.reserved());
}
TEST(ServerDispatchTest, ConnectionSeveredAbortsActiveCallsImmediately) {
allocator::test::AllocatorForTest<16384> allocator;
async2::DispatcherForTest dispatcher;
{
auto [conn, raw_conn] = test::MakeMockConnection(allocator);
auto task = dispatcher.Post<ClientConnectionTask>(
allocator, EstablishedConnection{conn}, allocator);
ASSERT_NE(task, nullptr);
auto call1 = internal::ClientCall::Create(*task, 1u, allocator);
ASSERT_NE(call1, nullptr);
auto call2 = internal::ClientCall::Create(*task, 2u, allocator);
ASSERT_NE(call2, nullptr);
task->CloseConnection(Status::Aborted());
EXPECT_TRUE(call1->is_completed());
EXPECT_EQ(call1->completion_status(), Status::Aborted());
EXPECT_TRUE(call2->is_completed());
EXPECT_EQ(call2->completion_status(), Status::Aborted());
}
dispatcher.RunUntilStalled();
EXPECT_EQ(allocator.metrics().allocated_bytes.value(), 0u);
}
TEST(ServerDispatchTest, PacketDecodeRejectsTruncatedPacket) {
allocator::test::AllocatorForTest<16384> allocator;
// A buffer too short to hold even the common header.
{
pw::Buf buf =
pw::Buf::Allocate(allocator, sizeof(internal::PacketHeader) - 1);
ASSERT_FALSE(buf.empty());
std::memset(buf.data(), 0, buf.size());
auto decode_res =
internal::InboundPacket::Decode(pw::ConstBuf(std::move(buf)));
EXPECT_EQ(decode_res.status(), Status::DataLoss());
}
// A buffer with a complete common header, but too short for the request
// fields that the packet type implies.
{
pw::Buf buf =
pw::Buf::Allocate(allocator, sizeof(internal::RequestWireFormat) - 1);
ASSERT_FALSE(buf.empty());
std::memset(buf.data(), 0, buf.size());
buf[offsetof(internal::PacketHeader, type)] = static_cast<std::byte>(
flags::kStart | flags::kHasPayload | flags::kStreamEnd);
auto decode_res =
internal::InboundPacket::Decode(pw::ConstBuf(std::move(buf)));
EXPECT_EQ(decode_res.status(), Status::DataLoss());
}
}
TEST(ServerDispatchTest, PacketDecodeRejectsUnrecognizedType) {
allocator::test::AllocatorForTest<16384> allocator;
pw::Buf buf = pw::Buf::Allocate(allocator, sizeof(internal::PacketHeader));
ASSERT_FALSE(buf.empty());
std::memset(buf.data(), 0, buf.size());
buf[offsetof(internal::PacketHeader, type)] = std::byte{0xFF};
auto decode_res =
internal::InboundPacket::Decode(pw::ConstBuf(std::move(buf)));
EXPECT_EQ(decode_res.status(), Status::InvalidArgument());
}
TEST(ServerDispatchTest, HandshakePacketDecodeVersionZeroRejection) {
allocator::test::AllocatorForTest<16384> allocator;
pw::Buf buf =
pw::Buf::Allocate(allocator, internal::HandshakePacket::kWireSizeBytes);
ASSERT_FALSE(buf.empty());
internal::HandshakePacket packet(internal::HandshakePacket::Type::kSyn);
EXPECT_TRUE(packet.Encode(ByteSpan(buf)).ok());
buf[4] = static_cast<std::byte>(0); // Version 0
auto decode_res = internal::HandshakePacket::Decode(buf);
EXPECT_FALSE(decode_res.ok());
EXPECT_EQ(decode_res.status(), Status::DataLoss());
}
} // namespace
} // namespace pw::rpc2::internal