Add webtransport::test::InMemoryStream for simplifying testing code that reads from WebTransport streams. PiperOrigin-RevId: 702111075
diff --git a/build/source_list.bzl b/build/source_list.bzl index 6b07808..10a897f 100644 --- a/build/source_list.bzl +++ b/build/source_list.bzl
@@ -867,6 +867,7 @@ "quic/test_tools/test_ticket_crypter.h", "quic/test_tools/web_transport_resets_backend.h", "quic/test_tools/web_transport_test_tools.h", + "web_transport/test_tools/in_memory_stream.h", "web_transport/test_tools/mock_web_transport.h", ] quiche_test_support_srcs = [ @@ -961,6 +962,7 @@ "quic/test_tools/test_ip_packets.cc", "quic/test_tools/test_ticket_crypter.cc", "quic/test_tools/web_transport_resets_backend.cc", + "web_transport/test_tools/in_memory_stream.cc", ] balsa_hdrs = [ "balsa/balsa_enums.h", @@ -1342,6 +1344,7 @@ "quic/tools/quic_tcp_like_trace_converter_test.cc", "quic/tools/simple_ticket_crypter_test.cc", "web_transport/encapsulated/encapsulated_web_transport_test.cc", + "web_transport/test_tools/in_memory_stream_test.cc", "web_transport/web_transport_headers_test.cc", "web_transport/web_transport_priority_scheduler_test.cc", ]
diff --git a/build/source_list.gni b/build/source_list.gni index 749e132..6fc039b 100644 --- a/build/source_list.gni +++ b/build/source_list.gni
@@ -867,6 +867,7 @@ "src/quiche/quic/test_tools/test_ticket_crypter.h", "src/quiche/quic/test_tools/web_transport_resets_backend.h", "src/quiche/quic/test_tools/web_transport_test_tools.h", + "src/quiche/web_transport/test_tools/in_memory_stream.h", "src/quiche/web_transport/test_tools/mock_web_transport.h", ] quiche_test_support_srcs = [ @@ -961,6 +962,7 @@ "src/quiche/quic/test_tools/test_ip_packets.cc", "src/quiche/quic/test_tools/test_ticket_crypter.cc", "src/quiche/quic/test_tools/web_transport_resets_backend.cc", + "src/quiche/web_transport/test_tools/in_memory_stream.cc", ] balsa_hdrs = [ "src/quiche/balsa/balsa_enums.h", @@ -1343,6 +1345,7 @@ "src/quiche/quic/tools/quic_tcp_like_trace_converter_test.cc", "src/quiche/quic/tools/simple_ticket_crypter_test.cc", "src/quiche/web_transport/encapsulated/encapsulated_web_transport_test.cc", + "src/quiche/web_transport/test_tools/in_memory_stream_test.cc", "src/quiche/web_transport/web_transport_headers_test.cc", "src/quiche/web_transport/web_transport_priority_scheduler_test.cc", ]
diff --git a/build/source_list.json b/build/source_list.json index c09bc72..1b46073 100644 --- a/build/source_list.json +++ b/build/source_list.json
@@ -866,6 +866,7 @@ "quiche/quic/test_tools/test_ticket_crypter.h", "quiche/quic/test_tools/web_transport_resets_backend.h", "quiche/quic/test_tools/web_transport_test_tools.h", + "quiche/web_transport/test_tools/in_memory_stream.h", "quiche/web_transport/test_tools/mock_web_transport.h" ], "quiche_test_support_srcs": [ @@ -959,7 +960,8 @@ "quiche/quic/test_tools/test_certificates.cc", "quiche/quic/test_tools/test_ip_packets.cc", "quiche/quic/test_tools/test_ticket_crypter.cc", - "quiche/quic/test_tools/web_transport_resets_backend.cc" + "quiche/quic/test_tools/web_transport_resets_backend.cc", + "quiche/web_transport/test_tools/in_memory_stream.cc" ], "balsa_hdrs": [ "quiche/balsa/balsa_enums.h", @@ -1342,6 +1344,7 @@ "quiche/quic/tools/quic_tcp_like_trace_converter_test.cc", "quiche/quic/tools/simple_ticket_crypter_test.cc", "quiche/web_transport/encapsulated/encapsulated_web_transport_test.cc", + "quiche/web_transport/test_tools/in_memory_stream_test.cc", "quiche/web_transport/web_transport_headers_test.cc", "quiche/web_transport/web_transport_priority_scheduler_test.cc" ],
diff --git a/quiche/BUILD.bazel b/quiche/BUILD.bazel index 0e3e26f..3937177 100644 --- a/quiche/BUILD.bazel +++ b/quiche/BUILD.bazel
@@ -229,6 +229,7 @@ "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:cord", "@com_google_absl//absl/strings:str_format", "@com_google_absl//absl/synchronization", "@com_google_absl//absl/time",
diff --git a/quiche/web_transport/test_tools/in_memory_stream.cc b/quiche/web_transport/test_tools/in_memory_stream.cc new file mode 100644 index 0000000..99d5354 --- /dev/null +++ b/quiche/web_transport/test_tools/in_memory_stream.cc
@@ -0,0 +1,71 @@ +// Copyright 2024 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "quiche/web_transport/test_tools/in_memory_stream.h" + +#include <cstddef> +#include <string> +#include <vector> + +#include "absl/strings/cord.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "quiche/common/platform/api/quiche_logging.h" +#include "quiche/common/quiche_stream.h" +#include "quiche/common/vectorized_io_utils.h" + +namespace webtransport::test { + +quiche::ReadStream::ReadResult InMemoryStream::Read(absl::Span<char> output) { + std::vector<absl::string_view> chunks; + for (absl::string_view chunk : buffer_.Chunks()) { + chunks.push_back(chunk); + } + size_t bytes_read = quiche::GatherStringViewSpan(chunks, output); + buffer_.RemovePrefix(bytes_read); + return ReadResult{bytes_read, buffer_.empty() && fin_received_}; +} + +quiche::ReadStream::ReadResult InMemoryStream::Read(std::string* output) { + ReadResult result; + result.bytes_read = buffer_.size(); + result.fin = fin_received_; + absl::AppendCordToString(buffer_, output); + buffer_.Clear(); + return result; +} + +size_t InMemoryStream::ReadableBytes() const { return buffer_.size(); } + +quiche::ReadStream::PeekResult InMemoryStream::PeekNextReadableRegion() const { + if (buffer_.empty()) { + return PeekResult{"", fin_received_, fin_received_}; + } + absl::string_view next_chunk = *buffer_.Chunks().begin(); + return PeekResult{next_chunk, + fin_received_ && next_chunk.size() == buffer_.size(), + fin_received_}; +} + +bool InMemoryStream::SkipBytes(size_t bytes) { + buffer_.RemovePrefix(bytes); + return buffer_.empty() && fin_received_; +} + +void InMemoryStream::Receive(absl::string_view data, bool fin) { + QUICHE_DCHECK(!abruptly_terminated_); + buffer_.Append(data); + fin_received_ |= fin; + if (visitor_ != nullptr) { + visitor_->OnCanRead(); + } +} + +void InMemoryStream::Terminate() { + abruptly_terminated_ = true; + buffer_.Clear(); + fin_received_ = false; +} + +} // namespace webtransport::test
diff --git a/quiche/web_transport/test_tools/in_memory_stream.h b/quiche/web_transport/test_tools/in_memory_stream.h new file mode 100644 index 0000000..46e8d80 --- /dev/null +++ b/quiche/web_transport/test_tools/in_memory_stream.h
@@ -0,0 +1,84 @@ +// Copyright 2024 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef QUICHE_WEB_TRANSPORT_TEST_TOOLS_IN_MEMORY_STREAM_H_ +#define QUICHE_WEB_TRANSPORT_TEST_TOOLS_IN_MEMORY_STREAM_H_ + +#include <cstddef> +#include <memory> +#include <string> +#include <utility> + +#include "absl/status/status.h" +#include "absl/strings/cord.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "quiche/common/platform/api/quiche_logging.h" +#include "quiche/common/quiche_stream.h" +#include "quiche/web_transport/web_transport.h" + +namespace webtransport::test { + +// InMemoryStream models an incoming readable WebTransport stream where all of +// the data is read from an in-memory buffer. +class QUICHE_NO_EXPORT InMemoryStream : public Stream { + public: + explicit InMemoryStream(StreamId id) : id_(id) {} + + // quiche::ReadStream implementation. + [[nodiscard]] ReadResult Read(absl::Span<char> output) override; + [[nodiscard]] ReadResult Read(std::string* output) override; + size_t ReadableBytes() const override; + PeekResult PeekNextReadableRegion() const override; + bool SkipBytes(size_t bytes) override; + + // quiche::WriteStream implementation. + absl::Status Writev(absl::Span<const absl::string_view> data, + const quiche::StreamWriteOptions& options) override { + QUICHE_NOTREACHED() << "Writev called on a read-only stream"; + } + bool CanWrite() const override { return false; } + + void AbruptlyTerminate(absl::Status) override { Terminate(); } + + // webtransport::Stream implementation. + StreamId GetStreamId() const override { return id_; } + void ResetWithUserCode(StreamErrorCode) override { + QUICHE_NOTREACHED() << "Reset called on a read-only stream"; + } + void ResetDueToInternalError() override { + QUICHE_NOTREACHED() << "Reset called on a read-only stream"; + } + void MaybeResetDueToStreamObjectGone() override { + QUICHE_NOTREACHED() << "Reset called on a read-only stream"; + } + void SendStopSending(StreamErrorCode) override { Terminate(); } + + const StreamPriority& priority() const { return priority_; } + void SetPriority(const StreamPriority& priority) override { + priority_ = priority; + } + StreamVisitor* visitor() override { return visitor_.get(); } + void SetVisitor(std::unique_ptr<StreamVisitor> visitor) override { + visitor_ = std::move(visitor); + } + + // Simulates receiving the specified stream data by appending it to the buffer + // and executing the visitor callback. + void Receive(absl::string_view data, bool fin = false); + + private: + void Terminate(); + + StreamId id_; + std::unique_ptr<StreamVisitor> visitor_; + StreamPriority priority_; + absl::Cord buffer_; + bool fin_received_ = false; + bool abruptly_terminated_ = false; +}; + +} // namespace webtransport::test + +#endif // QUICHE_WEB_TRANSPORT_TEST_TOOLS_IN_MEMORY_STREAM_H_
diff --git a/quiche/web_transport/test_tools/in_memory_stream_test.cc b/quiche/web_transport/test_tools/in_memory_stream_test.cc new file mode 100644 index 0000000..0886b43 --- /dev/null +++ b/quiche/web_transport/test_tools/in_memory_stream_test.cc
@@ -0,0 +1,83 @@ +// Copyright 2024 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "quiche/web_transport/test_tools/in_memory_stream.h" + +#include <string> + +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "quiche/common/platform/api/quiche_test.h" +#include "quiche/common/quiche_stream.h" +#include "quiche/web_transport/web_transport.h" + +namespace webtransport::test { +namespace { + +using ::testing::ElementsAre; + +TEST(InMemoryStreamTest, ReadSpan) { + InMemoryStream stream(0); + char buffer[4] = {'\0'}; + + Stream::ReadResult result = stream.Read(absl::MakeSpan(buffer)); + EXPECT_EQ(result.bytes_read, 0); + EXPECT_FALSE(result.fin); + + stream.Receive("test"); + result = stream.Read(absl::MakeSpan(buffer)); + EXPECT_EQ(result.bytes_read, 4); + EXPECT_FALSE(result.fin); + EXPECT_THAT(buffer, ElementsAre('t', 'e', 's', 't')); +} + +TEST(InMemoryStreamTest, ReadString) { + InMemoryStream stream(0); + std::string buffer = "> "; + + stream.Receive("test"); + Stream::ReadResult result = stream.Read(&buffer); + EXPECT_EQ(result.bytes_read, 4); + EXPECT_EQ(buffer, "> test"); +} + +TEST(InMemoryStreamTest, ReadFin) { + InMemoryStream stream(0); + char buffer[1]; + + stream.Receive("ab", /*fin=*/true); + Stream::ReadResult result = stream.Read(absl::MakeSpan(buffer)); + EXPECT_EQ(result.bytes_read, 1); + EXPECT_FALSE(result.fin); + EXPECT_EQ(buffer[0], 'a'); + + result = stream.Read(absl::MakeSpan(buffer)); + EXPECT_EQ(result.bytes_read, 1); + EXPECT_TRUE(result.fin); + EXPECT_EQ(buffer[0], 'b'); +} + +TEST(InMemoryStreamTest, Peek) { + std::string chunk_a(8192, 'a'); + std::string chunk_b(8192, 'a'); + + InMemoryStream stream(0); + stream.Receive(chunk_a); + stream.Receive(chunk_b, /*fin=*/true); + + Stream::PeekResult result = stream.PeekNextReadableRegion(); + EXPECT_EQ(result.peeked_data[0], 'a'); + EXPECT_TRUE(result.all_data_received); + + std::string merged_result; + bool fin_reached = quiche::ProcessAllReadableRegions( + stream, + [&](absl::string_view chunk) { absl::StrAppend(&merged_result, chunk); }); + EXPECT_EQ(merged_result, absl::StrCat(chunk_a, chunk_b)); + EXPECT_TRUE(fin_reached); +} + +} // namespace +} // namespace webtransport::test