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