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