From: Manuel Nickschas Date: Thu, 4 Oct 2018 17:03:02 +0000 (+0200) Subject: test: Add a way to mock peers for SignalProxy-related test cases X-Git-Tag: test-travis-01~110 X-Git-Url: https://git.quassel-irc.org/?p=quassel.git;a=commitdiff_plain;h=7dee6df30bc997b3045ec0e20cdefdf8a70ba93e test: Add a way to mock peers for SignalProxy-related test cases Introduce a new mock class MockedPeer that allows for setting expectations for dispatched Quassel protocol messages. Also provide matchers to express such expectations in a straightforward way. MockedPeer for now only supports SignalProxy messages; support for authentication messages can be added later if needed. Note that expectations can be set on the level of protocol messages, but things like a testing a particular serialization, the use of network sockets, or other lower-level implementation details are not supported, nor is MockedPeer intended for such use cases (we'd expect specific tests for specific peer types). --- diff --git a/src/test/util/CMakeLists.txt b/src/test/util/CMakeLists.txt index f6c4cc19..802c2099 100644 --- a/src/test/util/CMakeLists.txt +++ b/src/test/util/CMakeLists.txt @@ -2,6 +2,7 @@ quassel_add_module(Test::Util EXPORT NOINSTALL) target_sources(${TARGET} PRIVATE invocationspy.cpp + mockedpeer.cpp ) target_link_libraries(${TARGET} diff --git a/src/test/util/mockedpeer.cpp b/src/test/util/mockedpeer.cpp new file mode 100644 index 00000000..e182d464 --- /dev/null +++ b/src/test/util/mockedpeer.cpp @@ -0,0 +1,297 @@ +/*************************************************************************** + * Copyright (C) 2005-2018 by the Quassel Project * + * devel@quassel-irc.org * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) version 3. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "mockedpeer.h" + +#include + +using namespace ::testing; + +namespace test { + +// ---- Protocol message wrapper ----------------------------------------------------------------------------------------------------------- + +struct ProtocolMessage +{ + // Supported protocol message types. If extended, the various visitors in this file need to be extended too. + boost::variant< + Protocol::SyncMessage, + Protocol::RpcCall, + Protocol::InitRequest, + Protocol::InitData + > message; +}; + +void PrintTo(const ProtocolMessage& msg, std::ostream* os) +{ + struct PrintToVisitor : public boost::static_visitor + { + PrintToVisitor(std::ostream* os) + : _os{os} + {} + + void operator()(const Protocol::SyncMessage& syncMessage) const + { + *_os << "SyncMessage{className = " << PrintToString(syncMessage.className) + << ", objectName = " << PrintToString(syncMessage.objectName) + << ", slotName = " << PrintToString(syncMessage.slotName) + << ", params = " << PrintToString(syncMessage.params) + << "}"; + } + + void operator()(const Protocol::RpcCall& rpcCall) const + { + *_os << "RpcCall{slotName = " << PrintToString(rpcCall.slotName) + << ", params = " << PrintToString(rpcCall.params) + << "}"; + } + + void operator()(const Protocol::InitRequest& initRequest) const + { + *_os << "InitRequest{className = " << PrintToString(initRequest.className) + << ", objectName = " << PrintToString(initRequest.objectName) + << "}"; + } + + void operator()(const Protocol::InitData& initData) const + { + *_os << "InitData{className = " << PrintToString(initData.className) + << ", objectName = " << PrintToString(initData.objectName) + << ", initData = " << PrintToString(initData.initData) + << "}"; + } + + private: + std::ostream* _os; + }; + + boost::apply_visitor(PrintToVisitor{os}, msg.message); +} + +// ---- MockedPeer ------------------------------------------------------------------------------------------------------------------------- + +MockedPeer::MockedPeer(QObject* parent) + : InternalPeer(parent) +{ + // Default behavior will just delegate to InternalPeer (through the mocked Dispatches() method) + ON_CALL(*this, Dispatches(_)).WillByDefault(Invoke(this, &MockedPeer::dispatchInternal)); +} + +MockedPeer::~MockedPeer() = default; + +void MockedPeer::dispatch(const Protocol::SyncMessage& msg) +{ + Dispatches({msg}); +} + +void MockedPeer::dispatch(const Protocol::RpcCall& msg) +{ + Dispatches({msg}); +} + +void MockedPeer::dispatch(const Protocol::InitRequest& msg) +{ + Dispatches({msg}); +} + +void MockedPeer::dispatch(const Protocol::InitData& msg) +{ + Dispatches({msg}); +} + +// Unwraps the type before calling the correct overload of realDispatch() +struct DispatchVisitor : public boost::static_visitor +{ + DispatchVisitor(MockedPeer* mock) + : _mock{mock} + {} + + template + void operator()(const T& message) const + { + _mock->realDispatch(message); + } + + MockedPeer* _mock; +}; + +void MockedPeer::dispatchInternal(const ProtocolMessage& message) +{ + boost::apply_visitor(DispatchVisitor{this}, message.message); +} + +template +void MockedPeer::realDispatch(const T& message) +{ + InternalPeer::dispatch(message); +} + +// ---- Expectations and matchers ---------------------------------------------------------------------------------------------------------- + +namespace { + +struct SyncMessageExpectation +{ + Matcher className; + Matcher objectName; + Matcher slotName; + Matcher params; +}; + +struct RpcCallExpectation +{ + Matcher slotName; + Matcher params; +}; + +struct InitRequestExpectation +{ + Matcher className; + Matcher objectName; +}; + +struct InitDataExpectation +{ + Matcher className; + Matcher objectName; + Matcher initData; +}; + +/** + * Generic matcher for protocol messages. + * + * @note The matcher, maybe somewhat surprisingly, always matches; it does, however, add test failures if expectations fail. + * This makes for a much better readable output when used in EXPECT_CALL chains, while still letting test cases fail + * as expected. + */ +class ProtocolMessageMatcher : public MatcherInterface +{ +public: + template + ProtocolMessageMatcher(T expectation) + : _expectation{std::move(expectation)} + {} + + /** + * Visitor used for matching a particular type of protocol message. + * + * Each supported type requires a corresponding overload for the call operator. + */ + struct MatchVisitor : public boost::static_visitor + { + MatchVisitor(const boost::any& expectation) + : _expectation{expectation} + {} + + bool operator()(const Protocol::SyncMessage& syncMessage) const + { + auto e = boost::any_cast(&_expectation); + if (!e) { + ADD_FAILURE() << "Did not expect a SyncMessage!"; + return true; + } + EXPECT_THAT(syncMessage.className, e->className); + EXPECT_THAT(syncMessage.objectName, e->objectName); + EXPECT_THAT(syncMessage.slotName, e->slotName); + EXPECT_THAT(syncMessage.params, e->params); + return true; + } + + bool operator()(const Protocol::RpcCall& rpcCall) const + { + auto e = boost::any_cast(&_expectation); + if (!e) { + ADD_FAILURE() << "Did not expect an RpcCall!"; + return true; + } + EXPECT_THAT(rpcCall.slotName, e->slotName); + EXPECT_THAT(rpcCall.params, e->params); + return true; + } + + bool operator()(const Protocol::InitRequest& initRequest) const + { + auto e = boost::any_cast(&_expectation); + if (!e) { + ADD_FAILURE() << "Did not expect an InitRequest!"; + return true; + } + EXPECT_THAT(initRequest.className, e->className); + EXPECT_THAT(initRequest.objectName, e->objectName); + return true; + } + + bool operator()(const Protocol::InitData& initData) const + { + auto e = boost::any_cast(&_expectation); + if (!e) { + ADD_FAILURE() << "Did not expect InitData!"; + return true; + } + EXPECT_THAT(initData.className, e->className); + EXPECT_THAT(initData.objectName, e->objectName); + EXPECT_THAT(initData.initData, e->initData); + return true; + } + + private: + const boost::any& _expectation; + }; + + bool MatchAndExplain(const ProtocolMessage& protoMsg, MatchResultListener*) const override + { + return boost::apply_visitor(MatchVisitor{_expectation}, protoMsg.message); + } + + void DescribeTo(std::ostream* os) const override + { + // This should never be actually called because we always match (but fail sub-expectations if appropriate) + *os << "Matcher for protocol messages"; + } + +private: + boost::any _expectation; +}; + +} // anon + +// Create matcher instances + +Matcher SyncMessage(Matcher className, Matcher objectName, Matcher slotName, Matcher params) +{ + return MakeMatcher(new ProtocolMessageMatcher{SyncMessageExpectation{std::move(className), std::move(objectName), std::move(slotName), std::move(params)}}); +} + +Matcher RpcCall(Matcher slotName, Matcher params) +{ + return MakeMatcher(new ProtocolMessageMatcher{RpcCallExpectation{std::move(slotName), std::move(params)}}); +} + +Matcher InitRequest(Matcher className, Matcher objectName) +{ + return MakeMatcher(new ProtocolMessageMatcher{InitRequestExpectation{std::move(className), std::move(objectName)}}); +} + +Matcher InitData(Matcher className, Matcher objectName, Matcher initData) +{ + return MakeMatcher(new ProtocolMessageMatcher{InitDataExpectation{std::move(className), std::move(objectName), std::move(initData)}}); +} + +} // namespace test diff --git a/src/test/util/mockedpeer.h b/src/test/util/mockedpeer.h new file mode 100644 index 00000000..fde3d1c5 --- /dev/null +++ b/src/test/util/mockedpeer.h @@ -0,0 +1,105 @@ +/*************************************************************************** + * Copyright (C) 2005-2018 by the Quassel Project * + * devel@quassel-irc.org * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) version 3. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#pragma once + +#include "test-util-export.h" + +#include + +#include "internalpeer.h" +#include "testglobal.h" + +namespace test { + +/// Variant-based wrapper struct for supported protocol messages +struct ProtocolMessage; + +TEST_UTIL_EXPORT void PrintTo(const ProtocolMessage&, std::ostream*); + +/** + * Mocked peer that can be used in test cases involving SignalProxy. + * + * This mock class is intended to be used for testing on the abstraction level of Quassel protocol messages; it does not support + * mocking e.g. serialization or network connections, or other lower-level implementation details of specific peer types. + * + * To set expectations, use EXPECT_CALL on the mocked Dispatches() method and the provided protocol message matchers like this: + * + * @code + * EXPECT_CALL(*peer, Dispatches(RpcCall(QByteArray{SIGNAL(sendData(int,QString))}, ElementsAre(42, "Hello")))); + * @endcode + * + * The matchers take matchers themselves for their individual arguments, so you can do things like + * + * @code + * EXPECT_CALL(*peer, Dispatches(RpcCall(_, Contains(42)))); + * @endcode + * + * @note For now, MockedPeer only supports SignalProxy message types. It can be extended in the future to support auth messages, + * as well. + */ +class TEST_UTIL_EXPORT MockedPeer : public InternalPeer +{ + Q_OBJECT + +public: + MockedPeer(QObject* parent = nullptr); + ~MockedPeer() override; + + /// Every message dispatched goes through this mocked method, which can be used with the protocol message machters to define expectations + MOCK_METHOD1(Dispatches, void(const ProtocolMessage&)); + + using InternalPeer::dispatch; + void dispatch(const Protocol::SyncMessage&) override; + void dispatch(const Protocol::RpcCall&) override; + void dispatch(const Protocol::InitRequest&) override; + void dispatch(const Protocol::InitData&) override; + +private: + void dispatchInternal(const ProtocolMessage&); + + template + void realDispatch(const T&); + + friend struct DispatchVisitor; +}; + +// ---- Matchers for defining protocol message expectations for Dispatches() --------------------------------------------------------------- + +TEST_UTIL_EXPORT ::testing::Matcher SyncMessage( + ::testing::Matcher className, + ::testing::Matcher objectName, + ::testing::Matcher slotName, + ::testing::Matcher params); + +TEST_UTIL_EXPORT ::testing::Matcher RpcCall( + ::testing::Matcher slotName, + ::testing::Matcher params); + +TEST_UTIL_EXPORT ::testing::Matcher InitRequest( + ::testing::Matcher className, + ::testing::Matcher objectName); + +TEST_UTIL_EXPORT ::testing::Matcher InitData( + ::testing::Matcher className, + ::testing::Matcher objectName, + ::testing::Matcher initData); + +} // namespace test