diff --git a/coroutines/.clang-format b/coroutines/.clang-format new file mode 100644 index 000000000..020727d9c --- /dev/null +++ b/coroutines/.clang-format @@ -0,0 +1,168 @@ +--- +Language: Cpp +# BasedOnStyle: Google +AccessModifierOffset: -1 +AlignAfterOpenBracket: Align +AlignConsecutiveMacros: false +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Left +AlignOperands: true +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: true +AllowAllConstructorInitializersOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: All +AllowShortLambdasOnASingleLine: All +AllowShortIfStatementsOnASingleLine: WithoutElse +AllowShortLoopsOnASingleLine: true +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 120 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DeriveLineEnding: true +DerivePointerAlignment: true +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^' + Priority: 2 + SortPriority: 0 + - Regex: '^<.*\.h>' + Priority: 1 + SortPriority: 0 + - Regex: '^<.*' + Priority: 2 + SortPriority: 0 + - Regex: '.*' + Priority: 3 + SortPriority: 0 +IncludeIsMainRegex: '([-_](test|unittest))?$' +IncludeIsMainSourceRegex: '' +IndentCaseLabels: true +IndentGotoLabels: true +IndentPPDirectives: None +IndentWidth: 4 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Never +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Left +RawStringFormats: + - Language: Cpp + Delimiters: + - cc + - CC + - cpp + - Cpp + - CPP + - 'c++' + - 'C++' + CanonicalDelimiter: '' + BasedOnStyle: google + - Language: TextProto + Delimiters: + - pb + - PB + - proto + - PROTO + EnclosingFunctions: + - EqualsProto + - EquivToProto + - PARSE_PARTIAL_TEXT_PROTO + - PARSE_TEST_PROTO + - PARSE_TEXT_PROTO + - ParseTextOrDie + - ParseTextProtoOrDie + CanonicalDelimiter: '' + BasedOnStyle: google +ReflowComments: true +SortIncludes: true +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: false +SpacesInConditionalStatement: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpaceBeforeSquareBrackets: false +Standard: Auto +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TabWidth: 8 +UseCRLF: false +UseTab: Never +... + diff --git a/coroutines/CMakeLists.txt b/coroutines/CMakeLists.txt new file mode 100644 index 000000000..87c384059 --- /dev/null +++ b/coroutines/CMakeLists.txt @@ -0,0 +1,149 @@ +######################################################################## +# Note: cotest is being brought up using cmake initially. MSVC and +# hermetic builds probably won't work, for the time being. +# +# CMake build script for cotest's coroutines support library. +# +# To run the tests for coroutines on Linux, use 'make test' or +# ctest. You can select which tests to run using 'ctest -R regex'. +# For more options, run 'ctest --help'. +option(coro_build_samples "Build coro's sample programs." OFF) +option(coro_build_tests "Build all of coro's own tests." OFF) +option(gtest_disable_pthreads "Disable uses of pthreads in gtest." OFF) + +# A directory to find Google Test sources. +if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/gtest/CMakeLists.txt") + set(gtest_dir gtest) +else() + set(gtest_dir ../googletest) +endif() + +# A directory to find Google Mock sources. +if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/gmock/CMakeLists.txt") + set(gmock_dir gmock) +else() + set(gmock_dir ../googlemock) +endif() + +######################################################################## +# +# Project-wide settings + +# Name of the project. +# +# CMake files in this project can refer to the root source directory +# as ${coro_SOURCE_DIR} and to the root binary directory as +# ${coro_BINARY_DIR}. +# Language "C" is required for find_package(Threads). + +# Project version: +cmake_minimum_required(VERSION 3.5) +cmake_policy(SET CMP0048 NEW) +project(cotest VERSION ${COROUTINES_VERSION} LANGUAGES CXX C) + +if (POLICY CMP0063) # Visibility + cmake_policy(SET CMP0063 NEW) +endif (POLICY CMP0063) + +# Instructs CMake to process Google Mock's CMakeLists.txt and add its +# targets to the current scope. We are placing Google Mock's binary +# directory in a subdirectory of our own as VC compilation may break +# if they are the same (the default). +add_subdirectory("${gmock_dir}" "${coro_BINARY_DIR}/${gmock_dir}") + +# Define helper functions and macros used by Google Test. +include(${gtest_dir}/cmake/internal_utils.cmake) + +config_compiler_and_linker() # Defined in internal_utils.cmake. + +# Adds coroutines header directories to the search path. +set(coro_build_include_dirs + "${coro_SOURCE_DIR}/include" + "${coro_SOURCE_DIR}" + "${gmock_SOURCE_DIR}/include" + # This directory is needed to build directly from Google Mock sources. + "${gmock_SOURCE_DIR}") +include_directories(${coro_build_include_dirs}) + +######################################################################## +# +# Defines the coroutines library. This is an internal library. + +# coroutines library. We build it using more strict warnings than what +# are used for other targets, to ensure that cotest can be compiled by a user +# aggressive about warnings. + +# For combined sources +#cxx_library(cotest "${cxx_strict}" "${gmock_dir}/src/gmock-all.cc" src/cotest-all.cc) + +# For cotest development, helps keep tabs on deps +cxx_library(cotest + "${cxx_strict}" + "${gmock_dir}/src/gmock-all.cc" + src/cotest.cc + src/cotest-coro-thread.cc + src/cotest-crf-core.cc + src/cotest-crf-launch.cc + src/cotest-crf-mock.cc + src/cotest-crf-payloads.cc + src/cotest-crf-synch.cc + src/cotest-crf-test.cc + src/cotest-integ-finder.cc + src/cotest-integ-mock.cc) + + +# Attach header directory information +# to the targets for when we are part of a parent build (ie being pulled +# in via add_subdirectory() rather than being a standalone build). +string(REPLACE ";" "$" dirs "${coro_build_include_dirs}") +target_include_directories(cotest SYSTEM INTERFACE + "$" + "$/${CMAKE_INSTALL_INCLUDEDIR}>") + +######################################################################## +# +# coroutine library tests. +# +# The tests are not built by default. To build them, set the +# gtest_build_tests option to ON. You can do it by running ccmake +# or specifying the -Dgtest_build_tests=ON flag when running cmake. +if (coro_build_tests) + # Allow use of gtest + include_directories(PRIVATE "${gtest_dir}/include" "${gmock_dir}/include") + link_libraries(gtest gtest_main gmock) + + # This must be set in the root directory for the tests to be run by + # 'make test' or ctest. + enable_testing() + + ############################################################ + # Corountines internal tests + cxx_test(coro-test-thread cotest) + cxx_test(exp-finder-test cotest) + + ############################################################ + # Test cases that drive complete cotest - phase 1 + cxx_test(cotest-action-macro cotest) + cxx_test(cotest-action-functor cotest) + cxx_test(cotest-action-poly cotest) + cxx_test(cotest-ui cotest) + cxx_test(cotest-cardinality cotest) + cxx_test(cotest-ext-filter cotest) + cxx_test(cotest-int-filter cotest) + cxx_test(cotest-lambda cotest) + cxx_test(cotest-mockfunction cotest) + cxx_test(cotest-wild cotest) + cxx_test(cotest-types cotest) + + ############################################################ + # Test cases that drive complete cotest - phase 2 + cxx_test(cotest-launch cotest) + cxx_test(cotest-launch-mock cotest) + cxx_test(cotest-all-in cotest) + cxx_test(cotest-mutex cotest) + cxx_test(cotest-launch-multi-coro cotest) + cxx_test(cotest-launch-lifetime cotest) + cxx_test(cotest-serverised cotest) + +endif() + diff --git a/coroutines/include/cotest/cotest.h b/coroutines/include/cotest/cotest.h new file mode 100644 index 000000000..245ba697e --- /dev/null +++ b/coroutines/include/cotest/cotest.h @@ -0,0 +1,607 @@ +#ifndef COROUTINES_INCLUDE_CORO_COTEST_H_ +#define COROUTINES_INCLUDE_CORO_COTEST_H_ + +#include "cotest/internal/cotest-integ-mock.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace testing { + +// We use preprocessor macros when we need stringification or +// other trickery, and also when we expect to want to collect +// __FILE__ and __LINE__ (but not all have been added yet). + +// ------------------ utils ------------------ + +// Credit for this goes to the GMock implementors +#define COTEST_PP_VARIADIC_CALL(_Macro, ...) \ + GMOCK_PP_IDENTITY(GMOCK_PP_CAT(_Macro, GMOCK_PP_NARG0(__VA_ARGS__))(__VA_ARGS__)) + +#define COTEST_STR(S) COTEST_STRW(S) +#define COTEST_STRW(S) #S + +// ------------------ mock call session ------------------ + +#define DROP() Drop() + +#define ACCEPT() Accept() + +// Covers 0-arg and 1-arg cases including unprotected commas. +#define RETURN(...) Return(__VA_ARGS__) + +// See also MOCK_CALL_HANDLE, IS_CALL and WAIT_FOR_CALL below + +// ------------------ launch session ------------------------- + +// Launch the supplied expression and return launch session +#define LAUNCH(...) \ + cotest_coro_->Launch([&]() -> decltype(auto) { return (__VA_ARGS__); }, \ + COTEST_STR(__VA_ARGS__)) + +// See also IS_RESULT and WAIT_FOR_RESULT below. +// Result obtained using call syntax: +// result = event_session( launch_session ); + +// Note on cotest_coro_ +// cotest_coro_ is passed into coroutine body as a function parameter +// and is also a public member of a Coroutine. In both cases it just +// points to the coroutine. So, in the body we can use NEXT_EVENT() +// and outside, when we have a Coroutine, we can do coro->NEXT_EVENT(). +// cotest_coro_ is also helpful for reducing pollution of the global +// namespace. + +// ------------------ cardinality etc -------------------- + +// Indicate that the coroutine does not need to run to completion +// in order for the test to pass +#define SATISFY() cotest_coro_->SetSatisfied() + +// Indicate that it is not an error for the coroutine to see further +// mock calls after exiting (they will be dropped) +#define RETIRE() cotest_coro_->Retire() + +// Return from mthe coroutine body. Please use this in preference to return +// for forward-compatibility with C++20 coroutines (and so we can grab +// __FILE__ and __LINE__). +#define EXIT_COROUTINE() return + +// --------------------- utilities ------------------------- + +// De-allocate unused launch coroutines. This plugs a memory leak but +// could slow things down if used frequently. +#define COTEST_CLEANUP() (crf::LaunchCoroutinePool::GetInstance()->CleanUp()) + +// ------------------ Declaring coroutines ------------------ + +// Use one of: +// auto my_coro = COROUTINE(){ ...code... }; +// or +// auto my_coro = COROUTINE(MyCoroName){ ...code... }; +#define COROUTINE(...) COTEST_PP_VARIADIC_CALL(COROUTINE_ARG_, __VA_ARGS__) + +#define COROUTINE_ARG_0() ::testing::internal::LambdaCoroFactory() + [&](::testing::internal::Coroutine * cotest_coro_) + +#define COROUTINE_ARG_1(NAME) \ + ::testing::internal::LambdaCoroFactory(COTEST_STR(NAME)) + [&](::testing::internal::Coroutine * cotest_coro_) + +// Allocate a coroutine on the heap +#define NEW_COROUTINE(...) COTEST_PP_VARIADIC_CALL(NEW_COROUTINE_ARG_, __VA_ARGS__) + +#define NEW_COROUTINE_ARG_0() \ + ::testing::internal::LambdaCoroFactory() - [&](::testing::internal::Coroutine * cotest_coro_) + +#define NEW_COROUTINE_ARG_1(NAME) \ + ::testing::internal::LambdaCoroFactory(COTEST_STR(NAME)) - [&](::testing::internal::Coroutine * cotest_coro_) + +// ------------------ watching for mock calls ------------------ + +#define WATCH_CALL(...) COTEST_PP_VARIADIC_CALL(WATCH_CALL_ARG_, __VA_ARGS__) + +// WATCH_CALL(obj, call) is similar to EXPECT_CALL including implied priority +// scheme and With(). +#define WATCH_CALL_ARG_2(obj, call) \ + cotest_coro_->WatchCall(std::move(GMOCK_GET_MOCKSPEC(obj, call, gmock)), __FILE__, __LINE__, #obj, #call) + +// Wildcard version: any mocked call on supplied mock object +#define WATCH_CALL_ARG_1(obj) \ + cotest_coro_->WatchCall(__FILE__, __LINE__, static_cast<::testing::crf::UntypedMockObjectPointer>(&(obj))) + +// Wildcard version: any mocked call +#define WATCH_CALL_ARG_0() cotest_coro_->WatchCall(__FILE__, __LINE__) + +// ------------------ variadic MOCK_CALL_HANDLE() ------------------ + +#define MOCK_CALL_HANDLE(...) COTEST_PP_VARIADIC_CALL(MOCK_CALL_HANDLE_ARG_, __VA_ARGS__) + +// NULL session for any mock call +#define MOCK_CALL_HANDLE_ARG_0() (::testing::EventHandle()) + +// No different than MOCK_CALL_HANDLE() - not for human use, only consistency. +#define MOCK_CALL_HANDLE_ARG_1(OBJ) (::testing::EventHandle()) + +// NULL call session for any mock call with signature matching the supplied mock +// method +#define MOCK_CALL_HANDLE_ARG_2(OBJ, METHOD) \ + (::testing::internal::CreateSignatureHandle(std::move(GMOCK_GET_MOCKSPEC(OBJ, METHOD, gmockq)))) + +// Can also use SignatureHandle where SIGNATURE is eg int(void *) + +// ------------------ variadic IS_CALL() ------------------ + +#define IS_CALL(...) COTEST_PP_VARIADIC_CALL(IS_CALL_ARG_, __VA_ARGS__) + +// Is the event a mock call? +#define IS_CALL_ARG_0() IsMockCall() + +// Is the event a mock call on the supplied mock object? +#define IS_CALL_ARG_1(obj) IsObject((::testing::crf::UntypedMockObjectPointer) & (obj)) + +// Is the mock call a match to the method. Matchers and With() are +// supported as with EXPECT_CALL(). +#define IS_CALL_ARG_2(obj, call) CoTestIsCallImpl_(std::move(GMOCK_GET_MOCKSPEC(obj, call, gmockq))) + +// ------------------ variadic IS_RESULT ------------------ + +#define IS_RESULT(...) COTEST_PP_VARIADIC_CALL(IS_RESULT_ARG_, __VA_ARGS__) + +// Is this event a completed launch result? +#define IS_RESULT_ARG_0() IsLaunchResult() + +// Is this event a completed launch result from the given launch +// session? +#define IS_RESULT_ARG_1(DC) IsLaunchResult(DC) + +// Note: these operations also tell cotest that the launch return has +// been detected by the coroutine. If this does not happen, cotest will +// report an error. + +// ---------------- wait for call ------------------ + +#define WAIT_FOR_CALL_NSE(...) COTEST_PP_VARIADIC_CALL(WAIT_FOR_CALL_NSE_ARG_, __VA_ARGS__) + +#define WAIT_FOR_CALL(...) COTEST_PP_VARIADIC_CALL(WAIT_FOR_CALL_ARG_, __VA_ARGS__) + +// Note: _NSE versions do not use the gcc statement expression extension. +// This extnesion permits function-like macros safely that can yield +// inside C++20 coroutines. + +// ------------------ By method ------------------ + +// "Wait" for a mock call that satisfies a matcher similar to +// EXPECT_CALL(). With() not supported. This will drop non-matching +// calls and then when one matches it will accept it and return the +// session. + +#define WAIT_FOR_CALL_NSE_ARG_3(CS, OBJ, METHOD) \ + auto CS = MOCK_CALL_HANDLE_ARG_2(OBJ, METHOD); \ + do { \ + auto cg_ = NEXT_EVENT(); \ + CS = cg_.IS_CALL_ARG_2(OBJ, METHOD); \ + if (!CS) cg_.DROP(); \ + } while (!CS); \ + CS.ACCEPT() + +#define WAIT_FOR_CALL_ARG_2(OBJ, METHOD) \ + ({ \ + WAIT_FOR_CALL_NSE_ARG_3(cs, OBJ, METHOD); \ + cs; \ + }) + +// ------------------ By object ------------------ + +// "Wait" for a mock call on the given object. This will drop non-matching +// calls and then when one matches it will accept it and return the +// session. + +#define WAIT_FOR_CALL_NSE_ARG_2(CG, OBJ) \ + auto CG = MOCK_CALL_HANDLE_ARG_0(); \ + do { \ + auto cg_ = NEXT_EVENT(); \ + CG = cg_.IS_CALL_ARG_1(OBJ); \ + if (!CG) cg_.DROP(); \ + } while (!CG); \ + CG.ACCEPT() + +#define WAIT_FOR_CALL_ARG_1(OBJ) \ + ({ \ + WAIT_FOR_CALL_NSE_ARG_2(cg, OBJ); \ + cg; \ + }) + +// ------------------- Any call ----------------------- + +// "Wait" for any mock call. This will accept it and return the +// session. + +#define WAIT_FOR_CALL_NSE_ARG_1(CG) \ + auto CG = MOCK_CALL_HANDLE_ARG_0(); \ + do { \ + auto cg_ = NEXT_EVENT(); \ + CG = cg_.IS_CALL_ARG_0(); \ + if (!CG) cg_.DROP(); \ + } while (!CG); \ + CG.ACCEPT() + +#define WAIT_FOR_CALL_ARG_0() \ + ({ \ + WAIT_FOR_CALL_NSE_ARG_1(cg); \ + cg; \ + }) + +// -------------- wait for call from ---------------- + +#define WAIT_FOR_CALL_FROM_NSE(...) COTEST_PP_VARIADIC_CALL(WAIT_FOR_CALL_FROM_NSE_ARG_, __VA_ARGS__) + +#define WAIT_FOR_CALL_FROM(...) COTEST_PP_VARIADIC_CALL(WAIT_FOR_CALL_FROM_ARG_, __VA_ARGS__) + +// ------------------ By method ------------------ + +// As above, but from given lauch session only +#define WAIT_FOR_CALL_FROM_NSE_ARG_4(CS, OBJ, METHOD, DS) \ + auto CS = MOCK_CALL_HANDLE_ARG_2(OBJ, METHOD); \ + do { \ + auto cg_ = NEXT_EVENT(); \ + CS = cg_.IS_CALL_ARG_2(OBJ, METHOD).From(DS); \ + if (!CS) cg_.DROP(); \ + } while (!CS); \ + CS.ACCEPT() + +#define WAIT_FOR_CALL_FROM_ARG_3(OBJ, METHOD, DS) \ + ({ \ + WAIT_FOR_CALL_FROM_NSE_ARG_4(cs, OBJ, METHOD, DS); \ + cs; \ + }) + +// ------------------ By object ------------------ + +// As above, but from given lauch session only +#define WAIT_FOR_CALL_FROM_NSE_ARG_3(CG, OBJ, DS) \ + auto CG = MOCK_CALL_HANDLE_ARG_0(); \ + do { \ + auto cg_ = NEXT_EVENT(); \ + CG = cg_.IS_CALL_ARG_1(OBJ).From(DS); \ + if (!CG) cg_.DROP(); \ + } while (!CG); \ + CG.ACCEPT() + +#define WAIT_FOR_CALL_FROM_ARG_2(OBJ, DS) \ + ({ \ + WAIT_FOR_CALL_FROM_NSE_ARG_3(cg, OBJ, DS); \ + cg; \ + }) + +// ------------------- Any call ----------------------- + +// As above, but from given lauch session only + +#define WAIT_FOR_CALL_FROM_NSE_ARG_2(CG, DS) \ + auto CG = MOCK_CALL_HANDLE_ARG_0(); \ + do { \ + auto cg_ = NEXT_EVENT(); \ + CG = cg_.IS_CALL_ARG_0().From(DS); \ + if (!CG) cg_.DROP(); \ + } while (!CG); \ + CG.ACCEPT() + +#define WAIT_FOR_CALL_FROM_ARG_1(DS) \ + ({ \ + WAIT_FOR_CALL_FROM_NSE_ARG_2(cg, DS); \ + cg; \ + }) + +// ---------------- wait for result ------------------ + +// Wait for a completed launch, dropping mock calls +#define WAIT_FOR_RESULT_NSE(CG) \ + auto CG = ::testing::EventHandle(); \ + do { \ + auto cg_ = NEXT_EVENT(); \ + CG = cg_.IS_RESULT_ARG_0(); \ + if (!CG) cg_.DROP(); \ + } while (!CG); + +#define WAIT_FOR_RESULT() \ + ({ \ + WAIT_FOR_RESULT_NSE(cg); \ + cg; \ + }) + +// ---------------------- COTEST --------------------------- + +#define COTEST_TEST_CLASS_NAME_(TEST_SUITE_NAME, TEST_NAME) TEST_SUITE_NAME##_##TEST_NAME##_Cotest + +// Declare a "pure" cotest test. Usage is similar to TEST(). The given body +// is the body of a coroutine. Testing assets should be declared within +// the coro body. +#define COTEST(TEST_SUITE_NAME, TEST_NAME) \ + static void COTEST_TEST_CLASS_NAME_(TEST_SUITE_NAME, TEST_NAME)(::testing::internal::Coroutine * cotest_coro_); \ + TEST(TEST_SUITE_NAME, TEST_NAME) { \ + auto c = ::testing::internal::Coroutine(COTEST_TEST_CLASS_NAME_(TEST_SUITE_NAME, TEST_NAME), \ + COTEST_STR(TEST_NAME)); \ + } \ + static void COTEST_TEST_CLASS_NAME_(TEST_SUITE_NAME, TEST_NAME)(::testing::internal::Coroutine * cotest_coro_) + +// Note: this is not in fact the most flexible way to use cotest. A regular +// TEST() case can declare multiple coroutines, whereas a COTEST() only has one. + +// ------------------ serverised API ------------------ + +// Returns the next valid event session. An event can be a mock call, or a launch return. +#define NEXT_EVENT() cotest_coro_->NextEvent(__FILE__, __LINE__) + +// This is a lower-level alternative to the WAIT_FOR_ macros, and if the +// returned event is a mock call, the user is required to call DROP() or +// ACCEPT() on it before doing anything else with the cotest API, or exiting. +// NEXT_EVENT will return a mock call session for every event the coro +// can see: if WATCH_CALL() is used, this will be all mock calls not +// handled by a higher-priority watch or expectations. +// +// It is intended for use in a message loop, for when mock calls and launch +// completions must be handled in whatever order they arrive. This is +// termed serverised style. + +// ------------------ Classes ------------------ + +// Handle for the session created by a LAUNCH(). Templated on the session +// return type, which is simply the decltype() of the supplied expression. +template +class LaunchHandle { + public: + LaunchHandle() = default; + explicit LaunchHandle(std::shared_ptr> crf_ls_); + operator bool() const; + + crf::InteriorLaunchSession *GetCRF_(); + + private: + std::shared_ptr> crf_ls; +}; + +// Handle for any event received by NEXT_EVENT() which can be a call +// session or a launch result session. +class EventHandle { + public: + EventHandle() = default; + explicit EventHandle(std::shared_ptr crf_es_); + + EventHandle IsLaunchResult() const; + template + EventHandle IsLaunchResult(LaunchHandle launch_session) const; + template + RESULT_TYPE operator()(LaunchHandle launch_session) const; + + template + SignatureHandle CoTestIsCallImpl_(MockSpec &&mock_spec); + EventHandle IsObject(crf::UntypedMockObjectPointer object); + EventHandle IsMockCall(); + operator bool() const; + + EventHandle Drop(); + EventHandle Accept(); + EventHandle Return(); + template + EventHandle From(LaunchHandle &source); + EventHandle FromMain(); + + std::string GetName() const; + + private: + std::shared_ptr crf_es; +}; + +// Handle for a mock call session when the function type is known. Templated +// on the function type eg int(char *) +template +class SignatureHandle : public EventHandle { + public: + using ArgumentTuple = typename internal::Function::ArgumentTuple; + + SignatureHandle() = default; + SignatureHandle(std::shared_ptr crf_es_, + std::shared_ptr> &&crf_sig_); + + SignatureHandle IsMockCall(); + operator bool() const; + + SignatureHandle Drop(); + SignatureHandle Accept(); + SignatureHandle Return(); + template + SignatureHandle From(LaunchHandle &source); + SignatureHandle FromMain(); + + template + SignatureHandle Return(U &&retval); + const ArgumentTuple &GetArgs() const; + template + const typename internal::Function::Arg::type &GetArg() const; + SignatureHandle With(const Matcher &m); + template + SignatureHandle WithArg(const Matcher::Arg::type &> &m); + + private: + std::shared_ptr> crf_sig; +}; + +// ------------------ Templated members ------------------ + +template +LaunchHandle::LaunchHandle(std::shared_ptr> crf_ls_) + : crf_ls(crf_ls_) {} + +template +LaunchHandle::operator bool() const { + return !!crf_ls; +} + +template +crf::InteriorLaunchSession *LaunchHandle::GetCRF_() { + return crf_ls.get(); +} + +template +EventHandle EventHandle::IsLaunchResult(LaunchHandle launch_session) const { + if (crf_es->IsLaunchResult(launch_session.GetCRF_())) + return *this; + else + return EventHandle(); +} + +template +RESULT_TYPE EventHandle::operator()(LaunchHandle launch_session) const { + if (crf_es->IsLaunchResult(launch_session.GetCRF_())) + return launch_session.GetCRF_()->GetResult(crf_es.get()); + else + COTEST_ASSERT(!"Test failure: event is not a launch result"); +} + +template +SignatureHandle EventHandle::CoTestIsCallImpl_(MockSpec &&mock_spec) { + const FunctionMocker *mocker = mock_spec.InternalGetMocker(); + crf::UntypedMockObjectPointer mock_object = mocker->MockObjectLocked(); + auto utmb = static_cast(mocker); + auto untyped_mocker = static_cast(utmb); + + COTEST_ASSERT(mock_object && "NULL Mock object used with IS_CALL()"); + + COTEST_ASSERT(crf_es && "event session is NULL, check for failed test"); + if (!crf_es->IsMockCall()) return SignatureHandle(); + auto crf_mcs = std::static_pointer_cast(crf_es); + + // Check the mock object + if (mock_object != crf_mcs->GetMockObject()) return SignatureHandle(); + + // Check whether the same method is used (accurate even when the name + // and signature are the same due eg const overloading) + if (untyped_mocker != crf_mcs->GetMocker()) return SignatureHandle(); + + // This is the correct method, so try to match the values + // We have a mock spec (on a NULL mock object) and can get the matchers tuple + // from it + auto matchers = mock_spec.InternalGetMatchers(); + + // Get the arguments for matching and signature call session + auto args_tuple = crf_mcs->GetArgumentTuple(); + + // Let Google Test perform the matching + if (!internal::TupleMatches(matchers, *args_tuple)) return SignatureHandle(); + + auto crf_sig = std::make_shared>(crf_mcs.get(), args_tuple); + return SignatureHandle(crf_es, std::move(crf_sig)); +} + +template +EventHandle EventHandle::From(LaunchHandle &source) { + COTEST_ASSERT(crf_es); + bool ok = crf_es->IsFrom(source.GetCRF_()); + return ok ? *this : EventHandle(); +} + +template +SignatureHandle::SignatureHandle(std::shared_ptr crf_es_, + std::shared_ptr> &&crf_sig_) + : EventHandle(crf_es_), crf_sig(std::move(crf_sig_)) {} + +template +SignatureHandle SignatureHandle::IsMockCall() { + return *this; +} + +template +SignatureHandle::operator bool() const { + return !!crf_sig; +} + +template +SignatureHandle SignatureHandle::Drop() { + EventHandle::Drop(); + return *this; +} + +template +SignatureHandle SignatureHandle::Accept() { + EventHandle::Accept(); + return *this; +} + +template +SignatureHandle SignatureHandle::Return() { + EventHandle::Return(); + return *this; +} + +template +SignatureHandle SignatureHandle::FromMain() { + bool ok = EventHandle::FromMain(); + return ok ? *this : SignatureHandle(); +} + +template +template +SignatureHandle SignatureHandle::From(LaunchHandle &source) { + bool ok = EventHandle::From(source); + return ok ? *this : SignatureHandle(); +} + +template +template +SignatureHandle SignatureHandle::Return(U &&retval) { + COTEST_ASSERT(crf_sig && "call session is NULL, check for failed test"); + crf_sig->Return(std::forward(retval)); + return *this; +} + +template +const typename SignatureHandle::ArgumentTuple &SignatureHandle::GetArgs() const { + COTEST_ASSERT(crf_sig && "call session is NULL, check for failed test"); + return *crf_sig->GetArgumentTuple(); +} + +template +template +const typename internal::Function::Arg::type &SignatureHandle::GetArg() const { + return std::get(GetArgs()); +} + +template +SignatureHandle SignatureHandle::With(const Matcher &m) { + if (!crf_sig) return SignatureHandle(); // may have already mismatched + + // Let Google Test perform the matching + if (!m.Matches(GetArgs())) return SignatureHandle(); + + return *this; +} + +template +template +SignatureHandle SignatureHandle::WithArg( + const Matcher::Arg::type &> &m) { + if (!crf_sig) return SignatureHandle(); // may have already mismatched + + // Let Google Test perform the matching + if (!m.Matches(GetArg())) return SignatureHandle(); + + return *this; +} + +namespace internal { + +// I apologise for the operator overloads in this class. +class LambdaCoroFactory { + public: + LambdaCoroFactory(std::string name_ = "COROUTINE()") : name(name_) {} + + // Sorry about these + Coroutine operator+(Coroutine::BodyFunctionType lambda) { return Coroutine(lambda, name); } + + Coroutine *operator-(Coroutine::BodyFunctionType lambda) { return new Coroutine(lambda, name); } + + private: + const std::string name; +}; + +} // namespace internal +} // namespace testing + +#endif diff --git a/coroutines/include/cotest/internal/cotest-coro-common.h b/coroutines/include/cotest/internal/cotest-coro-common.h new file mode 100644 index 000000000..b5122ba49 --- /dev/null +++ b/coroutines/include/cotest/internal/cotest-coro-common.h @@ -0,0 +1,99 @@ +#ifndef COROUTINES_INCLUDE_CORO_COTEST_CORO_COMMON_H_ +#define COROUTINES_INCLUDE_CORO_COTEST_CORO_COMMON_H_ + +#include +#include +#include + +#include "cotest/internal/cotest-util-logging.h" + +namespace coro_impl { + +class Payload { + public: + Payload() = default; + Payload(const Payload &) = delete; + Payload(Payload &&) = delete; + Payload &operator=(const Payload &) = delete; + Payload &operator=(Payload &&) = delete; + virtual ~Payload() = default; + virtual std::string DebugString() const = 0; +}; + +template +const PAY &PeekPayload(const std::unique_ptr &p) { + return *static_cast(p.get()); +} + +template +std::unique_ptr SpecialisePayload(std::unique_ptr &&p) { + return std::unique_ptr(static_cast(p.release())); +} + +template +std::unique_ptr MakePayload(Args &&... args) { + return std::make_unique(std::forward(args)...); +} + +class ExteriorInterface { + public: + ExteriorInterface() = default; + ExteriorInterface(const ExteriorInterface &i) = delete; + ExteriorInterface(ExteriorInterface &&i) = delete; + ExteriorInterface &operator=(const ExteriorInterface &) = delete; + ExteriorInterface &operator=(ExteriorInterface &&) = delete; + virtual ~ExteriorInterface() = default; + + virtual std::unique_ptr Iterate(std::unique_ptr &&to_coro) = 0; + virtual std::unique_ptr ThrowIn(std::exception_ptr in_ex) = 0; + virtual void Cancel() = 0; + virtual bool IsCoroutineExited() const = 0; + virtual void SetName(std::string name_) = 0; + virtual std::string GetName() const = 0; +}; + +class InteriorInterface { + public: + InteriorInterface() = default; + InteriorInterface(const InteriorInterface &i) = delete; + InteriorInterface(InteriorInterface &&i) = delete; + InteriorInterface &operator=(const InteriorInterface &) = delete; + InteriorInterface &operator=(InteriorInterface &&) = delete; + virtual ~InteriorInterface() = default; + + virtual std::unique_ptr Yield(std::unique_ptr &&from_coro) = 0; + + // Find out which coro is currently running. Any of the instances of + // the same concrete type as the one called on, or NULL. + virtual InteriorInterface *GetActive() = 0; +}; + +using BodyFunction = std::function; + +class CancellationException : public std::exception {}; + +// std::exception_ptr lets us be flexible with exception objects without +// having to worry about slicing. The object is stored in the compiler's +// special place and should be considered const - it *cannot* be accessed +// through the exception_ptr - all you can do is pass it around and +// throw it using std::rethrow_exception(). Note that exception_ptr is +// nullable (use = nullptr) but you shouldn't rethrow a null one. The +// exception object could be copied, so don't use identity semantics. +template +std::exception_ptr MakeException(Args &&... args) try { + throw ETYPE(args...); + COTEST_ASSERT(false); // wut +} catch (ETYPE &) // Don't catch exceptions thrown in ETYPE's constructor +{ + return std::current_exception(); +} + +inline std::ostream &operator<<(std::ostream &os, const Payload *payload) { + if (payload) os << payload->DebugString(); + os << PtrToString(payload); + return os; +} + +} // namespace coro_impl + +#endif diff --git a/coroutines/include/cotest/internal/cotest-coro-thread.h b/coroutines/include/cotest/internal/cotest-coro-thread.h new file mode 100644 index 000000000..883de979f --- /dev/null +++ b/coroutines/include/cotest/internal/cotest-coro-thread.h @@ -0,0 +1,69 @@ +#ifndef COROUTINES_INCLUDE_CORO_INTERNAL_COTEST_CORO_THREAD_H_ +#define COROUTINES_INCLUDE_CORO_INTERNAL_COTEST_CORO_THREAD_H_ + +#include +#include +#include +#include +#include + +#include "cotest-coro-common.h" + +namespace coro_impl { + +/** + * Implement stacky coroutines on C++ threads + */ +class CoroOnThread final : public ExteriorInterface, public InteriorInterface { + public: + // Rule of 5 but disallow copy and move. Immutable and non-nullable. + CoroOnThread() = delete; + CoroOnThread(const CoroOnThread &i) = delete; + CoroOnThread(CoroOnThread &&i) = delete; + CoroOnThread &operator=(const CoroOnThread &) = delete; + CoroOnThread &operator=(CoroOnThread &&) = delete; + ~CoroOnThread(); + + CoroOnThread(BodyFunction cofn_, std::string name); + + // ExteriorInterface + std::unique_ptr Iterate(std::unique_ptr &&to_coro) final; + std::unique_ptr ThrowIn(std::exception_ptr in_ex) final; + void Cancel() final; + bool IsCoroutineExited() const final; + void SetName(std::string name_) final; + std::string GetName() const final; + + // InteriorInterface + std::unique_ptr Yield(std::unique_ptr &&from_coro) final; + + InteriorInterface *GetActive() final; + + private: + enum class Phase { CoroutineRuns, MainRuns, CoroutineExited }; + + void ThreadRun(); + void TrySetThreadName(); + void NotifyPhase(Phase new_phase); + void WaitPhases(std::set phases); + + BodyFunction coro_run_function; + + std::thread local_thread; + + Phase phase = Phase::MainRuns; + mutable std::mutex phase_mutex; + + std::condition_variable cv; + + std::unique_ptr payload; + std::exception_ptr payload_ex; + + std::string name; + + static InteriorInterface *active; +}; + +} // namespace coro_impl + +#endif diff --git a/coroutines/include/cotest/internal/cotest-crf-core.h b/coroutines/include/cotest/internal/cotest-crf-core.h new file mode 100644 index 000000000..6ca2894a9 --- /dev/null +++ b/coroutines/include/cotest/internal/cotest-crf-core.h @@ -0,0 +1,132 @@ +#ifndef COROUTINES_INCLUDE_CORO_INTERNAL_COTEST_CRF_CORE_H_ +#define COROUTINES_INCLUDE_CORO_INTERNAL_COTEST_CRF_CORE_H_ + +#include +#include +#include +#include + +#include "cotest-coro-common.h" +#include "cotest-coro-thread.h" +#include "cotest-crf-payloads.h" +#include "cotest-util-logging.h" +#include "cotest-util-types.h" + +namespace testing { +namespace crf { + +// Select the thread-based coroutine impl for all the CRF coroutines. +using CoroImplType = coro_impl::CoroOnThread; + +// ------------------ Classes ------------------ + +class MockRoutingSession; + +class InteriorEventSession; + +template +class InteriorSignatureMockCS; + +class InteriorLaunchSessionBase; + +template +class InteriorLaunchSession; + +class TestCoroutine; + +using coro_impl::MakePayload; +using coro_impl::PeekPayload; +using coro_impl::SpecialisePayload; + +class MessageNode { + public: + // Reply payload, reply destination + using ReplyPair = std::pair, MessageNode *>; + + MessageNode() = default; + MessageNode(const MessageNode &i) = delete; + MessageNode(MessageNode &&i) = delete; + MessageNode &operator=(const MessageNode &) = delete; + MessageNode &operator=(MessageNode &&) = delete; + virtual ~MessageNode() = default; + + virtual ReplyPair ReceiveMessage(std::unique_ptr &&to_node) = 0; + virtual std::string DebugString() const = 0; + + static std::unique_ptr SendMessageFromMain(MessageNode *dest, std::unique_ptr &&to_node); + + private: + static std::unique_ptr MessageLoop(MessageNode *dest, std::unique_ptr &&to_node); +}; + +inline std::ostream &operator<<(std::ostream &os, const MessageNode *payload) { + if (payload) os << payload->DebugString(); + os << coro_impl::PtrToString(payload); + return os; +} + +class CoroutineBase : public virtual MessageNode { + public: + CoroutineBase(const CoroutineBase &i) = delete; + CoroutineBase(CoroutineBase &&i) = delete; + CoroutineBase &operator=(const CoroutineBase &) = delete; + CoroutineBase &operator=(CoroutineBase &&) = delete; + virtual ~CoroutineBase(); + + CoroutineBase(coro_impl::BodyFunction cofn_, std::string name_); + + std::unique_ptr Iterate(std::unique_ptr &&to_coro); + void Cancel(); + bool IsCoroutineExited() const; + + std::unique_ptr Yield(std::unique_ptr &&from_coro); + + coro_impl::InteriorInterface *GetImpl(); + void SetName(std::string name_); + std::string GetName() const; + std::string ActiveStr() const; + + private: + std::string name; + std::unique_ptr impl; + bool initial = true; +}; + +class MockSource : public virtual MessageNode { + public: + MockSource() = default; + MockSource(const MockSource &i) = delete; + MockSource(MockSource &&i) = delete; + MockSource &operator=(const MockSource &) = delete; + MockSource &operator=(MockSource &&) = delete; + virtual ~MockSource() = default; + + virtual std::shared_ptr CreateMockRoutingSession(UntypedMockerPointer mocker_, + UntypedMockObjectPointer mock_obj_, + const char *name_) = 0; + void SetCurrentMockRS(std::shared_ptr current_mock_call_); + std::shared_ptr GetCurrentMockRS() const; + virtual LaunchCoroutine *GetAsCoroutine() = 0; + virtual const LaunchCoroutine *GetAsCoroutine() const = 0; + + private: + std::shared_ptr current_mock_rs; +}; + +class ProxyForMain : public MockSource, public std::enable_shared_from_this { + public: + ReplyPair ReceiveMessage(std::unique_ptr &&to_node) final; + std::shared_ptr CreateMockRoutingSession(UntypedMockerPointer mocker_, + UntypedMockObjectPointer mock_obj_, + const char *name_) final; + std::string DebugString() const final; + LaunchCoroutine *GetAsCoroutine() final; + const LaunchCoroutine *GetAsCoroutine() const final; + + static std::shared_ptr GetInstance(); +}; + +} // namespace crf +} // namespace testing + +#endif diff --git a/coroutines/include/cotest/internal/cotest-crf-launch.h b/coroutines/include/cotest/internal/cotest-crf-launch.h new file mode 100644 index 000000000..8ce47849e --- /dev/null +++ b/coroutines/include/cotest/internal/cotest-crf-launch.h @@ -0,0 +1,76 @@ +#ifndef COROUTINES_INCLUDE_CORO_INTERNAL_COTEST_CRF_LAUNCH_H_ +#define COROUTINES_INCLUDE_CORO_INTERNAL_COTEST_CRF_LAUNCH_H_ + +#include +#include +#include +#include + +#include "cotest-crf-core.h" +#include "cotest-crf-mock.h" +#include "cotest-crf-payloads.h" +#include "cotest-util-types.h" + +namespace testing { +namespace crf { + +// ------------------ Classes ------------------ + +class MockRoutingSession; + +class InteriorEventSession; + +template +class InteriorSignatureMockCS; + +class InteriorLaunchSessionBase; + +template +class InteriorLaunchSession; + +class TestCoroutine; + +class LaunchCoroutine final : public CoroutineBase, + public MockSource, + public std::enable_shared_from_this { + public: + LaunchCoroutine(std::string name_); + ~LaunchCoroutine(); + + void Body(); + ReplyPair ReceiveMessage(std::unique_ptr &&to_node) final; + ReplyPair IterateServer(std::unique_ptr &&to_coro); + std::shared_ptr CreateMockRoutingSession(UntypedMockerPointer mocker_, + UntypedMockObjectPointer mock_obj_, + const char *name_) final; + std::shared_ptr TryGetCurrentLaunchSession(); + std::shared_ptr TryGetCurrentLaunchSession() const; + LaunchCoroutine *GetAsCoroutine() final; + const LaunchCoroutine *GetAsCoroutine() const final; + std::string DebugString() const final; + + private: + std::weak_ptr current_launch_session; +}; + +class LaunchCoroutinePool final : public virtual MessageNode { + public: + using PoolType = std::map>; + + LaunchCoroutine *TryGetUnusedLaunchCoro(); + LaunchCoroutine *Allocate(std::string launch_text); + std::shared_ptr FindActiveMockSource(); + ReplyPair ReceiveMessage(std::unique_ptr &&to_node) final; + std::string DebugString() const final; + int CleanUp(); + + static LaunchCoroutinePool *GetInstance(); + + private: + PoolType pool; +}; + +} // namespace crf +} // namespace testing + +#endif diff --git a/coroutines/include/cotest/internal/cotest-crf-mock.h b/coroutines/include/cotest/internal/cotest-crf-mock.h new file mode 100644 index 000000000..c2ae25af1 --- /dev/null +++ b/coroutines/include/cotest/internal/cotest-crf-mock.h @@ -0,0 +1,97 @@ +#ifndef COROUTINES_INCLUDE_CORO_INTERNAL_COTEST_CRF_MOCK_H_ +#define COROUTINES_INCLUDE_CORO_INTERNAL_COTEST_CRF_MOCK_H_ + +#include +#include +#include +#include + +#include "cotest-crf-core.h" +#include "cotest-crf-payloads.h" +#include "cotest-util-types.h" + +namespace testing { +namespace crf { + +// ------------------ Classes ------------------ + +class MockRoutingSession; + +class InteriorEventSession; + +template +class InteriorSignatureMockCS; + +class InteriorLaunchSessionBase; + +template +class InteriorLaunchSession; + +class TestCoroutine; + +class MockRoutingSession : public std::enable_shared_from_this { + public: + MockRoutingSession(const MockRoutingSession &i) = delete; + MockRoutingSession(MockRoutingSession &&i) = delete; + MockRoutingSession &operator=(const MockRoutingSession &) = delete; + MockRoutingSession &operator=(MockRoutingSession &&) = delete; + ~MockRoutingSession() = default; + + MockRoutingSession(UntypedMockerPointer mocker_, UntypedMockObjectPointer mock_object_, std::string name_); + + void PreMockUnlocked(); + + void Configure(TestCoroutine *handling_coroutine_); + + bool SeenMockCallLocked(UntypedArgsPointer args); + UntypedReturnValuePointer ActionsAndReturnUnlocked(); + + TestCoroutine *GetHandlingTestCoro() const; + virtual std::shared_ptr GetMockSource() const = 0; + std::string GetName() const; + + private: + virtual std::unique_ptr SendMessageToSynchroniser(std::unique_ptr &&to_tc) const = 0; + virtual std::unique_ptr SendMessageToHandlingCoro(std::unique_ptr &&to_tc) const = 0; + + const UntypedMockerPointer mocker; + const UntypedMockObjectPointer mock_object; + const std::string name; + + std::weak_ptr call_session; + std::set handlers_that_dropped; + + TestCoroutine *handling_coroutine; +}; + +// Note on the extraction of these subclasses: we could get by using virtuals on +// the MockSource, but it's desirable to name the final classes for the constext +// in which their methods will run: ExteriorMockRS runs in main, and +// InteriorMockRS runs in a launch coroutine's interior. +class ExteriorMockRS final : public MockRoutingSession { + public: + using MockRoutingSession::MockRoutingSession; + + private: + std::shared_ptr GetMockSource() const final; + std::unique_ptr SendMessageToSynchroniser(std::unique_ptr &&to_tc) const final; + std::unique_ptr SendMessageToHandlingCoro(std::unique_ptr &&to_tc) const final; +}; + +class InteriorMockRS final : public MockRoutingSession { + public: + InteriorMockRS(std::shared_ptr launch_coro_, UntypedMockerPointer mocker_, + UntypedMockObjectPointer mock_object_, std::string name_); + + private: + std::shared_ptr GetMockSource() const final; + std::unique_ptr SendMessageToSynchroniser(std::unique_ptr &&to_tc) const final; + std::unique_ptr SendMessageToHandlingCoro(std::unique_ptr &&to_tc) const final; + + const std::weak_ptr launch_coro; +}; + +} // namespace crf +} // namespace testing + +#endif diff --git a/coroutines/include/cotest/internal/cotest-crf-payloads.h b/coroutines/include/cotest/internal/cotest-crf-payloads.h new file mode 100644 index 000000000..a8ed67c9e --- /dev/null +++ b/coroutines/include/cotest/internal/cotest-crf-payloads.h @@ -0,0 +1,218 @@ +#ifndef COROUTINES_INCLUDE_CORO_INTERNAL_COTEST_CRF_PAYLOADS_H_ +#define COROUTINES_INCLUDE_CORO_INTERNAL_COTEST_CRF_PAYLOADS_H_ + +#include +#include + +#include "cotest-coro-common.h" +#include "cotest-util-logging.h" +#include "cotest-util-types.h" + +namespace testing { +namespace crf { + +using UntypedMockerPointer = const void *; +using UntypedMockObjectPointer = const void *; +using UntypedArgsPointer = const void *; +using UntypedReturnValuePointer = const void *; + +// ------------------ Classes ------------------ + +class MockSource; +class TestCoroutine; +class LaunchCoroutine; +class MockRoutingSession; +class InteriorEventSession; +class InteriorLaunchSessionBase; +class InteriorMockCallSession; +class InteriorLaunchResultSession; + +template +class InteriorLaunchSession; + +enum class PayloadKind { + PreMock = 1000, // =1000 just to make valid values easy to spot in debugger + PreMockAck, + MockSeen, + DropMock, + AcceptMock, + MockAction, + ReturnMock, + Launch, + LaunchResult, + ResumeMain, + TCExited, + TCDestructing +}; + +class Payload : public coro_impl::Payload { + public: + virtual PayloadKind GetKind() const = 0; +}; + +class PreMockPayload final : public Payload { + public: + PreMockPayload(std::weak_ptr originator_, std::string name_, + UntypedMockObjectPointer mock_object_, UntypedMockerPointer mocker_); + PayloadKind GetKind() const final; + PreMockPayload *Clone() const; + std::weak_ptr GetOriginator() const; + std::string GetName() const; + UntypedMockObjectPointer GetMockObject() const; + UntypedMockerPointer GetMocker() const; + std::string DebugString() const final; + + private: + const std::weak_ptr originator; + const std::string name; + const UntypedMockObjectPointer mock_object; + const UntypedMockerPointer mocker; +}; + +class PreMockAckPayload : public Payload { + public: + explicit PreMockAckPayload(std::weak_ptr originator_); + PayloadKind GetKind() const; + std::weak_ptr GetOriginator() const; + std::string DebugString() const final; + + private: + const std::weak_ptr originator; +}; + +class MockSeenPayload final : public Payload { + public: + MockSeenPayload(std::weak_ptr originator_, UntypedArgsPointer args_, std::string name_, + UntypedMockObjectPointer mock_object_, UntypedMockerPointer mocker_); + PayloadKind GetKind() const final; + std::weak_ptr GetOriginator() const; + UntypedArgsPointer GetArgsUntyped() const; + std::string GetName() const; + UntypedMockObjectPointer GetMockObject() const; + UntypedMockerPointer GetMocker() const; + std::string DebugString() const final; + + private: + const std::weak_ptr originator; + const UntypedArgsPointer args; + const std::string name; + const UntypedMockObjectPointer mock_object; + const UntypedMockerPointer mocker; +}; + +class MockResponsePayload : public Payload { + public: + MockResponsePayload(std::weak_ptr originator_, + std::weak_ptr responder_); + std::weak_ptr GetOriginator() const; + std::weak_ptr GetResponder() const; + + protected: + const std::weak_ptr originator; + const std::weak_ptr responder; +}; + +class DropMockPayload final : public MockResponsePayload { + public: + using MockResponsePayload::MockResponsePayload; + PayloadKind GetKind() const final; + std::string DebugString() const final; +}; + +class AcceptMockPayload final : public MockResponsePayload { + public: + using MockResponsePayload::MockResponsePayload; + PayloadKind GetKind() const final; + std::string DebugString() const final; +}; + +class MockActionPayload final : public MockResponsePayload { + public: + using MockResponsePayload::MockResponsePayload; + PayloadKind GetKind() const final; + std::string DebugString() const final; +}; + +class ReturnMockPayload final : public MockResponsePayload { + public: + ReturnMockPayload(std::weak_ptr originator_, std::weak_ptr responder_, + UntypedReturnValuePointer result_); + PayloadKind GetKind() const final; + UntypedReturnValuePointer GetResult(); + std::string DebugString() const final; + + private: + const UntypedReturnValuePointer return_val_ptr; +}; + +class LaunchPayload final : public Payload { + public: + LaunchPayload(std::weak_ptr originator_, + internal::LaunchLambdaWrapperType wrapper_lambda_, std::string name_); + PayloadKind GetKind() const final; + std::weak_ptr GetOriginator() const; + internal::LaunchLambdaWrapperType GetLambdaWrapper() const; + std::string GetName() const; + std::string DebugString() const final; + + private: + const std::weak_ptr originator; + const internal::LaunchLambdaWrapperType wrapper_lambda; + const std::string name; +}; + +class LaunchResultPayload final : public Payload { + public: + LaunchResultPayload(std::weak_ptr originator_, std::weak_ptr responder_, + UntypedReturnValuePointer result_); + PayloadKind GetKind() const final; + std::weak_ptr GetOriginator() const; + std::weak_ptr GetResponder() const; + UntypedReturnValuePointer GetResult(); + std::string DebugString() const final; + + private: + const std::weak_ptr originator; + const std::weak_ptr responder; + const UntypedReturnValuePointer return_val_ptr; +}; + +class ResumeMainPayload : public Payload { + public: + explicit ResumeMainPayload(std::weak_ptr originator_); + PayloadKind GetKind() const; + std::weak_ptr GetOriginator() const; + std::string DebugString() const final; + + private: + const std::weak_ptr originator; +}; + +// Test Coroutine Exited +class TCExitedPayload : public Payload { + public: + explicit TCExitedPayload(std::weak_ptr originator_); + PayloadKind GetKind() const; + std::weak_ptr GetOriginator() const; + std::string DebugString() const final; + + private: + const std::weak_ptr originator; +}; + +// Test Coroutine Destructing +class TCDestructingPayload : public Payload { + public: + explicit TCDestructingPayload(std::weak_ptr originator_); + PayloadKind GetKind() const; + std::weak_ptr GetOriginator() const; + std::string DebugString() const final; + + private: + const std::weak_ptr originator; +}; + +} // namespace crf +} // namespace testing + +#endif diff --git a/coroutines/include/cotest/internal/cotest-crf-synch.h b/coroutines/include/cotest/internal/cotest-crf-synch.h new file mode 100644 index 000000000..9dae6076f --- /dev/null +++ b/coroutines/include/cotest/internal/cotest-crf-synch.h @@ -0,0 +1,49 @@ +#ifndef COROUTINES_INCLUDE_CORO_INTERNAL_COTEST_CRF_SYNCH_H_ +#define COROUTINES_INCLUDE_CORO_INTERNAL_COTEST_CRF_SYNCH_H_ + +#include +#include + +#include "cotest-crf-core.h" +#include "cotest-crf-payloads.h" +#include "cotest-util-types.h" + +namespace testing { +namespace crf { + +// ------------------ Classes ------------------ + +class MockRoutingSession; + +class InteriorEventSession; + +template +class InteriorSignatureMockCS; + +class InteriorLaunchSessionBase; + +template +class InteriorLaunchSession; + +class TestCoroutine; + +class PreMockSynchroniser : public virtual MessageNode { + public: + ReplyPair ReceiveMessage(std::unique_ptr &&to_node) override; + std::string DebugString() const override; + + static PreMockSynchroniser *GetInstance(); + + private: + enum class State { Idle, PassToMain, Start, Working, Complete, WaitingForAck }; + State state = State::Idle; + + std::unique_ptr current_pre_mock; + std::queue> send_pm_to; + static PreMockSynchroniser instance; +}; + +} // namespace crf +} // namespace testing + +#endif diff --git a/coroutines/include/cotest/internal/cotest-crf-test.h b/coroutines/include/cotest/internal/cotest-crf-test.h new file mode 100644 index 000000000..82659e340 --- /dev/null +++ b/coroutines/include/cotest/internal/cotest-crf-test.h @@ -0,0 +1,273 @@ +#ifndef COROUTINES_INCLUDE_CORO_INTERNAL_COTEST_CRF_TEST_H_ +#define COROUTINES_INCLUDE_CORO_INTERNAL_COTEST_CRF_TEST_H_ + +#include +#include + +#include "cotest-crf-core.h" +#include "cotest-crf-payloads.h" +#include "cotest-util-types.h" +#include "gmock/internal/gmock-internal-utils.h" + +namespace testing { +namespace crf { + +// ------------------ Classes ------------------ + +using coro_impl::MakePayload; +using coro_impl::PeekPayload; +using coro_impl::PtrToString; +using coro_impl::SpecialisePayload; + +class MockRoutingSession; + +class InteriorEventSession; + +template +class InteriorSignatureMockCS; + +class InteriorLaunchSessionBase; + +template +class InteriorLaunchSession; + +class TestCoroutine : public CoroutineBase, public std::enable_shared_from_this { + public: + using OnExitFunction = std::function; + + using CoroutineBase::CoroutineBase; + TestCoroutine(coro_impl::BodyFunction cofn_, std::string name_, OnExitFunction on_exit_function_); + ~TestCoroutine(); + + ReplyPair ReceiveMessage(std::unique_ptr &&to_node) override; + ReplyPair IterateServer(std::unique_ptr &&to_coro); + void InitialActivity(); + + void YieldServer(std::unique_ptr &&from_coro); + bool IsPendingEvent(); + + template + std::shared_ptr> Launch(internal::LaunchLambdaType &&user_lambda, std::string name); + + std::shared_ptr NextEvent(const char *file, int line); + bool IsPostMockIterationRequested(); + void DestructionIterations(); + std::string DebugString() const override; + + private: + const OnExitFunction on_exit_function; + std::unique_ptr next_payload; + bool mock_call_locked = false; + bool extra_iteration_requested = false; +}; + +class InteriorLaunchSessionBase : public std::enable_shared_from_this { + public: + InteriorLaunchSessionBase() = default; + InteriorLaunchSessionBase(const InteriorLaunchSessionBase &i) = delete; + InteriorLaunchSessionBase(InteriorLaunchSessionBase &&i) = delete; + InteriorLaunchSessionBase &operator=(const InteriorLaunchSessionBase &) = delete; + InteriorLaunchSessionBase &operator=(InteriorLaunchSessionBase &&) = delete; + ~InteriorLaunchSessionBase(); + + InteriorLaunchSessionBase(TestCoroutine *test_coroutine_, std::string dc_name_); + + void SetLaunchCompleted(); + + TestCoroutine *GetParentTestCoroutine() const; + std::string GetLaunchText() const; + + private: + TestCoroutine *const parent_coroutine; + bool launch_completed = false; + const std::string launch_text; +}; + +template +class InteriorLaunchSession : public InteriorLaunchSessionBase { + public: + InteriorLaunchSession(TestCoroutine *test_coroutine_, std::string dc_name_); + ~InteriorLaunchSession(); + + void Launch(internal::LaunchLambdaType &&user_lambda); + R GetResult(const InteriorEventSession *event) const; +}; + +class InteriorEventSession { + public: + InteriorEventSession() = delete; + InteriorEventSession(const InteriorEventSession &i) = delete; + InteriorEventSession(InteriorEventSession &&i) = delete; + InteriorEventSession &operator=(const InteriorEventSession &) = delete; + InteriorEventSession &operator=(InteriorEventSession &&) = delete; + virtual ~InteriorEventSession() = default; + + InteriorEventSession(TestCoroutine *test_coroutine_, bool via_main_, + std::shared_ptr via_launch_); + + virtual bool IsLaunchResult() const = 0; + virtual bool IsLaunchResult(InteriorLaunchSessionBase *launch_session) const = 0; + virtual bool IsMockCall() const = 0; + virtual void Drop() = 0; + virtual void Accept() = 0; + virtual void Return() = 0; + bool IsFrom(InteriorLaunchSessionBase *source); + virtual UntypedReturnValuePointer GetUntypedLaunchResult() const = 0; + + protected: + TestCoroutine *GetTestCoroutine() const; + + private: + TestCoroutine *const test_coroutine; + const bool via_main; + const std::weak_ptr via_launch; +}; + +class InteriorMockCallSession : public InteriorEventSession, + public std::enable_shared_from_this { + public: + InteriorMockCallSession(TestCoroutine *test_coroutine_, bool via_main_, + std::shared_ptr via_launch_, + std::unique_ptr &&payload_); + ~InteriorMockCallSession(); + + bool IsLaunchResult() const override; + bool IsLaunchResult(InteriorLaunchSessionBase *launch_session) const override; + bool IsMockCall() const override; + + std::string GetName() const; + UntypedMockObjectPointer GetMockObject() const; + UntypedMockerPointer GetMocker() const; + void SeenCall(UntypedArgsPointer args_); + + template + const typename internal::Function::ArgumentTuple *GetArgumentTuple() const; + + void Drop() override; + void Accept() override; + void Return() override; + UntypedReturnValuePointer GetUntypedLaunchResult() const override; + + void ReturnImpl(UntypedReturnValuePointer return_val_ptr); + bool IsReturned() const; + + private: + std::weak_ptr originator; + UntypedMockerPointer mocker; + UntypedMockObjectPointer mock_object; + std::string name; + enum class State { PreMock, Seen, Dropped, Accepted, Returned }; + State state = State::PreMock; + UntypedArgsPointer args; +}; + +class InteriorLaunchResultSession final : public InteriorEventSession { + public: + InteriorLaunchResultSession(TestCoroutine *test_coroutine_, std::unique_ptr &&payload_); + + bool IsLaunchResult() const override; + bool IsLaunchResult(InteriorLaunchSessionBase *launch_session) const override; + bool IsMockCall() const override; + + void Drop() override; + void Accept() override; + void Return() override; + UntypedReturnValuePointer GetUntypedLaunchResult() const override; + + private: + const std::weak_ptr originator; + + UntypedReturnValuePointer const return_value = nullptr; +}; + +template +class InteriorSignatureMockCS { + using FN = typename internal::Function; + using ArgumentTuple = typename FN::ArgumentTuple; + + public: + ~InteriorSignatureMockCS(); + + InteriorSignatureMockCS(InteriorMockCallSession *mcs_, const ArgumentTuple *args_tuple_); + + const ArgumentTuple *GetArgumentTuple() const; + + template + void Return(U &&retval); + + private: + InteriorMockCallSession *const mcs; + const ArgumentTuple *const args_tuple; +}; + +// ------------------ Templated members ------------------ + +template +std::shared_ptr> TestCoroutine::Launch(internal::LaunchLambdaType &&user_lambda, + std::string df_name) { + COTEST_ASSERT(!next_payload && "Launch(): must use NextEvent() to collect an event first"); + const auto ils = std::make_shared>(this, df_name); + ils->Launch(std::move(user_lambda)); + return ils; +} + +template +InteriorLaunchSession::InteriorLaunchSession(TestCoroutine *test_coroutine_, std::string dc_name_) + : InteriorLaunchSessionBase(test_coroutine_, dc_name_) {} + +template +InteriorLaunchSession::~InteriorLaunchSession() {} + +template +void InteriorLaunchSession::Launch(internal::LaunchLambdaType &&user_lambda) { + internal::LaunchLambdaWrapperType wrapper_lambda = + internal::CotestTypeUtils::WrapLaunchLambda(std::move(user_lambda)); + // Note that user_lambda captures all by reference and these captures will not + // be safe once launch coroutine yields eg to generate a mock call, so we need + // to iterate the launch coroutine immediately. It is assumed that the lambda + // is just a function call and this call should normally take arguments by + // value in order to be safe. Args passed by reference need to be checked by + // the user. + auto call_payload = MakePayload(shared_from_this(), wrapper_lambda, GetLaunchText()); + GetParentTestCoroutine()->YieldServer(std::move(call_payload)); +} + +template +R InteriorLaunchSession::GetResult(const InteriorEventSession *event) const { + return internal::CotestTypeUtils::Specialise(event->GetUntypedLaunchResult()); +} + +template +const typename internal::Function::ArgumentTuple *InteriorMockCallSession::GetArgumentTuple() const { + COTEST_ASSERT(state != State::Returned); + COTEST_ASSERT(IsMockCall()); + using FN = typename internal::Function; + return static_cast(args); +} + +template +InteriorSignatureMockCS::InteriorSignatureMockCS(InteriorMockCallSession *const mcs_, + const ArgumentTuple *args_tuple_) + : mcs(mcs_), args_tuple(args_tuple_) {} + +template +InteriorSignatureMockCS::~InteriorSignatureMockCS() {} + +template +const typename InteriorSignatureMockCS::ArgumentTuple * +InteriorSignatureMockCS::GetArgumentTuple() const { + COTEST_ASSERT(!mcs->IsReturned()); // Cannot rely on args after return - take a copy! + return args_tuple; +} + +template +template +void InteriorSignatureMockCS::Return(U &&retval) { + const UntypedReturnValuePointer p = internal::CotestTypeUtils::Generalise(std::forward(retval)); + mcs->ReturnImpl(p); +} + +} // namespace crf +} // namespace testing + +#endif diff --git a/coroutines/include/cotest/internal/cotest-integ-finder.h b/coroutines/include/cotest/internal/cotest-integ-finder.h new file mode 100644 index 000000000..5805e9ea2 --- /dev/null +++ b/coroutines/include/cotest/internal/cotest-integ-finder.h @@ -0,0 +1,66 @@ +#ifndef COROUTINES_INCLUDE_CORO_COTEST_INTEG_FINDER_H_ +#define COROUTINES_INCLUDE_CORO_COTEST_INTEG_FINDER_H_ + +#include +#include +#include +#include +#include + +#include "cotest-crf-core.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace testing { +namespace internal { + +using MockHandlerScheme = UntypedFunctionMockerBase::UntypedExpectations; + +class MockHandler { + protected: + MockHandler(); + MockHandler(const MockHandler &) = default; + MockHandler(MockHandler &&) = default; + MockHandler &operator=(const MockHandler &) = default; + MockHandler &operator=(MockHandler &&) = default; + virtual ~MockHandler(); + + public: + virtual const MockHandlerScheme *GetMockHandlerScheme() const = 0; + virtual std::string GetName() const = 0; + virtual std::shared_ptr GetCRFTestCoroutine() = 0; +}; + +class CotestMockHandlerPool : public AlternateMockCallManager { + public: + using ShouldHandleCallPredicate = std::function; + using ForTestCoroutineLambda = std::function &&crf_sp)>; + + static CotestMockHandlerPool *GetOrCreateInstance(); + + void AddOwnerLocked(MockHandler *owner); + void RemoveOwnerLocked(MockHandler *owner); + void AddExpectation(std::function creator); + + void PreMockUnlocked(const UntypedFunctionMockerBase *mocker, const void *mock_obj, const char *name) override; + void ForAll(ForTestCoroutineLambda &&lambda); + + // Cannot be a template since we want a virtual interface to keep the + // dependency into gmock weak + bool IsUninteresting(const UntypedFunctionMockerBase *mocker, const void *untyped_args) const override; + + ExpectationBase *FindMatchingExpectationLocked(const UntypedFunctionMockerBase *mocker, const void *untyped_args, + bool *is_mocker_exp) const override; + + static ExpectationBase *Finder(std::vector &schemes, ShouldHandleCallPredicate predicate, + unsigned *which); + + private: + bool got_untyped_watchers = false; + std::set owners; +}; + +} // namespace internal +} // namespace testing + +#endif diff --git a/coroutines/include/cotest/internal/cotest-integ-mock.h b/coroutines/include/cotest/internal/cotest-integ-mock.h new file mode 100644 index 000000000..aa792835a --- /dev/null +++ b/coroutines/include/cotest/internal/cotest-integ-mock.h @@ -0,0 +1,445 @@ +#ifndef COROUTINES_INCLUDE_CORO_INTERNAL_INTEG_LAYER_H_ +#define COROUTINES_INCLUDE_CORO_INTERNAL_INTEG_LAYER_H_ + +#include + +#include "cotest-crf-launch.h" +#include "cotest-crf-test.h" +#include "cotest-integ-finder.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace testing { + +// ------------------ Classes ------------------ + +template +class LaunchHandle; + +class EventHandle; + +template +class SignatureHandle; + +namespace internal { + +class UntypedFunctionMockerBase; // A GMock class + +template +class CotestExpectationFactory; + +template +class CotestWatcher; + +class CotestCardinality; + +class RAIISetFlag; + +class Coroutine : public MockHandler { + public: + using BodyFunctionType = std::function; + + Coroutine() = delete; + Coroutine(const Coroutine &i) = delete; + Coroutine(Coroutine &&i); + Coroutine &operator=(const Coroutine &) = delete; + Coroutine &operator=(Coroutine &&) = delete; + ~Coroutine(); + + Coroutine(BodyFunctionType body, std::string name); + + template + LaunchHandle Launch(LaunchLambdaType &&user_lambda, std::string name); + + void WatchCall(const char *file, int line, crf::UntypedMockObjectPointer obj = nullptr); + + template + TypedExpectation &WatchCall(MockSpec &&mock_spec, const char *file, int line, + const char *obj, const char *call); + void SetSatisfied(); + void Retire(); + bool IsRetired() const; + std::string GetName() const override; + + EventHandle NextEvent(const char *file, int line); + + std::shared_ptr GetCRFTestCoroutine() override; + + void OnTestCoroExit(); + void OnWatcherDestruct(); + void AddWatcher(std::shared_ptr watcher); + static RAIISetFlag RAIIGMockMutexIsLocked(); + + private: + const MockHandlerScheme *GetMockHandlerScheme() const override; + void DestructionIterations(); + + const std::shared_ptr crf; + const std::string name; + std::list> my_watchers_all; + const std::shared_ptr my_cardinality; + MockHandlerScheme my_untyped_watchers; + bool retired = false; + bool initial_activity_complete = false; + + static bool gmock_mutex_held; // because the mutex is static + + public: + // When cotest UI macros are used with an explicit object eg + // my_coro.WATCH_CALL(), suppress the error caused by the macro + // injecting cotest_coro_-> + Coroutine *const cotest_coro_; +}; + +template +class CotestExpectationFactory : public SpecFactory { + private: + using F = R(Args...); + + public: + CotestExpectationFactory() = delete; + CotestExpectationFactory(Coroutine *coroutine_, CotestCardinality *cardinality_) + : coroutine(coroutine_), cardinality(cardinality_) {} + + using ArgumentMatcherTuple = typename Function::ArgumentMatcherTuple; + + OnCallSpec *CreateOnCall(const char *a_file, int a_line, const ArgumentMatcherTuple &m) override; + std::shared_ptr> CreateExpectation(FunctionMocker *owning_mocker, const char *a_file, + int a_line, const std::string &a_source_text, + const ArgumentMatcherTuple &m) override; + + Coroutine *const coroutine; + CotestCardinality *const cardinality; +}; + +template +class CotestWatcher : public TypedExpectation { + using Result = typename Function::Result; + using ArgumentTuple = typename Function::ArgumentTuple; + using ArgumentMatcherTuple = typename Function::ArgumentMatcherTuple; + + public: + CotestWatcher() = delete; + CotestWatcher(const CotestWatcher &) = delete; + CotestWatcher(CotestWatcher &&) = delete; + CotestWatcher &operator=(const CotestWatcher &) = delete; + CotestWatcher &operator=(CotestWatcher &&) = delete; + ~CotestWatcher() override; + + CotestWatcher(Coroutine *const coroutine_, crf::UntypedMockObjectPointer watched_mock_object_, + FunctionMocker *owning_mocker_, const char *a_file, int a_line, + const std::string &a_source_text, const ArgumentMatcherTuple &m, CotestCardinality *cardinality_); + + void DetachCoroutine() override; + + private: + // Decide whether the coroutine should accept the call. GMock mutex is + // LOCKED during this call. + bool ShouldHandleCall(const UntypedFunctionMockerBase *mocker, const void *untyped_args) override; + + // Call passed exterior matching and must be shown to the coroutine + bool SeenMockCallLocked(const UntypedFunctionMockerBase *mocker, const void *untyped_args); + + // Similar side-effects to to GetActionForArguments() and GetCurrentAction() + bool UpdateCardinality(const UntypedFunctionMockerBase *mocker, const void *untyped_args, ::std::ostream *what, + ::std::ostream *why) override; + + // Implement cotest action here + bool TryPerformAction(const UntypedFunctionMockerBase *mocker, const void *untyped_args, + const void **untyped_return_value) override; + + // We need our own describe function because we haven't set up any + // matchers. At minimum, reveal that a coroutine is being used. + void ExplainMatchResultTo(const ArgumentTuple &args, ::std::ostream *os) const override; + + private: + const crf::UntypedMockObjectPointer watched_mock_object; + const bool is_typed_watcher; + Coroutine *coroutine; + CotestCardinality *const cardinality; +}; + +class CotestCardinality : public CardinalityInterface { + public: + CotestCardinality(); + + bool IsSatisfiedByCallCount(int call_count) const override; + bool IsSaturatedByCallCount(int call_count) const override; + void DescribeTo(::std::ostream *os) const override; + + void InteriorSetSatisfied(); + void OnTestCoroExit(); + bool OnSeenMockCall(); + + private: + enum class State { Unsatisfied, SatisfiedByUser, SatisfiedByExit, Oversaturated }; + State state = State::Unsatisfied; +}; + +class RAIISetFlag { + public: + RAIISetFlag(bool *flag_) : flag(flag_), old_flag(*flag) { + COTEST_ASSERT(flag); + *flag = true; + } + RAIISetFlag(RAIISetFlag &) = delete; + RAIISetFlag &operator=(RAIISetFlag &) = delete; + RAIISetFlag(RAIISetFlag &&) = default; + RAIISetFlag &operator=(RAIISetFlag &&) = delete; + ~RAIISetFlag() { + COTEST_ASSERT(*flag); + *flag = old_flag; + } + + private: + bool *const flag; + bool const old_flag; +}; + +template +SignatureHandle CreateSignatureHandle(MockSpec &&mock_spec) { + return SignatureHandle(); +} + +// ------------------ Templated members ------------------ + +template +LaunchHandle Coroutine::Launch(LaunchLambdaType &&user_lambda, std::string launch_text) { + return LaunchHandle(crf->Launch(std::move(user_lambda), launch_text)); +} + +template +TypedExpectation &Coroutine::WatchCall(MockSpec &&mock_spec, const char *file, int line, + const char *obj, const char *call) { + CotestExpectationFactory factory(this, my_cardinality.get()); + TypedExpectation &new_exp = mock_spec.InternalExpectedAt(&factory, file, line, obj, call); + + // This is only to get the right GMock behaviour + new_exp.set_repeated_action(DoDefault()); + + // Hooking up backend to cardinality interface so we can + // satisfy and saturate. + new_exp.set_cardinality(Cardinality(my_cardinality)); + + return new_exp; +} + +template +OnCallSpec *CotestExpectationFactory::CreateOnCall(const char *a_file, int a_line, + const ArgumentMatcherTuple &m) { + return new OnCallSpec(a_file, a_line, m); +} + +template +std::shared_ptr> CotestExpectationFactory::CreateExpectation( + FunctionMocker *owning_mocker, const char *a_file, int a_line, const std::string &a_source_text, + const ArgumentMatcherTuple &m) { + const auto sp = std::make_shared>(coroutine, nullptr, owning_mocker, a_file, a_line, a_source_text, + m, cardinality); + coroutine->AddWatcher(sp); + if (coroutine->IsRetired()) { + MutexLock l(&g_gmock_mutex); + sp->Retire(); + } + return sp; +} + +template +CotestWatcher::CotestWatcher(Coroutine *const coroutine_, + crf::UntypedMockObjectPointer watched_mock_object_, + FunctionMocker *owning_mocker_, const char *a_file, int a_line, + const std::string &a_source_text, const ArgumentMatcherTuple &m, + CotestCardinality *cardinality__) + : CotestWatcher::TypedExpectation(owning_mocker_, a_file, a_line, a_source_text, m), + watched_mock_object(watched_mock_object_), + // NOTE: the owning_mocker_ could fall out of scope and be destructed + // while we're still alive, so try to avoid using it and if it's really + // necessary, a DetachMocker() mechanism will be needed. + is_typed_watcher(owning_mocker_ != nullptr), + coroutine(coroutine_), + cardinality(cardinality__) {} + +template +CotestWatcher::~CotestWatcher() { + if (coroutine) coroutine->OnWatcherDestruct(); +} + +template +void CotestWatcher::DetachCoroutine() { + std::clog << COTEST_THIS << std::endl; + + coroutine = nullptr; + + if (!is_typed_watcher) { + // Since untyped watches are not registered with GMock registry, we + // need to directly request a cardinality state report. + MutexLock l(&g_gmock_mutex); + ExpectationBase::VerifyExpectationLocked(); + } +} + +template +bool CotestWatcher::ShouldHandleCall(const UntypedFunctionMockerBase *mocker, + crf::UntypedArgsPointer untyped_args) { + RAIISetFlag gmmh(Coroutine::RAIIGMockMutexIsLocked()); // This is called from GMock with + // mutex held + + COTEST_ASSERT(coroutine && "coroutine object was destructed before being sent a call"); + std::clog << "CotestWatcher::ShouldHandleCall() (typed=" << is_typed_watcher << ")..." << std::endl; + COTEST_ASSERT(mocker); + + // Respect retirement state and pre-requisites in gmock exp in case + // they are chained. + if (CotestWatcher::TypedExpectation::is_retired() || + !CotestWatcher::TypedExpectation::AllPrerequisitesAreSatisfied()) + return false; + + // Apply exterior filtering (main and extra) if we've got typed arguments + if (is_typed_watcher) { + // We can use types based on our template arguments + const ArgumentTuple *typed_args = static_cast(untyped_args); + if (this->Matches(*typed_args)) return SeenMockCallLocked(mocker, untyped_args); + } else { + // We cannot use types based on our template arguments + if (!watched_mock_object || watched_mock_object == mocker->MockObjectLocked()) + return SeenMockCallLocked(mocker, untyped_args); + } + return false; +} + +template +bool CotestWatcher::SeenMockCallLocked(const UntypedFunctionMockerBase *mocker, const void *untyped_args) { + std::clog << this << " CotestWatcher::SeenMockCallLocked()..." << std::endl; + + // If oversaturated, return in order to permit cmock to detect this and talk + // to the user about it. + if (cardinality->OnSeenMockCall()) return true; + + const std::shared_ptr mock_source = + crf::LaunchCoroutinePool::GetInstance()->FindActiveMockSource(); + const std::shared_ptr crf_mrs = mock_source->GetCurrentMockRS(); + auto crf_tc = coroutine->GetCRFTestCoroutine(); + crf_mrs->Configure(crf_tc.get()); + + // Permit coroutine to perform interior filtering + const std::string name = mocker->NameLocked(); + const bool accepted = crf_mrs->SeenMockCallLocked(untyped_args); + COTEST_ASSERT(crf::LaunchCoroutinePool::GetInstance()->FindActiveMockSource() == + mock_source); // should still be on same source after the yield + + if (!accepted || coroutine->GetCRFTestCoroutine()->IsCoroutineExited()) { + crf_mrs->Configure(nullptr); + return false; + } + + return true; +} + +template +bool CotestWatcher::UpdateCardinality(const UntypedFunctionMockerBase *mocker, const void *untyped_args, + ::std::ostream *what, ::std::ostream *why) { + // Note: code lifted from GetActionForArguments() and GetCurrentAction() + // without any attempt at integration into cotest. + std::clog << "CotestWatcher::UpdateCardinality() (typed=" << is_typed_watcher << ")..." << std::endl; + g_gmock_mutex.AssertHeld(); + RAIISetFlag gmmh(Coroutine::RAIIGMockMutexIsLocked()); // This is called from GMock with + // mutex held + + using Base = ExpectationBase; + + const ::std::string &expectation_description = ExpectationBase::GetDescription(); + if (Base::IsSaturated()) { + // We have an excessive call. + Base::IncrementCallCount(); + *what << "Mock function "; + if (!expectation_description.empty()) { + *what << "\"" << expectation_description << "\" "; + } + *what << "called more times than expected - "; + // mocker->DescribeDefaultActionTo(args, what); + Base::DescribeCallCountTo(why); + + return false; + } + + Base::IncrementCallCount(); + Base::RetireAllPreRequisites(); + + if (Base::retires_on_saturation_ && Base::IsSaturated()) { + Base::Retire(); + } + + // Must be done after IncrementCount()! + *what << "Mock function "; + if (!expectation_description.empty()) { + *what << "\"" << expectation_description << "\" "; + } + *what << "call matches " << Base::source_text() << "...\n"; + + const int count = Base::call_count(); + COTEST_ASSERT(count >= 1); + //"call_count() is <= 0 when UpdateCardinality() is " + //"called - this should never happen."); + + const int action_count = static_cast(Base::untyped_actions_.size()); + if (action_count > 0 && !Base::repeated_action_specified_ && count > action_count) { + // If there is at least one WillOnce() and no WillRepeatedly(), + // we warn the user when the WillOnce() clauses ran out. + ::std::stringstream ss; + Base::DescribeLocationTo(&ss); + ss << "Actions ran out in " << Base::source_text() << "...\n" + << "Called " << count << " times, but only " << action_count << " WillOnce()" + << (action_count == 1 ? " is" : "s are") << " specified - "; + // mocker->DescribeDefaultActionTo(args, &ss); + Log(kWarning, ss.str(), 1); + } + + return Base::repeated_action_specified_; +} + +template +bool CotestWatcher::TryPerformAction(const UntypedFunctionMockerBase *mocker, const void *untyped_args, + const void **untyped_return_value) { + // Note: this is called from GMock WITHOUT mutex held + const std::shared_ptr mock_source = + crf::LaunchCoroutinePool::GetInstance()->FindActiveMockSource(); + const std::shared_ptr crf_mrs = mock_source->GetCurrentMockRS(); + std::clog << "CotestWatcher::TryPerformAction() (typed=" << is_typed_watcher << " ms=" << mock_source << ")..." + << std::endl; + + COTEST_ASSERT(mocker); + COTEST_ASSERT(coroutine); // coroutine object was destructed before being sent a call + + // Let the coroutine run and collect the return value + *untyped_return_value = crf_mrs->ActionsAndReturnUnlocked(); + + if (is_typed_watcher) COTEST_ASSERT(CotestTypeUtils::NullCheck(*untyped_return_value)); + + crf_mrs->Configure(nullptr); + + return true; +} + +template +void CotestWatcher::ExplainMatchResultTo(const CotestWatcher::ArgumentTuple &args, + ::std::ostream *os) const { + RAIISetFlag gmmh(Coroutine::RAIIGMockMutexIsLocked()); // This is called from GMock with + // mutex held + const std::shared_ptr mock_source = + crf::LaunchCoroutinePool::GetInstance()->FindActiveMockSource(); + + // TOOD improve in phase 3 + + if (CotestWatcher::TypedExpectation::is_retired()) { + *os << " Expected: the coroutine is active\n" + << " Actual: it is retired\n"; + } else { + *os << " Expected: determined by coroutine\n" + << " Actual: mock call dropped or not seen\n"; + } +} + +} // namespace internal +} // namespace testing + +#endif diff --git a/coroutines/include/cotest/internal/cotest-util-logging.h b/coroutines/include/cotest/internal/cotest-util-logging.h new file mode 100644 index 000000000..070bf1d3b --- /dev/null +++ b/coroutines/include/cotest/internal/cotest-util-logging.h @@ -0,0 +1,48 @@ +#ifndef COROUTINES_INCLUDE_CORO_COTEST_UTIL_LOGGING_H_ +#define COROUTINES_INCLUDE_CORO_COTEST_UTIL_LOGGING_H_ + +//#define COMPARING_LOGS + +#include // setfill etc +#include // std::stringstream + +namespace coro_impl { + +#define COTEST_STR(S) COTEST_STRW(S) +#define COTEST_STRW(S) #S +#define COTEST_ASSERT(COND) \ + do { \ + if (!(COND)) { \ + std::cerr << __FILE__ << ":" << __LINE__ << std::endl \ + << " COTEST_ASSERT failed: " << COTEST_STR(COND) << std::endl; \ + std::abort(); \ + } \ + } while (0) + +inline std::string PtrToString(const void *p) { + if (!p) return "@NULL"; + +#ifdef COMPARING_LOGS + return "@PTR"; +#else + const int biggest_prime_below_1000 = 997; + int ptrhash = reinterpret_cast(p) % biggest_prime_below_1000; + + std::stringstream ss; + ss << "@" << std::setfill('0') << std::setw(3) << ptrhash; + return ss.str(); +#endif +} + +template +inline std::string PtrToString(const std::pair &p) { + return "(" + PtrToString(p.first) + ", " + PtrToString(p.second) + ")"; +} + +// TODO proper logging system +#define COTEST_THIS this << "::" << __func__ << "()" +#define COTEST_THIS_FL COTEST_THIS << " " << file << ":" << line + +} // namespace coro_impl + +#endif diff --git a/coroutines/include/cotest/internal/cotest-util-types.h b/coroutines/include/cotest/internal/cotest-util-types.h new file mode 100644 index 000000000..0ee9d536b --- /dev/null +++ b/coroutines/include/cotest/internal/cotest-util-types.h @@ -0,0 +1,96 @@ +#ifndef COROUTINES_INCLUDE_CORO_INTERNAL_COTEST_UTIL_TYPES_H_ +#define COROUTINES_INCLUDE_CORO_INTERNAL_COTEST_UTIL_TYPES_H_ + +#include +#include + +namespace testing { +namespace internal { + +template +using LaunchLambdaType = std::function; +using LaunchYielder = std::function; +using LaunchLambdaWrapperType = std::function; + +template +static void YieldWithAddressOfReturn(R &&returned_object, LaunchYielder yielder) { + // Get past refusing to take the address of a temporary by turning + // it into a named rvalue. Returned_object lasts until yielder returns, + // at which point we will have yielded the value back to the coro + // via its untyped address. Obviously the temporary only lasts as + // long as a single yield cycle of the launch coro. + yielder(static_cast(&returned_object)); +} + +template +struct CotestTypeUtils { + using StorableR = typename std::remove_reference::type; + + static const void *Generalise(R &&typed_value) { return &typed_value; } + + static const void *Generalise(R &typed_value) { return &typed_value; } + + static R &&Specialise(const void *untyped_value) { + if (!untyped_value) std::terminate(); // Implementation should use NullCheck() first + auto untyped_return_value_nc = const_cast(untyped_value); + // Returning std::move() is required because R may be unique_ptr or similar + return std::move(*static_cast(untyped_return_value_nc)); + } + + static bool NullCheck(const void *untyped_value) // return true if correct + { + return !!untyped_value; // non-NULL is correct + } + + static LaunchLambdaWrapperType WrapLaunchLambda(LaunchLambdaType user_lambda) { + return [user_lambda](LaunchYielder yielder) { YieldWithAddressOfReturn(user_lambda(), yielder); }; + } +}; + +template +struct CotestTypeUtils::value>::type> { + using StorableR = typename std::remove_reference::type; + + static const void *Generalise(R &typed_value) { return &typed_value; } + + static R Specialise(const void *untyped_value) { + if (!untyped_value) std::terminate(); // Implementation should use NullCheck() first + auto untyped_return_value_nc = const_cast(untyped_value); + return *static_cast(untyped_return_value_nc); + } + + static bool NullCheck(const void *untyped_value) // return true if correct + { + return !!untyped_value; // non-NULL is correct + } + + static LaunchLambdaWrapperType WrapLaunchLambda(LaunchLambdaType user_lambda) { + return [user_lambda](LaunchYielder yielder) { YieldWithAddressOfReturn(user_lambda(), yielder); }; + } +}; + +template <> +struct CotestTypeUtils { + inline static void Specialise(const void *untyped_value) { + if (untyped_value) std::terminate(); // Implementation should use NullCheck() first + (void)untyped_value; + return; + } + + static bool NullCheck(const void *untyped_value) // return true if correct + { + return !untyped_value; // NULL is correct + } + + inline static LaunchLambdaWrapperType WrapLaunchLambda(LaunchLambdaType user_lambda) { + return [user_lambda](LaunchYielder yielder) { + user_lambda(); + yielder(nullptr); + }; + } +}; + +} // namespace internal +} // namespace testing + +#endif diff --git a/coroutines/src/cotest-all.cc b/coroutines/src/cotest-all.cc new file mode 100644 index 000000000..99d1ef7c7 --- /dev/null +++ b/coroutines/src/cotest-all.cc @@ -0,0 +1,10 @@ +#include "src/cotest-coro-thread.cc" +#include "src/cotest-crf-core.cc" +#include "src/cotest-crf-launch.cc" +#include "src/cotest-crf-mock.cc" +#include "src/cotest-crf-payloads.cc" +#include "src/cotest-crf-synch.cc" +#include "src/cotest-crf-test.cc" +#include "src/cotest-integ-finder.cc" +#include "src/cotest-integ-mock.cc" +#include "src/cotest.cc" diff --git a/coroutines/src/cotest-coro-thread.cc b/coroutines/src/cotest-coro-thread.cc new file mode 100644 index 000000000..b2a1573f5 --- /dev/null +++ b/coroutines/src/cotest-coro-thread.cc @@ -0,0 +1,123 @@ +#include "cotest/internal/cotest-coro-thread.h" + +#include + +#include + +#include "cotest/internal/cotest-util-logging.h" + +namespace coro_impl { + +CoroOnThread::CoroOnThread(BodyFunction cofn_, std::string name_) : coro_run_function(cofn_), name(name_) { + local_thread = std::thread(&CoroOnThread::ThreadRun, this); + TrySetThreadName(); +} + +CoroOnThread::~CoroOnThread() { + COTEST_ASSERT(IsCoroutineExited()); + local_thread.join(); +} + +std::unique_ptr CoroOnThread::Iterate(std::unique_ptr &&to_coro) { + COTEST_ASSERT(phase != Phase::CoroutineExited); + COTEST_ASSERT(!payload); + COTEST_ASSERT(!payload_ex); + payload = std::move(to_coro); + NotifyPhase(Phase::CoroutineRuns); + WaitPhases({Phase::MainRuns, Phase::CoroutineExited}); + return std::move(payload); +} + +std::unique_ptr CoroOnThread::ThrowIn(std::exception_ptr in_ex) { + COTEST_ASSERT(phase != Phase::CoroutineExited); + COTEST_ASSERT(!payload); + COTEST_ASSERT(!payload_ex); + payload_ex = std::move(in_ex); + NotifyPhase(Phase::CoroutineRuns); + WaitPhases({Phase::MainRuns, Phase::CoroutineExited}); + return std::move(payload); +} + +void CoroOnThread::Cancel() { + // We do not support yields in destructors in the coro + std::unique_ptr coro_resp = ThrowIn(MakeException()); + COTEST_ASSERT(!coro_resp); // not expecting response to cancellation + COTEST_ASSERT(phase == Phase::CoroutineExited); +} + +bool CoroOnThread::IsCoroutineExited() const { + std::lock_guard lk(phase_mutex); + return phase == Phase::CoroutineExited; +} + +void CoroOnThread::SetName(std::string name_) { + name = name_; + TrySetThreadName(); +} + +std::string CoroOnThread::GetName() const { return name; } + +std::unique_ptr CoroOnThread::Yield(std::unique_ptr &&from_coro) { + COTEST_ASSERT(!payload); + payload = std::move(from_coro); + NotifyPhase(Phase::MainRuns); + WaitPhases({Phase::CoroutineRuns}); + COTEST_ASSERT(!(payload && payload_ex)); + if (payload_ex) + std::rethrow_exception(payload_ex); + else + return std::move(payload); +} + +void CoroOnThread::ThreadRun() { + WaitPhases({Phase::CoroutineRuns}); + COTEST_ASSERT(!payload); // coro starts up without a message + try { + if (payload_ex) std::rethrow_exception(payload_ex); + coro_run_function(); + payload.reset(); + } catch (const CancellationException &exc) { + payload_ex = nullptr; + } + // At present we don't catch other exceptions and so they cause a terminate. + // We could re-throw into exterior, but should only catch outside the scope + // of the coro, at which point the exception caused a cancel (effectively) + // on both sides. + NotifyPhase(Phase::CoroutineExited); +} + +void CoroOnThread::TrySetThreadName() { + std::thread::native_handle_type handle = local_thread.native_handle(); +#ifdef __linux__ + // Do not fail for OS's that support setting a name + const int max = 15; + // The last n characters are more likely to be distinct + std::string trimmed_name = name.size() <= max ? name : "..." + name.substr(name.size() - (max - 3)); + COTEST_ASSERT(pthread_setname_np(handle, trimmed_name.c_str()) == 0); +#endif + // COT-10 to add support for MSVC +} + +void CoroOnThread::NotifyPhase(Phase new_phase) { + std::lock_guard lk(phase_mutex); + phase = new_phase; + if (new_phase == Phase::CoroutineRuns) { + COTEST_ASSERT(!active); + active = this; + } else { + COTEST_ASSERT(active); + active = nullptr; + } + cv.notify_one(); +} + +void CoroOnThread::WaitPhases(std::set phases) { + std::unique_lock lk(phase_mutex); + cv.wait(lk, [&] { return phases.count(phase) > 0; }); +} + +InteriorInterface *CoroOnThread::GetActive() { return active; } + +InteriorInterface *CoroOnThread::active = nullptr; + +} // namespace coro_impl diff --git a/coroutines/src/cotest-crf-core.cc b/coroutines/src/cotest-crf-core.cc new file mode 100644 index 000000000..2f50ddb35 --- /dev/null +++ b/coroutines/src/cotest-crf-core.cc @@ -0,0 +1,147 @@ +#include "cotest/internal/cotest-crf-core.h" + +#include + +#include "cotest/internal/cotest-crf-mock.h" +#include "cotest/internal/cotest-crf-synch.h" +#include "cotest/internal/cotest-integ-finder.h" +#include "cotest/internal/cotest-util-logging.h" + +namespace testing { +namespace crf { + +using coro_impl::PtrToString; + +std::unique_ptr MessageNode::SendMessageFromMain(MessageNode *dest, std::unique_ptr &&to_node) { + // Fill in main as sender and then check return is for main + return MessageLoop(dest, std::move(to_node)); +} + +std::unique_ptr MessageNode::MessageLoop(MessageNode *dest, std::unique_ptr &&to_node) { + // std::clog << __func__ << "() starts: dispatching " << to_node.get() << " to + // " << dest->DebugString() << std::endl; + + while (1) { + // Dispatch a message + COTEST_ASSERT(dest); + + std::unique_ptr reply; + MessageNode *reply_dest; + std::tie(reply, reply_dest) = dest->ReceiveMessage(std::move(to_node)); + + // std::clog << __func__ << "() iterates: dispatching " << reply.get() << " + // to " << (reply_dest ? reply_dest->DebugString() : "NULL") << std::endl; + COTEST_ASSERT(reply_dest && "MessageNode did not provide reply destination"); + + // Messages for main are just returned to caller, which should be main + if (reply_dest == ProxyForMain::GetInstance().get()) return reply; + + // For the next iteration... + dest = reply_dest; + to_node = std::move(reply); + } + + // std::clog << __func__ << "() returns to main" << std::endl; +} + +CoroutineBase::~CoroutineBase() { COTEST_ASSERT(IsCoroutineExited()); } + +CoroutineBase::CoroutineBase(coro_impl::BodyFunction cofn, std::string name_) + : name(std::move(name_)), impl(std::make_unique(cofn, name)) {} + +std::unique_ptr CoroutineBase::Iterate(std::unique_ptr &&to_coro) { + // std::clog << ActiveStr() << COTEST_THIS << " iterates with " << + // to_coro.get() << std::endl; + initial = false; + std::unique_ptr from_coro_base = impl->Iterate(std::move(to_coro)); + // if( IsCoroutineExited() ) + // std::clog << ActiveStr() << COTEST_THIS << " exited" << std::endl; + if (from_coro_base) + return SpecialisePayload(std::move(from_coro_base)); + else + return nullptr; +} + +void CoroutineBase::Cancel() { + // std::clog << ActiveStr() << COTEST_THIS << " cancelling" << std::endl; + COTEST_ASSERT(!IsCoroutineExited()); + impl->Cancel(); +} + +bool CoroutineBase::IsCoroutineExited() const { return impl->IsCoroutineExited(); } + +std::unique_ptr CoroutineBase::Yield(std::unique_ptr &&from_coro) { + // std::clog << ActiveStr() << COTEST_THIS << " yields " << from_coro.get() + // << std::endl; + std::unique_ptr to_coro_base = impl->Yield(std::move(from_coro)); + // We have to assume this is a CRF payload. In practice, they all are. + if (to_coro_base) + return SpecialisePayload(std::move(to_coro_base)); + else + return nullptr; +} + +void CoroutineBase::SetName(std::string name_) { + name = name_; + impl->SetName(name); +} + +std::string CoroutineBase::GetName() const { return name; } + +std::string CoroutineBase::ActiveStr() const { + auto *active = static_cast(impl->GetActive()); + if (!active) + return ">< "; // running in main context + else if (active == impl.get()) + return "[] "; + else + return "[" + active->GetName() + "!!] "; // Wrong coro is active - probably an internal error +} + +coro_impl::InteriorInterface *CoroutineBase::GetImpl() { + return static_cast(impl.get()); +} + +void MockSource::SetCurrentMockRS(std::shared_ptr current_mock_call_) { + COTEST_ASSERT(current_mock_call_); + current_mock_rs = current_mock_call_; +} + +std::shared_ptr MockSource::GetCurrentMockRS() const { + COTEST_ASSERT(current_mock_rs); + return current_mock_rs; +} + +std::shared_ptr ProxyForMain::CreateMockRoutingSession(UntypedMockerPointer mocker_, + UntypedMockObjectPointer mock_obj_, + const char *name_) { + auto mrs_sp = std::make_shared(mocker_, mock_obj_, name_); + SetCurrentMockRS(mrs_sp); + return mrs_sp; +} + +LaunchCoroutine *ProxyForMain::GetAsCoroutine() { + COTEST_ASSERT(!"Internal error: attempting to get a coroutine for main (as a mock source)"); +} + +const LaunchCoroutine *ProxyForMain::GetAsCoroutine() const { + COTEST_ASSERT(!"Internal error: attempting to get a coroutine for main (as a mock source)"); +} + +std::shared_ptr ProxyForMain::GetInstance() { + static auto instance = std::make_shared(); + return instance; +} + +MessageNode::ReplyPair ProxyForMain::ReceiveMessage(std::unique_ptr &&to_node) { + COTEST_ASSERT(!"Messages should be dispatched to main by returning from the message loop (internal error)"); +} + +std::string ProxyForMain::DebugString() const { + std::stringstream ss; + ss << "ProxyForMain()"; + return ss.str(); +} + +} // namespace crf +} // namespace testing diff --git a/coroutines/src/cotest-crf-launch.cc b/coroutines/src/cotest-crf-launch.cc new file mode 100644 index 000000000..c097f38a5 --- /dev/null +++ b/coroutines/src/cotest-crf-launch.cc @@ -0,0 +1,218 @@ +#include "cotest/internal/cotest-crf-launch.h" + +#include + +#include "cotest/internal/cotest-crf-synch.h" +#include "cotest/internal/cotest-crf-test.h" +#include "cotest/internal/cotest-integ-finder.h" +#include "cotest/internal/cotest-util-logging.h" + +namespace testing { +namespace crf { + +using coro_impl::PtrToString; + +LaunchCoroutine::LaunchCoroutine(std::string name_) : CoroutineBase(std::bind(&LaunchCoroutine::Body, this), name_) { + // Permit the coroutine to yield in order to get its first message + std::unique_ptr from_coro = Iterate(nullptr); + COTEST_ASSERT(!from_coro); +} + +LaunchCoroutine::~LaunchCoroutine() { + if (!IsCoroutineExited()) Cancel(); +} + +void LaunchCoroutine::Body() { + // Get the first launch + std::unique_ptr to_coro = Yield(nullptr); + while (1) { + COTEST_ASSERT(to_coro->GetKind() == PayloadKind::Launch); + auto launch_payload = SpecialisePayload(std::move(to_coro)); + + internal::LaunchLambdaWrapperType lambda_wrapper = launch_payload->GetLambdaWrapper(); + + // The idea is that something will invoke this "yielder" while the return + // object is still in scope. + internal::LaunchYielder yielder = [this, &launch_payload, &to_coro](UntypedReturnValuePointer rvp) { + auto launch_result_payload = + MakePayload(launch_payload->GetOriginator(), shared_from_this(), rvp); + to_coro = Yield(std::move(launch_result_payload)); + }; + + // Invoking the MUT here + std::clog << ActiveStr() << COTEST_THIS << " launching lambda_wrapper for \"" << GetName() << "\"" << std::endl; + lambda_wrapper(yielder); + + std::clog << ActiveStr() << COTEST_THIS << " completed launch" << std::endl; + COTEST_ASSERT(to_coro); // Was set via lambda ref capture + } +} + +MessageNode::ReplyPair LaunchCoroutine::ReceiveMessage(std::unique_ptr &&to_node) { + // Collect a pointer to the current launch session, so that we can effectively + // garbage-collect the launch lambda context which is holding the return value + // as a temporary. We use a weak pointer, and will expire when the launch + // session is destructed + if (to_node && to_node->GetKind() == PayloadKind::Launch) + current_launch_session = PeekPayload(to_node).GetOriginator(); + + return IterateServer(std::move(to_node)); +} + +MessageNode::ReplyPair LaunchCoroutine::IterateServer(std::unique_ptr &&to_coro) { + COTEST_ASSERT(!IsCoroutineExited()); + + // It does not make sense to sent a waiting message into the launch coro, even + // though the message emerged from a test coro. The infrastructure must deal + // with it. + if (to_coro) COTEST_ASSERT(to_coro->GetKind() != PayloadKind::ResumeMain); + + std::unique_ptr from_coro = Iterate(std::move(to_coro)); + + COTEST_ASSERT(!IsCoroutineExited()); + COTEST_ASSERT(from_coro); + + MessageNode *reply_dest; + switch (from_coro->GetKind()) { + case PayloadKind::PreMock: { + std::shared_ptr orig = PeekPayload(from_coro).GetOriginator().lock(); + COTEST_ASSERT(orig && "we have a pre mock but session already expired"); + SetCurrentMockRS(orig); + + reply_dest = PreMockSynchroniser::GetInstance(); + break; + } + case PayloadKind::MockSeen: { + std::shared_ptr orig = PeekPayload(from_coro).GetOriginator().lock(); + COTEST_ASSERT(orig && "we have a mock call but session already expired"); + COTEST_ASSERT(GetCurrentMockRS().get() == orig.get()); + // To the test coro identified by GMock. MOCK_SEEN responses + // shall be by return if they are for our originator. + reply_dest = orig->GetHandlingTestCoro(); + break; + } + case PayloadKind::MockAction: { + std::shared_ptr orig = PeekPayload(from_coro).GetOriginator().lock(); + COTEST_ASSERT(orig && "we have a mock call but session already expired"); + COTEST_ASSERT(GetCurrentMockRS().get() == orig.get()); + // To the test coro identified by GMock. + reply_dest = orig->GetHandlingTestCoro(); + break; + } + case PayloadKind::LaunchResult: { + auto orig = PeekPayload(from_coro).GetOriginator().lock(); + reply_dest = orig->GetParentTestCoroutine(); + break; + } + case PayloadKind::DropMock: + case PayloadKind::AcceptMock: + case PayloadKind::ReturnMock: + case PayloadKind::TCExited: + case PayloadKind::Launch: + case PayloadKind::PreMockAck: + case PayloadKind::ResumeMain: + case PayloadKind::TCDestructing: { + COTEST_ASSERT("!unhandled message from launch coroutine"); + } + } + + COTEST_ASSERT(reply_dest); + return std::make_pair(std::move(from_coro), reply_dest); +} + +LaunchCoroutine *LaunchCoroutine::GetAsCoroutine() { return this; } + +const LaunchCoroutine *LaunchCoroutine::GetAsCoroutine() const { return this; } + +std::shared_ptr LaunchCoroutine::CreateMockRoutingSession(UntypedMockerPointer mocker_, + UntypedMockObjectPointer mock_obj_, + const char *name_) { + return std::make_shared(shared_from_this(), mocker_, mock_obj_, name_); +} + +std::shared_ptr LaunchCoroutine::TryGetCurrentLaunchSession() { + return current_launch_session.lock(); +} + +std::shared_ptr LaunchCoroutine::TryGetCurrentLaunchSession() const { + return current_launch_session.lock(); +} + +std::string LaunchCoroutine::DebugString() const { + std::stringstream ss; + ss << "LaunchCoroutine(\"" << GetName() << "\"" + << ", cur_ls=" << PtrToString(current_launch_session.lock().get()) << ")"; + return ss.str(); +} + +LaunchCoroutine *LaunchCoroutinePool::Allocate(std::string launch_text) { + // Leaky algorithm has no reclamation or limit but there is COTEST_CLEANUP() + auto dc = std::make_shared(launch_text); + pool.insert(std::make_pair(dc->GetImpl(), dc)); + return dc.get(); +} + +std::shared_ptr LaunchCoroutinePool::FindActiveMockSource() { + if (pool.empty()) return ProxyForMain::GetInstance(); // No launch coros so must be main + + // As long as all the coros in the pool have the same implementation type, + // we are free to choose any one of them to call GetActive() + auto active_ii = pool.begin()->first->GetActive(); + if (!active_ii) return ProxyForMain::GetInstance(); + + PoolType::iterator it = pool.find(active_ii); + if (it != pool.end()) return it->second; + + COTEST_ASSERT(!"Cannot invoke mock functions from within test coroutines"); +} + +LaunchCoroutine *LaunchCoroutinePool::TryGetUnusedLaunchCoro() { + for (auto p : pool) { + if (!p.second->TryGetCurrentLaunchSession()) return p.second.get(); + } + return nullptr; +} + +int LaunchCoroutinePool::CleanUp() { + if (pool.empty()) return 0; + COTEST_ASSERT(!pool.begin()->first->GetActive() && "Not allowed in coroutine interior"); + + int num = 0; + for (PoolType::const_iterator it = pool.cbegin(); it != pool.cend();) { + if (!it->second->TryGetCurrentLaunchSession()) { + it = pool.erase(it); + num++; + } else { + ++it; + } + } + return num; +} + +MessageNode::ReplyPair LaunchCoroutinePool::ReceiveMessage(std::unique_ptr &&to_node) { + COTEST_ASSERT(to_node); + COTEST_ASSERT(to_node->GetKind() == PayloadKind::Launch); + + const std::string launch_text = PeekPayload(to_node).GetName(); + LaunchCoroutine *dc = TryGetUnusedLaunchCoro(); + if (dc) + dc->SetName(launch_text); + else + dc = Allocate(launch_text); + + return make_pair(std::move(to_node), dc); +} + +std::string LaunchCoroutinePool::DebugString() const { + std::stringstream ss; + ss << "LaunchCoroutinePool( pool_size=" << pool.size() << ")"; + return ss.str(); +} + +LaunchCoroutinePool *LaunchCoroutinePool::GetInstance() { + static LaunchCoroutinePool instance; + return &instance; +} + +} // namespace crf +} // namespace testing diff --git a/coroutines/src/cotest-crf-mock.cc b/coroutines/src/cotest-crf-mock.cc new file mode 100644 index 000000000..6c5e3159d --- /dev/null +++ b/coroutines/src/cotest-crf-mock.cc @@ -0,0 +1,154 @@ +#include "cotest/internal/cotest-crf-mock.h" + +#include + +#include "cotest/internal/cotest-crf-core.h" +#include "cotest/internal/cotest-crf-launch.h" +#include "cotest/internal/cotest-crf-synch.h" +#include "cotest/internal/cotest-crf-test.h" +#include "cotest/internal/cotest-integ-finder.h" +#include "cotest/internal/cotest-util-logging.h" + +namespace testing { +namespace crf { + +using coro_impl::PtrToString; + +MockRoutingSession::MockRoutingSession(UntypedMockerPointer mocker_, UntypedMockObjectPointer mock_object_, + std::string name_) + : mocker(mocker_), mock_object(mock_object_), name(name_) {} + +void MockRoutingSession::Configure(TestCoroutine *handling_coroutine_) { handling_coroutine = handling_coroutine_; } + +void MockRoutingSession::PreMockUnlocked() { + // Note: mock calls are not necessarily handled by the test coroutine that + // owns the mock source. Indeed, when the mock source is main, no test + // coroutine owns it. At pre-mock stage, we don't yet know which test coro + // will handle the call. + auto pre_call_payload = MakePayload(shared_from_this(), name, mock_object, mocker); + + std::unique_ptr response_payload = SendMessageToSynchroniser(std::move(pre_call_payload)); + + COTEST_ASSERT(response_payload && response_payload->GetKind() == PayloadKind::PreMockAck); + // We can't get the mock call session from this message, because the + // synchroniser sent it to all the test coroutines - we don't know which will + // handle until one accepts. +} + +bool MockRoutingSession::SeenMockCallLocked(UntypedArgsPointer args) { + // This is the mutexed iteration for filtering - in MT applications, we need + // to get a DROP_MOCK or ACCEPT_MOCK while mutex is held. + // We may get called on behalf of multiple candidate test coroutines, before + // we know which will handle the call. + + if (handlers_that_dropped.count(handling_coroutine) > 0) + return false; // Multiple overlapping watches: drop again immediately + + auto mock_call_payload = MakePayload(shared_from_this(), args, name, mock_object, mocker); + + std::unique_ptr response_payload = SendMessageToHandlingCoro(std::move(mock_call_payload)); + COTEST_ASSERT(response_payload); + + const PayloadKind kind = response_payload->GetKind(); + auto mock_response_payload = SpecialisePayload(std::move(response_payload)); + COTEST_ASSERT(mock_response_payload->GetOriginator().lock() = shared_from_this()); + + switch (kind) { + case PayloadKind::DropMock: { + // TestCoroutine dropped the call: call not handled, there will not be a + // return + auto drop_payload = SpecialisePayload(std::move(mock_response_payload)); + COTEST_ASSERT(drop_payload->GetOriginator().lock().get() == this); + handlers_that_dropped.insert(handling_coroutine); + return false; + } + case PayloadKind::AcceptMock: { + // TestCoroutine accepted: can release mutex and let coroutine do actions + auto accept_payload = SpecialisePayload(std::move(mock_response_payload)); + COTEST_ASSERT(accept_payload->GetOriginator().lock().get() == this); + call_session = accept_payload->GetResponder(); + return true; + } + case PayloadKind::ReturnMock: + COTEST_ASSERT(!"Accept required before return"); + case PayloadKind::TCExited: + COTEST_ASSERT(false); // Can't exit while handling a mock call + return false; + case PayloadKind::Launch: + case PayloadKind::PreMockAck: + case PayloadKind::ResumeMain: + case PayloadKind::PreMock: + case PayloadKind::MockSeen: + case PayloadKind::MockAction: + case PayloadKind::LaunchResult: + case PayloadKind::TCDestructing: + COTEST_ASSERT(!"Bad response to mock call"); + } + return false; +} + +UntypedReturnValuePointer MockRoutingSession::ActionsAndReturnUnlocked() { + // This is the un-mutexed iteration for actions - in MT applications, mutex + // is released to avoid deadlocks. + COTEST_ASSERT(!handling_coroutine->IsCoroutineExited()); + auto action_payload = MakePayload(shared_from_this(), call_session); + + std::unique_ptr response_payload = SendMessageToHandlingCoro(std::move(action_payload)); + + COTEST_ASSERT(!handling_coroutine->IsCoroutineExited()); + COTEST_ASSERT(response_payload); + + COTEST_ASSERT(response_payload->GetKind() == PayloadKind::ReturnMock && + "Coroutine must return mock call from main before main can " + "supply more events"); + auto return_payload = SpecialisePayload(std::move(response_payload)); + COTEST_ASSERT(return_payload->GetOriginator().lock().get() == this); + + // Check return came from the correct coroutine + COTEST_ASSERT(return_payload->GetResponder().lock() == call_session.lock()); + + return return_payload->GetResult(); +} + +TestCoroutine *MockRoutingSession::GetHandlingTestCoro() const { return handling_coroutine; } + +std::string MockRoutingSession::GetName() const { return name; } + +std::shared_ptr ExteriorMockRS::GetMockSource() const { return ProxyForMain::GetInstance(); } + +std::unique_ptr ExteriorMockRS::SendMessageToSynchroniser(std::unique_ptr &&to_tc) const { + // We're in main, so start the message loop and provide destination + return MessageNode::SendMessageFromMain(PreMockSynchroniser::GetInstance(), std::move(to_tc)); +} + +std::unique_ptr ExteriorMockRS::SendMessageToHandlingCoro(std::unique_ptr &&to_tc) const { + // We're in main, so start the message loop and provide destination + return MessageNode::SendMessageFromMain(GetHandlingTestCoro(), std::move(to_tc)); +} + +std::shared_ptr InteriorMockRS::GetMockSource() const { + auto msl = launch_coro.lock(); + COTEST_ASSERT(msl); + return msl; +} + +InteriorMockRS::InteriorMockRS(std::shared_ptr launch_coro_, UntypedMockerPointer mocker_, + UntypedMockObjectPointer mock_object_, std::string name_) + : MockRoutingSession(mocker_, mock_object_, name_), launch_coro(launch_coro_) {} + +std::unique_ptr InteriorMockRS::SendMessageToSynchroniser(std::unique_ptr &&to_tc) const { + // We're in launch coro interior, so yield + auto dcl = launch_coro.lock(); + COTEST_ASSERT(dcl); + return dcl->Yield(std::move(to_tc)); +} + +std::unique_ptr InteriorMockRS::SendMessageToHandlingCoro(std::unique_ptr &&to_tc) const { + // We're in launch coro interior, so yield + auto dcl = launch_coro.lock(); + COTEST_ASSERT(dcl); + return dcl->Yield(std::move(to_tc)); +} + +} // namespace crf +} // namespace testing diff --git a/coroutines/src/cotest-crf-payloads.cc b/coroutines/src/cotest-crf-payloads.cc new file mode 100644 index 000000000..71a57ec60 --- /dev/null +++ b/coroutines/src/cotest-crf-payloads.cc @@ -0,0 +1,184 @@ +#include "cotest/internal/cotest-crf-payloads.h" + +#include + +#include "cotest/internal/cotest-util-logging.h" + +namespace testing { +namespace crf { + +using coro_impl::PtrToString; + +PreMockPayload::PreMockPayload(std::weak_ptr originator_, std::string name_, + UntypedMockObjectPointer mock_object_, UntypedMockerPointer mocker_) + : originator(originator_), name(name_), mock_object(mock_object_), mocker(mocker_) {} + +PayloadKind PreMockPayload::GetKind() const { return PayloadKind::PreMock; } + +PreMockPayload *PreMockPayload::Clone() const { return new PreMockPayload(originator, name, mock_object, mocker); } + +std::weak_ptr PreMockPayload::GetOriginator() const { return originator; } + +std::string PreMockPayload::GetName() const { + COTEST_ASSERT(!name.empty()); + return name; +} + +UntypedMockObjectPointer PreMockPayload::GetMockObject() const { + COTEST_ASSERT(mock_object); + return mock_object; +} + +UntypedMockerPointer PreMockPayload::GetMocker() const { + COTEST_ASSERT(mocker); + return mocker; +} + +std::string PreMockPayload::DebugString() const { + return "PayloadKind::PreMock(O=" + PtrToString(originator.lock().get()) + ", \"" + name + "\"" + + ", mo=" + PtrToString(mock_object) + ", m=" + PtrToString(mocker) + ")"; +} + +PreMockAckPayload::PreMockAckPayload(std::weak_ptr originator_) : originator(originator_) {} + +std::weak_ptr PreMockAckPayload::GetOriginator() const { return originator; } + +PayloadKind PreMockAckPayload::GetKind() const { return PayloadKind::PreMockAck; } + +std::string PreMockAckPayload::DebugString() const { + return "PayloadKind::PreMockAck(O=" + PtrToString(originator.lock().get()) + ")"; +} + +MockSeenPayload::MockSeenPayload(std::weak_ptr originator_, UntypedArgsPointer args_, + std::string name_, UntypedMockObjectPointer mock_object_, UntypedMockerPointer mocker_) + : originator(originator_), args(args_), name(name_), mock_object(mock_object_), mocker(mocker_) {} + +PayloadKind MockSeenPayload::GetKind() const { return PayloadKind::MockSeen; } + +std::weak_ptr MockSeenPayload::GetOriginator() const { return originator; } + +UntypedArgsPointer MockSeenPayload::GetArgsUntyped() const { return args; } + +std::string MockSeenPayload::GetName() const { + COTEST_ASSERT(!name.empty()); + return name; +} + +UntypedMockObjectPointer MockSeenPayload::GetMockObject() const { + COTEST_ASSERT(mock_object); + return mock_object; +} + +UntypedMockerPointer MockSeenPayload::GetMocker() const { + COTEST_ASSERT(mocker); + return mocker; +} + +std::string MockSeenPayload::DebugString() const { + return "PayloadKind::MockSeen(O=" + PtrToString(originator.lock().get()) + ", a=" + PtrToString(args) + ", \"" + + name + "\"" + ", mo=" + PtrToString(mock_object) + ", m=" + PtrToString(mocker) + ")"; +} + +MockResponsePayload::MockResponsePayload(std::weak_ptr originator_, + std::weak_ptr responder_) + : originator(originator_), responder(responder_) {} + +std::weak_ptr MockResponsePayload::GetOriginator() const { return originator; } + +std::weak_ptr MockResponsePayload::GetResponder() const { return responder; } + +PayloadKind DropMockPayload::GetKind() const { return PayloadKind::DropMock; } + +std::string DropMockPayload::DebugString() const { + return "PayloadKind::DropMock(O=" + PtrToString(originator.lock().get()) + + ", R=" + PtrToString(responder.lock().get()) + ")"; +} + +PayloadKind AcceptMockPayload::GetKind() const { return PayloadKind::AcceptMock; } + +std::string AcceptMockPayload::DebugString() const { + return "PayloadKind::AcceptMock(O=" + PtrToString(originator.lock().get()) + + ", R=" + PtrToString(responder.lock().get()) + ")"; +} + +PayloadKind MockActionPayload::GetKind() const { return PayloadKind::MockAction; } + +std::string MockActionPayload::DebugString() const { + return "PayloadKind::MockAction(O=" + PtrToString(originator.lock().get()) + + ", R=" + PtrToString(responder.lock().get()) + ")"; +} + +ReturnMockPayload::ReturnMockPayload(std::weak_ptr originator_, + std::weak_ptr responder_, + UntypedReturnValuePointer return_val_ptr_) + : MockResponsePayload(originator_, responder_), return_val_ptr(return_val_ptr_) {} + +PayloadKind ReturnMockPayload::GetKind() const { return PayloadKind::ReturnMock; } + +UntypedReturnValuePointer ReturnMockPayload::GetResult() { return return_val_ptr; } + +std::string ReturnMockPayload::DebugString() const { + return "PayloadKind::ReturnMock(O=" + PtrToString(originator.lock().get()) + + ", R=" + PtrToString(responder.lock().get()) + ", rv=" + PtrToString(return_val_ptr) + ")"; +} + +LaunchPayload::LaunchPayload(std::weak_ptr originator_, + internal::LaunchLambdaWrapperType wrapper_lambda_, std::string name_) + : originator(originator_), wrapper_lambda(wrapper_lambda_), name(name_) {} + +PayloadKind LaunchPayload::GetKind() const { return PayloadKind::Launch; } + +std::weak_ptr LaunchPayload::GetOriginator() const { return originator; } + +internal::LaunchLambdaWrapperType LaunchPayload::GetLambdaWrapper() const { return wrapper_lambda; } + +std::string LaunchPayload::GetName() const { return name; } + +std::string LaunchPayload::DebugString() const { + return "PayloadKind::Launch(O=" + PtrToString(originator.lock().get()) + ", \"" + name + "\"" + ")"; +} + +LaunchResultPayload::LaunchResultPayload(std::weak_ptr originator_, + std::weak_ptr responder_, + UntypedReturnValuePointer return_val_ptr_) + : originator(originator_), responder(responder_), return_val_ptr(return_val_ptr_) {} + +PayloadKind LaunchResultPayload::GetKind() const { return PayloadKind::LaunchResult; } + +std::weak_ptr LaunchResultPayload::GetOriginator() const { return originator; } + +std::weak_ptr LaunchResultPayload::GetResponder() const { return responder; } + +UntypedReturnValuePointer LaunchResultPayload::GetResult() { return return_val_ptr; } + +std::string LaunchResultPayload::DebugString() const { + return "PayloadKind::LaunchResult(O=" + PtrToString(originator.lock().get()) + + ", R=" + PtrToString(responder.lock().get()) + ", rv=" + PtrToString(return_val_ptr) + ")"; +} + +ResumeMainPayload::ResumeMainPayload(std::weak_ptr originator_) : originator(originator_) {} + +PayloadKind ResumeMainPayload::GetKind() const { return PayloadKind::ResumeMain; } + +std::weak_ptr ResumeMainPayload::GetOriginator() const { return originator; } + +std::string ResumeMainPayload::DebugString() const { return "PayloadKind::ResumeMain()"; } + +TCExitedPayload::TCExitedPayload(std::weak_ptr originator_) : originator(originator_) {} + +PayloadKind TCExitedPayload::GetKind() const { return PayloadKind::TCExited; } + +std::weak_ptr TCExitedPayload::GetOriginator() const { return originator; } + +std::string TCExitedPayload::DebugString() const { return "PayloadKind::TCExited()"; } + +TCDestructingPayload::TCDestructingPayload(std::weak_ptr originator_) : originator(originator_) {} + +PayloadKind TCDestructingPayload::GetKind() const { return PayloadKind::TCDestructing; } + +std::weak_ptr TCDestructingPayload::GetOriginator() const { return originator; } + +std::string TCDestructingPayload::DebugString() const { return "PayloadKind::TCDestructing()"; } + +} // namespace crf +} // namespace testing diff --git a/coroutines/src/cotest-crf-synch.cc b/coroutines/src/cotest-crf-synch.cc new file mode 100644 index 000000000..b33b902d0 --- /dev/null +++ b/coroutines/src/cotest-crf-synch.cc @@ -0,0 +1,141 @@ +#include "cotest/internal/cotest-crf-synch.h" + +#include + +#include "cotest/internal/cotest-crf-core.h" +#include "cotest/internal/cotest-crf-mock.h" +#include "cotest/internal/cotest-crf-test.h" +#include "cotest/internal/cotest-integ-finder.h" +#include "cotest/internal/cotest-util-logging.h" + +namespace testing { +namespace crf { + +using coro_impl::PtrToString; + +MessageNode::ReplyPair PreMockSynchroniser::ReceiveMessage(std::unique_ptr &&to_node) { + // Fall-through machine with early return on reply. + // Each state action is inside its own if-statement, meaning that multiple + // actions and state transitions can complete in a signle iteration. The order + // of the if-statement is immaterial to the algorithm implmented by the + // machine but does affect the states that can lead to an iteration + // completing. Here we choose an ordering that does not end the iteration + // until a reply message is ready. Then do ensure we do end in this case, we + // use early returns. + + if (state == State::Idle) { + // Handle incoming messages when we don't require acknowledgement + switch (to_node->GetKind()) { + case PayloadKind::PreMock: + COTEST_ASSERT(!current_pre_mock); + current_pre_mock = SpecialisePayload(std::move(to_node)); + state = State::Start; + break; + case PayloadKind::PreMockAck: + COTEST_ASSERT(!"Not expecting PRE_MOCK_ACK in State::Idle"); + case PayloadKind::ResumeMain: + case PayloadKind::TCExited: + state = State::PassToMain; + break; + case PayloadKind::MockSeen: + case PayloadKind::AcceptMock: + case PayloadKind::MockAction: + case PayloadKind::DropMock: + case PayloadKind::ReturnMock: + case PayloadKind::Launch: + case PayloadKind::LaunchResult: + case PayloadKind::TCDestructing: + COTEST_ASSERT(!"Unhanled message in State::Idle"); + } + } + + if (state == State::WaitingForAck) { + // Handle incoming messages when we require an acknowledgement + switch (to_node->GetKind()) { + case PayloadKind::PreMock: + COTEST_ASSERT(!"Not expecting another PRE_MOCK while State::WaitingForAck. A TC may be delaying acknowlegement of PM"); + break; + case PayloadKind::PreMockAck: { + auto pmack = SpecialisePayload(std::move(to_node)); + auto mock_source = pmack->GetOriginator().lock(); + auto expected_mock_source = current_pre_mock->GetOriginator().lock(); + COTEST_ASSERT(mock_source && expected_mock_source && "Mock source expired while synchronsing PRE_MOCK"); + COTEST_ASSERT(mock_source == expected_mock_source); // should relate to the same mock source + state = send_pm_to.empty() ? State::Complete : State::Working; + break; + } + case PayloadKind::ResumeMain: + // If we're waiting for an ack from a coroutine and instead get that it + // has nothing to do, we may be headed for a deadlock. Pass to main in + // case main can progress things. This path not covered by tests at the + // time of writing. + state = State::PassToMain; + break; + case PayloadKind::TCExited: + // Discard ack requirement for exited coro + state = send_pm_to.empty() ? State::Complete : State::Working; + break; + case PayloadKind::MockSeen: + case PayloadKind::AcceptMock: + case PayloadKind::MockAction: + case PayloadKind::DropMock: + case PayloadKind::ReturnMock: + case PayloadKind::Launch: + case PayloadKind::LaunchResult: + case PayloadKind::TCDestructing: + COTEST_ASSERT(!"Unhanled message in State::WaitingForAck"); + } + } + + if (state == State::PassToMain) { + // Forward the message to main + state = State::Idle; + return std::make_pair(std::move(to_node), ProxyForMain::GetInstance().get()); + } + + if (state == State::Start) { + // Start a new synchronisation cycle + auto pool = internal::CotestMockHandlerPool::GetOrCreateInstance(); + pool->ForAll([this](std::shared_ptr &&crf_sp) { send_pm_to.push(std::move(crf_sp)); }); + state = send_pm_to.empty() ? State::Complete : State::Working; + } + + if (state == State::Working) { + // Continue the current synchronisation cycle + COTEST_ASSERT(!send_pm_to.empty()); + ReplyPair p; + p.first = std::unique_ptr(current_pre_mock->Clone()); + auto pm_to_locked = send_pm_to.front().lock(); + COTEST_ASSERT(pm_to_locked); + p.second = pm_to_locked.get(); + send_pm_to.pop(); + state = State::WaitingForAck; + return p; + } + + if (state == State::Complete) { + // Complete the synchronisation cycle + COTEST_ASSERT(send_pm_to.empty()); + auto mock_call_session = current_pre_mock->GetOriginator().lock(); + current_pre_mock.reset(); + state = State::Idle; + COTEST_ASSERT(mock_call_session); + // Reply to current mock source + return std::make_pair(MakePayload(mock_call_session), + mock_call_session->GetMockSource().get()); + } + COTEST_ASSERT(!"We have to send a reply message on each iteration"); +} + +std::string PreMockSynchroniser::DebugString() const { + std::stringstream ss; + ss << "PreMockSynchroniser(PM=" << current_pre_mock.get() << " #TCs=" << send_pm_to.size() << ")"; + return ss.str(); +} + +PreMockSynchroniser *PreMockSynchroniser::GetInstance() { return &instance; } + +PreMockSynchroniser PreMockSynchroniser::instance; + +} // namespace crf +} // namespace testing diff --git a/coroutines/src/cotest-crf-test.cc b/coroutines/src/cotest-crf-test.cc new file mode 100644 index 000000000..bdcfc2e76 --- /dev/null +++ b/coroutines/src/cotest-crf-test.cc @@ -0,0 +1,391 @@ +#include "cotest/internal/cotest-crf-test.h" + +#include + +#include "cotest/internal/cotest-crf-core.h" +#include "cotest/internal/cotest-crf-launch.h" +#include "cotest/internal/cotest-crf-synch.h" +#include "cotest/internal/cotest-util-logging.h" + +namespace testing { +namespace crf { + +using coro_impl::PtrToString; + +TestCoroutine::TestCoroutine(coro_impl::BodyFunction cofn_, std::string name_, OnExitFunction on_exit_function_) + : CoroutineBase(cofn_, name_), on_exit_function(on_exit_function_) { + // Comment this out for logging + std::clog.setstate(std::ios_base::failbit); + + std::clog << ActiveStr() << COTEST_THIS << " constructing" << std::endl; +} + +TestCoroutine::~TestCoroutine() { std::clog << ActiveStr() << COTEST_THIS << " destructing" << std::endl; } + +MessageNode::ReplyPair TestCoroutine::ReceiveMessage(std::unique_ptr &&to_node) { + COTEST_ASSERT(!IsCoroutineExited()); + return IterateServer(std::move(to_node)); +} + +MessageNode::ReplyPair TestCoroutine::IterateServer(std::unique_ptr &&to_coro) { + COTEST_ASSERT(!IsCoroutineExited()); + + extra_iteration_requested = false; // requirement satisfied by this iteration + std::unique_ptr from_coro = Iterate(std::move(to_coro)); + + if (IsCoroutineExited()) { + on_exit_function(); + from_coro = MakePayload(shared_from_this()); + } + + if (!from_coro) return std::make_pair(std::move(from_coro), nullptr); + + MessageNode *reply_dest; + switch (from_coro->GetKind()) { + case PayloadKind::Launch: + reply_dest = LaunchCoroutinePool::GetInstance(); // let the pool deal with it + break; + + case PayloadKind::PreMockAck: + reply_dest = PreMockSynchroniser::GetInstance(); + break; + + case PayloadKind::DropMock: + case PayloadKind::AcceptMock: + case PayloadKind::ReturnMock: { + auto orig = PeekPayload(from_coro).GetOriginator().lock(); + COTEST_ASSERT(orig && "Call session expired unexpectedly"); + reply_dest = orig->GetMockSource().get(); + + // Coroutine will need to run beyond the cs.RETURN() before we ask it too + // many questions, in case it needs to SATISFY(), RETIRE() or exit. But we + // can't iterate it now, since it currently owns the return value. CRF + // constraints #3 and #4. + extra_iteration_requested = true; + break; + } + case PayloadKind::ResumeMain: + case PayloadKind::TCExited: + reply_dest = PreMockSynchroniser::GetInstance(); + break; + case PayloadKind::PreMock: + case PayloadKind::MockSeen: + case PayloadKind::MockAction: + case PayloadKind::LaunchResult: + case PayloadKind::TCDestructing: + COTEST_ASSERT(!"unhandled message from test coroutine"); + } + + COTEST_ASSERT(reply_dest); + return std::make_pair(std::move(from_coro), reply_dest); +} + +void TestCoroutine::InitialActivity() { + COTEST_ASSERT(!IsCoroutineExited() && "Cannot reuse coroutine instance"); + std::unique_ptr from_coro = SendMessageFromMain(this, nullptr); + // We should return from this when the coro is blocked, eg waiting for + // a mock call from somewhere other than a local launch + COTEST_ASSERT(from_coro->GetKind() == PayloadKind::TCExited || from_coro->GetKind() == PayloadKind::ResumeMain); +} + +static MockSource *GetMockSourceFromMessage(const std::unique_ptr &payload) { + COTEST_ASSERT(payload); + switch (payload->GetKind()) { + case PayloadKind::PreMock: { + auto ext_mock_cs = PeekPayload(payload).GetOriginator().lock(); + return ext_mock_cs->GetMockSource().get(); + } + case PayloadKind::MockSeen: { + auto ext_mock_cs = PeekPayload(payload).GetOriginator().lock(); + return ext_mock_cs->GetMockSource().get(); + } + case PayloadKind::MockAction: { + auto ext_mock_cs = PeekPayload(payload).GetOriginator().lock(); + return ext_mock_cs->GetMockSource().get(); + } + case PayloadKind::LaunchResult: { + return PeekPayload(payload).GetResponder().lock().get(); + } + case PayloadKind::TCDestructing: { + return ProxyForMain::GetInstance().get(); + } + + case PayloadKind::DropMock: + case PayloadKind::AcceptMock: + case PayloadKind::ReturnMock: + case PayloadKind::Launch: + case PayloadKind::PreMockAck: + case PayloadKind::ResumeMain: + case PayloadKind::TCExited: + COTEST_ASSERT(!"Unhandled payload type"); + break; + } + return nullptr; +} + +static std::pair> GetLaunchSessionFromMessage( + const std::unique_ptr &payload) { + MockSource *ms = GetMockSourceFromMessage(payload); + if (ms == ProxyForMain::GetInstance().get()) return std::make_pair(true, nullptr); + auto launch_coro = ms->GetAsCoroutine(); + return std::make_pair(false, launch_coro->TryGetCurrentLaunchSession()); +} + +void TestCoroutine::YieldServer(std::unique_ptr &&from_coro) { + // These outgoing payloads will release the mock lock + if (from_coro->GetKind() == PayloadKind::DropMock || from_coro->GetKind() == PayloadKind::AcceptMock) + mock_call_locked = false; + + std::unique_ptr to_coro = Yield(std::move(from_coro)); + COTEST_ASSERT(to_coro); + + if (to_coro->GetKind() == PayloadKind::TCDestructing || to_coro->GetKind() == PayloadKind::MockAction) + return; // Discard these here - the interior event or call session ensures + // a return will be made + + COTEST_ASSERT(!next_payload && "Internal error: already have an event"); + next_payload = std::move(to_coro); +} + +bool TestCoroutine::IsPendingEvent() { return !!next_payload; } + +std::shared_ptr TestCoroutine::NextEvent(const char *file, int line) { + COTEST_ASSERT(!mock_call_locked && + "Cannot request a new event until mock call has been dropped, " + "accepted or returned"); + + // Start loping, because we're going to handle NULL messages and PreMock + // locally + std::unique_ptr mock_call_event; + while (!next_payload || next_payload->GetKind() == PayloadKind::PreMock) { + std::unique_ptr response; + if (!next_payload) { + std::clog << ActiveStr() << COTEST_THIS_FL + << " no event has been sent to this coro, so requesting " + "resumption of main test function" + << std::endl; + response = MakePayload(shared_from_this()); + } else if (next_payload->GetKind() == PayloadKind::PreMock) { + // Acknowledge PreMock, and then grab the subsequent message. We can + // get another PreMock for example if GMock doesn't show us the mock call + // for the first one. We can also get somehting other than SeenMock this + // way. PreMock acknowledged here rather than YieldServer() because we + // need to be sure the test coro is really ready to handle a mock call. + auto p = GetLaunchSessionFromMessage(next_payload); + auto pm_payload = SpecialisePayload(std::move(next_payload)); + response = MakePayload(pm_payload->GetOriginator()); + mock_call_event = std::make_unique(this, p.first, p.second, std::move(pm_payload)); + } + std::clog << ActiveStr() << COTEST_THIS_FL << " acknowledging PreMock: " << response.get() << std::endl; + YieldServer(std::move(response)); + } + + if (next_payload->GetKind() == PayloadKind::MockSeen) { + COTEST_ASSERT(mock_call_event && "Got MOCK_SEEN without a State::PreMock"); + mock_call_locked = true; + auto call_payload = SpecialisePayload(std::move(next_payload)); + mock_call_event->SeenCall(call_payload->GetArgsUntyped()); + // Redundant info in messages just in case we need to match up messages eg + // in future multi-threaded scenario. + COTEST_ASSERT(call_payload->GetMocker() == mock_call_event->GetMocker()); + COTEST_ASSERT(call_payload->GetMockObject() == mock_call_event->GetMockObject()); + COTEST_ASSERT(call_payload->GetName() == mock_call_event->GetName()); + std::clog << ActiveStr() << COTEST_THIS_FL << " -> with InteriorMockCallSession " + << PtrToString(mock_call_event.get()) << std::endl; + return mock_call_event; + } else if (next_payload->GetKind() == PayloadKind::LaunchResult) { + auto dr_payload = SpecialisePayload(std::move(next_payload)); + auto launch_result_event = std::make_unique(this, std::move(dr_payload)); + std::clog << ActiveStr() << COTEST_THIS_FL << " -> InteriorLaunchResultSession " + << PtrToString(launch_result_event.get()) << std::endl; + return launch_result_event; + } else { + COTEST_ASSERT(!"Unhandled payload type in NextEvent()"); + } +} + +bool TestCoroutine::IsPostMockIterationRequested() { return extra_iteration_requested; } + +void TestCoroutine::DestructionIterations() { + COTEST_ASSERT(!IsCoroutineExited()); + // Note: + // extra_iteration_requested is to allow a test coroutine to "get ahead" after + // having returned from a mock call, for example so it can exit or set + // satisfied flag. + if (!extra_iteration_requested) return; + + extra_iteration_requested = false; + + std::unique_ptr from_coro = + SendMessageFromMain(this, MakePayload(shared_from_this())); + // This can cause coro to exit + COTEST_ASSERT(from_coro->GetKind() == PayloadKind::TCExited || from_coro->GetKind() == PayloadKind::ResumeMain); + if (from_coro->GetKind() == PayloadKind::TCExited) { + auto orig = PeekPayload(from_coro).GetOriginator().lock(); + COTEST_ASSERT(orig.get() == this); + } + if (from_coro->GetKind() == PayloadKind::ResumeMain) { + auto orig = PeekPayload(from_coro).GetOriginator().lock(); + COTEST_ASSERT(orig.get() == this); + } +} + +std::string TestCoroutine::DebugString() const { + std::stringstream ss; + ss << "TestCoroutine(\"" << GetName() << "\"" + << ", next_payload=" << !next_payload.get() << ", eir=" << (extra_iteration_requested ? "true" : "false") << ")"; + return ss.str(); +} + +InteriorLaunchSessionBase::InteriorLaunchSessionBase(TestCoroutine *parent_coroutine_, std::string dc_name_) + : parent_coroutine(parent_coroutine_), launch_text(dc_name_) {} + +InteriorLaunchSessionBase::~InteriorLaunchSessionBase() { + // Test coroutine has to "see" the result before we know for sure that the + // launch actually did run to completion. Successful IsLaunchResult() or + // IsLaunchResult(DC) is enough. We could detect launch completion earlier, + // but the rule is that the test coro has to do something to ensure this. + COTEST_ASSERT(launch_completed && "Launch session destructing before result confirmed"); +} + +void InteriorLaunchSessionBase::SetLaunchCompleted() { launch_completed = true; } + +TestCoroutine *InteriorLaunchSessionBase::GetParentTestCoroutine() const { return parent_coroutine; } + +std::string InteriorLaunchSessionBase::GetLaunchText() const { return launch_text; } + +InteriorEventSession::InteriorEventSession(TestCoroutine *test_coroutine_, bool via_main_, + std::shared_ptr via_launch_) + : test_coroutine(test_coroutine_), via_main(via_main_), via_launch(via_launch_) { + // Bad times trying to extract via_main_ and via_launch_ from the payload in + // this constructor: If we get the payload as a reference, the type of the + // unique_ptr<> is wrong. If we use rvalue reference, we consume it before the + // subclass can use it. +} + +bool InteriorEventSession::IsFrom(InteriorLaunchSessionBase *source) { + // source is NULL for IsFromMain() + if (via_main) return !source; + + auto via_launch_locked = via_launch.lock(); + if (via_launch_locked) + return source == via_launch_locked.get(); + else + return false; // Assuming source has not expired, the launch must be a + // different one +} + +TestCoroutine *InteriorEventSession::GetTestCoroutine() const { return test_coroutine; } + +InteriorMockCallSession::InteriorMockCallSession(TestCoroutine *test_coroutine_, bool via_main_, + std::shared_ptr via_launch_, + std::unique_ptr &&payload) + : InteriorEventSession(test_coroutine_, via_main_, via_launch_), + originator(payload->GetOriginator()), + mocker(payload->GetMocker()), + mock_object(payload->GetMockObject()), + name(payload->GetName()) + +{} + +InteriorMockCallSession::~InteriorMockCallSession() { + COTEST_ASSERT(state == State::PreMock || state == State::Dropped || state == State::Returned); +} + +std::string InteriorMockCallSession::GetName() const { return name; } + +UntypedMockObjectPointer InteriorMockCallSession::GetMockObject() const { return mock_object; } + +UntypedMockerPointer InteriorMockCallSession::GetMocker() const { return mocker; } + +void InteriorMockCallSession::SeenCall(UntypedArgsPointer args_) { + COTEST_ASSERT(state == State::PreMock); + args = args_; + state = State::Seen; +} + +bool InteriorMockCallSession::IsLaunchResult() const { return false; } + +bool InteriorMockCallSession::IsLaunchResult(InteriorLaunchSessionBase *launch_session) const { return false; } + +bool InteriorMockCallSession::IsMockCall() const { return true; } + +void InteriorMockCallSession::Drop() { + COTEST_ASSERT(state == State::Seen && "in DROP()"); + state = State::Dropped; + + COTEST_ASSERT(!GetTestCoroutine()->IsPendingEvent() && "Internal error: event received during mock lock"); + GetTestCoroutine()->YieldServer(MakePayload(originator, shared_from_this())); +} + +void InteriorMockCallSession::Accept() { + COTEST_ASSERT(state == State::Seen && "in ACCEPT()"); + state = State::Accepted; + + COTEST_ASSERT(!GetTestCoroutine()->IsPendingEvent() && "Internal error: event received during mock lock"); + GetTestCoroutine()->YieldServer(MakePayload(originator, shared_from_this())); +} + +void InteriorMockCallSession::Return() { ReturnImpl(nullptr); } + +UntypedReturnValuePointer InteriorMockCallSession::GetUntypedLaunchResult() const { + COTEST_ASSERT(!"Cannot get return value from a mock call session"); +} + +void InteriorMockCallSession::ReturnImpl(UntypedReturnValuePointer return_value) { + COTEST_ASSERT((state == State::Seen || state == State::Accepted) && "in RETURN()"); + if (state == State::Seen) Accept(); + state = State::Returned; + COTEST_ASSERT(!GetTestCoroutine()->IsPendingEvent() && "Return(): must use NextEvent() to collect an event first"); + GetTestCoroutine()->YieldServer(MakePayload(originator, shared_from_this(), return_value)); +} + +bool InteriorMockCallSession::IsReturned() const { return state == State::Returned; } + +InteriorLaunchResultSession::InteriorLaunchResultSession(TestCoroutine *test_coroutine_, + std::unique_ptr &&payload) + : InteriorEventSession(test_coroutine_, false, payload->GetOriginator().lock()), + originator(payload->GetOriginator()), + return_value(payload->GetResult()) {} + +bool InteriorLaunchResultSession::IsLaunchResult() const { + auto orig = originator.lock(); + if (orig) orig->SetLaunchCompleted(); + + return true; +} + +bool InteriorLaunchResultSession::IsLaunchResult(InteriorLaunchSessionBase *launch_session) const { + COTEST_ASSERT(launch_session); + std::shared_ptr orig = originator.lock(); + + if (!orig) + return false; // Rationale is: this launch session is still in existance, + // so the dead session must have been some other + + if (orig.get() != launch_session) return false; + + orig->SetLaunchCompleted(); + return true; +} + +bool InteriorLaunchResultSession::IsMockCall() const { return false; } + +void InteriorLaunchResultSession::Drop() { + // Dropping sessions in error is hard to debug. If the handler comes later in + // the coro, the coro will fall out at that point (dropped event) and CMock + // will report unsatisfied. But we want to be shown where the unwanted drop + // occurred. So fail the test immediately. There's no real case for fall-back + // handling as with mock calls (where you can have a lower priority + // EXPECT_CALL() or WATCH_CALL()) as far as I can see. + COTEST_ASSERT(!"Dropping a launch result is not allowed"); +} + +void InteriorLaunchResultSession::Accept() { COTEST_ASSERT(!"No need to accept a launch result"); } + +void InteriorLaunchResultSession::Return() { COTEST_ASSERT(!"Cannot return from a launch result"); } + +UntypedReturnValuePointer InteriorLaunchResultSession::GetUntypedLaunchResult() const { return return_value; } + +} // namespace crf +} // namespace testing diff --git a/coroutines/src/cotest-integ-finder.cc b/coroutines/src/cotest-integ-finder.cc new file mode 100644 index 000000000..71f0e7905 --- /dev/null +++ b/coroutines/src/cotest-integ-finder.cc @@ -0,0 +1,139 @@ +#include "cotest/internal/cotest-integ-finder.h" + +#include +#include + +#include "cotest/internal/cotest-crf-core.h" +#include "cotest/internal/cotest-crf-launch.h" +#include "cotest/internal/cotest-crf-payloads.h" +#include "cotest/internal/cotest-crf-test.h" +#include "cotest/internal/cotest-integ-finder.h" +#include "cotest/internal/cotest-util-logging.h" + +namespace testing { +namespace internal { + +using coro_impl::PtrToString; + +MockHandler::MockHandler() { + MutexLock l(&g_gmock_mutex); + CotestMockHandlerPool::GetOrCreateInstance()->AddOwnerLocked(this); +} + +MockHandler::~MockHandler() { + MutexLock l(&g_gmock_mutex); + CotestMockHandlerPool::GetOrCreateInstance()->RemoveOwnerLocked(this); +} + +CotestMockHandlerPool *CotestMockHandlerPool::GetOrCreateInstance() { + if (!instance) instance = new CotestMockHandlerPool(); + + auto cem = static_cast(instance); + return cem; +} + +void CotestMockHandlerPool::AddOwnerLocked(MockHandler *owner) { owners.insert(owner); } + +void CotestMockHandlerPool::RemoveOwnerLocked(MockHandler *owner) { owners.erase(owner); } + +void CotestMockHandlerPool::AddExpectation(std::function creator) { + // Rather than increment once for each new expectation, we increment twice + // around untyped watchers and not at all for other exps/watchers. This should + // help to reduce wraps in light of legacy tests with very large numbers of + // mocker exps. There should not be large numbers of untyped (=wildcard) + // watchers because the complexity should be in their coroutines. + next_global_priority++; + + creator(); + got_untyped_watchers = true; + + next_global_priority++; +} + +void CotestMockHandlerPool::PreMockUnlocked(const UntypedFunctionMockerBase *mocker, const void *mock_obj, + const char *name) { + std::clog << "CotestMockHandlerPool::PreMockUnlocked() call is " << name << std::endl; + const std::shared_ptr mock_source = + crf::LaunchCoroutinePool::GetInstance()->FindActiveMockSource(); + const auto crf_mock_routing_session = + mock_source->CreateMockRoutingSession(static_cast(mocker), mock_obj, name); + + crf_mock_routing_session->PreMockUnlocked(); +} + +void CotestMockHandlerPool::ForAll(ForTestCoroutineLambda &&lambda) { + for (MockHandler *o : owners) { + auto crf_sp = o->GetCRFTestCoroutine(); + if (crf_sp && !crf_sp->IsCoroutineExited()) lambda(std::move(crf_sp)); + } +} + +bool CotestMockHandlerPool::IsUninteresting(const UntypedFunctionMockerBase *mocker, const void *untyped_args) const { + MutexLock l(&g_gmock_mutex); + // For now, assume any untyped watcher is full wild, making all calls + // interesting + return !got_untyped_watchers && mocker->GetMockHandlerScheme()->empty(); +} + +ExpectationBase *CotestMockHandlerPool::FindMatchingExpectationLocked(const UntypedFunctionMockerBase *mocker, + const void *untyped_args, + bool *is_mocker_exp) const { + std::vector schemes(1); + schemes[0] = mocker->GetMockHandlerScheme(); // Be at index 0 + for (const MockHandler *o : owners) { + schemes.push_back(o->GetMockHandlerScheme()); + } + + auto predicate = [&](ExpectationBase *exp) { return exp->ShouldHandleCall(mocker, untyped_args); }; + + unsigned which = 0; + auto exp = Finder(schemes, predicate, &which); + *is_mocker_exp = (which == 0); + return exp; +} + +ExpectationBase *CotestMockHandlerPool::Finder(std::vector &schemes, + ShouldHandleCallPredicate predicate, unsigned *which) { + std::vector its; + std::map state; + + for (unsigned i = 0; i < schemes.size(); i++) { + const MockHandlerScheme *scheme = schemes.at(i); + its.push_back(scheme->rbegin()); // begin with highest prio expectation in this scheme + if (!scheme->empty()) { + Priority initial_prio = (*its.at(i))->GetPriority(); // Prio of last element is highest + COTEST_ASSERT(state.count(initial_prio) == 0); // no prio value should be in more than one scheme + state[initial_prio] = i; + } + } + + int num_remaining_schemes = state.size(); + while (num_remaining_schemes >= 1) { + const auto state_it = std::prev(state.end()); + const Priority p = state_it->first; + const unsigned i = state_it->second; // index of scheme with highest next prio + auto &it = its.at(i); // Its iterator + if (predicate(it->get())) { + // Found it + *which = i; + return it->get(); + } + state.erase(state_it); + const MockHandlerScheme *scheme = schemes.at(i); + ++it; + if (it == scheme->rend()) { + // this scheme has run out of exps + num_remaining_schemes--; + } else { + // this scheme has more, so re-insert + Priority new_p = (*it)->GetPriority(); + COTEST_ASSERT(new_p <= p); // prirorities must be ordered with each + // scheme, and we're working downwards in prio + state[new_p] = i; + } + } + return nullptr; +} + +} // namespace internal +} // namespace testing diff --git a/coroutines/src/cotest-integ-mock.cc b/coroutines/src/cotest-integ-mock.cc new file mode 100644 index 000000000..191c13480 --- /dev/null +++ b/coroutines/src/cotest-integ-mock.cc @@ -0,0 +1,224 @@ +#include "cotest/internal/cotest-integ-mock.h" + +#include "cotest/internal/cotest-util-logging.h" + +namespace testing { +namespace internal { + +using coro_impl::PtrToString; + +Coroutine::Coroutine(BodyFunctionType body, std::string name_) + : crf(std::make_shared(std::bind(body, this), name_, + std::bind(&Coroutine::OnTestCoroExit, this))), + name(name_), + my_cardinality(std::make_shared()), + cotest_coro_(this) { + // Strong RAII model: this call to InitialActivity() can complete the entire + // test, leaving an exited coroutine and nothing to do but clean up. + crf->InitialActivity(); + initial_activity_complete = true; +} + +Coroutine::Coroutine(Coroutine&& i) + : crf(std::move(i.crf)), + name(std::move(i.name)), + my_watchers_all(std::move(i.my_watchers_all)), + my_cardinality(std::move(i.my_cardinality)), + cotest_coro_(this) { + // To move the coroutine after adding Watchers would invalidate the + // coroutine pointers inside them, and we don't need to do it. This + // move constructor is just to help with the UI syntax around creating + // the coroutine. + COTEST_ASSERT(my_watchers_all.empty()); +} + +Coroutine::~Coroutine() { + // This destructor is doing a lot of stuff, including iterating coroutines. + // Justification is that we want RAII style in the UI following GMock/GTest UI + // design. + DestructionIterations(); + + if (!crf->IsCoroutineExited()) { + // Generate this message as early as possible - assists with fiding cause + if (my_cardinality->IsSatisfiedByCallCount(0)) + std::clog << "Cancelling a satisfied coroutine " << crf->GetName() << " - this is not an error." + << std::endl; + else + std::clog << "Cancelling an unsatisfied coroutine " << crf->GetName() << " - this will cause test failure." + << std::endl; + } + + for (auto p_exp : my_watchers_all) + if (auto p_exp_locked = p_exp.lock()) p_exp_locked->DetachCoroutine(); + + if (!crf->IsCoroutineExited()) { + crf->Cancel(); + } +} + +void Coroutine::WatchCall(const char* file, int line, crf::UntypedMockObjectPointer obj) { + MutexLock l(&g_gmock_mutex); + + // It's unfortunate that we have to provide a function type here, but we + // require functionality available in the templated types (TypedExpectation<> + // and CotestWatcher<>) and don't want to duplicate it. At least only one + // instantiation of the templated code arises as a result. + using UntypedWatcher = CotestWatcher; + + std::shared_ptr sp; + + // Add this expectation to our list, making use of the expectation + // finder singleton so it can update the global priority level. + auto cem = CotestMockHandlerPool::GetOrCreateInstance(); + cem->AddExpectation([&]() { + sp = std::make_shared(this, obj, nullptr, file, line, "", + Function::ArgumentMatcherTuple(), my_cardinality.get()); + my_untyped_watchers.push_back(sp); + }); + + AddWatcher(sp); + + // This is only to get the right GMock behaviour + sp->set_repeated_action(DoDefault()); + + // Hooking up backend to cardinality interface so we can + // satisfy and saturate. + sp->set_cardinality(Cardinality(my_cardinality)); +} + +void Coroutine::SetSatisfied() { my_cardinality->InteriorSetSatisfied(); } + +void Coroutine::Retire() { + // Lock if required since expectation retire flags are mutex-protected, so + // we should not change one while another thread has a lock on it. + std::unique_ptr plock; + if (!gmock_mutex_held) plock = std::make_unique(&testing::internal::g_gmock_mutex); + + retired = true; + + for (auto p_exp : my_watchers_all) { + if (auto p_exp_locked = p_exp.lock()) p_exp_locked->Retire(); + } +} + +bool Coroutine::IsRetired() const { return retired; } + +std::string Coroutine::GetName() const { return name; } + +const MockHandlerScheme* Coroutine::GetMockHandlerScheme() const { return &my_untyped_watchers; } + +void Coroutine::DestructionIterations() { + if (crf->IsCoroutineExited()) return; // no action required + + crf->DestructionIterations(); +} + +std::shared_ptr Coroutine::GetCRFTestCoroutine() { + COTEST_ASSERT(crf); + return crf; +} + +void Coroutine::OnTestCoroExit() { my_cardinality->OnTestCoroExit(); } + +void Coroutine::OnWatcherDestruct() { + // It is safe to destruct a mock object: + // - (a) if this coroutine is not watching it, or + // - (b) before waiting for any activity in main, or + // - (c) after coroutine body has exited, or + // - (d) after the coroutine object has been destructed. + // + // This is to prevent GMock from verifying end-of-life cardinality + // on a coroutine that might be about to call eg RETIRE() + // or SATURATE() but has not, because of CRF constraint #3 + // + // (b) Is OK because while in intial actions, everything is + // happening in coro body and therefore synchronised. + // (c) An exited coro body is also synchronised. + // (d) An extra isteration is provided in this case to allow + // corotuine to run and update cardinality state. + + // if coro destructed, this method is not called. + bool ok = !initial_activity_complete || crf->IsCoroutineExited(); + COTEST_ASSERT(ok && + "Mock object was destructed at an unsafe time: error reports " + "may be inaccurate"); +} + +void Coroutine::AddWatcher(std::shared_ptr watcher) { + my_watchers_all.push_back(watcher); +} + +RAIISetFlag Coroutine::RAIIGMockMutexIsLocked() { return RAIISetFlag(&gmock_mutex_held); } + +CotestCardinality::CotestCardinality() {} + +bool CotestCardinality::IsSatisfiedByCallCount(int call_count) const { + // These are our satisfied states. Final checks will pass if we destruct in + // these states. State::Unsatisfied fails the test because it is assumed that + // mock calls were expected but didn't arrive; State::Oversaturated is a fail + // for the opposite reson and should cause this method to return false. + return state == State::SatisfiedByUser || state == State::SatisfiedByExit; +} + +bool CotestCardinality::IsSaturatedByCallCount(int call_count) const { + // These states correspond to saturation and will cause GMock to + // report that the coroutine was saturated. + return state == State::SatisfiedByExit || state == State::Oversaturated; +} + +void CotestCardinality::DescribeTo(::std::ostream* os) const { *os << "called as determined by coroutine"; } + +void CotestCardinality::InteriorSetSatisfied() { + // User has invoked SATISFY() + switch (state) { + case State::Unsatisfied: + state = State::SatisfiedByUser; + break; + case State::SatisfiedByUser: + break; + case State::SatisfiedByExit: + COTEST_ASSERT(!"Interior call while exited"); + case State::Oversaturated: + COTEST_ASSERT(!"Interior call while exited"); + }; +} + +void CotestCardinality::OnTestCoroExit() { + // Cotest policy: exiting a coroutine satisfies its cardinality + // unless or until a mock call gets through exterior matching. + switch (state) { + case State::Unsatisfied: + case State::SatisfiedByUser: + state = State::SatisfiedByExit; + break; + case State::SatisfiedByExit: + COTEST_ASSERT(!"Multiple OnTestCoroExit()"); + case State::Oversaturated: + COTEST_ASSERT(!"Multiple OnTestCoroExit()"); + }; +} + +bool CotestCardinality::OnSeenMockCall() { + // Cotest policy: if the coroutine exited and we've matched on + // exterior criteria, report over-saturation. We will not be + // able to perform interior matching if the coroutine exited, + // so the user's intention must be considered undefined and we + // fail. However, the user can prevent this by invoking + // RETIRE() before exit. + switch (state) { + case State::Unsatisfied: + break; + case State::SatisfiedByUser: + break; + case State::SatisfiedByExit: + state = State::Oversaturated; + break; + case State::Oversaturated: + break; + }; + + return state == State::Oversaturated; // proceed to interior matching +} + +} // namespace internal +} // namespace testing diff --git a/coroutines/src/cotest.cc b/coroutines/src/cotest.cc new file mode 100644 index 000000000..b576ba29f --- /dev/null +++ b/coroutines/src/cotest.cc @@ -0,0 +1,63 @@ +#include "cotest/cotest.h" + +namespace testing { + +EventHandle internal::Coroutine::NextEvent(const char *file, int line) { + return EventHandle(crf->NextEvent(file, line)); +} + +bool internal::Coroutine::gmock_mutex_held = false; + +EventHandle::EventHandle(std::shared_ptr crf_es_) : crf_es(crf_es_) {} + +EventHandle EventHandle::IsLaunchResult() const { return crf_es->IsLaunchResult() ? *this : EventHandle(); } + +EventHandle EventHandle::IsObject(crf::UntypedMockObjectPointer object) { + COTEST_ASSERT(crf_es); + + if (!crf_es->IsMockCall()) return EventHandle(); + auto crf_mcs = std::static_pointer_cast(crf_es); + + // Check the mock object + if (object != crf_mcs->GetMockObject()) return EventHandle(); + + return *this; +} + +EventHandle EventHandle::IsMockCall() { + COTEST_ASSERT(crf_es); + return crf_es->IsMockCall() ? *this : EventHandle(); +} + +EventHandle::operator bool() const { return !!crf_es; } + +EventHandle EventHandle::Drop() { + COTEST_ASSERT(crf_es); + crf_es->Drop(); + return *this; +} + +EventHandle EventHandle::Accept() { + COTEST_ASSERT(crf_es); + crf_es->Accept(); + return *this; +} + +EventHandle EventHandle::Return() { + COTEST_ASSERT(crf_es); + crf_es->Return(); + return *this; +} + +EventHandle EventHandle::FromMain() { + COTEST_ASSERT(crf_es); + bool ok = crf_es->IsFrom(nullptr); + return ok ? *this : EventHandle(); +} + +std::string EventHandle::GetName() const { + COTEST_ASSERT(crf_es && "call session is NULL, check for failed test"); + return static_cast(crf_es.get())->GetName(); +} + +} // namespace testing diff --git a/coroutines/test/coro-test-thread.cc b/coroutines/test/coro-test-thread.cc new file mode 100644 index 000000000..4e405d569 --- /dev/null +++ b/coroutines/test/coro-test-thread.cc @@ -0,0 +1,160 @@ +#include + +#include "cotest/internal/cotest-coro-thread.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using namespace std; +using namespace coro_impl; + +struct TestPayload : public Payload { + TestPayload(int value_) : value(value_) {} + std::string DebugString() const final { return "TestPayload(" + to_string(value) + ")"; } + int value; +}; + +struct DestructorCheck { + DestructorCheck() { undestructed_count++; } + ~DestructorCheck() { undestructed_count--; } + static int undestructed_count; +}; + +int DestructorCheck::undestructed_count = 0; + +TEST(CoroTestThread, SimpleYieldingSequence) { + string coro_stage = "init"; + auto cl = [&](InteriorInterface *ii) { + DestructorCheck dc; + std::unique_ptr from_main; + coro_stage = "point 1"; + from_main = ii->Yield(MakePayload(10)); + EXPECT_TRUE(from_main); + EXPECT_EQ(PeekPayload(from_main).value, 100); + coro_stage = "point 2"; + from_main = ii->Yield(MakePayload(20)); + EXPECT_TRUE(from_main); + EXPECT_EQ(PeekPayload(from_main).value, 200); + coro_stage = "point 3"; + }; + CoroOnThread coroutine(std::bind(cl, &coroutine), "coroutine"); + + std::unique_ptr from_coro; + + EXPECT_FALSE(coroutine.IsCoroutineExited()); + + from_coro = coroutine.Iterate(nullptr); + EXPECT_FALSE(coroutine.IsCoroutineExited()); + EXPECT_TRUE(from_coro); + EXPECT_EQ(PeekPayload(from_coro).value, 10); + EXPECT_EQ(coro_stage, "point 1"); + + from_coro = coroutine.Iterate(MakePayload(100)); + EXPECT_TRUE(from_coro); + EXPECT_EQ(PeekPayload(from_coro).value, 20); + EXPECT_FALSE(coroutine.IsCoroutineExited()); + EXPECT_EQ(coro_stage, "point 2"); + + from_coro = coroutine.Iterate(MakePayload(200)); + EXPECT_FALSE(from_coro); + EXPECT_TRUE(coroutine.IsCoroutineExited()); + EXPECT_EQ(coro_stage, "point 3"); + + EXPECT_EQ(DestructorCheck::undestructed_count, 0); +} + +TEST(CoroTestThread, LongYieldingSequence) { + int coro_stage = -1; + const int n = 1000; + auto cl = [&](InteriorInterface *ii) { + DestructorCheck dc; + std::unique_ptr from_main; + for (int i = 0; i < n; i++) { + coro_stage = i; + from_main = ii->Yield(MakePayload(i * 10)); + EXPECT_TRUE(from_main); + EXPECT_EQ(PeekPayload(from_main).value, (i + 1) * 100); + } + coro_stage = n; + }; + CoroOnThread coroutine(std::bind(cl, &coroutine), "coroutine"); + + std::unique_ptr from_coro; + + EXPECT_FALSE(coroutine.IsCoroutineExited()); + + from_coro = coroutine.Iterate(nullptr); + EXPECT_FALSE(coroutine.IsCoroutineExited()); + EXPECT_TRUE(from_coro); + EXPECT_EQ(PeekPayload(from_coro).value, 0); + EXPECT_EQ(coro_stage, 0); + + for (int i = 1; i < n; i++) { + from_coro = coroutine.Iterate(MakePayload(i * 100)); + EXPECT_FALSE(coroutine.IsCoroutineExited()); + EXPECT_TRUE(from_coro); + EXPECT_EQ(PeekPayload(from_coro).value, i * 10); + EXPECT_EQ(coro_stage, i); + } + + from_coro = coroutine.Iterate(MakePayload(n * 100)); + EXPECT_TRUE(coroutine.IsCoroutineExited()); + EXPECT_FALSE(from_coro); + EXPECT_EQ(coro_stage, n); + + EXPECT_EQ(DestructorCheck::undestructed_count, 0); +} + +TEST(CoroTestThread, Cancelling) { + string coro_stage = "init"; + auto cl = [&](InteriorInterface *ii) { + DestructorCheck dc; + std::unique_ptr from_main; + coro_stage = "point 1"; + from_main = ii->Yield(MakePayload(10)); + EXPECT_TRUE(from_main); + EXPECT_EQ(PeekPayload(from_main).value, 100); + coro_stage = "point 2"; + from_main = ii->Yield(MakePayload(20)); + EXPECT_TRUE(from_main); + EXPECT_EQ(PeekPayload(from_main).value, 200); + coro_stage = "point 3"; + }; + CoroOnThread coroutine(std::bind(cl, &coroutine), "coroutine"); + + std::unique_ptr from_coro; + + EXPECT_FALSE(coroutine.IsCoroutineExited()); + + from_coro = coroutine.Iterate(nullptr); + EXPECT_FALSE(coroutine.IsCoroutineExited()); + EXPECT_TRUE(from_coro); + EXPECT_EQ(PeekPayload(from_coro).value, 10); + EXPECT_EQ(coro_stage, "point 1"); + + coroutine.Cancel(); + EXPECT_TRUE(coroutine.IsCoroutineExited()); + + EXPECT_EQ(DestructorCheck::undestructed_count, 0); +} + +TEST(CoroTestThread, ImmediateCancelling) { + string coro_stage = "init"; + auto cl = [&](InteriorInterface *ii) { + DestructorCheck dc; + std::unique_ptr from_main; + coro_stage = "point 1"; + from_main = ii->Yield(MakePayload(10)); + EXPECT_TRUE(from_main); + EXPECT_EQ(PeekPayload(from_main).value, 100); + coro_stage = "point 2"; + }; + CoroOnThread coroutine(std::bind(cl, &coroutine), "coroutine"); + + std::unique_ptr from_coro; + + EXPECT_FALSE(coroutine.IsCoroutineExited()); + coroutine.Cancel(); + EXPECT_TRUE(coroutine.IsCoroutineExited()); + + EXPECT_EQ(DestructorCheck::undestructed_count, 0); +} diff --git a/coroutines/test/cotest-action-functor.cc b/coroutines/test/cotest-action-functor.cc new file mode 100644 index 000000000..d319c59d3 --- /dev/null +++ b/coroutines/test/cotest-action-functor.cc @@ -0,0 +1,118 @@ +#include + +#include "cotest/internal/cotest-coro-thread.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +//#define EXPLICIT_FOR_GDB + +using namespace std; +using namespace coro_impl; + +using ::testing::StrictMock; + +using CoroImplType = CoroOnThread; + +////////////////////////////////////////////// +// Mocking assets + +class ClassToMock { + public: + virtual ~ClassToMock() {} + virtual void Mock2(int degrees) = 0; + virtual int Mock1() const = 0; +}; + +class MockClass : public ClassToMock { + public: + MOCK_METHOD(void, Mock2, (int degrees), (override)); + MOCK_METHOD(int, Mock1, (), (const, override)); +}; + +struct TestPayload : public Payload { + TestPayload(int value_) : value(value_) {} + std::string DebugString() const final { return "TestPayload(" + to_string(value) + ")"; } + int value; +}; + +std::unique_ptr from_coro; +struct IterateCoro0 { + IterateCoro0(ExteriorInterface *coroutine_) : coroutine(coroutine_) {} + int operator()() { + from_coro = coroutine->Iterate(nullptr); + return PeekPayload(from_coro).value; + } + + ExteriorInterface *const coroutine; +}; + +////////////////////////////////////////////// +// The actual tests + +TEST(ActionFunctorTest, WithReturn) { + StrictMock mock_object; + + auto cl = [](InteriorInterface *ii) { + std::unique_ptr from_main; + from_main = ii->Yield(nullptr); // waiting for the first mock call + from_main = ii->Yield(MakePayload(100)); + from_main = ii->Yield(MakePayload(200)); + from_main = ii->Yield(MakePayload(300)); + }; + CoroImplType coroutine(std::bind(cl, &coroutine), "coroutine"); +#ifdef EXPLICIT_FOR_GDB + const IterateCoro0 functor(&coroutine); + const testing::Action action(functor); // lands on Action(G&& fun) then void Init(G&& g, + // ::true_type) + EXPECT_CALL(mock_object, Mock1()).Times(3).WillRepeatedly(action); +#else + EXPECT_CALL(mock_object, Mock1()).Times(3).WillRepeatedly(IterateCoro0(&coroutine)); +#endif + from_coro = coroutine.Iterate(nullptr); + EXPECT_FALSE(from_coro); + + // This is the body of the test case + EXPECT_EQ(mock_object.Mock1(), 100); + EXPECT_EQ(mock_object.Mock1(), 200); + EXPECT_EQ(mock_object.Mock1(), 300); + + // Allow coroutine to exit + from_coro = coroutine.Iterate(nullptr); + EXPECT_FALSE(from_coro); +} + +struct IterateCoro1 { + IterateCoro1(ExteriorInterface *coroutine_) : coroutine(coroutine_) {} + void operator()(int arg0) { from_coro = coroutine->Iterate(MakePayload(arg0)); } + + ExteriorInterface *const coroutine; +}; + +TEST(ActionFunctorTest, WithArg) { + StrictMock mock_object; + + auto cl = [](InteriorInterface *ii) { + std::unique_ptr from_main; + from_main = ii->Yield(nullptr); + EXPECT_EQ(PeekPayload(from_main).value, 45); + from_main = ii->Yield(nullptr); + EXPECT_EQ(PeekPayload(from_main).value, 90); + from_main = ii->Yield(nullptr); + EXPECT_EQ(PeekPayload(from_main).value, 180); + from_main = ii->Yield(nullptr); // return from the last mock call + }; + CoroImplType coroutine(std::bind(cl, &coroutine), "coroutine"); + EXPECT_CALL(mock_object, Mock2).Times(3).WillRepeatedly(IterateCoro1(&coroutine)); + + from_coro = coroutine.Iterate(nullptr); + EXPECT_FALSE(from_coro); + + // This is the body of the test case + mock_object.Mock2(45); + mock_object.Mock2(90); + mock_object.Mock2(180); + + // Allow coroutine to exit + from_coro = coroutine.Iterate(nullptr); + EXPECT_FALSE(from_coro); +} diff --git a/coroutines/test/cotest-action-macro.cc b/coroutines/test/cotest-action-macro.cc new file mode 100644 index 000000000..ce3e886e7 --- /dev/null +++ b/coroutines/test/cotest-action-macro.cc @@ -0,0 +1,102 @@ +#include + +#include "cotest/internal/cotest-coro-thread.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using namespace std; +using namespace coro_impl; + +using ::testing::StrictMock; + +using CoroImplType = CoroOnThread; + +////////////////////////////////////////////// +// Mocking assets + +class ClassToMock { + public: + virtual ~ClassToMock() {} + virtual void Mock2(int degrees) = 0; + virtual int Mock1() const = 0; +}; + +class MockClass : public ClassToMock { + public: + MOCK_METHOD(void, Mock2, (int degrees), (override)); + MOCK_METHOD(int, Mock1, (), (const, override)); +}; + +struct TestPayload : public Payload { + TestPayload(int value_) : value(value_) {} + std::string DebugString() const final { return "TestPayload(" + to_string(value) + ")"; } + int value; +}; + +std::unique_ptr from_coro; +ACTION_P(IterateCoro0, coroutine) { + from_coro = coroutine->Iterate(nullptr); + return PeekPayload(from_coro).value; +} + +////////////////////////////////////////////// +// The actual tests + +TEST(ActionMacroTest, WithReturn) { + StrictMock mock_object; + + auto cl = [](InteriorInterface *ii) { + std::unique_ptr from_main; + from_main = ii->Yield(nullptr); // waiting for the first mock call + from_main = ii->Yield(MakePayload(100)); + from_main = ii->Yield(MakePayload(200)); + from_main = ii->Yield(MakePayload(300)); + }; + CoroImplType coroutine(std::bind(cl, &coroutine), "coroutine"); + EXPECT_CALL(mock_object, Mock1()).Times(3).WillRepeatedly(IterateCoro0(&coroutine)); + + from_coro = coroutine.Iterate(nullptr); + EXPECT_FALSE(from_coro); + + // This is the body of the test case + EXPECT_EQ(mock_object.Mock1(), 100); + EXPECT_EQ(mock_object.Mock1(), 200); + EXPECT_EQ(mock_object.Mock1(), 300); + + // Allow coroutine to exit + from_coro = coroutine.Iterate(nullptr); + EXPECT_FALSE(from_coro); +} + +int coro_arg0; + +ACTION_P(IterateCoro1, coroutine) { from_coro = coroutine->Iterate(MakePayload(arg0)); } + +TEST(ActionMacroTest, WithArg) { + StrictMock mock_object; + + auto cl = [](InteriorInterface *ii) { + std::unique_ptr from_main; + from_main = ii->Yield(nullptr); + EXPECT_EQ(PeekPayload(from_main).value, 45); + from_main = ii->Yield(nullptr); + EXPECT_EQ(PeekPayload(from_main).value, 90); + from_main = ii->Yield(nullptr); + EXPECT_EQ(PeekPayload(from_main).value, 180); + from_main = ii->Yield(nullptr); // return from the las mock call + }; + CoroImplType coroutine(std::bind(cl, &coroutine), "coroutine"); + EXPECT_CALL(mock_object, Mock2).Times(3).WillRepeatedly(IterateCoro1(&coroutine)); + + from_coro = coroutine.Iterate(nullptr); + EXPECT_FALSE(from_coro); + + // This is the body of the test case + mock_object.Mock2(45); + mock_object.Mock2(90); + mock_object.Mock2(180); + + // Allow coroutine to exit + from_coro = coroutine.Iterate(nullptr); + EXPECT_FALSE(from_coro); +} diff --git a/coroutines/test/cotest-action-poly.cc b/coroutines/test/cotest-action-poly.cc new file mode 100644 index 000000000..0f3f8e018 --- /dev/null +++ b/coroutines/test/cotest-action-poly.cc @@ -0,0 +1,126 @@ +#include + +#include "cotest/internal/cotest-coro-thread.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using namespace std; +using namespace coro_impl; + +using ::testing::StrictMock; + +using CoroImplType = CoroOnThread; + +////////////////////////////////////////////// +// Mocking assets + +class ClassToMock { + public: + virtual ~ClassToMock() {} + virtual void Mock2(int degrees) = 0; + virtual int Mock1() const = 0; +}; + +class MockClass : public ClassToMock { + public: + MOCK_METHOD(void, Mock2, (int degrees), (override)); + MOCK_METHOD(int, Mock1, (), (const, override)); +}; + +struct TestPayload : public Payload { + TestPayload(int value_) : value(value_) {} + std::string DebugString() const final { return "TestPayload(" + to_string(value) + ")"; } + int value; +}; + +std::unique_ptr from_coro; +class IterateCoro0Action { + public: + IterateCoro0Action(ExteriorInterface *coroutine_) : coroutine(coroutine_) {} + + template + Result Perform(const ArgumentTuple &args) const { + from_coro = coroutine->Iterate(nullptr); + return PeekPayload(from_coro).value; + } + ExteriorInterface *const coroutine; +}; + +inline testing::PolymorphicAction IterateCoro0(ExteriorInterface *coroutine) { + return testing::MakePolymorphicAction(IterateCoro0Action(coroutine)); +} + +////////////////////////////////////////////// +// The actual tests + +TEST(ActionPolymorphicTest, WithReturn) { + StrictMock mock_object; + + auto cl = [](InteriorInterface *ii) { + std::unique_ptr from_main; + from_main = ii->Yield(nullptr); // waiting for the first mock call + from_main = ii->Yield(MakePayload(100)); + from_main = ii->Yield(MakePayload(200)); + from_main = ii->Yield(MakePayload(300)); + }; + CoroImplType coroutine(std::bind(cl, &coroutine), "coroutine"); + + EXPECT_CALL(mock_object, Mock1()).Times(3).WillRepeatedly(IterateCoro0(&coroutine)); + from_coro = coroutine.Iterate(nullptr); + EXPECT_FALSE(from_coro); + + // This is the body of the test case + EXPECT_EQ(mock_object.Mock1(), 100); + EXPECT_EQ(mock_object.Mock1(), 200); + EXPECT_EQ(mock_object.Mock1(), 300); + + // Allow coroutine to exit + from_coro = coroutine.Iterate(nullptr); + EXPECT_FALSE(from_coro); +} + +int coro_arg0; + +class IterateCoro1Action { + public: + IterateCoro1Action(ExteriorInterface *coroutine_) : coroutine(coroutine_) {} + + template + Result Perform(const ArgumentTuple &args) const { + from_coro = coroutine->Iterate(MakePayload(get<0>(args))); + } + ExteriorInterface *const coroutine; +}; + +inline testing::PolymorphicAction IterateCoro1(ExteriorInterface *coroutine) { + return testing::MakePolymorphicAction(IterateCoro1Action(coroutine)); +} + +TEST(ActionPolymorphicTest, WithArg) { + StrictMock mock_object; + + auto cl = [](InteriorInterface *ii) { + std::unique_ptr from_main; + from_main = ii->Yield(nullptr); + EXPECT_EQ(PeekPayload(from_main).value, 45); + from_main = ii->Yield(nullptr); + EXPECT_EQ(PeekPayload(from_main).value, 90); + from_main = ii->Yield(nullptr); + EXPECT_EQ(PeekPayload(from_main).value, 180); + from_main = ii->Yield(nullptr); // return from the las mock call + }; + CoroImplType coroutine(std::bind(cl, &coroutine), "coroutine"); + EXPECT_CALL(mock_object, Mock2).Times(3).WillRepeatedly(IterateCoro1(&coroutine)); + + from_coro = coroutine.Iterate(nullptr); + EXPECT_FALSE(from_coro); + + // This is the body of the test case + mock_object.Mock2(45); + mock_object.Mock2(90); + mock_object.Mock2(180); + + // Allow coroutine to exit + from_coro = coroutine.Iterate(nullptr); + EXPECT_FALSE(from_coro); +} diff --git a/coroutines/test/cotest-all-in.cc b/coroutines/test/cotest-all-in.cc new file mode 100644 index 000000000..3c00dbe42 --- /dev/null +++ b/coroutines/test/cotest-all-in.cc @@ -0,0 +1,147 @@ +#include + +#include "cotest/cotest.h" +#include "gtest/gtest-spi.h" + +using namespace std; +using namespace testing; +using ::testing::StrictMock; + +//////////////////////////////////////////// +// Code under test + +class ClassToMock { + public: + virtual ~ClassToMock() {} + virtual int Mock1(int i) = 0; + virtual int Mock2(int i, int j) const = 0; + virtual int Mock3(int i) const = 0; + virtual int Mock4(int i) const = 0; + virtual int Mock4(int i) = 0; +}; + +class ExampleClass { + public: + ExampleClass(ClassToMock *dep_) : dep(dep_) {} + + int Example1(int a) { return dep->Mock1(a + 1) * 2; } + + int Example2(int a) { return dep->Mock1(a + 1) * dep->Mock1(0); } + + private: + ClassToMock *const dep; +}; + +////////////////////////////////////////////// +// Mocking assets + +class MockClass : public ClassToMock { + public: + MOCK_METHOD(int, Mock1, (int i), (override)); + MOCK_METHOD(int, Mock2, (int i, int j), (const, override)); + MOCK_METHOD(int, Mock3, (int i), (const, override)); + MOCK_METHOD(int, Mock4, (int i), (const, override)); + MOCK_METHOD(int, Mock4, (int i), (override)); +}; + +////////////////////////////////////////////// +// The actual tests + +StrictMock *mock_object_p = nullptr; + +/* + * Why this works + * + * Expectations, watches etc are created in the coro but under shared_ptr<> + * and are held alive by the mockers in the mock object. In this test case, + * the mockers outlast the coro itself, so we end up using DetachCoroutine() + * etc. + */ +TEST(AllInTest, WatchInside) { + StrictMock mock_object; + StrictMock mock_object2; + + auto coro = COROUTINE() { + WATCH_CALL(mock_object, Mock1); + WATCH_CALL(mock_object, Mock2); + + // See later tests in which LAUNCH() is used to run code-under-test + // from within coroutine. + mock_object_p = &mock_object; + + auto cg = WAIT_FOR_CALL(); + EXPECT_FALSE(cg.IS_CALL(mock_object, Mock1(201))); + EXPECT_FALSE(cg.IS_CALL(mock_object2, Mock1)); + EXPECT_FALSE(cg.IS_CALL(mock_object2, Mock1(_))); + EXPECT_FALSE(cg.IS_CALL(mock_object2, Mock1(200))); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock1)); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock1(_))); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock1(200)).RETURN(20)); + cg = WAIT_FOR_CALL(); + EXPECT_FALSE(cg.IS_CALL(mock_object, Mock2(_, 401))); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock2)); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock2(_, _))); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock2(_, 400))); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock2(200, _)).RETURN(30)); + }; + + ASSERT_TRUE(mock_object_p); + + EXPECT_EQ(mock_object_p->Mock1(200), 20); + EXPECT_EQ(mock_object_p->Mock2(200, 400), 30); + + mock_object_p = nullptr; +} + +TEST(AllInTest, LaunchMock) { + auto coro = COROUTINE() { + StrictMock mock_object; + ExampleClass example(&mock_object); + + WATCH_CALL(); + + auto d = LAUNCH(example.Example1(4)); + + auto e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_CALL(mock_object, Mock1(5)).RETURN(1000)); + + e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_RESULT(d)); + EXPECT_EQ(e(d), 2000); + }; +} + +COTEST(AllInTest, CotestMacro) { + StrictMock mock_object; + ExampleClass example(&mock_object); + + WATCH_CALL(); + + auto d = LAUNCH(example.Example1(4)); + + auto cs = WAIT_FOR_CALL(mock_object, Mock1); + EXPECT_TRUE(cs.GetArg<0>() == 5); + cs.RETURN(1000); + + auto e = WAIT_FOR_RESULT(); + EXPECT_EQ(e(d), 2000); +} + +COTEST(AllInTest, CotestMacroWithExpect) { + StrictMock mock_object; + ExampleClass example(&mock_object); + + // EXPECT_CALL() is fine inside a COTEST(). Semantics are the + // same. + EXPECT_CALL(mock_object, Mock1(0)).WillRepeatedly(Return(1)); + WATCH_CALL(); + + auto d = LAUNCH(example.Example2(4)); + + auto cs = WAIT_FOR_CALL(mock_object, Mock1(Gt(0))); + EXPECT_TRUE(cs.GetArg<0>() == 5); + cs.RETURN(1000); + + auto e = WAIT_FOR_RESULT(); + EXPECT_EQ(e(d), 1000); +} diff --git a/coroutines/test/cotest-cardinality.cc b/coroutines/test/cotest-cardinality.cc new file mode 100644 index 000000000..e991d5759 --- /dev/null +++ b/coroutines/test/cotest-cardinality.cc @@ -0,0 +1,516 @@ +#include + +#include "cotest/cotest.h" +#include "gtest/gtest-spi.h" + +using namespace std; +using ::testing::StrictMock; + +////////////////////////////////////////////// +// Mocking assets + +class ClassToMock { + public: + virtual ~ClassToMock() {} + virtual int Mock1() const = 0; + virtual int Mock2() const = 0; + virtual int Mock4() const = 0; + virtual int Mock5(int x) const = 0; +}; + +class MockClass : public ClassToMock { + public: + MOCK_METHOD(int, Mock1, (), (const, override)); + MOCK_METHOD(int, Mock2, (), (const, override)); + MOCK_METHOD(int, Mock4, (), (const, override)); + MOCK_METHOD(int, Mock5, (int x), (const, override)); +}; + +using ::testing::Return; + +////////////////////////////////////////////// +// The actual tests + +TEST(CardinalityTest, ExitAfterMethod1_OK) { + StrictMock mock_object; + + auto coro = COROUTINE(ExitAfterMethod1) { + auto cs = WAIT_FOR_CALL(mock_object, Mock1); + cs.RETURN(10); + }; + + // absorb mock calls not accepted by coroutine + EXPECT_CALL(mock_object, Mock2).WillOnce(Return(-1)); + coro.WATCH_CALL(mock_object, Mock1); + coro.WATCH_CALL(mock_object, Mock2); + + // This is the body of the test case + EXPECT_EQ(mock_object.Mock2(), -1); + EXPECT_EQ(mock_object.Mock1(), 10); +} + +TEST(CardinalityTest, ExitAfterMethod1_OK_Wild) { + StrictMock mock_object; + + auto coro = COROUTINE(ExitAfterMethod1) { + auto cs = WAIT_FOR_CALL(mock_object, Mock1); + cs.RETURN(10); + }; + + // absorb mock calls not accepted by coroutine + EXPECT_CALL(mock_object, Mock2).WillOnce(Return(-1)); + coro.WATCH_CALL(); + + // This is the body of the test case + EXPECT_EQ(mock_object.Mock2(), -1); + EXPECT_EQ(mock_object.Mock1(), 10); +} + +TEST(CardinalityTest, ExitAfterMethod1_Oversaturate) { + StrictMock mock_object; + + auto coro = COROUTINE(ExitAfterMethod1) { + auto cs = WAIT_FOR_CALL(mock_object, Mock1); + cs.RETURN(10); + }; + + // absorb mock calls not accepted by coroutine + EXPECT_CALL(mock_object, Mock2).WillOnce(Return(-1)); + coro.WATCH_CALL(mock_object, Mock1); + coro.WATCH_CALL(mock_object, Mock2); + coro.WATCH_CALL(mock_object, Mock4); + + // This is the body of the test case + mock_object.Mock2(); + mock_object.Mock1(); + + // We expect a TEST FAILURE while running the MUT, with a message about + // oversaturation + EXPECT_NONFATAL_FAILURE(mock_object.Mock4(), "Actual: called once - over-saturated and active"); +} + +TEST(CardinalityTest, ExitAfterMethod1_Oversaturate_Wild) { + StrictMock mock_object; + + auto coro = COROUTINE(ExitAfterMethod1) { + auto cs = WAIT_FOR_CALL(mock_object, Mock1); + cs.RETURN(10); + }; + + // absorb mock calls not accepted by coroutine + EXPECT_CALL(mock_object, Mock2).WillOnce(Return(-1)); + coro.WATCH_CALL(mock_object); + + // This is the body of the test case + mock_object.Mock2(); + mock_object.Mock1(); + + // We expect a TEST FAILURE while running the MUT, with a message about + // oversaturation + EXPECT_NONFATAL_FAILURE(mock_object.Mock4(), "Actual: called twice - over-saturated and active"); + // Note different message compared to ExitAfterMethod1_Oversaturate, + // this is because the call count used in the message is kept by the + // individual watchers (=expectations), not the coroutine. +} + +TEST(CardinalityTest, ExitAfterMethod1_Simple) { + StrictMock mock_object; + + auto coro = COROUTINE(ExitAfterMethod1) { + auto cs = WAIT_FOR_CALL(mock_object, Mock1); + cs.RETURN(10); + }; + + coro.WATCH_CALL(mock_object, Mock1); + + // This is the body of the test case + EXPECT_EQ(mock_object.Mock1(), 10); +} + +TEST(CardinalityTest, ExitAfterMethod1_Simple_Wild) { + StrictMock mock_object; + + auto coro = COROUTINE(ExitAfterMethod1) { + auto cs = WAIT_FOR_CALL(mock_object, Mock1); + cs.RETURN(10); + }; + + coro.WATCH_CALL(); + + // This is the body of the test case + EXPECT_EQ(mock_object.Mock1(), 10); +} + +// Note: this one does not use coroutines at all and only +// serves as an example of unsatisfaction in gmock. +TEST(CardinalityTest, NoCoroutine_Unsatisfy) { + auto mock_object_ptr = make_unique>(); + + EXPECT_CALL(*mock_object_ptr, Mock1).Times(3); + + // This is the body of the test case + mock_object_ptr->Mock1(); + + // The mock object should be unsatisfied and therefore fail in its destructor + EXPECT_NONFATAL_FAILURE(mock_object_ptr.reset(), "Actual: called once - unsatisfied and active"); +} + +TEST(CardinalityTest, SatisfyAfterMethod1_Unsatisfy) { + auto mock_object_ptr = make_unique>(); + + auto coro_ptr = NEW_COROUTINE(SatisfyAfterMethod1) { + WAIT_FOR_CALL(*mock_object_ptr, Mock1).RETURN(10); + // Mock calls after this are not required for the test to pass + SATISFY(); + WAIT_FOR_CALL(*mock_object_ptr, Mock1).RETURN(20); + }; + + // absorb mock calls + EXPECT_CALL(*mock_object_ptr, Mock2).WillOnce(Return(-1)); + + coro_ptr->WATCH_CALL(*mock_object_ptr, Mock1); + + // This is the body of the test case + mock_object_ptr->Mock2(); + + delete coro_ptr; // Coro may not have exited so we must delete it before the + // mock object + + // The mock object should be unsatisfied and therefore fail in its destructor + EXPECT_NONFATAL_FAILURE(mock_object_ptr.reset(), "Actual: never called - unsatisfied and active"); +} + +TEST(CardinalityTest, SatisfyAfterMethod1_Unsatisfy_Wild) { + auto mock_object_ptr = make_unique>(); + + auto coro_ptr = NEW_COROUTINE(SatisfyAfterMethod1) { + WAIT_FOR_CALL(*mock_object_ptr, Mock1).RETURN(10); + // Mock calls after this are not required for the test to pass + SATISFY(); + WAIT_FOR_CALL(*mock_object_ptr, Mock1).RETURN(20); + }; + + // absorb mock calls + EXPECT_CALL(*mock_object_ptr, Mock2).WillOnce(Return(-1)); + + coro_ptr->WATCH_CALL(*mock_object_ptr); + + // This is the body of the test case + mock_object_ptr->Mock2(); + + // The mock object should be unsatisfied and therefore fail in its destructor + EXPECT_NONFATAL_FAILURE(delete coro_ptr, "Actual: never called - unsatisfied and active"); + mock_object_ptr.reset(); +} + +TEST(CardinalityTest, SatisfyImmediate_OK) { + StrictMock mock_object; + + auto coro = COROUTINE(SatisfyImmediate) { + // Mock calls after this are not required for the test to pass + SATISFY(); + auto cs = WAIT_FOR_CALL(mock_object, Mock1); + cs.RETURN(10); + }; + + // absorb mock calls + EXPECT_CALL(mock_object, Mock2).WillOnce(Return(-1)); + + coro.WATCH_CALL(mock_object, Mock1); + + // This is the body of the test case + mock_object.Mock2(); +} + +TEST(CardinalityTest, SatisfyImmediate_OK_Wild) { + StrictMock mock_object; + + auto coro = COROUTINE(SatisfyImmediate) { + // Mock calls after this are not required for the test to pass + SATISFY(); + auto cs = WAIT_FOR_CALL(mock_object, Mock1); + cs.RETURN(10); + }; + + // absorb mock calls + EXPECT_CALL(mock_object, Mock2).WillOnce(Return(-1)); + + coro.WATCH_CALL(); + + // This is the body of the test case + mock_object.Mock2(); +} + +TEST(CardinalityTest, SatisfyAfterMethod1_OK) { + StrictMock mock_object; + + auto coro = COROUTINE(SatisfyAfterMethod1) { + WAIT_FOR_CALL(mock_object, Mock1).RETURN(10); + // Mock calls after this are not required for the test to pass + SATISFY(); + WAIT_FOR_CALL(mock_object, Mock1).RETURN(20); + }; + + coro.WATCH_CALL(mock_object, Mock1); + + // This is the body of the test case + mock_object.Mock1(); +} + +TEST(CardinalityTest, SatisfyAfterMethod1_OK_Wild) { + StrictMock mock_object; + + auto coro = COROUTINE(SatisfyAfterMethod1) { + WAIT_FOR_CALL(mock_object, Mock1).RETURN(10); + // Mock calls after this are not required for the test to pass + SATISFY(); + WAIT_FOR_CALL(mock_object, Mock1).RETURN(20); + }; + + coro.WATCH_CALL(mock_object); + + // This is the body of the test case + mock_object.Mock1(); +} + +TEST(CardinalityTest, RetireAfterMethod1_OK) { + StrictMock mock_object; + + auto coro = COROUTINE(RetireAfterMethod1) { + WAIT_FOR_CALL(mock_object, Mock1).RETURN(10); + // Retire will mean the mock will drop all further calls + RETIRE(); + }; + + EXPECT_CALL(mock_object, Mock4).WillOnce(Return(33)); + EXPECT_CALL(mock_object, Mock1).WillOnce(Return(-1)); + coro.WATCH_CALL(mock_object, Mock1); + + // This is the body of the test case + EXPECT_EQ(mock_object.Mock1(), 10); + EXPECT_EQ(mock_object.Mock1(), -1); + EXPECT_EQ(mock_object.Mock4(), 33); +} + +TEST(CardinalityTest, RetireAfterMethod1_OK_Wild) { + StrictMock mock_object; + + auto coro = COROUTINE(RetireAfterMethod1) { + WAIT_FOR_CALL(mock_object, Mock1).RETURN(10); + // Retire will mean the mock will drop all further calls + RETIRE(); + }; + + EXPECT_CALL(mock_object, Mock4).WillOnce(Return(33)); + EXPECT_CALL(mock_object, Mock1).WillOnce(Return(-1)); + coro.WATCH_CALL(); + + // This is the body of the test case + EXPECT_EQ(mock_object.Mock1(), 10); + EXPECT_EQ(mock_object.Mock1(), -1); + EXPECT_EQ(mock_object.Mock4(), 33); +} + +TEST(CardinalityTest, RetireAfterMethod1_Unsatisfy) { + auto mock_object_ptr = make_unique>(); + + auto coro_ptr = NEW_COROUTINE(RetireAfterMethod1) { + WAIT_FOR_CALL(*mock_object_ptr, Mock1).RETURN(10); + // Retire will mean the mock will drop all further calls + RETIRE(); + }; + + EXPECT_CALL(*mock_object_ptr, Mock4).WillOnce(Return(33)); + coro_ptr->WATCH_CALL(*mock_object_ptr, Mock1); + + // This is the body of the test case + EXPECT_EQ(mock_object_ptr->Mock4(), 33); + + delete coro_ptr; // Coro may not have exited so we must delete it before the + // mock object + + // The mock object should be unsatisfied and therefore fail in its destructor + EXPECT_NONFATAL_FAILURE(mock_object_ptr.reset(), "Actual: never called - unsatisfied and active"); +} + +TEST(CardinalityTest, RetireAfterMethod1_Unsatisfy_Wild) { + auto mock_object_ptr = make_unique>(); + + auto coro_ptr = NEW_COROUTINE(RetireAfterMethod1) { + WAIT_FOR_CALL(*mock_object_ptr, Mock1).RETURN(10); + // Retire will mean the mock will drop all further calls + RETIRE(); + }; + + EXPECT_CALL(*mock_object_ptr, Mock4).WillOnce(Return(33)); + coro_ptr->WATCH_CALL(); + + // This is the body of the test case + EXPECT_EQ(mock_object_ptr->Mock4(), 33); + + // The mock object should be unsatisfied and therefore fail in its destructor + EXPECT_NONFATAL_FAILURE(delete coro_ptr;, "Actual: never called - unsatisfied and active"); + mock_object_ptr.reset(); +} + +TEST(CardinalityTest, NoCoroutine_Unexpected) { + StrictMock mock_object; + + EXPECT_CALL(mock_object, Mock5(99)); + + // This is the body of the test case + mock_object.Mock5(99); + EXPECT_NONFATAL_FAILURE(mock_object.Mock5(55), "Unexpected mock function call - returning default value."); +} + +TEST(CardinalityTest, Coroutine_Unexpected) { + StrictMock mock_object; + + auto coro = COROUTINE(Unexpected) { + SATISFY(); + while (1) { + auto e = NEXT_EVENT(); + if (auto e2 = e.IS_CALL(mock_object, Mock5(99))) + e2.RETURN(0); + else + e.DROP(); + } + }; + coro.WATCH_CALL(); + + // This is the body of the test case + mock_object.Mock5(99); + EXPECT_NONFATAL_FAILURE(mock_object.Mock5(55), "Unexpected mock function call - returning default value."); +} + +TEST(CardinalityTest, ExitAfterMethod1_OversaturateOnSatisfy) { + StrictMock mock_object; + + auto coro = COROUTINE(ExitAfterMethod1) { + auto cs = WAIT_FOR_CALL(mock_object, Mock1); + cs.RETURN(10); + SATISFY(); + }; + + // absorb mock calls not accepted by coroutine + EXPECT_CALL(mock_object, Mock2).WillOnce(Return(-1)); + coro.WATCH_CALL(mock_object, Mock1); + coro.WATCH_CALL(mock_object, Mock2); + coro.WATCH_CALL(mock_object, Mock4); + + // This is the body of the test case + mock_object.Mock2(); + mock_object.Mock1(); + + // We expect a TEST FAILURE while running the MUT, with a message about + // oversaturation even though the interior indicated satisfaction. + EXPECT_NONFATAL_FAILURE(mock_object.Mock4(), "Actual: called once - over-saturated and active"); +} + +TEST(CardinalityTest, UnsatisfyRetired1) { + auto mock_object_ptr = make_unique>(); + + auto coro_ptr = NEW_COROUTINE(SatisfyAfterMethod1) { + RETIRE(); + WAIT_FOR_CALL(*mock_object_ptr, Mock1).RETURN(10); + }; + + // absorb mock calls + EXPECT_CALL(*mock_object_ptr, Mock2).WillOnce(Return(-1)); + + coro_ptr->WATCH_CALL(*mock_object_ptr, Mock1); + + // This is the body of the test case + mock_object_ptr->Mock2(); + + delete coro_ptr; // Coro may not have exited so we must delete it before the + // mock object + + // The mock object should be unsatisfied and therefore fail in its destructor + EXPECT_NONFATAL_FAILURE(mock_object_ptr.reset(), "Actual: never called - unsatisfied and retired"); +} + +TEST(CardinalityTest, UnsatisfyRetired2) { + auto mock_object_ptr = make_unique>(); + + auto coro_ptr = NEW_COROUTINE(SatisfyAfterMethod1) { + WAIT_FOR_CALL(*mock_object_ptr, Mock1).RETURN(10); + RETIRE(); + WAIT_FOR_CALL(*mock_object_ptr, Mock1).RETURN(20); // Ensure coro is unsatisfied + }; + + coro_ptr->WATCH_CALL(*mock_object_ptr, Mock1); + + // This is the body of the test case + mock_object_ptr->Mock1(); + + delete coro_ptr; // Coro may not have exited so we must delete it before the + // mock object + + // The mock object should be unsatisfied and therefore fail in its destructor + EXPECT_NONFATAL_FAILURE(mock_object_ptr.reset(), "Actual: called once - unsatisfied and retired"); +} + +TEST(CardinalityTest, UnsatisfyRetired3) { + auto mock_object_ptr = make_unique>(); + + auto coro_ptr = NEW_COROUTINE(SatisfyAfterMethod1) { + WAIT_FOR_CALL(*mock_object_ptr, Mock1).RETURN(10); + RETIRE(); + WAIT_FOR_CALL(*mock_object_ptr, Mock1).RETURN(10); // Ensure RETIRE() runs (see UnsatisfyRetired2) + WAIT_FOR_CALL(*mock_object_ptr, Mock1).RETURN(20); // Ensure coro is unsatisfied + }; + + coro_ptr->WATCH_CALL(*mock_object_ptr, Mock1); + + // This is the body of the test case + mock_object_ptr->Mock1(); + EXPECT_NONFATAL_FAILURE(mock_object_ptr->Mock1(), "Actual: it is retired"); + + delete coro_ptr; // Coro may not have exited so we must delete it before the + // mock object + + // The mock object should be unsatisfied and therefore fail in its destructor + EXPECT_NONFATAL_FAILURE(mock_object_ptr.reset(), "Actual: called once - unsatisfied and retired"); +} + +TEST(CardinalityTest, SatisfyDestruct) { + auto mock_object_ptr = make_unique>(); + + auto coro_ptr = NEW_COROUTINE(SatisfyAfterMethod1) { + WAIT_FOR_CALL(*mock_object_ptr, Mock1).RETURN(10); + SATISFY(); + WAIT_FOR_CALL(*mock_object_ptr, Mock1).RETURN(20); // Ensure coro is unsatisfied + }; + + coro_ptr->WATCH_CALL(*mock_object_ptr, Mock1); + + // This is the body of the test case + EXPECT_EQ(mock_object_ptr->Mock1(), 10); + + delete coro_ptr; // Coro may not have exited so we must delete it before the + // mock object + + // The mock object should be unsatisfied and therefore fail in its destructor + mock_object_ptr.reset(); +} + +TEST(CardinalityTest, SatisfyDestruct2) { + auto mock_object_ptr = make_unique>(); + + auto coro_ptr = NEW_COROUTINE(SatisfyAfterMethod1) { + WAIT_FOR_CALL(*mock_object_ptr, Mock1).RETURN(10); + SATISFY(); + }; + + coro_ptr->WATCH_CALL(*mock_object_ptr, Mock1); + + // This is the body of the test case + EXPECT_EQ(mock_object_ptr->Mock1(), 10); + + delete coro_ptr; // Coro may not have exited so we must delete it before the + // mock object + + // The mock object should be unsatisfied and therefore fail in its destructor + mock_object_ptr.reset(); +} diff --git a/coroutines/test/cotest-ext-filter.cc b/coroutines/test/cotest-ext-filter.cc new file mode 100644 index 000000000..c4402b079 --- /dev/null +++ b/coroutines/test/cotest-ext-filter.cc @@ -0,0 +1,235 @@ +#include + +#include "cotest/cotest.h" +#include "gtest/gtest-spi.h" + +using namespace std; +using namespace testing; +using ::testing::StrictMock; + +////////////////////////////////////////////// +// Mocking assets + +class ClassToMock { + public: + virtual ~ClassToMock() {} + virtual int Mock1(int i) const = 0; + virtual int Mock2(int i, int j) const = 0; +}; +class MockClass : public ClassToMock { + public: + MOCK_METHOD(int, Mock1, (int i), (const, override)); + MOCK_METHOD(int, Mock2, (int i, int j), (const, override)); +}; + +////////////////////////////////////////////// +// The actual tests + +TEST(UserInterfaceTest, Method1_Base) { + StrictMock mock_object; + + auto coro = COROUTINE(Method1) { + auto cs = WAIT_FOR_CALL(mock_object, Mock1); + EXPECT_EQ(cs.GetArg<0>(), 200); + cs.RETURN(10); + }; + + coro.WATCH_CALL(mock_object, Mock1); + + EXPECT_EQ(mock_object.Mock1(200), 10); +} + +TEST(UserInterfaceTest, Method1_Underscore) { + StrictMock mock_object; + + auto coro = COROUTINE(Method1) { + auto cs = WAIT_FOR_CALL(mock_object, Mock1); + EXPECT_EQ(cs.GetArg<0>(), 200); + cs.RETURN(10); + }; + + coro.WATCH_CALL(mock_object, Mock1(_)); + + EXPECT_EQ(mock_object.Mock1(200), 10); +} + +TEST(UserInterfaceTest, Method1_Match) { + StrictMock mock_object; + + auto coro = COROUTINE(Method1) { + auto cs = WAIT_FOR_CALL(mock_object, Mock1); + EXPECT_EQ(cs.GetArg<0>(), 200); + cs.RETURN(10); + }; + + coro.WATCH_CALL(mock_object, Mock1(200)); + + EXPECT_EQ(mock_object.Mock1(200), 10); +} + +TEST(UserInterfaceTest, Method1_NoMatch) { + StrictMock mock_object; + + auto coro = COROUTINE(FailIfCall) { + SATISFY(); // If we never get the call, we're fine + WAIT_FOR_CALL(); + COTEST_ASSERT(false); + }; + + EXPECT_CALL(mock_object, Mock1(200)).WillOnce(Return(20)); + coro.WATCH_CALL(mock_object, Mock1(100)); + + EXPECT_EQ(mock_object.Mock1(200), 20); +} + +TEST(UserInterfaceTest, Method2_Base) { + StrictMock mock_object; + + auto coro = COROUTINE(Method2) { + auto cs = WAIT_FOR_CALL(mock_object, Mock2); + EXPECT_EQ(cs.GetArg<0>(), 200); + EXPECT_EQ(cs.GetArg<1>(), 300); + cs.RETURN(10); + }; + + coro.WATCH_CALL(mock_object, Mock2); + + EXPECT_EQ(mock_object.Mock2(200, 300), 10); +} + +TEST(UserInterfaceTest, Method2_Underscore) { + StrictMock mock_object; + + auto coro = COROUTINE(Method2) { + auto cs = WAIT_FOR_CALL(mock_object, Mock2); + EXPECT_EQ(cs.GetArg<0>(), 200); + EXPECT_EQ(cs.GetArg<1>(), 300); + cs.RETURN(10); + }; + + coro.WATCH_CALL(mock_object, Mock2(_, _)); + + EXPECT_EQ(mock_object.Mock2(200, 300), 10); +} + +TEST(UserInterfaceTest, Method2_Mix1) { + StrictMock mock_object; + + auto coro = COROUTINE(Method2) { + auto cs = WAIT_FOR_CALL(mock_object, Mock2); + EXPECT_EQ(cs.GetArg<0>(), 200); + EXPECT_EQ(cs.GetArg<1>(), 300); + cs.RETURN(10); + }; + + coro.WATCH_CALL(mock_object, Mock2(_, 300)); + + EXPECT_EQ(mock_object.Mock2(200, 300), 10); +} + +TEST(UserInterfaceTest, Method2_Mix2) { + StrictMock mock_object; + + auto coro = COROUTINE(Method2) { + auto cs = WAIT_FOR_CALL(mock_object, Mock2); + EXPECT_EQ(cs.GetArg<0>(), 200); + EXPECT_EQ(cs.GetArg<1>(), 300); + cs.RETURN(10); + }; + + coro.WATCH_CALL(mock_object, Mock2(200, _)); + + EXPECT_EQ(mock_object.Mock2(200, 300), 10); +} + +TEST(UserInterfaceTest, Method2_Match) { + StrictMock mock_object; + + auto coro = COROUTINE(Method2) { + auto cs = WAIT_FOR_CALL(mock_object, Mock2); + EXPECT_EQ(cs.GetArg<0>(), 200); + EXPECT_EQ(cs.GetArg<1>(), 300); + cs.RETURN(10); + }; + + coro.WATCH_CALL(mock_object, Mock2(200, 300)); + + EXPECT_EQ(mock_object.Mock2(200, 300), 10); +} + +TEST(UserInterfaceTest, Method2_Mix_Mismatch) { + StrictMock mock_object; + + auto coro = COROUTINE(FailIfCall) { + SATISFY(); // If we never get the call, we're fine + WAIT_FOR_CALL(); + COTEST_ASSERT(false); + }; + + EXPECT_CALL(mock_object, Mock2(200, _)).WillOnce(Return(20)); + coro.WATCH_CALL(mock_object, Mock2(202, _)); + + EXPECT_EQ(mock_object.Mock2(200, 300), 20); +} + +TEST(UserInterfaceTest, Method2_Mismatch) { + StrictMock mock_object; + + auto coro = COROUTINE(FailIfCall) { + SATISFY(); // If we never get the call, we're fine + WAIT_FOR_CALL(); + COTEST_ASSERT(false); + }; + + EXPECT_CALL(mock_object, Mock2(200, 300)).WillOnce(Return(20)); + coro.WATCH_CALL(mock_object, Mock2(200, 303)); + + EXPECT_EQ(mock_object.Mock2(200, 300), 20); +} + +TEST(UserInterfaceTest, Method2_With) { + StrictMock mock_object; + + auto coro = COROUTINE(Method2_With) { + auto cs = WAIT_FOR_CALL(mock_object, Mock2); + EXPECT_EQ(cs.GetArg<0>(), 200); + EXPECT_EQ(cs.GetArg<1>(), 300); + cs.RETURN(10); + }; + + coro.WATCH_CALL(mock_object, Mock2).With(Lt()); + + EXPECT_EQ(mock_object.Mock2(200, 300), 10); +} + +TEST(UserInterfaceTest, Method2_With_Mismatch) { + StrictMock mock_object; + + auto coro = COROUTINE(Method2_With_Mismatch) { + SATISFY(); // If we never get the call, we're fine + WAIT_FOR_CALL(); + COTEST_ASSERT(false); + }; + + EXPECT_CALL(mock_object, Mock2).WillOnce(Return(20)); + coro.WATCH_CALL(mock_object, Mock2).With(Gt()); + + EXPECT_EQ(mock_object.Mock2(200, 300), 20); +} + +TEST(UserInterfaceTest, OverlappingWatchers) { + StrictMock mock_object; + + auto coro = COROUTINE() { + auto e = NEXT_EVENT(); + e.DROP(); + }; + + EXPECT_CALL(mock_object, Mock2).WillOnce(Return(20)); + // These two watchers overlap in scope: cotest must ensure the + // coro only "sees" the mock call once. + coro.WATCH_CALL(mock_object, Mock2); + coro.WATCH_CALL(); + + EXPECT_EQ(mock_object.Mock2(200, 300), 20); +} diff --git a/coroutines/test/cotest-int-filter.cc b/coroutines/test/cotest-int-filter.cc new file mode 100644 index 000000000..7d7cb7582 --- /dev/null +++ b/coroutines/test/cotest-int-filter.cc @@ -0,0 +1,347 @@ +#include + +#include "cotest/cotest.h" +#include "gtest/gtest-spi.h" + +using namespace std; +using namespace testing; +using ::testing::StrictMock; + +////////////////////////////////////////////// +// Mocking assets + +class ClassToMock { + public: + virtual ~ClassToMock() {} + virtual int Mock1(int i) const = 0; + virtual int Mock2(int i, int j) const = 0; + virtual int Mock3(int i) const = 0; + virtual int Mock4(int i) const = 0; + virtual int Mock4(int i) = 0; +}; +class MockClass : public ClassToMock { + public: + MOCK_METHOD(int, Mock1, (int i), (const, override)); + MOCK_METHOD(int, Mock2, (int i, int j), (const, override)); + MOCK_METHOD(int, Mock3, (int i), (const, override)); + MOCK_METHOD(int, Mock4, (int i), (const, override)); + MOCK_METHOD(int, Mock4, (int i), (override)); +}; + +class ClassToMockA { + public: + virtual ~ClassToMockA() {} + virtual int Mock(int i) const = 0; +}; +class MockClassA : public ClassToMockA { + public: + MOCK_METHOD(int, Mock, (int i), (const, override)); +}; + +class ClassToMockB { + public: + virtual ~ClassToMockB() {} + virtual int Mock(int i) const = 0; +}; +class MockClassB : public ClassToMockB { + public: + MOCK_METHOD(int, Mock, (int i), (const, override)); +}; + +////////////////////////////////////////////// +// The actual tests + +TEST(InteriorFilteringTest, MethodBasic) { + StrictMock mock_object; + + auto coro = COROUTINE(MethodBasic) { + auto cg = WAIT_FOR_CALL(); + EXPECT_TRUE(cg.FromMain()); + EXPECT_FALSE(cg.IS_CALL(mock_object, Mock1(201))); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock1)); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock1(_))); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock1(200)).RETURN(10)); + cg = WAIT_FOR_CALL(); + EXPECT_FALSE(cg.IS_CALL(mock_object, Mock2(_, 401))); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock2)); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock2(_, _))); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock2(_, 400))); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock2(200, _)).RETURN(20)); + }; + + coro.WATCH_CALL(mock_object, Mock1); + coro.WATCH_CALL(mock_object, Mock2); + + EXPECT_EQ(mock_object.Mock1(200), 10); + EXPECT_EQ(mock_object.Mock2(200, 400), 20); +} + +TEST(InteriorFilteringTest, MethodName) { + StrictMock mock_object; + + auto coro = COROUTINE(MethodName) { + auto cg = WAIT_FOR_CALL(); + EXPECT_FALSE(cg.IS_CALL(mock_object, Mock3)); + EXPECT_FALSE(cg.IS_CALL(mock_object, Mock3(_))); + EXPECT_FALSE(cg.IS_CALL(mock_object, Mock3(200))); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock1).RETURN(10)); + }; + + coro.WATCH_CALL(mock_object, Mock1); + + EXPECT_EQ(mock_object.Mock1(200), 10); +} + +TEST(InteriorFilteringTest, MethodConst) { + StrictMock mock_object; + + auto coro = COROUTINE(MethodConst) { + auto cg = WAIT_FOR_CALL(); + EXPECT_FALSE(cg.IS_CALL(Const(mock_object), Mock4(_))); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock4(_)).RETURN(30)); + cg = WAIT_FOR_CALL(); + EXPECT_FALSE(cg.IS_CALL(mock_object, Mock4(_))); + EXPECT_TRUE(cg.IS_CALL(Const(mock_object), Mock4(_)).RETURN(40)); + }; + + const StrictMock &const_ref_mock_object(mock_object); + coro.WATCH_CALL(mock_object, Mock4(_)); + coro.WATCH_CALL(Const(mock_object), Mock4(_)); + + EXPECT_EQ(mock_object.Mock4(200), 30); + EXPECT_EQ(const_ref_mock_object.Mock4(200), 40); +} + +TEST(InteriorFilteringTest, TwoMethod) { + StrictMock mock_object; + StrictMock mock_object2; + + auto coro = COROUTINE(TwoMethod) { + auto cg = WAIT_FOR_CALL(); + EXPECT_FALSE(cg.IS_CALL(mock_object, Mock1(201))); + EXPECT_FALSE(cg.IS_CALL(mock_object2, Mock1)); + EXPECT_FALSE(cg.IS_CALL(mock_object2, Mock1(_))); + EXPECT_FALSE(cg.IS_CALL(mock_object2, Mock1(200))); + EXPECT_TRUE(cg.IS_CALL(mock_object)); // 1-arg IS_CALL() matches by mock object + EXPECT_FALSE(cg.IS_CALL(mock_object2)); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock1)); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock1(_))); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock1(200)).RETURN(20)); + cg = WAIT_FOR_CALL(); + EXPECT_FALSE(cg.IS_CALL(mock_object, Mock2(_, 401))); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock2(Ne(201), Ne(399)))); + EXPECT_FALSE(cg.IS_CALL(mock_object, Mock2(Ne(200), _))); + EXPECT_TRUE(cg.IS_CALL(mock_object)); + EXPECT_FALSE(cg.IS_CALL(mock_object2)); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock2)); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock2(_, _))); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock2(_, 400))); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock2(200, _)).RETURN(30)); + }; + + coro.WATCH_CALL(mock_object, Mock1); + coro.WATCH_CALL(mock_object, Mock2); + + EXPECT_EQ(mock_object.Mock1(200), 20); + EXPECT_EQ(mock_object.Mock2(200, 400), 30); +} + +TEST(InteriorFilteringTest, MethodByClass) { + StrictMock mock_object_a; + StrictMock mock_object_b; + + auto coro = COROUTINE(MethodByClass) { + auto cg = WAIT_FOR_CALL(); + EXPECT_FALSE(cg.IS_CALL(mock_object_b, Mock)); + EXPECT_TRUE(cg.IS_CALL(mock_object_a, Mock).RETURN(10)); + }; + coro.WATCH_CALL(mock_object_a, Mock); + + EXPECT_EQ(mock_object_a.Mock(200), 10); +} + +TEST(InteriorFilteringTest, WithClause) { + StrictMock mock_object; + + auto coro = COROUTINE(WithClause) { + auto cg = WAIT_FOR_CALL(); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock2(_, _)).With(Lt())); + EXPECT_FALSE(cg.IS_CALL(mock_object, Mock2(0, 0)).With(Lt())); // With() on NULL call session must be safe + auto cs = cg.IS_CALL(mock_object, Mock2); + EXPECT_THAT(cs.GetArgs(), Lt()); + EXPECT_THAT(cs.GetArgs(), Not(Ge())); + cs.RETURN(10); + cg = WAIT_FOR_CALL(); + EXPECT_FALSE(cg.IS_CALL(mock_object, Mock2(_, _)).With(Lt())); + cg.IS_CALL(mock_object, Mock2).With(Gt()).RETURN(20); + }; + + coro.WATCH_CALL(mock_object, Mock2); + + EXPECT_EQ(mock_object.Mock2(200, 400), 10); + EXPECT_EQ(mock_object.Mock2(200, 100), 20); +} + +MATCHER(IsEven, "") { return (arg % 2) == 0; } + +TEST(InteriorFilteringTest, Complex) { + StrictMock mock_object; + + auto coro = COROUTINE(Complex) { + while (true) { + auto cs = MOCK_CALL_HANDLE(mock_object, Mock1(_)); + auto cs2 = MOCK_CALL_HANDLE(mock_object, Mock1); + + cs = WAIT_FOR_CALL().IS_CALL(mock_object, Mock1(IsEven())); + EXPECT_TRUE(cs); + cs.RETURN(10); + cs2 = WAIT_FOR_CALL().IS_CALL(mock_object, Mock1(Not(IsEven()))); + EXPECT_TRUE(cs2); + cs2.RETURN(10); + SATISFY(); + } + }; + + coro.WATCH_CALL(mock_object, Mock1); + for (int i = 0; i < 10; i++) EXPECT_EQ(mock_object.Mock1(i), 10); +} + +TEST(InteriorFilteringTest, ComplexWaiting) { + StrictMock mock_object; + + auto coro = COROUTINE(ComplexWaiting) { + while (true) { + // Be a better neighbour by passing on unmatching calls rather than + // generating an error here - but we may be passing on too many + auto cs = WAIT_FOR_CALL(mock_object, Mock1(IsEven())); + EXPECT_TRUE(cs); + cs.RETURN(10); + cs = WAIT_FOR_CALL(mock_object, Mock1(Not(IsEven()))); + EXPECT_TRUE(cs); + cs.RETURN(10); + SATISFY(); + } + }; + + coro.WATCH_CALL(mock_object, Mock1); + for (int i = 0; i < 10; i++) EXPECT_EQ(mock_object.Mock1(i), 10); +} + +TEST(InteriorFilteringTest, ComplexWaitingPrio) { + StrictMock mock_object; + + auto coro = COROUTINE(ComplexWaitingPrio) { + while (true) { + // Only drop negative args + auto cs = WAIT_FOR_CALL(mock_object, Mock1(Ge(0))); + EXPECT_TRUE(cs.WithArg<0>(IsEven())); + EXPECT_FALSE(cs.WithArg<0>(Not(IsEven()))); + cs.RETURN(10); + cs = WAIT_FOR_CALL(mock_object, Mock1(Ge(0))); + EXPECT_TRUE(cs.WithArg<0>(Not(IsEven()))); + EXPECT_FALSE(cs.WithArg<0>(IsEven())); + cs.RETURN(10); + SATISFY(); + } + }; + + // Of course, the EXPECT_CALL could come after the WATCH_CALL and + // therefore have a higher priority, and thus do the filtering, but this way + // tests the coro more. + EXPECT_CALL(mock_object, Mock1(-1)).Times(3).WillRepeatedly(Return(-1)); + coro.WATCH_CALL(mock_object, Mock1); + for (int i = 0; i < 10; i++) { + EXPECT_EQ(mock_object.Mock1(i), 10); + if (i % 7 == 0 || i % 5 == 0) { + EXPECT_EQ(mock_object.Mock1(-1), -1); + } + } +} + +TEST(InteriorFilteringTest, ComplexWaitingPrioET) { + StrictMock mock_object; + + auto coro = COROUTINE(ComplexWaitingPrioET) { + while (true) { + // Pass on negative args + auto cs = WAIT_FOR_CALL(mock_object, Mock1(Ge(0))); + EXPECT_THAT(cs.GetArg<0>(), IsEven()); + cs.RETURN(10); + cs = WAIT_FOR_CALL(mock_object, Mock1(Ge(0))); + EXPECT_THAT(cs.GetArg<0>(), Not(IsEven())); + EXPECT_THAT(cs.GetArg<0>() + 1, + IsEven()); // show that we have the underlying int + cs.RETURN(10); + SATISFY(); + } + }; + + EXPECT_CALL(mock_object, Mock1(-5)).Times(3).WillRepeatedly(Return(-2)); + coro.WATCH_CALL(mock_object, Mock1); + for (int i = 0; i < 10; i++) { + EXPECT_EQ(mock_object.Mock1(i), 10); + if (i % 7 == 0 || i % 5 == 0) { + EXPECT_EQ(mock_object.Mock1(-5), -2); + } + } +} + +TEST(InteriorFilteringTest, TwoMethodWaiting) { + StrictMock mock_object; + StrictMock mock_object2; + + auto coro = COROUTINE(TwoMethodWaiting) { + auto cg = WAIT_FOR_CALL(mock_object); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock1(200)).RETURN(20)); + cg = WAIT_FOR_CALL(mock_object); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock2(200, 400)).RETURN(30)); + }; + + EXPECT_CALL(mock_object2, Mock1).Times(2).WillRepeatedly(Return(-2)); + EXPECT_CALL(mock_object2, Mock2).Times(2).WillRepeatedly(Return(-3)); + coro.WATCH_CALL(mock_object, Mock1); + coro.WATCH_CALL(mock_object, Mock2); + coro.WATCH_CALL(mock_object2, Mock1); + coro.WATCH_CALL(mock_object2, Mock2); + + EXPECT_EQ(mock_object2.Mock1(500), -2); + EXPECT_EQ(mock_object2.Mock2(500, 600), -3); + EXPECT_EQ(mock_object.Mock1(200), 20); + EXPECT_EQ(mock_object2.Mock1(501), -2); + EXPECT_EQ(mock_object2.Mock2(501, 601), -3); + EXPECT_EQ(mock_object.Mock2(200, 400), 30); + // If we want to make further calls here, + // we will need the coro to RETIRE() to avoid oversaturation, + // see test case TwoMethodWaitingAndRetire +} + +TEST(InteriorFilteringTest, TwoMethodWaitingAndRetire) { + StrictMock mock_object; + StrictMock mock_object2; + + auto coro = COROUTINE(TwoMethodWaitingAndRetire) { + auto cg = WAIT_FOR_CALL(mock_object); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock1(200)).RETURN(20)); + cg = WAIT_FOR_CALL(mock_object); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock2(200, 400)).RETURN(30)); + // We believe we're done: retire to drop all subsequent calls + RETIRE(); + }; + + EXPECT_CALL(mock_object2, Mock1).Times(3).WillRepeatedly(Return(-2)); + EXPECT_CALL(mock_object2, Mock2).Times(3).WillRepeatedly(Return(-3)); + coro.WATCH_CALL(mock_object, Mock1); + coro.WATCH_CALL(mock_object, Mock2); + coro.WATCH_CALL(mock_object2, Mock1); + coro.WATCH_CALL(mock_object2, Mock2); + + EXPECT_EQ(mock_object2.Mock1(500), -2); + EXPECT_EQ(mock_object2.Mock2(500, 600), -3); + EXPECT_EQ(mock_object.Mock1(200), 20); + EXPECT_EQ(mock_object2.Mock1(501), -2); + EXPECT_EQ(mock_object2.Mock2(501, 601), -3); + EXPECT_EQ(mock_object.Mock2(200, 400), 30); + // These actually reach the retired coroutine + EXPECT_EQ(mock_object2.Mock1(502), -2); + EXPECT_EQ(mock_object2.Mock2(502, 602), -3); +} diff --git a/coroutines/test/cotest-lambda.cc b/coroutines/test/cotest-lambda.cc new file mode 100644 index 000000000..6e0e3f68f --- /dev/null +++ b/coroutines/test/cotest-lambda.cc @@ -0,0 +1,88 @@ +#include + +#include "cotest/cotest.h" +#include "gtest/gtest-spi.h" + +using namespace std; +using namespace testing; +using ::testing::StrictMock; + +////////////////////////////////////////////// +// Mocking assets + +class ClassToMock { + public: + virtual ~ClassToMock() {} + virtual int Mock1(int i) const = 0; + virtual int Mock2(int i, int j) const = 0; + virtual int Mock3(int i) const = 0; + virtual int Mock4(int i) const = 0; + virtual int Mock4(int i) = 0; +}; +class MockClass : public ClassToMock { + public: + MOCK_METHOD(int, Mock1, (int i), (const, override)); + MOCK_METHOD(int, Mock2, (int i, int j), (const, override)); + MOCK_METHOD(int, Mock3, (int i), (const, override)); + MOCK_METHOD(int, Mock4, (int i), (const, override)); + MOCK_METHOD(int, Mock4, (int i), (override)); +}; + +////////////////////////////////////////////// +// The actual tests +TEST(LambdaTest, Lambda) { + StrictMock mock_object; + StrictMock mock_object2; + + auto coro = COROUTINE(Lambda) { + auto cg = WAIT_FOR_CALL(); + EXPECT_FALSE(cg.IS_CALL(mock_object, Mock1(201))); + EXPECT_FALSE(cg.IS_CALL(mock_object2, Mock1)); + EXPECT_FALSE(cg.IS_CALL(mock_object2, Mock1(_))); + EXPECT_FALSE(cg.IS_CALL(mock_object2, Mock1(200))); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock1)); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock1(_))); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock1(200)).RETURN(20)); + cg = WAIT_FOR_CALL(); + EXPECT_FALSE(cg.IS_CALL(mock_object, Mock2(_, 401))); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock2)); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock2(_, _))); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock2(_, 400))); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock2(200, _)).RETURN(30)); + }; + + coro.WATCH_CALL(mock_object, Mock1); + coro.WATCH_CALL(mock_object, Mock2); + + EXPECT_EQ(mock_object.Mock1(200), 20); + EXPECT_EQ(mock_object.Mock2(200, 400), 30); +} + +TEST(LambdaTest, WatchInside) { + StrictMock mock_object; + StrictMock mock_object2; + + auto coro = COROUTINE() // Test the no name given case + { + WATCH_CALL(mock_object, Mock1); + WATCH_CALL(mock_object, Mock2); + + auto cg = WAIT_FOR_CALL(); + EXPECT_FALSE(cg.IS_CALL(mock_object, Mock1(201))); + EXPECT_FALSE(cg.IS_CALL(mock_object2, Mock1)); + EXPECT_FALSE(cg.IS_CALL(mock_object2, Mock1(_))); + EXPECT_FALSE(cg.IS_CALL(mock_object2, Mock1(200))); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock1)); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock1(_))); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock1(200)).RETURN(20)); + cg = WAIT_FOR_CALL(); + EXPECT_FALSE(cg.IS_CALL(mock_object, Mock2(_, 401))); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock2)); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock2(_, _))); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock2(_, 400))); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock2(200, _)).RETURN(30)); + }; + + EXPECT_EQ(mock_object.Mock1(200), 20); + EXPECT_EQ(mock_object.Mock2(200, 400), 30); +} diff --git a/coroutines/test/cotest-launch-lifetime.cc b/coroutines/test/cotest-launch-lifetime.cc new file mode 100644 index 000000000..126663de9 --- /dev/null +++ b/coroutines/test/cotest-launch-lifetime.cc @@ -0,0 +1,111 @@ +#include + +#include "cotest/cotest.h" +#include "gtest/gtest-spi.h" + +using namespace std; +using namespace testing; +using ::testing::StrictMock; + +//////////////////////////////////////////// +// Code under test + +struct ResultObjectDestructCounter { + ~ResultObjectDestructCounter() { count++; } + static int count; +}; + +int ResultObjectDestructCounter::count = 0; + +class ClassToMock { + public: + virtual ~ClassToMock() {} + virtual int Mock1() const = 0; +}; + +class ExampleClass { + public: + ResultObjectDestructCounter Example1() { return ResultObjectDestructCounter(); } +}; + +//////////////////////////////////////////// +// Mocking assets + +class MockClass : public ClassToMock { + public: + MOCK_METHOD(int, Mock1, (), (const, override)); +}; + +////////////////////////////////////////////// +// The actual tests + +TEST(ResultLifetimeTest, ReturnOverlapCase1) { + StrictMock mock_object; + ExampleClass example; + + COTEST_CLEANUP(); + ResultObjectDestructCounter::count = 0; + + auto coro = COROUTINE() { + auto d = LAUNCH(example.Example1()); + auto e = NEXT_EVENT(); + + auto d2 = LAUNCH(example.Example1()); + auto e2 = NEXT_EVENT(); + + EXPECT_TRUE(e.IS_RESULT(d)); + EXPECT_TRUE(e2.IS_RESULT(d2)); + + // Both launch sessions (d and d2) are still in scope and so the + // returned objects should not have been destructed. + EXPECT_EQ(ResultObjectDestructCounter::count, 0); + }; +} + +TEST(ResultLifetimeTest, ReturnOverlapCase2) { + StrictMock mock_object; + ExampleClass example; + + COTEST_CLEANUP(); + ResultObjectDestructCounter::count = 0; + + auto coro = COROUTINE() { + auto d = LAUNCH(example.Example1()); + auto e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_RESULT(d)); + + auto d2 = LAUNCH(example.Example1()); + auto e2 = NEXT_EVENT(); + EXPECT_TRUE(e2.IS_RESULT(d2)); + + // Both launch sessions (d and d2) are still in scope and so the + // returned objects should not have been destructed. + EXPECT_EQ(ResultObjectDestructCounter::count, 0); + }; +} + +TEST(ResultLifetimeTest, ReturnOverlapCase3) { + StrictMock mock_object; + ExampleClass example; + + COTEST_CLEANUP(); + ResultObjectDestructCounter::count = 0; + + { + auto coro = COROUTINE() { + { + auto d = LAUNCH(example.Example1()); + auto e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_RESULT(d)); + } + auto d2 = LAUNCH(example.Example1()); + auto e2 = NEXT_EVENT(); + EXPECT_TRUE(e2.IS_RESULT(d2)); + + // Check that at least one launch session (d2) has not had its + // return object destructed. Note the _LE condition - destruction + // is not required by the implementation. + EXPECT_LE(ResultObjectDestructCounter::count, 1); + }; + } +} diff --git a/coroutines/test/cotest-launch-mock.cc b/coroutines/test/cotest-launch-mock.cc new file mode 100644 index 000000000..b9083882c --- /dev/null +++ b/coroutines/test/cotest-launch-mock.cc @@ -0,0 +1,361 @@ +#include + +#include "cotest/cotest.h" +#include "gtest/gtest-spi.h" + +using namespace std; +using namespace testing; +using ::testing::StrictMock; + +//////////////////////////////////////////// +// Code under test + +class ClassToMock { + public: + virtual ~ClassToMock() {} + virtual int Mock1(int x) = 0; + virtual int Mock2(int x) = 0; +}; + +class ExampleClass { + public: + ExampleClass(ClassToMock *dep_) : dep(dep_) {} + + int Example1(int a) { return dep->Mock1(a + 1) * 2; } + + int Example1a(int a) { return dep->Mock1(a - 1) * 5; } + + int ExampleNoMock() { return 33; } + + int Example2(int a) { return dep->Mock2(a + 2) * 3; } + + int Example1And2(int a) { + int b = dep->Mock1(a); + return dep->Mock2(a + 1) + b * 2; + } + + void Example1Void(int a) { (void)dep->Mock1(a + 11); } + + private: + ClassToMock *const dep; +}; + +//////////////////////////////////////////// +// Mocking assets + +class MockClass : public ClassToMock { + public: + MOCK_METHOD(int, Mock1, (int x), (override)); + MOCK_METHOD(int, Mock2, (int x), (override)); +}; + +////////////////////////////////////////////// +// The actual tests + +TEST(LaunchWithMockTest, Simple) { + StrictMock mock_object; + ExampleClass example(&mock_object); + + auto coro = COROUTINE() { + WATCH_CALL(); // Needs to be inside coro body so it executes before we + // start driving MUT calls + + auto d = LAUNCH(example.Example1(4)); + + auto e = NEXT_EVENT().IS_CALL(mock_object, Mock1(5)); + EXPECT_TRUE(e); + EXPECT_TRUE(e.From(d)); + EXPECT_FALSE(e.FromMain()); + e.RETURN(1000); + + auto e2 = NEXT_EVENT(); + EXPECT_TRUE(e2.IS_RESULT(d)); + EXPECT_EQ(e2(d), 2000); + }; +} + +TEST(LaunchWithMockTest, SimpleVoidReturn) { + StrictMock mock_object; + ExampleClass example(&mock_object); + + auto coro = COROUTINE() { + WATCH_CALL(); // Needs to be inside coro body so it executes before we + // start driving MUT calls + + auto d = LAUNCH(example.Example1Void(4)); + + auto e = NEXT_EVENT().IS_CALL(mock_object, Mock1); + EXPECT_TRUE(e.IS_CALL(mock_object, Mock1(15))); + EXPECT_TRUE(e.From(d)); + e.RETURN(1000); + + auto e2 = NEXT_EVENT(); + EXPECT_TRUE(e2.IS_RESULT(d)); + }; +} + +TEST(LaunchWithMockTest, Dual) { + StrictMock mock_object; + ExampleClass example(&mock_object); + + auto coro = COROUTINE() { + WATCH_CALL(); // Needs to be inside coro body so it executes before we + // start driving MUT calls + + auto d1 = LAUNCH(example.Example1(4)); + + auto e3 = NEXT_EVENT().IS_CALL(mock_object, Mock1(5)); + EXPECT_TRUE(e3); // from d + e3.ACCEPT(); // required to avoid deadlocking on GMock's mutex + EXPECT_TRUE(e3.From(d1)); + + auto d2 = LAUNCH(example.Example2(3)); + auto e2 = NEXT_EVENT().IS_CALL(mock_object, Mock2(5)); + EXPECT_TRUE(e2); + EXPECT_TRUE(e2.From(d2)); + EXPECT_FALSE(e2.From(d1)); + EXPECT_TRUE(e2.IS_CALL()); + e2.ACCEPT(); // required to avoid deadlocking on GMock's mutex + + e3.RETURN(1000); + auto e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_RESULT(d1)); + EXPECT_EQ(e(d1), 2000); + + e2.RETURN(2000); + + e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_RESULT(d2)); + EXPECT_EQ(e(d2), 6000); + }; +} + +TEST(LaunchWithMockTest, DualNestMock) { + StrictMock mock_object; + ExampleClass example(&mock_object); + + auto coro = COROUTINE() { + WATCH_CALL(); // Needs to be inside coro body so it executes before we + // start driving MUT calls + + auto d = LAUNCH(example.Example1(4)); + + auto e = NEXT_EVENT().IS_CALL(mock_object, Mock1(5)); + EXPECT_TRUE(e.From(d)); + e.ACCEPT(); // required to avoid deadlocking on GMock's mutex + + auto d2 = LAUNCH(example.Example1a(3)); + + auto e2 = NEXT_EVENT(); + EXPECT_TRUE(e2.IS_CALL().From(d2)); + e2.ACCEPT(); // required to avoid deadlocking on GMock's mutex + EXPECT_TRUE(e2); + e2.IS_CALL(mock_object, Mock1(2)).RETURN(2000); + + e2 = NEXT_EVENT(); + EXPECT_TRUE(e2.IS_RESULT(d2)); + EXPECT_EQ(e2(d2), 10000); + + e.RETURN(1000); + + auto e3 = NEXT_EVENT(); + EXPECT_TRUE(e3.IS_RESULT(d)); + EXPECT_EQ(e3(d), 2000); + }; +} + +TEST(LaunchWithMockTest, DualWaitFrom) { + StrictMock mock_object; + ExampleClass example(&mock_object); + + auto coro = COROUTINE() { + WATCH_CALL(); // Needs to be inside coro body so it executes before we + // start driving MUT calls + + auto d = LAUNCH(example.Example1(4)); + auto e4 = WAIT_FOR_CALL_FROM(d).IS_CALL(mock_object, Mock1(5)); + EXPECT_TRUE(e4); + EXPECT_TRUE(e4.From(d)); + + auto d2 = LAUNCH(example.Example1a(3)); + auto e2 = WAIT_FOR_CALL_FROM(mock_object, Mock1(2), d2); + EXPECT_TRUE(e2.From(d2)); + + auto d3 = LAUNCH(example.Example1a(9)); + auto e3 = WAIT_FOR_CALL_FROM(mock_object, d3).IS_CALL(mock_object, Mock1(8)); + EXPECT_TRUE(e3); + EXPECT_TRUE(e3.From(d3)); + + e4.RETURN(1000); + auto e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_RESULT(d)); + EXPECT_EQ(e(d), 2000); + + e2.RETURN(2000); + auto er2 = NEXT_EVENT(); + EXPECT_TRUE(er2.IS_RESULT(d2)); + EXPECT_EQ(er2(d2), 10000); + + e3.RETURN(1001); + auto er3 = NEXT_EVENT(); + EXPECT_TRUE(er3.IS_RESULT(d3)); + EXPECT_EQ(er3(d3), 5005); + }; +} + +TEST(LaunchWithMockTest, EffectOfUnseenMockBase) { + StrictMock mock_object; + ExampleClass example(&mock_object); + + auto coro = COROUTINE() { + WATCH_CALL(); + // This seems like a strange place to add an expectation but + // we need to add it after the WATCH_CALL() because we want it + // to have a higher priority, and yet it must be added before the + // coroutine runs, which is immediately after its declaration. + // See DelayStartExample for another approach. + EXPECT_CALL(mock_object, Mock1).WillRepeatedly(Return(99)); + + auto d = LAUNCH(example.Example1(4)); + + auto e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_RESULT(d)); + EXPECT_EQ(e(d), 198); + }; +} + +TEST(LaunchWithMockTest, EffectOfUnseenMock1) { + StrictMock mock_object; + ExampleClass example(&mock_object); + + auto coro = COROUTINE() { + WATCH_CALL(); + EXPECT_CALL(mock_object, Mock1).WillRepeatedly(Return(99)); + + auto d1 = LAUNCH(example.Example1(4)); + auto e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_RESULT(d1)); + EXPECT_EQ(e(d1), 198); + + auto d2 = LAUNCH(example.ExampleNoMock()); + e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_RESULT(d2)); + EXPECT_EQ(e(d2), 33); + }; +} + +TEST(LaunchWithMockTest, EffectOfUnseenMock2) { + StrictMock mock_object; + ExampleClass example(&mock_object); + + auto coro = COROUTINE() { + WATCH_CALL(); + EXPECT_CALL(mock_object, Mock1).WillRepeatedly(Return(99)); + + auto d1 = LAUNCH(example.Example1And2(4)); + + auto e3 = NEXT_EVENT().IS_CALL(mock_object, Mock2(5)); + EXPECT_TRUE(e3); + EXPECT_TRUE(e3.From(d1)); + e3.ACCEPT(); + + auto d2 = LAUNCH(example.ExampleNoMock()); + + auto e2 = NEXT_EVENT(); + EXPECT_TRUE(e2.IS_RESULT(d2)); + EXPECT_EQ(e2(d2), 33); + + e3.RETURN(1000); + + auto e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_RESULT(d1)); + EXPECT_EQ(e(d1), 1000 + 99 * 2); + }; +} + +TEST(LaunchWithMockTest, NextEvent1) { + StrictMock mock_object; + ExampleClass example(&mock_object); + + auto coro = COROUTINE() { + WATCH_CALL(); + + auto d1 = LAUNCH(example.ExampleNoMock()); // no mock call + + // d1/EM3() makes no mock call, so we expect it to return immediately, + // and so we expect to collect its return before dealing with d2/EM4() + auto e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_RESULT(d1)); + EXPECT_EQ(e(d1), 33); + + auto d2 = LAUNCH(example.Example2(6)); // ->Mockmethod2() + + auto e2 = NEXT_EVENT().IS_CALL(mock_object, Mock2(8)); + EXPECT_TRUE(e2); + EXPECT_TRUE(e2.From(d2)); + e2.RETURN(1000); + + e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_RESULT(d2)); + EXPECT_EQ(e(d2), 3000); + }; +} + +TEST(LaunchWithMockTest, NextEvent2) { + StrictMock mock_object; + ExampleClass example(&mock_object); + + auto coro = COROUTINE() { + WATCH_CALL(); + + auto d1 = LAUNCH(example.Example1(4)); // ->Mock1() + auto e3 = NEXT_EVENT().IS_CALL(mock_object, Mock1); + EXPECT_TRUE(e3); + EXPECT_TRUE(e3.From(d1)); + e3.ACCEPT(); + + auto d2 = LAUNCH(example.Example2(6)); // ->Mock2() + auto e2 = NEXT_EVENT().IS_CALL(mock_object, Mock2(8)); + EXPECT_TRUE(e2); + EXPECT_TRUE(e2.From(d2)); + e2.ACCEPT(); + + e3.RETURN(99); + auto e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_RESULT(d1)); + EXPECT_EQ(e(d1), 99 * 2); + + e2.RETURN(1000); + + e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_RESULT(d2)); + EXPECT_EQ(e(d2), 3000); + }; +} + +TEST(LaunchWithMockTest, DelayStartExample) { + StrictMock mock_object; + ExampleClass example(&mock_object); + + // This test serves as an example of an alternate way of adding + // expectations/watches at a higher priority than the coroutine. + // Declare a MockFunction to wait for. + MockFunction delay_start; + + auto coro = COROUTINE() { + // Wait for it and immediately return + WAIT_FOR_CALL(delay_start).RETURN(); + + auto d = LAUNCH(example.Example1(4)); + + auto e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_RESULT(d)); + EXPECT_EQ(e(d), 198); + }; + + coro.WATCH_CALL(); + EXPECT_CALL(mock_object, Mock1).WillRepeatedly(Return(99)); + + // Only call it once all the expectations/watches are added. + delay_start.Call(); +} diff --git a/coroutines/test/cotest-launch-multi-coro.cc b/coroutines/test/cotest-launch-multi-coro.cc new file mode 100644 index 000000000..012f3b5c1 --- /dev/null +++ b/coroutines/test/cotest-launch-multi-coro.cc @@ -0,0 +1,351 @@ +#include + +#include "cotest/cotest.h" +#include "gtest/gtest-spi.h" + +using namespace std; +using namespace testing; +using ::testing::StrictMock; + +//////////////////////////////////////////// +// Code under test + +class ClassToMock { + public: + virtual ~ClassToMock() {} + virtual int MockA(int x) = 0; + virtual int MockB(int x) = 0; + virtual int MockC(int x) = 0; + virtual int MockD(int x) = 0; +}; + +class ExampleClass { + public: + ExampleClass(ClassToMock *dep_) : dep(dep_) {} + + int A(int x) { return dep->MockA(x); } + + int B(int x) { return dep->MockB(x); } + + int C(int x) { return dep->MockC(x); } + + int D(int x) { return dep->MockD(x); } + + private: + ClassToMock *const dep; +}; + +//////////////////////////////////////////// +// Mocking assets + +class MockClass : public ClassToMock { + public: + MOCK_METHOD(int, MockA, (int x), (override)); + MOCK_METHOD(int, MockB, (int x), (override)); + MOCK_METHOD(int, MockC, (int x), (override)); + MOCK_METHOD(int, MockD, (int x), (override)); +}; + +////////////////////////////////////////////// +// The actual tests + +/* + * In this example, we demonstate interleaved coroutines that share + * mock calls made by launch sessions. + */ + +TEST(LaunchMultiCoroTest, DualInSeqDrop) { + StrictMock mock_object; + ExampleClass example(&mock_object); + + // Notice that in these tests, the observing coroutine (that + // waits for mock calls) and the instigator (that launches calls) + // comes last. This ensures that the observer is not destructed + // before the instigator completes. + auto observer_coro = COROUTINE(Observer) { + WATCH_CALL(mock_object, MockA); // lower prio + + // We see this because instigator_coro drops it + auto e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_CALL(mock_object, MockA(1)).RETURN(9)); + + // We see this because higher prio + e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_CALL(mock_object, MockB(2))); + e.DROP(); + }; + + auto instigator_coro = COROUTINE(Instigator) { + WATCH_CALL(); + observer_coro.WATCH_CALL(mock_object, MockB); // higher prio + + auto da = LAUNCH(example.A(1)); + + auto ea = NEXT_EVENT(); + EXPECT_TRUE(ea.IS_CALL(mock_object, MockA(1)).From(da)); + ea.DROP(); // required to avoid deadlocking on GMock's mutex + + auto e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_RESULT(da)); + EXPECT_EQ(e(da), 9); + + auto db = LAUNCH(example.B(2)); + + auto eb = NEXT_EVENT(); + EXPECT_TRUE(eb.IS_CALL(mock_object, MockB(2)).From(db).RETURN(2000)); + + e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_RESULT(db)); + EXPECT_EQ(e(db), 2000); + }; +} + +/* + * Here we attempt to reverse the two mock calls using delayed return + * but we can only delay RETURN(), not DROP(), so we can't demonstrate + * a sheared call sequence as seen by watcher. + */ +TEST(LaunchMultiCoroTest, DualInSeqAccept) { + StrictMock mock_object; + ExampleClass example(&mock_object); + + auto observer_coro = COROUTINE(Observer) { + // We don't get this because instigator_coro cannot decide to drop + // MockA based on argument passed to MockB + // auto ea = NEXT_EVENT(); + // EXPECT_TRUE( ea.IS_CALL(mock_object, MockA(1)) ); + // ea.RETURN(9); + + // We see this because higher prio + auto e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_CALL(mock_object, MockB(2))); + e.DROP(); + }; + + auto instigator_coro = COROUTINE(Instigator) { + observer_coro.WATCH_CALL(mock_object, MockA); // lower prio + WATCH_CALL(); + observer_coro.WATCH_CALL(mock_object, MockB); // higher prio + + auto da = LAUNCH(example.A(1)); + + auto ea = NEXT_EVENT().IS_CALL(mock_object, MockA(1)); + EXPECT_TRUE(ea.From(da)); + ea.ACCEPT(); // required to avoid deadlocking on GMock's mutex + + auto db = LAUNCH(example.B(2)); + + auto eb = NEXT_EVENT().IS_CALL(mock_object, MockB(2)); + EXPECT_TRUE(eb.From(db)); + int x = eb.GetArg<0>(); + + eb.RETURN(2000); + auto e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_RESULT(db)); + EXPECT_EQ(e(db), 2000); + + ea.RETURN(x); + e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_RESULT(da)); + EXPECT_EQ(e(da), 2); + }; +} + +class ExampleClassAlternative { + public: + ExampleClassAlternative(ClassToMock *dep_) : dep(dep_) {} + + int A(int x) { return dep->MockA(x); } + + int B(int x) { + int y = dep->MockB(x); + y += dep->MockC(x); + return y; + } + + int C(int x) { return dep->MockC(x); } + + int D(int x) { return dep->MockD(x); } + + private: + ClassToMock *const dep; +}; + +TEST(LaunchMultiCoroTest, FollowOn) { + StrictMock mock_object; + ExampleClassAlternative example(&mock_object); + + auto observer_coro = COROUTINE(Observer) { + WATCH_CALL(mock_object, MockB); // lower prio + // We see this because higher prio + auto e = NEXT_EVENT(); + auto e2 = e.IS_CALL(mock_object, MockB(2)); + EXPECT_TRUE(e2.RETURN(1000)); + }; + + auto instigator_coro = COROUTINE(Instigator) { + WATCH_CALL(); + + auto db = LAUNCH(example.B(2)); + + auto eb = NEXT_EVENT(); + EXPECT_TRUE(eb.IS_CALL(mock_object, MockB(2)).From(db)); + eb.DROP(); + + eb = NEXT_EVENT(); + EXPECT_TRUE(eb.IS_CALL(mock_object, MockC(2)).From(db).RETURN(20)); + + auto e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_RESULT(db)); + EXPECT_EQ(e(db), 1020); + }; +} + +TEST(LaunchMultiCoroTest, Arbitrary) { + StrictMock mock_object; + ExampleClass example(&mock_object); + + StrictMock mock_object2; + ExampleClass example2(&mock_object2); + + ::testing::LaunchHandle *pdb = nullptr; + + auto observer_coro = COROUTINE(Observer) { + WATCH_CALL(mock_object, MockC); + WATCH_CALL(mock_object2, MockA); + WATCH_CALL(mock_object, MockB); + + auto ea = WAIT_FOR_CALL(mock_object2, MockA); // due to Instigator's da + ea.RETURN(567); + + auto eb = NEXT_EVENT().IS_CALL(mock_object, MockB(2)); // due to Instigator's db + EXPECT_TRUE(eb.From(*pdb)); + eb.ACCEPT(); + + auto dc = LAUNCH(example.C(7)); + auto e = NEXT_EVENT(); + auto cc = e.IS_CALL(mock_object, MockC); + e.ACCEPT(); + + eb.RETURN(123); + // No need for NEXT_EVENT since we're returning + + cc.RETURN(99); + + e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_RESULT(dc)); + }; + + auto instigator_coro = COROUTINE(Instigator) { + auto da = LAUNCH(example2.A(0)); // Observer will handle MockA() + + auto e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_RESULT(da)); + EXPECT_EQ(e(da), 567); + + auto db = LAUNCH(example.B(2)); // Observer will handle MockB() + pdb = &db; + + e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_RESULT(db)); + EXPECT_EQ(e(db), 123); + }; +} + +TEST(LaunchMultiCoroTest, ArbitraryEx) { + StrictMock mock_object; + ExampleClassAlternative example(&mock_object); + + StrictMock mock_object2; + ExampleClass example2(&mock_object2); + + ::testing::LaunchHandle *pdb = nullptr; + + auto observer_coro = COROUTINE(Observer) { + auto ea = WAIT_FOR_CALL(mock_object2, MockA); // due to Instigator's da + ea.RETURN(567); + + auto eb = NEXT_EVENT().IS_CALL(mock_object, MockB(2)); // due to Instigator's db + EXPECT_TRUE(eb.From(*pdb)); + eb.ACCEPT(); + + auto dc = LAUNCH(example.C(7)); + auto e = NEXT_EVENT(); + auto cc = e.IS_CALL(mock_object, MockC).From(dc); + e.ACCEPT(); + + eb.RETURN(123); + auto e3 = NEXT_EVENT(); + EXPECT_TRUE(e3.From(*pdb).ACCEPT()); + + cc.RETURN(99); + e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_RESULT(dc)); + + e3.IS_CALL(mock_object, MockC).RETURN(23); + }; + + observer_coro.WATCH_CALL(mock_object, MockC); + observer_coro.WATCH_CALL(mock_object2, MockA); + observer_coro.WATCH_CALL(mock_object, MockB); + + auto instigator_coro = COROUTINE(Instigator) { + auto da = LAUNCH(example2.A(0)); // Observer will handle MockA() + + auto e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_RESULT(da)); + EXPECT_EQ(e(da), 567); + + auto db = LAUNCH(example.B(2)); // Observer will handle MockB() + pdb = &db; + + e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_RESULT(db)); + EXPECT_EQ(e(db), 146); + }; +} + +TEST(LaunchMultiCoroTest, ObserverQueue) { + StrictMock mock_object; + ExampleClass example(&mock_object); + + EXPECT_CALL(mock_object, MockB).WillOnce(Return(102)); + + auto observer_coro = COROUTINE(Observer) { + WATCH_CALL(mock_object); + + WAIT_FOR_CALL(mock_object, MockA(0)).RETURN(10); + auto ea1 = WAIT_FOR_CALL(mock_object, MockA(1)); + auto dc = LAUNCH(example.C(3)); + auto ec = WAIT_FOR_CALL(mock_object, MockC(3)); + + ea1.RETURN(11); + + // Note: this is a rare case in which we require more than one + // consecutive mock return. The above return permits MockA() to + // return, launch session da2 to return and coro Instigator to exit. + // Since there are no launch sessions in main that might cause mock + // calls, we reach the RAII destructors for the test coroutines. + // These provide an extra iteration to the coro if it has not yet + // exited. A NEXT_EVENT() here would now run, but would not find + // any events waiting. It would request resumption of main hoping + // for more mock calls but destructors would complete leaving + // Observer unsatisfied (didn't exit or SATISFY()) and dc and ec + // uncompleted. + // auto e = NEXT_EVENT(); + + ec.RETURN(13); + + EXPECT_EQ(WAIT_FOR_RESULT()(dc), 13); + }; + + auto instigator_coro = COROUTINE(Instigator) { + auto da = LAUNCH(example.A(0)); + EXPECT_EQ(NEXT_EVENT().IS_RESULT()(da), 10); + auto db = LAUNCH(example.B(2)); + EXPECT_EQ(NEXT_EVENT().IS_RESULT()(db), 102); + auto da2 = LAUNCH(example.A(1)); + + EXPECT_EQ(NEXT_EVENT().IS_RESULT()(da2), 11); + }; +} diff --git a/coroutines/test/cotest-launch.cc b/coroutines/test/cotest-launch.cc new file mode 100644 index 000000000..9f6b1a674 --- /dev/null +++ b/coroutines/test/cotest-launch.cc @@ -0,0 +1,305 @@ +#include + +#include "cotest/cotest.h" +#include "gtest/gtest-spi.h" + +using namespace std; +using namespace testing; +using ::testing::StrictMock; + +//////////////////////////////////////////// +// Code under test + +class ClassToMock { + public: + virtual ~ClassToMock() {} + virtual int Mock1() const = 0; +}; + +class ExampleClass { + public: + int Example1() { return 6; } + int Example2(int i) { return i * 3; } + void Example3() {} + int m_i = 99; + int& Example4() { return m_i; } + int operator++() { return 7; } + int Example6(int i, int j) { return i * 3 - j; } +}; + +//////////////////////////////////////////// +// Mocking assets + +class MockClass : public ClassToMock { + public: + MOCK_METHOD(int, Mock1, (), (const, override)); +}; + +////////////////////////////////////////////// +// The actual tests + +TEST(LaunchTest, Simple) { + StrictMock mock_object; + ExampleClass example; + + auto coro = COROUTINE() { + auto d = LAUNCH(example.Example2(4)); + auto e = NEXT_EVENT(); + EXPECT_FALSE(e.IS_CALL()); + EXPECT_FALSE(e.IS_CALL(mock_object)); + EXPECT_FALSE(e.IS_CALL(mock_object, Mock1)); + EXPECT_TRUE(e.IS_RESULT()); + EXPECT_TRUE(e.IS_RESULT(d)); + EXPECT_EQ(e(d), 12); + }; +} + +TEST(LaunchTest, VoidReturn1) { + StrictMock mock_object; + ExampleClass example; + + auto coro = COROUTINE() { + auto d = LAUNCH(example.Example3()); + auto e = NEXT_EVENT(); + EXPECT_FALSE(e.IS_CALL()); + EXPECT_FALSE(e.IS_CALL(mock_object)); + EXPECT_FALSE(e.IS_CALL(mock_object, Mock1)); + EXPECT_TRUE(e.IS_RESULT()); + EXPECT_TRUE(e.IS_RESULT(d)); + }; +} + +TEST(LaunchTest, VoidReturn2) { + StrictMock mock_object; + ExampleClass example; + + auto coro = COROUTINE() { + auto d = LAUNCH(example.Example3()); + auto e = NEXT_EVENT(); + EXPECT_FALSE(e.IS_CALL()); + EXPECT_FALSE(e.IS_CALL(mock_object)); + EXPECT_FALSE(e.IS_CALL(mock_object, Mock1)); + EXPECT_TRUE(e.IS_RESULT()); + EXPECT_TRUE(e.IS_RESULT(d)); + e(d); // this is OK but evaluates to void + }; +} + +TEST(LaunchTest, RefReturn) { + StrictMock mock_object; + ExampleClass example; + + auto coro = COROUTINE() { + auto d = LAUNCH(example.Example4()); + auto e = NEXT_EVENT(); + EXPECT_FALSE(e.IS_CALL()); + EXPECT_FALSE(e.IS_CALL(mock_object)); + EXPECT_FALSE(e.IS_CALL(mock_object, Mock1)); + EXPECT_TRUE(e.IS_RESULT()); + EXPECT_TRUE(e.IS_RESULT(d)); + EXPECT_EQ(e(d), 99); + }; +} + +TEST(LaunchTest, NestedEasy) { + StrictMock mock_object; + ExampleClass example; + + auto coro = COROUTINE() { + auto d = LAUNCH(example.Example1()); + auto e = NEXT_EVENT(); + EXPECT_FALSE(e.IS_CALL()); + EXPECT_FALSE(e.IS_CALL(mock_object)); + EXPECT_FALSE(e.IS_CALL(mock_object, Mock1)); + EXPECT_TRUE(e.IS_RESULT()); + EXPECT_TRUE(e.IS_RESULT(d)); + EXPECT_EQ(e(d), 6); + + // Easy to support nesting between return value extraction and cleanup + // (but useful: this is when by-reference return objects are valid). + auto d2 = LAUNCH(example.Example2(5)); + auto e2 = NEXT_EVENT(); + EXPECT_FALSE(e2.IS_CALL()); + EXPECT_FALSE(e2.IS_CALL(mock_object)); + EXPECT_FALSE(e2.IS_CALL(mock_object, Mock1)); + EXPECT_TRUE(e2.IS_RESULT()); + EXPECT_FALSE(e2.IS_RESULT(d)); + EXPECT_TRUE(e2.IS_RESULT(d2)); + EXPECT_EQ(e2(d2), 15); + }; +} + +TEST(LaunchTest, NestedHard) { + StrictMock mock_object; + ExampleClass example; + + auto coro = COROUTINE() { + auto d = LAUNCH(example.Example1()); + auto e = NEXT_EVENT(); + + auto d2 = LAUNCH(example.Example2(5)); + + EXPECT_FALSE(e.IS_CALL()); + EXPECT_FALSE(e.IS_CALL(mock_object)); + EXPECT_FALSE(e.IS_CALL(mock_object, Mock1)); + EXPECT_TRUE(e.IS_RESULT()); + EXPECT_TRUE(e.IS_RESULT(d)); + EXPECT_EQ(e(d), 6); + + auto e2 = NEXT_EVENT(); + EXPECT_FALSE(e2.IS_CALL()); + EXPECT_FALSE(e2.IS_CALL(mock_object)); + EXPECT_FALSE(e2.IS_CALL(mock_object, Mock1)); + EXPECT_TRUE(e2.IS_RESULT()); + EXPECT_FALSE(e2.IS_RESULT(d)); + EXPECT_TRUE(e2.IS_RESULT(d2)); + EXPECT_EQ(e2(d2), 15); + }; +} + +TEST(LaunchTest, Operator) { + StrictMock mock_object; + ExampleClass example; + + auto coro = COROUTINE() { + auto d = LAUNCH(++example); + auto e = NEXT_EVENT(); + EXPECT_FALSE(e.IS_CALL()); + EXPECT_FALSE(e.IS_CALL(mock_object)); + EXPECT_FALSE(e.IS_CALL(mock_object, Mock1)); + EXPECT_TRUE(e.IS_RESULT()); + EXPECT_TRUE(e.IS_RESULT(d)); + EXPECT_EQ(e(d), 7); + }; +} + +TEST(LaunchTest, Exit1) { + GTEST_SKIP() << "Not allowed: exiting with a launch session that may not " + "have completed"; + + StrictMock mock_object; + ExampleClass example; + + auto coro = COROUTINE() { auto d = LAUNCH(example.Example2(4)); }; +} + +TEST(LaunchTest, Exit2) { + GTEST_SKIP() << "Not allowed: exiting with an incompleted event session"; + + StrictMock mock_object; + ExampleClass example; + + auto coro = COROUTINE() { + auto d = LAUNCH(example.Example2(4)); + auto e = NEXT_EVENT(); + }; +} + +TEST(LaunchTest, ReturnOverlapCase1) { + StrictMock mock_object; + ExampleClass example; + + auto coro = COROUTINE() { + auto d = LAUNCH(example.Example1()); + auto e = NEXT_EVENT(); + + auto d2 = LAUNCH(example.Example2(5)); + auto e2 = NEXT_EVENT(); + + EXPECT_TRUE(e.IS_RESULT(d)); + EXPECT_TRUE(e2.IS_RESULT(d2)); + EXPECT_EQ(e(d), 6); + EXPECT_EQ(e2(d2), 15); + }; +} + +TEST(LaunchTest, ReturnOverlapCase2) { + StrictMock mock_object; + ExampleClass example; + + auto coro = COROUTINE() { + auto d = LAUNCH(example.Example1()); + auto e = NEXT_EVENT(); + EXPECT_TRUE(e.IS_RESULT(d)); + + auto d2 = LAUNCH(example.Example2(5)); + auto e2 = NEXT_EVENT(); + EXPECT_TRUE(e2.IS_RESULT(d2)); + + EXPECT_EQ(e(d), 6); + EXPECT_EQ(e2(d2), 15); + }; +} + +TEST(LaunchTest, SimpleIR) { + StrictMock mock_object; + ExampleClass example; + + auto coro = COROUTINE() { + auto d = LAUNCH(example.Example2(4)); + auto e = NEXT_EVENT(); + EXPECT_FALSE(e.IS_CALL()); + EXPECT_FALSE(e.IS_CALL(mock_object)); + EXPECT_FALSE(e.IS_CALL(mock_object, Mock1)); + EXPECT_TRUE(e.IS_RESULT()); + // Demonstrate use of the return of IS_RESULT + EXPECT_EQ(e.IS_RESULT()(d), 12); + }; +} + +TEST(LaunchTest, FromMain) { + StrictMock mock_object; + + auto coro = COROUTINE(MethodName) { + auto cs = WAIT_FOR_CALL(mock_object, Mock1); + EXPECT_TRUE(cs.FromMain()); + cs.RETURN(10); + }; + + coro.WATCH_CALL(mock_object, Mock1); + + EXPECT_EQ(mock_object.Mock1(), 10); +} + +TEST(LaunchTest, CommaInExpr) { + StrictMock mock_object; + ExampleClass example; + + auto coro = COROUTINE() { + auto d = LAUNCH(example.Example6(4, 44)); + auto e = NEXT_EVENT(); + EXPECT_FALSE(e.IS_CALL()); + EXPECT_FALSE(e.IS_CALL(mock_object)); + EXPECT_FALSE(e.IS_CALL(mock_object, Mock1)); + EXPECT_TRUE(e.IS_RESULT()); + EXPECT_TRUE(e.IS_RESULT(d)); + EXPECT_EQ(e(d), 12 - 44); + }; +} + +TEST(LaunchTest, ShortForm) { + ExampleClass example; + + auto coro = COROUTINE() { + auto d = LAUNCH(example.Example6(4, 44)); + auto e = WAIT_FOR_RESULT(); + EXPECT_EQ(e(d), 12 - 44); + }; +} + +TEST(LaunchTest, ShorterForm) { + ExampleClass example; + + auto coro = COROUTINE() { EXPECT_EQ(WAIT_FOR_RESULT()(LAUNCH(example.Example6(4, 44))), 12 - 44); }; +} + +TEST(LaunchTest, ShortForm2) { + ExampleClass example; + + auto coro = COROUTINE() { + auto d = LAUNCH(example.Example6(4, 44)); + EXPECT_EQ(NEXT_EVENT()(d), 12 - 44); + }; +} + +COTEST(LaunchTest, VeryShortForm) { EXPECT_EQ(NEXT_EVENT()(LAUNCH(ExampleClass().Example6(4, 44))), 12 - 44); } diff --git a/coroutines/test/cotest-mockfunction.cc b/coroutines/test/cotest-mockfunction.cc new file mode 100644 index 000000000..04a69567e --- /dev/null +++ b/coroutines/test/cotest-mockfunction.cc @@ -0,0 +1,64 @@ +#include + +#include "cotest/cotest.h" +#include "gtest/gtest-spi.h" + +using namespace std; +using ::testing::MockFunction; + +////////////////////////////////////////////// +// The actual tests + +TEST(MockFunctionTest, RunsCallbackWithBarArgument) { + // 1. Create a mock object. + MockFunction mock_function; + + // 2. Set expectations on Call() method. + auto coro = COROUTINE() { + auto e = NEXT_EVENT(); + auto e2 = e.IS_CALL(mock_function, Call("bar")); + e2.RETURN(1); + WAIT_FOR_CALL(mock_function, Call("foo")).RETURN(2); + WAIT_FOR_CALL(mock_function, Call("Scooby")).RETURN(3); + WAIT_FOR_CALL(mock_function, Call("Doo")).RETURN(4); + }; + coro.WATCH_CALL(mock_function); + + // 3. Exercise code that uses std::function. + EXPECT_EQ(mock_function.Call("bar"), 1); + EXPECT_EQ(mock_function.AsStdFunction()("foo"), 2); + std::function mf2 = mock_function.AsStdFunction(); + EXPECT_EQ(mf2("Scooby"), 3); + EXPECT_EQ(mf2("Doo"), 4); +} + +TEST(MockFunctionTest, MockFunc) { + MockFunction mock_function; + + auto coro = COROUTINE() { + WATCH_CALL(mock_function); + auto d = LAUNCH(mock_function.Call("testing")); + WAIT_FOR_CALL(mock_function, Call("testing")).RETURN(4); + auto e = WAIT_FOR_RESULT(); + EXPECT_EQ(e(d), 4); + }; +} + +TEST(MockFunctionTest, Minimal) { + // Demonstrates use of GMock's MockFunction as a "semaphore" between + // launch coroutine and test coroutine. + MockFunction mock_function; + + auto coro = COROUTINE() { + WATCH_CALL(mock_function); + auto l = LAUNCH(mock_function.Call()); // Keep launch session in scope + WAIT_FOR_CALL().RETURN(); // return type is void so signature not required + WAIT_FOR_RESULT(); + }; +} + +COTEST(MockFunctionTest, HyperMinimal) { + WATCH_CALL(); + LAUNCH(MockFunction().Call()), // Temporary lasts to the semicolon. + WAIT_FOR_CALL().RETURN(), WAIT_FOR_RESULT(); +} diff --git a/coroutines/test/cotest-mutex.cc b/coroutines/test/cotest-mutex.cc new file mode 100644 index 000000000..7d7d6c9e5 --- /dev/null +++ b/coroutines/test/cotest-mutex.cc @@ -0,0 +1,151 @@ +#include + +#include "cotest/cotest.h" +#include "gtest/gtest-spi.h" + +using namespace std; +using namespace testing; +using ::testing::StrictMock; + +//////////////////////////////////////////// +// Code under test + +class MutexInterface { + public: + virtual ~MutexInterface() {} + virtual void lock() = 0; + virtual void unlock() = 0; +}; + +class ExampleClass { + public: + ExampleClass(MutexInterface *mutex_) : mutex(mutex_) {} + + /* The problem with this class: + * We know that Example1() is always called before Example2(), so + * we only need to test with that scenario. The implementation anticipates + * the "medium" difficulty case, in which the methods overlap and + * Example2() has started to run and then been blocked on the + * mutex by Example1(), by placing the var_x increment + * in Example2() at the end, apparently forcing the correct + * sequence of events. But this is wrong, and the "hard" test case + * discovers the problem. + */ + int Example1(int a) { + var_x += a; // unsafe: left outside of mutex + mutex->lock(); + var_y += a; + mutex->unlock(); + return var_x - var_y; + } + + int Example2(int a) { + // Note: if the var_x += a; is moved to here, the medium case fails. + mutex->lock(); + var_y += a; + mutex->unlock(); + var_x += a; // unsafe: left outside of mutex + return var_x - var_y; + } + + private: + int var_x = 0; + int var_y = 0; + MutexInterface *const mutex; +}; + +//////////////////////////////////////////// +// Mocking assets + +class MockMutex : public MutexInterface { + public: + MOCK_METHOD(void, lock, (), (override)); + MOCK_METHOD(void, unlock, (), (override)); +}; + +////////////////////////////////////////////// +// The actual tests + +COTEST(MutexScenarioTest, Simple) { + StrictMock mock_mutex; + ExampleClass example(&mock_mutex); + WATCH_CALL(); + + auto d = LAUNCH(example.Example1(22)); + WAIT_FOR_CALL_FROM(mock_mutex, lock, d).RETURN(); + WAIT_FOR_CALL_FROM(mock_mutex, unlock, d).RETURN(); + EXPECT_EQ(WAIT_FOR_RESULT()(d), 0); +} + +// The difficulty levels of the tests can be understood as increasing +// levels of eagerness of the imaginary thread that runs Example2(): +// - Easy: Doesn't even schedule until Example1() has finished +// - Medium: Preempts Example1() and then blocks on the mutex +// - Hard: Preempts Example1() and causes Example1() to block on the mutex + +COTEST(MutexScenarioTest, Easy) { + StrictMock mock_mutex; + ExampleClass example(&mock_mutex); + WATCH_CALL(); + + // Easy case, there is no conflict, Example1() returns before Example2() + // starts + + auto l1 = LAUNCH(example.Example1(11)); + WAIT_FOR_CALL_FROM(mock_mutex, lock, l1).RETURN(); + WAIT_FOR_CALL_FROM(mock_mutex, unlock, l1).RETURN(); + EXPECT_EQ(WAIT_FOR_RESULT()(l1), 0); + + // Example1() has finished, run Example2() + auto l2 = LAUNCH(example.Example1(22)); + WAIT_FOR_CALL_FROM(mock_mutex, lock, l2).RETURN(); + WAIT_FOR_CALL_FROM(mock_mutex, unlock, l2).RETURN(); + EXPECT_EQ(WAIT_FOR_RESULT()(l2), 0); +} + +COTEST(MutexScenarioTest, Medium) { // NOTE: MediumFixedSeq2 is better example + StrictMock mock_mutex; + ExampleClass example(&mock_mutex); + WATCH_CALL(); + + // Medium case, Example1() and Example2() overlap, but the lock/unlock + // sequences don't + + auto l1 = LAUNCH(example.Example1(11)); + auto l1_lock_call = WAIT_FOR_CALL_FROM(mock_mutex, lock, l1); + auto l2 = LAUNCH(example.Example2(22)); + auto l2_lock_call = WAIT_FOR_CALL_FROM(mock_mutex, lock, l2); + + l1_lock_call.RETURN(); // Example1 gets the lock + WAIT_FOR_CALL_FROM(mock_mutex, unlock, l1).RETURN(); + EXPECT_EQ(WAIT_FOR_RESULT()(l1), 0); + + // Example1() has unlocked while Example2() is still awaiting the mutex, which + // we now unblock + l2_lock_call.RETURN(); + WAIT_FOR_CALL_FROM(mock_mutex, unlock, l2).RETURN(); + EXPECT_EQ(WAIT_FOR_RESULT()(l2), 0); +} + +COTEST(MutexScenarioTest, Hard) { + StrictMock mock_mutex; + ExampleClass example(&mock_mutex); + WATCH_CALL(); + + // Hard case, Example1() starts first but Example2 gets lock first + auto l1 = LAUNCH(example.Example1(11)); + auto l1_lock_call = WAIT_FOR_CALL_FROM(mock_mutex, lock, l1); + auto l2 = LAUNCH(example.Example2(22)); + auto l2_lock_call = WAIT_FOR_CALL_FROM(mock_mutex, lock, l2); + + // Permit Example2 to: take the lock, unlock and return + l2_lock_call.RETURN(); // Example2 gets the lock + WAIT_FOR_CALL_FROM(mock_mutex, unlock, l2).RETURN(); + EXPECT_EQ(WAIT_FOR_RESULT()(l2), 11); // Would be 0 if not for the bug + + // Example2() has finished while Example1() is still awaiting the mutex, which + // we now unblock + l1_lock_call.RETURN(); + WAIT_FOR_CALL_FROM(mock_mutex, unlock, l1).RETURN(); + EXPECT_EQ(WAIT_FOR_RESULT()(l1), 0); +} diff --git a/coroutines/test/cotest-serverised.cc b/coroutines/test/cotest-serverised.cc new file mode 100644 index 000000000..f5dbfd68d --- /dev/null +++ b/coroutines/test/cotest-serverised.cc @@ -0,0 +1,170 @@ +#include + +#include "cotest/cotest.h" +#include "gtest/gtest-spi.h" + +using namespace std; +using namespace testing; +using ::testing::StrictMock; + +//////////////////////////////////////////// +// Code under test + +class ClassToMock { + public: + virtual ~ClassToMock() {} + virtual void Mock1(int i) const = 0; + // rule: call to this must be followed by call to MockExtra() + virtual void Mock2(int i, int j) const = 0; + virtual void Mock3(int i) const = 0; + virtual void MockExtra() const = 0; +}; + +class ExampleClass { + public: + ExampleClass(ClassToMock *dep_) : dep(dep_) {} + int Example1() { + // In order + dep->Mock1(1); + dep->Mock2(2, 10); + dep->MockExtra(); + dep->Mock3(3); + + // More calls, in a random order but obeying rule + dep->Mock3(3); + dep->Mock1(1); + dep->Mock2(2, 10); + dep->MockExtra(); + dep->Mock1(1); + dep->Mock3(3); + dep->Mock2(2, 10); + dep->MockExtra(); + dep->Mock2(2, 10); + dep->MockExtra(); + + return 100; + } + int Example2() { + // In order + dep->Mock1(1); + dep->Mock1(2); + dep->MockExtra(); + dep->Mock1(3); + + // More calls, in a random order but obeying rule + dep->Mock1(3); + dep->Mock1(1); + dep->Mock1(2); + dep->MockExtra(); + dep->Mock1(3); + dep->Mock1(2); + dep->MockExtra(); + dep->Mock1(2); + dep->MockExtra(); + dep->Mock1(3); + dep->Mock1(1); + dep->Mock1(1); + dep->Mock1(1); + dep->Mock1(3); + + return 101; + } + + private: + ClassToMock *const dep; +}; + +////////////////////////////////////////////// +// Mocking assets + +class MockClass : public ClassToMock { + public: + MOCK_METHOD(void, Mock1, (int i), (const, override)); + MOCK_METHOD(void, Mock2, (int i, int j), (const, override)); + MOCK_METHOD(void, Mock3, (int i), (const, override)); + MOCK_METHOD(void, MockExtra, (), (const, override)); +}; + +////////////////////////////////////////////// +// The actual tests + +COTEST(ServerisedTest, Example1) { + StrictMock mock_object; + ExampleClass example(&mock_object); + + EXPECT_CALL(mock_object, Mock3(3)).WillRepeatedly(Return()); + WATCH_CALL(); + + auto l = LAUNCH(example.Example1()); + + while (true) { + auto e = NEXT_EVENT(); + + if (auto e1 = e.IS_CALL(mock_object, Mock1)) { + // Mock1() is accepted, checked and returned + e1.ACCEPT(); + EXPECT_EQ(e1.GetArg<0>(), 1); + e1.RETURN(); + } else if (auto e2 = e.IS_CALL(mock_object, Mock2)) { + // Mock2() is accepted, checked and returned, but we require + // it to be followed by a call to MockExtra() + e2.ACCEPT(); + EXPECT_EQ(e2.GetArg<0>(), 2); + e2.RETURN(); + WAIT_FOR_CALL(mock_object, MockExtra).RETURN(); + } else if (auto e3 = e.IS_CALL(mock_object, Mock3)) { + // Mock3 is dropped and the expectation deals with it + e3.DROP(); + } else if (e.IS_RESULT()) { + EXPECT_EQ(e(l), 100); + // Avoid using return, for C++20 coro compatibility. + EXIT_COROUTINE(); + } else { + EXPECT_TRUE(!"unexpected event"); + } + } +} + +COTEST(ServerisedTest, Example2) { + StrictMock mock_object; + ExampleClass example(&mock_object); + + EXPECT_CALL(mock_object, Mock1(3)).WillRepeatedly(Return()); + WATCH_CALL(); + + auto l = LAUNCH(example.Example2()); + + while (true) { + auto e = NEXT_EVENT(); + + if (auto e1 = e.IS_CALL(mock_object, Mock1)) { + switch (e1.GetArg<0>()) { + case 1: + // When arg is 1, return + e1.RETURN(); + break; + case 2: + // When arg is 2, return and expect extra call + e1.RETURN(); + WAIT_FOR_CALL(mock_object, MockExtra).RETURN(); + break; + case 3: + // When arg is 3, drop and the expectation deals with it + e1.DROP(); + break; + default: + EXPECT_TRUE(!"unexpected event"); + break; + } + } else if (e.IS_RESULT()) { + EXPECT_EQ(e(l), 101); + // Avoid using return, for C++20 coro compatibility. + EXIT_COROUTINE(); + } else { + EXPECT_TRUE(!"unexpected event"); + } + } +} + +// TODO example in which we build our own version of WAIT_FOR_CALL that bakes in the +// special behaviours seen in these examples (requiring extra mock call, dropping) diff --git a/coroutines/test/cotest-types.cc b/coroutines/test/cotest-types.cc new file mode 100644 index 000000000..442bb4a3d --- /dev/null +++ b/coroutines/test/cotest-types.cc @@ -0,0 +1,365 @@ +#include + +#include "cotest/cotest.h" +#include "gtest/gtest-spi.h" + +using namespace std; +using namespace testing; +using coro_impl::PtrToString; +using ::testing::StrictMock; + +////////////////////////////////////////////// +// Mocking assets + +struct MyStruct { + int i; + char c; +}; + +class ClassToMock { + public: + virtual ~ClassToMock() {} + virtual int MockMethod1() const = 0; + virtual void MockMethod2() const = 0; + virtual int &MockMethod3() const = 0; + virtual int *MockMethod4() const = 0; + virtual unique_ptr MockMethod5() const = 0; + virtual shared_ptr MockMethod6() const = 0; + virtual MyStruct MockMethod7() const = 0; + + virtual void MockMethod11(int a) const = 0; + virtual void MockMethod12(int &a) const = 0; + virtual void MockMethod13(int *a) const = 0; + virtual void MockMethod14(unique_ptr a) const = 0; + virtual void MockMethod15(shared_ptr a) const = 0; + virtual void MockMethod16(MyStruct a) const = 0; + + virtual unique_ptr MockMethod20(unique_ptr a) const = 0; +}; + +class MockClass : public ClassToMock { + public: + MOCK_METHOD(int, MockMethod1, (), (const, override)); + MOCK_METHOD(void, MockMethod2, (), (const, override)); + MOCK_METHOD(int &, MockMethod3, (), (const, override)); + MOCK_METHOD(int *, MockMethod4, (), (const, override)); + MOCK_METHOD(unique_ptr, MockMethod5, (), (const, override)); + MOCK_METHOD(shared_ptr, MockMethod6, (), (const, override)); + MOCK_METHOD(MyStruct, MockMethod7, (), (const, override)); + + MOCK_METHOD(void, MockMethod11, (int a), (const, override)); + MOCK_METHOD(void, MockMethod12, (int &a), (const, override)); + MOCK_METHOD(void, MockMethod13, (int *a), (const, override)); + MOCK_METHOD(void, MockMethod14, (unique_ptr a), (const, override)); + MOCK_METHOD(void, MockMethod15, (shared_ptr a), (const, override)); + MOCK_METHOD(void, MockMethod16, (MyStruct a), (const, override)); + + MOCK_METHOD(unique_ptr, MockMethod20, (unique_ptr a), (const, override)); +}; + +////////////////////////////////////////////// +// The actual tests + +TEST(TypesTest, IntReturn) { + StrictMock mock_object; + + auto coro = COROUTINE() { WAIT_FOR_CALL(mock_object, MockMethod1).RETURN(10); }; + + coro.WATCH_CALL(mock_object, MockMethod1); + + EXPECT_EQ(mock_object.MockMethod1(), 10); +} + +TEST(TypesTest, IntReturnWild) { + StrictMock mock_object; + + auto coro = COROUTINE() { WAIT_FOR_CALL(mock_object, MockMethod1).RETURN(10); }; + + coro.WATCH_CALL(); + + EXPECT_EQ(mock_object.MockMethod1(), 10); +} + +TEST(TypesTest, VoidReturn) { + StrictMock mock_object; + + auto coro = COROUTINE() { WAIT_FOR_CALL().RETURN(); }; + + coro.WATCH_CALL(mock_object, MockMethod2); + + mock_object.MockMethod2(); +} + +TEST(TypesTest, VoidReturnWild) { + StrictMock mock_object; + + auto coro = COROUTINE() { WAIT_FOR_CALL().RETURN(); }; + + coro.WATCH_CALL(); + + mock_object.MockMethod2(); +} + +TEST(TypesTest, VoidReturnWildSignature) { + StrictMock mock_object; + + auto coro = COROUTINE() { WAIT_FOR_CALL(mock_object, MockMethod2).RETURN(); }; + + coro.WATCH_CALL(); + + mock_object.MockMethod2(); +} + +TEST(TypesTest, IntRefReturnSig) { + StrictMock mock_object; + int i = 10; + + auto coro = COROUTINE() { + std::clog << "address i=" << PtrToString(&i) << std::endl; + WAIT_FOR_CALL(mock_object, MockMethod3).RETURN(i); + }; + + coro.WATCH_CALL(mock_object, MockMethod3); + int &ri = mock_object.MockMethod3(); + std::clog << "address ri=" << PtrToString(&ri) << std::endl; + + EXPECT_EQ(ri, 10) << "returned ref has the right value"; + EXPECT_EQ(++ri, 11) << "returned ref increments successfully"; + EXPECT_EQ(i, 11) << "alias effect shows we didn't make a copy"; +} + +TEST(TypesTest, IntRefReturnGen) { + StrictMock mock_object; + int i = 10; + + auto coro = COROUTINE() { + std::clog << "address i=" << PtrToString(&i) << std::endl; + WAIT_FOR_CALL(mock_object, MockMethod3).RETURN(i); + }; + + coro.WATCH_CALL(); + int &ri = mock_object.MockMethod3(); + std::clog << "address ri=" << PtrToString(&ri) << std::endl; + + EXPECT_EQ(ri, 10) << "returned ref has the right value"; + EXPECT_EQ(++ri, 11) << "returned ref increments successfully"; + EXPECT_EQ(i, 11) << "alias effect shows we didn't make a copy"; +} + +TEST(TypesTest, IntPtrReturn) { + StrictMock mock_object; + int i = 10; + + auto coro = COROUTINE() { WAIT_FOR_CALL(mock_object, MockMethod4).RETURN(&i); }; + + coro.WATCH_CALL(mock_object, MockMethod4); + + EXPECT_EQ(mock_object.MockMethod4(), &i); +} + +TEST(TypesTest, IntPtrReturnWild) { + StrictMock mock_object; + int i = 10; + + auto coro = COROUTINE() { WAIT_FOR_CALL(mock_object, MockMethod4).RETURN(&i); }; + + coro.WATCH_CALL(); + + EXPECT_EQ(mock_object.MockMethod4(), &i); +} + +TEST(TypesTest, IntUniquePtrReturn) { + StrictMock mock_object; + + auto coro = COROUTINE() { WAIT_FOR_CALL(mock_object, MockMethod5).RETURN(make_unique(6)); }; + + coro.WATCH_CALL(mock_object, MockMethod5); + + EXPECT_EQ(*mock_object.MockMethod5(), 6); +} + +TEST(TypesTest, IntUniquePtrReturnWild) { + StrictMock mock_object; + + auto coro = COROUTINE() { WAIT_FOR_CALL(mock_object, MockMethod5).RETURN(make_unique(77)); }; + + coro.WATCH_CALL(); + + EXPECT_EQ(*mock_object.MockMethod5(), 77); +} + +TEST(TypesTest, IntUniquePtrReturnWildSignature) { + StrictMock mock_object; + + auto coro = COROUTINE() { WAIT_FOR_CALL(mock_object, MockMethod5).RETURN(make_unique(34)); }; + + coro.WATCH_CALL(); + + EXPECT_EQ(*mock_object.MockMethod5(), 34); +} + +TEST(TypesTest, IntSharedPtrReturn) { + StrictMock mock_object; + + auto coro = COROUTINE() { WAIT_FOR_CALL(mock_object, MockMethod6).RETURN(make_shared(63)); }; + + coro.WATCH_CALL(mock_object, MockMethod6); + + EXPECT_EQ(*mock_object.MockMethod6(), 63); +} + +TEST(TypesTest, IntSharedPtrReturnWild) { + StrictMock mock_object; + + auto coro = COROUTINE() { WAIT_FOR_CALL(mock_object, MockMethod6).RETURN(make_shared(69)); }; + + coro.WATCH_CALL(); + + EXPECT_EQ(*mock_object.MockMethod6(), 69); +} + +TEST(TypesTest, IntSharedPtrReturnWildSignature) { + StrictMock mock_object; + + auto coro = COROUTINE() { WAIT_FOR_CALL(mock_object, MockMethod6).RETURN(make_shared(3)); }; + + coro.WATCH_CALL(); + + EXPECT_EQ(*mock_object.MockMethod6(), 3); +} + +TEST(TypesTest, StructReturn) { + StrictMock mock_object; + + auto coro = COROUTINE() { WAIT_FOR_CALL(mock_object, MockMethod7).RETURN(MyStruct{34, 'b'}); }; + + coro.WATCH_CALL(mock_object, MockMethod7); + + auto s = mock_object.MockMethod7(); + EXPECT_EQ(s.i, 34); + EXPECT_EQ(s.c, 'b'); +} + +TEST(TypesTest, StructReturnWild) { + StrictMock mock_object; + + auto coro = COROUTINE() { WAIT_FOR_CALL(mock_object, MockMethod7).RETURN(MyStruct{14, 'L'}); }; + + coro.WATCH_CALL(); + + auto s = mock_object.MockMethod7(); + EXPECT_EQ(s.i, 14); + EXPECT_EQ(s.c, 'L'); +} + +TEST(TypesTest, IntArg) { + StrictMock mock_object; + + auto coro = COROUTINE() { + auto cg = WAIT_FOR_CALL(); + EXPECT_EQ(cg.IS_CALL(mock_object, MockMethod11).GetArg<0>(), 22); + cg.RETURN(); + }; + + coro.WATCH_CALL(); + + mock_object.MockMethod11(22); +} + +TEST(TypesTest, IntRefArg) { + StrictMock mock_object; + int i = 10; + + auto coro = COROUTINE() { + auto cg = WAIT_FOR_CALL(); + EXPECT_EQ(cg.IS_CALL(mock_object, MockMethod12).GetArg<0>(), 10); + cg.RETURN(); + }; + + coro.WATCH_CALL(); + + mock_object.MockMethod12(i); +} + +TEST(TypesTest, IntPtrArg) { + StrictMock mock_object; + int i = 10; + + auto coro = COROUTINE() { + auto cg = WAIT_FOR_CALL(); + EXPECT_EQ(cg.IS_CALL(mock_object, MockMethod13).GetArg<0>(), &i); + cg.RETURN(); + }; + + coro.WATCH_CALL(); + + mock_object.MockMethod13(&i); +} + +TEST(TypesTest, UniquePtrArg) { + StrictMock mock_object; + + auto coro = COROUTINE() { + auto cg = WAIT_FOR_CALL(); + EXPECT_EQ(*(cg.IS_CALL(mock_object, MockMethod14).GetArg<0>()), 9); + cg.RETURN(); + }; + + coro.WATCH_CALL(); + + mock_object.MockMethod14(make_unique(9)); +} + +TEST(TypesTest, SharedPtrArg) { + StrictMock mock_object; + + auto coro = COROUTINE() { + auto cg = WAIT_FOR_CALL(); + EXPECT_EQ(*(cg.IS_CALL(mock_object, MockMethod15).GetArg<0>()), 5); + cg.RETURN(); + }; + + coro.WATCH_CALL(); + + mock_object.MockMethod15(make_shared(5)); +} + +TEST(TypesTest, StructArg) { + StrictMock mock_object; + + auto coro = COROUTINE() { + auto cg = WAIT_FOR_CALL(); + EXPECT_EQ(cg.IS_CALL(mock_object, MockMethod16).GetArg<0>().i, 43); + EXPECT_EQ(cg.IS_CALL(mock_object, MockMethod16).GetArg<0>().c, '$'); + cg.RETURN(); + }; + + coro.WATCH_CALL(); + + mock_object.MockMethod16(MyStruct{43, '$'}); +} + +/* + * TODO I think it just needs a eg MoveArg<>(), obvs should be + * documented that it squishes the arg. */ +TEST(TypesTest, UniquePtrArgAndReturn) { + StrictMock mock_object; + + auto coro = COROUTINE() { + auto cg = WAIT_FOR_CALL(); + auto cs = cg.IS_CALL(mock_object, MockMethod20); + + // Does not build + // unique_ptr u = std::move( cs.GetArg<0>() ); + + // Builds and passes but is a bit of a cheat + unique_ptr u = std::make_unique(*(cs.GetArg<0>())); + + ++*u; + cs.RETURN(std::move(u)); + }; + + coro.WATCH_CALL(); + + EXPECT_EQ(*mock_object.MockMethod20(make_unique(9)), 10); +} diff --git a/coroutines/test/cotest-ui.cc b/coroutines/test/cotest-ui.cc new file mode 100644 index 000000000..effa43a63 --- /dev/null +++ b/coroutines/test/cotest-ui.cc @@ -0,0 +1,116 @@ +#include + +#include "cotest/cotest.h" +#include "gtest/gtest-spi.h" + +using namespace std; +using ::testing::StrictMock; + +////////////////////////////////////////////// +// Mocking assets + +class ClassToMock { + public: + virtual ~ClassToMock() {} + virtual int Mock1(int i) const = 0; + virtual int Mock2() const = 0; + virtual int Mock3(int x, const char *y, bool z) = 0; + virtual int Mock4(int i) const = 0; +}; +class MockClass : public ClassToMock { + public: + MOCK_METHOD(int, Mock1, (int i), (const, override)); + MOCK_METHOD(int, Mock2, (), (const, override)); + MOCK_METHOD(int, Mock3, (int x, const char *y, bool z), (override)); + MOCK_METHOD(int, Mock4, (int i), (const, override)); +}; + +using ::testing::Return; + +////////////////////////////////////////////// +// The actual tests + +TEST(UserInterfaceTest, MethodSE) { + StrictMock mock_object; + + auto coro = COROUTINE(MethodSE){{auto cs = WAIT_FOR_CALL(mock_object, Mock1); + EXPECT_EQ(cs.GetArg<0>(), 100); + cs.RETURN(10); +} + +{ + auto cs = WAIT_FOR_CALL(mock_object, Mock3); + EXPECT_EQ(cs.GetArg<0>(), 500); + EXPECT_EQ(cs.GetArg<1>(), "abcd"); + EXPECT_FALSE(cs.GetArg<2>()); + cs.RETURN(30); +} +} +; +// Note that all mock methods are being sent to the same coroutine: +// Cannot use ON_CALL for these. ON_CALL sets behaviour on "uninteresting" +// calls which are ones with no expectations. But WATCH_CALL actually sets +// an expectation. + +// absorb mock calls not accepted by coroutine +EXPECT_CALL(mock_object, Mock1).WillOnce(Return(-1)); +EXPECT_CALL(mock_object, Mock2).WillOnce(Return(-1)); +coro.WATCH_CALL(mock_object, Mock1); +coro.WATCH_CALL(mock_object, Mock2); +coro.WATCH_CALL(mock_object, Mock3); + +// This is the body of the test case +EXPECT_EQ(mock_object.Mock2(), -1); +EXPECT_EQ(mock_object.Mock1(100), 10); +EXPECT_EQ(mock_object.Mock1(300), -1); +EXPECT_EQ(mock_object.Mock3(500, "abcd", false), 30); +} + +TEST(UserInterfaceTest, MethodCheckNameSE) { + StrictMock mock_object; + auto coro = COROUTINE(MethodCheckNameSE){{auto cs = WAIT_FOR_CALL(mock_object, Mock4); + EXPECT_EQ(cs.GetArg<0>(), 100); + cs.RETURN(10); +} + +{ + auto cs = WAIT_FOR_CALL(mock_object, Mock3); + EXPECT_EQ(cs.GetArg<0>(), 500); + EXPECT_EQ(cs.GetArg<1>(), "abcd"); + EXPECT_FALSE(cs.GetArg<2>()); + cs.RETURN(30); +} +} +; + +// absorb mock calls not accepted by coroutine +EXPECT_CALL(mock_object, Mock1).WillOnce(Return(-1)); +EXPECT_CALL(mock_object, Mock2).WillOnce(Return(-1)); +coro.WATCH_CALL(mock_object, Mock1); +coro.WATCH_CALL(mock_object, Mock2); +coro.WATCH_CALL(mock_object, Mock4); +coro.WATCH_CALL(mock_object, Mock3); + +// This is the body of the test case +// WAIT_FOR_MOCK_CLASS_SE(MockClass, Mock4); in coro requires Mock4 but MUT +// still calls Mock1 which has same signature. Mock1 should be rejected causing +// MUT to return false. +EXPECT_EQ(mock_object.Mock2(), -1); // Passed by first wait due signature +EXPECT_EQ(mock_object.Mock1(200), -1); // Passed due name (test expects Mock4) +EXPECT_EQ(mock_object.Mock4(100), 10); // Accepted by first wait +EXPECT_EQ(mock_object.Mock3(500, "abcd", false), + 30); // Accepted by second wait +} + +TEST(UserInterfaceTest, NoMoveFromGenericCallSession) { + StrictMock mock_object; + auto coro = COROUTINE(NoMoveFromGenericCallSession) { + auto cg = WAIT_FOR_CALL(); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock1(200))); + COTEST_ASSERT(cg); // should still be valid now we use shared_ptr + cg.IS_CALL(mock_object, Mock1(200)).RETURN(10); + }; + coro.WATCH_CALL(mock_object, Mock1); + + EXPECT_EQ(mock_object.Mock1(200), 10); +} diff --git a/coroutines/test/cotest-wild.cc b/coroutines/test/cotest-wild.cc new file mode 100644 index 000000000..8f77599af --- /dev/null +++ b/coroutines/test/cotest-wild.cc @@ -0,0 +1,291 @@ +#include + +#include "cotest/cotest.h" +#include "gtest/gtest-spi.h" + +using namespace std; +using ::testing::StrictMock; +using namespace testing; + +////////////////////////////////////////////// +// Mocking assets + +class ClassToMock { + public: + virtual ~ClassToMock() {} + virtual int Mock1(int i) const = 0; + virtual int Mock2(int i, int j) const = 0; + virtual int Mock3(int i) const = 0; + virtual int Mock4(int i) const = 0; + virtual int Mock4(int i) = 0; + virtual void Mock5(int i) const = 0; + virtual void Mock6(int i, int j) const = 0; +}; +class MockClass : public ClassToMock { + public: + MOCK_METHOD(int, Mock1, (int i), (const, override)); + MOCK_METHOD(int, Mock2, (int i, int j), (const, override)); + MOCK_METHOD(int, Mock3, (int i), (const, override)); + MOCK_METHOD(int, Mock4, (int i), (const, override)); + MOCK_METHOD(int, Mock4, (int i), (override)); + MOCK_METHOD(void, Mock5, (int i), (const, override)); + MOCK_METHOD(void, Mock6, (int i, int j), (const, override)); +}; + +using ::testing::Return; + +////////////////////////////////////////////// +// The actual tests + +TEST(ExteriorWildcardTest, TwoMethodWaiting) { + StrictMock mock_object; + StrictMock mock_object2; + + // Try doing this early, to simulate a generic setup phase + EXPECT_CALL(mock_object2, Mock1).WillRepeatedly(Return(-2)); + + auto coro = COROUTINE(TwoMethodWaiting) { + auto cg = WAIT_FOR_CALL(mock_object); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock1(200)).RETURN(20)); + auto cg2 = WAIT_FOR_CALL(mock_object); + EXPECT_TRUE(cg2.IS_CALL(mock_object, Mock2(200, 400)).RETURN(30)); + }; + + EXPECT_CALL(mock_object2, Mock2).Times(2).WillRepeatedly(Return(-3)); + coro.WATCH_CALL(); + + EXPECT_EQ(mock_object2.Mock1(500), -2); + EXPECT_EQ(mock_object2.Mock2(500, 600), -3); + EXPECT_EQ(mock_object.Mock1(200), 20); + EXPECT_EQ(mock_object2.Mock1(501), -2); + EXPECT_EQ(mock_object2.Mock2(501, 601), -3); + EXPECT_EQ(mock_object.Mock2(200, 400), 30); +} + +TEST(ExteriorWildcardTest, TwoMethodWaitingMO) { + StrictMock mock_object; + StrictMock mock_object2; + + auto coro = COROUTINE(TwoMethodWaiting) { + auto cg = WAIT_FOR_CALL(); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock1(200)).RETURN(20)); + auto cg2 = WAIT_FOR_CALL(); + EXPECT_TRUE(cg2.IS_CALL(mock_object, Mock2(200, 400)).RETURN(30)); + }; + + EXPECT_CALL(mock_object2, Mock1).Times(2).WillRepeatedly(Return(-2)); + EXPECT_CALL(mock_object2, Mock2).Times(2).WillRepeatedly(Return(-3)); + coro.WATCH_CALL(mock_object); + + EXPECT_EQ(mock_object2.Mock1(500), -2); + EXPECT_EQ(mock_object2.Mock2(500, 600), -3); + EXPECT_EQ(mock_object.Mock1(200), 20); + EXPECT_EQ(mock_object2.Mock1(501), -2); + EXPECT_EQ(mock_object2.Mock2(501, 601), -3); + EXPECT_EQ(mock_object.Mock2(200, 400), 30); +} + +TEST(ExteriorWildcardTest, TwoMethodWaitingPre) { + StrictMock mock_object; + StrictMock mock_object2; + + auto coro = COROUTINE(TwoMethodWaitingPre) { + auto cg = WAIT_FOR_CALL(mock_object); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock1(200)).RETURN(20)); + cg = WAIT_FOR_CALL(mock_object); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock2(200, 400)).RETURN(30)); + }; + + EXPECT_CALL(mock_object2, Mock1).Times(2).WillRepeatedly(Return(-2)); + EXPECT_CALL(mock_object2, Mock2).Times(2).WillRepeatedly(Return(-3)); + coro.WATCH_CALL(); + EXPECT_CALL(mock_object, Mock1(1000)).WillRepeatedly(Return(-10)); + EXPECT_CALL(mock_object2, Mock1(1100)).WillRepeatedly(Return(-11)); + + EXPECT_EQ(mock_object2.Mock1(500), -2); + EXPECT_EQ(mock_object2.Mock2(500, 600), -3); + EXPECT_EQ(mock_object.Mock1(200), 20); + EXPECT_EQ(mock_object.Mock1(1000), -10); + EXPECT_EQ(mock_object2.Mock1(1100), -11); + EXPECT_EQ(mock_object2.Mock1(501), -2); + EXPECT_EQ(mock_object2.Mock2(501, 601), -3); + EXPECT_EQ(mock_object.Mock2(200, 400), 30); +} + +TEST(ExteriorWildcardTest, TwoMethodWaitingPreMO) { + StrictMock mock_object; + StrictMock mock_object2; + + auto coro = COROUTINE(TwoMethodWaitingPre) { + auto cg = WAIT_FOR_CALL(); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock1(200)).RETURN(20)); + cg = WAIT_FOR_CALL(); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock2(200, 400)).RETURN(30)); + }; + + EXPECT_CALL(mock_object2, Mock1).Times(2).WillRepeatedly(Return(-2)); + EXPECT_CALL(mock_object2, Mock2).Times(2).WillRepeatedly(Return(-3)); + coro.WATCH_CALL(mock_object); + EXPECT_CALL(mock_object, Mock1(1000)).WillRepeatedly(Return(-10)); + EXPECT_CALL(mock_object2, Mock1(1100)).WillRepeatedly(Return(-11)); + + EXPECT_EQ(mock_object2.Mock1(500), -2); + EXPECT_EQ(mock_object2.Mock2(500, 600), -3); + EXPECT_EQ(mock_object.Mock1(200), 20); + EXPECT_EQ(mock_object.Mock1(1000), -10); + EXPECT_EQ(mock_object2.Mock1(1100), -11); + EXPECT_EQ(mock_object2.Mock1(501), -2); + EXPECT_EQ(mock_object2.Mock2(501, 601), -3); + EXPECT_EQ(mock_object.Mock2(200, 400), 30); +} + +TEST(ExteriorWildcardTest, MultiPriority) { + StrictMock mock_object; + StrictMock mock_object2; + + auto coro1 = COROUTINE() { + auto cg = WAIT_FOR_CALL(mock_object); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock1(200)).RETURN(20)); + // Exit without RETIRE() is saturation + }; + + auto coro2 = COROUTINE() { + auto cg = WAIT_FOR_CALL(mock_object2); + EXPECT_TRUE(cg.IS_CALL(mock_object2, Mock2(_, 400)).With(Lt()).RETURN(30)); + RETIRE(); + }; + + coro1.WATCH_CALL(); + EXPECT_CALL(mock_object, Mock1).WillOnce(Return(-10)).RetiresOnSaturation(); // #1 + EXPECT_CALL(mock_object2, Mock2).WillOnce(Return(-11)); // #2 + coro2.WATCH_CALL(); + + EXPECT_EQ(mock_object.Mock1(1000), + -10); // coro2's wait drops; expectation #1 matches and retires + EXPECT_EQ(mock_object.Mock1(200), + 20); // coro2's wait drops; expectation #1 has retired; coro1 + // accepts and is saturated + + EXPECT_EQ(mock_object2.Mock2(200, 400), 30); // coro2 accepts and retires + EXPECT_EQ(mock_object2.Mock2(200, 400), + -11); // coro2 has retired; expectation #2 matches and is saturated +} + +TEST(ExteriorWildcardTest, MultiPriorityMO) { + StrictMock mock_object; + StrictMock mock_object2; + + auto coro1 = COROUTINE() { + auto cg = WAIT_FOR_CALL(); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock1(200)).RETURN(20)); + // Exit without RETIRE() is saturation + }; + + auto coro2 = COROUTINE() { + auto cg = WAIT_FOR_CALL(); + EXPECT_TRUE(cg.IS_CALL(mock_object2, Mock2(_, 400)).With(Lt()).RETURN(30)); + RETIRE(); + }; + + coro1.WATCH_CALL(mock_object); + EXPECT_CALL(mock_object, Mock1).WillOnce(Return(-10)).RetiresOnSaturation(); // #1 + EXPECT_CALL(mock_object2, Mock2).WillOnce(Return(-11)); // #2 + coro2.WATCH_CALL(mock_object2); + + EXPECT_EQ(mock_object.Mock1(1000), + -10); // coro2's wait drops; expectation #1 matches and retires + EXPECT_EQ(mock_object.Mock1(200), + 20); // coro2's wait drops; expectation #1 has retired; coro1 + // accepts and is saturated + + EXPECT_EQ(mock_object2.Mock2(200, 400), 30); // coro2 accepts and retires + EXPECT_EQ(mock_object2.Mock2(200, 400), + -11); // coro2 has retired; expectation #2 matches and is saturated +} + +TEST(ExteriorWildcardTest, MockObjectAddressAlias) { + auto coro = COROUTINE() { + WAIT_FOR_CALL().RETURN(); + WAIT_FOR_CALL().RETURN(); + }; + + { + StrictMock mock_object; + coro.WATCH_CALL(mock_object); + + mock_object.Mock5(200); + } + + { + StrictMock mock_object2; + + // Known bug with WATCH_CALL( mock object ) + // This call should not make it into the coroutine + // but it does because mock_object2 is at the same address as + // the now-deleted mock_object1. Not easy to fix: consider if + // we hadn't made any calls on mock_object - then the GMock code + // that registers the mocker would not have run. If we assume + // a call, then we could maybe use GMock's registry to recover + // the relationship - then we'd also need DetachMocker() so that + // the CotestWatcher can deduce that it needs to detach from the + // mock object too. + // However, the test case to cause this is very strange and seems + // to require the coro to be outside the scope of the mock objects - + // otherwise Watchers will be discarded before here. + mock_object2.Mock6(200, 400); + } +} + +TEST(ExteriorWildcardTest, StackedCoros) { + StrictMock mock_object; + StrictMock mock_object2; + + auto coro1 = COROUTINE(coro1) { + WATCH_CALL(); // watch all mock calls + + auto cg = WAIT_FOR_CALL(); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock1(1000)).RETURN(-10)); + cg = WAIT_FOR_CALL(mock_object); + EXPECT_TRUE(cg.IS_CALL(mock_object, Mock1(200)).RETURN(20)); + auto cs = WAIT_FOR_CALL(mock_object, Mock2); + EXPECT_TRUE(cs.IS_CALL(mock_object, Mock2(220, _))); + cs.RETURN(-11); + RETIRE(); + }; + + auto coro2 = COROUTINE(coro2) { + WATCH_CALL(); // watch all mock calls + + auto cg = WAIT_FOR_CALL(mock_object2); + EXPECT_TRUE(cg.IS_CALL(mock_object2, Mock2(_, 400)).With(Lt()).RETURN(30)); + auto cs = WAIT_FOR_CALL(mock_object, Mock1(1100)); + cs.RETURN(-5); + cg = WAIT_FOR_CALL(); + EXPECT_TRUE(cg.IS_CALL(mock_object2, Mock2(300, 350)).With(Lt()).RETURN(33)); + SATISFY(); + cg = WAIT_FOR_CALL(mock_object).RETURN(); + }; + + EXPECT_EQ(mock_object.Mock1(1000), + -10); // c2 is looking for any mock call on mo2, so drops it and c1 + // is looking for any so accepts, checks, returns + EXPECT_EQ(mock_object2.Mock2(200, 400), + 30); // c2 is looking for any mock call on mo2, so accepts, checks, + // returns + EXPECT_EQ(mock_object.Mock1(200), + 20); // c2 is now looking for mo.MM1 with arg==1100, so drops it and c1 + // is now looking for any call on mo so accepts, checks, returns + + EXPECT_EQ(mock_object.Mock2(220, 400), + -11); // c2 is still looking for mo.MM1 with arg==1100, so drops it and + // c1 is now looking for mo.MM2 mo so accepts, checks, returns + EXPECT_EQ(mock_object.Mock1(1100), + -5); // c2 is still looking for mo.MM1 with arg==1100, so accepts, + // checks, returns + EXPECT_EQ(mock_object2.Mock2(300, 350), + 33); // c2 is now looking for any call so accepts, checks, returns + + // c1 has retired, which means it will drop any further calls (none are made + // here) c2 is left looking for any call on mo, but it's satisfied, so if the + // call never arrives, there's no error +} diff --git a/coroutines/test/exp-finder-test.cc b/coroutines/test/exp-finder-test.cc new file mode 100644 index 000000000..7db0ef7b9 --- /dev/null +++ b/coroutines/test/exp-finder-test.cc @@ -0,0 +1,321 @@ +#include + +#include "cotest/cotest.h" +#include "cotest/internal/cotest-integ-finder.h" + +using namespace std; +using namespace testing; + +/* + * Note: at the time of writing, this test does not use wildcarded watches + * and so will not engage the untyped expectation finding mechanism. So + * internal::CotestMockHandlerPool::Finder() won't be being used by the test + * harness while we're testing it. + */ + +////////////////////////////////////////////// +// The actual tests + +TEST(ExpectationFinderTest, NoSchemes) { + MockFunction> mockLambda; + std::vector schemes; + + auto coro = COROUTINE(){ + // Empty coro oversaturates on any visible call + }; + coro.WATCH_CALL(mockLambda); + + // No schemes, so no exps. "which" is undefined in this case. + unsigned which; + auto exp = internal::CotestMockHandlerPool::Finder(schemes, mockLambda.AsStdFunction(), &which); + ASSERT_FALSE(exp); +} + +TEST(ExpectationFinderTest, EmptyScheme) { + MockFunction> mockLambda; + internal::MockHandlerScheme s1; + std::vector schemes{&s1}; + + auto coro = COROUTINE(){ + // Empty coro oversaturates on any visible call + }; + coro.WATCH_CALL(mockLambda); + + // No exps. "which" is undefined in this case. + unsigned which; + auto exp = internal::CotestMockHandlerPool::Finder(schemes, mockLambda.AsStdFunction(), &which); + ASSERT_FALSE(exp); +} + +TEST(ExpectationFinderTest, SimpleSchemeNo) { + MockFunction> mockLambda; + + auto e1 = make_shared>(nullptr, nullptr, 0, "", + internal::Function::ArgumentMatcherTuple()); + internal::MockHandlerScheme s1{e1}; + std::vector schemes{&s1}; + + auto coro = COROUTINE() { + // One exp was created, and it should be queried + WAIT_FOR_CALL(mockLambda, Call(e1.get())).RETURN(false); + }; + coro.WATCH_CALL(mockLambda); + + // The exp says no. "which" is undefined in this case. + unsigned which; + auto exp = internal::CotestMockHandlerPool::Finder(schemes, mockLambda.AsStdFunction(), &which); + ASSERT_FALSE(exp); +} + +TEST(ExpectationFinderTest, SimpleSchemeYes) { + MockFunction> mockLambda; + + auto e1 = make_shared>(nullptr, nullptr, 0, "", + internal::Function::ArgumentMatcherTuple()); + internal::MockHandlerScheme s1{e1}; + std::vector schemes{&s1}; + + auto coro = COROUTINE() { + // One exp was created, and it should be queried + WAIT_FOR_CALL(mockLambda, Call(e1.get())).RETURN(true); + }; + coro.WATCH_CALL(mockLambda); + + // The exp says yes. + unsigned which; + auto exp = internal::CotestMockHandlerPool::Finder(schemes, mockLambda.AsStdFunction(), &which); + ASSERT_TRUE(exp); + ASSERT_EQ(which, 0); +} + +TEST(ExpectationFinderTest, OneSchemeMultiExp) { + MockFunction> mockLambda; + + auto e1 = make_shared>(nullptr, nullptr, 0, "", + internal::Function::ArgumentMatcherTuple()); + auto e2 = make_shared>(nullptr, nullptr, 0, "", + internal::Function::ArgumentMatcherTuple()); + auto e3 = make_shared>(nullptr, nullptr, 0, "", + internal::Function::ArgumentMatcherTuple()); + auto e4 = make_shared>(nullptr, nullptr, 0, "", + internal::Function::ArgumentMatcherTuple()); + internal::MockHandlerScheme s1{e1, e2, e3, e4}; + std::vector schemes{&s1}; + + auto coro = COROUTINE() { + // Exps queried in reverse order until one says yes + WAIT_FOR_CALL(mockLambda, Call(e4.get())).RETURN(false); + WAIT_FOR_CALL(mockLambda, Call(e3.get())).RETURN(false); + WAIT_FOR_CALL(mockLambda, Call(e2.get())).RETURN(true); + }; + coro.WATCH_CALL(mockLambda); + + // The exp says yes. + unsigned which; + auto exp = internal::CotestMockHandlerPool::Finder(schemes, mockLambda.AsStdFunction(), &which); + ASSERT_TRUE(exp); + ASSERT_EQ(which, 0); +} + +TEST(ExpectationFinderTest, OneSchemeMultiExpIncing) { + MockFunction> mockLambda; + + auto e1 = make_shared>(nullptr, nullptr, 0, "", + internal::Function::ArgumentMatcherTuple()); + auto e2 = make_shared>(nullptr, nullptr, 0, "", + internal::Function::ArgumentMatcherTuple()); + auto e3 = make_shared>(nullptr, nullptr, 0, "", + internal::Function::ArgumentMatcherTuple()); + auto e4 = make_shared>(nullptr, nullptr, 0, "", + internal::Function::ArgumentMatcherTuple()); + internal::MockHandlerScheme s1{e1, e2, e3, e4}; + std::vector schemes{&s1}; + + auto coro = COROUTINE() { + // Exps queried in reverse order until one says yes + WAIT_FOR_CALL(mockLambda, Call(e4.get())).RETURN(false); + WAIT_FOR_CALL(mockLambda, Call(e3.get())).RETURN(false); + WAIT_FOR_CALL(mockLambda, Call(e2.get())).RETURN(true); + }; + coro.WATCH_CALL(mockLambda); + + // The exp says yes. + unsigned which; + auto exp = internal::CotestMockHandlerPool::Finder(schemes, mockLambda.AsStdFunction(), &which); + ASSERT_TRUE(exp); + ASSERT_EQ(which, 0); +} + +TEST(ExpectationFinderTest, MultiSchemeNone) { + MockFunction> mockLambda; + + auto e1 = make_shared>(nullptr, nullptr, 0, "", + internal::Function::ArgumentMatcherTuple()); + internal::CotestMockHandlerPool::GetOrCreateInstance()->AddExpectation( + []() {}); // bumps the static global priority + auto e2 = make_shared>(nullptr, nullptr, 0, "", + internal::Function::ArgumentMatcherTuple()); + internal::CotestMockHandlerPool::GetOrCreateInstance()->AddExpectation( + []() {}); // bumps the static global priority + auto e3 = make_shared>(nullptr, nullptr, 0, "", + internal::Function::ArgumentMatcherTuple()); + internal::CotestMockHandlerPool::GetOrCreateInstance()->AddExpectation( + []() {}); // bumps the static global priority + auto e4 = make_shared>(nullptr, nullptr, 0, "", + internal::Function::ArgumentMatcherTuple()); + internal::CotestMockHandlerPool::GetOrCreateInstance()->AddExpectation( + []() {}); // bumps the static global priority + auto e5 = make_shared>(nullptr, nullptr, 0, "", + internal::Function::ArgumentMatcherTuple()); + internal::CotestMockHandlerPool::GetOrCreateInstance()->AddExpectation( + []() {}); // bumps the static global priority + internal::MockHandlerScheme s1{e1, e3, e5}; + internal::MockHandlerScheme s2{e2}; + internal::MockHandlerScheme s3{e4}; + std::vector schemes{&s1, &s2, &s3}; + + auto coro = COROUTINE() { + // Exps queried in reverse order until one says yes + WAIT_FOR_CALL(mockLambda, Call(e5.get())).RETURN(false); + WAIT_FOR_CALL(mockLambda, Call(e4.get())).RETURN(false); + WAIT_FOR_CALL(mockLambda, Call(e3.get())).RETURN(false); + WAIT_FOR_CALL(mockLambda, Call(e2.get())).RETURN(false); + WAIT_FOR_CALL(mockLambda, Call(e1.get())).RETURN(false); + }; + coro.WATCH_CALL(mockLambda); + + // The exps say no. + unsigned which; + auto exp = internal::CotestMockHandlerPool::Finder(schemes, mockLambda.AsStdFunction(), &which); + ASSERT_FALSE(exp); +} + +TEST(ExpectationFinderTest, MultiSchemeLate) { + MockFunction> mockLambda; + + auto e1 = make_shared>(nullptr, nullptr, 0, "", + internal::Function::ArgumentMatcherTuple()); + internal::CotestMockHandlerPool::GetOrCreateInstance()->AddExpectation( + []() {}); // bumps the static global priority + auto e2 = make_shared>(nullptr, nullptr, 0, "", + internal::Function::ArgumentMatcherTuple()); + internal::CotestMockHandlerPool::GetOrCreateInstance()->AddExpectation( + []() {}); // bumps the static global priority + auto e3 = make_shared>(nullptr, nullptr, 0, "", + internal::Function::ArgumentMatcherTuple()); + internal::CotestMockHandlerPool::GetOrCreateInstance()->AddExpectation( + []() {}); // bumps the static global priority + auto e4 = make_shared>(nullptr, nullptr, 0, "", + internal::Function::ArgumentMatcherTuple()); + internal::CotestMockHandlerPool::GetOrCreateInstance()->AddExpectation( + []() {}); // bumps the static global priority + auto e5 = make_shared>(nullptr, nullptr, 0, "", + internal::Function::ArgumentMatcherTuple()); + internal::CotestMockHandlerPool::GetOrCreateInstance()->AddExpectation( + []() {}); // bumps the static global priority + internal::MockHandlerScheme s1{e1, e3, e5}; + internal::MockHandlerScheme s2{e2}; + internal::MockHandlerScheme s3{e4}; + std::vector schemes{&s1, &s2, &s3}; + + auto coro = COROUTINE() { + // Exps queried in reverse order until one says yes + WAIT_FOR_CALL(mockLambda, Call(e5.get())).RETURN(false); + WAIT_FOR_CALL(mockLambda, Call(e4.get())).RETURN(false); + WAIT_FOR_CALL(mockLambda, Call(e3.get())).RETURN(false); + WAIT_FOR_CALL(mockLambda, Call(e2.get())).RETURN(false); + WAIT_FOR_CALL(mockLambda, Call(e1.get())).RETURN(true); + }; + coro.WATCH_CALL(mockLambda); + + // The exp says yes. + unsigned which; + auto exp = internal::CotestMockHandlerPool::Finder(schemes, mockLambda.AsStdFunction(), &which); + ASSERT_EQ(exp, e1.get()); + ASSERT_EQ(which, 0); +} + +TEST(ExpectationFinderTest, MultiSchemeMid) { + MockFunction> mockLambda; + + auto e1 = make_shared>(nullptr, nullptr, 0, "", + internal::Function::ArgumentMatcherTuple()); + internal::CotestMockHandlerPool::GetOrCreateInstance()->AddExpectation( + []() {}); // bumps the static global priority + auto e2 = make_shared>(nullptr, nullptr, 0, "", + internal::Function::ArgumentMatcherTuple()); + internal::CotestMockHandlerPool::GetOrCreateInstance()->AddExpectation( + []() {}); // bumps the static global priority + auto e3 = make_shared>(nullptr, nullptr, 0, "", + internal::Function::ArgumentMatcherTuple()); + internal::CotestMockHandlerPool::GetOrCreateInstance()->AddExpectation( + []() {}); // bumps the static global priority + auto e4 = make_shared>(nullptr, nullptr, 0, "", + internal::Function::ArgumentMatcherTuple()); + internal::CotestMockHandlerPool::GetOrCreateInstance()->AddExpectation( + []() {}); // bumps the static global priority + auto e5 = make_shared>(nullptr, nullptr, 0, "", + internal::Function::ArgumentMatcherTuple()); + internal::CotestMockHandlerPool::GetOrCreateInstance()->AddExpectation( + []() {}); // bumps the static global priority + internal::MockHandlerScheme s1{e1, e3, e5}; + internal::MockHandlerScheme s2{e2}; + internal::MockHandlerScheme s3{e4}; + std::vector schemes{&s1, &s2, &s3}; + + auto coro = COROUTINE() { + // Exps queried in reverse order until one says yes + WAIT_FOR_CALL(mockLambda, Call(e5.get())).RETURN(false); + WAIT_FOR_CALL(mockLambda, Call(e4.get())).RETURN(false); + WAIT_FOR_CALL(mockLambda, Call(e3.get())).RETURN(false); + WAIT_FOR_CALL(mockLambda, Call(e2.get())).RETURN(true); + }; + coro.WATCH_CALL(mockLambda); + + // The exp says yes. + unsigned which; + auto exp = internal::CotestMockHandlerPool::Finder(schemes, mockLambda.AsStdFunction(), &which); + ASSERT_EQ(exp, e2.get()); + ASSERT_EQ(which, 1); +} + +TEST(ExpectationFinderTest, MultiSchemeEarly) { + MockFunction> mockLambda; + + auto e1 = make_shared>(nullptr, nullptr, 0, "", + internal::Function::ArgumentMatcherTuple()); + internal::CotestMockHandlerPool::GetOrCreateInstance()->AddExpectation( + []() {}); // bumps the static global priority + auto e2 = make_shared>(nullptr, nullptr, 0, "", + internal::Function::ArgumentMatcherTuple()); + internal::CotestMockHandlerPool::GetOrCreateInstance()->AddExpectation( + []() {}); // bumps the static global priority + auto e3 = make_shared>(nullptr, nullptr, 0, "", + internal::Function::ArgumentMatcherTuple()); + internal::CotestMockHandlerPool::GetOrCreateInstance()->AddExpectation( + []() {}); // bumps the static global priority + auto e4 = make_shared>(nullptr, nullptr, 0, "", + internal::Function::ArgumentMatcherTuple()); + internal::CotestMockHandlerPool::GetOrCreateInstance()->AddExpectation( + []() {}); // bumps the static global priority + auto e5 = make_shared>(nullptr, nullptr, 0, "", + internal::Function::ArgumentMatcherTuple()); + internal::CotestMockHandlerPool::GetOrCreateInstance()->AddExpectation( + []() {}); // bumps the static global priority + internal::MockHandlerScheme s1{e1, e3, e5}; + internal::MockHandlerScheme s2{e2}; + internal::MockHandlerScheme s3{e4}; + std::vector schemes{&s1, &s2, &s3}; + + auto coro = COROUTINE() { + // Exps queried in reverse order until one says yes + WAIT_FOR_CALL(mockLambda, Call(e5.get())).RETURN(true); + }; + coro.WATCH_CALL(mockLambda); + + // The exp says yes. + unsigned which; + auto exp = internal::CotestMockHandlerPool::Finder(schemes, mockLambda.AsStdFunction(), &which); + ASSERT_EQ(exp, e5.get()); + ASSERT_EQ(which, 0); +}