syncthingtray/testhelper/helper.h

420 lines
14 KiB
C
Raw Normal View History

#ifndef SYNCTHINGTESTHELPER_H
#define SYNCTHINGTESTHELPER_H
2017-03-09 23:06:03 +01:00
2020-01-10 18:01:37 +01:00
#include "./global.h"
2017-03-09 23:06:03 +01:00
#include <c++utilities/conversion/stringbuilder.h>
#include <c++utilities/tests/testutils.h>
2017-03-09 23:06:03 +01:00
#ifndef SYNCTHINGTESTHELPER_FOR_CLI
2017-03-09 23:06:03 +01:00
#include <cppunit/extensions/HelperMacros.h>
#endif
2017-03-09 23:06:03 +01:00
#include <QEventLoop>
#include <QMetaMethod>
#include <QSet>
2017-05-01 03:34:43 +02:00
#include <QString>
#include <QTimer>
2017-03-09 23:06:03 +01:00
#include <filesystem>
2017-03-09 23:06:03 +01:00
#include <functional>
2017-05-01 03:34:43 +02:00
#include <ostream>
2017-03-09 23:06:03 +01:00
#ifndef SYNCTHINGTESTHELPER_FOR_CLI
2019-06-10 22:48:26 +02:00
#define SYNCTHINGTESTHELPER_TIMEOUT(timeout) static_cast<int>(timeout * ::CppUtilities::timeoutFactor)
#else
#define SYNCTHINGTESTHELPER_TIMEOUT(timeout) timeout
#endif
2019-06-10 22:48:26 +02:00
using namespace CppUtilities;
2017-03-09 23:06:03 +01:00
/*!
* \brief Prints a QString; required to use QString with CPPUNIT_ASSERT_EQUAL_MESSAGE.
*/
2017-05-01 03:34:43 +02:00
inline std::ostream &operator<<(std::ostream &o, const QString &qstring)
2017-03-09 23:06:03 +01:00
{
return o << qstring.toLocal8Bit().data();
2017-03-09 23:06:03 +01:00
}
/*!
2017-09-30 18:51:50 +02:00
* \brief Prints a QStringList; required to use QStringList with CPPUNIT_ASSERT_EQUAL_MESSAGE.
2017-03-09 23:06:03 +01:00
*/
2017-05-01 03:34:43 +02:00
inline std::ostream &operator<<(std::ostream &o, const QStringList &qstringlist)
2017-03-09 23:06:03 +01:00
{
return o << qstringlist.join(QStringLiteral(", ")).toLocal8Bit().data();
2017-03-09 23:06:03 +01:00
}
/*!
2017-09-30 18:51:50 +02:00
* \brief Prints a QSet<QString>; required to use QSet<QString> with CPPUNIT_ASSERT_EQUAL_MESSAGE.
*/
2017-05-01 03:34:43 +02:00
inline std::ostream &operator<<(std::ostream &o, const QSet<QString> &qstringset)
{
return o << qstringset.values().join(QStringLiteral(", ")).toLocal8Bit().data();
}
2019-06-10 22:48:26 +02:00
namespace CppUtilities {
extern SYNCTHINGTESTHELPER_EXPORT double timeoutFactor;
/*!
* \brief Returns the temp directory using "/" consistently and ensuring a trailing "/" is always present.
*/
inline std::string tempDirectory()
{
auto dir = std::filesystem::temp_directory_path().make_preferred().string();
findAndReplace(dir, "\\", "/");
if (!dir.empty() && (dir.back() != '/' || dir.back() != '\\')) {
dir += '/';
}
return dir;
}
2017-03-09 23:06:03 +01:00
/*!
2017-09-30 18:51:50 +02:00
* \brief Waits for the \a duration specified in ms while keeping the event loop running.
2017-03-09 23:06:03 +01:00
*/
inline void wait(int duration)
{
QEventLoop loop;
QTimer::singleShot(SYNCTHINGTESTHELPER_TIMEOUT(duration), &loop, &QEventLoop::quit);
2017-03-09 23:06:03 +01:00
loop.exec();
}
2017-07-02 21:47:59 +02:00
/*!
* \brief Does nothing - meant to be used in waitForSignals() if no action needs to be triggered.
*/
2017-03-09 23:06:03 +01:00
inline void noop()
2017-05-01 03:34:43 +02:00
{
}
2017-03-09 23:06:03 +01:00
/*!
2017-07-02 21:47:59 +02:00
* \brief The TemporaryConnection class disconnects a QMetaObject::Connection when being destroyed.
2017-03-09 23:06:03 +01:00
*/
2017-07-02 22:08:30 +02:00
class TemporaryConnection {
2017-07-02 21:47:59 +02:00
public:
2017-07-02 22:08:30 +02:00
TemporaryConnection(QMetaObject::Connection connection)
: m_connection(connection)
2017-07-02 21:47:59 +02:00
{
}
2017-03-09 23:06:03 +01:00
TemporaryConnection(const TemporaryConnection &other) = delete;
TemporaryConnection(TemporaryConnection &&other)
: m_connection(std::move(other.m_connection))
{
}
2017-07-02 21:47:59 +02:00
~TemporaryConnection()
{
QObject::disconnect(m_connection);
}
2017-03-09 23:06:03 +01:00
2017-07-02 21:47:59 +02:00
private:
QMetaObject::Connection m_connection;
};
/*!
* \brief Returns whether the \a object is actually callable.
*
* This is supposed to be the case if \a object evaluates to true in boolean context (eg. std::function) or
* if there's no conversion to bool (eg. lambda).
*/
template <typename T, Traits::EnableIf<Traits::HasOperatorBool<T>> * = nullptr> inline bool isActuallyCallable(const T &object)
{
return object ? true : false;
}
/*!
* \brief Returns whether the \a object is actually callable.
*
* This is supposed to be the case if \a object evaluates to true in boolean context (eg. std::function) or
* if there's no conversion to bool (eg. lambda).
*/
template <typename T, Traits::DisableIf<Traits::HasOperatorBool<T>> * = nullptr> inline bool isActuallyCallable(const T &)
{
return true;
}
2017-07-02 21:47:59 +02:00
/*!
* \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.
*/
2017-07-02 22:08:30 +02:00
template <typename Signal, typename Handler> class SignalInfo {
2017-07-02 21:47:59 +02:00
public:
/*!
* \brief Constructs a dummy SignalInfo which will never be considered emitted.
*/
SignalInfo()
: m_sender(nullptr)
, m_signal(nullptr)
, m_correctSignalEmitted(nullptr)
, m_signalEmitted(false)
{
}
2017-07-02 21:47:59 +02:00
/*!
* \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(const typename QtPrivate::FunctionPointer<Signal>::Object *sender, Signal signal, const Handler &handler,
bool *correctSignalEmitted = nullptr)
: m_sender(sender)
, m_signal(signal)
, m_correctSignalEmitted(correctSignalEmitted)
, m_signalEmitted(false)
2017-07-02 21:47:59 +02:00
{
// register handler if specified
if (isActuallyCallable(handler)) {
m_handlerConnection = QObject::connect(sender, signal, sender, handler, Qt::DirectConnection);
#ifndef SYNCTHINGTESTHELPER_FOR_CLI
if (!m_handlerConnection) {
2017-07-02 21:47:59 +02:00
CPPUNIT_FAIL(argsToString("Unable to connect signal ", signalName().data(), " to handler"));
}
#endif
2017-07-02 21:47:59 +02:00
}
// register own handler to detect whether signal has been emitted
2024-04-07 23:29:23 +02:00
m_emittedConnection = QObject::connect(
sender, signal, sender, [this] { m_signalEmitted = true; }, Qt::DirectConnection);
#ifndef SYNCTHINGTESTHELPER_FOR_CLI
if (!m_emittedConnection) {
2017-07-02 21:47:59 +02:00
CPPUNIT_FAIL(argsToString("Unable to connect signal ", signalName().data(), " to check for signal emmitation"));
2017-03-09 23:06:03 +01:00
}
#endif
2017-03-09 23:06:03 +01:00
}
2017-07-02 21:47:59 +02:00
SignalInfo(const SignalInfo &other) = delete;
2017-07-02 22:08:30 +02:00
SignalInfo(SignalInfo &&other)
: m_sender(other.m_sender)
, m_signal(other.m_signal)
, m_handlerConnection(other.m_handlerConnection)
, m_emittedConnection(other.m_emittedConnection)
, m_loopConnection(other.m_loopConnection)
, m_correctSignalEmitted(other.m_correctSignalEmitted)
, m_signalEmitted(other.m_signalEmitted)
2017-07-02 21:47:59 +02:00
{
other.m_handlerConnection = other.m_emittedConnection = other.m_loopConnection = QMetaObject::Connection();
2017-07-02 21:47:59 +02:00
}
/*!
* \brief Disconnects any established connections.
*/
~SignalInfo()
{
QObject::disconnect(m_handlerConnection);
QObject::disconnect(m_emittedConnection);
QObject::disconnect(m_loopConnection);
2017-07-02 21:47:59 +02:00
}
/*!
* \brief Returns whether the signal has been emitted.
*/
operator bool() const
{
return (m_correctSignalEmitted && *m_correctSignalEmitted) || (!m_correctSignalEmitted && m_signalEmitted);
2017-03-09 23:06:03 +01:00
}
2017-07-02 21:47:59 +02:00
/*!
* \brief Returns the name of the signal as string.
*/
QByteArray signalName() const
{
return QMetaMethod::fromSignal(m_signal).name();
2017-07-02 21:47:59 +02:00
}
/*!
* \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
{
if (!m_sender) {
return;
}
QObject::disconnect(m_loopConnection);
m_loopConnection = QObject::connect(m_sender, m_signal, loop, &QEventLoop::quit, Qt::DirectConnection);
#ifndef SYNCTHINGTESTHELPER_FOR_CLI
if (!m_loopConnection) {
2017-07-02 21:47:59 +02:00
CPPUNIT_FAIL(argsToString("Unable to connect signal ", signalName().data(), " for waiting"));
}
#endif
2017-07-02 21:47:59 +02:00
}
private:
const typename QtPrivate::FunctionPointer<Signal>::Object *m_sender;
Signal m_signal;
QMetaObject::Connection m_handlerConnection;
QMetaObject::Connection m_emittedConnection;
mutable QMetaObject::Connection m_loopConnection;
bool *m_correctSignalEmitted = nullptr;
bool m_signalEmitted;
2017-07-02 21:47:59 +02:00
};
/*!
* \brief Constructs a new SignalInfo.
*/
2017-07-02 22:08:30 +02:00
template <typename Signal, typename Handler>
inline auto signalInfo(typename QtPrivate::FunctionPointer<Signal>::Object *sender, Signal signal, const Handler &handler = Handler(),
bool *correctSignalEmitted = nullptr)
2017-07-02 21:47:59 +02:00
{
return SignalInfo<Signal, Handler>(sender, signal, handler, correctSignalEmitted);
}
/*!
* \brief Constructs a new SignalInfo.
*/
template <typename Signal> inline auto signalInfo(typename QtPrivate::FunctionPointer<Signal>::Object *sender, Signal signal)
{
return SignalInfo<Signal, std::function<void(void)>>(sender, signal, std::function<void(void)>(), nullptr);
}
/*!
* \brief Constructs a new SignalInfo.
*/
inline auto dummySignalInfo()
{
return SignalInfo<decltype(&QObject::destroyed), std::function<void(void)>>();
}
2017-07-02 21:47:59 +02:00
/*!
2018-10-18 23:34:02 +02:00
* \brief Connects the specified signal info the \a loop via SignalInfo::connectToLoop().
2017-07-02 21:47:59 +02:00
*/
2018-10-18 23:34:02 +02:00
template <typename SignalInfo> inline void connectSignalInfoToLoop(QEventLoop *loop, const SignalInfo &signalInfo)
2017-07-02 21:47:59 +02:00
{
signalInfo.connectToLoop(loop);
}
/*!
2018-10-18 23:34:02 +02:00
* \brief Connects the specified signal info the \a loop via SignalInfo::connectToLoop().
2017-07-02 21:47:59 +02:00
*/
2018-10-18 23:34:02 +02:00
template <typename SignalInfo, typename... Signalinfo>
2020-12-17 17:52:02 +01:00
inline void connectSignalInfoToLoop(QEventLoop *loop, const SignalInfo &firstSignalInfo, const Signalinfo &...remainingSignalinfo)
2017-07-02 21:47:59 +02:00
{
2018-10-18 23:34:02 +02:00
connectSignalInfoToLoop(loop, firstSignalInfo);
connectSignalInfoToLoop(loop, remainingSignalinfo...);
2017-07-02 21:47:59 +02:00
}
/*!
* \brief Checks whether all specified signals have been emitted.
*/
2017-07-02 22:08:30 +02:00
template <typename SignalInfo> inline bool checkWhetherAllSignalsEmitted(const SignalInfo &signalInfo)
2017-07-02 21:47:59 +02:00
{
return signalInfo;
}
/*!
* \brief Checks whether all specified signals have been emitted.
*/
2018-10-18 23:34:02 +02:00
template <typename SignalInfo, typename... Signalinfo>
2020-12-17 17:52:02 +01:00
inline bool checkWhetherAllSignalsEmitted(const SignalInfo &firstSignalInfo, const Signalinfo &...remainingSignalinfo)
2017-07-02 21:47:59 +02:00
{
2018-10-18 23:34:02 +02:00
return firstSignalInfo && checkWhetherAllSignalsEmitted(remainingSignalinfo...);
2017-07-02 21:47:59 +02:00
}
/*!
2018-10-18 23:34:02 +02:00
* \brief Returns the names of all specified signal info which haven't been emitted yet as comma-separated string.
2017-07-02 21:47:59 +02:00
*/
2017-07-02 22:08:30 +02:00
template <typename SignalInfo> inline QByteArray failedSignalNames(const SignalInfo &signalInfo)
2017-07-02 21:47:59 +02:00
{
return !signalInfo ? signalInfo.signalName() : QByteArray();
}
/*!
2018-10-18 23:34:02 +02:00
* \brief Returns the names of all specified signal info which haven't been emitted yet as comma-separated string.
2017-07-02 21:47:59 +02:00
*/
2018-10-18 23:34:02 +02:00
template <typename SignalInfo, typename... Signalinfo>
2020-12-17 17:52:02 +01:00
inline QByteArray failedSignalNames(const SignalInfo &firstSignalInfo, const Signalinfo &...remainingSignalinfo)
2017-07-02 21:47:59 +02:00
{
const QByteArray firstSignalName = failedSignalNames(firstSignalInfo);
2017-07-02 22:08:30 +02:00
if (!firstSignalName.isEmpty()) {
2018-10-18 23:34:02 +02:00
return firstSignalName + ", " + failedSignalNames(remainingSignalinfo...);
2017-07-02 21:47:59 +02:00
} else {
2018-10-18 23:34:02 +02:00
return failedSignalNames(remainingSignalinfo...);
2017-03-09 23:06:03 +01:00
}
2017-07-02 21:47:59 +02:00
}
/*!
* \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.
2018-10-18 23:34:02 +02:00
* \arg signalinfo Specifies the signals to wait for.
2017-07-02 21:47:59 +02:00
* \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.
2018-10-18 23:34:02 +02:00
* \returns Returns true if all \a signalinfo have been omitted before the \a timeout exceeded.
2017-07-02 21:47:59 +02:00
*/
2020-12-17 17:52:02 +01:00
template <typename Action, typename... Signalinfo> bool waitForSignals(Action action, int timeout, const Signalinfo &...signalinfo)
{
2018-10-18 23:34:02 +02:00
return waitForSignalsOrFail(action, timeout, dummySignalInfo(), signalinfo...);
}
/*!
* \brief Waits until the specified signals have been emitted when performing async operations triggered by \a action. Aborts when \a failure is emitted.
* \arg action Specifies a method to trigger the action to run when waiting.
2018-04-01 20:21:51 +02:00
* \arg timeout Specifies the max. time to wait in ms. Set to zero to wait forever.
* \arg failure Specifies the signal indicating an error occurred.
2018-10-18 23:34:02 +02:00
* \arg signalinfo 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.
2018-10-18 23:34:02 +02:00
* \returns Returns true if all \a signalinfo have been omitted before \a failure as been emitted or the \a timeout exceeded.
*/
2018-10-18 23:34:02 +02:00
template <typename Action, typename SignalInfo, typename... Signalinfo>
2020-12-17 17:52:02 +01:00
bool waitForSignalsOrFail(Action action, int timeout, const SignalInfo &failure, const Signalinfo &...signalinfo)
2017-07-02 21:47:59 +02:00
{
// use loop for waiting
QEventLoop loop;
// connect all signals to loop so loop is interrupted when one of the signals is emitted
2018-10-18 23:34:02 +02:00
connectSignalInfoToLoop(&loop, failure);
connectSignalInfoToLoop(&loop, signalinfo...);
2017-03-09 23:06:03 +01:00
// perform specified action
action();
2017-07-02 21:47:59 +02:00
// no reason to enter event loop when all signals have been emitted directly
if (checkWhetherAllSignalsEmitted(failure)) {
return false;
2017-03-09 23:06:03 +01:00
}
2018-10-18 23:34:02 +02:00
if (checkWhetherAllSignalsEmitted(signalinfo...)) {
2018-10-18 23:25:24 +02:00
return true;
}
2017-03-09 23:06:03 +01:00
// also connect and start a timer if a timeout has been specified
QTimer timer;
2017-05-01 03:34:43 +02:00
if (timeout) {
2017-03-09 23:06:03 +01:00
QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit, Qt::DirectConnection);
timer.setSingleShot(true);
timer.setInterval(SYNCTHINGTESTHELPER_TIMEOUT(timeout));
2017-03-09 23:06:03 +01:00
timer.start();
}
// exec event loop as long as the right signal has not been emitted yet and there is still time
bool allSignalsEmitted = false, failureEmitted = false;
2017-07-02 21:47:59 +02:00
do {
2017-03-09 23:06:03 +01:00
loop.exec();
2018-10-18 23:34:02 +02:00
} while (!(failureEmitted = checkWhetherAllSignalsEmitted(failure)) && !(allSignalsEmitted = checkWhetherAllSignalsEmitted(signalinfo...))
&& (!timeout || timer.isActive()));
2017-03-09 23:06:03 +01:00
// check whether a timeout occurred
const bool timeoutFailed(!allSignalsEmitted && timeout && !timer.isActive());
#ifndef SYNCTHINGTESTHELPER_FOR_CLI
if (failureEmitted) {
CPPUNIT_FAIL(
argsToString("Signal(s) ", failedSignalNames(signalinfo...).data(), " has/have not emitted before ", failure.signalName().data(), '.'));
} else if (timeoutFailed) {
CPPUNIT_FAIL(argsToString("Signal(s) ", failedSignalNames(signalinfo...).data(), " has/have not emitted within at least ", timer.interval(),
" ms (set environment variable SYNCTHING_TEST_TIMEOUT_FACTOR to increase the timeout).",
timeoutFactor != 1.0 ? argsToString(" (original timeout: ", timeout, " ms)") : std::string()));
2017-03-09 23:06:03 +01:00
}
#endif
return !failureEmitted && !timeoutFailed;
2017-03-09 23:06:03 +01:00
}
2019-06-10 22:48:26 +02:00
} // namespace CppUtilities
2017-03-09 23:06:03 +01:00
#endif // SYNCTHINGTESTHELPER_H