#include #include #include #include #include #include #include #include #include #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"] = "stdio test" "

rktwebview stdio test

"; 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; }