initial commit
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
#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"
|
||||
Reference in New Issue
Block a user