#include "common/font_contract.h" #include "common/ipc.h" #include "common/sandbox.h" #include "common/worker_process.h" #include #include #include #include #include #include #include #include #include #include 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(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(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 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 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("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("stopped-font-session", 1); QSignalSpy failures(worker.get(), &WorkerProcess::failed); std::function 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 complete; QList 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 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 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("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("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"