diff --git a/tests/blackcore/expect.cpp b/tests/blackcore/expect.cpp new file mode 100644 index 000000000..6d7df102f --- /dev/null +++ b/tests/blackcore/expect.cpp @@ -0,0 +1,92 @@ +/* Copyright (C) 2013 VATSIM Community / contributors + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "expect.h" +#include +#include +#include + +namespace BlackCoreTest +{ + +void ExpectUnit::wait(int timeout) +{ + m_parent->wait(m_srcloc, timeout, *this); +} + +/* Connect slot for the first expectation, then send all queued signals. + */ +void ExpectUnit::init() const +{ + m_nextExpect = m_expects.begin(); + next(); + for (auto i = m_sends.begin(); i != m_sends.end(); ++i) + { + (*i)(); + } +} + +/* Connect slot for the next expectation, or call the onDone() callback if that was the last one. + * Each expectation slot calls this function, so each successful expectation connects the next one. + */ +void ExpectUnit::next() const +{ + m_guard.cleanup(); + + if (m_nextExpect == m_expects.end()) + { + if (m_onDone) + { + m_onDone(this); + } + } + else + { + (*m_nextExpect)(); + ++m_nextExpect; + } +} + +/* For each unit queued by an overloaded wait(), setup its onDone() callback and call its init() method. + * The onDone() callback removes the unit from the queue. When all units have been removed from the queue, + * then the expectation has succeeded. If the timer times out before that happens, then the expectation + * has failed. Failures cause subsequent calls to wait() to do nothing. + */ +void Expect::wait(const SourceLocation& srcloc, int timeout) +{ + auto unitsCopy = m_units; + m_units.clear(); + + if (m_failed) + { + return; + } + + for (auto i = unitsCopy.begin(); i != unitsCopy.end(); ++i) + { + (*i)->onDone([&](const ExpectUnit* u){ unitsCopy.remove(u); }); + (*i)->init(); + } + + QTimer timer; + timer.setSingleShot(true); + QObject::connect(&timer, &QTimer::timeout, [=, &unitsCopy]{ + for (auto i = unitsCopy.begin(); i != unitsCopy.end(); ++i) + { + (*i)->onDone(nullptr); //paranoia + } + unitsCopy.clear(); + QTest::qFail("*** Timed Out ***", qPrintable(srcloc.file), srcloc.line); + m_failed = true; + }); + timer.start(timeout * 1000); + + while (! unitsCopy.isEmpty()) + { + QCoreApplication::processEvents(QEventLoop::WaitForMoreEvents, 10); + } +} + +} //namespace BlackCoreTest \ No newline at end of file diff --git a/tests/blackcore/expect.h b/tests/blackcore/expect.h new file mode 100644 index 000000000..2310fc62e --- /dev/null +++ b/tests/blackcore/expect.h @@ -0,0 +1,216 @@ +/* Copyright (C) 2013 VATSIM Community / contributors + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef BLACKCORETEST_EXPECT_H +#define BLACKCORETEST_EXPECT_H + +#include +#include +#include +#include +#include + +namespace BlackCoreTest +{ + +class Expect; + +//! Class representing a position in a source code file, for error reporting. Used in combination with the SOURCE_LOCATION macro. +class SourceLocation +{ +public: + SourceLocation(const QString& f, int ln) : file(f), line(ln) {} //!< Constructor + const QString file; //!< Source code filename + const int line; //!< Line number +}; + +//! Evaluates to an instance of SourceLocation representing the position where the macro is used. +#define SOURCE_LOCATION (BlackCoreTest::SourceLocation(__FILE__, __LINE__)) + +//! RAII class for Qt signal/slot connections. All connections are closed when object is destroyed. +class ConnectGuard +{ +public: + //! Constructor. + ConnectGuard() {} + + //! Destructor. + ~ConnectGuard() { cleanup(); } + + //! Add a connection to the object. + ConnectGuard& operator+= (const QMetaObject::Connection& conn) { m_conns += conn; return *this; } + + //! Disconnect and remove all stored connections. + void cleanup() { for (auto i = m_conns.begin(); i != m_conns.end(); ++i) QObject::disconnect(*i); m_conns.clear(); } + + //! \{ + //! Copying is only allowed when there are no connections stored. + ConnectGuard(const ConnectGuard& other) { Q_ASSERT(other.m_conns.isEmpty()); } + ConnectGuard& operator= (const ConnectGuard& other) { Q_ASSERT(other.m_conns.isEmpty()); } + //! \} + +private: + QVector m_conns; +}; + +/*! + * One unit of expectation, as returned by Expect::unit(). + * + * Contains a sequence of signals to send, and a sequence of signals that are expected to be received in reply. + */ +class ExpectUnit +{ +public: + //! \{ + /*! + * Adds a signal to the list of signals to send, with optional arguments. + * \param slot A pointer-to-member-function of the subject class. + * \return this object, so methods can be chained. + */ + template ExpectUnit& send(F slot) { m_sends.push_back(std::bind(slot, subject())); return *this; } + template ExpectUnit& send(F slot, T1 arg1) { m_sends.push_back(std::bind(slot, subject(), arg1)); return *this; } + template ExpectUnit& send(F slot, T1 arg1, T2 arg2) { m_sends.push_back(std::bind(slot, subject(), arg1, arg2)); return *this; } + template ExpectUnit& send(F slot, T1 arg1, T2 arg2, T3 arg3) { m_sends.push_back(std::bind(slot, subject(), arg1, arg2, arg3)); return *this; } + template ExpectUnit& send(F slot, T1 arg1, T2 arg2, T3 arg3, T4 arg4) { m_sends.push_back(std::bind(slot, subject(), arg1, arg2, arg3, arg4)); return *this; } + template ExpectUnit& send(F slot, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) { m_sends.push_back(std::bind(slot, subject(), arg1, arg2, arg3, arg4, arg5)); return *this; } + //! \} + + /*! + * Adds a signal to the list of signals which are expects to be received. + * \param signal A pointer-to-member-function of the subject class. + * \param slot A callable object which will be connected to the signal, e.g. to validate the signal arguments. + * \return this object, so methods can be chained. + */ + template ExpectUnit& expect(F1 signal, F2 slot) + { + auto subj = subject(); + m_expects.push_back([=]{ + m_guard += QObject::connect(subj, signal, slot); + m_guard += QObject::connect(subj, signal, [=]{ next(); }); + }); + return *this; + } + + /*! + * Adds a signal to the list of signals which are expected to be received. + * \param signal A pointer-to-member-function of the subject class. + * \return this object, so methods can be chained. + */ + template ExpectUnit& expect(F signal) { return expect(signal, []{}); } + + /*! + * Sends all the queued signals, then waits for each of the expected signals in sequence. + * If the full sequence of expected signals is not received, calls QTest::qFail(). + * \param timeout Time to wait in seconds. Qt event queue is processed when waiting. + */ + void wait(int timeout); + +private: + friend class Expect; + + ExpectUnit(Expect* parent, QObject* subject, const SourceLocation& srcloc) : m_parent(parent), m_subject(subject), m_srcloc(srcloc) {} + + void init() const; + void next() const; + + QPointer m_parent; + QPointer m_subject; + SourceLocation m_srcloc; + QVector> m_sends; + QVector> m_expects; + mutable QVector>::const_iterator m_nextExpect; + mutable ConnectGuard m_guard; + + mutable std::function m_onDone; + void onDone(std::function callback) const { m_onDone = callback; } + + // Helper traits class. Given T is a pointer-to-member-of-U, ClassOf::type is U. + template struct ClassOf; + template struct ClassOf { typedef U type; }; + + // Given T is a pointer-to-member-of-U, subject() returns Expect's subject casted to U*. + template typename ClassOf::type* subject() const { return qobject_cast::type*>(m_subject.data()); } +}; + +/*! + * Class for writing unit tests in which signals are sent to some object, and other signals are expected to be received in reply. + * Loosely modelled on the C library libexpect. + * + * Commonly used in combination with the macros EXPECT_UNIT or EXPECT_WAIT. + */ +class Expect : public QObject +{ + Q_OBJECT + +public: + /*! + * Constructor. + * \param testSubject The object to be tested. + */ + explicit Expect(QObject* testSubject) : m_subject(testSubject), m_failed(false) {} + + /*! + * Add a single expectation that will remain for the lifetime of the object. + * \param signal A pointer-to-member-function of the subject class. + * \param slot A callable object which will be connected to the signal, e.g. to validate the signal arguments. + */ + template void always(F1 signal, F2 slot) + { + m_guard += QObject::connect(m_subject, signal, slot); + } + + /*! + * Returns a new unit of expectation for the subject. Commonly called via the EXPECT_UNIT macro. + * \param srcloc Represents the caller's location in the source code, for error reporting. + */ + ExpectUnit unit(const SourceLocation& srcloc = SOURCE_LOCATION) { return ExpectUnit(this, m_subject, srcloc); } + + //! \{ + /*! + * Allows two or more units of expectation to be waited on simultaneously. Commonly valled via the EXPECT_WAIT macro. + * \param srcloc Represents the caller's location in the source code, for error reporting. + * \param timeout Time to wait in seconds. Qt event queue is processed when waiting. + */ + void wait(const SourceLocation& srcloc, int timeout, const ExpectUnit& u1) { m_units.insert(&u1); wait(srcloc, timeout); } + void wait(const SourceLocation& srcloc, int timeout, const ExpectUnit& u1, const ExpectUnit& u2) { m_units.insert(&u1); m_units.insert(&u2); wait(srcloc, timeout); } + void wait(const SourceLocation& srcloc, int timeout, const ExpectUnit& u1, const ExpectUnit& u2, const ExpectUnit& u3) { m_units.insert(&u1); m_units.insert(&u2); m_units.insert(&u3); wait(srcloc, timeout); } + void wait(const SourceLocation& srcloc, int timeout, const ExpectUnit& u1, const ExpectUnit& u2, const ExpectUnit& u3, const ExpectUnit& u4) { m_units.insert(&u1); m_units.insert(&u2); m_units.insert(&u3); m_units.insert(&u4); wait(srcloc, timeout); } + void wait(const SourceLocation& srcloc, int timeout, const ExpectUnit& u1, const ExpectUnit& u2, const ExpectUnit& u3, const ExpectUnit& u4, const ExpectUnit& u5) { m_units.insert(&u1); m_units.insert(&u2); m_units.insert(&u3); m_units.insert(&u4); m_units.insert(&u5); wait(srcloc, timeout); } + //! \} + +private: + friend class ExpectUnit; + + void wait(const SourceLocation& srcloc, int timeout); + + QPointer m_subject; + QSet m_units; + ConnectGuard m_guard; + bool m_failed; +}; + +/*! + * Wrapper for Expect::unit() which fills in the source location parameter. + * Returns a new unit of expectation for the subject. + * \param EXP Instance of Expect on which to call unit(). + */ +#define EXPECT_UNIT(EXP) ((EXP).unit(SOURCE_LOCATION)) + +//! \{ +/*! + * Wrapper for Expect::wait() which fills in the source location parameter. + * Allows two or more units of expectation to be waited on simultaneously. + * \param EXP Instance of Expect on which to call wait(). + * \param TIME Time to wait in seconds. Qt event queue is processed when waiting. + */ +#define EXPECT_WAIT_2(EXP, TIME, U1, U2) ((EXP).wait(SOURCE_LOCATION, (TIME), (U1), (U2))) +#define EXPECT_WAIT_3(EXP, TIME, U1, U2, U3) ((EXP).wait(SOURCE_LOCATION, (TIME), (U1), (U2), (U3))) +#define EXPECT_WAIT_4(EXP, TIME, U1, U2, U3, U4) ((EXP).wait(SOURCE_LOCATION, (TIME), (U1), (U2), (U3), (U4))) +#define EXPECT_WAIT_5(EXP, TIME, U1, U2, U3, U4, U5) ((EXP).wait(SOURCE_LOCATION, (TIME), (U1), (U2), (U3), (U4), (U5))) +//! \} + +} //namespace BlackCoreTest + +#endif //BLACKCORETEST_EXPECT_H \ No newline at end of file diff --git a/tests/blackcore/test_blackcore.pro b/tests/blackcore/test_blackcore.pro index 1a681b773..0af06adbc 100644 --- a/tests/blackcore/test_blackcore.pro +++ b/tests/blackcore/test_blackcore.pro @@ -13,10 +13,14 @@ HEADERS += *.h SOURCES += *.cpp LIBS += -L../../lib -lblackcore -lblackmisc +LIBS += -L../../../vatlib -lvatlib win32:!win32-g++*: PRE_TARGETDEPS += ../../lib/blackmisc.lib \ - ../../lib/blackcore.lib + ../../lib/blackcore.lib \ + ../../../vatlib/vatlib.lib else: PRE_TARGETDEPS += ../../lib/libblackmisc.a \ - ../../lib/libblackcore.a + ../../lib/libblackcore.a \ + ../../../vatlib/libvatlib.a + #TODO standardize dependency locations DESTDIR = ../../bin diff --git a/tests/blackcore/testblackcoremain.cpp b/tests/blackcore/testblackcoremain.cpp index 4e1251dea..b7c2fdcea 100644 --- a/tests/blackcore/testblackcoremain.cpp +++ b/tests/blackcore/testblackcoremain.cpp @@ -5,6 +5,7 @@ #include "testblackcoremain.h" #include "testinterpolator.h" +#include "testnetwork.h" #include "testnetmediators.h" namespace BlackCoreTest @@ -20,6 +21,10 @@ int CBlackCoreTestMain::unitMain(int argc, char *argv[]) CTestInterpolator interpolatorTests; status |= QTest::qExec(&interpolatorTests, argc, argv); } + { + CTestNetwork networkTests; + status |= QTest::qExec(&networkTests, argc, argv); + } { CTestNetMediators mediatorTests; status |= QTest::qExec(&mediatorTests, argc, argv); diff --git a/tests/blackcore/testnetwork.cpp b/tests/blackcore/testnetwork.cpp new file mode 100644 index 000000000..944909589 --- /dev/null +++ b/tests/blackcore/testnetwork.cpp @@ -0,0 +1,36 @@ +/* Copyright (C) 2013 VATSIM Community / contributors + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "testnetwork.h" +#include "expect.h" + +using namespace BlackCore; +using namespace BlackMisc; + +void BlackCoreTest::CTestNetwork::networkTest(BlackCore::INetwork* net) +{ + Expect e(net); + QString callsign = "TEST01"; + + EXPECT_UNIT(e) + .send(&INetwork::setServerDetails, "vatsim-germany.org", 6809) + .send(&INetwork::setUserCredentials, "guest", "guest") + .send(&INetwork::setRealName, "Pilot Client Tester") + .send(&INetwork::setCallsign, callsign) + .send(&INetwork::initiateConnection) + .expect(&INetwork::connectionStatusConnecting, []{ qDebug() << "CONNECTING"; }) + .expect(&INetwork::connectionStatusConnected, []{ qDebug() << "CONNECTED"; }) + .wait(10); + + EXPECT_UNIT(e) + .send(&INetwork::ping, "server") + .expect(&INetwork::pong, [](QString s, PhysicalQuantities::CTime t){ qDebug() << "PONG" << s << t; }) + .wait(10); + + EXPECT_UNIT(e) + .send(&INetwork::terminateConnection) + .expect(&INetwork::connectionStatusDisconnected, []{ qDebug() << "DISCONNECTED"; }) + .wait(10); +} \ No newline at end of file diff --git a/tests/blackcore/testnetwork.h b/tests/blackcore/testnetwork.h new file mode 100644 index 000000000..7796ae1bd --- /dev/null +++ b/tests/blackcore/testnetwork.h @@ -0,0 +1,46 @@ +/* Copyright (C) 2013 VATSIM Community / contributors + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef BLACKCORETEST_TESTNETWORK_H +#define BLACKCORETEST_TESTNETWORK_H + +#include "blackcore/network_vatlib.h" +#include + +namespace BlackCoreTest +{ + + /*! + * INetwork implementation classes tests + */ + class CTestNetwork : public QObject + { + Q_OBJECT + + public: + /*! + * Constructor. + * \param parent + */ + explicit CTestNetwork(QObject *parent = 0) : QObject(parent) {} + + private slots: + /*! + * Test NetworkVatlib + */ + void networkVatlibTest() { networkTest(&m_networkVatlib); } + + private: + /*! + * Common part used by all tests. + */ + void networkTest(BlackCore::INetwork *); + + BlackCore::NetworkVatlib m_networkVatlib; + }; + +} //namespace BlackCoreTest + +#endif //BLACKCORETEST_TESTNETWORK_H \ No newline at end of file