Files
RedBear-OS/local/recipes/kde/kf6-kio/source/autotests/threadtest.cpp
T

230 lines
9.1 KiB
C++

/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2004-2014 David Faure <faure@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include <QTest>
#include <QPointer>
#include <QScopeGuard>
#include <QThread>
#include <QTimer>
#include "kio/filecopyjob.h"
#include "kio/transferjob.h"
#include "kiotesthelper.h" // homeTmpDir, createTestFile etc.
#include "workerthread_p.h" // KIO::WorkerThread test hooks
class KIOThreadTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void initTestCase();
void asyncConcurrentCopying();
void copyJobFromThread();
void cancelJobWhileWorkerIsBlocked();
void cancelDoesNotFlushUnrelatedDeferredDeletes();
void cleanupTestCase();
private:
static bool copyLocalFile(const QString &src, const QString &dest);
};
void KIOThreadTest::initTestCase()
{
QStandardPaths::setTestModeEnabled(true);
cleanupTestCase();
homeTmpDir();
}
void KIOThreadTest::cleanupTestCase()
{
QDir(homeTmpDir()).removeRecursively();
// A worker deref'd from inside an event loop (a job completing or being cancelled
// mid-dispatch) deletes its WorkerThread asynchronously through QThread::finished ->
// deleteLater(), rather than joining the thread under a running loop (that join is the
// bug 468673 deadlock). In a running application the next event-loop iteration reaps
// those threads; a test process exits right after its last loop, so on some Qt versions
// the queued DeferredDelete events are never delivered and LeakSanitizer reports the
// worker threads and their QPluginLoaders as leaked. Force-deliver them here.
// This drain used to live in ~Worker(), but flushing the global queue from a destructor
// re-entrantly freed unrelated objects mid-destruction; doing it once from the test
// teardown is the correct scope.
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
}
bool KIOThreadTest::copyLocalFile(const QString &src, const QString &dest)
{
const QUrl u = QUrl::fromLocalFile(src);
const QUrl d = QUrl::fromLocalFile(dest);
std::unique_ptr<KIO::Job> job(KIO::file_copy(u, d, -1, KIO::HideProgressInfo));
job->setUiDelegate(nullptr);
return job->exec();
}
void KIOThreadTest::asyncConcurrentCopying()
{
const int numFiles = 10;
QList<QString> srcs;
QList<QString> dests;
srcs.reserve(numFiles);
dests.reserve(numFiles);
for (int i = 0; i < numFiles; ++i) {
srcs << homeTmpDir() + QLatin1String("file") + QString::number(i);
dests << homeTmpDir() + QLatin1String("file") + QString::number(i) + QLatin1String("_copy");
createTestFile(srcs.last());
}
// All jobs are queued before the event loop runs so the scheduler
// dispatches them concurrently from a single SchedulerPrivate instance.
int completedJobs = 0;
QEventLoop loop;
for (int i = 0; i < numFiles; ++i) {
auto *job = KIO::file_copy(QUrl::fromLocalFile(srcs.at(i)), QUrl::fromLocalFile(dests.at(i)), -1, KIO::HideProgressInfo);
job->setUiDelegate(nullptr);
connect(job, &KJob::result, this, [&completedJobs, numFiles, &loop](KJob *j) {
QVERIFY(!j->error());
if (++completedJobs == numFiles) {
loop.quit();
}
});
}
QTimer::singleShot(std::chrono::seconds(60), &loop, &QEventLoop::quit);
loop.exec();
QCOMPARE(completedJobs, numFiles);
for (const QString &dest : std::as_const(dests)) {
QVERIFY(QFile::exists(dest));
}
}
void KIOThreadTest::copyJobFromThread()
{
const QString src = homeTmpDir() + QLatin1String("src_thread");
const QString dest = homeTmpDir() + QLatin1String("dst_thread");
createTestFile(src);
// One thread: Q_PLUGIN_INSTANCE is not thread-safe for concurrent first access.
bool jobSucceeded = false;
auto *thread = QThread::create([&src, &dest, &jobSucceeded]() {
jobSucceeded = copyLocalFile(src, dest);
});
thread->start();
QVERIFY(thread->wait(30000));
delete thread;
QVERIFY(jobSucceeded);
QVERIFY(QFile::exists(dest));
}
void KIOThreadTest::cancelJobWhileWorkerIsBlocked()
{
// Regression test for the Worker::deref() deadlock (bug 468673). Cancelling a running job
// derefs its in-process worker, and a worker thread that is mid-transfer can only finish
// once the main thread keeps running its event loop (a real worker blocks in
// waitForBytesWritten() until the main thread drains its socket). If deref() joins that
// thread synchronously while we are inside the event loop dispatching the worker's data,
// the main thread blocks on the join instead of running its event loop, and both threads
// wedge. This used to surface only downstream (Dolphin's concurrent directory-size
// listings); reproduce it here. WorkerThread's BUILD_TESTING exit gate makes the worker's
// "needs the main thread to make progress" dependency deterministic: without the fix this
// test deadlocks and is killed by the harness timeout; with it the loop drains cleanly.
KIO::WorkerThread::setTestExitGateEnabled(true);
auto gateGuard = qScopeGuard([] {
// Make sure no worker (including idle ones torn down at exit) can stay gated.
KIO::WorkerThread::setTestExitGateEnabled(false);
KIO::WorkerThread::releaseTestExitGate();
});
const QString path = homeTmpDir() + QLatin1String("bigfile");
const QByteArray bigData(16 * 1024 * 1024, 'a'); // big enough that data() fires mid-transfer
QFile f(path);
QVERIFY(f.open(QIODevice::WriteOnly));
QCOMPARE(f.write(bigData), qint64(bigData.size()));
f.close();
auto *job = KIO::get(QUrl::fromLocalFile(path), KIO::NoReload, KIO::HideProgressInfo);
job->setUiDelegate(nullptr);
QEventLoop loop;
bool cancelled = false;
connect(job, &KIO::TransferJob::data, this, [&](KIO::Job *, const QByteArray &) {
if (cancelled) {
return;
}
cancelled = true;
// Cancel from inside the data dispatch (loopLevel > 0): this derefs the gated worker.
// A synchronous join here would block the main thread before it can release the gate.
job->kill();
// Release the gate and finish - but only from the event loop. With the deadlock bug,
// control never returns here, so the worker stays gated and the test hangs.
QTimer::singleShot(0, this, [] {
KIO::WorkerThread::releaseTestExitGate();
});
QTimer::singleShot(100, &loop, &QEventLoop::quit);
});
// Absolute safety net; with the fix the 100ms timer above quits the loop far sooner.
QTimer::singleShot(std::chrono::seconds(30), &loop, &QEventLoop::quit);
loop.exec();
QVERIFY(cancelled);
}
void KIOThreadTest::cancelDoesNotFlushUnrelatedDeferredDeletes()
{
// Regression test: Worker::~Worker() must not flush the global deferred-delete queue
// (QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete)). ~Worker() runs
// synchronously when a running job is cancelled, and that often happens deep inside an
// unrelated object's destruction. Flushing every pending deleteLater() from there
// re-entrantly deletes objects that are merely scheduled for deletion - or, worse, already
// being destroyed higher up the stack - which crashed on shutdown (a
// new-delete-type-mismatch / use-after-free). Assert the observable behaviour: tearing a
// worker down must not process an unrelated, still-pending deleteLater().
const QString path = homeTmpDir() + QLatin1String("bigfile_flush");
const QByteArray bigData(16 * 1024 * 1024, 'a'); // big enough that data() fires mid-transfer
QFile f(path);
QVERIFY(f.open(QIODevice::WriteOnly));
QCOMPARE(f.write(bigData), qint64(bigData.size()));
f.close();
auto *job = KIO::get(QUrl::fromLocalFile(path), KIO::NoReload, KIO::HideProgressInfo);
job->setUiDelegate(nullptr);
QEventLoop loop;
bool checked = false;
QPointer<QObject> victim;
connect(job, &KIO::TransferJob::data, this, [&](KIO::Job *, const QByteArray &) {
if (checked) {
return;
}
checked = true;
// Schedule an unrelated object for deletion and immediately, in the same
// synchronous block (no event-loop iteration in between, so the deleteLater is
// still pending), cancel the running job. Cancelling derefs and destroys its
// worker synchronously (Worker::kill() -> deref() -> ~Worker()). Only a
// re-entrant flush inside ~Worker() could consume the deleteLater here.
victim = new QObject;
victim->deleteLater();
job->kill();
QVERIFY(victim); // ~Worker() must not have flushed the unrelated deleteLater()
loop.quit();
});
QTimer::singleShot(std::chrono::seconds(30), &loop, &QEventLoop::quit);
loop.exec();
QVERIFY(checked);
QVERIFY(victim); // still alive: the worker teardown did not flush it
// Sanity: a real deferred-delete flush still works and reclaims the victim.
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
QVERIFY(!victim);
}
QTEST_MAIN(KIOThreadTest)
#include "threadtest.moc"