#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