syncthingtray/connector/tests/connectiontests.cpp

283 lines
10 KiB
C++
Raw Normal View History

2016-09-21 21:05:31 +02:00
#include "./syncthingconnection.h"
#include <c++utilities/tests/testutils.h>
#include <c++utilities/conversion/stringbuilder.h>
#include <cppunit/extensions/HelperMacros.h>
#include <cppunit/TestFixture.h>
#include <QCoreApplication>
#include <QProcess>
#include <QFileInfo>
#include <QDir>
#include <QMetaMethod>
using namespace std;
using namespace Data;
using namespace TestUtilities;
using namespace ConversionUtilities;
using namespace CPPUNIT_NS;
/*!
* \brief The ConnectionTests class tests the SyncthingConnector.
*/
class ConnectionTests : public TestFixture
{
CPPUNIT_TEST_SUITE(ConnectionTests);
CPPUNIT_TEST(testConnection);
CPPUNIT_TEST_SUITE_END();
public:
ConnectionTests();
void testConnection();
void setUp();
void tearDown();
private:
template<typename Signal, typename Action, typename Handler = function<void(void)> >
void waitForConnection(Signal signal, Action action, int timeout = 2500, Handler handler = nullptr);
template<typename Signal, typename Handler = function<void(void)> >
void waitForConnection(Signal signal, int timeout = 2500, Handler handler = nullptr);
QString m_apiKey;
QCoreApplication m_app;
QProcess m_syncthingProcess;
SyncthingConnection m_connection;
};
CPPUNIT_TEST_SUITE_REGISTRATION(ConnectionTests);
int dummy1 = 0;
char *dummy2;
ConnectionTests::ConnectionTests() :
m_apiKey(QStringLiteral("syncthingconnectortest")),
m_app(dummy1, &dummy2)
{}
//
// test setup
//
/*!
* \brief Starts Syncthing and prepares connecting.
*/
void ConnectionTests::setUp()
{
cerr << "\n - Launching Syncthing and setup connection ..." << endl;
// setup st config
const string configFilePath = workingCopyPath("testconfig/config.xml");
if(configFilePath.empty()) {
throw runtime_error("Unable to setup Syncthing config directory.");
}
const QFileInfo configFile(QString::fromLocal8Bit(configFilePath.data()));
// clean config dir
const QDir configDir(configFile.dir());
for(QFileInfo &configEntry : configDir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot)) {
if(configEntry.isDir()) {
QDir(configEntry.absoluteFilePath()).removeRecursively();
} else if(configEntry.fileName() != QStringLiteral("config.xml")) {
QFile::remove(configEntry.absoluteFilePath());
}
}
// determine st path
const QByteArray syncthingPathFromEnv(qgetenv("SYNCTHING_PATH"));
const QString syncthingPath(syncthingPathFromEnv.isEmpty() ? QStringLiteral("syncthing") : QString::fromLocal8Bit(syncthingPathFromEnv));
// determine st port
const int syncthingPortFromEnv(qEnvironmentVariableIntValue("SYNCTHING_PORT"));
const QString syncthingPort(!syncthingPortFromEnv ? QStringLiteral("4001") : QString::number(syncthingPortFromEnv));
// start st
QStringList args;
args.reserve(2);
args << QStringLiteral("-gui-address=http://localhost:") + syncthingPort;
args << QStringLiteral("-gui-apikey=") + m_apiKey;
args << QStringLiteral("-home=") + configFile.absolutePath();
args << QStringLiteral("-no-browser");
args << QStringLiteral("-verbose");
m_syncthingProcess.start(syncthingPath, args);
// setup connection
m_connection.setSyncthingUrl(QStringLiteral("http://localhost:") + syncthingPort);
// keep track of status changes
QObject::connect(&m_connection, &SyncthingConnection::statusChanged, [this] {
cerr << " - Connection status changed to: " << m_connection.statusText().toLocal8Bit().data() << endl;
});
}
/*!
* \brief Terminates Syncthing and prints stdout/stderr from Syncthing.
*/
void ConnectionTests::tearDown()
{
if(m_syncthingProcess.state() == QProcess::Running) {
cerr << "\n - Waiting for Syncthing to terminate ..." << endl;
m_syncthingProcess.terminate();
m_syncthingProcess.waitForFinished();
}
if(m_syncthingProcess.isOpen()) {
cerr << "\n - Syncthing terminated with exit code " << m_syncthingProcess.exitCode() << ".\n";
cerr << "\n - Syncthing stdout during the testrun:\n" << m_syncthingProcess.readAllStandardOutput().data();
cerr << "\n - Syncthing stderr during the testrun:\n" << m_syncthingProcess.readAllStandardError().data();
}
}
//
// test helper
//
/*!
* \brief Prints a QString; required to use QString with CPPUNIT_ASSERT_EQUAL_MESSAGE.
*/
std::ostream &operator <<(std::ostream &o, const QString &qstring)
{
o << qstring.toLocal8Bit().data();
return o;
}
/*!
* \brief Waits for the in ms specified \a duration keeping the event loop running.
*/
void wait(int duration)
{
QEventLoop loop;
QTimer::singleShot(duration, &loop, &QEventLoop::quit);
loop.exec();
}
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.
* \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.
*/
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)
{
// determine name of the signal for error messages
const QByteArray signalName(QMetaMethod::fromSignal(signal).name());
// create loop and connect the signal to its quit slot
QEventLoop loop;
if(!QObject::connect(sender, signal, &loop, &QEventLoop::quit, Qt::DirectConnection)) {
CPPUNIT_FAIL(argsToString("Unable to connect signal ", signalName.data(), " for waiting"));
}
// if specified, also connect handler to signal
if(handler) {
if(!QObject::connect(sender, signal, sender, handler, Qt::DirectConnection)) {
CPPUNIT_FAIL(argsToString("Unable to connect signal ", signalName.data(), " to handler"));
}
}
// handle case when signal is directly emitted
bool signalDirectlyEmitted = false;
if(!QObject::connect(sender, signal, sender, [&signalDirectlyEmitted] {
signalDirectlyEmitted = true;
}, Qt::DirectConnection)) {
CPPUNIT_FAIL(argsToString("Unable to connect signal ", signalName.data(), " to check for direct emmitation"));
}
// perform specified action
action();
// no reason to enter event loop when signal has been emitted directly
if(signalDirectlyEmitted) {
return;
}
// 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(timeout);
timer.start();
}
// enter event loop
loop.exec();
// check whether a timeout occured
if(timeout && !timer.isActive()) {
CPPUNIT_FAIL(argsToString("Signal ", signalName.data(), " has not emmitted within at least ", timeout, " ms."));
}
}
/*!
* \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, int timeout, Handler handler)
{
waitForSignal(&m_connection, signal, bind(action, &m_connection), timeout, handler);
}
/*!
* \brief Variant of waitForSignal() where sender is the connection and the action is a method of the connection.
*/
template<typename Signal, typename Handler>
void ConnectionTests::waitForConnection(Signal signal, int timeout, Handler handler)
{
waitForSignal(&m_connection, signal, noop, timeout, handler);
}
//
// actual test
//
/*!
* \brief Tests basic behaviour of the SyncthingConnection class.
*/
void ConnectionTests::testConnection()
{
cerr << "\n - Connecting initially ..." << endl;
// error in case of wrong API key
m_connection.setAutoReconnectInterval(200);
m_connection.setApiKey(QByteArray("wrong API key"));
waitForConnection(&SyncthingConnection::error, static_cast<void(SyncthingConnection::*)(void)>(&SyncthingConnection::connect), 10000, [] (const QString &errorMessage) {
if(errorMessage != QStringLiteral("Unable to request Syncthing config: Connection refused")
&& errorMessage != QStringLiteral("Unable to request Syncthing status: Connection refused")) {
CPPUNIT_FAIL(argsToString("wrong error message in case of wrong API key: ", errorMessage.toLocal8Bit().data()));
}
});
// initial connection
m_connection.setApiKey(m_apiKey.toUtf8());
waitForConnection(&SyncthingConnection::statusChanged, static_cast<void(SyncthingConnection::*)(void)>(&SyncthingConnection::connect), 10000);
CPPUNIT_ASSERT_EQUAL_MESSAGE("connected and paused (one dev is initially paused)", QStringLiteral("connected, paused"), m_connection.statusText());
// dirs present
const auto &dirInfo = m_connection.dirInfo();
CPPUNIT_ASSERT_EQUAL_MESSAGE("2 dirs present", 2ul, dirInfo.size());
CPPUNIT_ASSERT_EQUAL(QStringLiteral("test1"), dirInfo.front().id);
CPPUNIT_ASSERT_EQUAL(QStringLiteral("test2"), dirInfo.back().id);
// devs present
const auto &devInfo = m_connection.devInfo();
CPPUNIT_ASSERT_EQUAL_MESSAGE("3 devs present", 3ul, devInfo.size());
// reconnecting
cerr << "\n - Reconnecting ..." << endl;
waitForConnection(&SyncthingConnection::statusChanged, static_cast<void(SyncthingConnection::*)(void)>(&SyncthingConnection::reconnect), 1000);
CPPUNIT_ASSERT_EQUAL_MESSAGE("reconnecting", QStringLiteral("reconnecting"), m_connection.statusText());
waitForConnection(&SyncthingConnection::statusChanged, 1000);
CPPUNIT_ASSERT_EQUAL_MESSAGE("connected again", QStringLiteral("connected, paused"), m_connection.statusText());
// disconnecting
cerr << "\n - Disconnecting ..." << endl;
waitForConnection(&SyncthingConnection::statusChanged, static_cast<void(SyncthingConnection::*)(void)>(&SyncthingConnection::disconnect), 5000);
CPPUNIT_ASSERT_EQUAL_MESSAGE("disconnected", QStringLiteral("disconnected"), m_connection.statusText());
}