#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