diff --git a/CMakeLists.txt b/CMakeLists.txt index 8b901df722a..1d018425ab2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -962,6 +962,7 @@ if(gRPC_BUILD_TESTS) endif() add_dependencies(buildtests_cxx file_watcher_certificate_provider_factory_test) add_dependencies(buildtests_cxx filter_end2end_test) + add_dependencies(buildtests_cxx filter_test_test) add_dependencies(buildtests_cxx flaky_network_test) add_dependencies(buildtests_cxx flow_control_test) add_dependencies(buildtests_cxx for_each_test) @@ -8437,7 +8438,13 @@ endif() if(gRPC_BUILD_TESTS) add_executable(client_auth_filter_test + ${_gRPC_PROTO_GENS_DIR}/test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.pb.cc + ${_gRPC_PROTO_GENS_DIR}/test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.grpc.pb.cc + ${_gRPC_PROTO_GENS_DIR}/test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.pb.h + ${_gRPC_PROTO_GENS_DIR}/test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.grpc.pb.h + test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.cc test/core/filters/client_auth_filter_test.cc + test/core/filters/filter_test.cc third_party/googletest/googletest/src/gtest-all.cc third_party/googletest/googlemock/src/gmock-all.cc ) @@ -8466,7 +8473,7 @@ target_link_libraries(client_auth_filter_test ${_gRPC_PROTOBUF_LIBRARIES} ${_gRPC_ZLIB_LIBRARIES} ${_gRPC_ALLTARGETS_LIBRARIES} - grpc + grpc_test_util ) @@ -8474,7 +8481,13 @@ endif() if(gRPC_BUILD_TESTS) add_executable(client_authority_filter_test + ${_gRPC_PROTO_GENS_DIR}/test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.pb.cc + ${_gRPC_PROTO_GENS_DIR}/test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.grpc.pb.cc + ${_gRPC_PROTO_GENS_DIR}/test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.pb.h + ${_gRPC_PROTO_GENS_DIR}/test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.grpc.pb.h + test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.cc test/core/filters/client_authority_filter_test.cc + test/core/filters/filter_test.cc third_party/googletest/googletest/src/gtest-all.cc third_party/googletest/googlemock/src/gmock-all.cc ) @@ -8503,7 +8516,7 @@ target_link_libraries(client_authority_filter_test ${_gRPC_PROTOBUF_LIBRARIES} ${_gRPC_ZLIB_LIBRARIES} ${_gRPC_ALLTARGETS_LIBRARIES} - grpc + grpc_test_util ) @@ -11011,6 +11024,50 @@ target_link_libraries(filter_end2end_test ) +endif() +if(gRPC_BUILD_TESTS) + +add_executable(filter_test_test + ${_gRPC_PROTO_GENS_DIR}/test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.pb.cc + ${_gRPC_PROTO_GENS_DIR}/test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.grpc.pb.cc + ${_gRPC_PROTO_GENS_DIR}/test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.pb.h + ${_gRPC_PROTO_GENS_DIR}/test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.grpc.pb.h + test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.cc + test/core/filters/filter_test.cc + test/core/filters/filter_test_test.cc + third_party/googletest/googletest/src/gtest-all.cc + third_party/googletest/googlemock/src/gmock-all.cc +) +target_compile_features(filter_test_test PUBLIC cxx_std_14) +target_include_directories(filter_test_test + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${_gRPC_ADDRESS_SORTING_INCLUDE_DIR} + ${_gRPC_RE2_INCLUDE_DIR} + ${_gRPC_SSL_INCLUDE_DIR} + ${_gRPC_UPB_GENERATED_DIR} + ${_gRPC_UPB_GRPC_GENERATED_DIR} + ${_gRPC_UPB_INCLUDE_DIR} + ${_gRPC_XXHASH_INCLUDE_DIR} + ${_gRPC_ZLIB_INCLUDE_DIR} + third_party/googletest/googletest/include + third_party/googletest/googletest + third_party/googletest/googlemock/include + third_party/googletest/googlemock + ${_gRPC_PROTO_GENS_DIR} +) + +target_link_libraries(filter_test_test + ${_gRPC_BASELIB_LIBRARIES} + ${_gRPC_PROTOBUF_LIBRARIES} + ${_gRPC_ZLIB_LIBRARIES} + ${_gRPC_ALLTARGETS_LIBRARIES} + grpc_unsecure + grpc_test_util +) + + endif() if(gRPC_BUILD_TESTS) diff --git a/build_autogenerated.yaml b/build_autogenerated.yaml index d49e2403c98..faa8c9d8eff 100644 --- a/build_autogenerated.yaml +++ b/build_autogenerated.yaml @@ -5918,22 +5918,30 @@ targets: build: test language: c++ headers: - - test/core/promise/test_context.h + - test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.h + - test/core/filters/filter_test.h src: + - test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.proto + - test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.cc - test/core/filters/client_auth_filter_test.cc + - test/core/filters/filter_test.cc deps: - - grpc + - grpc_test_util uses_polling: false - name: client_authority_filter_test gtest: true build: test language: c++ headers: - - test/core/promise/test_context.h + - test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.h + - test/core/filters/filter_test.h src: + - test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.proto + - test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.cc - test/core/filters/client_authority_filter_test.cc + - test/core/filters/filter_test.cc deps: - - grpc + - grpc_test_util uses_polling: false - name: client_callback_end2end_test gtest: true @@ -7074,6 +7082,22 @@ targets: - test/cpp/end2end/filter_end2end_test.cc deps: - grpc++_test_util +- name: filter_test_test + gtest: true + build: test + language: c++ + headers: + - test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.h + - test/core/filters/filter_test.h + src: + - test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.proto + - test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.cc + - test/core/filters/filter_test.cc + - test/core/filters/filter_test_test.cc + deps: + - grpc_unsecure + - grpc_test_util + uses_polling: false - name: flaky_network_test gtest: true build: test diff --git a/src/core/lib/transport/metadata_batch.h b/src/core/lib/transport/metadata_batch.h index 3e83ffc320f..320e9b1b5f6 100644 --- a/src/core/lib/transport/metadata_batch.h +++ b/src/core/lib/transport/metadata_batch.h @@ -1270,8 +1270,6 @@ class MetadataMap { // Parse metadata from a key/value pair, and return an object representing // that result. - // TODO(ctiller): key should probably be an absl::string_view. - // Once we don't care about interning anymore, make that change! static ParsedMetadata Parse(absl::string_view key, Slice value, uint32_t transport_size, MetadataParseErrorFn on_error) { diff --git a/test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.cc b/test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.cc index 7f7edb60479..c12cb9822db 100644 --- a/test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.cc +++ b/test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.cc @@ -175,6 +175,16 @@ void FuzzingEventEngine::Tick() { } } +void FuzzingEventEngine::TickUntilIdle() { + while (true) { + { + grpc_core::MutexLock lock(&*mu_); + if (tasks_by_id_.empty()) return; + } + Tick(); + } +} + FuzzingEventEngine::Time FuzzingEventEngine::Now() { grpc_core::MutexLock lock(&*mu_); return now_; diff --git a/test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.h b/test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.h index b12e54a109d..27b931fb0f2 100644 --- a/test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.h +++ b/test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.h @@ -65,6 +65,8 @@ class FuzzingEventEngine : public EventEngine { void FuzzingDone() ABSL_LOCKS_EXCLUDED(mu_); // Increment time once and perform any scheduled work. void Tick() ABSL_LOCKS_EXCLUDED(mu_); + // Repeatedly call Tick() until there is no more work to do. + void TickUntilIdle() ABSL_LOCKS_EXCLUDED(mu_); absl::StatusOr> CreateListener( Listener::AcceptCallback on_accept, diff --git a/test/core/filters/BUILD b/test/core/filters/BUILD index 4905d499090..bda2e163a91 100644 --- a/test/core/filters/BUILD +++ b/test/core/filters/BUILD @@ -12,39 +12,109 @@ # See the License for the specific language governing permissions and # limitations under the License. -load("//bazel:grpc_build_system.bzl", "grpc_cc_test", "grpc_package") +load("//bazel:grpc_build_system.bzl", "grpc_cc_library", "grpc_cc_test", "grpc_package") load("//test/core/util:grpc_fuzzer.bzl", "grpc_proto_fuzzer") licenses(["notice"]) grpc_package(name = "test/core/filters") +grpc_cc_library( + name = "filter_test", + srcs = ["filter_test.cc"], + hdrs = ["filter_test.h"], + external_deps = [ + "absl/memory", + "absl/strings", + "absl/strings:str_format", + "absl/types:optional", + "absl/types:variant", + "gtest", + ], + language = "c++", + tags = ["nofixdeps"], # until event engine tests are under fixbuilddeps + deps = [ + "//:gpr", + "//:grpc", + "//:ref_counted_ptr", + "//src/core:activity", + "//src/core:arena", + "//src/core:arena_promise", + "//src/core:basic_seq", + "//src/core:context", + "//src/core:memory_quota", + "//src/core:pipe", + "//src/core:poll", + "//src/core:resource_quota", + "//src/core:slice", + "//src/core:slice_buffer", + "//test/core/event_engine/fuzzing_event_engine", + ], +) + +grpc_cc_test( + name = "filter_test_test", + srcs = ["filter_test_test.cc"], + external_deps = ["gtest"], + uses_event_engine = False, + uses_polling = False, + deps = [ + "filter_test", + "//:grpc_unsecure", + "//src/core:activity", + "//src/core:arena_promise", + "//src/core:map", + "//src/core:pipe", + "//src/core:poll", + "//src/core:seq", + "//src/core:slice", + ], +) + grpc_cc_test( name = "client_auth_filter_test", srcs = ["client_auth_filter_test.cc"], - external_deps = ["gtest"], + external_deps = [ + "absl/status", + "absl/status:statusor", + "absl/strings", + "absl/types:optional", + "gtest", + ], language = "c++", uses_event_engine = False, uses_polling = False, deps = [ + "filter_test", "//:grpc", + "//:grpc_public_hdrs", "//:grpc_security_base", + "//:promise", + "//:ref_counted_ptr", + "//src/core:arena_promise", "//src/core:channel_args", - "//test/core/promise:test_context", + "//src/core:grpc_fake_credentials", + "//src/core:unique_type_name", + "//src/core:useful", ], ) grpc_cc_test( name = "client_authority_filter_test", srcs = ["client_authority_filter_test.cc"], - external_deps = ["gtest"], + external_deps = [ + "absl/status", + "absl/strings", + "absl/types:optional", + "gtest", + ], language = "c++", uses_event_engine = False, uses_polling = False, deps = [ + "filter_test", "//:grpc", "//src/core:grpc_client_authority_filter", - "//test/core/promise:test_context", ], ) @@ -52,16 +122,52 @@ grpc_proto_fuzzer( name = "filter_fuzzer", srcs = ["filter_fuzzer.cc"], corpus = "filter_fuzzer_corpus", + external_deps = [ + "absl/base:core_headers", + "absl/status", + "absl/status:statusor", + "absl/strings", + "absl/types:optional", + ], language = "C++", proto = "filter_fuzzer.proto", tags = ["no_windows"], uses_polling = False, deps = [ + "//:config", + "//:debug_location", + "//:exec_ctx", "//:gpr", "//:grpc", + "//:grpc_http_filters", + "//:grpc_public_hdrs", + "//:grpc_security_base", + "//:handshaker", + "//:iomgr_timer", + "//:ref_counted_ptr", + "//:tsi_base", + "//src/core:activity", + "//src/core:arena", + "//src/core:arena_promise", "//src/core:channel_args", + "//src/core:channel_args_preconditioning", + "//src/core:channel_fwd", + "//src/core:channel_stack_type", "//src/core:closure", - "//test/core/end2end:ssl_test_data", - "//test/core/util:grpc_test_util", + "//src/core:context", + "//src/core:env", + "//src/core:error", + "//src/core:grpc_authorization_base", + "//src/core:grpc_channel_idle_filter", + "//src/core:grpc_client_authority_filter", + "//src/core:iomgr_fwd", + "//src/core:memory_quota", + "//src/core:pipe", + "//src/core:poll", + "//src/core:resource_quota", + "//src/core:slice", + "//src/core:time", + "//src/core:transport_fwd", + "//src/core:useful", ], ) diff --git a/test/core/filters/client_auth_filter_test.cc b/test/core/filters/client_auth_filter_test.cc index e0711c453b6..7e22c6d40a1 100644 --- a/test/core/filters/client_auth_filter_test.cc +++ b/test/core/filters/client_auth_filter_test.cc @@ -12,9 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include - -#include #include #include @@ -22,35 +19,28 @@ #include "absl/status/statusor.h" #include "absl/strings/string_view.h" #include "absl/types/optional.h" +#include "gmock/gmock.h" #include "gtest/gtest.h" -#include #include #include #include #include #include "src/core/lib/channel/channel_args.h" -#include "src/core/lib/channel/context.h" #include "src/core/lib/channel/promise_based_filter.h" #include "src/core/lib/gpr/useful.h" #include "src/core/lib/gprpp/ref_counted_ptr.h" #include "src/core/lib/gprpp/unique_type_name.h" #include "src/core/lib/promise/arena_promise.h" -#include "src/core/lib/promise/poll.h" #include "src/core/lib/promise/promise.h" -#include "src/core/lib/resource_quota/arena.h" -#include "src/core/lib/resource_quota/memory_quota.h" -#include "src/core/lib/resource_quota/resource_quota.h" #include "src/core/lib/security/context/security_context.h" #include "src/core/lib/security/credentials/credentials.h" #include "src/core/lib/security/credentials/fake/fake_credentials.h" #include "src/core/lib/security/security_connector/security_connector.h" #include "src/core/lib/security/transport/auth_filters.h" -#include "src/core/lib/slice/slice.h" -#include "src/core/lib/transport/metadata_batch.h" #include "src/core/lib/transport/transport.h" -#include "test/core/promise/test_context.h" +#include "test/core/filters/filter_test.h" // TODO(roth): Need to add a lot more tests here. I created this file // as part of adding a feature, and I added tests only for the feature I @@ -60,7 +50,7 @@ namespace grpc_core { namespace { -class ClientAuthFilterTest : public ::testing::Test { +class ClientAuthFilterTest : public FilterTest { protected: class FailCallCreds : public grpc_call_credentials { public: @@ -90,23 +80,10 @@ class ClientAuthFilterTest : public ::testing::Test { }; ClientAuthFilterTest() - : memory_allocator_( - ResourceQuota::Default()->memory_quota()->CreateMemoryAllocator( - "test")), - arena_(MakeScopedArena(1024, &memory_allocator_)), - initial_metadata_batch_(arena_.get()), - trailing_metadata_batch_(arena_.get()), - target_(Slice::FromStaticString("localhost:1234")), - channel_creds_(grpc_fake_transport_security_credentials_create()) { - initial_metadata_batch_.Set(HttpAuthorityMetadata(), target_.Ref()); - } + : channel_creds_(grpc_fake_transport_security_credentials_create()) {} - ~ClientAuthFilterTest() override { - for (size_t i = 0; i < GRPC_CONTEXT_COUNT; ++i) { - if (call_context_[i].destroy != nullptr) { - call_context_[i].destroy(call_context_[i].value); - } - } + Channel MakeChannelWithCallCredsResult(absl::Status status) { + return MakeChannel(MakeChannelArgs(std::move(status))).value(); } ChannelArgs MakeChannelArgs(absl::Status status_for_call_creds) { @@ -115,7 +92,7 @@ class ClientAuthFilterTest : public ::testing::Test { status_for_call_creds.ok() ? nullptr : MakeRefCounted(std::move(status_for_call_creds)), - std::string(target_.as_string_view()).c_str(), &args); + std::string(target()).c_str(), &args); auto auth_context = MakeRefCounted(nullptr); absl::string_view security_level = "TSI_SECURITY_NONE"; auth_context->add_property(GRPC_TRANSPORT_SECURITY_LEVEL_PROPERTY_NAME, @@ -124,13 +101,9 @@ class ClientAuthFilterTest : public ::testing::Test { .SetObject(std::move(auth_context)); } - MemoryAllocator memory_allocator_; - ScopedArenaPtr arena_; - grpc_metadata_batch initial_metadata_batch_; - grpc_metadata_batch trailing_metadata_batch_; - Slice target_; + absl::string_view target() { return "localhost:1234"; } + RefCountedPtr channel_creds_; - grpc_call_context_element call_context_[GRPC_CONTEXT_COUNT]; }; TEST_F(ClientAuthFilterTest, CreateFailsWithoutRequiredChannelArgs) { @@ -139,72 +112,26 @@ TEST_F(ClientAuthFilterTest, CreateFailsWithoutRequiredChannelArgs) { } TEST_F(ClientAuthFilterTest, CreateSucceeds) { - auto filter = ClientAuthFilter::Create(MakeChannelArgs(absl::OkStatus()), - ChannelFilter::Args()); + auto filter = MakeChannel(MakeChannelArgs(absl::OkStatus())); EXPECT_TRUE(filter.ok()) << filter.status(); } TEST_F(ClientAuthFilterTest, CallCredsFails) { - auto filter = ClientAuthFilter::Create( - MakeChannelArgs(absl::UnauthenticatedError("access denied")), - ChannelFilter::Args()); - // TODO(ctiller): use Activity here, once it's ready. - TestContext context(arena_.get()); - TestContext promise_call_context(call_context_); - auto promise = filter->MakeCallPromise( - CallArgs{ClientMetadataHandle(&initial_metadata_batch_, - Arena::PooledDeleter(nullptr)), - ClientInitialMetadataOutstandingToken::Empty(), nullptr, nullptr, - nullptr}, - [&](CallArgs /*call_args*/) { - return ArenaPromise( - [&]() -> Poll { - return ServerMetadataHandle(&trailing_metadata_batch_, - Arena::PooledDeleter(nullptr)); - }); - }); - auto result = promise(); - ServerMetadataHandle* server_metadata = result.value_if_ready(); - ASSERT_TRUE(server_metadata != nullptr); - auto status_md = (*server_metadata)->get(GrpcStatusMetadata()); - ASSERT_TRUE(status_md.has_value()); - EXPECT_EQ(*status_md, GRPC_STATUS_UNAUTHENTICATED); - const Slice* message_md = - (*server_metadata)->get_pointer(GrpcMessageMetadata()); - ASSERT_TRUE(message_md != nullptr); - EXPECT_EQ(message_md->as_string_view(), "access denied"); + Call call(MakeChannelWithCallCredsResult( + absl::UnauthenticatedError("access denied"))); + call.Start(call.NewClientMetadata({{":authority", target()}})); + EXPECT_EVENT(Finished( + &call, HasMetadataResult(absl::UnauthenticatedError("access denied")))); + Step(); } TEST_F(ClientAuthFilterTest, RewritesInvalidStatusFromCallCreds) { - auto filter = ClientAuthFilter::Create( - MakeChannelArgs(absl::AbortedError("nope")), ChannelFilter::Args()); - // TODO(ctiller): use Activity here, once it's ready. - TestContext context(arena_.get()); - TestContext promise_call_context(call_context_); - auto promise = filter->MakeCallPromise( - CallArgs{ClientMetadataHandle(&initial_metadata_batch_, - Arena::PooledDeleter(nullptr)), - ClientInitialMetadataOutstandingToken::Empty(), nullptr, nullptr, - nullptr}, - [&](CallArgs /*call_args*/) { - return ArenaPromise( - [&]() -> Poll { - return ServerMetadataHandle(&trailing_metadata_batch_, - Arena::PooledDeleter(nullptr)); - }); - }); - auto result = promise(); - ServerMetadataHandle* server_metadata = result.value_if_ready(); - ASSERT_TRUE(server_metadata != nullptr); - auto status_md = (*server_metadata)->get(GrpcStatusMetadata()); - ASSERT_TRUE(status_md.has_value()); - EXPECT_EQ(*status_md, GRPC_STATUS_INTERNAL); - const Slice* message_md = - (*server_metadata)->get_pointer(GrpcMessageMetadata()); - ASSERT_TRUE(message_md != nullptr); - EXPECT_EQ(message_md->as_string_view(), - "Illegal status code from call credentials; original status: " - "ABORTED: nope"); + Call call(MakeChannelWithCallCredsResult(absl::AbortedError("nope"))); + call.Start(call.NewClientMetadata({{":authority", target()}})); + EXPECT_EVENT(Finished(&call, HasMetadataResult(absl::InternalError( + "Illegal status code from call credentials; " + "original status: ABORTED: nope")))); + Step(); } } // namespace diff --git a/test/core/filters/client_authority_filter_test.cc b/test/core/filters/client_authority_filter_test.cc index df2656e637f..8998b75cf7a 100644 --- a/test/core/filters/client_authority_filter_test.cc +++ b/test/core/filters/client_authority_filter_test.cc @@ -14,116 +14,56 @@ #include "src/core/ext/filters/http/client_authority_filter.h" -#include - #include "absl/status/status.h" #include "absl/strings/string_view.h" +#include "absl/types/optional.h" +#include "gmock/gmock.h" #include "gtest/gtest.h" -#include #include -#include "src/core/lib/gprpp/ref_counted_ptr.h" -#include "src/core/lib/promise/poll.h" -#include "src/core/lib/resource_quota/arena.h" -#include "src/core/lib/resource_quota/memory_quota.h" -#include "src/core/lib/resource_quota/resource_quota.h" -#include "src/core/lib/transport/metadata_batch.h" -#include "test/core/promise/test_context.h" +#include "test/core/filters/filter_test.h" + +using ::testing::StrictMock; namespace grpc_core { namespace { +using ClientAuthorityFilterTest = FilterTest; + ChannelArgs TestChannelArgs(absl::string_view default_authority) { return ChannelArgs().Set(GRPC_ARG_DEFAULT_AUTHORITY, default_authority); } -TEST(ClientAuthorityFilterTest, DefaultFails) { - EXPECT_FALSE( - ClientAuthorityFilter::Create(ChannelArgs(), ChannelFilter::Args()).ok()); +TEST_F(ClientAuthorityFilterTest, DefaultFails) { + EXPECT_FALSE(MakeChannel(ChannelArgs()).ok()); } -TEST(ClientAuthorityFilterTest, WithArgSucceeds) { - EXPECT_EQ(ClientAuthorityFilter::Create(TestChannelArgs("foo.test.google.au"), - ChannelFilter::Args()) - .status(), +TEST_F(ClientAuthorityFilterTest, WithArgSucceeds) { + EXPECT_EQ(MakeChannel(TestChannelArgs("foo.test.google.au")).status(), absl::OkStatus()); } -TEST(ClientAuthorityFilterTest, NonStringArgFails) { - EXPECT_FALSE(ClientAuthorityFilter::Create( - ChannelArgs().Set(GRPC_ARG_DEFAULT_AUTHORITY, 123), - ChannelFilter::Args()) - .ok()); +TEST_F(ClientAuthorityFilterTest, NonStringArgFails) { + EXPECT_FALSE( + MakeChannel(ChannelArgs().Set(GRPC_ARG_DEFAULT_AUTHORITY, 123)).ok()); } -TEST(ClientAuthorityFilterTest, PromiseCompletesImmediatelyAndSetsAuthority) { - auto filter = *ClientAuthorityFilter::Create( - TestChannelArgs("foo.test.google.au"), ChannelFilter::Args()); - MemoryAllocator memory_allocator = MemoryAllocator( - ResourceQuota::Default()->memory_quota()->CreateMemoryAllocator("test")); - auto arena = MakeScopedArena(1024, &memory_allocator); - grpc_metadata_batch initial_metadata_batch(arena.get()); - grpc_metadata_batch trailing_metadata_batch(arena.get()); - bool seen = false; - // TODO(ctiller): use Activity here, once it's ready. - TestContext context(arena.get()); - auto promise = filter.MakeCallPromise( - CallArgs{ClientMetadataHandle(&initial_metadata_batch, - Arena::PooledDeleter(nullptr)), - ClientInitialMetadataOutstandingToken::Empty(), nullptr, nullptr, - nullptr}, - [&](CallArgs call_args) { - EXPECT_EQ(call_args.client_initial_metadata - ->get_pointer(HttpAuthorityMetadata()) - ->as_string_view(), - "foo.test.google.au"); - seen = true; - return ArenaPromise( - [&]() -> Poll { - return ServerMetadataHandle(&trailing_metadata_batch, - Arena::PooledDeleter(nullptr)); - }); - }); - auto result = promise(); - EXPECT_TRUE(result.ready()); - EXPECT_TRUE(seen); +TEST_F(ClientAuthorityFilterTest, PromiseCompletesImmediatelyAndSetsAuthority) { + StrictMock call( + MakeChannel(TestChannelArgs("foo.test.google.au")).value()); + EXPECT_EVENT( + Started(&call, HasMetadataKeyValue(":authority", "foo.test.google.au"))); + call.Start(call.NewClientMetadata()); } -TEST(ClientAuthorityFilterTest, - PromiseCompletesImmediatelyAndDoesNotClobberAlreadySetsAuthority) { - auto filter = *ClientAuthorityFilter::Create( - TestChannelArgs("foo.test.google.au"), ChannelFilter::Args()); - MemoryAllocator memory_allocator = MemoryAllocator( - ResourceQuota::Default()->memory_quota()->CreateMemoryAllocator("test")); - auto arena = MakeScopedArena(1024, &memory_allocator); - grpc_metadata_batch initial_metadata_batch(arena.get()); - grpc_metadata_batch trailing_metadata_batch(arena.get()); - initial_metadata_batch.Set(HttpAuthorityMetadata(), - Slice::FromStaticString("bar.test.google.au")); - bool seen = false; - // TODO(ctiller): use Activity here, once it's ready. - TestContext context(arena.get()); - auto promise = filter.MakeCallPromise( - CallArgs{ClientMetadataHandle(&initial_metadata_batch, - Arena::PooledDeleter(nullptr)), - ClientInitialMetadataOutstandingToken::Empty(), nullptr, nullptr, - nullptr}, - [&](CallArgs call_args) { - EXPECT_EQ(call_args.client_initial_metadata - ->get_pointer(HttpAuthorityMetadata()) - ->as_string_view(), - "bar.test.google.au"); - seen = true; - return ArenaPromise( - [&]() -> Poll { - return ServerMetadataHandle(&trailing_metadata_batch, - Arena::PooledDeleter(nullptr)); - }); - }); - auto result = promise(); - EXPECT_TRUE(result.ready()); - EXPECT_TRUE(seen); +TEST_F(ClientAuthorityFilterTest, + PromiseCompletesImmediatelyAndDoesNotSetAuthority) { + StrictMock call( + MakeChannel(TestChannelArgs("foo.test.google.au")).value()); + EXPECT_EVENT( + Started(&call, HasMetadataKeyValue(":authority", "bar.test.google.au"))); + call.Start(call.NewClientMetadata({{":authority", "bar.test.google.au"}})); } } // namespace diff --git a/test/core/filters/filter_test.cc b/test/core/filters/filter_test.cc new file mode 100644 index 00000000000..00e1af43676 --- /dev/null +++ b/test/core/filters/filter_test.cc @@ -0,0 +1,425 @@ +// Copyright 2023 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "test/core/filters/filter_test.h" + +#include +#include +#include +#include + +#include "absl/memory/memory.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" +#include "absl/types/optional.h" +#include "gtest/gtest.h" + +#include "src/core/lib/channel/context.h" +#include "src/core/lib/gprpp/crash.h" +#include "src/core/lib/iomgr/timer_manager.h" +#include "src/core/lib/promise/activity.h" +#include "src/core/lib/promise/arena_promise.h" +#include "src/core/lib/promise/context.h" +#include "src/core/lib/promise/detail/basic_seq.h" +#include "src/core/lib/promise/pipe.h" +#include "src/core/lib/promise/poll.h" +#include "src/core/lib/resource_quota/arena.h" +#include "src/core/lib/slice/slice.h" +#include "test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.pb.h" + +namespace grpc_core { + +/////////////////////////////////////////////////////////////////////////////// +// FilterTestBase::Call::Impl + +class FilterTestBase::Call::Impl + : public std::enable_shared_from_this { + public: + Impl(Call* call, std::shared_ptr channel) + : call_(call), channel_(std::move(channel)) {} + ~Impl(); + + Arena* arena() { return arena_.get(); } + grpc_call_context_element* legacy_context() { return legacy_context_; } + const std::shared_ptr& channel() const { return channel_; } + + void Start(ClientMetadataHandle md); + void ForwardServerInitialMetadata(ServerMetadataHandle md); + void ForwardMessageClientToServer(MessageHandle msg); + void ForwardMessageServerToClient(MessageHandle msg); + void FinishNextFilter(ServerMetadataHandle md); + + void StepLoop(); + + grpc_event_engine::experimental::EventEngine* event_engine() { + return channel_->test->event_engine(); + } + + Events& events() { return channel_->test->events; } + + private: + bool StepOnce(); + Poll PollNextFilter(); + void ForceWakeup(); + + Call* const call_; + std::shared_ptr const channel_; + ScopedArenaPtr arena_{MakeScopedArena(channel_->initial_arena_size, + &channel_->memory_allocator)}; + absl::optional> promise_; + Poll poll_next_filter_result_; + Pipe pipe_server_initial_metadata_{arena_.get()}; + Pipe pipe_server_to_client_messages_{arena_.get()}; + Pipe pipe_client_to_server_messages_{arena_.get()}; + PipeSender* server_initial_metadata_sender_ = nullptr; + PipeSender* server_to_client_messages_sender_ = nullptr; + PipeReceiver* client_to_server_messages_receiver_ = nullptr; + absl::optional::PushType> + push_server_initial_metadata_; + absl::optional> + next_server_initial_metadata_; + absl::optional::PushType> + push_server_to_client_messages_; + absl::optional> + next_server_to_client_messages_; + absl::optional::PushType> + push_client_to_server_messages_; + absl::optional> + next_client_to_server_messages_; + absl::optional forward_server_initial_metadata_; + std::queue forward_client_to_server_messages_; + std::queue forward_server_to_client_messages_; + // Contexts for various subsystems (security, tracing, ...). + grpc_call_context_element legacy_context_[GRPC_CONTEXT_COUNT] = {}; +}; + +FilterTestBase::Call::Impl::~Impl() { + for (size_t i = 0; i < GRPC_CONTEXT_COUNT; ++i) { + if (legacy_context_[i].destroy != nullptr) { + legacy_context_[i].destroy(legacy_context_[i].value); + } + } +} + +void FilterTestBase::Call::Impl::Start(ClientMetadataHandle md) { + EXPECT_EQ(promise_, absl::nullopt); + promise_ = channel_->filter->MakeCallPromise( + CallArgs{std::move(md), ClientInitialMetadataOutstandingToken::Empty(), + &pipe_server_initial_metadata_.sender, + &pipe_client_to_server_messages_.receiver, + &pipe_server_to_client_messages_.sender}, + [this](CallArgs args) -> ArenaPromise { + server_initial_metadata_sender_ = args.server_initial_metadata; + client_to_server_messages_receiver_ = args.client_to_server_messages; + server_to_client_messages_sender_ = args.server_to_client_messages; + next_server_initial_metadata_.emplace( + pipe_server_initial_metadata_.receiver.Next()); + events().Started(call_, *args.client_initial_metadata); + return [this]() { return PollNextFilter(); }; + }); + EXPECT_NE(promise_, absl::nullopt); + ForceWakeup(); +} + +Poll FilterTestBase::Call::Impl::PollNextFilter() { + return std::exchange(poll_next_filter_result_, Pending()); +} + +void FilterTestBase::Call::Impl::ForwardServerInitialMetadata( + ServerMetadataHandle md) { + EXPECT_FALSE(forward_server_initial_metadata_.has_value()); + forward_server_initial_metadata_ = std::move(md); + ForceWakeup(); +} + +void FilterTestBase::Call::Impl::ForwardMessageClientToServer( + MessageHandle msg) { + forward_client_to_server_messages_.push(std::move(msg)); + ForceWakeup(); +} + +void FilterTestBase::Call::Impl::ForwardMessageServerToClient( + MessageHandle msg) { + forward_server_to_client_messages_.push(std::move(msg)); + ForceWakeup(); +} + +void FilterTestBase::Call::Impl::FinishNextFilter(ServerMetadataHandle md) { + poll_next_filter_result_ = std::move(md); + ForceWakeup(); +} + +bool FilterTestBase::Call::Impl::StepOnce() { + if (!promise_.has_value()) return true; + + if (forward_server_initial_metadata_.has_value() && + !push_server_initial_metadata_.has_value()) { + push_server_initial_metadata_.emplace(server_initial_metadata_sender_->Push( + std::move(*forward_server_initial_metadata_))); + forward_server_initial_metadata_.reset(); + } + + if (push_server_initial_metadata_.has_value()) { + auto r = (*push_server_initial_metadata_)(); + if (r.ready()) push_server_initial_metadata_.reset(); + } + + if (next_server_initial_metadata_.has_value()) { + auto r = (*next_server_initial_metadata_)(); + if (auto* p = r.value_if_ready()) { + if (p->has_value()) { + events().ForwardedServerInitialMetadata(call_, *p->value()); + } + next_server_initial_metadata_.reset(); + } + } + + if (server_initial_metadata_sender_ != nullptr && + !next_server_initial_metadata_.has_value()) { + // We've finished sending server initial metadata, so we can + // process server-to-client messages. + if (!next_server_to_client_messages_.has_value()) { + next_server_to_client_messages_.emplace( + pipe_server_to_client_messages_.receiver.Next()); + } + + if (push_server_to_client_messages_.has_value()) { + auto r = (*push_server_to_client_messages_)(); + if (r.ready()) push_server_to_client_messages_.reset(); + } + + { + auto r = (*next_server_to_client_messages_)(); + if (auto* p = r.value_if_ready()) { + if (p->has_value()) { + events().ForwardedMessageServerToClient(call_, *p->value()); + } + next_server_to_client_messages_.reset(); + Activity::current()->ForceImmediateRepoll(); + } + } + + if (!push_server_to_client_messages_.has_value() && + !forward_server_to_client_messages_.empty()) { + push_server_to_client_messages_.emplace( + server_to_client_messages_sender_->Push( + std::move(forward_server_to_client_messages_.front()))); + forward_server_to_client_messages_.pop(); + Activity::current()->ForceImmediateRepoll(); + } + } + + if (client_to_server_messages_receiver_ != nullptr) { + if (!next_client_to_server_messages_.has_value()) { + next_client_to_server_messages_.emplace( + client_to_server_messages_receiver_->Next()); + } + + if (push_client_to_server_messages_.has_value()) { + auto r = (*push_client_to_server_messages_)(); + if (r.ready()) push_client_to_server_messages_.reset(); + } + + { + auto r = (*next_client_to_server_messages_)(); + if (auto* p = r.value_if_ready()) { + if (p->has_value()) { + events().ForwardedMessageClientToServer(call_, *p->value()); + } + next_client_to_server_messages_.reset(); + Activity::current()->ForceImmediateRepoll(); + } + } + + if (!push_client_to_server_messages_.has_value() && + !forward_client_to_server_messages_.empty()) { + push_client_to_server_messages_.emplace( + pipe_client_to_server_messages_.sender.Push( + std::move(forward_client_to_server_messages_.front()))); + forward_client_to_server_messages_.pop(); + Activity::current()->ForceImmediateRepoll(); + } + } + + auto r = (*promise_)(); + if (r.pending()) return false; + promise_.reset(); + events().Finished(call_, *r.value()); + return true; +} + +/////////////////////////////////////////////////////////////////////////////// +// FilterTestBase::Call::ScopedContext + +class FilterTestBase::Call::ScopedContext final + : public Activity, + public promise_detail::Context, + public promise_detail::Context { + private: + class TestWakeable final : public Wakeable { + public: + explicit TestWakeable(ScopedContext* ctx) + : tag_(ctx->DebugTag()), impl_(ctx->impl_) {} + void Wakeup(WakeupMask) override { + std::unique_ptr self(this); + auto impl = impl_.lock(); + if (impl == nullptr) return; + impl->event_engine()->Run([weak_impl = impl_]() { + auto impl = weak_impl.lock(); + if (impl != nullptr) impl->StepLoop(); + }); + } + void Drop(WakeupMask) override { delete this; } + std::string ActivityDebugTag(WakeupMask) const override { return tag_; } + + private: + const std::string tag_; + const std::weak_ptr impl_; + }; + + public: + explicit ScopedContext(std::shared_ptr impl) + : promise_detail::Context(impl->arena()), + promise_detail::Context( + impl->legacy_context()), + impl_(std::move(impl)) {} + + void Orphan() override { Crash("Orphan called on Call::ScopedContext"); } + void ForceImmediateRepoll(WakeupMask) override { repoll_ = true; } + Waker MakeOwningWaker() override { return Waker(new TestWakeable(this), 0); } + Waker MakeNonOwningWaker() override { + return Waker(new TestWakeable(this), 0); + } + std::string DebugTag() const override { + return absl::StrFormat("FILTER_TEST_CALL[%p]", impl_.get()); + } + + bool repoll() const { return repoll_; } + + private: + ScopedActivity scoped_activity_{this}; + const std::shared_ptr impl_; + bool repoll_ = false; +}; + +void FilterTestBase::Call::Impl::StepLoop() { + for (;;) { + ScopedContext ctx(shared_from_this()); + if (!StepOnce() && ctx.repoll()) continue; + return; + } +} + +void FilterTestBase::Call::Impl::ForceWakeup() { + ScopedContext(shared_from_this()).MakeOwningWaker().Wakeup(); +} + +/////////////////////////////////////////////////////////////////////////////// +// FilterTestBase::Call + +FilterTestBase::Call::Call(const Channel& channel) + : impl_(std::make_unique(this, channel.impl_)) {} + +FilterTestBase::Call::~Call() { ScopedContext x(std::move(impl_)); } + +ClientMetadataHandle FilterTestBase::Call::NewClientMetadata( + std::initializer_list> + init) { + auto md = impl_->arena()->MakePooled(impl_->arena()); + for (auto& p : init) { + auto parsed = ClientMetadata::Parse( + p.first, Slice::FromCopiedString(p.second), + p.first.length() + p.second.length() + 32, + [p](absl::string_view, const Slice&) { + Crash(absl::StrCat("Illegal metadata value: ", p.first, ": ", + p.second)); + }); + md->Set(parsed); + } + return md; +} + +ServerMetadataHandle FilterTestBase::Call::NewServerMetadata( + std::initializer_list> + init) { + auto md = impl_->arena()->MakePooled(impl_->arena()); + for (auto& p : init) { + auto parsed = ServerMetadata::Parse( + p.first, Slice::FromCopiedString(p.second), + p.first.length() + p.second.length() + 32, + [p](absl::string_view, const Slice&) { + Crash(absl::StrCat("Illegal metadata value: ", p.first, ": ", + p.second)); + }); + md->Set(parsed); + } + return md; +} + +MessageHandle FilterTestBase::Call::NewMessage(absl::string_view payload, + uint32_t flags) { + SliceBuffer buffer; + if (!payload.empty()) buffer.Append(Slice::FromCopiedString(payload)); + return impl_->arena()->MakePooled(std::move(buffer), flags); +} + +void FilterTestBase::Call::Start(ClientMetadataHandle md) { + ScopedContext ctx(impl_); + impl_->Start(std::move(md)); +} + +void FilterTestBase::Call::Cancel() { + ScopedContext ctx(impl_); + impl_ = absl::make_unique(this, impl_->channel()); +} + +void FilterTestBase::Call::ForwardServerInitialMetadata( + ServerMetadataHandle md) { + impl_->ForwardServerInitialMetadata(std::move(md)); +} + +void FilterTestBase::Call::ForwardMessageClientToServer(MessageHandle msg) { + impl_->ForwardMessageClientToServer(std::move(msg)); +} + +void FilterTestBase::Call::ForwardMessageServerToClient(MessageHandle msg) { + impl_->ForwardMessageServerToClient(std::move(msg)); +} + +void FilterTestBase::Call::FinishNextFilter(ServerMetadataHandle md) { + impl_->FinishNextFilter(std::move(md)); +} + +/////////////////////////////////////////////////////////////////////////////// +// FilterTestBase + +FilterTestBase::FilterTestBase() + : event_engine_( + []() { + grpc_timer_manager_set_threading(false); + grpc_event_engine::experimental::FuzzingEventEngine::Options + options; + options.final_tick_length = std::chrono::milliseconds(1); + return options; + }(), + fuzzing_event_engine::Actions()) {} + +FilterTestBase::~FilterTestBase() { event_engine_.UnsetGlobalHooks(); } + +void FilterTestBase::Step() { + event_engine_.TickUntilIdle(); + ::testing::Mock::VerifyAndClearExpectations(&events); +} + +} // namespace grpc_core diff --git a/test/core/filters/filter_test.h b/test/core/filters/filter_test.h new file mode 100644 index 00000000000..af73a51fc6a --- /dev/null +++ b/test/core/filters/filter_test.h @@ -0,0 +1,225 @@ +// Copyright 2023 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef GRPC_TEST_CORE_FILTERS_FILTER_TEST_H +#define GRPC_TEST_CORE_FILTERS_FILTER_TEST_H + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/escaping.h" +#include "absl/strings/string_view.h" +#include "gmock/gmock.h" + +#include +#include + +#include "src/core/lib/channel/channel_args.h" +#include "src/core/lib/channel/promise_based_filter.h" +#include "src/core/lib/gprpp/ref_counted_ptr.h" +#include "src/core/lib/resource_quota/memory_quota.h" +#include "src/core/lib/resource_quota/resource_quota.h" +#include "src/core/lib/slice/slice_buffer.h" +#include "src/core/lib/transport/metadata_batch.h" +#include "src/core/lib/transport/transport.h" +#include "test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.h" +#include "test/core/filters/filter_test.h" + +// gmock matcher to ensure that metadata has a key/value pair. +MATCHER_P2(HasMetadataKeyValue, key, value, "") { + std::string temp; + auto r = arg.GetStringValue(key, &temp); + return r == value; +} + +// gmock matcher to ensure that a message has a given set of flags. +MATCHER_P(HasMessageFlags, value, "") { return arg.flags() == value; } + +MATCHER_P(HasMetadataResult, absl_status, "") { + auto status = arg.get(grpc_core::GrpcStatusMetadata()); + if (!status.has_value()) return false; + if (static_cast(status.value()) != absl_status.code()) { + return false; + } + auto* message = arg.get_pointer(grpc_core::GrpcMessageMetadata()); + if (message == nullptr) return absl_status.message().empty(); + return message->as_string_view() == absl_status.message(); +} + +// gmock matcher to ensure that a message has a given payload. +MATCHER_P(HasMessagePayload, value, "") { + return arg.payload()->JoinIntoString() == value; +} + +namespace grpc_core { + +inline std::ostream& operator<<(std::ostream& os, + const grpc_metadata_batch& md) { + return os << md.DebugString(); +} + +inline std::ostream& operator<<(std::ostream& os, const Message& msg) { + return os << "flags:" << msg.flags() + << " payload:" << absl::CEscape(msg.payload()->JoinIntoString()); +} + +class FilterTestBase : public ::testing::Test { + public: + class Call; + + class Channel { + private: + struct Impl { + Impl(std::unique_ptr filter, FilterTestBase* test) + : filter(std::move(filter)), test(test) {} + size_t initial_arena_size = 1024; + MemoryAllocator memory_allocator = + ResourceQuota::Default()->memory_quota()->CreateMemoryAllocator( + "test"); + std::unique_ptr filter; + FilterTestBase* const test; + }; + + public: + void set_initial_arena_size(size_t size) { + impl_->initial_arena_size = size; + } + + Call MakeCall(); + + private: + friend class FilterTestBase; + friend class Call; + + explicit Channel(std::unique_ptr filter, + FilterTestBase* test) + : impl_(std::make_shared(std::move(filter), test)) {} + + std::shared_ptr impl_; + }; + + // One "call" outstanding against this filter. + // In reality - this filter is the only thing in the call. + // Provides mocks to trap events that happen on the call. + class Call { + public: + explicit Call(const Channel& channel); + + Call(const Call&) = delete; + Call& operator=(const Call&) = delete; + + ~Call(); + + // Construct client metadata in the arena of this call. + // Optional argument is a list of key/value pairs to add to the metadata. + ClientMetadataHandle NewClientMetadata( + std::initializer_list> + init = {}); + // Construct server metadata in the arena of this call. + // Optional argument is a list of key/value pairs to add to the metadata. + ServerMetadataHandle NewServerMetadata( + std::initializer_list> + init = {}); + // Construct a message in the arena of this call. + MessageHandle NewMessage(absl::string_view payload = "", + uint32_t flags = 0); + + // Start the call. + void Start(ClientMetadataHandle md); + // Cancel the call. + void Cancel(); + // Forward server initial metadata through this filter. + void ForwardServerInitialMetadata(ServerMetadataHandle md); + // Forward a message from client to server through this filter. + void ForwardMessageClientToServer(MessageHandle msg); + // Forward a message from server to client through this filter. + void ForwardMessageServerToClient(MessageHandle msg); + // Have the 'next' filter in the chain finish this call and return trailing + // metadata. + void FinishNextFilter(ServerMetadataHandle md); + + private: + friend class Channel; + class ScopedContext; + class Impl; + + std::shared_ptr impl_; + }; + + struct Events { + // Mock to trap starting the next filter in the chain. + MOCK_METHOD(void, Started, + (Call * call, const ClientMetadata& client_initial_metadata)); + // Mock to trap receiving server initial metadata in the next filter in the + // chain. + MOCK_METHOD(void, ForwardedServerInitialMetadata, + (Call * call, const ServerMetadata& server_initial_metadata)); + // Mock to trap seeing a message forward from client to server. + MOCK_METHOD(void, ForwardedMessageClientToServer, + (Call * call, const Message& msg)); + // Mock to trap seeing a message forward from server to client. + MOCK_METHOD(void, ForwardedMessageServerToClient, + (Call * call, const Message& msg)); + // Mock to trap seeing a call finish in the next filter in the chain. + MOCK_METHOD(void, Finished, + (Call * call, const ServerMetadata& server_trailing_metadata)); + }; + + ::testing::StrictMock events; + + protected: + FilterTestBase(); + ~FilterTestBase() override; + absl::StatusOr MakeChannel(std::unique_ptr filter) { + return Channel(std::move(filter), this); + } + + grpc_event_engine::experimental::EventEngine* event_engine() { + return &event_engine_; + } + + void Step(); + + private: + grpc_event_engine::experimental::FuzzingEventEngine event_engine_; +}; + +template +class FilterTest : public FilterTestBase { + public: + absl::StatusOr MakeChannel(const ChannelArgs& args) { + auto filter = Filter::Create(args, ChannelFilter::Args()); + if (!filter.ok()) return filter.status(); + return FilterTestBase::MakeChannel( + std::make_unique(std::move(*filter))); + } +}; + +} // namespace grpc_core + +// Expect one of the events corresponding to the methods in FilterTest::Events. +#define EXPECT_EVENT(event) EXPECT_CALL(events, event) + +#endif // GRPC_TEST_CORE_FILTERS_FILTER_TEST_H diff --git a/test/core/filters/filter_test_test.cc b/test/core/filters/filter_test_test.cc new file mode 100644 index 00000000000..8ffa907b429 --- /dev/null +++ b/test/core/filters/filter_test_test.cc @@ -0,0 +1,253 @@ +// Copyright 2023 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "test/core/filters/filter_test.h" + +#include +#include +#include +#include + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include +#include + +#include "src/core/lib/channel/promise_based_filter.h" +#include "src/core/lib/promise/activity.h" +#include "src/core/lib/promise/arena_promise.h" +#include "src/core/lib/promise/map.h" +#include "src/core/lib/promise/pipe.h" +#include "src/core/lib/promise/poll.h" +#include "src/core/lib/promise/seq.h" +#include "src/core/lib/slice/slice.h" +#include "src/core/lib/transport/metadata_batch.h" +#include "src/core/lib/transport/transport.h" + +using ::testing::_; + +namespace grpc_core { +namespace { + +class NoOpFilter final : public ChannelFilter { + public: + ArenaPromise MakeCallPromise( + CallArgs args, NextPromiseFactory next) override { + return next(std::move(args)); + } + + static absl::StatusOr Create(const ChannelArgs&, + ChannelFilter::Args) { + return NoOpFilter(); + } +}; +using NoOpFilterTest = FilterTest; + +class DelayStartFilter final : public ChannelFilter { + public: + ArenaPromise MakeCallPromise( + CallArgs args, NextPromiseFactory next) override { + return Seq( + [args = std::move(args), i = 10]() mutable -> Poll { + --i; + if (i == 0) return std::move(args); + Activity::current()->ForceImmediateRepoll(); + return Pending{}; + }, + next); + } + + static absl::StatusOr Create(const ChannelArgs&, + ChannelFilter::Args) { + return DelayStartFilter(); + } +}; +using DelayStartFilterTest = FilterTest; + +class AddClientInitialMetadataFilter final : public ChannelFilter { + public: + ArenaPromise MakeCallPromise( + CallArgs args, NextPromiseFactory next) override { + args.client_initial_metadata->Set(HttpPathMetadata(), + Slice::FromCopiedString("foo.bar")); + return next(std::move(args)); + } + + static absl::StatusOr Create( + const ChannelArgs&, ChannelFilter::Args) { + return AddClientInitialMetadataFilter(); + } +}; +using AddClientInitialMetadataFilterTest = + FilterTest; + +class AddServerTrailingMetadataFilter final : public ChannelFilter { + public: + ArenaPromise MakeCallPromise( + CallArgs args, NextPromiseFactory next) override { + return Map(next(std::move(args)), [](ServerMetadataHandle handle) { + handle->Set(HttpStatusMetadata(), 420); + return handle; + }); + } + + static absl::StatusOr Create( + const ChannelArgs&, ChannelFilter::Args) { + return AddServerTrailingMetadataFilter(); + } +}; +using AddServerTrailingMetadataFilterTest = + FilterTest; + +class AddServerInitialMetadataFilter final : public ChannelFilter { + public: + ArenaPromise MakeCallPromise( + CallArgs args, NextPromiseFactory next) override { + args.server_initial_metadata->InterceptAndMap([](ServerMetadataHandle md) { + md->Set(GrpcEncodingMetadata(), GRPC_COMPRESS_GZIP); + return md; + }); + return next(std::move(args)); + } + + static absl::StatusOr Create( + const ChannelArgs&, ChannelFilter::Args) { + return AddServerInitialMetadataFilter(); + } +}; +using AddServerInitialMetadataFilterTest = + FilterTest; + +TEST_F(NoOpFilterTest, NoOp) {} + +TEST_F(NoOpFilterTest, MakeCall) { + Call call(MakeChannel(ChannelArgs()).value()); +} + +TEST_F(NoOpFilterTest, MakeClientMetadata) { + Call call(MakeChannel(ChannelArgs()).value()); + auto md = call.NewClientMetadata({{":path", "foo.bar"}}); + EXPECT_EQ(md->get_pointer(HttpPathMetadata())->as_string_view(), "foo.bar"); +} + +TEST_F(NoOpFilterTest, MakeServerMetadata) { + Call call(MakeChannel(ChannelArgs()).value()); + auto md = call.NewServerMetadata({{":status", "200"}}); + EXPECT_EQ(md->get(HttpStatusMetadata()), HttpStatusMetadata::ValueType(200)); +} + +TEST_F(NoOpFilterTest, CanStart) { + Call call(MakeChannel(ChannelArgs()).value()); + EXPECT_EVENT(Started(&call, _)); + call.Start(call.NewClientMetadata()); + Step(); +} + +TEST_F(DelayStartFilterTest, CanStartWithDelay) { + Call call(MakeChannel(ChannelArgs()).value()); + EXPECT_EVENT(Started(&call, _)); + call.Start(call.NewClientMetadata()); + Step(); +} + +TEST_F(NoOpFilterTest, CanCancel) { + Call call(MakeChannel(ChannelArgs()).value()); + EXPECT_EVENT(Started(&call, _)); + call.Start(call.NewClientMetadata()); + call.Cancel(); +} + +TEST_F(DelayStartFilterTest, CanCancelWithDelay) { + Call call(MakeChannel(ChannelArgs()).value()); + call.Start(call.NewClientMetadata()); + call.Cancel(); +} + +TEST_F(AddClientInitialMetadataFilterTest, CanSetClientInitialMetadata) { + Call call(MakeChannel(ChannelArgs()).value()); + EXPECT_EVENT(Started(&call, HasMetadataKeyValue(":path", "foo.bar"))); + call.Start(call.NewClientMetadata()); + Step(); +} + +TEST_F(NoOpFilterTest, CanFinish) { + Call call(MakeChannel(ChannelArgs()).value()); + EXPECT_EVENT(Started(&call, _)); + call.Start(call.NewClientMetadata()); + call.FinishNextFilter(call.NewServerMetadata()); + EXPECT_EVENT(Finished(&call, _)); + Step(); +} + +TEST_F(AddServerTrailingMetadataFilterTest, CanSetServerTrailingMetadata) { + Call call(MakeChannel(ChannelArgs()).value()); + EXPECT_EVENT(Started(&call, _)); + call.Start(call.NewClientMetadata()); + call.FinishNextFilter(call.NewServerMetadata()); + EXPECT_EVENT(Finished(&call, HasMetadataKeyValue(":status", "420"))); + Step(); +} + +TEST_F(NoOpFilterTest, CanProcessServerInitialMetadata) { + Call call(MakeChannel(ChannelArgs()).value()); + EXPECT_EVENT(Started(&call, _)); + call.Start(call.NewClientMetadata()); + call.ForwardServerInitialMetadata(call.NewServerMetadata()); + EXPECT_EVENT(ForwardedServerInitialMetadata(&call, _)); + Step(); +} + +TEST_F(AddServerInitialMetadataFilterTest, CanSetServerInitialMetadata) { + Call call(MakeChannel(ChannelArgs()).value()); + EXPECT_EVENT(Started(&call, _)); + call.Start(call.NewClientMetadata()); + call.ForwardServerInitialMetadata(call.NewServerMetadata()); + EXPECT_EVENT(ForwardedServerInitialMetadata( + &call, HasMetadataKeyValue("grpc-encoding", "gzip"))); + Step(); +} + +TEST_F(NoOpFilterTest, CanProcessClientToServerMessage) { + Call call(MakeChannel(ChannelArgs()).value()); + EXPECT_EVENT(Started(&call, _)); + call.Start(call.NewClientMetadata()); + call.ForwardMessageClientToServer(call.NewMessage("abc")); + EXPECT_CALL(events, + ForwardedMessageClientToServer(&call, HasMessagePayload("abc"))); + Step(); +} + +TEST_F(NoOpFilterTest, CanProcessServerToClientMessage) { + Call call(MakeChannel(ChannelArgs()).value()); + EXPECT_EVENT(Started(&call, _)); + call.Start(call.NewClientMetadata()); + call.ForwardServerInitialMetadata(call.NewServerMetadata()); + call.ForwardMessageServerToClient(call.NewMessage("abc")); + EXPECT_EVENT(ForwardedServerInitialMetadata(&call, _)); + EXPECT_CALL(events, + ForwardedMessageServerToClient(&call, HasMessagePayload("abc"))); + Step(); +} + +} // namespace +} // namespace grpc_core + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + grpc_init(); + int r = RUN_ALL_TESTS(); + grpc_shutdown(); + return r; +} diff --git a/tools/distrib/fix_build_deps.py b/tools/distrib/fix_build_deps.py index a567a0bb376..2e9bb2d3721 100755 --- a/tools/distrib/fix_build_deps.py +++ b/tools/distrib/fix_build_deps.py @@ -446,6 +446,7 @@ for dirname in [ "test/core/util", "test/core/end2end", "test/core/event_engine", + "test/core/filters", "test/core/promise", "test/core/resource_quota", "test/core/transport/chaotic_good", diff --git a/tools/run_tests/generated/tests.json b/tools/run_tests/generated/tests.json index 7254bbeaef7..a7264007bda 100644 --- a/tools/run_tests/generated/tests.json +++ b/tools/run_tests/generated/tests.json @@ -3035,6 +3035,30 @@ ], "uses_polling": true }, + { + "args": [], + "benchmark": false, + "ci_platforms": [ + "linux", + "mac", + "posix", + "windows" + ], + "cpu_cost": 1.0, + "exclude_configs": [], + "exclude_iomgrs": [], + "flaky": false, + "gtest": true, + "language": "c++", + "name": "filter_test_test", + "platforms": [ + "linux", + "mac", + "posix", + "windows" + ], + "uses_polling": false + }, { "args": [], "benchmark": false,