#ifndef SYNCTHINGTESTHELPER_H #define SYNCTHINGTESTHELPER_H #include "./global.h" #include #include #ifndef SYNCTHINGTESTHELPER_FOR_CLI #include #endif #include #include #include #include #include #include #include #include #ifndef SYNCTHINGTESTHELPER_FOR_CLI #define SYNCTHINGTESTHELPER_TIMEOUT(timeout) static_cast(timeout * ::CppUtilities::timeoutFactor) #else #define SYNCTHINGTESTHELPER_TIMEOUT(timeout) timeout #endif using namespace CppUtilities; /*! * \brief Prints a QString; required to use QString with CPPUNIT_ASSERT_EQUAL_MESSAGE. */ inline std::ostream &operator<<(std::ostream &o, const QString &qstring) { return o << qstring.toLocal8Bit().data(); } /*! * \brief Prints a QStringList; required to use QStringList with CPPUNIT_ASSERT_EQUAL_MESSAGE. */ inline std::ostream &operator<<(std::ostream &o, const QStringList &qstringlist) { return o << qstringlist.join(QStringLiteral(", ")).toLocal8Bit().data(); } /*! * \brief Prints a QSet; required to use QSet with CPPUNIT_ASSERT_EQUAL_MESSAGE. */ inline std::ostream &operator<<(std::ostream &o, const QSet &qstringset) { return o << qstringset.values().join(QStringLiteral(", ")).toLocal8Bit().data(); } 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; } /*! * \brief Waits for the \a duration specified in ms while keeping the event loop running. */ inline void wait(int duration) { QEventLoop loop; QTimer::singleShot(SYNCTHINGTESTHELPER_TIMEOUT(duration), &loop, &QEventLoop::quit); loop.exec(); } /*! * \brief Does nothing - meant to be used in waitForSignals() if no action needs to be triggered. */ inline void noop() { } /*! * \brief The TemporaryConnection class disconnects a QMetaObject::Connection when being destroyed. */ class TemporaryConnection { public: TemporaryConnection(QMetaObject::Connection connection) : m_connection(connection) { } TemporaryConnection(const TemporaryConnection &other) = delete; TemporaryConnection(TemporaryConnection &&other) : m_connection(std::move(other.m_connection)) { } ~TemporaryConnection() { QObject::disconnect(m_connection); } 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 > * = 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 > * = nullptr> inline bool isActuallyCallable(const T &) { return true; } /*! * \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 class SignalInfo { 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) { } /*! * \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::Object *sender, Signal signal, const Handler &handler, bool *correctSignalEmitted = nullptr) : m_sender(sender) , m_signal(signal) , m_correctSignalEmitted(correctSignalEmitted) , m_signalEmitted(false) { // register handler if specified if (isActuallyCallable(handler)) { m_handlerConnection = QObject::connect(sender, signal, sender, handler, Qt::DirectConnection); #ifndef SYNCTHINGTESTHELPER_FOR_CLI if (!m_handlerConnection) { CPPUNIT_FAIL(argsToString("Unable to connect signal ", signalName().data(), " to handler")); } #endif } // register own handler to detect whether signal has been emitted m_emittedConnection = QObject::connect( sender, signal, sender, [this] { m_signalEmitted = true; }, Qt::DirectConnection); #ifndef SYNCTHINGTESTHELPER_FOR_CLI if (!m_emittedConnection) { CPPUNIT_FAIL(argsToString("Unable to connect signal ", signalName().data(), " to check for signal emmitation")); } #endif } SignalInfo(const SignalInfo &other) = delete; 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) { other.m_handlerConnection = other.m_emittedConnection = other.m_loopConnection = QMetaObject::Connection(); } /*! * \brief Disconnects any established connections. */ ~SignalInfo() { QObject::disconnect(m_handlerConnection); QObject::disconnect(m_emittedConnection); QObject::disconnect(m_loopConnection); } /*! * \brief Returns whether the signal has been emitted. */ operator bool() const { return (m_correctSignalEmitted && *m_correctSignalEmitted) || (!m_correctSignalEmitted && m_signalEmitted); } /*! * \brief Returns the name of the signal as string. */ QByteArray signalName() const { return QMetaMethod::fromSignal(m_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 { 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) { CPPUNIT_FAIL(argsToString("Unable to connect signal ", signalName().data(), " for waiting")); } #endif } private: const typename QtPrivate::FunctionPointer::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; }; /*! * \brief Constructs a new SignalInfo. */ template inline auto signalInfo(typename QtPrivate::FunctionPointer::Object *sender, Signal signal, const Handler &handler = Handler(), bool *correctSignalEmitted = nullptr) { return SignalInfo(sender, signal, handler, correctSignalEmitted); } /*! * \brief Constructs a new SignalInfo. */ template inline auto signalInfo(typename QtPrivate::FunctionPointer::Object *sender, Signal signal) { return SignalInfo>(sender, signal, std::function(), nullptr); } /*! * \brief Constructs a new SignalInfo. */ inline auto dummySignalInfo() { return SignalInfo>(); } /*! * \brief Connects the specified signal info the \a loop via SignalInfo::connectToLoop(). */ template inline void connectSignalInfoToLoop(QEventLoop *loop, const SignalInfo &signalInfo) { signalInfo.connectToLoop(loop); } /*! * \brief Connects the specified signal info the \a loop via SignalInfo::connectToLoop(). */ template inline void connectSignalInfoToLoop(QEventLoop *loop, const SignalInfo &firstSignalInfo, const Signalinfo &...remainingSignalinfo) { connectSignalInfoToLoop(loop, firstSignalInfo); connectSignalInfoToLoop(loop, remainingSignalinfo...); } /*! * \brief Checks whether all specified signals have been emitted. */ template inline bool checkWhetherAllSignalsEmitted(const SignalInfo &signalInfo) { return signalInfo; } /*! * \brief Checks whether all specified signals have been emitted. */ template inline bool checkWhetherAllSignalsEmitted(const SignalInfo &firstSignalInfo, const Signalinfo &...remainingSignalinfo) { return firstSignalInfo && checkWhetherAllSignalsEmitted(remainingSignalinfo...); } /*! * \brief Returns the names of all specified signal info which haven't been emitted yet as comma-separated string. */ template inline QByteArray failedSignalNames(const SignalInfo &signalInfo) { return !signalInfo ? signalInfo.signalName() : QByteArray(); } /*! * \brief Returns the names of all specified signal info which haven't been emitted yet as comma-separated string. */ template inline QByteArray failedSignalNames(const SignalInfo &firstSignalInfo, const Signalinfo &...remainingSignalinfo) { const QByteArray firstSignalName = failedSignalNames(firstSignalInfo); if (!firstSignalName.isEmpty()) { return firstSignalName + ", " + failedSignalNames(remainingSignalinfo...); } else { return failedSignalNames(remainingSignalinfo...); } } /*! * \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 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. * \returns Returns true if all \a signalinfo have been omitted before the \a timeout exceeded. */ template bool waitForSignals(Action action, int timeout, const Signalinfo &...signalinfo) { 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. * \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. * \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. * \returns Returns true if all \a signalinfo have been omitted before \a failure as been emitted or the \a timeout exceeded. */ template bool waitForSignalsOrFail(Action action, int timeout, const SignalInfo &failure, const Signalinfo &...signalinfo) { // use loop for waiting QEventLoop loop; // connect all signals to loop so loop is interrupted when one of the signals is emitted connectSignalInfoToLoop(&loop, failure); connectSignalInfoToLoop(&loop, signalinfo...); // perform specified action action(); // no reason to enter event loop when all signals have been emitted directly if (checkWhetherAllSignalsEmitted(failure)) { return false; } if (checkWhetherAllSignalsEmitted(signalinfo...)) { return true; } // also connect and start a timer if a timeout has been specified QTimer timer; if (timeout) { QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit, Qt::DirectConnection); timer.setSingleShot(true); timer.setInterval(SYNCTHINGTESTHELPER_TIMEOUT(timeout)); 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; do { loop.exec(); } while (!(failureEmitted = checkWhetherAllSignalsEmitted(failure)) && !(allSignalsEmitted = checkWhetherAllSignalsEmitted(signalinfo...)) && (!timeout || timer.isActive())); // 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())); } #endif return !failureEmitted && !timeoutFailed; } } // namespace CppUtilities #endif // SYNCTHINGTESTHELPER_H