File indexing completed on 2024-11-10 04:40:08

0001 /*
0002  * SPDX-FileCopyrightText: 2008 Igor Trindade Oliveira <igor_trindade@yahoo.com.br>
0003  * SPDX-FileCopyrightText: 2013 Volker Krause <vkrause@kde.org>
0004  *
0005  * SPDX-License-Identifier: LGPL-2.1-or-later
0006  */
0007 
0008 #include "setup.h"
0009 #include "akonaditest_debug.h"
0010 #include "config.h" //krazy:exclude=includes
0011 
0012 #include "agentinstance.h"
0013 #include "agentinstancecreatejob.h"
0014 #include "private/standarddirs_p.h"
0015 #include "resourcesynchronizationjob.h"
0016 
0017 #include <KConfig>
0018 #include <KConfigGroup>
0019 #include <KProcess>
0020 
0021 #include <QCoreApplication>
0022 #include <QDBusConnection>
0023 #include <QDir>
0024 #include <QFile>
0025 #include <QSettings>
0026 #include <QTimer>
0027 #include <chrono>
0028 
0029 using namespace std::chrono_literals;
0030 
0031 bool SetupTest::startAkonadiDaemon()
0032 {
0033     Q_ASSERT(Akonadi::ServerManager::hasInstanceIdentifier());
0034 
0035     if (!mAkonadiDaemonProcess) {
0036         mAkonadiDaemonProcess = std::make_unique<KProcess>();
0037         connect(mAkonadiDaemonProcess.get(), &KProcess::finished, this, &SetupTest::slotAkonadiDaemonProcessFinished);
0038     }
0039 
0040     mAkonadiDaemonProcess->setProgram(Akonadi::StandardDirs::findExecutable(QStringLiteral("akonadi_control")), {QStringLiteral("--instance"), instanceId()});
0041     mAkonadiDaemonProcess->start();
0042     const bool started = mAkonadiDaemonProcess->waitForStarted(5000);
0043     qCInfo(AKONADITEST_LOG) << "Started akonadi daemon with pid:" << mAkonadiDaemonProcess->processId();
0044     return started;
0045 }
0046 
0047 void SetupTest::stopAkonadiDaemon()
0048 {
0049     if (!mAkonadiDaemonProcess) {
0050         return;
0051     }
0052     disconnect(mAkonadiDaemonProcess.get(), &KProcess::finished, this, nullptr);
0053     mAkonadiDaemonProcess->terminate();
0054     const bool finished = mAkonadiDaemonProcess->waitForFinished(5000);
0055     if (!finished) {
0056         qCDebug(AKONADITEST_LOG) << "Problem finishing process.";
0057     }
0058     mAkonadiDaemonProcess.reset();
0059 }
0060 
0061 void SetupTest::setupAgents()
0062 {
0063     if (mAgentsCreated) {
0064         return;
0065     }
0066     mAgentsCreated = true;
0067     Config *config = Config::instance();
0068     const auto agents = config->agents();
0069     for (const auto &[instance, sync] : agents) {
0070         qCDebug(AKONADITEST_LOG) << "Creating agent" << instance << "...";
0071         ++mSetupJobCount;
0072         auto job = new Akonadi::AgentInstanceCreateJob(instance, this);
0073         job->setProperty("sync", sync);
0074         connect(job, &Akonadi::AgentInstanceCreateJob::result, this, &SetupTest::agentCreationResult);
0075         job->start();
0076     }
0077 
0078     checkSetupDone();
0079 }
0080 
0081 void SetupTest::agentCreationResult(KJob *job)
0082 {
0083     qCDebug(AKONADITEST_LOG) << "Agent created";
0084     --mSetupJobCount;
0085     if (job->error()) {
0086         qCritical() << "Failed to create agent:" << job->errorString();
0087         setupFailed();
0088     } else {
0089         const bool needsSync = job->property("sync").toBool();
0090         const auto instance = qobject_cast<Akonadi::AgentInstanceCreateJob *>(job)->instance();
0091         qCDebug(AKONADITEST_LOG) << "Agent" << instance.identifier() << "created";
0092         if (needsSync) {
0093             ++mSetupJobCount;
0094             qCDebug(AKONADITEST_LOG) << "Scheduling Agent sync of" << instance.identifier();
0095             auto sync = new Akonadi::ResourceSynchronizationJob(instance, this);
0096             connect(sync, &Akonadi::ResourceSynchronizationJob::result, this, &SetupTest::synchronizationResult);
0097             sync->start();
0098         }
0099     }
0100 
0101     checkSetupDone();
0102 }
0103 
0104 void SetupTest::synchronizationResult(KJob *job)
0105 {
0106     auto instance = qobject_cast<Akonadi::ResourceSynchronizationJob *>(job)->resource();
0107     qCDebug(AKONADITEST_LOG) << "Sync of" << instance.identifier() << "done";
0108 
0109     --mSetupJobCount;
0110     if (job->error()) {
0111         qCritical() << job->errorString();
0112         setupFailed();
0113     }
0114 
0115     checkSetupDone();
0116 }
0117 
0118 void SetupTest::serverStateChanged(Akonadi::ServerManager::State state)
0119 {
0120     if (state == Akonadi::ServerManager::Running) {
0121         setupAgents();
0122     } else if (mShuttingDown && state == Akonadi::ServerManager::NotRunning) {
0123         shutdownHarder();
0124     }
0125 }
0126 
0127 void SetupTest::copyXdgDirectory(const QString &src, const QString &dst)
0128 {
0129     qCDebug(AKONADITEST_LOG) << "Copying" << src << "to" << dst;
0130     const QDir srcDir(src);
0131     const auto entries = srcDir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot);
0132     for (const auto &fi : entries) {
0133         if (fi.isDir()) {
0134             if (fi.fileName() == QLatin1StringView("akonadi")) {
0135                 // namespace according to instance identifier
0136 #ifdef Q_OS_WIN
0137                 const bool isXdgConfig = src.contains(QLatin1StringView("/xdgconfig/"));
0138                 copyDirectory(fi.absoluteFilePath(),
0139                               dst + QStringLiteral("/akonadi/") + (isXdgConfig ? QStringLiteral("config/") : QStringLiteral("data/"))
0140                                   + QStringLiteral("instance/") + instanceId());
0141 #else
0142                 copyDirectory(fi.absoluteFilePath(), dst + QStringLiteral("/akonadi/instance/") + instanceId());
0143 #endif
0144             } else {
0145                 copyDirectory(fi.absoluteFilePath(), dst + QLatin1Char('/') + fi.fileName());
0146             }
0147         } else {
0148             if (fi.fileName().startsWith(QLatin1StringView("akonadi_")) && fi.fileName().endsWith(QLatin1StringView("rc"))) {
0149                 // namespace according to instance identifier
0150                 const QString baseName = fi.fileName().left(fi.fileName().size() - 2);
0151                 const QString dstPath = dst + QLatin1Char('/') + Akonadi::ServerManager::addNamespace(baseName) + QStringLiteral("rc");
0152                 if (!QFile::copy(fi.absoluteFilePath(), dstPath)) {
0153                     qCWarning(AKONADITEST_LOG) << "Failed to copy" << fi.absoluteFilePath() << "to" << dstPath;
0154                 }
0155             } else {
0156                 const QString dstPath = dst + QLatin1Char('/') + fi.fileName();
0157                 if (!QFile::copy(fi.absoluteFilePath(), dstPath)) {
0158                     qCWarning(AKONADITEST_LOG) << "Failed to copy" << fi.absoluteFilePath() << "to" << dstPath;
0159                 }
0160             }
0161         }
0162     }
0163 }
0164 
0165 void SetupTest::copyDirectory(const QString &src, const QString &dst)
0166 {
0167     const QDir srcDir(src);
0168     QDir::root().mkpath(dst);
0169     const auto entries = srcDir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot);
0170     for (const auto &fi : entries) {
0171         const QString dstPath = dst + QLatin1Char('/') + fi.fileName();
0172         if (fi.isDir()) {
0173             copyDirectory(fi.absoluteFilePath(), dstPath);
0174         } else {
0175             if (!QFile::copy(fi.absoluteFilePath(), dstPath)) {
0176                 qCWarning(AKONADITEST_LOG) << "Failed to copy" << fi.absoluteFilePath() << "to" << dstPath;
0177             }
0178         }
0179     }
0180 }
0181 
0182 void SetupTest::createTempEnvironment()
0183 {
0184     qCDebug(AKONADITEST_LOG) << "Creating test environment in" << basePath();
0185 
0186     const Config *config = Config::instance();
0187 #ifdef Q_OS_WIN
0188     // Always copy the generic xdgconfig dir
0189     copyXdgDirectory(config->basePath() + QStringLiteral("/xdgconfig"), basePath());
0190     if (!config->xdgConfigHome().isEmpty()) {
0191         copyXdgDirectory(config->xdgConfigHome(), basePath());
0192     }
0193     copyXdgDirectory(config->xdgDataHome(), basePath());
0194     setEnvironmentVariable("XDG_DATA_HOME", basePath());
0195     setEnvironmentVariable("XDG_CONFIG_HOME", basePath());
0196     writeAkonadiserverrc(basePath() + QStringLiteral("/akonadi/config/instance/%1/akonadiserverrc").arg(instanceId()));
0197 #else
0198     const QDir tmpDir(basePath());
0199     const QString testRunnerDataDir = QStringLiteral("data");
0200     const QString testRunnerConfigDir = QStringLiteral("config");
0201     const QString testRunnerTmpDir = QStringLiteral("tmp");
0202 
0203     tmpDir.mkpath(testRunnerConfigDir);
0204     tmpDir.mkpath(testRunnerDataDir);
0205     tmpDir.mkpath(testRunnerTmpDir);
0206 
0207     // Always copy the generic xdgconfig dir
0208     copyXdgDirectory(config->basePath() + QStringLiteral("/xdgconfig"), basePath() + testRunnerConfigDir);
0209     if (!config->xdgConfigHome().isEmpty()) {
0210         copyXdgDirectory(config->xdgConfigHome(), basePath() + testRunnerConfigDir);
0211     }
0212     copyXdgDirectory(config->xdgDataHome(), basePath() + testRunnerDataDir);
0213 
0214     setEnvironmentVariable("XDG_DATA_HOME", basePath() + testRunnerDataDir);
0215     setEnvironmentVariable("XDG_CONFIG_HOME", basePath() + testRunnerConfigDir);
0216     setEnvironmentVariable("TMPDIR", basePath() + testRunnerTmpDir);
0217     writeAkonadiserverrc(basePath() + testRunnerConfigDir + QStringLiteral("/akonadi/instance/%1/akonadiserverrc").arg(instanceId()));
0218 #endif
0219 
0220     QString backend;
0221     if (Config::instance()->dbBackend() == QLatin1StringView("pgsql")) {
0222         backend = QStringLiteral("postgresql");
0223     } else {
0224         backend = Config::instance()->dbBackend();
0225     }
0226     setEnvironmentVariable("TESTRUNNER_DB_ENVIRONMENT", backend);
0227 }
0228 
0229 void SetupTest::writeAkonadiserverrc(const QString &path)
0230 {
0231     QString backend;
0232     if (Config::instance()->dbBackend() == QLatin1StringView("sqlite")) {
0233         backend = QStringLiteral("QSQLITE");
0234     } else if (Config::instance()->dbBackend() == QLatin1StringView("mysql")) {
0235         backend = QStringLiteral("QMYSQL");
0236     } else if (Config::instance()->dbBackend() == QLatin1StringView("pgsql")) {
0237         backend = QStringLiteral("QPSQL");
0238     } else {
0239         qCCritical(AKONADITEST_LOG, "Invalid backend name %s", qPrintable(backend));
0240         return;
0241     }
0242 
0243     QSettings settings(path, QSettings::IniFormat);
0244     settings.beginGroup(QStringLiteral("General"));
0245     settings.setValue(QStringLiteral("Driver"), backend);
0246     settings.endGroup();
0247     settings.beginGroup(QStringLiteral("Search"));
0248     settings.setValue(QStringLiteral("Manager"), QStringLiteral("Dummy"));
0249     settings.endGroup();
0250     settings.beginGroup(QStringLiteral("Debug"));
0251     settings.setValue(QStringLiteral("Tracer"), QStringLiteral("null"));
0252     settings.endGroup();
0253     qCDebug(AKONADITEST_LOG) << "Written akonadiserverrc to" << settings.fileName();
0254 }
0255 
0256 void SetupTest::cleanTempEnvironment() const
0257 {
0258 #ifdef Q_OS_WIN
0259     QDir(basePath() + QStringLiteral("akonadi/config/instance/") + instanceId()).removeRecursively();
0260     QDir(basePath() + QStringLiteral("akonadi/data/instance/") + instanceId()).removeRecursively();
0261 #else
0262     QDir(basePath()).removeRecursively();
0263 #endif
0264 }
0265 
0266 SetupTest::SetupTest()
0267     : mAkonadiDaemonProcess(nullptr)
0268     , mShuttingDown(false)
0269     , mAgentsCreated(false)
0270     , mTrackAkonadiProcess(true)
0271     , mSetupJobCount(0)
0272     , mExitCode(0)
0273 {
0274     setupInstanceId();
0275     cleanTempEnvironment();
0276     createTempEnvironment();
0277 
0278     // switch off agent auto-starting by default, can be re-enabled if really needed inside the config.xml
0279     setEnvironmentVariable("AKONADI_DISABLE_AGENT_AUTOSTART", QStringLiteral("true"));
0280     setEnvironmentVariable("AKONADI_TESTRUNNER_PID", QString::number(QCoreApplication::instance()->applicationPid()));
0281     // enable all debugging, so we get some useful information when test fails
0282     setEnvironmentVariable("QT_LOGGING_RULES",
0283                            QStringLiteral("* = true\n"
0284                                           "qt.* = false\n"
0285                                           "kf5.coreaddons.desktopparser.debug = false"));
0286 
0287     setEnvironmentVariable("KIO_DISABLE_CACHE_CLEANER", QStringLiteral("yes"));
0288 
0289     QHashIterator<QString, QString> iter(Config::instance()->envVars());
0290     while (iter.hasNext()) {
0291         iter.next();
0292         qCDebug(AKONADITEST_LOG) << "Setting environment variable" << iter.key() << "=" << iter.value();
0293         setEnvironmentVariable(iter.key().toLocal8Bit(), iter.value());
0294     }
0295 
0296     // No kres-migrator please
0297     KConfig migratorConfig(basePath() + QStringLiteral("config/kres-migratorrc"));
0298     KConfigGroup migrationCfg(&migratorConfig, QStringLiteral("Migration"));
0299     migrationCfg.writeEntry("Enabled", false);
0300 
0301     connect(Akonadi::ServerManager::self(), &Akonadi::ServerManager::stateChanged, this, &SetupTest::serverStateChanged);
0302 
0303     QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.Akonadi.Testrunner-")
0304                                                   + QString::number(QCoreApplication::instance()->applicationPid()));
0305     QDBusConnection::sessionBus().registerObject(QStringLiteral("/"), this, QDBusConnection::ExportScriptableSlots);
0306 }
0307 
0308 SetupTest::~SetupTest()
0309 {
0310     cleanTempEnvironment();
0311 }
0312 
0313 void SetupTest::shutdown()
0314 {
0315     if (mShuttingDown) {
0316         return;
0317     }
0318     mShuttingDown = true;
0319 
0320     switch (Akonadi::ServerManager::self()->state()) {
0321     case Akonadi::ServerManager::Running:
0322     case Akonadi::ServerManager::Starting:
0323     case Akonadi::ServerManager::Upgrading:
0324         qCInfo(AKONADITEST_LOG) << "Shutting down Akonadi control...";
0325         Akonadi::ServerManager::self()->stop();
0326         // safety timeout
0327         QTimer::singleShot(30s, this, &SetupTest::shutdownHarder);
0328         break;
0329     case Akonadi::ServerManager::NotRunning:
0330     case Akonadi::ServerManager::Broken:
0331         shutdownHarder();
0332         break;
0333     case Akonadi::ServerManager::Stopping:
0334         // safety timeout
0335         QTimer::singleShot(30s, this, &SetupTest::shutdownHarder);
0336         break;
0337     }
0338 }
0339 
0340 void SetupTest::shutdownHarder()
0341 {
0342     qCDebug(AKONADITEST_LOG) << "Forcing akonaditest shutdown";
0343     mShuttingDown = false;
0344     stopAkonadiDaemon();
0345     QCoreApplication::instance()->exit(mExitCode);
0346 }
0347 
0348 void SetupTest::restartAkonadiServer()
0349 {
0350     qCDebug(AKONADITEST_LOG) << "Restarting Akonadi";
0351     disconnect(mAkonadiDaemonProcess.get(), &KProcess::finished, this, nullptr);
0352     Akonadi::ServerManager::self()->stop();
0353     const bool shutdownResult = mAkonadiDaemonProcess->waitForFinished();
0354     if (!shutdownResult) {
0355         qCWarning(AKONADITEST_LOG) << "Akonadi control did not shut down in time, killing it.";
0356         mAkonadiDaemonProcess->kill();
0357     }
0358     // we don't use Control::start() since we want to be able to kill
0359     // it forcefully, if necessary, and know the pid
0360     startAkonadiDaemon();
0361     // from here on, the server exiting is an error again
0362     connect(mAkonadiDaemonProcess.get(), &KProcess::finished, this, &SetupTest::slotAkonadiDaemonProcessFinished);
0363 }
0364 
0365 QString SetupTest::basePath() const
0366 {
0367 #ifdef Q_OS_WIN
0368     // On Windows we are forced to share the same data directory as production instances
0369     // because there's no way to override QStandardPaths like we can on Unix.
0370     // This means that on Windows we rely on Instances providing us the necessary isolation
0371     return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation);
0372 #else
0373     QString sysTempDirPath = QDir::tempPath();
0374 #ifdef Q_OS_UNIX
0375     // QDir::tempPath() makes sure to use the fully sym-link exploded
0376     // absolute path to the temp dir. That is nice, but on OSX it makes
0377     // that path really long. MySQL chokes on this, for it's socket path,
0378     // so work around that
0379     sysTempDirPath = QStringLiteral("/tmp");
0380 #endif
0381 
0382     const QDir sysTempDir(sysTempDirPath);
0383     const QString tempDir = QStringLiteral("/aktestrunner-%1/").arg(QCoreApplication::instance()->applicationPid());
0384     if (!sysTempDir.exists(tempDir)) {
0385         sysTempDir.mkdir(tempDir);
0386     }
0387     return sysTempDirPath + tempDir;
0388 #endif
0389 }
0390 
0391 void SetupTest::slotAkonadiDaemonProcessFinished(int exitCode)
0392 {
0393     if (mTrackAkonadiProcess || exitCode != EXIT_SUCCESS) {
0394         qCWarning(AKONADITEST_LOG) << "Akonadi server process was terminated externally!";
0395         Q_EMIT serverExited(exitCode);
0396     }
0397     mAkonadiDaemonProcess.reset();
0398 }
0399 
0400 void SetupTest::trackAkonadiProcess(bool track)
0401 {
0402     mTrackAkonadiProcess = track;
0403 }
0404 
0405 QString SetupTest::instanceId() const
0406 {
0407     return QStringLiteral("testrunner-") + QString::number(QCoreApplication::instance()->applicationPid());
0408 }
0409 
0410 void SetupTest::setupInstanceId()
0411 {
0412     setEnvironmentVariable("AKONADI_INSTANCE", instanceId());
0413 }
0414 
0415 void SetupTest::checkSetupDone()
0416 {
0417     qCDebug(AKONADITEST_LOG) << "checkSetupDone: pendingJobs =" << mSetupJobCount << ", exitCode =" << mExitCode;
0418     if (mSetupJobCount == 0) {
0419         if (mExitCode != 0) {
0420             qCInfo(AKONADITEST_LOG) << "Setup has failed, aborting test.";
0421             shutdown();
0422         } else {
0423             qCInfo(AKONADITEST_LOG) << "Setup successful";
0424             Q_EMIT setupDone();
0425         }
0426     }
0427 }
0428 
0429 void SetupTest::setupFailed()
0430 {
0431     mExitCode = 1;
0432 }
0433 
0434 void SetupTest::setEnvironmentVariable(const QByteArray &name, const QString &value)
0435 {
0436     mEnvVars.push_back(qMakePair(name, value.toLocal8Bit()));
0437     qputenv(name.constData(), value.toLatin1());
0438 }
0439 
0440 QList<SetupTest::EnvVar> SetupTest::environmentVariables() const
0441 {
0442     return mEnvVars;
0443 }
0444 
0445 #include "moc_setup.cpp"