Moves MetadataVisitor and MetadataSerializer to //third_party/spdy/core for reuse in QUICHE.
PiperOrigin-RevId: 381565468
diff --git a/spdy/core/http2_header_block_hpack_listener.h b/spdy/core/http2_header_block_hpack_listener.h
new file mode 100644
index 0000000..19352e5
--- /dev/null
+++ b/spdy/core/http2_header_block_hpack_listener.h
@@ -0,0 +1,47 @@
+#ifndef QUICHE_SPDY_CORE_HTTP2_HEADER_BLOCK_HPACK_LISTENER_H_
+#define QUICHE_SPDY_CORE_HTTP2_HEADER_BLOCK_HPACK_LISTENER_H_
+
+#include "absl/strings/string_view.h"
+#include "http2/hpack/decoder/hpack_decoder_listener.h"
+#include "common/platform/api/quiche_logging.h"
+#include "spdy/core/spdy_header_block.h"
+
+namespace spdy {
+
+// This class simply gathers the key-value pairs emitted by an HpackDecoder in
+// a SpdyHeaderBlock.
+class Http2HeaderBlockHpackListener : public http2::HpackDecoderListener {
+ public:
+ Http2HeaderBlockHpackListener() {}
+
+ void OnHeaderListStart() override {
+ header_block_.clear();
+ hpack_error_ = false;
+ }
+
+ void OnHeader(const std::string& name, const std::string& value) override {
+ header_block_.AppendValueOrAddHeader(name, value);
+ }
+
+ void OnHeaderListEnd() override {}
+
+ void OnHeaderErrorDetected(absl::string_view error_message) override {
+ QUICHE_VLOG(1) << error_message;
+ hpack_error_ = true;
+ }
+
+ SpdyHeaderBlock release_header_block() {
+ SpdyHeaderBlock block = std::move(header_block_);
+ header_block_ = {};
+ return block;
+ }
+ bool hpack_error() const { return hpack_error_; }
+
+ private:
+ SpdyHeaderBlock header_block_;
+ bool hpack_error_ = false;
+};
+
+} // namespace spdy
+
+#endif // QUICHE_SPDY_CORE_HTTP2_HEADER_BLOCK_HPACK_LISTENER_H_
diff --git a/spdy/core/metadata_extension.cc b/spdy/core/metadata_extension.cc
new file mode 100644
index 0000000..12f6af1
--- /dev/null
+++ b/spdy/core/metadata_extension.cc
@@ -0,0 +1,195 @@
+#include "spdy/core/metadata_extension.h"
+
+#include <list>
+#include <string>
+
+#include "absl/memory/memory.h"
+#include "absl/strings/str_cat.h"
+#include "http2/decoder/decode_buffer.h"
+#include "http2/hpack/decoder/hpack_decoder.h"
+#include "common/platform/api/quiche_bug_tracker.h"
+#include "common/platform/api/quiche_logging.h"
+#include "spdy/core/hpack/hpack_encoder.h"
+#include "spdy/core/http2_header_block_hpack_listener.h"
+
+namespace spdy {
+
+// Non-standard constants related to METADATA frames.
+const SpdySettingsId MetadataVisitor::kMetadataExtensionId = 0x4d44;
+const uint8_t MetadataVisitor::kMetadataFrameType = 0x4d;
+const uint8_t MetadataVisitor::kEndMetadataFlag = 0x4;
+
+namespace {
+
+const size_t kMaxMetadataBlockSize = 1 << 20; // 1 MB
+
+// This class uses an HpackEncoder to serialize a METADATA block as a series of
+// METADATA frames.
+class MetadataFrameSequence : public MetadataSerializer::FrameSequence {
+ public:
+ MetadataFrameSequence(SpdyStreamId stream_id, spdy::SpdyHeaderBlock payload)
+ : stream_id_(stream_id), payload_(std::move(payload)) {
+ // Metadata should not use HPACK compression.
+ encoder_.DisableCompression();
+ HpackEncoder::Representations r;
+ for (const auto& kv_pair : payload_) {
+ r.push_back(kv_pair);
+ }
+ progressive_encoder_ = encoder_.EncodeRepresentations(r);
+ }
+
+ // Copies are not allowed.
+ MetadataFrameSequence(const MetadataFrameSequence& other) = delete;
+ MetadataFrameSequence& operator=(const MetadataFrameSequence& other) = delete;
+
+ std::unique_ptr<spdy::SpdyFrameIR> Next() override;
+
+ private:
+ SpdyStreamId stream_id_;
+ SpdyHeaderBlock payload_;
+ HpackEncoder encoder_;
+ std::unique_ptr<HpackEncoder::ProgressiveEncoder> progressive_encoder_;
+};
+
+std::unique_ptr<spdy::SpdyFrameIR> MetadataFrameSequence::Next() {
+ if (!progressive_encoder_->HasNext()) {
+ return nullptr;
+ }
+ std::string payload;
+ // METADATA frames obey the HTTP/2 maximum frame size.
+ progressive_encoder_->Next(spdy::kHttp2DefaultFramePayloadLimit, &payload);
+ const bool end_metadata = (!progressive_encoder_->HasNext());
+ const uint8_t flags = end_metadata ? MetadataVisitor::kEndMetadataFlag : 0;
+ return absl::make_unique<spdy::SpdyUnknownIR>(
+ stream_id_, MetadataVisitor::kMetadataFrameType, flags,
+ std::move(payload));
+}
+
+} // anonymous namespace
+
+struct MetadataVisitor::MetadataPayloadState {
+ MetadataPayloadState(size_t remaining, bool end)
+ : bytes_remaining(remaining), end_metadata(end) {}
+ std::list<std::string> buffer;
+ size_t bytes_remaining;
+ bool end_metadata;
+};
+
+MetadataVisitor::MetadataVisitor(OnCompletePayload on_payload,
+ OnMetadataSupport on_support)
+ : on_payload_(std::move(on_payload)),
+ on_support_(std::move(on_support)),
+ peer_supports_metadata_(MetadataSupportState::UNSPECIFIED) {}
+
+MetadataVisitor::~MetadataVisitor() {}
+
+void MetadataVisitor::OnSetting(SpdySettingsId id, uint32_t value) {
+ QUICHE_VLOG(1) << "MetadataVisitor::OnSetting(" << id << ", " << value << ")";
+ if (id == kMetadataExtensionId) {
+ if (value == 0) {
+ const MetadataSupportState previous_state = peer_supports_metadata_;
+ peer_supports_metadata_ = MetadataSupportState::NOT_SUPPORTED;
+ if (previous_state == MetadataSupportState::UNSPECIFIED ||
+ previous_state == MetadataSupportState::SUPPORTED) {
+ on_support_(false);
+ }
+ } else if (value == 1) {
+ const MetadataSupportState previous_state = peer_supports_metadata_;
+ peer_supports_metadata_ = MetadataSupportState::SUPPORTED;
+ if (previous_state == MetadataSupportState::UNSPECIFIED ||
+ previous_state == MetadataSupportState::NOT_SUPPORTED) {
+ on_support_(true);
+ }
+ } else {
+ LOG_EVERY_N_SEC(WARNING, 1)
+ << "Unrecognized value for setting " << id << ": " << value;
+ }
+ }
+}
+
+bool MetadataVisitor::OnFrameHeader(SpdyStreamId stream_id, size_t length,
+ uint8_t type, uint8_t flags) {
+ QUICHE_VLOG(1) << "OnFrameHeader(stream_id=" << stream_id
+ << ", length=" << length << ", type=" << static_cast<int>(type)
+ << ", flags=" << static_cast<int>(flags);
+ // TODO(birenroy): Consider disabling METADATA handling until our setting
+ // advertising METADATA support has been acked.
+ if (type != kMetadataFrameType) {
+ return false;
+ }
+ auto it = metadata_map_.find(stream_id);
+ if (it == metadata_map_.end()) {
+ auto state = absl::make_unique<MetadataPayloadState>(
+ length, flags & kEndMetadataFlag);
+ auto result = metadata_map_.insert(std::make_pair(stream_id,
+ std::move(state)));
+ QUICHE_BUG_IF(bug_if_2781_1, !result.second) << "Map insertion failed.";
+ it = result.first;
+ } else {
+ QUICHE_BUG_IF(bug_22051_1, it->second->end_metadata)
+ << "Inconsistent metadata payload state!";
+ QUICHE_BUG_IF(bug_if_2781_2, it->second->bytes_remaining > 0)
+ << "Incomplete metadata block!";
+ }
+
+ if (it->second == nullptr) {
+ QUICHE_BUG(bug_2781_3) << "Null metadata payload state!";
+ return false;
+ }
+ current_stream_ = stream_id;
+ it->second->bytes_remaining = length;
+ it->second->end_metadata = (flags & kEndMetadataFlag);
+ return true;
+}
+
+void MetadataVisitor::OnFramePayload(const char* data, size_t len) {
+ QUICHE_VLOG(1) << "OnFramePayload(stream_id=" << current_stream_
+ << ", len=" << len << ")";
+ auto it = metadata_map_.find(current_stream_);
+ if (it == metadata_map_.end() || it->second == nullptr) {
+ QUICHE_BUG(bug_2781_4) << "Invalid order of operations on MetadataVisitor.";
+ } else {
+ MetadataPayloadState* state = it->second.get(); // For readability.
+ state->buffer.push_back(std::string(data, len));
+ if (len < state->bytes_remaining) {
+ state->bytes_remaining -= len;
+ } else {
+ QUICHE_BUG_IF(bug_22051_2, len > state->bytes_remaining)
+ << "Metadata payload overflow! len: " << len
+ << " bytes_remaining: " << state->bytes_remaining;
+ state->bytes_remaining = 0;
+ if (state->end_metadata) {
+ // The whole process of decoding the HPACK-encoded metadata block,
+ // below, is more cumbersome than it ought to be.
+ spdy::Http2HeaderBlockHpackListener listener;
+ http2::HpackDecoder decoder(&listener, kMaxMetadataBlockSize);
+
+ // If any operations fail, the decode process should be aborted.
+ bool success = decoder.StartDecodingBlock();
+ for (const std::string& slice : state->buffer) {
+ if (!success) {
+ break;
+ }
+ http2::DecodeBuffer buffer(slice.data(), slice.size());
+ success = success && decoder.DecodeFragment(&buffer);
+ }
+ success =
+ success && decoder.EndDecodingBlock() && !listener.hpack_error();
+ if (success) {
+ on_payload_(current_stream_, listener.release_header_block());
+ }
+ // TODO(birenroy): add varz counting metadata decode successes/failures.
+ metadata_map_.erase(it);
+ }
+ }
+ }
+}
+
+std::unique_ptr<MetadataSerializer::FrameSequence>
+MetadataSerializer::FrameSequenceForPayload(SpdyStreamId stream_id,
+ MetadataPayload payload) {
+ return absl::make_unique<MetadataFrameSequence>(stream_id,
+ std::move(payload));
+}
+
+} // namespace spdy
diff --git a/spdy/core/metadata_extension.h b/spdy/core/metadata_extension.h
new file mode 100644
index 0000000..043d6f0
--- /dev/null
+++ b/spdy/core/metadata_extension.h
@@ -0,0 +1,116 @@
+#ifndef QUICHE_SPDY_CORE_METADATA_EXTENSION_H_
+#define QUICHE_SPDY_CORE_METADATA_EXTENSION_H_
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "absl/container/flat_hash_map.h"
+#include "spdy/core/http2_frame_decoder_adapter.h"
+#include "spdy/core/spdy_header_block.h"
+#include "spdy/core/spdy_protocol.h"
+#include "spdy/core/zero_copy_output_buffer.h"
+
+namespace spdy {
+
+// An implementation of the ExtensionVisitorInterface that can parse
+// METADATA frames. METADATA is a non-standard HTTP/2 extension developed and
+// used internally at Google. A peer advertises support for METADATA by sending
+// a setting with a setting ID of kMetadataExtensionId and a value of 1.
+//
+// Metadata is represented as a HPACK header block with literal encoding.
+class MetadataVisitor : public spdy::ExtensionVisitorInterface {
+ public:
+ using MetadataPayload = spdy::SpdyHeaderBlock;
+
+ static_assert(!std::is_copy_constructible<MetadataPayload>::value,
+ "MetadataPayload should be a move-only type!");
+
+ using OnMetadataSupport = std::function<void(bool)>;
+ using OnCompletePayload =
+ std::function<void(spdy::SpdyStreamId, MetadataPayload)>;
+
+ // The HTTP/2 SETTINGS ID that is used to indicate support for METADATA
+ // frames.
+ static const spdy::SpdySettingsId kMetadataExtensionId;
+
+ // The 8-bit frame type code for a METADATA frame.
+ static const uint8_t kMetadataFrameType;
+
+ // The flag that indicates the end of a logical metadata block. Due to frame
+ // size limits, a single metadata block may be emitted as several HTTP/2
+ // frames.
+ static const uint8_t kEndMetadataFlag;
+
+ // |on_payload| is invoked whenever a complete metadata payload is received.
+ // |on_support| is invoked whenever the peer's advertised support for metadata
+ // changes.
+ MetadataVisitor(OnCompletePayload on_payload, OnMetadataSupport on_support);
+ ~MetadataVisitor() override;
+
+ MetadataVisitor(const MetadataVisitor&) = delete;
+ MetadataVisitor& operator=(const MetadataVisitor&) = delete;
+
+ // Interprets the non-standard setting indicating support for METADATA.
+ void OnSetting(spdy::SpdySettingsId id, uint32_t value) override;
+
+ // Returns true iff |type| indicates a METADATA frame.
+ bool OnFrameHeader(spdy::SpdyStreamId stream_id, size_t length, uint8_t type,
+ uint8_t flags) override;
+
+ // Consumes a METADATA frame payload. Invokes the registered callback when a
+ // complete payload has been received.
+ void OnFramePayload(const char* data, size_t len) override;
+
+ // Returns true if the peer has advertised support for METADATA via the
+ // appropriate setting.
+ bool PeerSupportsMetadata() const {
+ return peer_supports_metadata_ == MetadataSupportState::SUPPORTED;
+ }
+
+ private:
+ enum class MetadataSupportState : uint8_t {
+ UNSPECIFIED,
+ SUPPORTED,
+ NOT_SUPPORTED,
+ };
+
+ struct MetadataPayloadState;
+
+ using StreamMetadataMap =
+ absl::flat_hash_map<spdy::SpdyStreamId,
+ std::unique_ptr<MetadataPayloadState>>;
+
+ OnCompletePayload on_payload_;
+ OnMetadataSupport on_support_;
+ StreamMetadataMap metadata_map_;
+ spdy::SpdyStreamId current_stream_;
+ MetadataSupportState peer_supports_metadata_;
+};
+
+// A class that serializes metadata blocks as sequences of frames.
+class MetadataSerializer {
+ public:
+ using MetadataPayload = spdy::SpdyHeaderBlock;
+
+ class FrameSequence {
+ public:
+ virtual ~FrameSequence() {}
+
+ // Returns nullptr once the sequence has been exhausted.
+ virtual std::unique_ptr<spdy::SpdyFrameIR> Next() = 0;
+ };
+
+ MetadataSerializer() {}
+
+ MetadataSerializer(const MetadataSerializer&) = delete;
+ MetadataSerializer& operator=(const MetadataSerializer&) = delete;
+
+ // Returns nullptr on failure.
+ std::unique_ptr<FrameSequence> FrameSequenceForPayload(
+ spdy::SpdyStreamId stream_id, MetadataPayload payload);
+};
+
+} // namespace spdy
+
+#endif // QUICHE_SPDY_CORE_METADATA_EXTENSION_H_
diff --git a/spdy/core/metadata_extension_test.cc b/spdy/core/metadata_extension_test.cc
new file mode 100644
index 0000000..07a5ee1
--- /dev/null
+++ b/spdy/core/metadata_extension_test.cc
@@ -0,0 +1,226 @@
+#include "spdy/core/metadata_extension.h"
+
+#include "absl/container/flat_hash_map.h"
+#include "absl/functional/bind_front.h"
+#include "absl/strings/str_cat.h"
+#include "absl/strings/string_view.h"
+#include "common/platform/api/quiche_test.h"
+#include "spdy/core/array_output_buffer.h"
+#include "spdy/core/mock_spdy_framer_visitor.h"
+#include "spdy/core/spdy_framer.h"
+#include "spdy/core/spdy_header_block.h"
+#include "spdy/core/spdy_no_op_visitor.h"
+#include "spdy/core/spdy_protocol.h"
+
+namespace spdy {
+namespace test {
+namespace {
+
+using ::absl::bind_front;
+using ::spdy::SpdyFramer;
+using ::spdy::SpdyHeaderBlock;
+using ::spdy::test::MockSpdyFramerVisitor;
+using ::testing::ElementsAre;
+using ::testing::IsEmpty;
+
+const size_t kBufferSize = 64 * 1024;
+char kBuffer[kBufferSize];
+
+class MetadataExtensionTest : public QuicheTest {
+ protected:
+ MetadataExtensionTest() : test_buffer_(kBuffer, kBufferSize) {}
+
+ void SetUp() override {
+ extension_ = absl::make_unique<MetadataVisitor>(
+ bind_front(&MetadataExtensionTest::OnCompletePayload, this),
+ bind_front(&MetadataExtensionTest::OnMetadataSupport, this));
+ }
+
+ void OnCompletePayload(spdy::SpdyStreamId stream_id,
+ MetadataVisitor::MetadataPayload payload) {
+ ++received_count_;
+ received_payload_map_.insert(std::make_pair(stream_id, std::move(payload)));
+ }
+
+ void OnMetadataSupport(bool peer_supports_metadata) {
+ EXPECT_EQ(peer_supports_metadata, extension_->PeerSupportsMetadata());
+ received_metadata_support_.push_back(peer_supports_metadata);
+ }
+
+ MetadataSerializer::MetadataPayload PayloadForData(absl::string_view data) {
+ SpdyHeaderBlock block;
+ block["example-payload"] = data;
+ return block;
+ }
+
+ std::unique_ptr<MetadataVisitor> extension_;
+ absl::flat_hash_map<spdy::SpdyStreamId, SpdyHeaderBlock>
+ received_payload_map_;
+ std::vector<bool> received_metadata_support_;
+ size_t received_count_ = 0;
+ spdy::ArrayOutputBuffer test_buffer_;
+};
+
+// This test verifies that the MetadataVisitor is initialized to a state where
+// it believes the peer does not support metadata.
+TEST_F(MetadataExtensionTest, MetadataNotSupported) {
+ EXPECT_FALSE(extension_->PeerSupportsMetadata());
+ EXPECT_THAT(received_metadata_support_, IsEmpty());
+}
+
+// This test verifies that upon receiving a specific setting, the extension
+// realizes that the peer supports metadata.
+TEST_F(MetadataExtensionTest, MetadataSupported) {
+ EXPECT_FALSE(extension_->PeerSupportsMetadata());
+ // 3 is not an appropriate value for the metadata extension key.
+ extension_->OnSetting(MetadataVisitor::kMetadataExtensionId, 3);
+ EXPECT_FALSE(extension_->PeerSupportsMetadata());
+ extension_->OnSetting(MetadataVisitor::kMetadataExtensionId, 1);
+ ASSERT_TRUE(extension_->PeerSupportsMetadata());
+ extension_->OnSetting(MetadataVisitor::kMetadataExtensionId, 0);
+ EXPECT_FALSE(extension_->PeerSupportsMetadata());
+ EXPECT_THAT(received_metadata_support_, ElementsAre(true, false));
+}
+
+TEST_F(MetadataExtensionTest, MetadataIgnoredWithoutExtension) {
+ const char kData[] = "some payload";
+ SpdyHeaderBlock payload = PayloadForData(kData);
+
+ extension_->OnSetting(MetadataVisitor::kMetadataExtensionId, 1);
+ ASSERT_TRUE(extension_->PeerSupportsMetadata());
+
+ MetadataSerializer serializer;
+ auto sequence = serializer.FrameSequenceForPayload(3, std::move(payload));
+ ASSERT_TRUE(sequence != nullptr);
+
+ http2::Http2DecoderAdapter deframer;
+ ::testing::StrictMock<MockSpdyFramerVisitor> visitor;
+ deframer.set_visitor(&visitor);
+
+ // The Return(true) should not be necessary. http://b/36023792
+ EXPECT_CALL(visitor, OnUnknownFrame(3, MetadataVisitor::kMetadataFrameType))
+ .WillOnce(::testing::Return(true));
+
+ SpdyFramer framer(SpdyFramer::ENABLE_COMPRESSION);
+ auto frame = sequence->Next();
+ ASSERT_TRUE(frame != nullptr);
+ while (frame != nullptr) {
+ const size_t frame_size = framer.SerializeFrame(*frame, &test_buffer_);
+ ASSERT_GT(frame_size, 0);
+ ASSERT_FALSE(deframer.HasError());
+ ASSERT_EQ(frame_size, test_buffer_.Size());
+ EXPECT_EQ(frame_size, deframer.ProcessInput(kBuffer, frame_size));
+ test_buffer_.Reset();
+ frame = sequence->Next();
+ }
+ EXPECT_FALSE(deframer.HasError());
+ EXPECT_THAT(received_metadata_support_, ElementsAre(true));
+}
+
+// This test verifies that the METADATA frame emitted by a MetadataExtension
+// can be parsed by another SpdyFramer with a MetadataVisitor.
+TEST_F(MetadataExtensionTest, MetadataPayloadEndToEnd) {
+ SpdyHeaderBlock block1;
+ block1["foo"] = "Some metadata value.";
+ SpdyHeaderBlock block2;
+ block2["bar"] =
+ "The color taupe truly represents a triumph of the human spirit over "
+ "adversity.";
+ block2["baz"] =
+ "Or perhaps it represents abject surrender to the implacable and "
+ "incomprehensible forces of the universe.";
+ const absl::string_view binary_payload{"binary\0payload", 14};
+ block2["qux"] = binary_payload;
+ EXPECT_EQ(binary_payload, block2["qux"]);
+ for (const SpdyHeaderBlock& payload_block :
+ {std::move(block1), std::move(block2)}) {
+ extension_->OnSetting(MetadataVisitor::kMetadataExtensionId, 1);
+ ASSERT_TRUE(extension_->PeerSupportsMetadata());
+
+ MetadataSerializer serializer;
+ auto sequence =
+ serializer.FrameSequenceForPayload(3, payload_block.Clone());
+ ASSERT_TRUE(sequence != nullptr);
+
+ http2::Http2DecoderAdapter deframer;
+ ::spdy::SpdyNoOpVisitor visitor;
+ deframer.set_visitor(&visitor);
+ deframer.set_extension_visitor(extension_.get());
+ SpdyFramer framer(SpdyFramer::ENABLE_COMPRESSION);
+ auto frame = sequence->Next();
+ ASSERT_TRUE(frame != nullptr);
+ while (frame != nullptr) {
+ const size_t frame_size = framer.SerializeFrame(*frame, &test_buffer_);
+ ASSERT_GT(frame_size, 0);
+ ASSERT_FALSE(deframer.HasError());
+ ASSERT_EQ(frame_size, test_buffer_.Size());
+ EXPECT_EQ(frame_size, deframer.ProcessInput(kBuffer, frame_size));
+ test_buffer_.Reset();
+ frame = sequence->Next();
+ }
+ EXPECT_EQ(1, received_count_);
+ auto it = received_payload_map_.find(3);
+ ASSERT_TRUE(it != received_payload_map_.end());
+ EXPECT_EQ(payload_block, it->second);
+
+ received_count_ = 0;
+ received_payload_map_.clear();
+ }
+}
+
+// This test verifies that METADATA frames for two different streams can be
+// interleaved and still successfully parsed by another SpdyFramer with a
+// MetadataVisitor.
+TEST_F(MetadataExtensionTest, MetadataPayloadInterleaved) {
+ const std::string kData1 = std::string(65 * 1024, 'a');
+ const std::string kData2 = std::string(65 * 1024, 'b');
+ const SpdyHeaderBlock payload1 = PayloadForData(kData1);
+ const SpdyHeaderBlock payload2 = PayloadForData(kData2);
+
+ extension_->OnSetting(MetadataVisitor::kMetadataExtensionId, 1);
+ ASSERT_TRUE(extension_->PeerSupportsMetadata());
+
+ MetadataSerializer serializer;
+ auto sequence1 = serializer.FrameSequenceForPayload(3, payload1.Clone());
+ ASSERT_TRUE(sequence1 != nullptr);
+
+ auto sequence2 = serializer.FrameSequenceForPayload(5, payload2.Clone());
+ ASSERT_TRUE(sequence2 != nullptr);
+
+ http2::Http2DecoderAdapter deframer;
+ ::spdy::SpdyNoOpVisitor visitor;
+ deframer.set_visitor(&visitor);
+ deframer.set_extension_visitor(extension_.get());
+
+ SpdyFramer framer(SpdyFramer::ENABLE_COMPRESSION);
+ auto frame1 = sequence1->Next();
+ ASSERT_TRUE(frame1 != nullptr);
+ auto frame2 = sequence2->Next();
+ ASSERT_TRUE(frame2 != nullptr);
+ while (frame1 != nullptr || frame2 != nullptr) {
+ for (auto frame : {frame1.get(), frame2.get()}) {
+ if (frame != nullptr) {
+ const size_t frame_size = framer.SerializeFrame(*frame, &test_buffer_);
+ ASSERT_GT(frame_size, 0);
+ ASSERT_FALSE(deframer.HasError());
+ ASSERT_EQ(frame_size, test_buffer_.Size());
+ EXPECT_EQ(frame_size, deframer.ProcessInput(kBuffer, frame_size));
+ test_buffer_.Reset();
+ }
+ }
+ frame1 = sequence1->Next();
+ frame2 = sequence2->Next();
+ }
+ EXPECT_EQ(2, received_count_);
+ auto it = received_payload_map_.find(3);
+ ASSERT_TRUE(it != received_payload_map_.end());
+ EXPECT_EQ(payload1, it->second);
+
+ it = received_payload_map_.find(5);
+ ASSERT_TRUE(it != received_payload_map_.end());
+ EXPECT_EQ(payload2, it->second);
+}
+
+} // anonymous namespace
+} // namespace test
+} // namespace spdy