[call-v3] Yodel - a call testing framework (#36635)

Introduce "Yodel" - a framework for testing things vaguely related to calls.

This is breaking up some work I did for transport test suites - we've got a nice way of spawning test-only promises and tracking them visually, and support for setting up an environment that can run as a test or a fuzzer. I'm making that piece a little more reusable, and then rebasing the transport test suite atop that infrastructure.

Closes #36635

COPYBARA_INTEGRATE_REVIEW=https://github.com/grpc/grpc/pull/36635 from ctiller:transport-refs-6 843a9f4b7e
PiperOrigin-RevId: 637022756
pull/36727/head
Craig Tiller 6 months ago committed by Copybara-Service
parent aa83a3fe32
commit 621aa4e5ce
  1. 12
      CMakeLists.txt
  2. 2
      bazel/grpc_build_system.bzl
  3. 20
      build_autogenerated.yaml
  4. 81
      test/core/call/yodel/BUILD
  5. 12
      test/core/call/yodel/README.md
  6. 1
      test/core/call/yodel/fuzzer.proto
  7. 34
      test/core/call/yodel/fuzzer_main.cc
  8. 59
      test/core/call/yodel/grpc_yodel_test.bzl
  9. 27
      test/core/call/yodel/test_main.cc
  10. 230
      test/core/call/yodel/yodel_test.cc
  11. 462
      test/core/call/yodel/yodel_test.h
  12. 4
      test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.cc
  13. 54
      test/core/transport/test_suite/BUILD
  14. 2
      test/core/transport/test_suite/call_content.cc
  15. 2
      test/core/transport/test_suite/call_shapes.cc
  16. 2
      test/core/transport/test_suite/chaotic_good_fixture.cc
  17. 29
      test/core/transport/test_suite/fixture.cc
  18. 77
      test/core/transport/test_suite/fixture.h
  19. 42
      test/core/transport/test_suite/grpc_transport_test.bzl
  20. 2
      test/core/transport/test_suite/inproc_fixture.cc
  21. 2
      test/core/transport/test_suite/no_op.cc
  22. 2
      test/core/transport/test_suite/stress.cc
  23. 368
      test/core/transport/test_suite/test.h
  24. 69
      test/core/transport/test_suite/transport_test.cc
  25. 87
      test/core/transport/test_suite/transport_test.h
  26. 74
      tools/distrib/fix_build_deps.py

12
CMakeLists.txt generated

@ -17562,15 +17562,15 @@ if(_gRPC_PLATFORM_LINUX OR _gRPC_PLATFORM_POSIX)
${_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/call/yodel/test_main.cc
test/core/call/yodel/yodel_test.cc
test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.cc
test/core/transport/test_suite/call_content.cc
test/core/transport/test_suite/call_shapes.cc
test/core/transport/test_suite/fixture.cc
test/core/transport/test_suite/inproc_fixture.cc
test/core/transport/test_suite/no_op.cc
test/core/transport/test_suite/stress.cc
test/core/transport/test_suite/test.cc
test/core/transport/test_suite/test_main.cc
test/core/transport/test_suite/transport_test.cc
)
if(WIN32 AND MSVC)
if(BUILD_SHARED_LIBS)
@ -30294,15 +30294,15 @@ if(_gRPC_PLATFORM_LINUX OR _gRPC_PLATFORM_POSIX)
src/core/ext/transport/chaotic_good/frame_header.cc
src/core/ext/transport/chaotic_good/server_transport.cc
src/core/lib/transport/promise_endpoint.cc
test/core/call/yodel/test_main.cc
test/core/call/yodel/yodel_test.cc
test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.cc
test/core/transport/test_suite/call_content.cc
test/core/transport/test_suite/call_shapes.cc
test/core/transport/test_suite/chaotic_good_fixture.cc
test/core/transport/test_suite/fixture.cc
test/core/transport/test_suite/no_op.cc
test/core/transport/test_suite/stress.cc
test/core/transport/test_suite/test.cc
test/core/transport/test_suite/test_main.cc
test/core/transport/test_suite/transport_test.cc
)
if(WIN32 AND MSVC)
if(BUILD_SHARED_LIBS)

