initial commit
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
#include "common/ipc.h"
|
||||
#include "common/sandbox.h"
|
||||
#include "common/worker_process.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QCryptographicHash>
|
||||
#include <QElapsedTimer>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QLocalSocket>
|
||||
#include <QSaveFile>
|
||||
#include <QSignalSpy>
|
||||
#include <QTemporaryDir>
|
||||
#include <QTimer>
|
||||
#include <QtTest>
|
||||
#include <cstdlib>
|
||||
|
||||
using namespace docview;
|
||||
|
||||
namespace {
|
||||
int stalledPeer(QCoreApplication &app) {
|
||||
const auto args = app.arguments();
|
||||
const int socketIndex = args.indexOf("--socket");
|
||||
if (socketIndex < 0 || socketIndex + 1 >= args.size())
|
||||
return 120;
|
||||
QLocalSocket socket;
|
||||
socket.connectToServer(args[socketIndex + 1]);
|
||||
if (!socket.waitForConnected(5000))
|
||||
return 121;
|
||||
QString error;
|
||||
if (!enterSandbox({}, {}, &error))
|
||||
return 122;
|
||||
IpcChannel channel(&socket);
|
||||
QCborMap extracting;
|
||||
QTimer chunks;
|
||||
chunks.setInterval(1000);
|
||||
const auto reply = [&](QCborMap request, const QCborMap &result) {
|
||||
request.remove(QStringLiteral("payload"));
|
||||
request.insert(QStringLiteral("result"), result);
|
||||
if (!channel.send(request))
|
||||
std::_Exit(123);
|
||||
};
|
||||
QObject::connect(&chunks, &QTimer::timeout, &app, [&] {
|
||||
// Progress is deliberately nonterminal: it must not reset the resource
|
||||
// deadline or allow a queued request to overtake this extraction.
|
||||
reply(extracting, {{"more", true}, {"data", QByteArray("progress")}});
|
||||
});
|
||||
QObject::connect(&socket, &QLocalSocket::disconnected, &app, &QCoreApplication::quit);
|
||||
QObject::connect(&channel, &IpcChannel::protocolError, &app, [] { std::_Exit(124); });
|
||||
QObject::connect(&channel, &IpcChannel::messageReceived, &app, [&](const QCborMap &request) {
|
||||
const auto op = request.value("operation").toString();
|
||||
if (op == "ping")
|
||||
reply(request, {{"sandboxed", true}, {"pid", QCoreApplication::applicationPid()}});
|
||||
else if (op == "extract") {
|
||||
extracting = request;
|
||||
chunks.start();
|
||||
}
|
||||
// Rendering deliberately never replies. Keep the real socket and
|
||||
// event loop alive until the production broker terminates this peer.
|
||||
});
|
||||
return app.exec();
|
||||
}
|
||||
|
||||
QString digest(const QString &path) {
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
return {};
|
||||
QCryptographicHash hash(QCryptographicHash::Sha256);
|
||||
if (!hash.addData(&file))
|
||||
return {};
|
||||
return QString::fromLatin1(hash.result().toHex());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
class WorkerDeadlinesTest : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void realBudgetsKeepTheEventLoopResponsive() {
|
||||
QTemporaryDir directory;
|
||||
QVERIFY(directory.isValid());
|
||||
const auto archivePath = directory.filePath("docview-archive-worker");
|
||||
QVERIFY(QFile::link(QCoreApplication::applicationFilePath(), archivePath));
|
||||
WorkerProcess pdf("deadline-pdf", 1), archive("deadline-archive", 1), cancelled("cancel-pdf", 1);
|
||||
QSignalSpy pdfFailures(&pdf, &WorkerProcess::failed),
|
||||
archiveFailures(&archive, &WorkerProcess::failed);
|
||||
QSignalSpy cancelFailures(&cancelled, &WorkerProcess::failed);
|
||||
qint64 pdfPid = 0, archivePid = 0, cancelPid = 0;
|
||||
int handshakes = 0;
|
||||
for (auto pair : {qMakePair(&pdf, &pdfPid), qMakePair(&archive, &archivePid),
|
||||
qMakePair(&cancelled, &cancelPid)}) {
|
||||
QVERIFY(pair.first->start(pair.first == &archive ? archivePath
|
||||
: QCoreApplication::applicationFilePath(),
|
||||
{"--stalled-peer"}));
|
||||
pair.first->request("ping", {}, [&, pid = pair.second](const QCborMap &value) {
|
||||
const auto result = value.value("result").toMap();
|
||||
if (result.value("sandboxed").toBool()) {
|
||||
*pid = result.value("pid").toInteger();
|
||||
++handshakes;
|
||||
}
|
||||
});
|
||||
}
|
||||
QTRY_COMPARE_WITH_TIMEOUT(handshakes, 3, 5000);
|
||||
QElapsedTimer clock;
|
||||
clock.start();
|
||||
qint64 pdfSlow = -1, archiveSlow = -1, pdfStopped = -1, archiveStopped = -1;
|
||||
qint64 cancelDuration = -1, lastHeartbeat = 0, maxHeartbeatGap = 0;
|
||||
int pdfSlowCount = 0, archiveSlowCount = 0, terminalReplies = 0, chunks = 0, heartbeats = 0;
|
||||
connect(&pdf, &WorkerProcess::slow, this, [&] {
|
||||
pdfSlow = clock.elapsed();
|
||||
++pdfSlowCount;
|
||||
});
|
||||
connect(&archive, &WorkerProcess::slow, this, [&] {
|
||||
archiveSlow = clock.elapsed();
|
||||
++archiveSlowCount;
|
||||
});
|
||||
connect(&pdf, &WorkerProcess::failed, this, [&] { pdfStopped = clock.elapsed(); });
|
||||
connect(&archive, &WorkerProcess::failed, this, [&] { archiveStopped = clock.elapsed(); });
|
||||
QTimer heartbeat;
|
||||
heartbeat.setInterval(10);
|
||||
connect(&heartbeat, &QTimer::timeout, this, [&] {
|
||||
const auto now = clock.elapsed();
|
||||
maxHeartbeatGap = qMax(maxHeartbeatGap, now - lastHeartbeat);
|
||||
lastHeartbeat = now;
|
||||
++heartbeats;
|
||||
});
|
||||
heartbeat.start();
|
||||
pdf.request("render", {}, [&](const QCborMap &) { ++terminalReplies; });
|
||||
pdf.request("ping", {}, [&](const QCborMap &) { ++terminalReplies; });
|
||||
archive.request("extract", {}, [&](const QCborMap &value) {
|
||||
if (value.value("result").toMap().value("more").toBool())
|
||||
++chunks;
|
||||
else
|
||||
++terminalReplies;
|
||||
});
|
||||
archive.request("ping", {}, [&](const QCborMap &) { ++terminalReplies; });
|
||||
cancelled.request("render", {}, [&](const QCborMap &) { ++terminalReplies; });
|
||||
cancelled.request("ping", {}, [&](const QCborMap &) { ++terminalReplies; });
|
||||
QTimer::singleShot(200, &cancelled, [&] {
|
||||
QElapsedTimer stopClock;
|
||||
stopClock.start();
|
||||
cancelled.stop();
|
||||
cancelDuration = stopClock.elapsed();
|
||||
});
|
||||
QTRY_VERIFY_WITH_TIMEOUT(pdfStopped >= 0 && archiveStopped >= 0, 125000);
|
||||
QTest::qWait(100);
|
||||
heartbeat.stop();
|
||||
QCOMPARE(pdfFailures.size(), 1);
|
||||
QCOMPARE(archiveFailures.size(), 1);
|
||||
QCOMPARE(cancelFailures.size(), 0);
|
||||
QCOMPARE(pdfFailures[0][0].toString(), "E_WORKER_TIMEOUT");
|
||||
QCOMPARE(archiveFailures[0][0].toString(), "E_WORKER_TIMEOUT");
|
||||
QCOMPARE(pdfSlowCount, 1);
|
||||
QCOMPARE(archiveSlowCount, 1);
|
||||
QVERIFY2(pdfSlow >= 30000 && pdfSlow < 34000, qPrintable(QString::number(pdfSlow)));
|
||||
QVERIFY2(archiveSlow >= 10000 && archiveSlow < 14000, qPrintable(QString::number(archiveSlow)));
|
||||
QVERIFY2(pdfStopped >= 120000 && pdfStopped < 124000, qPrintable(QString::number(pdfStopped)));
|
||||
QVERIFY2(archiveStopped >= 30000 && archiveStopped < 34000,
|
||||
qPrintable(QString::number(archiveStopped)));
|
||||
QVERIFY(cancelDuration >= 0 && cancelDuration < 1000);
|
||||
QVERIFY(heartbeats > 5000);
|
||||
QVERIFY2(maxHeartbeatGap < 1500, qPrintable(QString::number(maxHeartbeatGap)));
|
||||
QVERIFY(chunks >= 20);
|
||||
QCOMPARE(terminalReplies, 0);
|
||||
QVERIFY(!pdf.isConnected() && !archive.isConnected() && !cancelled.isConnected());
|
||||
for (const auto pid : {pdfPid, archivePid, cancelPid}) {
|
||||
QVERIFY(pid > 0);
|
||||
QVERIFY(!QFileInfo::exists(QStringLiteral("/proc/%1").arg(pid)));
|
||||
}
|
||||
const QJsonObject report{
|
||||
{"success", true},
|
||||
{"platform", "Linux; sandboxed stalled IPC fixture; production WorkerProcess timers"},
|
||||
{"testExecutableSha256", digest(QCoreApplication::applicationFilePath())},
|
||||
{"commonLibrarySha256", digest(QCoreApplication::applicationDirPath() + "/libdocview_common.a")},
|
||||
{"pdfSlowMs", pdfSlow},
|
||||
{"pdfStopMs", pdfStopped},
|
||||
{"archiveSlowMs", archiveSlow},
|
||||
{"archiveStopMs", archiveStopped},
|
||||
{"cancelDurationMs", cancelDuration},
|
||||
{"heartbeatCount", heartbeats},
|
||||
{"maxHeartbeatGapMs", maxHeartbeatGap},
|
||||
{"archiveProgressChunks", chunks},
|
||||
{"queuedOrTerminalReplies", terminalReplies},
|
||||
{"childrenReaped", true},
|
||||
{"unrelatedUserDocuments", "none"}};
|
||||
qInfo().noquote() << QJsonDocument(report).toJson(QJsonDocument::Compact);
|
||||
const auto reportPath = qEnvironmentVariable("DOCVIEW_WATCHDOG_REPORT");
|
||||
if (!reportPath.isEmpty()) {
|
||||
QSaveFile output(reportPath);
|
||||
QVERIFY(output.open(QIODevice::WriteOnly));
|
||||
const auto data = QJsonDocument(report).toJson();
|
||||
QCOMPARE(output.write(data), data.size());
|
||||
QVERIFY(output.commit());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
QCoreApplication app(argc, argv);
|
||||
if (app.arguments().contains("--stalled-peer"))
|
||||
return stalledPeer(app);
|
||||
WorkerDeadlinesTest test;
|
||||
return QTest::qExec(&test, argc, argv);
|
||||
}
|
||||
|
||||
#include "test_worker_deadlines.moc"
|
||||
Reference in New Issue
Block a user