blob: e0dfec60007739572495db3788e5fbffc80aca96 [file] [log] [blame]
#include "http2/adapter/test_utils.h"
#include "http2/adapter/nghttp2_util.h"
#include "common/quiche_endian.h"
#include "spdy/core/spdy_frame_reader.h"
namespace http2 {
namespace adapter {
namespace test {
TestDataFrameSource::TestDataFrameSource(Http2VisitorInterface& visitor,
absl::string_view data_payload,
bool has_fin)
: visitor_(visitor), has_fin_(has_fin) {
if (!data_payload.empty()) {
payload_fragments_.push_back(std::string(data_payload));
current_fragment_ = payload_fragments_.front();
}
}
TestDataFrameSource::TestDataFrameSource(
Http2VisitorInterface& visitor,
absl::Span<absl::string_view> payload_fragments,
bool has_fin)
: visitor_(visitor), has_fin_(has_fin) {
payload_fragments_.reserve(payload_fragments.size());
for (absl::string_view fragment : payload_fragments) {
if (!fragment.empty()) {
payload_fragments_.push_back(std::string(fragment));
}
}
if (!payload_fragments_.empty()) {
current_fragment_ = payload_fragments_.front();
}
}
std::pair<ssize_t, bool> TestDataFrameSource::SelectPayloadLength(
size_t max_length) {
if (!is_data_available_) {
return {kBlocked, false};
}
// The stream is done if there's no more data, or if |max_length| is at least
// as large as the remaining data.
const bool end_data =
current_fragment_.empty() || (payload_fragments_.size() == 1 &&
max_length >= current_fragment_.size());
const ssize_t length = std::min(max_length, current_fragment_.size());
return {length, end_data};
}
bool TestDataFrameSource::Send(absl::string_view frame_header,
size_t payload_length) {
QUICHE_LOG_IF(DFATAL, payload_length > current_fragment_.size())
<< "payload_length: " << payload_length
<< " current_fragment_size: " << current_fragment_.size();
const std::string concatenated =
absl::StrCat(frame_header, current_fragment_.substr(0, payload_length));
const ssize_t result = visitor_.OnReadyToSend(concatenated);
if (result < 0) {
// Write encountered error.
visitor_.OnConnectionError();
current_fragment_ = {};
payload_fragments_.clear();
return false;
} else if (result == 0) {
// Write blocked.
return false;
} else if (result < concatenated.size()) {
// Probably need to handle this better within this test class.
QUICHE_LOG(DFATAL)
<< "DATA frame not fully flushed. Connection will be corrupt!";
visitor_.OnConnectionError();
current_fragment_ = {};
payload_fragments_.clear();
return false;
}
current_fragment_.remove_prefix(payload_length);
if (current_fragment_.empty()) {
payload_fragments_.erase(payload_fragments_.begin());
if (!payload_fragments_.empty()) {
current_fragment_ = payload_fragments_.front();
}
}
return true;
}
namespace {
using TypeAndOptionalLength =
std::pair<spdy::SpdyFrameType, absl::optional<size_t>>;
std::vector<std::pair<const char*, std::string>> LogFriendly(
const std::vector<TypeAndOptionalLength>& types_and_lengths) {
std::vector<std::pair<const char*, std::string>> out;
out.reserve(types_and_lengths.size());
for (const auto type_and_length : types_and_lengths) {
out.push_back({spdy::FrameTypeToString(type_and_length.first),
type_and_length.second
? absl::StrCat(type_and_length.second.value())
: "<unspecified>"});
}
return out;
}
// Custom gMock matcher, used to implement EqualsFrames().
class SpdyControlFrameMatcher
: public testing::MatcherInterface<absl::string_view> {
public:
explicit SpdyControlFrameMatcher(
std::vector<TypeAndOptionalLength> types_and_lengths)
: expected_types_and_lengths_(std::move(types_and_lengths)) {}
bool MatchAndExplain(absl::string_view s,
testing::MatchResultListener* listener) const override {
spdy::SpdyFrameReader reader(s.data(), s.size());
for (TypeAndOptionalLength expected : expected_types_and_lengths_) {
if (!MatchAndExplainOneFrame(expected.first, expected.second, &reader,
listener)) {
return false;
}
}
if (!reader.IsDoneReading()) {
size_t bytes_remaining = s.size() - reader.GetBytesConsumed();
*listener << "; " << bytes_remaining << " bytes left to read!";
return false;
}
return true;
}
bool MatchAndExplainOneFrame(spdy::SpdyFrameType expected_type,
absl::optional<size_t> expected_length,
spdy::SpdyFrameReader* reader,
testing::MatchResultListener* listener) const {
uint32_t payload_length;
if (!reader->ReadUInt24(&payload_length)) {
*listener << "; unable to read length field for expected_type "
<< FrameTypeToString(expected_type) << ". data too short!";
return false;
}
if (expected_length && payload_length != expected_length.value()) {
*listener << "; actual length: " << payload_length
<< " but expected length: " << expected_length.value();
return false;
}
uint8_t raw_type;
if (!reader->ReadUInt8(&raw_type)) {
*listener << "; unable to read type field for expected_type "
<< FrameTypeToString(expected_type) << ". data too short!";
return false;
}
if (!spdy::IsDefinedFrameType(raw_type)) {
*listener << "; expected type " << FrameTypeToString(expected_type)
<< " but raw type " << static_cast<int>(raw_type)
<< " is not a defined frame type!";
return false;
}
spdy::SpdyFrameType actual_type = spdy::ParseFrameType(raw_type);
if (actual_type != expected_type) {
*listener << "; actual type: " << FrameTypeToString(actual_type)
<< " but expected type: " << FrameTypeToString(expected_type);
return false;
}
// Seek past flags (1B), stream ID (4B), and payload. Reach the next frame.
reader->Seek(5 + payload_length);
return true;
}
void DescribeTo(std::ostream* os) const override {
*os << "Data contains frames of types in sequence "
<< LogFriendly(expected_types_and_lengths_);
}
void DescribeNegationTo(std::ostream* os) const override {
*os << "Data does not contain frames of types in sequence "
<< LogFriendly(expected_types_and_lengths_);
}
private:
const std::vector<TypeAndOptionalLength> expected_types_and_lengths_;
};
// Custom gMock matcher, used to implement HasFrameHeader().
class FrameHeaderMatcher
: public testing::MatcherInterface<const nghttp2_frame_hd*> {
public:
FrameHeaderMatcher(int32_t streamid,
uint8_t type,
const testing::Matcher<int> flags)
: stream_id_(streamid), type_(type), flags_(flags) {}
bool MatchAndExplain(const nghttp2_frame_hd* frame,
testing::MatchResultListener* listener) const override {
bool matched = true;
if (stream_id_ != frame->stream_id) {
*listener << "; expected stream " << stream_id_ << ", saw "
<< frame->stream_id;
matched = false;
}
if (type_ != frame->type) {
*listener << "; expected frame type " << type_ << ", saw "
<< static_cast<int>(frame->type);
matched = false;
}
if (!flags_.MatchAndExplain(frame->flags, listener)) {
matched = false;
}
return matched;
}
void DescribeTo(std::ostream* os) const override {
*os << "contains a frame header with stream " << stream_id_ << ", type "
<< type_ << ", ";
flags_.DescribeTo(os);
}
void DescribeNegationTo(std::ostream* os) const override {
*os << "does not contain a frame header with stream " << stream_id_
<< ", type " << type_ << ", ";
flags_.DescribeNegationTo(os);
}
private:
const int32_t stream_id_;
const int type_;
const testing::Matcher<int> flags_;
};
class DataMatcher : public testing::MatcherInterface<const nghttp2_frame*> {
public:
DataMatcher(const testing::Matcher<uint32_t> stream_id,
const testing::Matcher<size_t> length,
const testing::Matcher<int> flags)
: stream_id_(stream_id), length_(length), flags_(flags) {}
bool MatchAndExplain(const nghttp2_frame* frame,
testing::MatchResultListener* listener) const override {
if (frame->hd.type != NGHTTP2_DATA) {
*listener << "; expected DATA frame, saw frame of type "
<< static_cast<int>(frame->hd.type);
return false;
}
bool matched = true;
if (!stream_id_.MatchAndExplain(frame->hd.stream_id, listener)) {
matched = false;
}
if (!length_.MatchAndExplain(frame->hd.length, listener)) {
matched = false;
}
if (!flags_.MatchAndExplain(frame->hd.flags, listener)) {
matched = false;
}
return matched;
}
void DescribeTo(std::ostream* os) const override {
*os << "contains a DATA frame, ";
stream_id_.DescribeTo(os);
length_.DescribeTo(os);
flags_.DescribeTo(os);
}
void DescribeNegationTo(std::ostream* os) const override {
*os << "does not contain a DATA frame, ";
stream_id_.DescribeNegationTo(os);
length_.DescribeNegationTo(os);
flags_.DescribeNegationTo(os);
}
private:
const testing::Matcher<uint32_t> stream_id_;
const testing::Matcher<size_t> length_;
const testing::Matcher<int> flags_;
};
class HeadersMatcher : public testing::MatcherInterface<const nghttp2_frame*> {
public:
HeadersMatcher(const testing::Matcher<uint32_t> stream_id,
const testing::Matcher<int> flags,
const testing::Matcher<int> category)
: stream_id_(stream_id), flags_(flags), category_(category) {}
bool MatchAndExplain(const nghttp2_frame* frame,
testing::MatchResultListener* listener) const override {
if (frame->hd.type != NGHTTP2_HEADERS) {
*listener << "; expected HEADERS frame, saw frame of type "
<< static_cast<int>(frame->hd.type);
return false;
}
bool matched = true;
if (!stream_id_.MatchAndExplain(frame->hd.stream_id, listener)) {
matched = false;
}
if (!flags_.MatchAndExplain(frame->hd.flags, listener)) {
matched = false;
}
if (!category_.MatchAndExplain(frame->headers.cat, listener)) {
matched = false;
}
return matched;
}
void DescribeTo(std::ostream* os) const override {
*os << "contains a HEADERS frame, ";
stream_id_.DescribeTo(os);
flags_.DescribeTo(os);
category_.DescribeTo(os);
}
void DescribeNegationTo(std::ostream* os) const override {
*os << "does not contain a HEADERS frame, ";
stream_id_.DescribeNegationTo(os);
flags_.DescribeNegationTo(os);
category_.DescribeNegationTo(os);
}
private:
const testing::Matcher<uint32_t> stream_id_;
const testing::Matcher<int> flags_;
const testing::Matcher<int> category_;
};
class RstStreamMatcher
: public testing::MatcherInterface<const nghttp2_frame*> {
public:
RstStreamMatcher(const testing::Matcher<uint32_t> stream_id,
const testing::Matcher<uint32_t> error_code)
: stream_id_(stream_id), error_code_(error_code) {}
bool MatchAndExplain(const nghttp2_frame* frame,
testing::MatchResultListener* listener) const override {
if (frame->hd.type != NGHTTP2_RST_STREAM) {
*listener << "; expected RST_STREAM frame, saw frame of type "
<< static_cast<int>(frame->hd.type);
return false;
}
bool matched = true;
if (!stream_id_.MatchAndExplain(frame->hd.stream_id, listener)) {
matched = false;
}
if (!error_code_.MatchAndExplain(frame->rst_stream.error_code, listener)) {
matched = false;
}
return matched;
}
void DescribeTo(std::ostream* os) const override {
*os << "contains a RST_STREAM frame, ";
stream_id_.DescribeTo(os);
error_code_.DescribeTo(os);
}
void DescribeNegationTo(std::ostream* os) const override {
*os << "does not contain a RST_STREAM frame, ";
stream_id_.DescribeNegationTo(os);
error_code_.DescribeNegationTo(os);
}
private:
const testing::Matcher<uint32_t> stream_id_;
const testing::Matcher<uint32_t> error_code_;
};
class SettingsMatcher : public testing::MatcherInterface<const nghttp2_frame*> {
public:
SettingsMatcher(const testing::Matcher<std::vector<Http2Setting>> values)
: values_(values) {}
bool MatchAndExplain(const nghttp2_frame* frame,
testing::MatchResultListener* listener) const override {
if (frame->hd.type != NGHTTP2_SETTINGS) {
*listener << "; expected SETTINGS frame, saw frame of type "
<< static_cast<int>(frame->hd.type);
return false;
}
std::vector<Http2Setting> settings;
settings.reserve(frame->settings.niv);
for (int i = 0; i < frame->settings.niv; ++i) {
const auto& p = frame->settings.iv[i];
settings.push_back({static_cast<uint16_t>(p.settings_id), p.value});
}
return values_.MatchAndExplain(settings, listener);
}
void DescribeTo(std::ostream* os) const override {
*os << "contains a SETTINGS frame, ";
values_.DescribeTo(os);
}
void DescribeNegationTo(std::ostream* os) const override {
*os << "does not contain a SETTINGS frame, ";
values_.DescribeNegationTo(os);
}
private:
const testing::Matcher<std::vector<Http2Setting>> values_;
};
class PingMatcher : public testing::MatcherInterface<const nghttp2_frame*> {
public:
PingMatcher(const testing::Matcher<uint64_t> id, bool is_ack)
: id_(id), is_ack_(is_ack) {}
bool MatchAndExplain(const nghttp2_frame* frame,
testing::MatchResultListener* listener) const override {
if (frame->hd.type != NGHTTP2_PING) {
*listener << "; expected PING frame, saw frame of type "
<< static_cast<int>(frame->hd.type);
return false;
}
bool matched = true;
bool frame_ack = frame->hd.flags & NGHTTP2_FLAG_ACK;
if (is_ack_ != frame_ack) {
*listener << "; expected is_ack=" << is_ack_ << ", saw " << frame_ack;
matched = false;
}
uint64_t data;
std::memcpy(&data, frame->ping.opaque_data, sizeof(data));
data = quiche::QuicheEndian::HostToNet64(data);
if (!id_.MatchAndExplain(data, listener)) {
matched = false;
}
return matched;
}
void DescribeTo(std::ostream* os) const override {
*os << "contains a PING frame, ";
id_.DescribeTo(os);
}
void DescribeNegationTo(std::ostream* os) const override {
*os << "does not contain a PING frame, ";
id_.DescribeNegationTo(os);
}
private:
const testing::Matcher<uint64_t> id_;
const bool is_ack_;
};
class GoAwayMatcher : public testing::MatcherInterface<const nghttp2_frame*> {
public:
GoAwayMatcher(const testing::Matcher<uint32_t> last_stream_id,
const testing::Matcher<uint32_t> error_code,
const testing::Matcher<absl::string_view> opaque_data)
: last_stream_id_(last_stream_id),
error_code_(error_code),
opaque_data_(opaque_data) {}
bool MatchAndExplain(const nghttp2_frame* frame,
testing::MatchResultListener* listener) const override {
if (frame->hd.type != NGHTTP2_GOAWAY) {
*listener << "; expected GOAWAY frame, saw frame of type "
<< static_cast<int>(frame->hd.type);
return false;
}
bool matched = true;
if (!last_stream_id_.MatchAndExplain(frame->goaway.last_stream_id,
listener)) {
matched = false;
}
if (!error_code_.MatchAndExplain(frame->goaway.error_code, listener)) {
matched = false;
}
auto opaque_data =
ToStringView(frame->goaway.opaque_data, frame->goaway.opaque_data_len);
if (!opaque_data_.MatchAndExplain(opaque_data, listener)) {
matched = false;
}
return matched;
}
void DescribeTo(std::ostream* os) const override {
*os << "contains a GOAWAY frame, ";
last_stream_id_.DescribeTo(os);
error_code_.DescribeTo(os);
opaque_data_.DescribeTo(os);
}
void DescribeNegationTo(std::ostream* os) const override {
*os << "does not contain a GOAWAY frame, ";
last_stream_id_.DescribeNegationTo(os);
error_code_.DescribeNegationTo(os);
opaque_data_.DescribeNegationTo(os);
}
private:
const testing::Matcher<uint32_t> last_stream_id_;
const testing::Matcher<uint32_t> error_code_;
const testing::Matcher<absl::string_view> opaque_data_;
};
class WindowUpdateMatcher
: public testing::MatcherInterface<const nghttp2_frame*> {
public:
WindowUpdateMatcher(const testing::Matcher<uint32_t> delta) : delta_(delta) {}
bool MatchAndExplain(const nghttp2_frame* frame,
testing::MatchResultListener* listener) const override {
if (frame->hd.type != NGHTTP2_WINDOW_UPDATE) {
*listener << "; expected WINDOW_UPDATE frame, saw frame of type "
<< static_cast<int>(frame->hd.type);
return false;
}
return delta_.MatchAndExplain(frame->window_update.window_size_increment,
listener);
}
void DescribeTo(std::ostream* os) const override {
*os << "contains a WINDOW_UPDATE frame, ";
delta_.DescribeTo(os);
}
void DescribeNegationTo(std::ostream* os) const override {
*os << "does not contain a WINDOW_UPDATE frame, ";
delta_.DescribeNegationTo(os);
}
private:
const testing::Matcher<uint32_t> delta_;
};
} // namespace
testing::Matcher<absl::string_view> EqualsFrames(
std::vector<std::pair<spdy::SpdyFrameType, absl::optional<size_t>>>
types_and_lengths) {
return MakeMatcher(new SpdyControlFrameMatcher(std::move(types_and_lengths)));
}
testing::Matcher<absl::string_view> EqualsFrames(
std::vector<spdy::SpdyFrameType> types) {
std::vector<std::pair<spdy::SpdyFrameType, absl::optional<size_t>>>
types_and_lengths;
types_and_lengths.reserve(types.size());
for (spdy::SpdyFrameType type : types) {
types_and_lengths.push_back({type, absl::nullopt});
}
return MakeMatcher(new SpdyControlFrameMatcher(std::move(types_and_lengths)));
}
testing::Matcher<const nghttp2_frame_hd*> HasFrameHeader(
uint32_t streamid,
uint8_t type,
const testing::Matcher<int> flags) {
return MakeMatcher(new FrameHeaderMatcher(streamid, type, flags));
}
testing::Matcher<const nghttp2_frame*> IsData(
const testing::Matcher<uint32_t> stream_id,
const testing::Matcher<size_t> length,
const testing::Matcher<int> flags) {
return MakeMatcher(new DataMatcher(stream_id, length, flags));
}
testing::Matcher<const nghttp2_frame*> IsHeaders(
const testing::Matcher<uint32_t> stream_id,
const testing::Matcher<int> flags,
const testing::Matcher<int> category) {
return MakeMatcher(new HeadersMatcher(stream_id, flags, category));
}
testing::Matcher<const nghttp2_frame*> IsRstStream(
const testing::Matcher<uint32_t> stream_id,
const testing::Matcher<uint32_t> error_code) {
return MakeMatcher(new RstStreamMatcher(stream_id, error_code));
}
testing::Matcher<const nghttp2_frame*> IsSettings(
const testing::Matcher<std::vector<Http2Setting>> values) {
return MakeMatcher(new SettingsMatcher(values));
}
testing::Matcher<const nghttp2_frame*> IsPing(
const testing::Matcher<uint64_t> id) {
return MakeMatcher(new PingMatcher(id, false));
}
testing::Matcher<const nghttp2_frame*> IsPingAck(
const testing::Matcher<uint64_t> id) {
return MakeMatcher(new PingMatcher(id, true));
}
testing::Matcher<const nghttp2_frame*> IsGoAway(
const testing::Matcher<uint32_t> last_stream_id,
const testing::Matcher<uint32_t> error_code,
const testing::Matcher<absl::string_view> opaque_data) {
return MakeMatcher(
new GoAwayMatcher(last_stream_id, error_code, opaque_data));
}
testing::Matcher<const nghttp2_frame*> IsWindowUpdate(
const testing::Matcher<uint32_t> delta) {
return MakeMatcher(new WindowUpdateMatcher(delta));
}
} // namespace test
} // namespace adapter
} // namespace http2