@ -235,6 +235,7 @@ def grpc_proto_library(
name,
srcs = [],
deps = [],
visibility = None,
well_known_protos = False,
has_services = True,
use_external = False,
@ -243,6 +244,7 @@ def grpc_proto_library(
name = name,
srcs = srcs,
deps = deps,
visibility = visibility,
well_known_protos = well_known_protos,
proto_only = not has_services,
use_external = use_external,

@ -11453,20 +11453,20 @@ targets:
build: test
language: c++
headers:
- test/core/call/yodel/yodel_test.h
- test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.h
- test/core/transport/test_suite/fixture.h
- test/core/transport/test_suite/test.h
- test/core/transport/test_suite/transport_test.h
src:
- test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.proto
- test/core/call/yodel/test_main.cc
- test/core/call/yodel/yodel_test.cc
- test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.cc
- test/core/transport/test_suite/call_content.cc
- test/core/transport/test_suite/call_shapes.cc
- test/core/transport/test_suite/fixture.cc
- test/core/transport/test_suite/inproc_fixture.cc
- test/core/transport/test_suite/no_op.cc
- test/core/transport/test_suite/stress.cc
- test/core/transport/test_suite/test.cc
- test/core/transport/test_suite/test_main.cc
- test/core/transport/test_suite/transport_test.cc
deps:
- gtest
- protobuf
@ -19488,9 +19488,9 @@ targets:
- src/core/lib/promise/switch.h
- src/core/lib/promise/wait_set.h
- src/core/lib/transport/promise_endpoint.h
- test/core/call/yodel/yodel_test.h
- test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.h
- test/core/transport/test_suite/fixture.h
- test/core/transport/test_suite/test.h
- test/core/transport/test_suite/transport_test.h
src:
- test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.proto
- src/core/ext/transport/chaotic_good/chaotic_good_transport.cc
@ -19499,15 +19499,15 @@ targets:
- src/core/ext/transport/chaotic_good/frame_header.cc
- src/core/ext/transport/chaotic_good/server_transport.cc
- src/core/lib/transport/promise_endpoint.cc
- test/core/call/yodel/test_main.cc
- test/core/call/yodel/yodel_test.cc
- test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.cc
- test/core/transport/test_suite/call_content.cc
- test/core/transport/test_suite/call_shapes.cc
- test/core/transport/test_suite/chaotic_good_fixture.cc
- test/core/transport/test_suite/fixture.cc
- test/core/transport/test_suite/no_op.cc
- test/core/transport/test_suite/stress.cc
- test/core/transport/test_suite/test.cc
- test/core/transport/test_suite/test_main.cc
- test/core/transport/test_suite/transport_test.cc
deps:
- gtest
- protobuf

@ -0,0 +1,81 @@
# Copyright 2024 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.
load(
"//bazel:grpc_build_system.bzl",
"grpc_cc_library",
"grpc_package",
"grpc_proto_library",
)
grpc_package(name = "test/core/call/yodel")
exports_files(
["fuzzer_main.cc"],
visibility = ["//test:__subpackages__"],
)
grpc_cc_library(
name = "yodel_test",
testonly = 1,
srcs = ["yodel_test.cc"],
hdrs = ["yodel_test.h"],
external_deps = [
"absl/functional:any_invocable",
"absl/log",
"absl/random",
"absl/random:bit_gen_ref",
"absl/strings",
"gtest",
],
visibility = ["//test:__subpackages__"],
deps = [
"//:debug_location",
"//:event_engine_base_hdrs",
"//:iomgr_timer",
"//:promise",
"//src/core:call_arena_allocator",
"//src/core:call_spine",
"//src/core:cancel_callback",
"//src/core:metadata",
"//src/core:promise_factory",
"//src/core:resource_quota",
"//test/core/event_engine/fuzzing_event_engine",
"//test/core/test_util:grpc_test_util",
],
)
grpc_cc_library(
name = "test_main",
testonly = 1,
srcs = ["test_main.cc"],
external_deps = ["absl/random"],
visibility = ["//test:__subpackages__"],
deps = [
"yodel_test",
"//:grpc_trace",
"//test/core/test_util:grpc_test_util",
],
)
grpc_proto_library(
name = "fuzzer_proto",
srcs = ["fuzzer.proto"],
has_services = False,
visibility = ["//test:__subpackages__"],
deps = [
"//test/core/event_engine/fuzzing_event_engine:fuzzing_event_engine_proto",
"//test/core/test_util:fuzz_config_vars_proto",
],
)

@ -0,0 +1,12 @@
Yodel is a foundational test framework for unit testing parts of a call.
It provides infrastructure to write tests around some call actor (the various
frameworks built atop of it specify what that actor is). It also provides
utilities to fill in various call details, interact with promises in a
way that's convenient to debug, and run as either a unit test or a fuzzer.
Various frameworks are built atop it:
- transports use it as part of the transport test_suite
Planned:
- interceptors & filters should also use this

@ -21,7 +21,6 @@ import "test/core/test_util/fuzz_config_vars.proto";
message Msg {
uint32 test_id = 1;
uint32 fixture_id = 2;
fuzzing_event_engine.Actions event_engine_actions = 10;
grpc.testing.FuzzConfigVars config_vars = 11;

@ -12,37 +12,32 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#include <stdio.h>
#include <gtest/gtest.h>
#include "absl/log/check.h"
#include <grpc/event_engine/event_engine.h>
#include <grpc/support/log.h>
#include <gtest/gtest.h>
#include <stdio.h>
#include "src/core/lib/config/config_vars.h"
#include "src/core/lib/event_engine/default_event_engine.h"
#include "src/core/lib/experiments/config.h"
#include "src/core/lib/gprpp/env.h"
#include "src/libfuzzer/libfuzzer_macro.h"
#include "test/core/call/yodel/fuzzer.pb.h"
#include "test/core/call/yodel/yodel_test.h"
#include "test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.h"
#include "test/core/test_util/fuzz_config_vars.h"
#include "test/core/test_util/proto_bit_gen.h"
#include "test/core/transport/test_suite/fixture.h"
#include "test/core/transport/test_suite/fuzzer.pb.h"
#include "test/core/transport/test_suite/test.h"
#include "absl/log/check.h"
bool squelch = true;
static void dont_log(gpr_log_func_args* /*args*/) {}
DEFINE_PROTO_FUZZER(const transport_test_suite::Msg& msg) {
const auto& tests = grpc_core::TransportTestRegistry::Get().tests();
const auto& fixtures = grpc_core::TransportFixtureRegistry::Get().fixtures();
CHECK(!tests.empty());
CHECK(!fixtures.empty());
const int test_id = msg.test_id() % tests.size();
const int fixture_id = msg.fixture_id() % fixtures.size();
static const grpc_core::NoDestruct<
std::vector<grpc_core::yodel_detail::TestRegistry::Test>>
tests{grpc_core::yodel_detail::TestRegistry::AllTests()};
CHECK(!tests->empty());
const int test_id = msg.test_id() % tests->size();
if (squelch && !grpc_core::GetEnv("GRPC_TRACE_FUZZER").has_value()) {
gpr_set_log_function(dont_log);
@ -53,15 +48,10 @@ DEFINE_PROTO_FUZZER(const transport_test_suite::Msg& msg) {
grpc_core::ConfigVars::SetOverrides(overrides);
grpc_core::TestOnlyReloadExperimentsFromConfigVariables();
if (!squelch) {
fprintf(stderr, "RUN TEST '%s' with fixture '%s'\n",
std::string(tests[test_id].name).c_str(),
std::string(fixtures[fixture_id].name).c_str());
LOG(INFO) << "RUN TEST '" << (*tests)[test_id].name << "'";
}
grpc_core::ProtoBitGen bitgen(msg.rng());
auto test =
tests[test_id].create(std::unique_ptr<grpc_core::TransportFixture>(
fixtures[fixture_id].create()),
msg.event_engine_actions(), bitgen);
auto test = (*tests)[test_id].make(msg.event_engine_actions(), bitgen);
test->RunTest();
delete test;
CHECK(!::testing::Test::HasFailure());

@ -0,0 +1,59 @@
# Copyright 2024 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.
"""
Generate one transport test & associated fuzzer
"""
load("//bazel:grpc_build_system.bzl", "grpc_cc_test")
load("//test/core/test_util:grpc_fuzzer.bzl", "grpc_proto_fuzzer")
def grpc_yodel_test(name, deps):
grpc_cc_test(
name = name + "_test",
srcs = [],
tags = ["no_windows", "no_mac"],
deps = [
"//test/core/call/yodel:test_main",
] + deps,
uses_polling = False,
)
grpc_proto_fuzzer(
name = name + "_fuzzer",
srcs = ["//test/core/call/yodel:fuzzer_main.cc"],
tags = ["no_windows", "no_mac"],
external_deps = [
"absl/log:check",
"gtest",
],
deps = [
"//test/core/call/yodel:yodel_test",
"//test/core/call/yodel:fuzzer_proto",
"//:event_engine_base_hdrs",
"//:config_vars",
"//:exec_ctx",
"//:gpr",
"//:grpc_unsecure",
"//:iomgr_timer",
"//src/core:default_event_engine",
"//src/core:env",
"//src/core:experiments",
"//test/core/event_engine/fuzzing_event_engine",
"//test/core/test_util:fuzz_config_vars",
"//test/core/test_util:proto_bit_gen",
] + deps,
corpus = "corpus/%s" % name,
proto = None,
)

@ -15,27 +15,24 @@
#include "absl/random/random.h"
#include "src/core/lib/debug/trace.h"
#include "test/core/call/yodel/yodel_test.h"
#include "test/core/test_util/test_config.h"
#include "test/core/transport/test_suite/fixture.h"
#include "test/core/transport/test_suite/test.h"
int main(int argc, char** argv) {
grpc::testing::TestEnvironment env(&argc, argv);
absl::BitGen bitgen;
::testing::InitGoogleTest(&argc, argv);
for (const auto& test : grpc_core::TransportTestRegistry::Get().tests()) {
for (const auto& fixture :
grpc_core::TransportFixtureRegistry::Get().fixtures()) {
::testing::RegisterTest(
"TransportTest", absl::StrCat(test.name, "/", fixture.name).c_str(),
nullptr, nullptr, __FILE__, __LINE__,
[test = &test, fixture = &fixture,
&bitgen]() -> grpc_core::TransportTest* {
return test->create(
std::unique_ptr<grpc_core::TransportFixture>(fixture->create()),
fuzzing_event_engine::Actions(), bitgen);
});
}
static grpc_core::NoDestruct<
std::vector<grpc_core::yodel_detail::TestRegistry::Test>>
tests{grpc_core::yodel_detail::TestRegistry::AllTests()};
CHECK(!tests->empty());
for (const auto& test : *tests) {
CHECK(test.make != nullptr) << "test:" << test.name;
::testing::RegisterTest(
test.test_type.c_str(), test.name.c_str(), nullptr, nullptr, __FILE__,
__LINE__, [test = &test, &bitgen]() -> grpc_core::YodelTest* {
return test->make(fuzzing_event_engine::Actions(), bitgen);
});
}
grpc_tracer_init();
return RUN_ALL_TESTS();

@ -1,4 +1,4 @@
// Copyright 2023 gRPC authors.
// Copyright 2024 gRPC authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -12,75 +12,159 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#include "test/core/transport/test_suite/test.h"
#include <initializer_list>
#include "test/core/call/yodel/yodel_test.h"
#include "absl/random/random.h"
#include "src/core/lib/iomgr/timer_manager.h"
#include "src/core/lib/resource_quota/resource_quota.h"
namespace grpc_core {
namespace yodel_detail {
TestRegistry* TestRegistry::root_ = nullptr;
///////////////////////////////////////////////////////////////////////////////
// ActionState
ActionState::ActionState(NameAndLocation name_and_location, int step)
: name_and_location_(name_and_location), step_(step), state_(kNotCreated) {}
absl::string_view ActionState::StateString(State state) {
// We use emoji here to make it easier to visually scan the logs.
switch (state) {
case kNotCreated:
return "🚦";
case kNotStarted:
return "";
case kStarted:
return "🚗";
case kDone:
return "🏁";
case kCancelled:
return "💥";
}
}
void ActionState::Set(State state, SourceLocation whence) {
LOG(INFO) << StateString(state) << " " << name() << " [" << step() << "] "
<< file() << ":" << line() << " @ " << whence.file() << ":"
<< whence.line();
state_ = state;
}
bool ActionState::IsDone() {
switch (state_) {
case kNotCreated:
case kNotStarted:
case kStarted:
return false;
case kDone:
case kCancelled:
return true;
}
}
///////////////////////////////////////////////////////////////////////////////
// TransportTestRegistry
// TestRegistry
TransportTestRegistry& TransportTestRegistry::Get() {
static TransportTestRegistry* registry = new TransportTestRegistry();
return *registry;
std::vector<TestRegistry::Test> TestRegistry::AllTests() {
std::vector<Test> tests;
for (auto* r = root_; r; r = r->next_) {
r->ContributeTests(tests);
}
std::vector<Test> out;
for (auto& test : tests) {
if (absl::StartsWith(test.name, "DISABLED_")) continue;
out.emplace_back(std::move(test));
}
std::stable_sort(out.begin(), out.end(), [](const Test& a, const Test& b) {
return std::make_tuple(a.file, a.line) < std::make_tuple(b.file, b.line);
});
return out;
}
void TransportTestRegistry::RegisterTest(
///////////////////////////////////////////////////////////////////////////////
// SimpleTestRegistry
void SimpleTestRegistry::RegisterTest(
absl::string_view file, int line, absl::string_view test_type,
absl::string_view name,
absl::AnyInvocable<TransportTest*(std::unique_ptr<TransportFixture>,
const fuzzing_event_engine::Actions&,
absl::BitGenRef) const>
absl::AnyInvocable<YodelTest*(const fuzzing_event_engine::Actions&,
absl::BitGenRef) const>
create) {
if (absl::StartsWith(name, "DISABLED_")) return;
tests_.push_back({name, std::move(create)});
tests_.push_back({file, line, std::string(test_type), std::string(name),
std::move(create)});
}
void SimpleTestRegistry::ContributeTests(std::vector<Test>& tests) {
for (const auto& test : tests_) {
tests.push_back(
{test.file, test.line, test.test_type, test.name,
[test = &test](const fuzzing_event_engine::Actions& actions,
absl::BitGenRef rng) {
return test->make(actions, rng);
}});
}
}
} // namespace yodel_detail
///////////////////////////////////////////////////////////////////////////////
// YodelTest::WatchDog
class YodelTest::WatchDog {
public:
explicit WatchDog(YodelTest* test) : test_(test) {}
~WatchDog() { test_->event_engine_->Cancel(timer_); }
private:
YodelTest* const test_;
grpc_event_engine::experimental::EventEngine::TaskHandle const timer_{
test_->event_engine_->RunAfter(Duration::Minutes(5),
[this]() { test_->Timeout(); })};
};
///////////////////////////////////////////////////////////////////////////////
// TransportTest
// YodelTest
void TransportTest::RunTest() {
YodelTest::YodelTest(const fuzzing_event_engine::Actions& actions,
absl::BitGenRef rng)
: rng_(rng),
event_engine_{
std::make_shared<grpc_event_engine::experimental::FuzzingEventEngine>(
[]() {
grpc_timer_manager_set_threading(false);
grpc_event_engine::experimental::FuzzingEventEngine::Options
options;
return options;
}(),
actions)},
call_arena_allocator_{MakeRefCounted<CallArenaAllocator>(
MakeResourceQuota("test-quota")
->memory_quota()
->CreateMemoryAllocator("test-allocator"),
1024)} {}
void YodelTest::RunTest() {
TestImpl();
EXPECT_EQ(pending_actions_.size(), 0)
<< "There are still pending actions: did you forget to call "
"WaitForAllPendingWork()?";
transport_pair_.client.reset();
transport_pair_.server.reset();
Shutdown();
event_engine_->TickUntilIdle();
event_engine_->UnsetGlobalHooks();
}
void TransportTest::SetServerCallDestination() {
transport_pair_.server->server_transport()->SetCallDestination(
server_call_destination_);
}
CallInitiator TransportTest::CreateCall(
ClientMetadataHandle client_initial_metadata) {
auto call = MakeCall(std::move(client_initial_metadata));
call.handler.SpawnInfallible(
"start-call", [this, handler = call.handler]() mutable {
transport_pair_.client->client_transport()->StartCall(
handler.V2HackToStartCallWithoutACallFilterStack());
return Empty{};
});
return std::move(call.initiator);
}
CallHandler TransportTest::TickUntilServerCall() {
void YodelTest::TickUntilTrue(absl::FunctionRef<bool()> poll) {
WatchDog watchdog(this);
for (;;) {
auto handler = server_call_destination_->PopHandler();
if (handler.has_value()) {
return std::move(*handler);
}
while (!poll()) {
event_engine_->Tick();
}
}
void TransportTest::WaitForAllPendingWork() {
void YodelTest::WaitForAllPendingWork() {
WatchDog watchdog(this);
while (!pending_actions_.empty()) {
if (pending_actions_.front()->IsDone()) {
@ -91,7 +175,7 @@ void TransportTest::WaitForAllPendingWork() {
}
}
void TransportTest::Timeout() {
void YodelTest::Timeout() {
std::vector<std::string> lines;
lines.emplace_back("Timeout waiting for pending actions to complete");
while (!pending_actions_.empty()) {
@ -99,7 +183,7 @@ void TransportTest::Timeout() {
pending_actions_.pop();
if (action->IsDone()) continue;
absl::string_view state_name =
transport_test_detail::ActionState::StateString(action->Get());
yodel_detail::ActionState::StateString(action->Get());
absl::string_view file_name = action->file();
auto pos = file_name.find_last_of('/');
if (pos != absl::string_view::npos) {
@ -112,8 +196,8 @@ void TransportTest::Timeout() {
Crash(absl::StrJoin(lines, "\n"));
}
std::string TransportTest::RandomString(int min_length, int max_length,
absl::string_view character_set) {
std::string YodelTest::RandomString(int min_length, int max_length,
absl::string_view character_set) {
std::string out;
int length = absl::LogUniform<int>(rng_, min_length, max_length + 1);
for (int i = 0; i < length; ++i) {
@ -123,7 +207,7 @@ std::string TransportTest::RandomString(int min_length, int max_length,
return out;
}
std::string TransportTest::RandomStringFrom(
std::string YodelTest::RandomStringFrom(
std::initializer_list<absl::string_view> choices) {
size_t idx = absl::Uniform<size_t>(rng_, 0, choices.size());
auto it = choices.begin();
@ -131,7 +215,7 @@ std::string TransportTest::RandomStringFrom(
return std::string(*it);
}
std::string TransportTest::RandomMetadataKey() {
std::string YodelTest::RandomMetadataKey() {
if (absl::Bernoulli(rng_, 0.1)) {
return RandomStringFrom({
":path",
@ -148,7 +232,7 @@ std::string TransportTest::RandomMetadataKey() {
return out;
}
std::string TransportTest::RandomMetadataValue(absl::string_view key) {
std::string YodelTest::RandomMetadataValue(absl::string_view key) {
if (key == ":method") {
return RandomStringFrom({"GET", "POST", "PUT"});
}
@ -169,11 +253,11 @@ std::string TransportTest::RandomMetadataValue(absl::string_view key) {
return RandomString(0, 128, *kChars);
}
std::string TransportTest::RandomMetadataBinaryKey() {
std::string YodelTest::RandomMetadataBinaryKey() {
return RandomString(1, 128, "abcdefghijklmnopqrstuvwxyz-_") + "-bin";
}
std::string TransportTest::RandomMetadataBinaryValue() {
std::string YodelTest::RandomMetadataBinaryValue() {
static const NoDestruct<std::string> kChars{[]() {
std::string out;
for (int c = 0; c < 256; c++) {
@ -184,8 +268,7 @@ std::string TransportTest::RandomMetadataBinaryValue() {
return RandomString(0, 4096, *kChars);
}
std::vector<std::pair<std::string, std::string>>
TransportTest::RandomMetadata() {
std::vector<std::pair<std::string, std::string>> YodelTest::RandomMetadata() {
size_t size = 0;
const size_t max_size = absl::LogUniform<size_t>(rng_, 64, 8000);
std::vector<std::pair<std::string, std::string>> out;
@ -218,7 +301,7 @@ TransportTest::RandomMetadata() {
return out;
}
std::string TransportTest::RandomMessage() {
std::string YodelTest::RandomMessage() {
static const NoDestruct<std::string> kChars{[]() {
std::string out;
for (int c = 0; c < 256; c++) {
@ -229,43 +312,4 @@ std::string TransportTest::RandomMessage() {
return RandomString(0, 1024 * 1024, *kChars);
}
///////////////////////////////////////////////////////////////////////////////
// TransportTest::ServerCallDestination
void TransportTest::ServerCallDestination::StartCall(
UnstartedCallHandler handler) {
handlers_.push(handler.V2HackToStartCallWithoutACallFilterStack());
}
absl::optional<CallHandler> TransportTest::ServerCallDestination::PopHandler() {
if (!handlers_.empty()) {
auto handler = std::move(handlers_.front());
handlers_.pop();
return handler;
}
return absl::nullopt;
}
///////////////////////////////////////////////////////////////////////////////
// ActionState
namespace transport_test_detail {
ActionState::ActionState(NameAndLocation name_and_location)
: name_and_location_(name_and_location), state_(kNotCreated) {}
bool ActionState::IsDone() {
switch (state_) {
case kNotCreated:
case kNotStarted:
case kStarted:
return false;
case kDone:
case kCancelled:
return true;
}
}
} // namespace transport_test_detail
} // namespace grpc_core

@ -0,0 +1,462 @@
// Copyright 2024 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_CALL_YODEL_YODEL_TEST_H
#define GRPC_TEST_CORE_CALL_YODEL_YODEL_TEST_H
#include "absl/functional/any_invocable.h"
#include "absl/log/log.h"
#include "absl/random/bit_gen_ref.h"
#include "absl/strings/string_view.h"
#include "gtest/gtest.h"
#include <grpc/event_engine/event_engine.h>
#include "src/core/lib/gprpp/debug_location.h"
#include "src/core/lib/promise/cancel_callback.h"
#include "src/core/lib/promise/detail/promise_factory.h"
#include "src/core/lib/promise/promise.h"
#include "src/core/lib/transport/call_arena_allocator.h"
#include "src/core/lib/transport/call_spine.h"
#include "src/core/lib/transport/metadata.h"
#include "test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.h"
#include "test/core/test_util/test_config.h"
namespace grpc_core {
class YodelTest;
namespace yodel_detail {
// Capture the name and location of a test step.
class NameAndLocation {
public:
// Allow implicit construction from a string, to capture the start location
// from the variadic StartTestSeq name argument.
// NOLINTNEXTLINE
NameAndLocation(const char* name, SourceLocation location = {})
: location_(location), name_(name) {}
SourceLocation location() const { return location_; }
absl::string_view name() const { return name_; }
private:
SourceLocation location_;
absl::string_view name_;
};
// Capture the state of a test step.
class ActionState {
public:
enum State : uint8_t {
// Initial state: construction of this step in the sequence has not been
// performed.
kNotCreated,
// The step has been created, but not yet started (the initial poll of the
// created promise has not occurred).
kNotStarted,
// The step has been polled, but it's not yet been completed.
kStarted,
// The step has been completed.
kDone,
// The step has been cancelled.
kCancelled,
};
// Generate a nice little prefix for log messages.
static absl::string_view StateString(State state);
ActionState(NameAndLocation name_and_location, int step);
State Get() const { return state_; }
void Set(State state, SourceLocation whence = {});
const NameAndLocation& name_and_location() const {
return name_and_location_;
}
SourceLocation location() const { return name_and_location().location(); }
const char* file() const { return location().file(); }
int line() const { return location().line(); }
absl::string_view name() const { return name_and_location().name(); }
int step() const { return step_; }
bool IsDone();
private:
const NameAndLocation name_and_location_;
const int step_;
std::atomic<State> state_;
};
class SequenceSpawner {
public:
SequenceSpawner(
NameAndLocation name_and_location,
absl::AnyInvocable<void(absl::string_view, Promise<Empty>)>
promise_spawner,
absl::FunctionRef<std::shared_ptr<ActionState>(NameAndLocation, int)>
action_state_factory)
: name_and_location_(name_and_location),
promise_spawner_(
std::make_shared<
absl::AnyInvocable<void(absl::string_view, Promise<Empty>)>>(
std::move(promise_spawner))),
action_state_factory_(action_state_factory) {}
template <typename First, typename... FollowUps>
void Start(First first, FollowUps... followups) {
using Factory = promise_detail::OncePromiseFactory<void, First>;
using FactoryPromise = typename Factory::Promise;
using Result = typename FactoryPromise::Result;
auto action_state = action_state_factory_(name_and_location_, step_);
++step_;
auto next = MakeNext<Result>(std::move(followups)...);
(*promise_spawner_)(
name_and_location_.name(),
[spawner = promise_spawner_, first = Factory(std::move(first)),
next = std::move(next), action_state = std::move(action_state),
name_and_location = name_and_location_]() mutable {
action_state->Set(ActionState::kNotStarted);
auto promise = first.Make();
(*spawner)(name_and_location.name(),
WrapPromiseAndNext(std::move(action_state),
std::move(promise), std::move(next)));
return Empty{};
});
}
private:
template <typename Arg, typename FirstFollowUp, typename... FollowUps>
absl::AnyInvocable<void(Arg)> MakeNext(FirstFollowUp first,
FollowUps... followups) {
using Factory = promise_detail::OncePromiseFactory<Arg, FirstFollowUp>;
using FactoryPromise = typename Factory::Promise;
using Result = typename FactoryPromise::Result;
auto action_state = action_state_factory_(name_and_location_, step_);
++step_;
auto next = MakeNext<Result>(std::move(followups)...);
return [spawner = promise_spawner_, factory = Factory(std::move(first)),
next = std::move(next), action_state = std::move(action_state),
name_and_location = name_and_location_](Arg arg) mutable {
action_state->Set(ActionState::kNotStarted);
(*spawner)(
name_and_location.name(),
WrapPromiseAndNext(std::move(action_state),
factory.Make(std::move(arg)), std::move(next)));
};
}
template <typename R, typename P>
static Promise<Empty> WrapPromiseAndNext(
std::shared_ptr<ActionState> action_state, P promise,
absl::AnyInvocable<void(R)> next) {
return Promise<Empty>(OnCancel(
[action_state, promise = std::move(promise),
next = std::move(next)]() mutable -> Poll<Empty> {
action_state->Set(ActionState::kStarted);
auto r = promise();
if (auto* p = r.value_if_ready()) {
action_state->Set(ActionState::kDone);
next(std::move(*p));
return Empty{};
} else {
return Pending{};
}
},
[action_state]() { action_state->Set(ActionState::kCancelled); }));
}
template <typename Arg>
absl::AnyInvocable<void(Arg)> MakeNext() {
// Enforce last-arg is Empty so we don't drop things
return [](Empty) {};
}
NameAndLocation name_and_location_;
std::shared_ptr<absl::AnyInvocable<void(absl::string_view, Promise<Empty>)>>
promise_spawner_;
absl::FunctionRef<std::shared_ptr<ActionState>(NameAndLocation, int)>
action_state_factory_;
int step_ = 1;
};
template <typename Context>
auto SpawnerForContext(
Context context,
grpc_event_engine::experimental::EventEngine* event_engine) {
return [context = std::move(context), event_engine](
absl::string_view name, Promise<Empty> promise) mutable {
// Pass new promises via event engine to allow fuzzers to explore
// reorderings of possibly interleaved spawns.
event_engine->Run([name, context, promise = std::move(promise)]() mutable {
context.SpawnInfallible(name, std::move(promise));
});
};
}
class TestRegistry {
public:
TestRegistry() : next_(root_) { root_ = this; }
struct Test {
absl::string_view file;
int line;
std::string test_type;
std::string name;
absl::AnyInvocable<YodelTest*(const fuzzing_event_engine::Actions&,
absl::BitGenRef) const>
make;
};
static std::vector<Test> AllTests();
protected:
~TestRegistry() {
Crash("unreachable: TestRegistry should never be destroyed");
}
private:
virtual void ContributeTests(std::vector<Test>& tests) = 0;
TestRegistry* next_;
static TestRegistry* root_;
};
class SimpleTestRegistry final : public TestRegistry {
public:
SimpleTestRegistry() {}
~SimpleTestRegistry() = delete;
void RegisterTest(
absl::string_view file, int line, absl::string_view test_type,
absl::string_view name,
absl::AnyInvocable<YodelTest*(const fuzzing_event_engine::Actions&,
absl::BitGenRef) const>
create);
static SimpleTestRegistry& Get() {
static SimpleTestRegistry* const p = new SimpleTestRegistry;
return *p;
}
private:
void ContributeTests(std::vector<Test>& tests) override;
std::vector<Test> tests_;
};
template <typename /*test_type*/, typename T>
class ParameterizedTestRegistry final : public TestRegistry {
public:
ParameterizedTestRegistry() {}
~ParameterizedTestRegistry() = delete;
void RegisterTest(absl::string_view file, int line,
absl::string_view test_type, absl::string_view name,
absl::AnyInvocable<YodelTest*(
const T&, const fuzzing_event_engine::Actions&,
absl::BitGenRef) const>
make) {
tests_.push_back({file, line, test_type, name, std::move(make)});
}
void RegisterParameter(absl::string_view name, T value) {
parameters_.push_back({name, std::move(value)});
}
static ParameterizedTestRegistry& Get() {
static ParameterizedTestRegistry* const p = new ParameterizedTestRegistry;
return *p;
}
private:
struct ParameterizedTest {
absl::string_view file;
int line;
absl::string_view test_type;
absl::string_view name;
absl::AnyInvocable<YodelTest*(
const T&, const fuzzing_event_engine::Actions&, absl::BitGenRef) const>
make;
};
struct Parameter {
absl::string_view name;
T value;
};
void ContributeTests(std::vector<Test>& tests) override {
for (const auto& test : tests_) {
for (const auto& parameter : parameters_) {
tests.push_back({test.file, test.line, std::string(test.test_type),
absl::StrCat(test.name, "/", parameter.name),
[test = &test, parameter = &parameter](
const fuzzing_event_engine::Actions& actions,
absl::BitGenRef rng) {
return test->make(parameter->value, actions, rng);
}});
}
}
}
std::vector<ParameterizedTest> tests_;
std::vector<Parameter> parameters_;
};
} // namespace yodel_detail
class YodelTest : public ::testing::Test {
public:
void RunTest();
protected:
YodelTest(const fuzzing_event_engine::Actions& actions, absl::BitGenRef rng);
// Helpers to generate various random values.
// When we're fuzzing, delegates to the fuzzer input to generate this data.
std::string RandomString(int min_length, int max_length,
absl::string_view character_set);
std::string RandomStringFrom(
std::initializer_list<absl::string_view> choices);
std::string RandomMetadataKey();
std::string RandomMetadataValue(absl::string_view key);
std::string RandomMetadataBinaryKey();
std::string RandomMetadataBinaryValue();
std::vector<std::pair<std::string, std::string>> RandomMetadata();
std::string RandomMessage();
absl::BitGenRef rng() { return rng_; }
// Alternative for Seq for test driver code.
// Registers each step so that WaitForAllPendingWork() can report progress,
// and wait for completion... AND generate good failure messages when a
// sequence doesn't complete in a timely manner.
// Uses the `SpawnInfallible` method on `context` to provide an execution
// environment for each step.
// Initiates each step in a different event engine closure to maximize
// opportunities for fuzzers to reorder the steps, or thready-tsan to expose
// potential threading issues.
template <typename Context, typename... Actions>
void SpawnTestSeq(Context context,
yodel_detail::NameAndLocation name_and_location,
Actions... actions) {
yodel_detail::SequenceSpawner(
name_and_location,
yodel_detail::SpawnerForContext(std::move(context),
event_engine_.get()),
[this](yodel_detail::NameAndLocation name_and_location, int step) {
auto action = std::make_shared<yodel_detail::ActionState>(
name_and_location, step);
pending_actions_.push(action);
return action;
})
.Start(std::move(actions)...);
}
auto MakeCall(ClientMetadataHandle client_initial_metadata) {
auto* arena = call_arena_allocator_->MakeArena();
return MakeCallPair(std::move(client_initial_metadata), event_engine_.get(),
arena, call_arena_allocator_, nullptr);
}
void WaitForAllPendingWork();
template <typename T>
T TickUntil(absl::FunctionRef<Poll<T>()> poll) {
absl::optional<T> result;
TickUntilTrue([poll, &result]() {
auto r = poll();
if (auto* p = r.value_if_ready()) {
result = std::move(*p);
return true;
}
return false;
});
return std::move(*result);
}
const std::shared_ptr<grpc_event_engine::experimental::FuzzingEventEngine>&
event_engine() {
return event_engine_;
}
private:
class WatchDog;
virtual void TestImpl() = 0;
void Timeout();
void TickUntilTrue(absl::FunctionRef<bool()> poll);
// Called after the test has run, but before the event engine is shut down.
virtual void Shutdown() {}
grpc::testing::TestGrpcScope grpc_scope_;
absl::BitGenRef rng_;
const std::shared_ptr<grpc_event_engine::experimental::FuzzingEventEngine>
event_engine_;
const RefCountedPtr<CallArenaAllocator> call_arena_allocator_;
std::queue<std::shared_ptr<yodel_detail::ActionState>> pending_actions_;
};
} // namespace grpc_core
#define YODEL_TEST(test_type, name) \
class YodelTest_##name : public grpc_core::test_type { \
public: \
using test_type::test_type; \
void TestBody() override { RunTest(); } \
\
private: \
void TestImpl() override; \
static grpc_core::YodelTest* Create( \
const fuzzing_event_engine::Actions& actions, absl::BitGenRef rng) { \
return new YodelTest_##name(actions, rng); \
} \
static int registered_; \
}; \
int YodelTest_##name::registered_ = \
(grpc_core::yodel_detail::SimpleTestRegistry::Get().RegisterTest( \
__FILE__, __LINE__, #test_type, #name, &Create), \
0); \
void YodelTest_##name::TestImpl()
// NOLINTBEGIN(bugprone-macro-parentheses)
#define YODEL_TEST_P(test_type, parameter_type, name) \
class YodelTest_##name : public grpc_core::test_type { \
public: \
using test_type::test_type; \
void TestBody() override { RunTest(); } \
\
private: \
void TestImpl() override; \
static grpc_core::YodelTest* Create( \
const parameter_type& parameter, \
const fuzzing_event_engine::Actions& actions, absl::BitGenRef rng) { \
return new YodelTest_##name(parameter, actions, rng); \
} \
static int registered_; \
}; \
int YodelTest_##name::registered_ = \
(grpc_core::yodel_detail::ParameterizedTestRegistry< \
grpc_core::test_type, parameter_type>::Get() \
.RegisterTest(__FILE__, __LINE__, #test_type, #name, &Create), \
0); \
void YodelTest_##name::TestImpl()
#define YODEL_TEST_PARAM(test_type, parameter_type, name, value) \
int YodelTestParam_##name = \
(grpc_core::yodel_detail::ParameterizedTestRegistry< \
grpc_core::test_type, parameter_type>::Get() \
.RegisterParameter(#name, value), \
0)
// NOLINTEND(bugprone-macro-parentheses)
#endif // GRPC_TEST_CORE_CALL_YODEL_YODEL_TEST_H

@ -579,7 +579,7 @@ EventEngine::TaskHandle FuzzingEventEngine::RunAfterLocked(
}
if (trace_timers.enabled()) {
gpr_log(GPR_INFO,
"Schedule timer %" PRIx64 " @ %" PRIu64 " (now=%" PRIu64
"Schedule timer %" PRIxPTR " @ %" PRIu64 " (now=%" PRIu64
"; delay=%" PRIu64 "; fuzzing_added=%" PRIu64 "; type=%d)",
id, static_cast<uint64_t>(final_time.time_since_epoch().count()),
now.time_since_epoch().count(), when.count(), delay_taken.count(),
@ -600,7 +600,7 @@ bool FuzzingEventEngine::Cancel(TaskHandle handle) {
return false;
}
if (trace_timers.enabled()) {
gpr_log(GPR_INFO, "Cancel timer %" PRIx64, id);
gpr_log(GPR_INFO, "Cancel timer %" PRIxPTR, id);
}
it->second->closure = nullptr;
return true;

@ -16,29 +16,17 @@ load(
"//bazel:grpc_build_system.bzl",
"grpc_cc_library",
"grpc_package",
"grpc_proto_library",
)
load("grpc_transport_test.bzl", "grpc_transport_test")
grpc_package(name = "test/core/transport/test_suite")
grpc_cc_library(
name = "fixture",
testonly = 1,
srcs = ["fixture.cc"],
hdrs = ["fixture.h"],
deps = [
"//:grpc",
"//test/core/event_engine/fuzzing_event_engine",
],
)
grpc_cc_library(
name = "inproc_fixture",
testonly = 1,
srcs = ["inproc_fixture.cc"],
deps = [
"fixture",
"test",
"//src/core:grpc_transport_inproc",
],
alwayslink = 1,
@ -53,7 +41,7 @@ grpc_cc_library(
"gtest",
],
deps = [
"fixture",
"test",
"//src/core:chaotic_good_client_transport",
"//src/core:chaotic_good_server_transport",
"//src/core:event_engine_memory_allocator_factory",
@ -67,38 +55,16 @@ grpc_cc_library(
grpc_cc_library(
name = "test",
testonly = 1,
srcs = ["test.cc"],
hdrs = ["test.h"],
srcs = ["transport_test.cc"],
hdrs = ["transport_test.h"],
external_deps = [
"absl/functional:any_invocable",
"absl/log:log",
"absl/random",
"absl/random:bit_gen_ref",
"absl/strings",
"gtest",
],
deps = [
"fixture",
"//:iomgr_timer",
"//:promise",
"//src/core:cancel_callback",
"//src/core:resource_quota",
"//src/core:time",
"//test/core/event_engine/fuzzing_event_engine",
"//test/core/event_engine/fuzzing_event_engine:fuzzing_event_engine_proto",
],
)
grpc_cc_library(
name = "test_main",
testonly = 1,
srcs = ["test_main.cc"],
external_deps = ["absl/random"],
deps = [
"fixture",
"test",
"//:grpc_trace",
"//test/core/test_util:grpc_test_util",
"//test/core/call/yodel:yodel_test",
],
)
@ -136,16 +102,6 @@ grpc_cc_library(
alwayslink = 1,
)
grpc_proto_library(
name = "fuzzer_proto",
srcs = ["fuzzer.proto"],
has_services = False,
deps = [
"//test/core/event_engine/fuzzing_event_engine:fuzzing_event_engine_proto",
"//test/core/test_util:fuzz_config_vars_proto",
],
)
grpc_transport_test(
name = "inproc",
deps = [

@ -14,7 +14,7 @@
#include "gmock/gmock.h"
#include "test/core/transport/test_suite/test.h"
#include "test/core/transport/test_suite/transport_test.h"
using testing::UnorderedElementsAreArray;

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#include "test/core/transport/test_suite/test.h"
#include "test/core/transport/test_suite/transport_test.h"
namespace grpc_core {

@ -23,7 +23,7 @@
#include "src/core/lib/event_engine/tcp_socket_utils.h"
#include "src/core/lib/resource_quota/resource_quota.h"
#include "src/core/lib/transport/promise_endpoint.h"
#include "test/core/transport/test_suite/fixture.h"
#include "test/core/transport/test_suite/transport_test.h"
using grpc_event_engine::experimental::EndpointConfig;
using grpc_event_engine::experimental::EventEngine;

@ -1,29 +0,0 @@
// 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/transport/test_suite/fixture.h"
namespace grpc_core {
TransportFixtureRegistry& TransportFixtureRegistry::Get() {
static TransportFixtureRegistry* registry = new TransportFixtureRegistry();
return *registry;
}
void TransportFixtureRegistry::RegisterFixture(
absl::string_view name,
absl::AnyInvocable<TransportFixture*() const> create) {
fixtures_.push_back({name, std::move(create)});
}
} // namespace grpc_core

@ -1,77 +0,0 @@
// 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_TRANSPORT_TEST_SUITE_FIXTURE_H
#define GRPC_TEST_CORE_TRANSPORT_TEST_SUITE_FIXTURE_H
#include "src/core/lib/transport/transport.h"
#include "test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.h"
namespace grpc_core {
class TransportFixture {
public:
struct ClientAndServerTransportPair {
OrphanablePtr<Transport> client;
OrphanablePtr<Transport> server;
};
virtual ~TransportFixture() = default;
virtual ClientAndServerTransportPair CreateTransportPair(
std::shared_ptr<grpc_event_engine::experimental::FuzzingEventEngine>
event_engine) = 0;
};
class TransportFixtureRegistry {
public:
static TransportFixtureRegistry& Get();
void RegisterFixture(absl::string_view name,
absl::AnyInvocable<TransportFixture*() const> create);
struct Fixture {
absl::string_view name;
absl::AnyInvocable<TransportFixture*() const> create;
};
const std::vector<Fixture>& fixtures() const { return fixtures_; }
private:
std::vector<Fixture> fixtures_;
};
} // namespace grpc_core
#define TRANSPORT_FIXTURE(name) \
class TransportFixture_##name : public grpc_core::TransportFixture { \
public: \
using TransportFixture::TransportFixture; \
ClientAndServerTransportPair CreateTransportPair( \
std::shared_ptr<grpc_event_engine::experimental::FuzzingEventEngine> \
event_engine) override; \
\
private: \
static grpc_core::TransportFixture* Create() { \
return new TransportFixture_##name(); \
} \
static int registered_; \
}; \
int TransportFixture_##name::registered_ = \
(grpc_core::TransportFixtureRegistry::Get().RegisterFixture( \
#name, &TransportFixture_##name::Create), \
0); \
grpc_core::TransportFixture::ClientAndServerTransportPair \
TransportFixture_##name::CreateTransportPair( \
std::shared_ptr<grpc_event_engine::experimental::FuzzingEventEngine> \
event_engine GRPC_UNUSED)
#endif // GRPC_TEST_CORE_TRANSPORT_TEST_SUITE_FIXTURE_H

@ -16,45 +16,7 @@
Generate one transport test & associated fuzzer
"""
load("//bazel:grpc_build_system.bzl", "grpc_cc_test")
load("//test/core/test_util:grpc_fuzzer.bzl", "grpc_proto_fuzzer")
load("//test/core/call/yodel:grpc_yodel_test.bzl", "grpc_yodel_test")
def grpc_transport_test(name, deps):
grpc_cc_test(
name = name + "_test",
srcs = [],
tags = ["no_windows", "no_mac"],
deps = [
":test_main",
] + deps,
uses_polling = False,
)
grpc_proto_fuzzer(
name = name + "_fuzzer",
srcs = ["fuzzer_main.cc"],
tags = ["no_windows", "no_mac"],
external_deps = [
"absl/log:check",
"gtest",
],
deps = [
":test",
":fixture",
":fuzzer_proto",
"//:event_engine_base_hdrs",
"//:config_vars",
"//:exec_ctx",
"//:gpr",
"//:grpc_unsecure",
"//:iomgr_timer",
"//src/core:default_event_engine",
"//src/core:env",
"//src/core:experiments",
"//test/core/event_engine/fuzzing_event_engine",
"//test/core/test_util:fuzz_config_vars",
"//test/core/test_util:proto_bit_gen",
] + deps,
corpus = "corpus/%s" % name,
proto = None,
)
grpc_yodel_test(name, deps)

@ -14,7 +14,7 @@
#include "src/core/ext/transport/inproc/inproc_transport.h"
#include "src/core/lib/config/core_configuration.h"
#include "test/core/transport/test_suite/fixture.h"
#include "test/core/transport/test_suite/transport_test.h"
namespace grpc_core {

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#include "test/core/transport/test_suite/test.h"
#include "test/core/transport/test_suite/transport_test.h"
namespace grpc_core {

@ -14,7 +14,7 @@
#include "absl/random/random.h"
#include "test/core/transport/test_suite/test.h"
#include "test/core/transport/test_suite/transport_test.h"
namespace grpc_core {

@ -1,368 +0,0 @@
// 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_TRANSPORT_TEST_SUITE_TEST_H
#define GRPC_TEST_CORE_TRANSPORT_TEST_SUITE_TEST_H
#include <initializer_list>
#include <memory>
#include <queue>
#include "absl/functional/any_invocable.h"
#include "absl/log/log.h"
#include "absl/random/bit_gen_ref.h"
#include "absl/strings/string_view.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "src/core/lib/gprpp/time.h"
#include "src/core/lib/iomgr/timer_manager.h"
#include "src/core/lib/promise/cancel_callback.h"
#include "src/core/lib/promise/promise.h"
#include "src/core/lib/resource_quota/resource_quota.h"
#include "test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.h"
#include "test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.pb.h"
#include "test/core/test_util/test_config.h"
#include "test/core/transport/test_suite/fixture.h"
namespace grpc_core {
namespace transport_test_detail {
struct NameAndLocation {
// NOLINTNEXTLINE
NameAndLocation(const char* name, SourceLocation location = {})
: location_(location), name_(name) {}
NameAndLocation Next() const {
return NameAndLocation(name_, location_, step_ + 1);
}
SourceLocation location() const { return location_; }
absl::string_view name() const { return name_; }
int step() const { return step_; }
private:
NameAndLocation(absl::string_view name, SourceLocation location, int step)
: location_(location), name_(name), step_(step) {}
SourceLocation location_;
absl::string_view name_;
int step_ = 1;
};
class ActionState {
public:
enum State : uint8_t {
kNotCreated,
kNotStarted,
kStarted,
kDone,
kCancelled,
};
static absl::string_view StateString(State state) {
switch (state) {
case kNotCreated:
return "🚦";
case kNotStarted:
return "";
case kStarted:
return "🚗";
case kDone:
return "🏁";
case kCancelled:
return "💥";
}
}
explicit ActionState(NameAndLocation name_and_location);
State Get() const { return state_; }
void Set(State state, SourceLocation whence = {}) {
LOG(INFO) << StateString(state) << " " << name() << " [" << step() << "] "
<< file() << ":" << line() << " @ " << whence.file() << ":"
<< whence.line();
state_ = state;
}
const NameAndLocation& name_and_location() const {
return name_and_location_;
}
SourceLocation location() const { return name_and_location().location(); }
const char* file() const { return location().file(); }
int line() const { return location().line(); }
absl::string_view name() const { return name_and_location().name(); }
int step() const { return name_and_location().step(); }
bool IsDone();
private:
const NameAndLocation name_and_location_;
std::atomic<State> state_;
};
using PromiseSpawner = std::function<void(absl::string_view, Promise<Empty>)>;
using ActionStateFactory =
absl::FunctionRef<std::shared_ptr<ActionState>(NameAndLocation)>;
template <typename Context>
PromiseSpawner SpawnerForContext(
Context context,
grpc_event_engine::experimental::EventEngine* event_engine) {
return [context = std::move(context), event_engine](
absl::string_view name, Promise<Empty> promise) mutable {
// Pass new promises via event engine to allow fuzzers to explore
// reorderings of possibly interleaved spawns.
event_engine->Run([name, context = std::move(context),
promise = std::move(promise)]() mutable {
context.SpawnInfallible(name, std::move(promise));
});
};
}
template <typename Arg>
using NextSpawner = absl::AnyInvocable<void(Arg)>;
template <typename R, typename P>
Promise<Empty> WrapPromiseAndNext(std::shared_ptr<ActionState> action_state,
P promise, NextSpawner<R> next) {
return Promise<Empty>(OnCancel(
[action_state, promise = std::move(promise),
next = std::move(next)]() mutable -> Poll<Empty> {
action_state->Set(ActionState::kStarted);
auto r = promise();
if (auto* p = r.value_if_ready()) {
action_state->Set(ActionState::kDone);
next(std::move(*p));
return Empty{};
} else {
return Pending{};
}
},
[action_state]() { action_state->Set(ActionState::kCancelled); }));
}
template <typename Arg>
NextSpawner<Arg> WrapFollowUps(NameAndLocation, ActionStateFactory,
PromiseSpawner) {
return [](Empty) {};
}
template <typename Arg, typename FirstFollowUp, typename... FollowUps>
NextSpawner<Arg> WrapFollowUps(NameAndLocation loc,
ActionStateFactory action_state_factory,
PromiseSpawner spawner, FirstFollowUp first,
FollowUps... follow_ups) {
using Factory = promise_detail::OncePromiseFactory<Arg, FirstFollowUp>;
using FactoryPromise = typename Factory::Promise;
using Result = typename FactoryPromise::Result;
auto action_state = action_state_factory(loc);
return [spawner, factory = Factory(std::move(first)),
next = WrapFollowUps<Result>(loc.Next(), action_state_factory,
spawner, std::move(follow_ups)...),
action_state = std::move(action_state),
name = loc.name()](Arg arg) mutable {
action_state->Set(ActionState::kNotStarted);
spawner(name,
WrapPromiseAndNext(std::move(action_state),
factory.Make(std::move(arg)), std::move(next)));
};
}
template <typename First, typename... FollowUps>
void StartSeq(NameAndLocation loc, ActionStateFactory action_state_factory,
PromiseSpawner spawner, First first, FollowUps... followups) {
using Factory = promise_detail::OncePromiseFactory<void, First>;
using FactoryPromise = typename Factory::Promise;
using Result = typename FactoryPromise::Result;
auto action_state = action_state_factory(loc);
auto next = WrapFollowUps<Result>(loc.Next(), action_state_factory, spawner,
std::move(followups)...);
spawner(
loc.name(),
[spawner, first = Factory(std::move(first)), next = std::move(next),
action_state = std::move(action_state), name = loc.name()]() mutable {
action_state->Set(ActionState::kNotStarted);
auto promise = first.Make();
spawner(name, WrapPromiseAndNext(std::move(action_state),
std::move(promise), std::move(next)));
return Empty{};
});
}
}; // namespace transport_test_detail
class TransportTest : public ::testing::Test {
public:
void RunTest();
protected:
TransportTest(std::unique_ptr<TransportFixture> fixture,
const fuzzing_event_engine::Actions& actions,
absl::BitGenRef rng)
: event_engine_(std::make_shared<
grpc_event_engine::experimental::FuzzingEventEngine>(
[]() {
grpc_timer_manager_set_threading(false);
grpc_event_engine::experimental::FuzzingEventEngine::Options
options;
return options;
}(),
actions)),
fixture_(std::move(fixture)),
rng_(rng) {}
void SetServerCallDestination();
CallInitiator CreateCall(ClientMetadataHandle client_initial_metadata);
std::string RandomString(int min_length, int max_length,
absl::string_view character_set);
std::string RandomStringFrom(
std::initializer_list<absl::string_view> choices);
std::string RandomMetadataKey();
std::string RandomMetadataValue(absl::string_view key);
std::string RandomMetadataBinaryKey();
std::string RandomMetadataBinaryValue();
std::vector<std::pair<std::string, std::string>> RandomMetadata();
std::string RandomMessage();
absl::BitGenRef rng() { return rng_; }
CallHandler TickUntilServerCall();
void WaitForAllPendingWork();
auto MakeCall(ClientMetadataHandle client_initial_metadata) {
auto* arena = call_arena_allocator_->MakeArena();
return MakeCallPair(std::move(client_initial_metadata), event_engine_.get(),
arena, call_arena_allocator_, nullptr);
}
// Alternative for Seq for test driver code.
// Registers each step so that WaitForAllPendingWork() can report progress,
// and wait for completion... AND generate good failure messages when a
// sequence doesn't complete in a timely manner.
template <typename Context, typename... Actions>
void SpawnTestSeq(Context context,
transport_test_detail::NameAndLocation name_and_location,
Actions... actions) {
transport_test_detail::StartSeq(
name_and_location,
[this](transport_test_detail::NameAndLocation name_and_location) {
auto action = std::make_shared<transport_test_detail::ActionState>(
name_and_location);
pending_actions_.push(action);
return action;
},
transport_test_detail::SpawnerForContext(std::move(context),
event_engine_.get()),
std::move(actions)...);
}
private:
virtual void TestImpl() = 0;
void Timeout();
class ServerCallDestination final : public UnstartedCallDestination {
public:
void StartCall(UnstartedCallHandler unstarted_call_handler) override;
void Orphaned() override {}
absl::optional<CallHandler> PopHandler();
private:
std::queue<CallHandler> handlers_;
};
class WatchDog {
public:
explicit WatchDog(TransportTest* test) : test_(test) {}
~WatchDog() { test_->event_engine_->Cancel(timer_); }
private:
TransportTest* const test_;
grpc_event_engine::experimental::EventEngine::TaskHandle const timer_{
test_->event_engine_->RunAfter(Duration::Minutes(5),
[this]() { test_->Timeout(); })};
};
grpc::testing::TestGrpcScope grpc_scope_;
std::shared_ptr<grpc_event_engine::experimental::FuzzingEventEngine>
event_engine_{
std::make_shared<grpc_event_engine::experimental::FuzzingEventEngine>(
[]() {
grpc_timer_manager_set_threading(false);
grpc_event_engine::experimental::FuzzingEventEngine::Options
options;
return options;
}(),
fuzzing_event_engine::Actions())};
std::unique_ptr<TransportFixture> fixture_;
RefCountedPtr<CallArenaAllocator> call_arena_allocator_{
MakeRefCounted<CallArenaAllocator>(
MakeResourceQuota("test-quota")
->memory_quota()
->CreateMemoryAllocator("test-allocator"),
1024)};
RefCountedPtr<ServerCallDestination> server_call_destination_ =
MakeRefCounted<ServerCallDestination>();
TransportFixture::ClientAndServerTransportPair transport_pair_ =
fixture_->CreateTransportPair(event_engine_);
std::queue<std::shared_ptr<transport_test_detail::ActionState>>
pending_actions_;
absl::BitGenRef rng_;
};
class TransportTestRegistry {
public:
static TransportTestRegistry& Get();
void RegisterTest(
absl::string_view name,
absl::AnyInvocable<TransportTest*(std::unique_ptr<TransportFixture>,
const fuzzing_event_engine::Actions&,
absl::BitGenRef) const>
create);
struct Test {
absl::string_view name;
absl::AnyInvocable<TransportTest*(std::unique_ptr<TransportFixture>,
const fuzzing_event_engine::Actions&,
absl::BitGenRef) const>
create;
};
const std::vector<Test>& tests() const { return tests_; }
private:
std::vector<Test> tests_;
};
} // namespace grpc_core
#define TRANSPORT_TEST(name) \
class TransportTest_##name : public grpc_core::TransportTest { \
public: \
using TransportTest::TransportTest; \
void TestBody() override { RunTest(); } \
\
private: \
void TestImpl() override; \
static grpc_core::TransportTest* Create( \
std::unique_ptr<grpc_core::TransportFixture> fixture, \
const fuzzing_event_engine::Actions& actions, absl::BitGenRef rng) { \
return new TransportTest_##name(std::move(fixture), actions, rng); \
} \
static int registered_; \
}; \
int TransportTest_##name::registered_ = \
(grpc_core::TransportTestRegistry::Get().RegisterTest(#name, &Create), \
0); \
void TransportTest_##name::TestImpl()
#endif // GRPC_TEST_CORE_TRANSPORT_TEST_SUITE_TEST_H

@ -0,0 +1,69 @@
// 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/transport/test_suite/transport_test.h"
#include <initializer_list>
#include "absl/random/random.h"
namespace grpc_core {
///////////////////////////////////////////////////////////////////////////////
// TransportTest
void TransportTest::SetServerCallDestination() {
transport_pair_.server->server_transport()->SetCallDestination(
server_call_destination_);
}
CallInitiator TransportTest::CreateCall(
ClientMetadataHandle client_initial_metadata) {
auto call = MakeCall(std::move(client_initial_metadata));
call.handler.SpawnInfallible(
"start-call", [this, handler = call.handler]() mutable {
transport_pair_.client->client_transport()->StartCall(
handler.V2HackToStartCallWithoutACallFilterStack());
return Empty{};
});
return std::move(call.initiator);
}
CallHandler TransportTest::TickUntilServerCall() {
auto poll = [this]() -> Poll<CallHandler> {
auto handler = server_call_destination_->PopHandler();
if (handler.has_value()) return std::move(*handler);
return Pending();
};
return TickUntil(absl::FunctionRef<Poll<CallHandler>()>(poll));
}
///////////////////////////////////////////////////////////////////////////////
// TransportTest::ServerCallDestination
void TransportTest::ServerCallDestination::StartCall(
UnstartedCallHandler handler) {
handlers_.push(handler.V2HackToStartCallWithoutACallFilterStack());
}
absl::optional<CallHandler> TransportTest::ServerCallDestination::PopHandler() {
if (!handlers_.empty()) {
auto handler = std::move(handlers_.front());
handlers_.pop();
return handler;
}
return absl::nullopt;
}
} // namespace grpc_core

@ -0,0 +1,87 @@
// 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_TRANSPORT_TEST_SUITE_TRANSPORT_TEST_H
#define GRPC_TEST_CORE_TRANSPORT_TEST_SUITE_TRANSPORT_TEST_H
#include <memory>
#include <queue>
#include "absl/random/bit_gen_ref.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "src/core/lib/transport/transport.h"
#include "test/core/call/yodel/yodel_test.h"
#include "test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.h"
namespace grpc_core {
struct ClientAndServerTransportPair {
OrphanablePtr<Transport> client;
OrphanablePtr<Transport> server;
};
using TransportFixture = absl::AnyInvocable<ClientAndServerTransportPair(
std::shared_ptr<grpc_event_engine::experimental::FuzzingEventEngine>)
const>;
class TransportTest : public YodelTest {
protected:
TransportTest(const TransportFixture& fixture,
const fuzzing_event_engine::Actions& actions,
absl::BitGenRef rng)
: YodelTest(actions, rng), transport_pair_(fixture(event_engine())) {}
void SetServerCallDestination();
CallInitiator CreateCall(ClientMetadataHandle client_initial_metadata);
CallHandler TickUntilServerCall();
private:
class ServerCallDestination final : public UnstartedCallDestination {
public:
void StartCall(UnstartedCallHandler unstarted_call_handler) override;
void Orphaned() override {}
absl::optional<CallHandler> PopHandler();
private:
std::queue<CallHandler> handlers_;
};
void Shutdown() override {
transport_pair_.client.reset();
transport_pair_.server.reset();
}
RefCountedPtr<ServerCallDestination> server_call_destination_ =
MakeRefCounted<ServerCallDestination>();
ClientAndServerTransportPair transport_pair_;
};
} // namespace grpc_core
#define TRANSPORT_TEST(name) YODEL_TEST_P(TransportTest, TransportFixture, name)
#define TRANSPORT_FIXTURE(name) \
static grpc_core::ClientAndServerTransportPair name( \
std::shared_ptr<grpc_event_engine::experimental::FuzzingEventEngine> \
event_engine); \
YODEL_TEST_PARAM(TransportTest, TransportFixture, name, name); \
static grpc_core::ClientAndServerTransportPair name( \
GRPC_UNUSED \
std::shared_ptr<grpc_event_engine::experimental::FuzzingEventEngine> \
event_engine)
#endif // GRPC_TEST_CORE_TRANSPORT_TEST_SUITE_TRANSPORT_TEST_H

@ -65,6 +65,7 @@ EXTERNAL_DEPS = {
"absl/functional/function_ref.h": "absl/functional:function_ref",
"absl/hash/hash.h": "absl/hash",
"absl/log/check.h": "absl/log:check",
"absl/log/log.h": "absl/log",
"absl/memory/memory.h": "absl/memory",
"absl/meta/type_traits.h": "absl/meta:type_traits",
"absl/numeric/int128.h": "absl/numeric:int128",
@ -261,40 +262,44 @@ def grpc_cc_library(
global num_opted_out_cc_libraries
global parsing_path
assert parsing_path is not None
name = "//%s:%s" % (parsing_path, name)
num_cc_libraries += 1
if select_deps or "nofixdeps" in tags:
if args.whats_left and not select_deps and "nofixdeps" not in tags:
num_opted_out_cc_libraries += 1
print("Not opted in: {}".format(name))
no_update.add(name)
scores[name] = len(public_hdrs + hdrs)
# avoid_dep is the internal way of saying prefer something else
# we add grpc_avoid_dep to allow internal grpc-only stuff to avoid each
# other, whilst not biasing dependent projects
if "avoid_dep" in tags or "grpc_avoid_dep" in tags:
avoidness[name] += 10
if proto:
proto_hdr = "%s%s" % (
(parsing_path + "/" if parsing_path else ""),
proto.replace(".proto", ".pb.h"),
)
skip_headers[name].add(proto_hdr)
for hdr in hdrs + public_hdrs:
vendors[_get_filename(hdr, parsing_path)].append(name)
inc = set()
original_deps[name] = frozenset(deps)
original_external_deps[name] = frozenset(external_deps)
for src in hdrs + public_hdrs + srcs:
for line in open(_get_filename(src, parsing_path)):
m = re.search(r"^#include <(.*)>", line)
if m:
inc.add(m.group(1))
m = re.search(r'^#include "(.*)"', line)
if m:
inc.add(m.group(1))
consumes[name] = list(inc)
try:
name = "//%s:%s" % (parsing_path, name)
num_cc_libraries += 1
if select_deps or "nofixdeps" in tags:
if args.whats_left and not select_deps and "nofixdeps" not in tags:
num_opted_out_cc_libraries += 1
print("Not opted in: {}".format(name))
no_update.add(name)
scores[name] = len(public_hdrs + hdrs)
# avoid_dep is the internal way of saying prefer something else
# we add grpc_avoid_dep to allow internal grpc-only stuff to avoid each
# other, whilst not biasing dependent projects
if "avoid_dep" in tags or "grpc_avoid_dep" in tags:
avoidness[name] += 10
if proto:
proto_hdr = "%s%s" % (
(parsing_path + "/" if parsing_path else ""),
proto.replace(".proto", ".pb.h"),
)
skip_headers[name].add(proto_hdr)
for hdr in hdrs + public_hdrs:
vendors[_get_filename(hdr, parsing_path)].append(name)
inc = set()
original_deps[name] = frozenset(deps)
original_external_deps[name] = frozenset(external_deps)
for src in hdrs + public_hdrs + srcs:
for line in open(_get_filename(src, parsing_path)):
m = re.search(r"^#include <(.*)>", line)
if m:
inc.add(m.group(1))
m = re.search(r'^#include "(.*)"', line)
if m:
inc.add(m.group(1))
consumes[name] = list(inc)
except:
print("Error while parsing ", name)
raise
def grpc_proto_library(name, srcs, **kwargs):
@ -394,6 +399,7 @@ for dirname in [
"src/cpp/ext/csm",
"src/cpp/ext/otel",
"test/core/backoff",
"test/core/call/yodel",
"test/core/experiments",
"test/core/uri",
"test/core/test_util",

Loading…
Cancel
Save