Skip to content

cache filter: mock cache in CacheFilterTest#20593

Closed
mkbehr wants to merge 7 commits intoenvoyproxy:mainfrom
mkbehr:cache-filter-test-mock-cache
Closed

cache filter: mock cache in CacheFilterTest#20593
mkbehr wants to merge 7 commits intoenvoyproxy:mainfrom
mkbehr:cache-filter-test-mock-cache

Conversation

@mkbehr
Copy link
Copy Markdown
Contributor

@mkbehr mkbehr commented Mar 30, 2022

Commit Message: Refactor CacheFilterTest to use a mock cache.
Additional Description: Add a mock cache and associated objects, and restructure the CacheFilter unit test to use them instead of SimpleHttpCache. This focuses the test on specifically exercising the CacheFilter's interface, and lets tests skip the extra requests they were sending to set up the cache for a lookup or verify the results of an insert. It'll also let future tests exercise more fine-grained control over the cache's behavior, e.g. to test flow control.
Risk Level: Low
Testing: bazel test //test/extensions/filters/http/cache/...
Docs Changes: N/A
Release Notes: N/A
Platform Specific Features: N/A

Signed-off-by: Michael Behr <mkbehr@google.com>
@repokitteh-read-only
Copy link
Copy Markdown

As a reminder, PRs marked as draft will not be automatically assigned reviewers,
or be handled by maintainer-oncall triage.

Please mark your PR as ready when you want it to be reviewed!

🐱

Caused by: #20593 was opened by mkbehr.

see: more, trace.

mkbehr added 2 commits March 30, 2022 13:14
Signed-off-by: Michael Behr <mkbehr@google.com>
Signed-off-by: Michael Behr <mkbehr@google.com>
@mkbehr mkbehr marked this pull request as ready for review March 30, 2022 20:01
@mkbehr mkbehr requested a review from jmarantz as a code owner March 30, 2022 20:01
@mkbehr
Copy link
Copy Markdown
Contributor Author

mkbehr commented Mar 30, 2022

Presubmit failures look to be an upstream Bazel bug; I filed issue #20599 for them.

@adisuissa
Copy link
Copy Markdown
Contributor

cc @toddmgreer @jmarantz @penguingao @mpwarres @capoferro as codeowners

}

SimpleHttpCache simple_cache_;
std::shared_ptr<StrictMock<MockHttpCache>> cache_;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please document why this test needs the additional complexity of using a mock and having to respecify its behavior.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a test fixture comment. This is most important for flow control tests in a future change - I can imagine splitting the test fixture to let the tests that don't need as fine-grained control use a fake, but for now this is at least an improvement over specifying the fake's behavior via extra copies of CacheFilter.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would make sense to me to add such a test in this PR to demonstrate the value, if that seems feasible.

I'm always skeptical with complex mocks whether they are mocking the right behavior, and wonder why (without looking too deeply) you think that's an improvement over using the CacheFilter itself.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's hard to add those tests before the CacheFilter implements its flow control logic: right now we can't test e.g. downstream backpressure while serving from cache because the whole response gets buffered, so there aren't any signals from the connection manager that we can interleave with cache body fragments.

I take your point about it being hard to trust that complex mocks are doing the right thing. My thinking is that when it comes to setting up the environment and reading results, it's even worse to trust that the code under test is doing the right thing. So in the existing state of the test, we test CacheFilter inserts by using CacheFilter lookups to verify the state of the cache, and we test lookups by using inserts to set up the cache, but we can never verify the behavior of one of those operations independently of the other. So there's a class of bugs that affect both insert and lookup that the existing test setup can never catch.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing inserts and lookups independently is not a goal, because they have practically no independently defined behavior. The (simplified) spec is that if you insert something then look it up, you get the response you inserted (with certain modifications). We don't use CacheFilter lookups to verify the state of the cache--we use CacheFilter lookups to verify that the observable behavior of a particular sequence of operations is correct. If some "bug" doesn't change the observable behavior, then it isn't really a bug.

There are times when it is helpful to write tests that are less faithful to deployed reality, and testing a slow HttpCache is probably such a time. However, this PR changes all of the tests in this file to using mocks, including those that are better off using the real thing. Perhaps CacheFilterTest should have RealCacheFilterTest and MockCacheFilterTest subclasses, or should use a simple mock that defaults to letting SimpleHttpCache handle everything, with ON_CALLs only appearing in specific tests?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the past I've done some amount of insert/lookup testing against stats held in the cache object. That allows some modest testing of insert/lookup in isolation. E.g. you could verify that inserts fill the cache with data without doing a lookup. You could verify that lookups fail due to expiry. This doesn't require mocks.

@mbehr maybe a spec of what your testing plan is would make it easier to understand the future value of the mocks.


