blob: 0b8dbc4c416224c149376aa92889c880a6dbc376 [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 "pw_rpc2/client_call.h"
#include <cstddef>
#include <cstring>
#include <optional>
#include <type_traits>
#include <utility>
#include "pw_allocator/testing.h"
#include "pw_assert/check.h"
#include "pw_async2/dispatcher_for_test.h"
#include "pw_bytes/endian.h"
#include "pw_rpc2/client.h"
#include "pw_rpc2/internal/call.h"
#include "pw_rpc2/internal/handshake.h"
#include "pw_rpc2/internal/packet.h"
#include "pw_rpc2/internal/packet_testing.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 {
namespace {
namespace flags = ::pw::rpc2::internal::flags;
static_assert(!std::is_copy_constructible_v<RawReadFuture>);
static_assert(!std::is_copy_assignable_v<RawReadFuture>);
static_assert(std::is_move_constructible_v<RawReadFuture>);
static_assert(std::is_move_assignable_v<RawReadFuture>);
static_assert(!std::is_copy_constructible_v<ReadFuture<pw::ConstBuf>>);
static_assert(!std::is_copy_assignable_v<ReadFuture<pw::ConstBuf>>);
static_assert(std::is_move_constructible_v<ReadFuture<pw::ConstBuf>>);
static_assert(std::is_move_assignable_v<ReadFuture<pw::ConstBuf>>);
static_assert(
!std::is_copy_constructible_v<UnaryCallFuture<pw::ConstBuf, pw::ConstBuf>>);
static_assert(
!std::is_copy_assignable_v<UnaryCallFuture<pw::ConstBuf, pw::ConstBuf>>);
static_assert(
std::is_move_constructible_v<UnaryCallFuture<pw::ConstBuf, pw::ConstBuf>>);
static_assert(
std::is_move_assignable_v<UnaryCallFuture<pw::ConstBuf, pw::ConstBuf>>);
static_assert(!std::is_copy_constructible_v<
ServerStreamFuture<pw::ConstBuf, pw::ConstBuf>>);
static_assert(
!std::is_copy_assignable_v<ServerStreamFuture<pw::ConstBuf, pw::ConstBuf>>);
static_assert(std::is_move_constructible_v<
ServerStreamFuture<pw::ConstBuf, pw::ConstBuf>>);
static_assert(
std::is_move_assignable_v<ServerStreamFuture<pw::ConstBuf, pw::ConstBuf>>);
static_assert(!std::is_copy_constructible_v<
ClientStreamFuture<pw::ConstBuf, pw::ConstBuf>>);
static_assert(
!std::is_copy_assignable_v<ClientStreamFuture<pw::ConstBuf, pw::ConstBuf>>);
static_assert(std::is_move_constructible_v<
ClientStreamFuture<pw::ConstBuf, pw::ConstBuf>>);
static_assert(
std::is_move_assignable_v<ClientStreamFuture<pw::ConstBuf, pw::ConstBuf>>);
static_assert(!std::is_copy_constructible_v<
BidiStreamFuture<pw::ConstBuf, pw::ConstBuf>>);
static_assert(
!std::is_copy_assignable_v<BidiStreamFuture<pw::ConstBuf, pw::ConstBuf>>);
static_assert(
std::is_move_constructible_v<BidiStreamFuture<pw::ConstBuf, pw::ConstBuf>>);
static_assert(
std::is_move_assignable_v<BidiStreamFuture<pw::ConstBuf, pw::ConstBuf>>);
class ClientConnectTask : public async2::Task {
public:
explicit ClientConnectTask(ClientFuture fut) : fut_(std::move(fut)) {}
async2::Poll<> DoPend(async2::Context& cx) override {
auto poll = fut_.Pend(cx);
if (poll.IsPending()) {
return async2::Pending();
}
client_ = std::move(*poll);
return async2::Ready();
}
std::optional<Result<Client>>& client() { return client_; }
const std::optional<Result<Client>>& client() const { return client_; }
~ClientConnectTask() override {
Deregister();
// A `Client` must be closed before it is destroyed. If nothing took
// this one, close it here.
if (client_.has_value() && client_->ok()) {
auto closed = (*client_)->Close();
}
}
private:
ClientFuture fut_;
std::optional<Result<Client>> client_;
};
template <typename Fut = UnaryCallFuture<pw::ConstBuf, pw::ConstBuf>>
class UnaryTestTask : public async2::Task {
public:
explicit UnaryTestTask(Fut fut) : fut_(std::move(fut)) {}
async2::Poll<> DoPend(async2::Context& cx) override {
auto poll = fut_.Pend(cx);
if (poll.IsPending()) {
return async2::Pending();
}
result_ = std::move(*poll);
return async2::Ready();
}
const std::optional<Result<pw::ConstBuf>>& result() const { return result_; }
// Exposed so tests can assert the future's state after it resolves.
const Fut& future() const { return fut_; }
private:
Fut fut_;
std::optional<Result<pw::ConstBuf>> result_;
};
template <typename Fut>
UnaryTestTask(Fut) -> UnaryTestTask<Fut>;
template <typename Fut = RawReadFuture>
class ReadTestTask : public async2::Task {
public:
explicit ReadTestTask(Fut fut) : fut_(std::move(fut)) {}
async2::Poll<> DoPend(async2::Context& cx) override {
auto poll = fut_.Pend(cx);
if (poll.IsPending()) {
return async2::Pending();
}
result_ = std::move(*poll);
return async2::Ready();
}
const std::optional<Result<pw::ConstBuf>>& result() const { return result_; }
private:
Fut fut_;
std::optional<Result<pw::ConstBuf>> result_;
};
template <typename Fut>
ReadTestTask(Fut) -> ReadTestTask<Fut>;
template <typename Fut>
class WriteTestTask : public async2::Task {
public:
explicit WriteTestTask(Fut fut) : fut_(std::move(fut)) {}
async2::Poll<> DoPend(async2::Context& cx) override {
auto poll = fut_.Pend(cx);
if (poll.IsPending()) {
return async2::Pending();
}
result_ = *poll;
return async2::Ready();
}
const std::optional<Status>& result() const { return result_; }
private:
Fut fut_;
std::optional<Status> result_;
};
template <typename Fut>
WriteTestTask(Fut) -> WriteTestTask<Fut>;
template <typename Fut = PacketReservationFuture>
class ReserveTestTask : public async2::Task {
public:
explicit ReserveTestTask(Fut fut) : fut_(std::move(fut)) {}
async2::Poll<> DoPend(async2::Context& cx) override {
auto poll = fut_.Pend(cx);
if (poll.IsPending()) {
return async2::Pending();
}
result_ = std::move(*poll);
return async2::Ready();
}
std::optional<Result<PacketReservation>>& result() { return result_; }
const std::optional<Result<PacketReservation>>& result() const {
return result_;
}
private:
Fut fut_;
std::optional<Result<PacketReservation>> result_;
};
template <typename Fut>
ReserveTestTask(Fut) -> ReserveTestTask<Fut>;
template <typename Fut>
class StreamCallTestTask : public async2::Task {
public:
explicit StreamCallTestTask(Fut fut) : fut_(std::move(fut)) {}
async2::Poll<> DoPend(async2::Context& cx) override {
auto poll = fut_.Pend(cx);
if (poll.IsPending()) {
return async2::Pending();
}
result_ = std::move(*poll);
return async2::Ready();
}
using ResType = typename Fut::value_type;
const std::optional<ResType>& result() const { return result_; }
std::optional<ResType>& result() { return result_; }
private:
Fut fut_;
std::optional<ResType> result_;
};
template <typename Fut>
StreamCallTestTask(Fut) -> StreamCallTestTask<Fut>;
class TestServiceClient : public ServiceClient {
public:
TestServiceClient(const Client& client, uint32_t service_id)
: ServiceClient(client, service_id) {}
using ServiceClient::CallBidiStream;
using ServiceClient::CallClientStream;
using ServiceClient::CallServerStream;
using ServiceClient::CallUnary;
};
Client SetupClient(pw::Allocator& allocator,
async2::DispatcherForTest& dispatcher,
test::MockConnection* raw_conn,
transport::ReliableDatagramSocket conn) {
ClientConnectTask connect_task(Client::Create(dispatcher, allocator, conn));
dispatcher.Post(connect_task);
dispatcher.RunUntilStalled();
pw::Buf hs_resp =
pw::Buf::Allocate(allocator, internal::HandshakePacket::kWireSizeBytes);
auto hs_pkt =
internal::HandshakePacket(internal::HandshakePacket::Type::kSynAck);
auto enc_res = hs_pkt.Encode(std::move(hs_resp));
PW_CHECK(enc_res.ok());
raw_conn->SetNextRead(std::move(enc_res.value()));
dispatcher.RunUntilStalled();
PW_CHECK(connect_task.client().has_value() && connect_task.client()->ok());
Client client = std::move(**connect_task.client());
connect_task.Deregister();
return client;
}
// Frames a server packet of any type that is neither a start nor an error
// packet.
Result<pw::Buf> FrameServerPacket(pw::Allocator& allocator,
internal::PacketType type,
uint32_t call_id,
pw::ConstByteSpan payload = {}) {
PW_CHECK(type.is_server());
return internal::PacketFramer::FramePacket(allocator, type, call_id, payload);
}
// Returns the call ID of the most recent packet the client sent.
uint32_t LastSentCallId(test::MockConnection& raw_conn) {
auto decoded = internal::InboundPacket::Decode(raw_conn.last_written_buf());
PW_CHECK_OK(decoded.status());
return decoded->call_id();
}
// Checks that the most recent packet the client sent reports a method type
// mismatch for `call_id`.
void ExpectSentMethodTypeMismatch(test::MockConnection& raw_conn,
uint32_t call_id) {
auto decoded = internal::InboundPacket::Decode(raw_conn.last_written_buf());
ASSERT_TRUE(decoded.ok());
EXPECT_EQ(decoded->type(),
internal::PacketType::Make<flags::kErrorTerminal>());
EXPECT_EQ(decoded->call_id(), call_id);
EXPECT_EQ(decoded->client_error(),
internal::ClientError::kMethodTypeMismatch);
}
// Server packets that cannot answer a unary or client-streaming call, which
// takes exactly one packet with a payload that completes the RPC.
constexpr internal::PacketType kNotSingleResponse[] = {
internal::PacketType::Make<flags::kServer, flags::kHasPayload>(),
internal::PacketType::Make<flags::kServer, flags::kStreamEnd>(),
internal::PacketType::
Make<flags::kServer, flags::kHasPayload, flags::kStreamEnd>(),
internal::PacketType::Make<flags::kServer, flags::kOkTerminal>(),
};
TEST(ClientCallTest, UnaryCallSendsRequestFirstThenReadsResponse) {
allocator::test::AllocatorForTest<16384> allocator;
async2::DispatcherForTest dispatcher;
auto [conn, raw_conn] = test::MakeMockConnection(allocator);
test::ScopedClient client(SetupClient(allocator, dispatcher, raw_conn, conn),
dispatcher);
pw::Buf req_buf = pw::Buf::Allocate(allocator, 3);
req_buf[0] = std::byte(10);
req_buf[1] = std::byte(20);
req_buf[2] = std::byte(30);
TestServiceClient test_service(client, 100u);
auto unary_call = test_service.CallUnary<pw::ConstBuf, pw::ConstBuf>(
200u, pw::ConstBuf(std::move(req_buf)));
UnaryTestTask task(std::move(unary_call));
dispatcher.Post(task);
dispatcher.RunUntilStalled();
EXPECT_GE(raw_conn->commit_count(), 3u); // 2 handshake + 1 request
auto decode_req =
internal::InboundPacket::Decode(raw_conn->last_written_buf());
ASSERT_TRUE(decode_req.ok());
EXPECT_EQ(decode_req->type(),
(internal::PacketType::
Make<flags::kStart, flags::kHasPayload, flags::kStreamEnd>()));
EXPECT_EQ(decode_req->service_id(), 100u);
EXPECT_EQ(decode_req->method_id(), 200u);
EXPECT_FALSE(task.result().has_value());
pw::Buf resp_payload = pw::Buf::Allocate(allocator, 4);
std::memset(resp_payload.data(), 0xAA, 4);
auto framed_resp = internal::PacketFramer::FrameResponsePacket(
allocator, /*call_id=*/1, pw::ConstByteSpan(resp_payload));
ASSERT_TRUE(framed_resp.ok());
raw_conn->SetNextRead(std::move(*framed_resp));
dispatcher.RunUntilStalled();
ASSERT_TRUE(task.result().has_value() && task.result()->ok());
EXPECT_EQ(task.result()->value().size(), 4u);
task.Deregister();
}
// A default-constructed future is empty: it must report itself as neither
// pendable nor complete, so that polling it is detectably a programming error.
TEST(ClientCallTest, DefaultConstructedUnaryCallFutureIsNotPendable) {
UnaryCallFuture<pw::ConstBuf, pw::ConstBuf> fut;
EXPECT_FALSE(fut.is_pendable());
EXPECT_FALSE(fut.is_complete());
}
// Completion is tracked by the future itself rather than inferred from the
// read half, which never starts if the request fails.
TEST(ClientCallTest, UnaryCallFutureReportsCompleteOnceResolved) {
allocator::test::AllocatorForTest<16384> allocator;
async2::DispatcherForTest dispatcher;
auto [conn, raw_conn] = test::MakeMockConnection(allocator);
test::ScopedClient client(SetupClient(allocator, dispatcher, raw_conn, conn),
dispatcher);
pw::Buf req_buf = pw::Buf::Allocate(allocator, 1);
req_buf[0] = std::byte(7);
TestServiceClient test_service(client, 100u);
auto unary_call = test_service.CallUnary<pw::ConstBuf, pw::ConstBuf>(
200u, pw::ConstBuf(std::move(req_buf)));
EXPECT_TRUE(unary_call.is_pendable());
EXPECT_FALSE(unary_call.is_complete());
UnaryTestTask task(std::move(unary_call));
dispatcher.Post(task);
dispatcher.RunUntilStalled();
pw::Buf resp_payload = pw::Buf::Allocate(allocator, 2);
std::memset(resp_payload.data(), 0xBB, 2);
auto framed_resp = internal::PacketFramer::FrameResponsePacket(
allocator, /*call_id=*/1, pw::ConstByteSpan(resp_payload));
ASSERT_TRUE(framed_resp.ok());
raw_conn->SetNextRead(std::move(*framed_resp));
dispatcher.RunUntilStalled();
ASSERT_TRUE(task.result().has_value() && task.result()->ok());
EXPECT_TRUE(task.future().is_complete());
EXPECT_FALSE(task.future().is_pendable());
task.Deregister();
}
TEST(ClientCallTest, ServerStreamingReceivesMessagesAndStreamEnd) {
allocator::test::AllocatorForTest<16384> allocator;
async2::DispatcherForTest dispatcher;
auto [conn, raw_conn] = test::MakeMockConnection(allocator);
test::ScopedClient client(SetupClient(allocator, dispatcher, raw_conn, conn),
dispatcher);
TestServiceClient test_service(client, 100u);
auto server_stream_call =
test_service.CallServerStream<pw::ConstBuf, pw::ConstBuf>(300u,
pw::ConstBuf());
StreamCallTestTask call_task(std::move(server_stream_call));
dispatcher.Post(call_task);
dispatcher.RunUntilStalled();
ASSERT_TRUE(call_task.result().has_value() && call_task.result()->ok());
Reader<pw::ConstBuf> reader = std::move(**call_task.result());
call_task.Deregister();
// 1. Read message 1
ReadTestTask read1(reader.Read());
dispatcher.Post(read1);
dispatcher.RunUntilStalled();
EXPECT_FALSE(read1.result().has_value());
std::byte chunk1[3] = {std::byte{1}, std::byte{2}, std::byte{3}};
auto pkt1 =
internal::PacketFramer::FrameServerMessagePacket(allocator, 1u, chunk1);
ASSERT_TRUE(pkt1.ok());
raw_conn->SetNextRead(std::move(*pkt1));
dispatcher.RunUntilStalled();
ASSERT_TRUE(read1.result().has_value() && read1.result()->ok());
EXPECT_EQ(read1.result()->value().size(), 3u);
read1.Deregister();
// 2. Read message 2
ReadTestTask read2(reader.Read());
dispatcher.Post(read2);
std::byte chunk2[2] = {std::byte{10}, std::byte{20}};
auto pkt2 =
internal::PacketFramer::FrameServerMessagePacket(allocator, 1u, chunk2);
ASSERT_TRUE(pkt2.ok());
raw_conn->SetNextRead(std::move(*pkt2));
dispatcher.RunUntilStalled();
ASSERT_TRUE(read2.result().has_value() && read2.result()->ok());
EXPECT_EQ(read2.result()->value().size(), 2u);
read2.Deregister();
// 3. Receive StreamEnd
ReadTestTask read3(reader.Read());
dispatcher.Post(read3);
auto end_pkt = internal::PacketFramer::FrameServerFinishPacket(allocator, 1u);
ASSERT_TRUE(end_pkt.ok());
raw_conn->SetNextRead(std::move(*end_pkt));
dispatcher.RunUntilStalled();
ASSERT_TRUE(read3.result().has_value());
EXPECT_FALSE(read3.result()->ok());
EXPECT_EQ(read3.result()->status(), Status::OutOfRange());
read3.Deregister();
}
TEST(ClientCallTest, ClientStreamingSendsChunksThenReadsResponse) {
allocator::test::AllocatorForTest<16384> allocator;
async2::DispatcherForTest dispatcher;
auto [conn, raw_conn] = test::MakeMockConnection(allocator);
test::ScopedClient client(SetupClient(allocator, dispatcher, raw_conn, conn),
dispatcher);
TestServiceClient test_service(client, 100u);
auto client_stream_call =
test_service.CallClientStream<pw::ConstBuf, pw::ConstBuf>(400u);
StreamCallTestTask call_task(std::move(client_stream_call));
dispatcher.Post(call_task);
dispatcher.RunUntilStalled();
ASSERT_TRUE(call_task.result().has_value() && call_task.result()->ok());
auto [writer, response_future] = std::move(**call_task.result());
call_task.Deregister();
// Write chunk 1 via ReserveWrite
ReserveTestTask write1(writer.ReserveWrite(2));
dispatcher.Post(write1);
dispatcher.RunUntilStalled();
ASSERT_TRUE(write1.result().has_value() && write1.result()->ok());
(*write1.result())->payload()[0] = std::byte{1};
(*write1.result())->payload()[1] = std::byte{2};
PW_TEST_EXPECT_OK((*write1.result())->Commit());
write1.Deregister();
// Write chunk 2 via ReserveWrite
ReserveTestTask write2(writer.ReserveWrite(2));
dispatcher.Post(write2);
dispatcher.RunUntilStalled();
ASSERT_TRUE(write2.result().has_value() && write2.result()->ok());
(*write2.result())->payload()[0] = std::byte{3};
(*write2.result())->payload()[1] = std::byte{4};
PW_TEST_EXPECT_OK((*write2.result())->Commit());
write2.Deregister();
// Close writer
WriteTestTask close_task(writer.Finish());
dispatcher.Post(close_task);
dispatcher.RunUntilStalled();
ASSERT_TRUE(close_task.result().has_value() && close_task.result()->ok());
close_task.Deregister();
// Read response
ReadTestTask resp_task(std::move(response_future));
dispatcher.Post(resp_task);
dispatcher.RunUntilStalled();
EXPECT_FALSE(resp_task.result().has_value());
pw::Buf resp_data = pw::Buf::Allocate(allocator, 4);
std::memset(resp_data.data(), 0xEE, 4);
auto framed_resp = internal::PacketFramer::FrameResponsePacket(
allocator, /*call_id=*/1, pw::ConstByteSpan(resp_data));
ASSERT_TRUE(framed_resp.ok());
raw_conn->SetNextRead(std::move(*framed_resp));
dispatcher.RunUntilStalled();
ASSERT_TRUE(resp_task.result().has_value() && resp_task.result()->ok());
EXPECT_EQ(resp_task.result()->value().size(), 4u);
resp_task.Deregister();
}
TEST(ClientCallTest, BidirectionalStreamingConcurrentReadWrite) {
allocator::test::AllocatorForTest<16384> allocator;
async2::DispatcherForTest dispatcher;
auto [conn, raw_conn] = test::MakeMockConnection(allocator);
test::ScopedClient client(SetupClient(allocator, dispatcher, raw_conn, conn),
dispatcher);
TestServiceClient test_service(client, 100u);
auto bidi_call =
test_service.CallBidiStream<pw::ConstBuf, pw::ConstBuf>(500u);
StreamCallTestTask call_task(std::move(bidi_call));
dispatcher.Post(call_task);
dispatcher.RunUntilStalled();
ASSERT_TRUE(call_task.result().has_value() && call_task.result()->ok());
auto [writer, reader] = std::move(**call_task.result());
call_task.Deregister();
// Write from client via ReserveWrite
ReserveTestTask write_task(writer.ReserveWrite(2));
dispatcher.Post(write_task);
dispatcher.RunUntilStalled();
ASSERT_TRUE(write_task.result().has_value() && write_task.result()->ok());
(*write_task.result())->payload()[0] = std::byte{10};
(*write_task.result())->payload()[1] = std::byte{20};
PW_TEST_EXPECT_OK((*write_task.result())->Commit());
write_task.Deregister();
// Read server message
ReadTestTask read_task(reader.Read());
dispatcher.Post(read_task);
std::byte srv_msg[3] = {std::byte{1}, std::byte{2}, std::byte{3}};
auto pkt =
internal::PacketFramer::FrameServerMessagePacket(allocator, 1u, srv_msg);
ASSERT_TRUE(pkt.ok());
raw_conn->SetNextRead(std::move(*pkt));
dispatcher.RunUntilStalled();
ASSERT_TRUE(read_task.result().has_value() && read_task.result()->ok());
EXPECT_EQ(read_task.result()->value().size(), 3u);
read_task.Deregister();
// Client half-close
WriteTestTask close_task(writer.Finish());
dispatcher.Post(close_task);
dispatcher.RunUntilStalled();
ASSERT_TRUE(close_task.result().has_value() && close_task.result()->ok());
close_task.Deregister();
// Server sends StreamEnd
ReadTestTask end_read_task(reader.Read());
dispatcher.Post(end_read_task);
auto end_pkt = internal::PacketFramer::FrameServerFinishPacket(allocator, 1u);
ASSERT_TRUE(end_pkt.ok());
raw_conn->SetNextRead(std::move(*end_pkt));
dispatcher.RunUntilStalled();
ASSERT_TRUE(end_read_task.result().has_value());
EXPECT_EQ(end_read_task.result()->status(), Status::OutOfRange());
end_read_task.Deregister();
}
// The server always terminates the RPC when it ends its stream, so a client
// writer that is still open is closed too.
TEST(ClientCallTest, BidirectionalServerStreamEndClosesClientWriter) {
allocator::test::AllocatorForTest<16384> allocator;
async2::DispatcherForTest dispatcher;
auto [conn, raw_conn] = test::MakeMockConnection(allocator);
test::ScopedClient client(SetupClient(allocator, dispatcher, raw_conn, conn),
dispatcher);
TestServiceClient test_service(client, 100u);
StreamCallTestTask call_task(
test_service.CallBidiStream<pw::ConstBuf, pw::ConstBuf>(500u));
dispatcher.Post(call_task);
dispatcher.RunUntilStalled();
ASSERT_TRUE(call_task.result().has_value() && call_task.result()->ok());
auto [writer, reader] = std::move(**call_task.result());
call_task.Deregister();
ReadTestTask end_read_task(reader.Read());
dispatcher.Post(end_read_task);
auto end_pkt = internal::PacketFramer::FrameServerFinishPacket(allocator, 1u);
ASSERT_TRUE(end_pkt.ok());
raw_conn->SetNextRead(std::move(*end_pkt));
dispatcher.RunUntilStalled();
ASSERT_TRUE(end_read_task.result().has_value());
EXPECT_EQ(end_read_task.result()->status(), Status::OutOfRange());
end_read_task.Deregister();
const size_t commits_before = raw_conn->commit_count();
ReserveTestTask write_task(writer.ReserveWrite(2));
dispatcher.Post(write_task);
dispatcher.RunUntilStalled();
ASSERT_TRUE(write_task.result().has_value());
EXPECT_FALSE(write_task.result()->ok());
write_task.Deregister();
// The client does not end a stream for a call that is already over.
{
auto dropped = std::move(writer);
}
dispatcher.RunUntilStalled();
EXPECT_EQ(raw_conn->commit_count(), commits_before);
}
// A server message that also ends the server's stream without terminating the
// RPC (0x0B) delivers the message, but the reader stays open until the terminal
// packet. The C++ server does not send this packet, but it is valid on the
// wire.
TEST(ClientCallTest, ServerFinalMessageWaitsForTerminalPacket) {
allocator::test::AllocatorForTest<16384> allocator;
async2::DispatcherForTest dispatcher;
auto [conn, raw_conn] = test::MakeMockConnection(allocator);
test::ScopedClient client(SetupClient(allocator, dispatcher, raw_conn, conn),
dispatcher);
TestServiceClient test_service(client, 100u);
StreamCallTestTask call_task(
test_service.CallServerStream<pw::ConstBuf, pw::ConstBuf>(
300u, pw::ConstBuf()));
dispatcher.Post(call_task);
dispatcher.RunUntilStalled();
ASSERT_TRUE(call_task.result().has_value() && call_task.result()->ok());
Reader<pw::ConstBuf> reader = std::move(**call_task.result());
call_task.Deregister();
std::byte chunk[3] = {std::byte{1}, std::byte{2}, std::byte{3}};
auto pkt = FrameServerPacket(allocator,
internal::PacketType::Make<flags::kServer,
flags::kHasPayload,
flags::kStreamEnd>(),
1u,
chunk);
ASSERT_TRUE(pkt.ok());
ReadTestTask read_task(reader.Read());
dispatcher.Post(read_task);
raw_conn->SetNextRead(std::move(*pkt));
dispatcher.RunUntilStalled();
ASSERT_TRUE(read_task.result().has_value() && read_task.result()->ok());
EXPECT_EQ(read_task.result()->value().size(), sizeof(chunk));
read_task.Deregister();
ReadTestTask end_read_task(reader.Read());
dispatcher.Post(end_read_task);
dispatcher.RunUntilStalled();
EXPECT_FALSE(end_read_task.result().has_value());
auto finish = internal::PacketFramer::FrameServerFinishPacket(allocator, 1u);
ASSERT_TRUE(finish.ok());
raw_conn->SetNextRead(std::move(*finish));
dispatcher.RunUntilStalled();
ASSERT_TRUE(end_read_task.result().has_value());
EXPECT_EQ(end_read_task.result()->status(), Status::OutOfRange());
end_read_task.Deregister();
}
// After a server half-close (0x09), the client drops any further messages and
// reports the status from the terminal packet that follows.
TEST(ClientCallTest, ServerStreamEndThenErrorReportsError) {
allocator::test::AllocatorForTest<16384> allocator;
async2::DispatcherForTest dispatcher;
auto [conn, raw_conn] = test::MakeMockConnection(allocator);
test::ScopedClient client(SetupClient(allocator, dispatcher, raw_conn, conn),
dispatcher);
TestServiceClient test_service(client, 100u);
StreamCallTestTask call_task(
test_service.CallBidiStream<pw::ConstBuf, pw::ConstBuf>(500u));
dispatcher.Post(call_task);
dispatcher.RunUntilStalled();
ASSERT_TRUE(call_task.result().has_value() && call_task.result()->ok());
auto [writer, reader] = std::move(**call_task.result());
call_task.Deregister();
const uint32_t call_id = LastSentCallId(*raw_conn);
ReadTestTask read_task(reader.Read());
dispatcher.Post(read_task);
auto stream_end = FrameServerPacket(
allocator,
internal::PacketType::Make<flags::kServer, flags::kStreamEnd>(),
call_id);
ASSERT_TRUE(stream_end.ok());
raw_conn->SetNextRead(std::move(*stream_end));
dispatcher.RunUntilStalled();
EXPECT_FALSE(read_task.result().has_value());
std::byte chunk[1] = {std::byte{1}};
auto late_message = internal::PacketFramer::FrameServerMessagePacket(
allocator, call_id, chunk);
ASSERT_TRUE(late_message.ok());
raw_conn->SetNextRead(std::move(*late_message));
dispatcher.RunUntilStalled();
EXPECT_FALSE(read_task.result().has_value());
// The client's stream is still open until the RPC ends.
ReserveTestTask write_task(writer.ReserveWrite(1));
dispatcher.Post(write_task);
dispatcher.RunUntilStalled();
ASSERT_TRUE(write_task.result().has_value());
PW_TEST_EXPECT_OK(write_task.result()->status());
write_task.Deregister();
auto error = internal::PacketFramer::FrameServerErrorPacket(
allocator, call_id, internal::ServerError::kInternal);
ASSERT_TRUE(error.ok());
raw_conn->SetNextRead(std::move(*error));
dispatcher.RunUntilStalled();
ASSERT_TRUE(read_task.result().has_value());
EXPECT_EQ(read_task.result()->status(), Status::Internal());
read_task.Deregister();
}
// A unary call takes exactly one packet that carries the response and ends the
// RPC. Any other server packet means the server thinks the method streams its
// responses, so the client fails the call rather than taking a streamed message
// as the response. If the packet did not already end the RPC, the client tells
// the server why.
TEST(ClientCallTest, UnaryCallRejectsResponseThatIsNotSingleResponse) {
allocator::test::AllocatorForTest<16384> allocator;
async2::DispatcherForTest dispatcher;
auto [conn, raw_conn] = test::MakeMockConnection(allocator);
test::ScopedClient client(SetupClient(allocator, dispatcher, raw_conn, conn),
dispatcher);
TestServiceClient test_service(client, 100u);
constexpr std::byte kPayload[2] = {std::byte{1}, std::byte{2}};
for (internal::PacketType type : kNotSingleResponse) {
UnaryTestTask task(test_service.CallUnary<pw::ConstBuf, pw::ConstBuf>(
200u, pw::ConstBuf()));
dispatcher.Post(task);
dispatcher.RunUntilStalled();
const uint32_t call_id = LastSentCallId(*raw_conn);
auto pkt = FrameServerPacket(
allocator,
type,
call_id,
type.has_payload() ? pw::ConstByteSpan(kPayload) : pw::ConstByteSpan());
ASSERT_TRUE(pkt.ok());
raw_conn->SetNextRead(std::move(*pkt));
dispatcher.RunUntilStalled();
ASSERT_TRUE(task.result().has_value());
EXPECT_EQ(task.result()->status(), Status::FailedPrecondition());
if (type ==
internal::PacketType::Make<flags::kServer, flags::kOkTerminal>()) {
auto last = internal::InboundPacket::Decode(raw_conn->last_written_buf());
ASSERT_TRUE(last.ok());
EXPECT_EQ(last->type(),
(internal::PacketType::Make<flags::kStart,
flags::kHasPayload,
flags::kStreamEnd>()));
EXPECT_EQ(last->call_id(), call_id);
} else {
ExpectSentMethodTypeMismatch(*raw_conn, call_id);
}
task.Deregister();
}
}
TEST(ClientCallTest, ClientStreamingCallRejectsStreamedResponse) {
allocator::test::AllocatorForTest<16384> allocator;
async2::DispatcherForTest dispatcher;
auto [conn, raw_conn] = test::MakeMockConnection(allocator);
test::ScopedClient client(SetupClient(allocator, dispatcher, raw_conn, conn),
dispatcher);
TestServiceClient test_service(client, 100u);
StreamCallTestTask call_task(
test_service.CallClientStream<pw::ConstBuf, pw::ConstBuf>(400u));
dispatcher.Post(call_task);
dispatcher.RunUntilStalled();
ASSERT_TRUE(call_task.result().has_value() && call_task.result()->ok());
auto [writer, response_future] = std::move(**call_task.result());
call_task.Deregister();
const uint32_t call_id = LastSentCallId(*raw_conn);
ReadTestTask resp_task(std::move(response_future));
dispatcher.Post(resp_task);
std::byte srv_msg[3] = {std::byte{1}, std::byte{2}, std::byte{3}};
auto pkt = internal::PacketFramer::FrameServerMessagePacket(
allocator, call_id, srv_msg);
ASSERT_TRUE(pkt.ok());
raw_conn->SetNextRead(std::move(*pkt));
dispatcher.RunUntilStalled();
ASSERT_TRUE(resp_task.result().has_value());
EXPECT_EQ(resp_task.result()->status(), Status::FailedPrecondition());
ExpectSentMethodTypeMismatch(*raw_conn, call_id);
resp_task.Deregister();
// The call is over, so the client's stream is closed too.
ReserveTestTask write_task(writer.ReserveWrite(2));
dispatcher.Post(write_task);
dispatcher.RunUntilStalled();
ASSERT_TRUE(write_task.result().has_value());
EXPECT_EQ(write_task.result()->status(), Status::FailedPrecondition());
write_task.Deregister();
}
// A streaming call accepts a single terminal response: it is unambiguously a
// stream of one message, which a streaming server may send.
TEST(ClientCallTest, ServerStreamingCallAcceptsSingleResponse) {
allocator::test::AllocatorForTest<16384> allocator;
async2::DispatcherForTest dispatcher;
auto [conn, raw_conn] = test::MakeMockConnection(allocator);
test::ScopedClient client(SetupClient(allocator, dispatcher, raw_conn, conn),
dispatcher);
TestServiceClient test_service(client, 100u);
StreamCallTestTask call_task(
test_service.CallServerStream<pw::ConstBuf, pw::ConstBuf>(
300u, pw::ConstBuf()));
dispatcher.Post(call_task);
dispatcher.RunUntilStalled();
ASSERT_TRUE(call_task.result().has_value() && call_task.result()->ok());
Reader<pw::ConstBuf> reader = std::move(**call_task.result());
call_task.Deregister();
const uint32_t call_id = LastSentCallId(*raw_conn);
ReadTestTask read_task(reader.Read());
dispatcher.Post(read_task);
std::byte resp[2] = {std::byte{7}, std::byte{8}};
auto pkt =
internal::PacketFramer::FrameResponsePacket(allocator, call_id, resp);
ASSERT_TRUE(pkt.ok());
raw_conn->SetNextRead(std::move(*pkt));
dispatcher.RunUntilStalled();
ASSERT_TRUE(read_task.result().has_value() && read_task.result()->ok());
EXPECT_EQ(read_task.result()->value().size(), sizeof(resp));
read_task.Deregister();
ReadTestTask end_read_task(reader.Read());
dispatcher.Post(end_read_task);
dispatcher.RunUntilStalled();
ASSERT_TRUE(end_read_task.result().has_value());
EXPECT_EQ(end_read_task.result()->status(), Status::OutOfRange());
end_read_task.Deregister();
// Nothing was reported to the server.
EXPECT_EQ(LastSentCallId(*raw_conn), call_id);
auto last = internal::InboundPacket::Decode(raw_conn->last_written_buf());
ASSERT_TRUE(last.ok());
EXPECT_EQ(last->type(),
(internal::PacketType::
Make<flags::kStart, flags::kHasPayload, flags::kStreamEnd>()));
}
TEST(ClientCallTest, ServerErrorResolvesClientFutureWithStatus) {
allocator::test::AllocatorForTest<16384> allocator;
async2::DispatcherForTest dispatcher;
auto [conn, raw_conn] = test::MakeMockConnection(allocator);
test::ScopedClient client(SetupClient(allocator, dispatcher, raw_conn, conn),
dispatcher);
TestServiceClient test_service(client, 100u);
pw::Buf req_buf = pw::Buf::Allocate(allocator, 2);
auto unary_call = test_service.CallUnary<pw::ConstBuf, pw::ConstBuf>(
600u, pw::ConstBuf(std::move(req_buf)));
UnaryTestTask task(std::move(unary_call));
dispatcher.Post(task);
dispatcher.RunUntilStalled();
// Server emits Error packet with ServerError::kUnknownMethod
auto err_pkt = internal::PacketFramer::FrameServerErrorPacket(
allocator,
/*call_id=*/1,
internal::ServerError::kUnknownMethod);
ASSERT_TRUE(err_pkt.ok());
raw_conn->SetNextRead(std::move(*err_pkt));
dispatcher.RunUntilStalled();
ASSERT_TRUE(task.result().has_value());
EXPECT_FALSE(task.result()->ok());
EXPECT_EQ(task.result()->status(), Status::NotFound());
task.Deregister();
}
TEST(ClientCallTest, ClientDropHandleEndsClientStream) {
allocator::test::AllocatorForTest<16384> allocator;
async2::DispatcherForTest dispatcher;
auto [conn, raw_conn] = test::MakeMockConnection(allocator);
test::ScopedClient client(SetupClient(allocator, dispatcher, raw_conn, conn),
dispatcher);
TestServiceClient test_service(client, 100u);
size_t initial_commits = raw_conn->commit_count();
// Keep the response pending: dropping it would cancel the call.
std::optional<ReadFuture<pw::ConstBuf>> response;
{
auto client_stream_call =
test_service.CallClientStream<pw::ConstBuf, pw::ConstBuf>(700u);
StreamCallTestTask call_task(std::move(client_stream_call));
dispatcher.Post(call_task);
dispatcher.RunUntilStalled();
ASSERT_TRUE(call_task.result().has_value() && call_task.result()->ok());
ClientStreamCall<pw::ConstBuf, pw::ConstBuf> stream =
std::move(**call_task.result());
call_task.Deregister();
response.emplace(std::move(stream.response));
EXPECT_EQ(raw_conn->commit_count(), initial_commits + 1u); // +1 request
// stream.writer is destroyed at end of scope without Close()
}
// Destruction of an unclosed Writer ends the outbound stream normally.
EXPECT_EQ(raw_conn->commit_count(), initial_commits + 2u);
auto decode_end =
internal::InboundPacket::Decode(raw_conn->last_written_buf());
ASSERT_TRUE(decode_end.ok());
EXPECT_EQ(decode_end->type(),
internal::PacketType::Make<flags::kStreamEnd>());
// Dropping the response before the server answers cancels the call.
response.reset();
dispatcher.RunUntilStalled();
EXPECT_EQ(raw_conn->commit_count(), initial_commits + 3u);
auto decode_cancel =
internal::InboundPacket::Decode(raw_conn->last_written_buf());
ASSERT_TRUE(decode_cancel.ok());
EXPECT_EQ(decode_cancel->type(),
internal::PacketType::Make<flags::kErrorTerminal>());
EXPECT_EQ(decode_cancel->client_error(), internal::ClientError::kCancelled);
}
// Checks that the most recent packet the client sent cancels `call_id`.
void ExpectSentCancel(test::MockConnection& raw_conn, uint32_t call_id) {
auto decoded = internal::InboundPacket::Decode(raw_conn.last_written_buf());
ASSERT_TRUE(decoded.ok());
EXPECT_EQ(decoded->type(),
internal::PacketType::Make<flags::kErrorTerminal>());
EXPECT_EQ(decoded->call_id(), call_id);
EXPECT_EQ(decoded->client_error(), internal::ClientError::kCancelled);
}
// Cancelling through the writer completes the call locally, so a pending read
// resolves instead of waiting for a server reply that will never come.
TEST(ClientCallTest, BidiWriterCancelResolvesPendingRead) {
allocator::test::AllocatorForTest<16384> allocator;
async2::DispatcherForTest dispatcher;
auto [conn, raw_conn] = test::MakeMockConnection(allocator);
test::ScopedClient client(SetupClient(allocator, dispatcher, raw_conn, conn),
dispatcher);
TestServiceClient test_service(client, 100u);
StreamCallTestTask call_task(
test_service.CallBidiStream<pw::ConstBuf, pw::ConstBuf>(500u));
dispatcher.Post(call_task);
dispatcher.RunUntilStalled();
ASSERT_TRUE(call_task.result().has_value() && call_task.result()->ok());
auto [writer, reader] = std::move(**call_task.result());
call_task.Deregister();
ReadTestTask read_task(reader.Read());
dispatcher.Post(read_task);
dispatcher.RunUntilStalled();
EXPECT_FALSE(read_task.result().has_value());
const size_t commits_before = raw_conn->commit_count();
writer.Cancel();
dispatcher.RunUntilStalled();
ASSERT_TRUE(read_task.result().has_value());
EXPECT_EQ(read_task.result()->status(), Status::Cancelled());
EXPECT_EQ(raw_conn->commit_count(), commits_before + 1u);
ExpectSentCancel(*raw_conn, 1u);
read_task.Deregister();
// The call is over, so dropping the handles sends nothing more.
{
auto dropped_writer = std::move(writer);
auto dropped_reader = std::move(reader);
}
dispatcher.RunUntilStalled();
EXPECT_EQ(raw_conn->commit_count(), commits_before + 1u);
}
TEST(ClientCallTest, ClientStreamWriterCancelResolvesResponse) {
allocator::test::AllocatorForTest<16384> allocator;
async2::DispatcherForTest dispatcher;
auto [conn, raw_conn] = test::MakeMockConnection(allocator);
test::ScopedClient client(SetupClient(allocator, dispatcher, raw_conn, conn),
dispatcher);
TestServiceClient test_service(client, 100u);
StreamCallTestTask call_task(
test_service.CallClientStream<pw::ConstBuf, pw::ConstBuf>(400u));
dispatcher.Post(call_task);
dispatcher.RunUntilStalled();
ASSERT_TRUE(call_task.result().has_value() && call_task.result()->ok());
auto [writer, response_future] = std::move(**call_task.result());
call_task.Deregister();
ReadTestTask resp_task(std::move(response_future));
dispatcher.Post(resp_task);
dispatcher.RunUntilStalled();
EXPECT_FALSE(resp_task.result().has_value());
writer.Cancel();
dispatcher.RunUntilStalled();
ASSERT_TRUE(resp_task.result().has_value());
EXPECT_EQ(resp_task.result()->status(), Status::Cancelled());
ExpectSentCancel(*raw_conn, 1u);
resp_task.Deregister();
}
// A client that drops a server stream's reader before the stream ends has no
// further interest in the call, so the server is told to stop.
TEST(ClientCallTest, DroppingServerStreamReaderCancelsCall) {
allocator::test::AllocatorForTest<16384> allocator;
async2::DispatcherForTest dispatcher;
auto [conn, raw_conn] = test::MakeMockConnection(allocator);
test::ScopedClient client(SetupClient(allocator, dispatcher, raw_conn, conn),
dispatcher);
TestServiceClient test_service(client, 100u);
StreamCallTestTask call_task(
test_service.CallServerStream<pw::ConstBuf, pw::ConstBuf>(
300u, pw::ConstBuf()));
dispatcher.Post(call_task);
dispatcher.RunUntilStalled();
ASSERT_TRUE(call_task.result().has_value() && call_task.result()->ok());
std::optional<Reader<pw::ConstBuf>> reader(std::move(**call_task.result()));
call_task.Deregister();
const size_t commits_before = raw_conn->commit_count();
reader.reset();
dispatcher.RunUntilStalled();
EXPECT_EQ(raw_conn->commit_count(), commits_before + 1u);
ExpectSentCancel(*raw_conn, 1u);
}
// Dropping a reader after the stream ended normally sends nothing.
TEST(ClientCallTest, DroppingFinishedServerStreamReaderSendsNothing) {
allocator::test::AllocatorForTest<16384> allocator;
async2::DispatcherForTest dispatcher;
auto [conn, raw_conn] = test::MakeMockConnection(allocator);
test::ScopedClient client(SetupClient(allocator, dispatcher, raw_conn, conn),
dispatcher);
TestServiceClient test_service(client, 100u);
StreamCallTestTask call_task(
test_service.CallServerStream<pw::ConstBuf, pw::ConstBuf>(
300u, pw::ConstBuf()));
dispatcher.Post(call_task);
dispatcher.RunUntilStalled();
ASSERT_TRUE(call_task.result().has_value() && call_task.result()->ok());
std::optional<Reader<pw::ConstBuf>> reader(std::move(**call_task.result()));
call_task.Deregister();
ReadTestTask end_read(reader->Read());
dispatcher.Post(end_read);
auto end_pkt = internal::PacketFramer::FrameServerFinishPacket(allocator, 1u);
ASSERT_TRUE(end_pkt.ok());
raw_conn->SetNextRead(std::move(*end_pkt));
dispatcher.RunUntilStalled();
ASSERT_TRUE(end_read.result().has_value());
EXPECT_EQ(end_read.result()->status(), Status::OutOfRange());
end_read.Deregister();
const size_t commits_before = raw_conn->commit_count();
reader.reset();
dispatcher.RunUntilStalled();
EXPECT_EQ(raw_conn->commit_count(), commits_before);
}
TEST(ClientCallTest, ReaderCancelResolvesPendingReadAndCancelsCall) {
allocator::test::AllocatorForTest<16384> allocator;
async2::DispatcherForTest dispatcher;
auto [conn, raw_conn] = test::MakeMockConnection(allocator);
test::ScopedClient client(SetupClient(allocator, dispatcher, raw_conn, conn),
dispatcher);
TestServiceClient test_service(client, 100u);
StreamCallTestTask call_task(
test_service.CallServerStream<pw::ConstBuf, pw::ConstBuf>(
300u, pw::ConstBuf()));
dispatcher.Post(call_task);
dispatcher.RunUntilStalled();
ASSERT_TRUE(call_task.result().has_value() && call_task.result()->ok());
std::optional<Reader<pw::ConstBuf>> reader(std::move(**call_task.result()));
call_task.Deregister();
ReadTestTask read_task(reader->Read());
dispatcher.Post(read_task);
dispatcher.RunUntilStalled();
EXPECT_FALSE(read_task.result().has_value());
const size_t commits_before = raw_conn->commit_count();
reader->Cancel();
dispatcher.RunUntilStalled();
ASSERT_TRUE(read_task.result().has_value());
EXPECT_EQ(read_task.result()->status(), Status::Cancelled());
EXPECT_EQ(raw_conn->commit_count(), commits_before + 1u);
ExpectSentCancel(*raw_conn, 1u);
read_task.Deregister();
// A second cancel, or dropping the reader, sends nothing more.
reader->Cancel();
reader.reset();
dispatcher.RunUntilStalled();
EXPECT_EQ(raw_conn->commit_count(), commits_before + 1u);
}
// Dropping a unary call after its request was sent cancels the call.
TEST(ClientCallTest, DroppingUnaryCallAfterRequestCancelsCall) {
allocator::test::AllocatorForTest<16384> allocator;
async2::DispatcherForTest dispatcher;
auto [conn, raw_conn] = test::MakeMockConnection(allocator);
test::ScopedClient client(SetupClient(allocator, dispatcher, raw_conn, conn),
dispatcher);
TestServiceClient test_service(client, 100u);
size_t commits_after_request = 0;
{
UnaryTestTask task(test_service.CallUnary<pw::ConstBuf, pw::ConstBuf>(
200u, pw::ConstBuf()));
dispatcher.Post(task);
dispatcher.RunUntilStalled();
EXPECT_FALSE(task.result().has_value());
commits_after_request = raw_conn->commit_count();
task.Deregister();
}
dispatcher.RunUntilStalled();
EXPECT_EQ(raw_conn->commit_count(), commits_after_request + 1u);
ExpectSentCancel(*raw_conn, 1u);
}
TEST(ClientCallTest, ClientStreamSendsStartStreamFirst) {
allocator::test::AllocatorForTest<16384> allocator;
async2::DispatcherForTest dispatcher;
auto [conn, raw_conn] = test::MakeMockConnection(allocator);
ClientConnectTask connect_task(Client::Create(dispatcher, allocator, conn));
dispatcher.Post(connect_task);
dispatcher.RunUntilStalled();
pw::Buf hs_resp =
pw::Buf::Allocate(allocator, internal::HandshakePacket::kWireSizeBytes);
auto hs_pkt =
internal::HandshakePacket(internal::HandshakePacket::Type::kSynAck);
auto enc_res = hs_pkt.Encode(std::move(hs_resp));
ASSERT_TRUE(enc_res.ok());
raw_conn->SetNextRead(std::move(enc_res.value()));
dispatcher.RunUntilStalled();
ASSERT_TRUE(connect_task.client().has_value() && connect_task.client()->ok());
test::ScopedClient client(std::move(**connect_task.client()), dispatcher);
connect_task.Deregister();
TestServiceClient test_service(client, 100u);
auto client_stream_fut =
test_service.CallClientStream<pw::ConstBuf, pw::ConstBuf>(200u);
struct Task : public async2::Task {
ClientStreamFuture<pw::ConstBuf, pw::ConstBuf> fut;
std::optional<Result<ClientStreamCall<pw::ConstBuf, pw::ConstBuf>>> res;
Task(ClientStreamFuture<pw::ConstBuf, pw::ConstBuf> f)
: fut(std::move(f)) {}
async2::Poll<> DoPend(async2::Context& cx) override {
auto poll = fut.Pend(cx);
if (poll.IsPending()) {
return async2::Pending();
}
res = std::move(*poll);
return async2::Ready();
}
} task(std::move(client_stream_fut));
dispatcher.Post(task);
dispatcher.RunUntilStalled();
// Pending RequestFuture transmits the initial payload-less START packet.
EXPECT_GE(raw_conn->commit_count(), 3u); // 2 handshake + 1 start
auto decode_req =
internal::InboundPacket::Decode(raw_conn->last_written_buf());
ASSERT_TRUE(decode_req.ok());
EXPECT_EQ(decode_req->type(), internal::PacketType::Make<flags::kStart>());
EXPECT_FALSE(decode_req->type().has_payload());
EXPECT_EQ(decode_req->type().close_mode(), internal::CloseMode::kOpen);
EXPECT_EQ(decode_req->service_id(), 100u);
EXPECT_EQ(decode_req->method_id(), 200u);
EXPECT_EQ(decode_req->payload().size(), 0u);
ASSERT_TRUE(task.res.has_value() && task.res->ok());
auto [writer, reader] = std::move(**task.res);
// Subsequent Write transmits a message packet (not a START packet)
ReserveTestTask write_task(writer.ReserveWrite(2));
dispatcher.Post(write_task);
dispatcher.RunUntilStalled();
ASSERT_TRUE(write_task.result().has_value() && write_task.result()->ok());
(*write_task.result())->payload()[0] = std::byte(1);
(*write_task.result())->payload()[1] = std::byte(2);
PW_TEST_EXPECT_OK((*write_task.result())->Commit());
auto decode_msg =
internal::InboundPacket::Decode(raw_conn->last_written_buf());
ASSERT_TRUE(decode_msg.ok());
EXPECT_EQ(decode_msg->type(),
internal::PacketType::Make<flags::kHasPayload>());
EXPECT_EQ(decode_msg->payload().size(), 2u);
task.Deregister();
write_task.Deregister();
}
} // namespace
} // namespace pw::rpc2