connector: Refactor and improve tests

This commit is contained in:
Martchus 2017-07-02 21:47:59 +02:00
parent 04c9caf7d4
commit 55757b1e57
3 changed files with 343 additions and 98 deletions

View File

@ -21,6 +21,8 @@ QT_FORWARD_DECLARE_CLASS(QUrlQuery)
QT_FORWARD_DECLARE_CLASS(QJsonObject)
QT_FORWARD_DECLARE_CLASS(QJsonArray)
class ConnectionTests;
namespace Data {
#undef Q_NAMESPACE
#define Q_NAMESPACE
@ -53,6 +55,8 @@ struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingLogEntry {
};
class LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingConnection : public QObject {
friend ConnectionTests;
Q_OBJECT
Q_PROPERTY(QString syncthingUrl READ syncthingUrl WRITE setSyncthingUrl)
Q_PROPERTY(QByteArray apiKey READ apiKey WRITE setApiKey)

View File

@ -8,7 +8,9 @@
#include <cppunit/TestFixture.h>
#include <QDir>
#include <QJsonArray>
#include <QStringBuilder>
#include <QJsonDocument>
using namespace std;
using namespace Data;
@ -30,18 +32,33 @@ public:
void testConnection();
void testErrorCases();
void testInitialConnection();
void checkDevices();
void checkDirectories() const;
void testReconnecting();
void testResumingAllDevices();
void testResumingDirectory();
void testPausingDirectory();
void testDisconnecting();
void setUp();
void tearDown();
private:
template <typename Signal, typename Action, typename Handler = function<void(void)>>
void waitForConnection(Signal signal, Action action, Handler handler = nullptr, bool *ok = nullptr, int timeout = 1000);
template <typename Signal, typename Action, typename Handler = function<void(void)>>
void waitForConnectionAnyAction(Signal signal, Action action, Handler handler = nullptr, bool *ok = nullptr, int timeout = 1000);
template <typename Handler> QMetaObject::Connection handleNewDevices(Handler handler, bool *ok);
template <typename Handler> QMetaObject::Connection handleNewDirs(Handler handler, bool *ok);
template <typename Action, typename... SignalInfos>
void waitForConnection(Action action, int timeout, const SignalInfos &... signalInfos);
template<typename Signal, typename Handler = function<void(void)>>
SignalInfo<Signal, Handler> connectionSignal(Signal signal, const Handler &handler = function<void(void)>(), bool *correctSignalEmitted = nullptr);
static void (SyncthingConnection::*defaultConnect())(void);
static void (SyncthingConnection::*defaultReconnect())(void);
static void (SyncthingConnection::*defaultDisconnect())(void);
template <typename Handler> TemporaryConnection handleNewDevices(Handler handler, bool *ok);
template <typename Handler> TemporaryConnection handleNewDirs(Handler handler, bool *ok);
SyncthingConnection m_connection;
QString m_ownDevId;
};
CPPUNIT_TEST_SUITE_REGISTRATION(ConnectionTests);
@ -67,6 +84,10 @@ void ConnectionTests::setUp()
// keep track of status changes
QObject::connect(&m_connection, &SyncthingConnection::statusChanged,
[this] { cerr << " - Connection status changed to: " << m_connection.statusText().toLocal8Bit().data() << endl; });
// log configuration change
QObject::connect(&m_connection, &SyncthingConnection::newConfig,
[] (const QJsonObject &config) { cerr << " - New config: " << QJsonDocument(config).toJson(QJsonDocument::Compact).data() << endl; });
}
/*!
@ -84,25 +105,37 @@ void ConnectionTests::tearDown()
/*!
* \brief Variant of waitForSignal() where sender is the connection and the action is a method of the connection.
*/
template <typename Signal, typename Action, typename Handler>
void ConnectionTests::waitForConnection(Signal signal, Action action, Handler handler, bool *ok, int timeout)
template <typename Action, typename... SignalInfos>
void ConnectionTests::waitForConnection(Action action, int timeout, const SignalInfos &...signalInfos)
{
waitForSignal(&m_connection, signal, bind(action, &m_connection), timeout, handler, ok);
waitForSignals(bind(action, &m_connection), timeout, signalInfos...);
}
/*!
* \brief Variant of waitForSignal() where sender is the connection.
*/
template <typename Signal, typename Action, typename Handler>
void ConnectionTests::waitForConnectionAnyAction(Signal signal, Action action, Handler handler, bool *ok, int timeout)
template<typename Signal, typename Handler>
SignalInfo<Signal, Handler> ConnectionTests::connectionSignal(Signal signal, const Handler &handler, bool *correctSignalEmitted)
{
waitForSignal(&m_connection, signal, action, timeout, handler, ok);
return SignalInfo<Signal, Handler>(&m_connection, signal, handler, correctSignalEmitted);
}
void (SyncthingConnection::*ConnectionTests::defaultConnect())(void)
{
return static_cast<void(SyncthingConnection::*)(void)>(&SyncthingConnection::connect);
}
void (SyncthingConnection::*ConnectionTests::defaultReconnect())(void)
{
return static_cast<void(SyncthingConnection::*)(void)>(&SyncthingConnection::reconnect);
}
void (SyncthingConnection::*ConnectionTests::defaultDisconnect())(void)
{
return static_cast<void(SyncthingConnection::*)(void)>(&SyncthingConnection::disconnect);
}
/*!
* \brief Helps handling newDevices() signal when waiting for device change.
*/
template <typename Handler> QMetaObject::Connection ConnectionTests::handleNewDevices(Handler handler, bool *ok)
template <typename Handler> TemporaryConnection ConnectionTests::handleNewDevices(Handler handler, bool *ok)
{
return QObject::connect(&m_connection, &SyncthingConnection::newDevices, [ok, &handler](const std::vector<SyncthingDev> &devs) {
for (const SyncthingDev &dev : devs) {
@ -114,7 +147,7 @@ template <typename Handler> QMetaObject::Connection ConnectionTests::handleNewDe
/*!
* \brief Helps handling newDirs() signal when waiting for directory change.
*/
template <typename Handler> QMetaObject::Connection ConnectionTests::handleNewDirs(Handler handler, bool *ok)
template <typename Handler> TemporaryConnection ConnectionTests::handleNewDirs(Handler handler, bool *ok)
{
return QObject::connect(&m_connection, &SyncthingConnection::newDirs, [ok, &handler](const std::vector<SyncthingDir> &dirs) {
for (const SyncthingDir &dir : dirs) {
@ -132,9 +165,26 @@ template <typename Handler> QMetaObject::Connection ConnectionTests::handleNewDi
*/
void ConnectionTests::testConnection()
{
cerr << "\n - Connecting initially ..." << endl;
testErrorCases();
testInitialConnection();
checkDevices();
checkDirectories();
testReconnecting();
testResumingAllDevices();
testResumingDirectory();
testPausingDirectory();
testDisconnecting();
}
// error in case of inavailability and wrong API key
void ConnectionTests::testErrorCases()
{
cerr << "\n - Error handling in case of insufficient conficuration ..." << endl;
waitForConnection(defaultConnect(), 1000,
connectionSignal(&SyncthingConnection::error, [](const QString &errorMessage) {
CPPUNIT_ASSERT_EQUAL(QStringLiteral("Connection configuration is insufficient."), errorMessage);
}));
cerr << "\n - Error handling in case of inavailability and wrong API key ..." << endl;
m_connection.setApiKey(QByteArray("wrong API key"));
bool syncthingAvailable = false;
const function<void(const QString &errorMessage)> errorHandler = [this, &syncthingAvailable](const QString &errorMessage) {
@ -153,24 +203,29 @@ void ConnectionTests::testConnection()
}
};
while (!syncthingAvailable) {
waitForConnection(&SyncthingConnection::error, static_cast<void (SyncthingConnection::*)(void)>(&SyncthingConnection::connect), errorHandler);
waitForConnection(defaultConnect(), 5000, connectionSignal(&SyncthingConnection::error, errorHandler));
}
}
// initial connection
void ConnectionTests::testInitialConnection()
{
cerr << "\n - Connecting initially ..." << endl;
m_connection.setApiKey(apiKey().toUtf8());
waitForConnection(&SyncthingConnection::statusChanged, static_cast<void (SyncthingConnection::*)(void)>(&SyncthingConnection::connect));
waitForConnection(defaultConnect(), 5000, connectionSignal(&SyncthingConnection::statusChanged));
CPPUNIT_ASSERT_EQUAL_MESSAGE(
"connected and paused (one dev is initially paused)", QStringLiteral("connected, paused"), m_connection.statusText());
CPPUNIT_ASSERT_MESSAGE("no dirs out-of-sync", !m_connection.hasOutOfSyncDirs());
}
// devs present
void ConnectionTests::checkDevices()
{
const auto &devInfo = m_connection.devInfo();
CPPUNIT_ASSERT_EQUAL_MESSAGE("3 devs present", 3_st, devInfo.size());
QString ownDevId;
for (const SyncthingDev &dev : devInfo) {
if (dev.id != QStringLiteral("MMGUI6U-WUEZQCP-XZZ6VYB-LCT4TVC-ER2HAVX-QYT6X7D-S6ZSG2B-323KLQ7")
&& dev.id != QStringLiteral("6EIS2PN-J2IHWGS-AXS3YUL-HC5FT3K-77ZXTLL-AKQLJ4C-7SWVPUS-AZW4RQ4")) {
CPPUNIT_ASSERT_EQUAL_MESSAGE("own device", QStringLiteral("own device"), dev.statusString());
ownDevId = dev.id;
m_ownDevId = dev.id;
}
}
for (const SyncthingDev &dev : devInfo) {
@ -188,8 +243,10 @@ void ConnectionTests::testConnection()
CPPUNIT_ASSERT_EQUAL(QStringLiteral("dynamic"), dev.addresses.front());
}
}
}
// dirs present
void ConnectionTests::checkDirectories() const
{
const auto &dirInfo = m_connection.dirInfo();
CPPUNIT_ASSERT_EQUAL_MESSAGE("2 dirs present", 2_st, dirInfo.size());
const SyncthingDir &dir1 = dirInfo.front();
@ -200,7 +257,7 @@ void ConnectionTests::testConnection()
CPPUNIT_ASSERT(!dir1.readOnly);
CPPUNIT_ASSERT(!dir1.paused);
CPPUNIT_ASSERT_EQUAL(dir1.devices.toSet(), QSet<QString>({ QStringLiteral("MMGUI6U-WUEZQCP-XZZ6VYB-LCT4TVC-ER2HAVX-QYT6X7D-S6ZSG2B-323KLQ7"),
QStringLiteral("6EIS2PN-J2IHWGS-AXS3YUL-HC5FT3K-77ZXTLL-AKQLJ4C-7SWVPUS-AZW4RQ4"), ownDevId }));
QStringLiteral("6EIS2PN-J2IHWGS-AXS3YUL-HC5FT3K-77ZXTLL-AKQLJ4C-7SWVPUS-AZW4RQ4"), m_ownDevId }));
const SyncthingDir &dir2 = dirInfo.back();
CPPUNIT_ASSERT_EQUAL(QStringLiteral("test2"), dir2.id);
CPPUNIT_ASSERT_EQUAL(QStringLiteral("Test dir 2"), dir2.label);
@ -209,58 +266,83 @@ void ConnectionTests::testConnection()
CPPUNIT_ASSERT(!dir2.readOnly);
CPPUNIT_ASSERT(dir2.paused);
CPPUNIT_ASSERT_EQUAL(
dir2.devices.toSet(), QSet<QString>({ QStringLiteral("MMGUI6U-WUEZQCP-XZZ6VYB-LCT4TVC-ER2HAVX-QYT6X7D-S6ZSG2B-323KLQ7"), ownDevId }));
// reconnecting
dir2.devices.toSet(), QSet<QString>({ QStringLiteral("MMGUI6U-WUEZQCP-XZZ6VYB-LCT4TVC-ER2HAVX-QYT6X7D-S6ZSG2B-323KLQ7"), m_ownDevId }));
}
void ConnectionTests::testReconnecting()
{
cerr << "\n - Reconnecting ..." << endl;
waitForConnection(&SyncthingConnection::statusChanged, static_cast<void (SyncthingConnection::*)(void)>(&SyncthingConnection::reconnect));
waitForConnection(defaultReconnect(), 1000, connectionSignal(&SyncthingConnection::statusChanged));
CPPUNIT_ASSERT_EQUAL_MESSAGE("reconnecting", QStringLiteral("reconnecting"), m_connection.statusText());
waitForConnectionAnyAction(&SyncthingConnection::statusChanged, noop);
waitForSignals(noop, 1000, connectionSignal(&SyncthingConnection::statusChanged));
if (m_connection.isConnected() && m_connection.status() != SyncthingStatus::Paused) {
// FIXME: Maybe it takes one further update to recon paused dev?
waitForConnectionAnyAction(&SyncthingConnection::statusChanged, noop);
// FIXME: Maybe it takes one further update to recognize paused dev?
waitForSignals(noop, 1000, connectionSignal(&SyncthingConnection::statusChanged));
}
CPPUNIT_ASSERT_EQUAL_MESSAGE("connected again", QStringLiteral("connected, paused"), m_connection.statusText());
}
cerr << "\n - Pausing/resuming devs/dirs ..." << endl;
// resume all devs
void ConnectionTests::testResumingAllDevices()
{
cerr << "\n - Resuming all devices ..." << endl;
bool devResumed = false;
const function<void(const SyncthingDev &, int)> devResumedHandler = [&devResumed](const SyncthingDev &dev, int) {
if (dev.name == QStringLiteral("Test dev 2") && !dev.paused) {
devResumed = true;
}
};
const auto newDevsConnection1 = handleNewDevices(devResumedHandler, &devResumed);
waitForConnection(&SyncthingConnection::devStatusChanged, &SyncthingConnection::resumeAllDevs, devResumedHandler, &devResumed);
QObject::disconnect(newDevsConnection1);
const function<void(const QStringList &)> devResumedTriggeredHandler = [this](const QStringList &devIds) {
CPPUNIT_ASSERT_EQUAL(m_connection.deviceIds(), devIds);
};
const auto newDevsConnection = handleNewDevices(devResumedHandler, &devResumed);
waitForConnection(&SyncthingConnection::resumeAllDevs, 1000,
connectionSignal(&SyncthingConnection::devStatusChanged, devResumedHandler, &devResumed),
connectionSignal(&SyncthingConnection::deviceResumeTriggered, devResumedTriggeredHandler));
CPPUNIT_ASSERT(devResumed);
CPPUNIT_ASSERT_EQUAL_MESSAGE("not paused anymore", QStringLiteral("connected"), m_connection.statusText());
for (const QJsonValue &devValue : m_connection.m_rawConfig.value(QStringLiteral("devices")).toArray()) {
const QJsonObject &devObj(devValue.toObject());
CPPUNIT_ASSERT(!devObj.isEmpty());
CPPUNIT_ASSERT_MESSAGE("raw config updated accordingly", !devObj.value(QStringLiteral("paused")).toBool(true));
}
}
// resume all dirs
void ConnectionTests::testResumingDirectory()
{
cerr << "\n - Resuming all dirs ..." << endl;
bool dirResumed = false;
const function<void(const SyncthingDir &, int)> dirResumedHandler = [&dirResumed](const SyncthingDir &dir, int) {
if (dir.id == QStringLiteral("test2") && !dir.paused) {
dirResumed = true;
}
};
const auto newDirsConnection1 = handleNewDirs(dirResumedHandler, &dirResumed);
waitForConnection(&SyncthingConnection::dirStatusChanged, &SyncthingConnection::resumeAllDirs, dirResumedHandler, &dirResumed);
QObject::disconnect(newDirsConnection1);
const auto newDirsConnection = handleNewDirs(dirResumedHandler, &dirResumed);
waitForConnection(&SyncthingConnection::resumeAllDirs, 1000, connectionSignal(&SyncthingConnection::dirStatusChanged, dirResumedHandler, &dirResumed));
CPPUNIT_ASSERT(dirResumed);
CPPUNIT_ASSERT_EQUAL_MESSAGE("still 2 dirs present", 2_st, m_connection.dirInfo().size());
CPPUNIT_ASSERT_EQUAL_MESSAGE("still not paused anymore", QStringLiteral("connected"), m_connection.statusText());
}
// pause dir 1
void ConnectionTests::testPausingDirectory()
{
cerr << "\n - Pause dir 1 ..." << endl;
bool dirPaused = false;
const function<void(const SyncthingDir &, int)> dirPausedHandler = [&dirPaused](const SyncthingDir &dir, int) {
if (dir.id == QStringLiteral("test1") && dir.paused) {
dirPaused = true;
}
};
const auto newDirsConnection2 = handleNewDirs(dirPausedHandler, &dirPaused);
waitForConnectionAnyAction(&SyncthingConnection::dirStatusChanged,
bind(&SyncthingConnection::pauseDirectories, &m_connection, QStringList({ QStringLiteral("test1") })), dirPausedHandler, &dirPaused);
QObject::disconnect(newDirsConnection2);
const auto newDirsConnection = handleNewDirs(dirPausedHandler, &dirPaused);
waitForSignals(bind(&SyncthingConnection::pauseDirectories, &m_connection, QStringList({ QStringLiteral("test1") })), 1000,
connectionSignal(&SyncthingConnection::dirStatusChanged, dirPausedHandler, &dirPaused));
CPPUNIT_ASSERT(dirPaused);
CPPUNIT_ASSERT_EQUAL_MESSAGE("still 2 dirs present", 2_st, m_connection.dirInfo().size());
CPPUNIT_ASSERT_EQUAL_MESSAGE("still not paused anymore", QStringLiteral("connected"), m_connection.statusText());
}
// disconnecting
void ConnectionTests::testDisconnecting()
{
cerr << "\n - Disconnecting ..." << endl;
waitForConnection(&SyncthingConnection::statusChanged, static_cast<void (SyncthingConnection::*)(void)>(&SyncthingConnection::disconnect));
waitForConnection(defaultDisconnect(), 1000, connectionSignal(&SyncthingConnection::statusChanged));
CPPUNIT_ASSERT_EQUAL_MESSAGE("disconnected", QStringLiteral("disconnected"), m_connection.statusText());
}

View File

@ -44,7 +44,7 @@ inline std::ostream &operator<<(std::ostream &o, const QSet<QString> &qstringset
}
/*!
* \brief Waits for the in ms specified \a duration keeping the event loop running.
* \brief Waits for the\a duration specified in ms while keeping the event loop running.
*/
inline void wait(int duration)
{
@ -53,62 +53,227 @@ inline void wait(int duration)
loop.exec();
}
/*!
* \brief Does nothing - meant to be used in waitForSignals() if no action needs to be triggered.
*/
inline void noop()
{
}
/*!
* \brief Waits until the \a signal is emitted by \a sender when performing \a action and connects \a signal with \a handler if specified.
* \arg sender Specifies the sender which is assumed to emit \a signal.
* \arg signal Specifies the signal.
* \arg action Specifies the action to be invoked when waiting.
* \arg timeout Specifies the max. time to wait.
* \arg handler Specifies a handler which will be also connected to \a signal.
* \arg ok Specifies whether the correct signal has been emitted. Should be set in \a handler to indicate that the emitted signal is actually the one
* the test is waiting for (and not just one which has been emitted as side-effect).
* \throws Fails if \a signal is not emitted in at least \a timeout milliseconds or when at least one of the required
* connections can not be established.
* \remarks The handler is disconnected before the function returns.
* \brief The TemporaryConnection class disconnects a QMetaObject::Connection when being destroyed.
*/
template <typename Signal, typename Action, typename Handler = std::function<void(void)>>
void waitForSignal(typename QtPrivate::FunctionPointer<Signal>::Object *sender, Signal signal, Action action, int timeout = 2500,
Handler handler = nullptr, bool *ok = nullptr)
class TemporaryConnection
{
// determine name of the signal for error messages
const QByteArray signalName(QMetaMethod::fromSignal(signal).name());
public:
TemporaryConnection(QMetaObject::Connection connection) :
m_connection(connection)
{
}
// use loop for waiting
QEventLoop loop;
~TemporaryConnection()
{
QObject::disconnect(m_connection);
}
// if specified, connect handler to signal
QMetaObject::Connection handlerConnection;
if (handler) {
handlerConnection = QObject::connect(sender, signal, sender, handler, Qt::DirectConnection);
if (!handlerConnection) {
CPPUNIT_FAIL(argsToString("Unable to connect signal ", signalName.data(), " to handler"));
private:
QMetaObject::Connection m_connection;
};
/*!
* \brief The SignalInfo class represents a connection of a signal with a handler.
*
* SignalInfo objects are meant to be passed to waitForSignals() so the function can keep track
* of emitted signals.
*/
template<typename Signal, typename Handler>
class SignalInfo
{
public:
/*!
* \brief Constructs a SignalInfo with handler and automatically connects the handler to the signal.
* \param sender Specifies the object which will emit \a signal.
* \param signal Specifies the signal.
* \param handler Specifies a handler to be connected to \a signal.
* \param correctSignalEmitted Specifies whether the correct signal has been emitted. Should be set in \a handler to indicate that the emitted signal is actually the one
* the test is waiting for (and not just one which has been emitted as side-effect).
*/
SignalInfo(typename QtPrivate::FunctionPointer<Signal>::Object *sender, Signal signal, const Handler &handler, bool *correctSignalEmitted = nullptr) :
sender(sender),
signal(signal),
correctSignalEmitted(correctSignalEmitted),
signalEmitted(false)
{
// register handler if specified
if(handler) {
handlerConnection = QObject::connect(sender, signal, sender, handler, Qt::DirectConnection);
if (!handlerConnection) {
CPPUNIT_FAIL(argsToString("Unable to connect signal ", signalName().data(), " to handler"));
}
}
// register own handler to detect whether signal has been emitted
emittedConnection = QObject::connect(sender, signal, sender, [this] {
signalEmitted = true;
}, Qt::DirectConnection);
if (!emittedConnection) {
CPPUNIT_FAIL(argsToString("Unable to connect signal ", signalName().data(), " to check for signal emmitation"));
}
}
// connect the signal to the quit slot of the loop
if (!QObject::connect(sender, signal, &loop, &QEventLoop::quit, Qt::DirectConnection)) {
CPPUNIT_FAIL(argsToString("Unable to connect signal ", signalName.data(), " for waiting"));
SignalInfo(const SignalInfo &other) = delete;
SignalInfo(SignalInfo &&other) :
sender(other.sender),
signal(other.signal),
handlerConnection(other.handlerConnection),
emittedConnection(other.emittedConnection),
loopConnection(other.loopConnection),
correctSignalEmitted(other.correctSignalEmitted),
signalEmitted(other.signalEmitted)
{
other.handlerConnection = other.emittedConnection = other.loopConnection = QMetaObject::Connection();
}
// handle case when signal is directly emitted
bool signalDirectlyEmitted = false;
QMetaObject::Connection signalDirectlyEmittedConnection
= QObject::connect(sender, signal, sender, [&signalDirectlyEmitted] { signalDirectlyEmitted = true; }, Qt::DirectConnection);
if (!signalDirectlyEmittedConnection) {
CPPUNIT_FAIL(argsToString("Unable to connect signal ", signalName.data(), " to check for direct emmitation"));
/*!
* \brief Disconnects any established connections.
*/
~SignalInfo()
{
QObject::disconnect(handlerConnection);
QObject::disconnect(emittedConnection);
QObject::disconnect(loopConnection);
}
/*!
* \brief Returns whether the signal has been emitted.
*/
operator bool() const
{
return (correctSignalEmitted && *correctSignalEmitted) || (!correctSignalEmitted && signalEmitted);
}
/*!
* \brief Returns the name of the signal as string.
*/
QByteArray signalName() const
{
return QMetaMethod::fromSignal(signal).name();
}
/*!
* \brief Connects the signal to the specified \a loop so the loop is being interrupted when the signal
* has been emitted.
*/
void connectToLoop(QEventLoop *loop) const
{
QObject::disconnect(loopConnection);
loopConnection = QObject::connect(sender, signal, loop, &QEventLoop::quit, Qt::DirectConnection);
if (!loopConnection) {
CPPUNIT_FAIL(argsToString("Unable to connect signal ", signalName().data(), " for waiting"));
}
}
private:
typename QtPrivate::FunctionPointer<Signal>::Object *sender;
Signal signal;
QMetaObject::Connection handlerConnection;
QMetaObject::Connection emittedConnection;
mutable QMetaObject::Connection loopConnection;
bool *correctSignalEmitted = nullptr;
bool signalEmitted;
};
/*!
* \brief Constructs a new SignalInfo.
*/
template<typename Signal, typename Handler>
inline SignalInfo<Signal, Handler> signalInfo(typename QtPrivate::FunctionPointer<Signal>::Object *sender, Signal signal, const Handler &handler = Handler(), bool *correctSignalEmitted = nullptr)
{
return SignalInfo<Signal, Handler>(sender, signal, handler, correctSignalEmitted);
}
/*!
* \brief Connects the specified signal infos the \a loop via SignalInfo::connectToLoop().
*/
template <typename SignalInfo>
inline void connectSignalInfosToLoop(QEventLoop *loop, const SignalInfo &signalInfo)
{
signalInfo.connectToLoop(loop);
}
/*!
* \brief Connects the specified signal infos the \a loop via SignalInfo::connectToLoop().
*/
template <typename SignalInfo, typename... SignalInfos>
inline void connectSignalInfosToLoop(QEventLoop *loop, const SignalInfo &firstSignalInfo, const SignalInfos &... remainingSignalInfos)
{
connectSignalInfosToLoop(loop, firstSignalInfo);
connectSignalInfosToLoop(loop, remainingSignalInfos...);
}
/*!
* \brief Checks whether all specified signals have been emitted.
*/
template <typename SignalInfo>
inline bool checkWhetherAllSignalsEmitted(const SignalInfo &signalInfo)
{
return signalInfo;
}
/*!
* \brief Checks whether all specified signals have been emitted.
*/
template <typename SignalInfo, typename... SignalInfos>
inline bool checkWhetherAllSignalsEmitted(const SignalInfo &firstSignalInfo, const SignalInfos &... remainingSignalInfos)
{
return firstSignalInfo && checkWhetherAllSignalsEmitted(remainingSignalInfos...);
}
/*!
* \brief Returns the names of all specified signal infos which haven't been emitted yet as comma-separated string.
*/
template <typename SignalInfo>
inline QByteArray failedSignalNames(const SignalInfo &signalInfo)
{
return !signalInfo ? signalInfo.signalName() : QByteArray();
}
/*!
* \brief Returns the names of all specified signal infos which haven't been emitted yet as comma-separated string.
*/
template <typename SignalInfo, typename... SignalInfos>
inline QByteArray failedSignalNames(const SignalInfo &firstSignalInfo, const SignalInfos &... remainingSignalInfos)
{
const QByteArray firstSignalName = failedSignalNames(firstSignalInfo);
if(!firstSignalName.isEmpty()) {
return firstSignalName + ", " + failedSignalNames(remainingSignalInfos...);
} else {
return failedSignalNames(remainingSignalInfos...);
}
}
/*!
* \brief Waits until the specified signals have been emitted when performing async operations triggered by \a action.
* \arg action Specifies a method to trigger the action to run when waiting.
* \arg timeout Specifies the max. time to wait. Set to zero to wait forever.
* \arg signalInfos Specifies the signals to wait for.
* \throws Fails if not all signals have been emitted in at least \a timeout milliseconds or when at least one of the
* required connections can not be established.
*/
template <typename Action, typename... SignalInfos>
void waitForSignals(Action action, int timeout, const SignalInfos &... signalInfos)
{
// use loop for waiting
QEventLoop loop;
// connect all signals to loop so loop is interrupted when one of the signals is emitted
connectSignalInfosToLoop(&loop, signalInfos...);
// perform specified action
action();
// no reason to enter event loop when signal has been emitted directly
if ((!ok || *ok) && signalDirectlyEmitted) {
QObject::disconnect(signalDirectlyEmittedConnection);
QObject::disconnect(handlerConnection);
// no reason to enter event loop when all signals have been emitted directly
if(checkWhetherAllSignalsEmitted(signalInfos...)) {
return;
}
@ -122,21 +287,15 @@ void waitForSignal(typename QtPrivate::FunctionPointer<Signal>::Object *sender,
}
// exec event loop as long as the right signal has not been emitted yet and there is still time
if (!ok) {
bool allSignalsEmitted = false;
do {
loop.exec();
} else {
while (!*ok && (!timeout || timer.isActive())) {
loop.exec();
}
}
} while (!(allSignalsEmitted = checkWhetherAllSignalsEmitted(signalInfos...)) && (!timeout || timer.isActive()));
// check whether a timeout occured
if ((!ok || !*ok) && timeout && !timer.isActive()) {
CPPUNIT_FAIL(argsToString("Signal ", signalName.data(), " has not emmitted within at least ", timeout, " ms."));
if (!allSignalsEmitted && timeout && !timer.isActive()) {
CPPUNIT_FAIL(argsToString("Signal(s) ", failedSignalNames(signalInfos...).data(), " has/have not emmitted within at least ", timeout, " ms."));
}
QObject::disconnect(signalDirectlyEmittedConnection);
QObject::disconnect(handlerConnection);
}
#endif // SYNCTHINGTESTHELPER_H