[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
parent
aa83a3fe32
commit
621aa4e5ce
26 changed files with 1004 additions and 752 deletions
@ -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 |
@ -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, |
||||||
|
) |
@ -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 = ¶meter]( |
||||||
|
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
|
@ -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
|
|
@ -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
|
Loading…
Reference in new issue