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

671 lines
31 KiB
C++

#include "common/font_contract.h"
#include "common/ipc.h"
#include "common/sandbox.h"
#include "common/worker_process.h"
#include <QCoreApplication>
#include <QFile>
#include <QLocalSocket>
#include <QSignalSpy>
#include <QTemporaryDir>
#include <QTimer>
#include <QtEndian>
#include <QtTest>
#include <cstdlib>
#include <thread>
using namespace docview;
#ifndef Q_MOC_RUN
namespace {
QCborMap replyTo(const QCborMap &request, const QCborMap &result) {
QCborMap reply = request;
reply.remove(QStringLiteral("payload"));
reply.insert(QStringLiteral("result"), result);
return reply;
}
bool writeRaw(QLocalSocket &socket, const QCborMap &reply) {
// Deliberately bypass the sender's envelope validation: this process is
// the hostile peer, and the test exercises the receiving broker.
const auto bytes = QCborValue(reply).toCbor();
char header[4];
qToBigEndian<quint32>(quint32(bytes.size()), header);
const bool ok = socket.write(header, 4) == 4 && socket.write(bytes) == bytes.size();
socket.flush();
return ok;
}
int fakeWorker(QCoreApplication &app) {
const auto args = app.arguments();
const int socketArgument = args.indexOf("--socket");
const int behaviorArgument = args.indexOf("--fake-worker");
if (socketArgument < 0 || behaviorArgument < 0 || socketArgument + 1 >= args.size() ||
behaviorArgument + 1 >= args.size())
return 120;
const auto behavior = args[behaviorArgument + 1];
QLocalSocket socket;
socket.connectToServer(args[socketArgument + 1]);
if (!socket.waitForConnected(5000))
return 121;
// The socket is the only resource this fixture needs. Use the same
// fail-closed OS confinement as the real worker, with no file grants.
QString sandboxError;
if (!enterSandbox({}, {}, &sandboxError))
return 122;
IpcChannel incoming(&socket);
QObject::connect(&socket, &QLocalSocket::disconnected, &app, &QCoreApplication::quit);
QObject::connect(&incoming, &IpcChannel::protocolError, &app, [] { std::_Exit(123); });
int received = 0;
bool streaming = false;
QCborMap fontParent;
QString selectedFontId;
QByteArray fontBytes;
const auto fontRequest = [&](const QString &operation, const QCborMap &payload) {
auto request = fontParent;
request.insert(QStringLiteral("operation"), operation);
request.insert(QStringLiteral("payload"), payload);
return request;
};
const auto mapRequest = [&] {
return fontRequest("font.map", {{"fontRequestId", 1},
{"faceLocal", QByteArray("Helvetica")},
{"weight", 400},
{"italic", false},
{"charset", 0},
{"pitchFamily", 0}});
};
QObject::connect(&incoming, &IpcChannel::messageReceived, &app, [&](const QCborMap &request) {
if (behavior.startsWith("font-") && isFontOperation(request.value("operation").toString())) {
const auto operation = request.value("operation").toString();
const auto result = request.value("result").toMap();
if (behavior == "font-replay") {
if (!writeRaw(socket, mapRequest()))
std::_Exit(125);
return;
}
if (behavior == "font-flow" || behavior == "font-forged-id" || behavior == "font-overread" ||
behavior == "font-closed-id") {
if (operation == "font.map") {
selectedFontId = result.value("fontId").toString();
const auto id = behavior == "font-forged-id" ? QString(32, 'b') : selectedFontId;
const auto offset = behavior == "font-overread" ? 3 : 0;
if (!writeRaw(socket, fontRequest("font.read", {{"fontRequestId", 2},
{"fontId", id},
{"offset", offset},
{"length", 4}})))
std::_Exit(125);
return;
}
if (operation == "font.read") {
fontBytes = result.value("data").toByteArray();
if (!writeRaw(socket, fontRequest("font.close",
{{"fontRequestId", 3}, {"fontId", selectedFontId}})))
std::_Exit(125);
return;
}
if (behavior == "font-closed-id") {
if (!writeRaw(socket, fontRequest("font.close",
{{"fontRequestId", 4}, {"fontId", selectedFontId}})))
std::_Exit(125);
return;
}
}
if (!writeRaw(socket, replyTo(fontParent, {{"fontResult", result},
{"fontError", request.value("error").toMap()},
{"data", fontBytes},
{"received", received}})))
std::_Exit(125);
return;
}
++received;
const QCborMap result{{"sandboxed", true},
{"received", received},
{"receivedDuringStream", streaming},
{"echo", request.value("payload")}};
auto reply = replyTo(request, result);
if (behavior.startsWith("font-") && request.value("operation").toString() == "render") {
fontParent = request;
auto font = mapRequest();
if (behavior == "font-wrong-parent")
font.insert(QStringLiteral("requestId"), request.value("requestId").toInteger() + 1);
if (behavior == "font-wrong-session")
font.insert(QStringLiteral("sessionId"), "other-session");
if (behavior == "font-wrong-generation")
font.insert(QStringLiteral("generation"), request.value("generation").toInteger() + 1);
if (behavior == "font-bad-schema") {
auto invalid = font.value("payload").toMap();
invalid.insert(QStringLiteral("path"), "/secret/font.ttf");
font.insert(QStringLiteral("payload"), invalid);
}
if (!writeRaw(socket, font))
std::_Exit(125);
if (behavior == "font-double") {
auto second = mapRequest();
auto payload = second.value("payload").toMap();
payload.insert(QStringLiteral("fontRequestId"), 2);
second.insert(QStringLiteral("payload"), payload);
if (!writeRaw(socket, second))
std::_Exit(125);
}
if (behavior == "font-premature-parent" && !writeRaw(socket, reply))
std::_Exit(125);
if (behavior == "font-timeout") {
QTimer::singleShot(20, &app, [&, request] {
auto timeout = request;
timeout.remove(QStringLiteral("payload"));
timeout.insert(QStringLiteral("error"),
QCborMap{{"code", "E_WORKER_TIMEOUT"}, {"message", "fixture timeout"}});
if (!writeRaw(socket, timeout))
std::_Exit(125);
});
}
return;
}
if (behavior == "crash")
std::_Exit(73); // Unexpected exit with a live request; no core file.
if (behavior == "sandbox-unavailable")
std::_Exit(5); // The PDF worker reports sandbox failure with this code.
if (behavior == "close-without-reply")
std::_Exit(0);
if (behavior == "close-reply-exit" && request.value("operation").toString() == "close") {
if (!writeRaw(socket, replyTo(request, {{"closed", true}})))
std::_Exit(125);
while (socket.bytesToWrite() > 0)
if (!socket.waitForBytesWritten(1000))
std::_Exit(126);
std::_Exit(0);
}
if (behavior == "oversize") {
char header[4];
qToBigEndian<quint32>(MaxControlFrame + 1, header);
if (socket.write(header, 4) != 4)
std::_Exit(124);
socket.flush();
return; // No body: rejection must occur at the length boundary.
}
if (behavior == "wrong-session")
reply.insert(QStringLiteral("sessionId"), QStringLiteral("different-session"));
else if (behavior == "wrong-generation")
reply.insert(QStringLiteral("generation"), request.value("generation").toInteger() + 1);
else if (behavior == "wrong-request")
reply.insert(QStringLiteral("requestId"), request.value("requestId").toInteger() + 1);
else if (behavior == "wrong-operation")
reply.insert(QStringLiteral("operation"), QStringLiteral("metadata"));
else if (behavior == "wrong-version")
reply.insert(QStringLiteral("protocolVersion"), 2);
else if (behavior == "error") {
reply.remove(QStringLiteral("result"));
reply.insert(QStringLiteral("error"),
QCborMap{{"code", "E_FIXTURE"}, {"message", "fixture error"}});
} else if (behavior == "illegal-more") {
reply.insert(QStringLiteral("result"), QCborMap{{"more", true}, {"data", QByteArray("chunk")}});
} else if (behavior == "stream" && request.value("operation").toString() == "extract") {
streaming = true;
if (!writeRaw(
socket,
replyTo(request, {{"more", true}, {"data", QByteArray("alpha")}, {"sandboxed", true}})))
std::_Exit(125);
QTimer::singleShot(30, &app, [&, request] {
if (!writeRaw(socket, replyTo(request, {{"more", true}, {"data", QByteArray("beta")}})))
std::_Exit(125);
});
QTimer::singleShot(60, &app, [&, request] {
streaming = false;
if (!writeRaw(socket,
replyTo(request, {{"more", false}, {"size", 9}, {"received", received}})))
std::_Exit(125);
});
return;
}
if (!writeRaw(socket, reply))
std::_Exit(125);
});
return app.exec();
}
bool launch(WorkerProcess &worker, const QString &behavior) {
return worker.start(QCoreApplication::applicationFilePath(), {"--fake-worker", behavior});
}
} // namespace
#endif
class WorkerProcessTest : public QObject {
Q_OBJECT
private slots:
void initTestCase() {
#ifndef Q_OS_LINUX
QSKIP("The production worker sandbox is verified only on Linux; no confinement bypass is used");
#endif
}
void successfulEnvelopesPreserveIdentityAndOrder() {
WorkerProcess worker("broker-session", 37);
QSignalSpy ready(&worker, &WorkerProcess::ready);
QSignalSpy failures(&worker, &WorkerProcess::failed);
QList<QCborMap> replies;
QVERIFY(launch(worker, "echo"));
// Queue before the asynchronous socket connection is ready.
worker.request("ping", {{"ordinal", 1}}, [&](const QCborMap &reply) { replies << reply; });
worker.request("metadata", {{"ordinal", 2}}, [&](const QCborMap &reply) { replies << reply; });
QTRY_COMPARE_WITH_TIMEOUT(replies.size(), 2, 5000);
QCOMPARE(ready.size(), 1);
QVERIFY(worker.isConnected());
QVERIFY(worker.idle());
for (int i = 0; i < replies.size(); ++i) {
const auto reply = replies[i];
QCOMPARE(reply.value("requestId").toInteger(), i + 1);
QCOMPARE(reply.value("sessionId").toString(), "broker-session");
QCOMPARE(reply.value("generation").toInteger(), 37);
QCOMPARE(reply.value("operation").toString(), i == 0 ? "ping" : "metadata");
const auto result = reply.value("result").toMap();
QCOMPARE(result.value("echo").toMap().value("ordinal").toInteger(), i + 1);
QCOMPARE(result.value("received").toInteger(), i + 1);
QVERIFY(result.value("sandboxed").toBool());
}
worker.stop();
QCOMPARE(failures.size(), 0);
}
void fontReverseRpcPreservesParentAndQueue() {
WorkerProcess worker("font-broker-session", 5);
QSignalSpy failures(&worker, &WorkerProcess::failed);
QStringList operations;
QList<QCborMap> replies;
worker.setFontRequestHandler([&](const QString &operation, const QCborMap &payload, auto completion) {
QVERIFY(!payload.contains(QStringLiteral("fontRequestId")));
QVERIFY(!worker.idle());
QCOMPARE(replies.size(), 0);
operations << operation;
QCborMap result;
if (operation == "font.map") {
result = {{"found", true},
{"fontId", QString(32, 'a')},
{"actualFaceLocal", QByteArray("Selected")},
{"charset", 0},
{"size", 4},
{"sha256", QByteArray(32, 's')},
{"faceIndex", 0},
{"substituted", true}};
} else if (operation == "font.read") {
result = {{"fontId", payload.value("fontId")},
{"offset", payload.value("offset")},
{"data", QByteArray("font")}};
} else {
result = {{"released", true}};
}
// A service running on a broker thread may complete off the GUI
// thread. WorkerProcess must post back before touching its channel.
std::thread([completion = std::move(completion), result] { completion(result); }).join();
});
QVERIFY(launch(worker, "font-flow"));
worker.request("render", {}, [&](const QCborMap &reply) { replies << reply; });
worker.request("ping", {}, [&](const QCborMap &reply) { replies << reply; });
QTRY_COMPARE_WITH_TIMEOUT(replies.size(), 2, 5000);
QCOMPARE(operations, QStringList({"font.map", "font.read", "font.close"}));
QCOMPARE(replies[0].value("requestId").toInteger(), 1);
QCOMPARE(replies[0].value("result").toMap().value("data").toByteArray(), QByteArray("font"));
QCOMPARE(replies[0].value("result").toMap().value("received").toInteger(), 1);
QCOMPARE(replies[1].value("result").toMap().value("received").toInteger(), 2);
QVERIFY(worker.idle());
QCOMPARE(failures.size(), 0);
}
void fontRequestWithoutExplicitServiceFails() {
WorkerProcess worker("no-font-service", 1);
QSignalSpy failures(&worker, &WorkerProcess::failed);
int replies = 0;
QVERIFY(launch(worker, "font-map"));
worker.request("render", {}, [&](const QCborMap &) { ++replies; });
QTRY_COMPARE_WITH_TIMEOUT(failures.size(), 1, 5000);
QCOMPARE(failures[0][0].toString(), "E_WORKER_CRASH");
QCOMPARE(replies, 0);
}
void fontRoleUsesExecutableNameRatherThanParentDirectory() {
QTemporaryDir directory(QDir::tempPath() + "/docview-archive-font-test-XXXXXX");
QVERIFY(directory.isValid());
const auto executable = directory.filePath("docview-pdf-worker");
QVERIFY(QFile::link(QCoreApplication::applicationFilePath(), executable));
WorkerProcess worker("font-path-session", 1);
QSignalSpy failures(&worker, &WorkerProcess::failed);
int replies = 0;
worker.setFontRequestHandler(
[](const QString &, const QCborMap &, auto complete) { complete({{"found", false}}); });
QVERIFY(worker.start(executable, {"--fake-worker", "font-map"}));
worker.request("render", {}, [&](const QCborMap &) { ++replies; });
QTRY_COMPARE_WITH_TIMEOUT(replies, 1, 5000);
QCOMPARE(failures.size(), 0);
}
void hostileFontRequest_data() {
QTest::addColumn<QString>("behavior");
for (const auto *behavior : {"font-wrong-parent", "font-wrong-session", "font-wrong-generation",
"font-bad-schema", "font-double", "font-premature-parent", "font-replay",
"font-forged-id", "font-overread", "font-closed-id"})
QTest::newRow(behavior) << QString::fromLatin1(behavior);
}
void hostileFontRequest() {
QFETCH(QString, behavior);
WorkerProcess worker("hostile-font-session", 1);
QSignalSpy failures(&worker, &WorkerProcess::failed);
int replies = 0;
worker.setFontRequestHandler([&](const QString &operation, const QCborMap &payload, auto completion) {
QCborMap result;
if (operation == "font.map")
result = {{"found", true},
{"fontId", QString(32, 'a')},
{"actualFaceLocal", QByteArray("Selected")},
{"charset", 0},
{"size", 4},
{"sha256", QByteArray(32, 's')},
{"faceIndex", 0},
{"substituted", true}};
else if (operation == "font.read")
result = {{"fontId", payload.value("fontId")},
{"offset", payload.value("offset")},
{"data", QByteArray("font")}};
else
result = {{"released", true}};
QTimer::singleShot(10, &worker,
[completion = std::move(completion), result] { completion(result); });
});
QVERIFY(launch(worker, behavior));
worker.request("render", {}, [&](const QCborMap &) { ++replies; });
QTRY_COMPARE_WITH_TIMEOUT(failures.size(), 1, 5000);
QCOMPARE(failures[0][0].toString(), "E_WORKER_CRASH");
QCOMPARE(replies, 0);
}
void fontCompletionAfterStopAndDestructionIsDiscarded() {
auto worker = std::make_unique<WorkerProcess>("stopped-font-session", 1);
QSignalSpy failures(worker.get(), &WorkerProcess::failed);
std::function<void(QCborMap)> complete;
int replies = 0;
worker->setFontRequestHandler(
[&](const QString &, const QCborMap &, auto completion) { complete = std::move(completion); });
QVERIFY(launch(*worker, "font-map"));
worker->request("render", {}, [&](const QCborMap &) { ++replies; });
QTRY_VERIFY_WITH_TIMEOUT(bool(complete), 5000);
worker->stop();
complete({{"found", false}});
QCoreApplication::processEvents();
QCOMPARE(replies, 0);
QCOMPARE(failures.size(), 0);
worker.reset();
std::thread([complete] { complete({{"found", false}}); }).join();
QCoreApplication::processEvents();
QCOMPARE(replies, 0);
}
void parentFontTimeoutDiscardsLateServiceReply() {
WorkerProcess worker("font-timeout-session", 1);
QSignalSpy failures(&worker, &WorkerProcess::failed);
std::function<void(QCborMap)> complete;
QList<QCborMap> replies;
worker.setFontRequestHandler(
[&](const QString &, const QCborMap &, auto completion) { complete = std::move(completion); });
QVERIFY(launch(worker, "font-timeout"));
worker.request("render", {}, [&](const QCborMap &reply) { replies << reply; });
worker.request("ping", {}, [&](const QCborMap &reply) { replies << reply; });
QTRY_COMPARE_WITH_TIMEOUT(replies.size(), 2, 5000);
QCOMPARE(replies[0].value("error").toMap().value("code").toString(), "E_WORKER_TIMEOUT");
QVERIFY(bool(complete));
complete({{"found", false}});
QTest::qWait(20);
QCOMPARE(failures.size(), 0);
QVERIFY(worker.idle());
}
void fontServiceErrorIsReturnedWithSubrequestIdentity() {
WorkerProcess worker("font-error-session", 1);
QSignalSpy failures(&worker, &WorkerProcess::failed);
QList<QCborMap> replies;
worker.setFontRequestHandler([](const QString &, const QCborMap &, auto complete) {
complete({{"error", QCborMap{{"code", "E_FONT_LIMIT"}, {"message", "fixture quota"}}}});
});
QVERIFY(launch(worker, "font-map"));
worker.request("render", {}, [&](const QCborMap &reply) { replies << reply; });
QTRY_COMPARE_WITH_TIMEOUT(replies.size(), 1, 5000);
const auto error = replies[0].value("result").toMap().value("fontError").toMap();
QCOMPARE(error.value("code").toString(), "E_FONT_LIMIT");
QCOMPARE(error.value("fontRequestId").toInteger(), 1);
QCOMPARE(failures.size(), 0);
}
void errorEnvelopeIsDeliveredWithoutBrokerFailure() {
WorkerProcess worker("error-session", 1);
QSignalSpy failures(&worker, &WorkerProcess::failed);
QList<QCborMap> replies;
QVERIFY(launch(worker, "error"));
worker.request("open", {}, [&](const QCborMap &reply) { replies << reply; });
QTRY_COMPARE_WITH_TIMEOUT(replies.size(), 1, 5000);
QCOMPARE(replies.front().value("error").toMap().value("code").toString(), "E_FIXTURE");
QVERIFY(worker.idle());
QCOMPARE(failures.size(), 0);
}
void closeAcknowledgementIsTerminal() {
WorkerProcess worker("close-session", 1);
QSignalSpy failures(&worker, &WorkerProcess::failed);
int closed = 0, afterClose = 0;
QVERIFY(launch(worker, "close-reply-exit"));
worker.request("close", {}, [&](const QCborMap &reply) {
QVERIFY(reply.value("result").toMap().value("closed").toBool());
++closed;
worker.request("ping", {}, [&](const QCborMap &) { ++afterClose; });
});
worker.request("metadata", {}, [&](const QCborMap &) { ++afterClose; });
QTRY_COMPARE_WITH_TIMEOUT(closed, 1, 5000);
QTest::qWait(100);
QCOMPARE(failures.size(), 0);
QCOMPARE(afterClose, 0);
QVERIFY(worker.idle());
QVERIFY(!worker.isConnected());
}
void closeExitWithoutAcknowledgementFails() {
WorkerProcess worker("missing-close-session", 1);
QSignalSpy failures(&worker, &WorkerProcess::failed);
int closed = 0;
QVERIFY(launch(worker, "close-without-reply"));
worker.request("close", {}, [&](const QCborMap &) { ++closed; });
QTRY_COMPARE_WITH_TIMEOUT(failures.size(), 1, 5000);
QCOMPARE(failures.first()[0].toString(), "E_WORKER_CRASH");
QCOMPARE(closed, 0);
QVERIFY(worker.idle());
}
void hostileEnvelope_data() {
QTest::addColumn<QString>("behavior");
for (const auto *behavior : {"wrong-session", "wrong-generation", "wrong-request", "wrong-operation",
"wrong-version", "oversize", "illegal-more"})
QTest::newRow(behavior) << QString::fromLatin1(behavior);
}
void hostileEnvelope() {
QFETCH(QString, behavior);
WorkerProcess worker("correct-session", 7);
QSignalSpy failures(&worker, &WorkerProcess::failed);
int callbacks = 0;
QVERIFY(launch(worker, behavior));
worker.request("ping", {}, [&](const QCborMap &) { ++callbacks; });
worker.request("metadata", {}, [&](const QCborMap &) { ++callbacks; });
QTRY_COMPARE_WITH_TIMEOUT(failures.size(), 1, 5000);
QCOMPARE(failures.front()[0].toString(), "E_WORKER_CRASH");
QVERIFY(!failures.front()[1].toString().isEmpty());
QCOMPARE(callbacks, 0);
QVERIFY(worker.idle());
QTest::qWait(100); // Process termination must not emit a second failure.
QCOMPARE(failures.size(), 1);
QCOMPARE(callbacks, 0);
}
void unexpectedExitWhileActiveFailsOnce() {
WorkerProcess worker("crash-session", 1);
QSignalSpy ready(&worker, &WorkerProcess::ready);
QSignalSpy failures(&worker, &WorkerProcess::failed);
int callbacks = 0;
QVERIFY(launch(worker, "crash"));
worker.request("render", {}, [&](const QCborMap &) { ++callbacks; });
worker.request("pages", {}, [&](const QCborMap &) { ++callbacks; });
QTRY_COMPARE_WITH_TIMEOUT(failures.size(), 1, 5000);
QCOMPARE(ready.size(), 1);
QCOMPARE(failures.front()[0].toString(), "E_WORKER_CRASH");
QTest::qWait(100);
QCOMPARE(failures.size(), 1);
QCOMPARE(callbacks, 0);
QVERIFY(worker.idle());
}
void unavailableSandboxHasSpecificError() {
WorkerProcess worker("sandbox-session", 1);
QSignalSpy failures(&worker, &WorkerProcess::failed);
int callbacks = 0;
QVERIFY(launch(worker, "sandbox-unavailable"));
worker.request("open", {}, [&](const QCborMap &) { ++callbacks; });
QTRY_COMPARE_WITH_TIMEOUT(failures.size(), 1, 5000);
QCOMPARE(failures.front()[0].toString(), "E_SANDBOX_UNAVAILABLE");
QVERIFY(failures.front()[1].toString().contains("保護"));
QTest::qWait(100);
QCOMPARE(failures.size(), 1);
QCOMPARE(callbacks, 0);
QVERIFY(worker.idle());
}
void extractChunksKeepRequestActiveUntilFinal() {
WorkerProcess worker("stream-session", 3);
QSignalSpy failures(&worker, &WorkerProcess::failed);
QStringList delivered;
QByteArray content;
bool final = false;
QVERIFY(launch(worker, "stream"));
worker.request("extract", {{"path", "chapter.xhtml"}}, [&](const QCborMap &reply) {
const auto result = reply.value("result").toMap();
QCOMPARE(reply.value("requestId").toInteger(), 1);
if (result.value("more").toBool()) {
QVERIFY(!worker.idle());
QVERIFY(!final);
content += result.value("data").toByteArray();
delivered << "chunk";
} else {
final = true;
QCOMPARE(result.value("size").toInteger(), 9);
QCOMPARE(result.value("received").toInteger(), 1);
delivered << "final";
}
});
worker.request("ping", {}, [&](const QCborMap &reply) {
QVERIFY(final);
const auto result = reply.value("result").toMap();
QVERIFY(!result.value("receivedDuringStream").toBool());
QCOMPARE(result.value("received").toInteger(), 2);
delivered << "ping";
});
QTRY_COMPARE_WITH_TIMEOUT(delivered.size(), 4, 5000);
QCOMPARE(delivered, QStringList({"chunk", "chunk", "final", "ping"}));
QCOMPARE(content, "alphabeta");
QVERIFY(worker.idle());
QCOMPARE(failures.size(), 0);
}
void stopDropsActiveAndQueuedReplies() {
WorkerProcess worker("stopped-session", 3);
QSignalSpy failures(&worker, &WorkerProcess::failed);
int activeCallbacks = 0, queuedCallbacks = 0;
QVERIFY(launch(worker, "stream"));
worker.request("extract", {}, [&](const QCborMap &reply) {
++activeCallbacks;
QVERIFY(reply.value("result").toMap().value("more").toBool());
QCOMPARE(worker.activeRequestCount(), 1);
QCOMPARE(worker.activeRequestCount("extract"), 1);
QCOMPARE(worker.activeRequestCount("ping"), 0);
QCOMPARE(worker.queuedRequestCount(), 1);
QCOMPARE(worker.queuedRequestCount("ping"), 1);
worker.stop();
QCOMPARE(worker.activeRequestCount(), 0);
QCOMPARE(worker.queuedRequestCount(), 0);
QCOMPARE(worker.discardedQueuedRequestCount(), quint64(1));
worker.stop(); // Repeated stop cannot count the same cancellation twice.
QCOMPARE(worker.discardedQueuedRequestCount(), quint64(1));
});
worker.request("ping", {}, [&](const QCborMap &) { ++queuedCallbacks; });
QTRY_COMPARE_WITH_TIMEOUT(activeCallbacks, 1, 5000);
worker.request("metadata", {}, [&](const QCborMap &) { ++queuedCallbacks; });
QTest::qWait(150);
QCOMPARE(activeCallbacks, 1);
QCOMPARE(queuedCallbacks, 0);
QCOMPARE(failures.size(), 0);
QVERIFY(worker.idle());
QCOMPARE(worker.discardedQueuedRequestCount(), quint64(1));
}
void discardedQueuedRequestsNeverReachWorker_data() {
QTest::addColumn<bool>("selective");
QTest::newRow("operation-only") << true;
QTest::newRow("all-queued") << false;
}
void discardedQueuedRequestsNeverReachWorker() {
QFETCH(bool, selective);
WorkerProcess worker("discard-session", 3);
QSignalSpy failures(&worker, &WorkerProcess::failed);
int activeCallbacks = 0, droppedCallbacks = 0, keptCallbacks = 0;
bool final = false;
const auto kept = [&](const QCborMap &reply) {
++keptCallbacks;
QVERIFY(final);
QCOMPARE(reply.value("result").toMap().value("received").toInteger(), 2);
};
QVERIFY(launch(worker, "stream"));
worker.request("extract", {}, [&](const QCborMap &reply) {
++activeCallbacks;
if (activeCallbacks == 1) {
QCOMPARE(worker.activeRequestCount("extract"), 1);
QCOMPARE(worker.queuedRequestCount(), 3);
QCOMPARE(worker.queuedRequestCount("render"), 2);
QCOMPARE(worker.queuedRequestCount("metadata"), 1);
QCOMPARE(worker.discardedQueuedRequestCount(), quint64(0));
worker.discardQueued(selective ? "render" : QString{});
QCOMPARE(worker.queuedRequestCount(), selective ? 1 : 0);
QCOMPARE(worker.queuedRequestCount("render"), 0);
QCOMPARE(worker.activeRequestCount("extract"), 1);
QCOMPARE(worker.discardedQueuedRequestCount(), quint64(selective ? 2 : 3));
if (!selective)
worker.request("ping", {}, kept);
}
if (!reply.value("result").toMap().value("more").toBool())
final = true;
});
worker.request("render", {{"page", 1}}, [&](const QCborMap &) { ++droppedCallbacks; });
worker.request("render", {{"page", 2}}, [&](const QCborMap &) { ++droppedCallbacks; });
worker.request("metadata", {}, [&](const QCborMap &reply) {
if (selective)
kept(reply);
else
++droppedCallbacks;
});
QTRY_COMPARE_WITH_TIMEOUT(keptCallbacks, 1, 5000);
QCOMPARE(activeCallbacks, 3);
QCOMPARE(droppedCallbacks, 0);
QVERIFY(worker.idle());
QCOMPARE(worker.activeRequestCount(), 0);
QCOMPARE(worker.queuedRequestCount(), 0);
QCOMPARE(worker.discardedQueuedRequestCount(), quint64(selective ? 2 : 3));
QTest::qWait(100);
QCOMPARE(droppedCallbacks, 0);
QCOMPARE(failures.size(), 0);
}
};
int main(int argc, char **argv) {
QCoreApplication app(argc, argv);
if (app.arguments().contains("--fake-worker"))
return fakeWorker(app);
WorkerProcessTest test;
return QTest::qExec(&test, argc, argv);
}
#include "test_worker_process.moc"