Commit c7255f01 authored by Renjie Tang's avatar Renjie Tang Committed by Commit Bot

Complete the implementation of QuicClientSessionCache to allow multiple TLS...

Complete the implementation of QuicClientSessionCache to allow multiple TLS session ticket for a single QUIC session.

Change-Id: Iff6bbcb74708ac0584662273c0ad01162b597a98
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2155165Reviewed-by: default avatarNick Harper <nharper@chromium.org>
Commit-Queue: Renjie Tang <renjietang@chromium.org>
Cr-Commit-Position: refs/heads/master@{#761149}
parent af14fc18
......@@ -4340,9 +4340,7 @@ test("net_unittests") {
"quic/quic_chromium_client_session_test.cc",
"quic/quic_chromium_client_stream_test.cc",
"quic/quic_chromium_connection_helper_test.cc",
# Temporarily disabled due to version reordering.
# "quic/quic_client_session_cache_unittests.cc",
"quic/quic_client_session_cache_unittests.cc",
"quic/quic_clock_skew_detector_test.cc",
"quic/quic_connectivity_probing_manager_test.cc",
"quic/quic_end_to_end_unittest.cc",
......
......@@ -12,20 +12,31 @@ namespace net {
namespace {
const size_t kDefaultMaxEntries = 1024;
// Check whether the SSL session inside |state| is expired at |now|.
bool IsExpired(quic::QuicResumptionState* state, time_t now) {
// Returns false if the SSL |session| doesn't exist or it is expired at |now|.
bool IsValid(SSL_SESSION* session, time_t now) {
if (!session)
return false;
if (now < 0)
return true;
return false;
SSL_SESSION* session = state->tls_session.get();
uint64_t now_u64 = static_cast<uint64_t>(now);
// now_u64 may be slightly behind because of differences in how
// time is calculated at this layer versus BoringSSL.
// Add a second of wiggle room to account for this.
return now_u64 < SSL_SESSION_get_time(session) - 1 ||
now_u64 >=
SSL_SESSION_get_time(session) + SSL_SESSION_get_timeout(session);
return !(now_u64 < SSL_SESSION_get_time(session) - 1 ||
now_u64 >= SSL_SESSION_get_time(session) +
SSL_SESSION_get_timeout(session));
}
bool DoApplicationStatesMatch(quic::ApplicationState* state,
quic::ApplicationState* other) {
if ((state && !other) || (!state && other))
return false;
if ((!state && !other) || *state == *other)
return true;
return false;
}
} // namespace
......@@ -47,11 +58,29 @@ QuicClientSessionCache::~QuicClientSessionCache() {
void QuicClientSessionCache::Insert(const quic::QuicServerId& server_id,
bssl::UniquePtr<SSL_SESSION> session,
quic::TransportParameters* params,
std::vector<uint8_t>* application_states) {
auto state = std::make_unique<quic::QuicResumptionState>();
state->tls_session = std::move(session);
// TODO: Do something with TransportParams and application_states.
cache_.Put(server_id, std::move(state));
quic::ApplicationState* application_state) {
DCHECK(session) << "TLS session is not inserted into client cache.";
DCHECK(params) << "Transport Parameters is not inserted into client cache.";
auto iter = cache_.Get(server_id);
if (iter == cache_.end()) {
CreateAndInsertEntry(server_id, std::move(session), params,
application_state);
return;
}
DCHECK(iter->second.params);
// The states are both the same, so only need to insert sessions.
if (*params == *iter->second.params &&
DoApplicationStatesMatch(application_state,
iter->second.application_state.get())) {
iter->second.PushSession(std::move(session));
return;
}
// Erase the existing entry because this Insert call must come from a
// different QUIC session.
cache_.Erase(iter);
CreateAndInsertEntry(server_id, std::move(session), params,
application_state);
}
std::unique_ptr<quic::QuicResumptionState> QuicClientSessionCache::Lookup(
......@@ -62,18 +91,23 @@ std::unique_ptr<quic::QuicResumptionState> QuicClientSessionCache::Lookup(
return nullptr;
time_t now = clock_->Now().ToTimeT();
std::unique_ptr<quic::QuicResumptionState> state = std::move(iter->second);
if (!IsValid(iter->second.PeekSession(), now)) {
cache_.Erase(iter);
if (IsExpired(state.get(), now))
state = nullptr;
return nullptr;
}
auto state = std::make_unique<quic::QuicResumptionState>();
state->tls_session = iter->second.PopSession();
state->transport_params = iter->second.params.get();
state->application_state = iter->second.application_state.get();
return state;
}
void QuicClientSessionCache::FlushExpiredStates() {
void QuicClientSessionCache::FlushInvalidEntries() {
time_t now = clock_->Now().ToTimeT();
auto iter = cache_.begin();
while (iter != cache_.end()) {
if (IsExpired(iter->second.get(), now)) {
if (!IsValid(iter->second.PeekSession(), now)) {
iter = cache_.Erase(iter);
} else {
++iter;
......@@ -87,7 +121,7 @@ void QuicClientSessionCache::OnMemoryPressure(
case base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_NONE:
break;
case base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE:
FlushExpiredStates();
FlushInvalidEntries();
break;
case base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL:
Flush();
......@@ -95,4 +129,48 @@ void QuicClientSessionCache::OnMemoryPressure(
}
}
void QuicClientSessionCache::Flush() {
cache_.Clear();
}
void QuicClientSessionCache::CreateAndInsertEntry(
const quic::QuicServerId& server_id,
bssl::UniquePtr<SSL_SESSION> session,
quic::TransportParameters* params,
quic::ApplicationState* application_state) {
Entry entry;
entry.PushSession(std::move(session));
entry.params = std::make_unique<quic::TransportParameters>(*params);
if (application_state) {
entry.application_state =
std::make_unique<quic::ApplicationState>(*application_state);
}
cache_.Put(server_id, std::move(entry));
}
QuicClientSessionCache::Entry::Entry() = default;
QuicClientSessionCache::Entry::Entry(Entry&&) = default;
QuicClientSessionCache::Entry::~Entry() = default;
void QuicClientSessionCache::Entry::PushSession(
bssl::UniquePtr<SSL_SESSION> session) {
if (sessions[0] != nullptr) {
sessions[1] = std::move(sessions[0]);
}
sessions[0] = std::move(session);
}
bssl::UniquePtr<SSL_SESSION> QuicClientSessionCache::Entry::PopSession() {
if (sessions[0] == nullptr)
return nullptr;
bssl::UniquePtr<SSL_SESSION> session = std::move(sessions[0]);
sessions[0] = std::move(sessions[1]);
sessions[1] = nullptr;
return session;
}
SSL_SESSION* QuicClientSessionCache::Entry::PeekSession() {
return sessions[0].get();
}
} // namespace net
......@@ -33,7 +33,7 @@ class NET_EXPORT_PRIVATE QuicClientSessionCache : public quic::SessionCache {
void Insert(const quic::QuicServerId& server_id,
bssl::UniquePtr<SSL_SESSION> session,
quic::TransportParameters* params,
std::vector<uint8_t>* application_states) override;
quic::ApplicationState* application_state) override;
std::unique_ptr<quic::QuicResumptionState> Lookup(
const quic::QuicServerId& server_id,
......@@ -43,17 +43,40 @@ class NET_EXPORT_PRIVATE QuicClientSessionCache : public quic::SessionCache {
size_t size() const { return cache_.size(); }
void Flush() { cache_.Clear(); }
void Flush();
void OnMemoryPressure(
base::MemoryPressureListener::MemoryPressureLevel memory_pressure_level);
private:
void FlushExpiredStates();
struct Entry {
Entry();
Entry(Entry&&);
~Entry();
// Adds a new |session| onto sessions, dropping the oldest one if two are
// already stored.
void PushSession(bssl::UniquePtr<SSL_SESSION> session);
// Retrieves the latest session from the entry, meanwhile removing it.
bssl::UniquePtr<SSL_SESSION> PopSession();
SSL_SESSION* PeekSession();
bssl::UniquePtr<SSL_SESSION> sessions[2];
std::unique_ptr<quic::TransportParameters> params;
std::unique_ptr<quic::ApplicationState> application_state;
};
void FlushInvalidEntries();
// Creates a new entry and insert into |cache_|.
void CreateAndInsertEntry(const quic::QuicServerId& server_id,
bssl::UniquePtr<SSL_SESSION> session,
quic::TransportParameters* params,
quic::ApplicationState* application_state);
base::Clock* clock_;
base::MRUCache<quic::QuicServerId, std::unique_ptr<quic::QuicResumptionState>>
cache_;
base::MRUCache<quic::QuicServerId, Entry> cache_;
std::unique_ptr<base::MemoryPressureListener> memory_pressure_listener_;
};
......
......@@ -15,6 +15,28 @@ namespace net {
namespace {
const quic::QuicVersionLabel kFakeVersionLabel = 0x01234567;
const quic::QuicVersionLabel kFakeVersionLabel2 = 0x89ABCDEF;
const uint64_t kFakeIdleTimeoutMilliseconds = 12012;
const uint8_t kFakeStatelessResetTokenData[16] = {
0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,
0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F};
const uint64_t kFakeMaxPacketSize = 9001;
const uint64_t kFakeInitialMaxData = 101;
const bool kFakeDisableMigration = true;
const auto kCustomParameter1 =
static_cast<quic::TransportParameters::TransportParameterId>(0xffcd);
const char* kCustomParameter1Value = "foo";
const auto kCustomParameter2 =
static_cast<quic::TransportParameters::TransportParameterId>(0xff34);
const char* kCustomParameter2Value = "bar";
std::vector<uint8_t> CreateFakeStatelessResetToken() {
return std::vector<uint8_t>(
kFakeStatelessResetTokenData,
kFakeStatelessResetTokenData + base::size(kFakeStatelessResetTokenData));
}
std::unique_ptr<base::SimpleTestClock> MakeTestClock() {
std::unique_ptr<base::SimpleTestClock> clock =
std::make_unique<base::SimpleTestClock>();
......@@ -24,6 +46,23 @@ std::unique_ptr<base::SimpleTestClock> MakeTestClock() {
return clock;
}
// Make a TransportParameters that has a few fields set to help test comparison.
std::unique_ptr<quic::TransportParameters> MakeFakeTransportParams() {
auto params = std::make_unique<quic::TransportParameters>();
params->perspective = quic::Perspective::IS_CLIENT;
params->version = kFakeVersionLabel;
params->supported_versions.push_back(kFakeVersionLabel);
params->supported_versions.push_back(kFakeVersionLabel2);
params->idle_timeout_milliseconds.set_value(kFakeIdleTimeoutMilliseconds);
params->stateless_reset_token = CreateFakeStatelessResetToken();
params->max_packet_size.set_value(kFakeMaxPacketSize);
params->initial_max_data.set_value(kFakeInitialMaxData);
params->disable_migration = kFakeDisableMigration;
params->custom_parameters[kCustomParameter1] = kCustomParameter1Value;
params->custom_parameters[kCustomParameter2] = kCustomParameter2Value;
return params;
}
class QuicClientSessionCacheTest : public testing::Test {
public:
QuicClientSessionCacheTest() : ssl_ctx_(SSL_CTX_new(TLS_method())) {}
......@@ -50,42 +89,40 @@ class QuicClientSessionCacheTest : public testing::Test {
} // namespace
// Tests that simple insertion and lookup work correctly.
TEST_F(QuicClientSessionCacheTest, Basic) {
TEST_F(QuicClientSessionCacheTest, SingleSession) {
QuicClientSessionCache cache;
std::unique_ptr<quic::QuicResumptionState> state1 =
std::make_unique<quic::QuicResumptionState>();
state1->application_state->push_back('a');
state1->tls_session = NewSSLSession();
auto params = MakeFakeTransportParams();
auto session = NewSSLSession();
quic::QuicServerId id1("a.com", 443);
std::unique_ptr<quic::QuicResumptionState> state2 =
std::make_unique<quic::QuicResumptionState>();
state2->application_state->push_back('b');
state2->tls_session = NewSSLSession();
auto params2 = MakeFakeTransportParams();
auto session2 = NewSSLSession();
SSL_SESSION* unowned2 = session2.get();
quic::QuicServerId id2("b.com", 443);
EXPECT_EQ(nullptr, cache.Lookup(id1, ssl_ctx_.get()));
EXPECT_EQ(nullptr, cache.Lookup(id2, ssl_ctx_.get()));
EXPECT_EQ(0u, cache.size());
cache.Insert(id1, std::move(state1->tls_session), nullptr,
state1->application_state);
cache.Insert(id1, std::move(session), params.get(), nullptr);
EXPECT_EQ(1u, cache.size());
EXPECT_EQ('a', cache.Lookup(id1, ssl_ctx_.get())->application_state->front());
EXPECT_EQ(*params, *(cache.Lookup(id1, ssl_ctx_.get())->transport_params));
EXPECT_EQ(nullptr, cache.Lookup(id2, ssl_ctx_.get()));
// No session is available for id1, even though the entry exists.
EXPECT_EQ(1u, cache.size());
EXPECT_EQ(nullptr, cache.Lookup(id1, ssl_ctx_.get()));
// Lookup() will trigger a deletion of invalid entry.
EXPECT_EQ(0u, cache.size());
std::unique_ptr<quic::QuicResumptionState> state3 =
std::make_unique<quic::QuicResumptionState>();
state3->application_state->push_back('c');
state3->tls_session = NewSSLSession();
auto session3 = NewSSLSession();
SSL_SESSION* unowned3 = session3.get();
quic::QuicServerId id3("c.com", 443);
cache.Insert(id3, std::move(state3->tls_session), nullptr,
state3->application_state);
cache.Insert(id2, std::move(state2->tls_session), nullptr,
state2->application_state);
cache.Insert(id3, std::move(session3), params.get(), nullptr);
cache.Insert(id2, std::move(session2), params2.get(), nullptr);
EXPECT_EQ(2u, cache.size());
EXPECT_EQ('b', cache.Lookup(id2, ssl_ctx_.get())->application_state->front());
EXPECT_EQ('c', cache.Lookup(id3, ssl_ctx_.get())->application_state->front());
EXPECT_EQ(unowned2, cache.Lookup(id2, ssl_ctx_.get())->tls_session.get());
EXPECT_EQ(unowned3, cache.Lookup(id3, ssl_ctx_.get())->tls_session.get());
// Verify that the cache is cleared after Lookups.
EXPECT_EQ(nullptr, cache.Lookup(id1, ssl_ctx_.get()));
......@@ -94,38 +131,117 @@ TEST_F(QuicClientSessionCacheTest, Basic) {
EXPECT_EQ(0u, cache.size());
}
TEST_F(QuicClientSessionCacheTest, MultipleSessions) {
QuicClientSessionCache cache;
auto params = MakeFakeTransportParams();
auto session = NewSSLSession();
quic::QuicServerId id1("a.com", 443);
auto session2 = NewSSLSession();
SSL_SESSION* unowned2 = session2.get();
auto session3 = NewSSLSession();
SSL_SESSION* unowned3 = session3.get();
cache.Insert(id1, std::move(session), params.get(), nullptr);
cache.Insert(id1, std::move(session2), params.get(), nullptr);
cache.Insert(id1, std::move(session3), params.get(), nullptr);
// The latest session is popped first.
EXPECT_EQ(unowned3, cache.Lookup(id1, ssl_ctx_.get())->tls_session.get());
EXPECT_EQ(unowned2, cache.Lookup(id1, ssl_ctx_.get())->tls_session.get());
// Only two sessions are cached.
EXPECT_EQ(nullptr, cache.Lookup(id1, ssl_ctx_.get()));
}
// Test that when a different TransportParameter is inserted for
// the same server id, the existing entry is removed.
TEST_F(QuicClientSessionCacheTest, DifferentTransportParams) {
QuicClientSessionCache cache;
auto params = MakeFakeTransportParams();
auto session = NewSSLSession();
quic::QuicServerId id1("a.com", 443);
auto session2 = NewSSLSession();
auto session3 = NewSSLSession();
SSL_SESSION* unowned3 = session3.get();
cache.Insert(id1, std::move(session), params.get(), nullptr);
cache.Insert(id1, std::move(session2), params.get(), nullptr);
// tweak the transport parameters a little bit.
params->perspective = quic::Perspective::IS_SERVER;
cache.Insert(id1, std::move(session3), params.get(), nullptr);
auto resumption_state = cache.Lookup(id1, ssl_ctx_.get());
EXPECT_EQ(unowned3, resumption_state->tls_session.get());
EXPECT_EQ(*params.get(), *resumption_state->transport_params);
EXPECT_EQ(nullptr, cache.Lookup(id1, ssl_ctx_.get()));
}
TEST_F(QuicClientSessionCacheTest, DifferentApplicationState) {
QuicClientSessionCache cache;
auto params = MakeFakeTransportParams();
auto session = NewSSLSession();
quic::QuicServerId id1("a.com", 443);
auto session2 = NewSSLSession();
auto session3 = NewSSLSession();
SSL_SESSION* unowned3 = session3.get();
quic::ApplicationState state;
state.push_back('a');
cache.Insert(id1, std::move(session), params.get(), &state);
cache.Insert(id1, std::move(session2), params.get(), &state);
cache.Insert(id1, std::move(session3), params.get(), nullptr);
auto resumption_state = cache.Lookup(id1, ssl_ctx_.get());
EXPECT_EQ(unowned3, resumption_state->tls_session.get());
EXPECT_EQ(nullptr, resumption_state->application_state);
EXPECT_EQ(nullptr, cache.Lookup(id1, ssl_ctx_.get()));
}
TEST_F(QuicClientSessionCacheTest, BothStatesDifferent) {
QuicClientSessionCache cache;
auto params = MakeFakeTransportParams();
auto session = NewSSLSession();
quic::QuicServerId id1("a.com", 443);
auto session2 = NewSSLSession();
auto session3 = NewSSLSession();
SSL_SESSION* unowned3 = session3.get();
quic::ApplicationState state;
state.push_back('a');
cache.Insert(id1, std::move(session), params.get(), &state);
cache.Insert(id1, std::move(session2), params.get(), &state);
params->perspective = quic::Perspective::IS_SERVER;
cache.Insert(id1, std::move(session3), params.get(), nullptr);
auto resumption_state = cache.Lookup(id1, ssl_ctx_.get());
EXPECT_EQ(unowned3, resumption_state->tls_session.get());
EXPECT_EQ(*params.get(), *resumption_state->transport_params);
EXPECT_EQ(nullptr, resumption_state->application_state);
EXPECT_EQ(nullptr, cache.Lookup(id1, ssl_ctx_.get()));
}
// When the size limit is exceeded, the oldest entry should be erased.
TEST_F(QuicClientSessionCacheTest, SizeLimit) {
QuicClientSessionCache cache(2);
std::unique_ptr<quic::QuicResumptionState> state1 =
std::make_unique<quic::QuicResumptionState>();
state1->application_state->push_back('a');
state1->tls_session = NewSSLSession();
auto params = MakeFakeTransportParams();
auto session = NewSSLSession();
quic::QuicServerId id1("a.com", 443);
std::unique_ptr<quic::QuicResumptionState> state2 =
std::make_unique<quic::QuicResumptionState>();
state2->application_state->push_back('b');
state2->tls_session = NewSSLSession();
auto session2 = NewSSLSession();
SSL_SESSION* unowned2 = session2.get();
quic::QuicServerId id2("b.com", 443);
std::unique_ptr<quic::QuicResumptionState> state3 =
std::make_unique<quic::QuicResumptionState>();
state3->application_state->push_back('c');
state3->tls_session = NewSSLSession();
auto session3 = NewSSLSession();
SSL_SESSION* unowned3 = session3.get();
quic::QuicServerId id3("c.com", 443);
cache.Insert(id1, std::move(state1->tls_session), nullptr,
state1->application_state);
cache.Insert(id2, std::move(state2->tls_session), nullptr,
state2->application_state);
cache.Insert(id3, std::move(state3->tls_session), nullptr,
state3->application_state);
cache.Insert(id1, std::move(session), params.get(), nullptr);
cache.Insert(id2, std::move(session2), params.get(), nullptr);
cache.Insert(id3, std::move(session3), params.get(), nullptr);
EXPECT_EQ(2u, cache.size());
EXPECT_EQ('b', cache.Lookup(id2, ssl_ctx_.get())->application_state->front());
EXPECT_EQ('c', cache.Lookup(id3, ssl_ctx_.get())->application_state->front());
EXPECT_EQ(unowned2, cache.Lookup(id2, ssl_ctx_.get())->tls_session.get());
EXPECT_EQ(unowned3, cache.Lookup(id3, ssl_ctx_.get())->tls_session.get());
EXPECT_EQ(nullptr, cache.Lookup(id1, ssl_ctx_.get()));
}
......@@ -137,22 +253,16 @@ TEST_F(QuicClientSessionCacheTest, Expiration) {
std::unique_ptr<base::SimpleTestClock> clock = MakeTestClock();
cache.SetClockForTesting(clock.get());
std::unique_ptr<quic::QuicResumptionState> state1 =
std::make_unique<quic::QuicResumptionState>();
state1->tls_session = MakeTestSession(clock->Now(), kTimeout);
auto params = MakeFakeTransportParams();
auto session = MakeTestSession(clock->Now(), kTimeout);
quic::QuicServerId id1("a.com", 443);
std::unique_ptr<quic::QuicResumptionState> state2 =
std::make_unique<quic::QuicResumptionState>();
state2->tls_session = MakeTestSession(clock->Now(), 3 * kTimeout);
;
state2->application_state->push_back('b');
auto session2 = MakeTestSession(clock->Now(), 3 * kTimeout);
SSL_SESSION* unowned2 = session2.get();
quic::QuicServerId id2("b.com", 443);
cache.Insert(id1, std::move(state1->tls_session), nullptr,
state1->application_state);
cache.Insert(id2, std::move(state2->tls_session), nullptr,
state2->application_state);
cache.Insert(id1, std::move(session), params.get(), nullptr);
cache.Insert(id2, std::move(session2), params.get(), nullptr);
EXPECT_EQ(2u, cache.size());
// Expire the session.
......@@ -162,8 +272,8 @@ TEST_F(QuicClientSessionCacheTest, Expiration) {
EXPECT_EQ(nullptr, cache.Lookup(id1, ssl_ctx_.get()));
EXPECT_EQ(1u, cache.size());
EXPECT_EQ('b', cache.Lookup(id2, ssl_ctx_.get())->application_state->front());
EXPECT_EQ(0u, cache.size());
EXPECT_EQ(unowned2, cache.Lookup(id2, ssl_ctx_.get())->tls_session.get());
EXPECT_EQ(1u, cache.size());
}
TEST_F(QuicClientSessionCacheTest, FlushOnMemoryNotifications) {
......@@ -173,22 +283,15 @@ TEST_F(QuicClientSessionCacheTest, FlushOnMemoryNotifications) {
std::unique_ptr<base::SimpleTestClock> clock = MakeTestClock();
cache.SetClockForTesting(clock.get());
std::unique_ptr<quic::QuicResumptionState> state1 =
std::make_unique<quic::QuicResumptionState>();
state1->tls_session = MakeTestSession(clock->Now(), kTimeout);
auto params = MakeFakeTransportParams();
auto session = MakeTestSession(clock->Now(), kTimeout);
quic::QuicServerId id1("a.com", 443);
std::unique_ptr<quic::QuicResumptionState> state2 =
std::make_unique<quic::QuicResumptionState>();
state2->tls_session = MakeTestSession(clock->Now(), 3 * kTimeout);
state2->application_state->push_back('b');
auto session2 = MakeTestSession(clock->Now(), 3 * kTimeout);
quic::QuicServerId id2("b.com", 443);
cache.Insert(id1, std::move(state1->tls_session), nullptr,
state1->application_state);
cache.Insert(id2, std::move(state2->tls_session), nullptr,
state2->application_state);
cache.Insert(id1, std::move(session), params.get(), nullptr);
cache.Insert(id2, std::move(session2), params.get(), nullptr);
EXPECT_EQ(2u, cache.size());
// Expire the session.
......@@ -201,7 +304,7 @@ TEST_F(QuicClientSessionCacheTest, FlushOnMemoryNotifications) {
base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE);
base::RunLoop().RunUntilIdle();
// session1 is expired and should be flushed.
// session is expired and should be flushed.
EXPECT_EQ(nullptr, cache.Lookup(id1, ssl_ctx_.get()));
EXPECT_EQ(1u, cache.size());
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment