syncthingtray/testhelper/helper.h

420 lines
14 KiB
C++

#ifndef SYNCTHINGTESTHELPER_H
#define SYNCTHINGTESTHELPER_H
#include "./global.h"
#include <c++utilities/conversion/stringbuilder.h>
#include <c++utilities/tests/testutils.h>
#ifndef SYNCTHINGTESTHELPER_FOR_CLI
#include <cppunit/extensions/HelperMacros.h>
#endif
#include <QEventLoop>
#include <QMetaMethod>
#include <QSet>
#include <QString>
#include <QTimer>
#include <filesystem>
#include <functional>
#include <ostream>
#ifndef SYNCTHINGTESTHELPER_FOR_CLI
#define SYNCTHINGTESTHELPER_TIMEOUT(timeout) static_cast<int>(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<QString>; required to use QSet<QString> with CPPUNIT_ASSERT_EQUAL_MESSAGE.
*/
inline std::ostream &operator<<(std::ostream &o, const QSet<QString> &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 <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;
}
/*!
* \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 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<Signal>::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<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;
};
/*!
* \brief Constructs a new SignalInfo.
*/
template <typename Signal, typename Handler>
inline auto 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 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)>>();
}
/*!
* \brief Connects the specified signal info the \a loop via SignalInfo::connectToLoop().
*/
template <typename SignalInfo> inline void connectSignalInfoToLoop(QEventLoop *loop, const SignalInfo &signalInfo)
{
signalInfo.connectToLoop(loop);
}
/*!
* \brief Connects the specified signal info the \a loop via SignalInfo::connectToLoop().
*/
template <typename SignalInfo, typename... Signalinfo>
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 <typename SignalInfo> inline bool checkWhetherAllSignalsEmitted(const SignalInfo &signalInfo)
{
return signalInfo;
}
/*!
* \brief Checks whether all specified signals have been emitted.
*/
template <typename SignalInfo, typename... Signalinfo>
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 <typename SignalInfo> 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 <typename SignalInfo, typename... Signalinfo>
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 <typename Action, typename... Signalinfo> 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 <typename Action, typename SignalInfo, typename... Signalinfo>
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