Files
docview/tests/test_renderer_watchdog.cpp
T
2026-09-21 13:41:40 +09:00

239 lines
13 KiB
C++

#include "web/renderer_watchdog.h"
#include <QCoreApplication>
#include <QProcess>
#include <QSignalSpy>
#include <QTest>
#include <QTimer>
#include <limits>
class RendererWatchdogTests : public QObject {
Q_OBJECT
private slots:
void procStatParsing();
void rendererArgumentParsing();
void rejectsForeignAndNonRenderer();
void terminatesOnlyRegisteredRenderer();
void forgetsExitedRenderer();
void boundedSpreadSlots();
void responseProbeLifetime();
void responseTimeoutStopsOnlyStalledSlot();
void suspensionRestartsResponseBudget();
};
void RendererWatchdogTests::procStatParsing() {
QList<QByteArray> fields;
for (int i = 0; i < 22; ++i) fields << "0";
fields[0] = "S"; fields[1] = "123"; fields[19] = "456"; fields[21] = "789";
auto data = QByteArray("999 (name with ) parens) ") + fields.join(' ');
docview::RendererProcessInfo info;
QVERIFY(docview::parseRendererProcStat(data, 4096, &info));
QCOMPARE(info.parentPid, 123); QCOMPARE(info.startTime, 456u); QCOMPARE(info.residentBytes, 789ull * 4096);
QVERIFY(!docview::parseRendererProcStat(data, 0, &info));
QVERIFY(!docview::parseRendererProcStat(data, std::numeric_limits<quint64>::max(), &info));
QVERIFY(!docview::parseRendererProcStat("invalid", 4096, &info));
fields[21] = "-1";
QVERIFY(!docview::parseRendererProcStat(QByteArray("999 (name) ") + fields.join(' '), 4096, &info));
}
void RendererWatchdogTests::rejectsForeignAndNonRenderer() {
docview::RendererWatchdog watchdog(nullptr, docview::RendererWatchdog::DefaultLimitBytes, QCoreApplication::applicationFilePath());
QString error;
QVERIFY(!watchdog.registerRenderer("foreign", 1, &error));
QVERIFY(!watchdog.registerRenderer("self", QCoreApplication::applicationPid(), &error));
QVERIFY(!watchdog.registerRenderer("overflow", std::numeric_limits<qint64>::max(), &error));
QProcess child;
child.start(QCoreApplication::applicationFilePath(), {"--watchdog-child", "--allocate", "1"});
QVERIFY(child.waitForStarted()); QVERIFY(child.waitForReadyRead());
QVERIFY(!watchdog.registerRenderer("wrong-type", child.processId(), &error));
QCOMPARE(watchdog.monitoredCount(), 0);
QCOMPARE(child.state(), QProcess::Running);
child.kill(); QVERIFY(child.waitForFinished());
}
void RendererWatchdogTests::rendererArgumentParsing() {
const QByteArray executable("/trusted path/QtWebEngineProcess");
QVERIFY(docview::isRendererCommandLine(executable + '\0' + "--type=renderer" + '\0' + "--other=x" + '\0', executable));
QVERIFY(docview::isRendererCommandLine(executable + " --type=renderer --other=x" + '\0', executable));
QVERIFY(!docview::isRendererCommandLine(executable + " --type=renderer-other" + '\0', executable));
QVERIFY(!docview::isRendererCommandLine(executable + " --url=space --type=renderer" + '\0', executable));
QVERIFY(!docview::isRendererCommandLine(executable + "-other --type=renderer" + '\0', executable));
QVERIFY(!docview::isRendererCommandLine(executable + '\0' + "--url= --type=renderer" + '\0', executable));
QVERIFY(!docview::isRendererCommandLine(QByteArray(65537, 'x'), executable));
}
void RendererWatchdogTests::terminatesOnlyRegisteredRenderer() {
#if defined(Q_OS_LINUX) || defined(Q_OS_WIN)
QProcess renderer, sibling;
renderer.start(QCoreApplication::applicationFilePath(), {"--watchdog-child", "--type=renderer", "--allocate", "64"});
sibling.start(QCoreApplication::applicationFilePath(), {"--watchdog-child", "--allocate", "1"});
QVERIFY(renderer.waitForStarted()); QVERIFY(renderer.waitForReadyRead());
QVERIFY(sibling.waitForStarted()); QVERIFY(sibling.waitForReadyRead());
docview::RendererWatchdog watchdog(nullptr, 32ull * 1024 * 1024, QCoreApplication::applicationFilePath());
QString error;
QVERIFY2(watchdog.registerRenderer("test-session", renderer.processId(), &error), qPrintable(error));
QSignalSpy exceeded(&watchdog, &docview::RendererWatchdog::limitExceeded);
watchdog.checkNow();
QTRY_COMPARE(exceeded.count(), 1);
QCOMPARE(exceeded.first().first().toString(), "test-session");
QVERIFY(renderer.waitForFinished());
#ifdef Q_OS_WIN
QCOMPARE(renderer.exitCode(), 137);
#else
QCOMPARE(renderer.exitStatus(), QProcess::CrashExit);
#endif
QCOMPARE(sibling.state(), QProcess::Running);
sibling.kill(); QVERIFY(sibling.waitForFinished());
QCOMPARE(watchdog.monitoredCount(), 0);
#else
docview::RendererWatchdog watchdog; QString error;
QVERIFY(!watchdog.registerRenderer("session", 123, &error)); QCOMPARE(error, "E_SANDBOX_UNAVAILABLE");
#endif
}
void RendererWatchdogTests::forgetsExitedRenderer() {
#if defined(Q_OS_LINUX) || defined(Q_OS_WIN)
QProcess renderer;
renderer.start(QCoreApplication::applicationFilePath(), {"--watchdog-child", "--type=renderer", "--allocate", "1"});
QVERIFY(renderer.waitForStarted()); QVERIFY(renderer.waitForReadyRead());
docview::RendererWatchdog watchdog(nullptr, docview::RendererWatchdog::DefaultLimitBytes, QCoreApplication::applicationFilePath());
QString error;
QVERIFY2(watchdog.registerRenderer("session", renderer.processId(), &error), qPrintable(error));
QSignalSpy exceeded(&watchdog, &docview::RendererWatchdog::limitExceeded);
renderer.kill(); QVERIFY(renderer.waitForFinished());
watchdog.checkNow();
QCOMPARE(watchdog.monitoredCount(), 0); QCOMPARE(exceeded.count(), 0);
#endif
}
void RendererWatchdogTests::boundedSpreadSlots() {
#if defined(Q_OS_LINUX) || defined(Q_OS_WIN)
QProcess first, second;
const QStringList args{"--watchdog-child", "--type=renderer", "--allocate", "1"};
first.start(QCoreApplication::applicationFilePath(), args);
second.start(QCoreApplication::applicationFilePath(), args);
QVERIFY(first.waitForStarted()); QVERIFY(first.waitForReadyRead());
QVERIFY(second.waitForStarted()); QVERIFY(second.waitForReadyRead());
docview::RendererWatchdog watchdog(nullptr, docview::RendererWatchdog::DefaultLimitBytes, QCoreApplication::applicationFilePath());
QString error;
QVERIFY(watchdog.registerRenderer("spread", first.processId(), &error, 0));
QVERIFY(watchdog.registerRenderer("spread", second.processId(), &error, 1));
QCOMPARE(watchdog.monitoredCount(), 2);
QVERIFY(!watchdog.registerRenderer("spread", first.processId(), &error, 2));
QVERIFY(!watchdog.registerRenderer("spread", 0, &error, -1));
QCOMPARE(watchdog.monitoredCount(), 2);
QVERIFY(watchdog.registerRenderer("spread", first.processId(), &error, 1));
QCOMPARE(watchdog.monitoredCount(), 2); // Shared renderer, independently owned slots.
watchdog.removeSlot("spread", 1);
QCOMPARE(watchdog.monitoredCount(), 1);
watchdog.checkNow();
QCOMPARE(first.state(), QProcess::Running); QCOMPARE(second.state(), QProcess::Running);
QVERIFY(watchdog.registerRenderer("spread", second.processId(), &error, 1));
first.kill(); QVERIFY(first.waitForFinished()); watchdog.checkNow();
QCOMPARE(watchdog.monitoredCount(), 1); // Exited primary must not unmonitor companion.
watchdog.removeSession("spread"); QCOMPARE(watchdog.monitoredCount(), 0);
second.kill(); QVERIFY(second.waitForFinished());
#endif
}
void RendererWatchdogTests::responseProbeLifetime() {
#if defined(Q_OS_LINUX) || defined(Q_OS_WIN)
QProcess first, replacement;
const QStringList args{"--watchdog-child", "--type=renderer", "--allocate", "1"};
first.start(QCoreApplication::applicationFilePath(), args);
replacement.start(QCoreApplication::applicationFilePath(), args);
QVERIFY(first.waitForStarted()); QVERIFY(first.waitForReadyRead());
QVERIFY(replacement.waitForStarted()); QVERIFY(replacement.waitForReadyRead());
docview::RendererWatchdog productionExecutable;
QVERIFY(!productionExecutable.registerRenderer("impostor", first.processId()));
docview::RendererWatchdog watchdog(nullptr, docview::RendererWatchdog::DefaultLimitBytes, QCoreApplication::applicationFilePath(), 1000);
QVERIFY(!watchdog.beginProbe("absent", 0));
QVERIFY(watchdog.registerRenderer("session", first.processId()));
const auto firstProbe = watchdog.beginProbe("session", 0);
QVERIFY(firstProbe); QCOMPARE(watchdog.beginProbe("session", 0), 0u);
QVERIFY(!watchdog.acknowledgeProbe("session", 1, firstProbe));
QVERIFY(watchdog.acknowledgeProbe("session", 0, firstProbe));
const auto oldDocumentProbe = watchdog.beginProbe("session", 0);
QVERIFY(oldDocumentProbe != firstProbe);
// Navigation cancellation invalidates only the exact outstanding probe.
QVERIFY(watchdog.acknowledgeProbe("session", 0, oldDocumentProbe));
const auto oldProcessProbe = watchdog.beginProbe("session", 0);
QVERIFY(!watchdog.acknowledgeProbe("session", 0, oldDocumentProbe));
QVERIFY(watchdog.registerRenderer("session", replacement.processId()));
const auto current = watchdog.beginProbe("session", 0);
QVERIFY(current && current != oldProcessProbe);
QVERIFY(!watchdog.acknowledgeProbe("session", 0, oldProcessProbe));
// Two visible views can share a renderer but retain independent deadlines.
QVERIFY(watchdog.registerRenderer("session", replacement.processId(), nullptr, 1));
const auto companion = watchdog.beginProbe("session", 1);
QVERIFY(companion && companion != current);
QVERIFY(watchdog.acknowledgeProbe("session", 0, current));
QCOMPARE(watchdog.beginProbe("session", 1), 0u);
watchdog.removeSession("session");
QVERIFY(!watchdog.acknowledgeProbe("session", 1, companion));
watchdog.checkNow();
QCOMPARE(first.state(), QProcess::Running); QCOMPARE(replacement.state(), QProcess::Running);
first.kill(); replacement.kill();
QVERIFY(first.waitForFinished()); QVERIFY(replacement.waitForFinished());
#endif
}
void RendererWatchdogTests::responseTimeoutStopsOnlyStalledSlot() {
#if defined(Q_OS_LINUX) || defined(Q_OS_WIN)
QProcess primary, companion;
const QStringList args{"--watchdog-child", "--type=renderer", "--allocate", "1"};
primary.start(QCoreApplication::applicationFilePath(), args);
companion.start(QCoreApplication::applicationFilePath(), args);
QVERIFY(primary.waitForStarted()); QVERIFY(primary.waitForReadyRead());
QVERIFY(companion.waitForStarted()); QVERIFY(companion.waitForReadyRead());
docview::RendererWatchdog watchdog(nullptr, docview::RendererWatchdog::DefaultLimitBytes, QCoreApplication::applicationFilePath(), 100);
QVERIFY(watchdog.registerRenderer("spread", primary.processId()));
QVERIFY(watchdog.registerRenderer("spread", companion.processId(), nullptr, 1));
const auto successful = watchdog.beginProbe("spread", 0);
QVERIFY(watchdog.acknowledgeProbe("spread", 0, successful));
QVERIFY(watchdog.beginProbe("spread", 1));
QSignalSpy timedOut(&watchdog, &docview::RendererWatchdog::responseTimedOut);
QSignalSpy memory(&watchdog, &docview::RendererWatchdog::limitExceeded);
QTest::qSleep(150); watchdog.checkNow();
QCOMPARE(timedOut.count(), 1); QCOMPARE(memory.count(), 0);
QCOMPARE(timedOut.first(), QVariantList({"spread", "E_RENDERER_TIMEOUT"}));
QVERIFY(companion.waitForFinished());
QCOMPARE(primary.state(), QProcess::Running);
QCOMPARE(watchdog.monitoredCount(), 0); // Controller disposes the whole failed session.
primary.kill(); QVERIFY(primary.waitForFinished());
#endif
}
void RendererWatchdogTests::suspensionRestartsResponseBudget() {
#if defined(Q_OS_LINUX) || defined(Q_OS_WIN)
QProcess renderer;
renderer.start(QCoreApplication::applicationFilePath(), {"--watchdog-child", "--type=renderer", "--allocate", "1"});
QVERIFY(renderer.waitForStarted()); QVERIFY(renderer.waitForReadyRead());
docview::RendererWatchdog watchdog(nullptr, docview::RendererWatchdog::DefaultLimitBytes, QCoreApplication::applicationFilePath(), 100);
QVERIFY(watchdog.registerRenderer("session", renderer.processId()));
const auto probe = watchdog.beginProbe("session", 0);
QSignalSpy timedOut(&watchdog, &docview::RendererWatchdog::responseTimedOut);
QTest::qSleep(5100); watchdog.checkNow(); // No GUI callbacks during a host pause.
QCOMPARE(timedOut.count(), 0); QCOMPARE(renderer.state(), QProcess::Running);
QVERIFY(watchdog.acknowledgeProbe("session", 0, probe));
QVERIFY(watchdog.beginProbe("session", 0));
QTest::qSleep(150); watchdog.checkNow();
QCOMPARE(timedOut.count(), 1); QVERIFY(renderer.waitForFinished());
#endif
}
int main(int argc, char **argv) {
QCoreApplication app(argc, argv);
if (app.arguments().contains("--watchdog-child")) {
const auto index = app.arguments().indexOf("--allocate");
const int mib = index >= 0 && index + 1 < app.arguments().size() ? app.arguments()[index + 1].toInt() : 1;
if (mib < 1 || mib > 128) return 2;
QByteArray memory(qsizetype(mib) * 1024 * 1024, 'x');
fwrite("ready\n", 1, 6, stdout); fflush(stdout);
QTimer::singleShot(15000, &app, &QCoreApplication::quit);
const int result = app.exec();
return result + (memory.at(0) == 'x' ? 0 : 1);
}
RendererWatchdogTests tests;
return QTest::qExec(&tests, argc, argv);
}
#include "test_renderer_watchdog.moc"