diff --git a/autotests/libs/testrunner/setup.cpp b/autotests/libs/testrunner/setup.cpp index 20c960d81..bb45d8344 100644 --- a/autotests/libs/testrunner/setup.cpp +++ b/autotests/libs/testrunner/setup.cpp @@ -1,453 +1,457 @@ /* * Copyright (c) 2008 Igor Trindade Oliveira * Copyright (c) 2013 Volker Krause * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #include "setup.h" #include "config.h" //krazy:exclude=includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include bool SetupTest::startAkonadiDaemon() { Q_ASSERT(Akonadi::ServerManager::hasInstanceIdentifier()); if (!mAkonadiDaemonProcess) { mAkonadiDaemonProcess = new KProcess(this); connect(mAkonadiDaemonProcess, QOverload::of(&KProcess::finished), this, &SetupTest::slotAkonadiDaemonProcessFinished); } mAkonadiDaemonProcess->setProgram(Akonadi::StandardDirs::findExecutable(QStringLiteral("akonadi_control")), { QStringLiteral("--instance"), instanceId() }); mAkonadiDaemonProcess->start(); const bool started = mAkonadiDaemonProcess->waitForStarted(5000); qDebug() << "Started akonadi daemon with pid:" << mAkonadiDaemonProcess->pid(); return started; } void SetupTest::stopAkonadiDaemon() { if (!mAkonadiDaemonProcess) { return; } disconnect(mAkonadiDaemonProcess, SIGNAL(finished(int)), this, nullptr); mAkonadiDaemonProcess->terminate(); const bool finished = mAkonadiDaemonProcess->waitForFinished(5000); if (!finished) { qDebug() << "Problem finishing process."; } mAkonadiDaemonProcess->close(); mAkonadiDaemonProcess->deleteLater(); mAkonadiDaemonProcess = nullptr; } void SetupTest::setupAgents() { if (mAgentsCreated) { return; } mAgentsCreated = true; Config *config = Config::instance(); const auto agents = config->agents(); for (const auto agent : agents) { qDebug() << "Creating agent" << agent.first << "..."; ++mSetupJobCount; Akonadi::AgentInstanceCreateJob *job = new Akonadi::AgentInstanceCreateJob(agent.first, this); job->setProperty("sync", agent.second); connect(job, &Akonadi::AgentInstanceCreateJob::result, this, &SetupTest::agentCreationResult); job->start(); } - if (isSetupDone()) { - Q_EMIT setupDone(); - } + checkSetupDone(); } void SetupTest::agentCreationResult(KJob *job) { - qDebug() << "Agent created"; --mSetupJobCount; if (job->error()) { - qCritical() << job->errorString(); + qCritical() << "Failed to create agent:" << job->errorString(); setupFailed(); - return; - } - const bool needsSync = job->property("sync").toBool(); - if (needsSync) { - ++mSetupJobCount; - qDebug() << "Scheduling Agent sync"; - Akonadi::ResourceSynchronizationJob *sync = new Akonadi::ResourceSynchronizationJob( - qobject_cast(job)->instance(), this); - connect(sync, &Akonadi::ResourceSynchronizationJob::result, this, &SetupTest::synchronizationResult); - sync->start(); + } else { + const bool needsSync = job->property("sync").toBool(); + const auto instance = qobject_cast(job)->instance(); + qDebug() << "Agent" << instance.identifier() << "created"; + if (needsSync) { + ++mSetupJobCount; + qDebug() << "Scheduling Agent sync of" << instance.identifier(); + Akonadi::ResourceSynchronizationJob *sync = new Akonadi::ResourceSynchronizationJob(instance, this); + connect(sync, &Akonadi::ResourceSynchronizationJob::result, this, &SetupTest::synchronizationResult); + sync->start(); + } } - if (isSetupDone()) { - Q_EMIT setupDone(); - } + checkSetupDone(); } void SetupTest::synchronizationResult(KJob *job) { - qDebug() << "Sync done"; + auto instance = qobject_cast(job)->resource(); + qDebug() << "Sync of" << instance.identifier() << "done"; + --mSetupJobCount; if (job->error()) { qCritical() << job->errorString(); setupFailed(); } - if (isSetupDone()) { - Q_EMIT setupDone(); - } + checkSetupDone(); } void SetupTest::serverStateChanged(Akonadi::ServerManager::State state) { if (state == Akonadi::ServerManager::Running) { setupAgents(); } else if (mShuttingDown && state == Akonadi::ServerManager::NotRunning) { shutdownHarder(); } } void SetupTest::copyXdgDirectory(const QString &src, const QString &dst) { qDebug() << "Copying" << src << "to" << dst; const QDir srcDir(src); const auto entries = srcDir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot); for (const auto &fi : entries) { if (fi.isDir()) { if (fi.fileName() == QLatin1String("akonadi")) { // namespace according to instance identifier #ifdef Q_OS_WIN const bool isXdgConfig = src.contains(QLatin1String("/xdgconfig/")); copyDirectory(fi.absoluteFilePath(), dst + QStringLiteral("/akonadi/") + (isXdgConfig ? QStringLiteral("config/") : QStringLiteral("data/")) + QStringLiteral("instance/") + instanceId()); #else copyDirectory(fi.absoluteFilePath(), dst + QStringLiteral("/akonadi/instance/") + instanceId()); #endif } else { copyDirectory(fi.absoluteFilePath(), dst + QLatin1Char('/') + fi.fileName()); } } else { if (fi.fileName().startsWith(QLatin1String("akonadi_")) && fi.fileName().endsWith(QLatin1String("rc"))) { // namespace according to instance identifier const QString baseName = fi.fileName().left(fi.fileName().size() - 2); const QString dstPath = dst + QLatin1Char('/') + Akonadi::ServerManager::addNamespace(baseName) + QStringLiteral("rc"); if (!QFile::copy(fi.absoluteFilePath(), dstPath)) { qWarning() << "Failed to copy" << fi.absoluteFilePath() << "to" << dstPath; } } else { const QString dstPath = dst + QLatin1Char('/') + fi.fileName(); if (!QFile::copy(fi.absoluteFilePath(), dstPath)) { qWarning() << "Failed to copy" << fi.absoluteFilePath() << "to" << dstPath; } } } } } void SetupTest::copyDirectory(const QString &src, const QString &dst) { const QDir srcDir(src); QDir::root().mkpath(dst); const auto entries = srcDir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot); for (const auto &fi : entries) { const QString dstPath = dst + QLatin1Char('/') + fi.fileName(); if (fi.isDir()) { copyDirectory(fi.absoluteFilePath(), dstPath); } else { if (!QFile::copy(fi.absoluteFilePath(), dstPath)) { qWarning() << "Failed to copy" << fi.absoluteFilePath() << "to" << dstPath; } } } } void SetupTest::createTempEnvironment() { qDebug() << "Creating test environment in" << basePath(); const Config *config = Config::instance(); #ifdef Q_OS_WIN // Always copy the generic xdgconfig dir copyXdgDirectory(config->basePath() + QStringLiteral("/xdgconfig"), basePath()); if (!config->xdgConfigHome().isEmpty()) { copyXdgDirectory(config->xdgConfigHome(), basePath()); } copyXdgDirectory(config->xdgDataHome(), basePath()); setEnvironmentVariable("XDG_DATA_HOME", basePath()); setEnvironmentVariable("XDG_CONFIG_HOME", basePath()); writeAkonadiserverrc(basePath() + QStringLiteral("/akonadi/config/instance/%1/akonadiserverrc").arg(instanceId())); #else const QDir tmpDir(basePath()); const QString testRunnerDataDir = QStringLiteral("data"); const QString testRunnerConfigDir = QStringLiteral("config"); const QString testRunnerTmpDir = QStringLiteral("tmp"); tmpDir.mkpath(testRunnerConfigDir); tmpDir.mkpath(testRunnerDataDir); tmpDir.mkpath(testRunnerTmpDir); // Always copy the generic xdgconfig dir copyXdgDirectory(config->basePath() + QStringLiteral("/xdgconfig"), basePath() + testRunnerConfigDir); if (!config->xdgConfigHome().isEmpty()) { copyXdgDirectory(config->xdgConfigHome(), basePath() + testRunnerConfigDir); } copyXdgDirectory(config->xdgDataHome(), basePath() + testRunnerDataDir); setEnvironmentVariable("XDG_DATA_HOME", basePath() + testRunnerDataDir); setEnvironmentVariable("XDG_CONFIG_HOME", basePath() + testRunnerConfigDir); setEnvironmentVariable("TMPDIR", basePath() + testRunnerTmpDir); writeAkonadiserverrc(basePath() + testRunnerConfigDir + QStringLiteral("/akonadi/instance/%1/akonadiserverrc").arg(instanceId())); #endif QString backend; if (Config::instance()->dbBackend() == QLatin1String("pgsql")) { backend = QStringLiteral("postgresql"); } else { backend = Config::instance()->dbBackend(); } setEnvironmentVariable("TESTRUNNER_DB_ENVIRONMENT", backend); } void SetupTest::writeAkonadiserverrc(const QString &path) { QString backend; if (Config::instance()->dbBackend() == QLatin1String("sqlite")) { backend = QStringLiteral("QSQLITE3"); } else if (Config::instance()->dbBackend() == QLatin1String("mysql")) { backend = QStringLiteral("QMYSQL"); } else if (Config::instance()->dbBackend() == QLatin1String("pgsql")) { backend = QStringLiteral("QPSQL"); } else { qCritical("Invalid backend name %s", qPrintable(backend)); return; } QSettings settings(path, QSettings::IniFormat); settings.beginGroup(QStringLiteral("General")); settings.setValue(QStringLiteral("Driver"), backend); settings.endGroup(); settings.beginGroup(QStringLiteral("Search")); settings.setValue(QStringLiteral("Manager"), QStringLiteral("Dummy")); settings.endGroup(); settings.beginGroup(QStringLiteral("Debug")); settings.setValue(QStringLiteral("Tracer"), QStringLiteral("null")); settings.endGroup(); qDebug() << "Written akonadiserverrc to" << settings.fileName(); } void SetupTest::cleanTempEnvironment() { #ifdef Q_OS_WIN QDir(basePath() + QStringLiteral("akonadi/config/instance/") + instanceId()).removeRecursively(); QDir(basePath() + QStringLiteral("akonadi/data/instance/") + instanceId()).removeRecursively(); #else QDir(basePath()).removeRecursively(); #endif } SetupTest::SetupTest() : mAkonadiDaemonProcess(nullptr) , mShuttingDown(false) , mAgentsCreated(false) , mTrackAkonadiProcess(true) , mSetupJobCount(0) , mExitCode(0) { setupInstanceId(); cleanTempEnvironment(); createTempEnvironment(); // switch off agent auto-starting by default, can be re-enabled if really needed inside the config.xml setEnvironmentVariable("AKONADI_DISABLE_AGENT_AUTOSTART", QStringLiteral("true")); setEnvironmentVariable("AKONADI_TESTRUNNER_PID", QString::number(QCoreApplication::instance()->applicationPid())); // enable all debugging, so we get some useful information when test fails setEnvironmentVariable("QT_LOGGING_RULES", QStringLiteral("* = true\n" "qt.* = false\n" "kf5.coreaddons.desktopparser.debug = false")); // avoid KIO starting klauncher which can get the CI stuck setEnvironmentVariable("KDE_FORK_SLAVES", QStringLiteral("yes")); setEnvironmentVariable("KIO_DISABLE_CACHE_CLEANER", QStringLiteral("yes")); QHashIterator iter(Config::instance()->envVars()); while (iter.hasNext()) { iter.next(); qDebug() << "Setting environment variable" << iter.key() << "=" << iter.value(); setEnvironmentVariable(iter.key().toLocal8Bit(), iter.value()); } // No kres-migrator please KConfig migratorConfig(basePath() + QStringLiteral("config/kres-migratorrc")); KConfigGroup migrationCfg(&migratorConfig, "Migration"); migrationCfg.writeEntry("Enabled", false); connect(Akonadi::ServerManager::self(), &Akonadi::ServerManager::stateChanged, this, &SetupTest::serverStateChanged); QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.Akonadi.Testrunner-") + QString::number(QCoreApplication::instance()->applicationPid())); QDBusConnection::sessionBus().registerObject(QStringLiteral("/"), this, QDBusConnection::ExportScriptableSlots); } SetupTest::~SetupTest() { cleanTempEnvironment(); } void SetupTest::shutdown() { if (mShuttingDown) { return; } mShuttingDown = true; switch (Akonadi::ServerManager::self()->state()) { case Akonadi::ServerManager::Running: case Akonadi::ServerManager::Starting: case Akonadi::ServerManager::Upgrading: qDebug() << "Shutting down Akonadi control..."; Akonadi::ServerManager::self()->stop(); // safety timeout QTimer::singleShot(30 * 1000, this, &SetupTest::shutdownHarder); break; case Akonadi::ServerManager::NotRunning: case Akonadi::ServerManager::Broken: shutdownHarder(); - Q_FALLTHROUGH(); + break; case Akonadi::ServerManager::Stopping: // safety timeout QTimer::singleShot(30 * 1000, this, &SetupTest::shutdownHarder); break; } } void SetupTest::shutdownHarder() { qDebug(); mShuttingDown = false; stopAkonadiDaemon(); QCoreApplication::instance()->exit(mExitCode); } void SetupTest::restartAkonadiServer() { qDebug(); disconnect(mAkonadiDaemonProcess, SIGNAL(finished(int)), this, nullptr); Akonadi::ServerManager::self()->stop(); const bool shutdownResult = mAkonadiDaemonProcess->waitForFinished(); if (!shutdownResult) { qWarning() << "Akonadi control did not shut down in time, killing it."; mAkonadiDaemonProcess->kill(); } // we don't use Control::start() since we want to be able to kill // it forcefully, if necessary, and know the pid startAkonadiDaemon(); // from here on, the server exiting is an error again connect(mAkonadiDaemonProcess, SIGNAL(finished(int)), this, SLOT(slotAkonadiDaemonProcessFinished(int))); } QString SetupTest::basePath() const { #ifdef Q_OS_WIN // On Windows we are forced to share the same data directory as production instances // because there's no way to override QStandardPaths like we can on Unix. // This means that on Windows we rely on Instances providing us the necessary isolation return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation); #else QString sysTempDirPath = QDir::tempPath(); #ifdef Q_OS_UNIX // QDir::tempPath() makes sure to use the fully sym-link exploded // absolute path to the temp dir. That is nice, but on OSX it makes // that path really long. MySQL chokes on this, for it's socket path, // so work around that sysTempDirPath = QStringLiteral("/tmp"); #endif const QDir sysTempDir(sysTempDirPath); const QString tempDir = QStringLiteral("/aktestrunner-%1/") .arg(QCoreApplication::instance()->applicationPid()); if (!sysTempDir.exists(tempDir)) { sysTempDir.mkdir(tempDir); } return sysTempDirPath + tempDir; #endif } void SetupTest::slotAkonadiDaemonProcessFinished(int exitCode) { if (mTrackAkonadiProcess || exitCode != EXIT_SUCCESS) { qWarning() << "Akonadi server process was terminated externally!"; Q_EMIT serverExited(exitCode); } mAkonadiDaemonProcess = nullptr; } void SetupTest::trackAkonadiProcess(bool track) { mTrackAkonadiProcess = track; } QString SetupTest::instanceId() const { return QStringLiteral("testrunner-") + QString::number(QCoreApplication::instance()->applicationPid()); } void SetupTest::setupInstanceId() { setEnvironmentVariable("AKONADI_INSTANCE", instanceId()); } -bool SetupTest::isSetupDone() const +void SetupTest::checkSetupDone() { - qDebug() << "isSetupDone:" << mSetupJobCount << mExitCode; - return mSetupJobCount == 0 && mExitCode == 0; + qDebug() << "checkSetupDone: pendingJobs =" << mSetupJobCount << ", exitCode =" << mExitCode; + if (mSetupJobCount == 0) { + if (mExitCode != 0) { + qInfo() << "Setup has failed, aborting test."; + shutdown(); + } else { + qInfo() << "Setup successful"; + Q_EMIT setupDone(); + } + } + } void SetupTest::setupFailed() { mExitCode = 1; - shutdown(); } void SetupTest::setEnvironmentVariable(const QByteArray &name, const QString &value) { mEnvVars.push_back(qMakePair(name, value.toLocal8Bit())); qputenv(name.constData(), value.toLatin1()); } QVector< SetupTest::EnvVar > SetupTest::environmentVariables() const { return mEnvVars; } diff --git a/autotests/libs/testrunner/setup.h b/autotests/libs/testrunner/setup.h index f7cc00734..883f41b2b 100644 --- a/autotests/libs/testrunner/setup.h +++ b/autotests/libs/testrunner/setup.h @@ -1,95 +1,95 @@ /* * Copyright (c) 2008 Igor Trindade Oliveira * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #ifndef SETUP_H #define SETUP_H #include #include #include #include class KProcess; class KJob; class SetupTest : public QObject { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.Akonadi.Testrunner") public: SetupTest(); ~SetupTest(); /** Sets the instance identifier for the Akonadi session. Call this before using any other Akonadi API! */ void setupInstanceId(); bool startAkonadiDaemon(); void stopAkonadiDaemon(); QString basePath() const; /// Identifier used for the Akonadi session QString instanceId() const; /// set an environment variable void setEnvironmentVariable(const QByteArray &name, const QString &value); /// retrieve all modified environment variables, for writing the shell script typedef QPair EnvVar; QVector environmentVariables() const; public Q_SLOTS: Q_SCRIPTABLE void shutdown(); Q_SCRIPTABLE void shutdownHarder(); /** Synchronously restarts the server. */ Q_SCRIPTABLE void restartAkonadiServer(); Q_SCRIPTABLE void trackAkonadiProcess(bool track); Q_SIGNALS: void setupDone(); void serverExited(int exitCode); private Q_SLOTS: void serverStateChanged(Akonadi::ServerManager::State state); void slotAkonadiDaemonProcessFinished(int exitCode); void agentCreationResult(KJob *job); void synchronizationResult(KJob *job); private: void setupAgents(); void copyXdgDirectory(const QString &src, const QString &dst); void copyDirectory(const QString &src, const QString &dst); void createTempEnvironment(); void cleanTempEnvironment(); - bool isSetupDone() const; void setupFailed(); void writeAkonadiserverrc(const QString &path); + void checkSetupDone(); private: KProcess *mAkonadiDaemonProcess = nullptr; bool mShuttingDown; bool mAgentsCreated; bool mTrackAkonadiProcess; int mSetupJobCount; int mExitCode; QVector mEnvVars; }; #endif diff --git a/autotests/libs/testrunner/testrunner.cpp b/autotests/libs/testrunner/testrunner.cpp index 1d946114a..8d7c2229f 100644 --- a/autotests/libs/testrunner/testrunner.cpp +++ b/autotests/libs/testrunner/testrunner.cpp @@ -1,83 +1,83 @@ /* * Copyright (c) 2009 Volker Krause * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #include "testrunner.h" #include #include TestRunner::TestRunner(const QStringList &args, QObject *parent) : QObject(parent) , mArguments(args) , mExitCode(0) , mProcess(nullptr) { } int TestRunner::exitCode() const { return mExitCode; } void TestRunner::run() { - qDebug() << mArguments; + qDebug() << "Starting test" << mArguments; mProcess = new KProcess(this); mProcess->setProgram(mArguments); connect(mProcess, QOverload::of(&KProcess::finished), this, &TestRunner::processFinished); connect(mProcess, &KProcess::errorOccurred, this, &TestRunner::processError); // environment setup seems to have been done by setuptest globally already mProcess->start(); if (!mProcess->waitForStarted()) { qWarning() << mArguments << "failed to start!"; mExitCode = 255; Q_EMIT finished(); } } void TestRunner::triggerTermination(int exitCode) { processFinished(exitCode); } void TestRunner::processFinished(int exitCode) { // Only update the exit code when it is 0. This prevents overwriting a non-zero // value with 0. This can happen when multiple processes finish or triggerTermination // is called after a process has finished. if (mExitCode == 0) { mExitCode = exitCode; qDebug() << exitCode; } Q_EMIT finished(); } void TestRunner::processError(QProcess::ProcessError error) { qWarning() << mArguments << "exited with an error:" << error; mExitCode = 255; Q_EMIT finished(); } void TestRunner::terminate() { if (mProcess) { mProcess->terminate(); } }