// Headers of the cached response.
Http::ResponseHeaderMapPtr headers_;
Http::ResponseHeaderMapPtr headers_ = nullptr;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the preferred way in Envoy is to rely on default construction. (Please correct me if I'm mistaken.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That way doesn't compile for me; breaks on -Wmissing-field-initializers.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did this get resolved somehow? Why is it compiling without initializers without your PR? What changed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing ever constructed a LookupResult before this, just declared it with everything uninitialized and then set its fields. Is there a problem with doing it this way? We could explicitly define a constructor instead.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why it doesn't work to just revert all the initializers in this class and let them default-construct. You mentioned that doesn't compile for you which seems strange because it works in main today.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What doesn't compile is the new constructor calls to LookupResult that I added in CacheFilterTest. They can't default-construct without these changes because -Wmissing-field-initializers prevents that. They weren't default-constructing before because the existing usage initialized them to indeterminate values.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I bet if you add std::move you have to have a lot more detailed constructors. What's the implication if you drop that?

Other than that, after a quick scan, I just noticed a bunch of new code where you pass LookupResult by value and it might work better to pass by const ref.

mkbehr added 2 commits April 5, 2022 16:44
Signed-off-by: Michael Behr <mkbehr@google.com>
…-filter-test-mock-cache

Signed-off-by: Michael Behr <mkbehr@google.com>
@wrowe
Copy link
Copy Markdown
Contributor

wrowe commented Apr 5, 2022

Definitely breakage in this patch, not main;

@wrowe
Copy link
Copy Markdown
Contributor

wrowe commented Apr 5, 2022

test/extensions/filters/http/cache/cache_filter_test.cc: In member function ‘std::unique_ptr<Envoy::Extensions::HttpFilters::Cache::MockInsertContext, std::default_delete<Envoy::Extensions::HttpFilters::Cache::MockInsertContext> > Envoy::Extensions::HttpFilters::Cache::{anonymous}::CacheFilterTest::headerOnlyInsertContext() const’:
test/extensions/filters/http/cache/cache_filter_test.cc:188:21: error: redundant move in return statement [-Werror=redundant-move]
  188 |     return std::move(insert_context);
      |            ~~~~~~~~~^~~~~~~~~~~~~~~~
test/extensions/filters/http/cache/cache_filter_test.cc:188:21: note: remove ‘std::move’ call
test/extensions/filters/http/cache/cache_filter_test.cc: In member function ‘std::unique_ptr<Envoy::Extensions::HttpFilters::Cache::MockInsertContext, std::default_delete<Envoy::Extensions::HttpFilters::Cache::MockInsertContext> > Envoy::Extensions::HttpFilters::Cache::{anonymous}::CacheFilterTest::bodyInsertContext(const std::vector<absl::string_view>&) const’:
test/extensions/filters/http/cache/cache_filter_test.cc:214:21: error: redundant move in return statement [-Werror=redundant-move]
  214 |     return std::move(insert_context);
      |            ~~~~~~~~~^~~~~~~~~~~~~~~~
test/extensions/filters/http/cache/cache_filter_test.cc:214:21: note: remove ‘std::move’ call
test/extensions/filters/http/cache/cache_filter_test.cc: In member function ‘std::unique_ptr<Envoy::Extensions::HttpFilters::Cache::MockInsertContext, std::default_delete<Envoy::Extensions::HttpFilters::Cache::MockInsertContext> > Envoy::Extensions::HttpFilters::Cache::{anonymous}::CacheFilterTest::headerOnlyInsertContext() const’:
test/extensions/filters/http/cache/cache_filter_test.cc:188:21: error: redundant move in return statement [-Werror=redundant-move]
  188 |     return std::move(insert_context);
      |            ~~~~~~~~~^~~~~~~~~~~~~~~~
test/extensions/filters/http/cache/cache_filter_test.cc:188:21: note: remove ‘std::move’ call
test/extensions/filters/http/cache/cache_filter_test.cc: In member function ‘std::unique_ptr<Envoy::Extensions::HttpFilters::Cache::MockInsertContext, std::default_delete<Envoy::Extensions::HttpFilters::Cache::MockInsertContext> > Envoy::Extensions::HttpFilters::Cache::{anonymous}::CacheFilterTest::bodyInsertContext(const std::vector<absl::string_view>&) const’:
test/extensions/filters/http/cache/cache_filter_test.cc:214:21: error: redundant move in return statement [-Werror=redundant-move]
  214 |     return std::move(insert_context);
      |            ~~~~~~~~~^~~~~~~~~~~~~~~~
test/extensions/filters/http/cache/cache_filter_test.cc:214:21: note: remove ‘std::move’ call

@wrowe
Copy link
Copy Markdown
Contributor

wrowe commented Apr 5, 2022

/wait

Signed-off-by: Michael Behr <mkbehr@google.com>
@mkbehr
Copy link
Copy Markdown
Contributor Author

mkbehr commented Apr 6, 2022

Thanks for catching that - looks like #20599 was masking that breakage. Should be fixed now.

@jmarantz
Copy link
Copy Markdown
Contributor

Can we have @toddmgreer or @capoferro take a look first and then I'l do a final pass?

capoferro
capoferro previously approved these changes Apr 13, 2022
Copy link
Copy Markdown
Contributor

@capoferro capoferro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thanks!

Copy link
Copy Markdown
Contributor

@jmarantz jmarantz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm mostly - the largest problem is I haven't been sold on the end-goal of these mocks being better than the current state, but I'm OK with it.


// Headers of the cached response.
Http::ResponseHeaderMapPtr headers_;
Http::ResponseHeaderMapPtr headers_ = nullptr;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did this get resolved somehow? Why is it compiling without initializers without your PR? What changed?

filter->setDecoderFilterCallbacks(decoder_callbacks_);
filter->setEncoderFilterCallbacks(encoder_callbacks_);
return filter;
constexpr absl::string_view kBody = "abc";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

envoy style convention for constants would use Body here but I'd suggest a slightly longer name for a file-scoped constant. Maybe CacheResponseBody?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

void SetUp() override {
ON_CALL(decoder_callbacks_, dispatcher()).WillByDefault(::testing::ReturnRef(*dispatcher_));
cache_ = std::make_shared<StrictMock<MockHttpCache>>();
filter_ = makeFilter();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was pre-existing, but why not just put all this in the CacheFilterTest ctor? Ditto for TearDown vs dtor.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

void expectCacheHitNoBody() {
// The filter should ask the cache for a lookup context, which will return a
// headers-only hit.
expectLookup({/*cache_entry_status_=*/CacheEntryStatus::Ok,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The /*cache_entry_status_=*/ annotation seems unnecessary given the well-qualified enum, here & below.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

std::make_unique<Envoy::Http::TestResponseHeaderMapImpl>(response_headers);
response_headers_from_cache->setCopy(Http::CustomHeaders::get().Age, age);
cb({/*cache_entry_status_=*/cache_entry_status,
/*headers_=*/std::move(response_headers_from_cache),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all these comments seem unnecessary in this case and just clutter the code.

We don't generally have these in Envoy style though there were cases above where you were explaining what the nullptr was for, and that case makes sense to me.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sg, removed all of these that could be inferred from context.

}

SimpleHttpCache simple_cache_;
std::shared_ptr<StrictMock<MockHttpCache>> cache_;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would make sense to me to add such a test in this PR to demonstrate the value, if that seems feasible.

I'm always skeptical with complex mocks whether they are mocking the right behavior, and wonder why (without looking too deeply) you think that's an improvement over using the CacheFilter itself.

filter->onDestroy();
}
// Add Etag & Last-Modified headers to the response for validation
response_headers_.setReferenceKey(Envoy::Http::CustomHeaders::get().Etag, etag);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional: this all might be easier to read (and slightly faster) if you put Envoy::Http::CustomHeaders::get() into a temp reference.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@jmarantz
Copy link
Copy Markdown
Contributor

/wait

Signed-off-by: Michael Behr <mkbehr@google.com>
encodeHeaders_(testing::AllOf(IsSupersetOfHeaders(response_headers_),
HeaderHasValueRef(Http::CustomHeaders::get().Age, age)),
true));
void expectLookup(LookupResult lookup_result,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const ref

std::function<void(MockLookupContext&)> context_callback = nullptr) {
// Wrap the move-only LookupResult in a copyable shared_ptr to fit with
// EXPECT_CALL(...).WillOnce()'s interface.
auto result_wrapper = std::make_shared<LookupResult>(std::move(lookup_result));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will everything be cleaner if you just drop the std::move here?

return lookup_context;
}

std::unique_ptr<MockLookupContext> makeLookupContext(LookupResult result) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const ref

// Returns a std::function that can be used as an action with
// MockLookupContext::getHeaders to find response_headers_, with other fields
// of the LookupResult as specified.
std::function<void(LookupHeadersCallback)> getHeaders(LookupResult lookup_result) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const ref


// Headers of the cached response.
Http::ResponseHeaderMapPtr headers_;
Http::ResponseHeaderMapPtr headers_ = nullptr;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I bet if you add std::move you have to have a lot more detailed constructors. What's the implication if you drop that?

Other than that, after a quick scan, I just noticed a bunch of new code where you pass LookupResult by value and it might work better to pass by const ref.

@jmarantz
Copy link
Copy Markdown
Contributor

/wait

@github-actions
Copy link
Copy Markdown

This pull request has been automatically marked as stale because it has not had activity in the last 30 days. It will be closed in 7 days if no further activity occurs. Please feel free to give a status update now, ping for review, or re-open when it's ready. Thank you for your contributions!

@github-actions github-actions bot added the stale stalebot believes this issue/PR has not been touched recently label May 19, 2022
@github-actions
Copy link
Copy Markdown

This pull request has been automatically closed because it has not had activity in the last 37 days. Please feel free to give a status update now, ping for review, or re-open when it's ready. Thank you for your contributions!

@github-actions github-actions bot closed this May 26, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale stalebot believes this issue/PR has not been touched recently waiting

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants