259 lines
13 KiB
C++
259 lines
13 KiB
C++
#include "common/font_contract.h"
|
|
#include "common/ipc.h"
|
|
#include "common/worker_font_client.h"
|
|
#include "common/worker_runtime.h"
|
|
|
|
#include <QElapsedTimer>
|
|
#include <QLocalServer>
|
|
#include <QLocalSocket>
|
|
#include <QSignalSpy>
|
|
#include <QTimer>
|
|
#include <QtTest>
|
|
#include <limits>
|
|
|
|
using namespace docview;
|
|
namespace {
|
|
const QString fontId(32, 'a');
|
|
QCborMap mapPayload(qint64 id = 1) {
|
|
return {{"fontRequestId", id}, {"faceLocal", QByteArray("Helvetica")},
|
|
{"weight", 400}, {"italic", false},
|
|
{"charset", 0}, {"pitchFamily", 0}};
|
|
}
|
|
QCborMap selected(qint64 id = 1) {
|
|
return {{"fontRequestId", id},
|
|
{"found", true},
|
|
{"fontId", fontId},
|
|
{"actualFaceLocal", QByteArray("Actual Face")},
|
|
{"charset", 0},
|
|
{"size", 4},
|
|
{"sha256", QByteArray(32, 'x')},
|
|
{"faceIndex", 0},
|
|
{"substituted", true}};
|
|
}
|
|
QCborMap parentRequest() {
|
|
return {{"protocolVersion", 1}, {"sessionId", "font-session"}, {"generation", 3},
|
|
{"requestId", 7}, {"operation", "render"}, {"payload", QCborMap{{"page", 1}}}};
|
|
}
|
|
QCborMap resultReply(QCborMap request, QCborMap result) {
|
|
request.remove(QStringLiteral("payload"));
|
|
request.insert(QStringLiteral("result"), result);
|
|
return request;
|
|
}
|
|
struct ChannelPair {
|
|
QLocalServer server;
|
|
WorkerRuntime runtime;
|
|
QLocalSocket *peer = nullptr;
|
|
bool open() {
|
|
server.setSocketOptions(QLocalServer::UserAccessOption);
|
|
if (!server.listen("font-contract-" + QUuid::createUuid().toString(QUuid::Id128)))
|
|
return false;
|
|
auto socket = std::make_unique<QLocalSocket>();
|
|
socket->connectToServer(server.fullServerName());
|
|
if (!socket->waitForConnected(1000) || !server.waitForNewConnection(1000))
|
|
return false;
|
|
peer = server.nextPendingConnection();
|
|
runtime.transport = std::move(socket);
|
|
return peer;
|
|
}
|
|
};
|
|
} // namespace
|
|
|
|
class FontContractTest : public QObject {
|
|
Q_OBJECT
|
|
private slots:
|
|
void strictMapSchema() {
|
|
const auto request = mapPayload();
|
|
QVERIFY(validateFontPayload("font.map", request));
|
|
auto emptyName = request;
|
|
emptyName.insert(QStringLiteral("faceLocal"), QByteArray{});
|
|
QVERIFY(validateFontPayload("font.map", emptyName));
|
|
const QList<QPair<QString, QCborValue>> bad{{"faceLocal", "string coercion"},
|
|
{"faceLocal", QByteArray(257, 'a')},
|
|
{"faceLocal", QByteArray("a\0b", 3)},
|
|
{"weight", -1},
|
|
{"weight", 1001},
|
|
{"weight", 400.0},
|
|
{"italic", 1},
|
|
{"charset", 130},
|
|
{"charset", "0"},
|
|
{"pitchFamily", 3},
|
|
{"pitchFamily", 0x60},
|
|
{"fontRequestId", 0},
|
|
{"fontRequestId", 1.0},
|
|
{"path", "/secret/font.ttf"}};
|
|
for (const auto &[field, value] : bad) {
|
|
auto invalid = request;
|
|
invalid.insert(field, value);
|
|
QVERIFY2(!validateFontPayload("font.map", invalid), qPrintable(field));
|
|
}
|
|
auto missing = request;
|
|
missing.remove(QStringLiteral("charset"));
|
|
QVERIFY(!validateFontPayload("font.map", missing));
|
|
QVERIFY(!validateFontPayload("font.list", request));
|
|
}
|
|
void boundedReadSchema() {
|
|
QCborMap request{
|
|
{"fontRequestId", 2}, {"fontId", fontId}, {"offset", 0}, {"length", MaxFontReadBytes}};
|
|
QVERIFY(validateFontPayload("font.read", request));
|
|
for (const auto badLength :
|
|
{qint64(-1), qint64(0), qint64(MaxFontReadBytes + 1), std::numeric_limits<qint64>::max()}) {
|
|
auto invalid = request;
|
|
invalid.insert(QStringLiteral("length"), badLength);
|
|
QVERIFY(!validateFontPayload("font.read", invalid));
|
|
}
|
|
for (const auto badOffset :
|
|
{qint64(-1), MaxFontSnapshotBytes - 1, std::numeric_limits<qint64>::max()}) {
|
|
auto invalid = request;
|
|
invalid.insert(QStringLiteral("offset"), badOffset);
|
|
QVERIFY(!validateFontPayload("font.read", invalid));
|
|
}
|
|
request.insert(QStringLiteral("offset"), MaxFontSnapshotBytes - MaxFontReadBytes);
|
|
QVERIFY(validateFontPayload("font.read", request));
|
|
request.insert(QStringLiteral("fontId"), fontId.toUpper());
|
|
QVERIFY(!validateFontPayload("font.read", request));
|
|
QVERIFY(validateFontPayload("font.close", {{"fontRequestId", 3}, {"fontId", fontId}}));
|
|
}
|
|
void selectedMetadataAndReadRepliesAreExact() {
|
|
const auto request = mapPayload();
|
|
QVERIFY(validateFontResult("font.map", request, selected()));
|
|
auto offset = selected();
|
|
offset.remove(QStringLiteral("faceIndex"));
|
|
offset.insert(QStringLiteral("faceOffset"), 3);
|
|
QVERIFY(validateFontResult("font.map", request, offset));
|
|
offset.insert(QStringLiteral("faceOffset"), 4);
|
|
QVERIFY(!validateFontResult("font.map", request, offset));
|
|
const QList<QPair<QString, QCborValue>> bad{{"fontRequestId", 2},
|
|
{"fontId", QByteArray(32, 'a')},
|
|
{"fontId", QString(31, 'a')},
|
|
{"actualFaceLocal", QByteArray{}},
|
|
{"charset", 255},
|
|
{"size", 0},
|
|
{"size", MaxFontSnapshotBytes + 1},
|
|
{"sha256", QByteArray(31, 'x')},
|
|
{"sha256", QString(64, 'a')},
|
|
{"faceIndex", -1},
|
|
{"faceIndex", 65536},
|
|
{"faceOffset", 0},
|
|
{"substituted", 1},
|
|
{"more", false}};
|
|
for (const auto &[field, value] : bad) {
|
|
auto invalid = selected();
|
|
invalid.insert(field, value);
|
|
QVERIFY2(!validateFontResult("font.map", request, invalid), qPrintable(field));
|
|
}
|
|
QVERIFY(validateFontResult("font.map", request, {{"fontRequestId", 1}, {"found", false}}));
|
|
QVERIFY(!validateFontResult("font.map", request,
|
|
{{"fontRequestId", 1}, {"found", false}, {"fontId", fontId}}));
|
|
const QCborMap read{{"fontRequestId", 2}, {"fontId", fontId}, {"offset", 1}, {"length", 3}};
|
|
const QCborMap good{
|
|
{"fontRequestId", 2}, {"fontId", fontId}, {"offset", 1}, {"data", QByteArray("abc")}};
|
|
QVERIFY(validateFontResult("font.read", read, good));
|
|
for (const auto &[field, value] : QList<QPair<QString, QCborValue>>{{"fontId", QString(32, 'b')},
|
|
{"offset", 0},
|
|
{"offset", 1.0},
|
|
{"data", QByteArray("ab")},
|
|
{"data", "abc"},
|
|
{"more", true}}) {
|
|
auto invalid = good;
|
|
invalid.insert(field, value);
|
|
QVERIFY(!validateFontResult("font.read", read, invalid));
|
|
}
|
|
}
|
|
void errorAndEnvelopeRoleValidation() {
|
|
QCborMap error{{"fontRequestId", 1}, {"code", "E_FONT_LIMIT"}, {"message", "Font quota reached"}};
|
|
QVERIFY(validateFontError(error));
|
|
error.insert(QStringLiteral("code"), "E_UNREGISTERED");
|
|
QVERIFY(!validateFontError(error));
|
|
auto envelope = parentRequest();
|
|
envelope.insert(QStringLiteral("operation"), "font.map");
|
|
envelope.insert(QStringLiteral("payload"), mapPayload());
|
|
QVERIFY(validateEnvelope(envelope));
|
|
envelope.insert(QStringLiteral("unexpected"), true);
|
|
QVERIFY(!validateEnvelope(envelope));
|
|
}
|
|
void synchronousClientRoutesMultipleQueuedResponses() {
|
|
ChannelPair pair;
|
|
QVERIFY(pair.open());
|
|
IpcChannel incoming(pair.runtime.transport.get()), broker(pair.peer);
|
|
WorkerFontClient client(&incoming, &pair.runtime);
|
|
QSignalSpy errors(&client, &WorkerFontClient::protocolError);
|
|
connect(&incoming, &IpcChannel::messageReceived, &client,
|
|
[&](const QCborMap &reply) { QVERIFY(client.routeReply(reply)); });
|
|
QList<qint64> ids;
|
|
connect(&broker, &IpcChannel::messageReceived, &broker, [&](const QCborMap &request) {
|
|
QCOMPARE(request.value("requestId").toInteger(), 7);
|
|
QCOMPARE(request.value("generation").toInteger(), 3);
|
|
const auto id = request.value("payload").toMap().value("fontRequestId").toInteger();
|
|
ids << id;
|
|
QTimer::singleShot(0, &broker, [&, request, id] {
|
|
QVERIFY(broker.send(resultReply(request, {{"fontRequestId", id}, {"found", false}})));
|
|
});
|
|
});
|
|
QVERIFY(client.setParentRequest(parentRequest()));
|
|
auto payload = mapPayload();
|
|
payload.remove(QStringLiteral("fontRequestId"));
|
|
QCOMPARE(client.call("font.map", payload), QCborMap({{"found", false}}));
|
|
QCOMPARE(client.call("font.map", payload), QCborMap({{"found", false}}));
|
|
QCOMPARE(ids, QList<qint64>({1, 2}));
|
|
QVERIFY(!client.failed());
|
|
QCOMPARE(errors.size(), 0);
|
|
client.clearParentRequest();
|
|
}
|
|
void clientRejectsMismatchedReply() {
|
|
ChannelPair pair;
|
|
QVERIFY(pair.open());
|
|
IpcChannel incoming(pair.runtime.transport.get()), broker(pair.peer);
|
|
WorkerFontClient client(&incoming, &pair.runtime);
|
|
QSignalSpy errors(&client, &WorkerFontClient::protocolError);
|
|
connect(&incoming, &IpcChannel::messageReceived, &client,
|
|
[&](const QCborMap &reply) { client.routeReply(reply); });
|
|
connect(&broker, &IpcChannel::messageReceived, &broker, [&](const QCborMap &request) {
|
|
QVERIFY(broker.send(resultReply(request, {{"fontRequestId", 9}, {"found", false}})));
|
|
});
|
|
QVERIFY(client.setParentRequest(parentRequest()));
|
|
auto payload = mapPayload();
|
|
payload.remove(QStringLiteral("fontRequestId"));
|
|
QCOMPARE(client.call("font.map", payload).value("error").toMap().value("code").toString(),
|
|
"E_WORKER_CRASH");
|
|
QCOMPARE(errors.size(), 1);
|
|
QVERIFY(client.failed());
|
|
client.clearParentRequest();
|
|
}
|
|
void clientTimeoutIsStickyAndDoesNotSendAnotherRequest() {
|
|
ChannelPair pair;
|
|
QVERIFY(pair.open());
|
|
IpcChannel incoming(pair.runtime.transport.get()), broker(pair.peer);
|
|
WorkerFontClient client(&incoming, &pair.runtime);
|
|
QSignalSpy requests(&broker, &IpcChannel::messageReceived);
|
|
QVERIFY(client.setParentRequest(parentRequest()));
|
|
auto payload = mapPayload();
|
|
payload.remove(QStringLiteral("fontRequestId"));
|
|
QElapsedTimer elapsed;
|
|
elapsed.start();
|
|
const auto failure = client.call("font.map", payload, 20);
|
|
QCOMPARE(failure.value("error").toMap().value("code").toString(), "E_WORKER_TIMEOUT");
|
|
QVERIFY(elapsed.elapsed() < 1000);
|
|
QCOMPARE(client.call("font.map", payload), failure);
|
|
QCOMPARE(requests.size(), 1);
|
|
client.clearParentRequest();
|
|
}
|
|
void clientTransportDisconnectEndsWaitImmediately() {
|
|
ChannelPair pair;
|
|
QVERIFY(pair.open());
|
|
IpcChannel incoming(pair.runtime.transport.get()), broker(pair.peer);
|
|
WorkerFontClient client(&incoming, &pair.runtime);
|
|
QVERIFY(client.setParentRequest(parentRequest()));
|
|
auto payload = mapPayload();
|
|
payload.remove(QStringLiteral("fontRequestId"));
|
|
QTimer::singleShot(10, pair.peer, &QLocalSocket::close);
|
|
QElapsedTimer elapsed;
|
|
elapsed.start();
|
|
QCOMPARE(client.call("font.map", payload).value("error").toMap().value("code").toString(),
|
|
"E_WORKER_CRASH");
|
|
QVERIFY(elapsed.elapsed() < 1000);
|
|
client.clearParentRequest();
|
|
}
|
|
};
|
|
QTEST_GUILESS_MAIN(FontContractTest)
|
|
#include "test_font_contract.moc"
|