Files
racket-webview-qt/rktwebview_test.cpp
T
2026-08-03 08:22:42 +02:00

302 lines
9.2 KiB
C++

#include <QCoreApplication>
#include <QElapsedTimer>
#include <QFileInfo>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcess>
#include <QStringList>
#include <cstdio>
#include <utility>
#include "rkt_protocol.h"
namespace {
constexpr int protocolVersion = 1;
constexpr int timeoutMs = 10000;
class ProtocolClient
{
public:
explicit ProtocolClient(QString program)
: program_(std::move(program))
{
process_.setProcessChannelMode(QProcess::SeparateChannels);
}
bool start()
{
process_.start(program_, {});
if (!process_.waitForStarted(timeoutMs)) {
fail(QString("Could not start %1: %2")
.arg(program_, process_.errorString()));
return false;
}
const QJsonObject ready = readMessage("ready", -1);
if (ready.isEmpty()) {
return false;
}
if (ready["protocol"].toInt(-1) != protocolVersion) {
fail(QString("Unexpected protocol version: %1")
.arg(ready["protocol"].toInt(-1)));
return false;
}
std::fprintf(stderr, "PASS: backend ready (protocol %d)\n", protocolVersion);
return true;
}
QJsonObject command(int command, const QJsonObject &data = {})
{
const int id = nextId_++;
QJsonObject request;
request["type"] = "command";
request["id"] = id;
request["command"] = command;
request["data"] = data;
const QByteArray json = QJsonDocument(request).toJson(QJsonDocument::Compact) + '\n';
if (process_.write(json) != json.size() || !process_.waitForBytesWritten(timeoutMs)) {
fail(QString("Could not write command %1: %2")
.arg(command)
.arg(process_.errorString()));
return {};
}
return readMessage("result", id);
}
bool stop()
{
const QJsonObject result = command(CMD_QUIT);
if (result.isEmpty() || result["result"].toInt() != RESULT_QUIT) {
fail("QUIT did not return RESULT_QUIT");
process_.kill();
process_.waitForFinished(timeoutMs);
return false;
}
process_.closeWriteChannel();
if (!process_.waitForFinished(timeoutMs)) {
fail("Backend did not stop after QUIT");
process_.kill();
process_.waitForFinished(timeoutMs);
return false;
}
drainStderr();
std::fprintf(stderr, "PASS: backend stopped cleanly\n");
return process_.exitStatus() == QProcess::NormalExit && process_.exitCode() == 0;
}
void abort()
{
if (process_.state() != QProcess::NotRunning) {
process_.kill();
process_.waitForFinished(timeoutMs);
}
drainStderr();
}
private:
QJsonObject readMessage(const QString &expectedType, int expectedId)
{
QElapsedTimer timer;
timer.start();
while (timer.elapsed() < timeoutMs) {
while (process_.canReadLine()) {
const QByteArray line = process_.readLine().trimmed();
if (line.isEmpty()) {
continue;
}
QJsonParseError error;
const QJsonDocument document = QJsonDocument::fromJson(line, &error);
if (error.error != QJsonParseError::NoError || !document.isObject()) {
fail(QString("Invalid JSON from backend: %1").arg(QString::fromUtf8(line)));
return {};
}
const QJsonObject message = document.object();
const QString type = message["type"].toString();
if (type == "event") {
std::fprintf(stderr,
"EVENT: webview=%d %s\n",
message["wv"].toInt(),
message["data"].toString().toUtf8().constData());
continue;
}
if (type == "protocol-error") {
fail(QString("Protocol error: %1").arg(message["message"].toString()));
return {};
}
if (type != expectedType) {
fail(QString("Expected message type %1, got %2")
.arg(expectedType, type));
return {};
}
if (expectedId >= 0 && message["id"].toInt(-1) != expectedId) {
fail(QString("Expected result id %1, got %2")
.arg(expectedId)
.arg(message["id"].toInt(-1)));
return {};
}
return message;
}
drainStderr();
if (process_.state() == QProcess::NotRunning) {
fail(QString("Backend stopped unexpectedly with exit code %1")
.arg(process_.exitCode()));
return {};
}
process_.waitForReadyRead(100);
}
fail(QString("Timed out waiting for %1").arg(expectedType));
return {};
}
void drainStderr()
{
const QByteArray diagnostics = process_.readAllStandardError();
if (!diagnostics.isEmpty()) {
std::fwrite(diagnostics.constData(), 1, diagnostics.size(), stderr);
}
}
static void fail(const QString &message)
{
std::fprintf(stderr, "FAIL: %s\n", message.toUtf8().constData());
}
QString program_;
QProcess process_;
int nextId_ = 1;
};
bool expectResult(const QJsonObject &message, int expected, const char *description)
{
if (message.isEmpty()) {
return false;
}
const int actual = message["result"].toInt();
if (actual != expected) {
std::fprintf(stderr,
"FAIL: %s returned %d, expected %d\n",
description,
actual,
expected);
return false;
}
std::fprintf(stderr, "PASS: %s\n", description);
return true;
}
QString backendProgram(const QCoreApplication &app)
{
const QString environmentProgram = qEnvironmentVariable("RKT_WEBVIEW_PRG");
if (!environmentProgram.isEmpty()) {
return environmentProgram;
}
#ifdef Q_OS_WIN
return app.applicationDirPath() + "/rktwebview_prg.exe";
#else
return app.applicationDirPath() + "/rktwebview_prg";
#endif
}
} // namespace
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
const bool protocolOnly = app.arguments().contains("--protocol-only");
const QString program = backendProgram(app);
if (!QFileInfo::exists(program)) {
std::fprintf(stderr,
"FAIL: backend executable not found: %s\n",
program.toUtf8().constData());
return 1;
}
ProtocolClient client(program);
if (!client.start()) {
client.abort();
return 1;
}
bool ok = true;
ok = expectResult(client.command(CMD_NOOP), 0, "NOOP") && ok;
const QJsonObject initialInfo = client.command(CMD_INFO);
ok = expectResult(initialInfo, 0, "INFO reports no open windows") && ok;
if (!protocolOnly && ok) {
QJsonObject contextData;
contextData["boilerplate_js"] = "";
contextData["has_pem_cert"] = false;
contextData["pem_cert"] = "";
const QJsonObject contextResult = client.command(CMD_CONTEXT_NEW, contextData);
const int context = contextResult["result"].toInt(-1);
if (context < 0) {
std::fprintf(stderr, "FAIL: CONTEXT_NEW returned %d\n", context);
ok = false;
} else {
std::fprintf(stderr, "PASS: context created (%d)\n", context);
}
QJsonObject createData;
createData["context"] = context;
createData["parent"] = 0;
const QJsonObject createResult = client.command(CMD_CREATE_WV, createData);
const int webview = createResult["result"].toInt(-1);
if (webview <= 0) {
std::fprintf(stderr, "FAIL: CREATE_WV returned %d\n", webview);
ok = false;
} else {
std::fprintf(stderr, "PASS: webview created (%d)\n", webview);
}
if (ok) {
QJsonObject titleData;
titleData["wv"] = webview;
titleData["title"] = "rktwebview stdio integration test";
ok = expectResult(client.command(CMD_SET_TITLE, titleData), 0, "SET_TITLE") && ok;
QJsonObject htmlData;
htmlData["wv"] = webview;
htmlData["html"] = "<html><head><title>stdio test</title></head>"
"<body><h1>rktwebview stdio test</h1></body></html>";
ok = expectResult(client.command(CMD_SET_HTML, htmlData), 0, "SET_HTML") && ok;
QJsonObject validData;
validData["wv"] = webview;
ok = expectResult(client.command(CMD_HANDLE_IS_VALID, validData), 1,
"HANDLE_IS_VALID before close") && ok;
QJsonObject closeData;
closeData["wv"] = webview;
ok = expectResult(client.command(CMD_CLOSE_WV, closeData), 0, "CLOSE_WV") && ok;
}
}
const bool stopped = client.stop();
return ok && stopped ? 0 : 1;
}