/**************************************************************************** ** ** Copyright (C) 2018 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the Qt WebGL module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:GPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 or (at your option) any later version ** approved by the KDE Free Qt Foundation. The licenses are as published by ** the Free Software Foundation and appearing in the file LICENSE.GPL3 ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "parameters.h" #include #define PORT 29836 #define PORTSTRING QT_STRINGIFY(PORT) class tst_WebGL : public QObject { Q_OBJECT struct GLObject {}; struct Buffer : GLObject {}; struct Shader : GLObject { Shader(GLuint type = GL_INVALID_VALUE) : type(type) {} GLuint type; QString source; bool compiled = false; }; struct Program : GLObject { QMap attached; bool linked = false; }; struct Texture : GLObject {}; struct Context { Context(int winId = -1) : winId(winId) {} int winId; Buffer *arrayBuffer = nullptr; Buffer *elementArrayBuffer = nullptr; Program *program = nullptr; }; QHash contexts; QHash buffers; QHash programs; QHash shaders; QHash textures; Context *currentContext = nullptr; QNetworkAccessManager manager; QWebSocket webSocket; QStringList functions; QProcess process; qintptr websocketPort; void connectToQmlScene(); void sendMouseEvent(Qt::MouseButtons buttons, quint32 x, quint32 y, int winId); void sendMouseClick(quint32 x, quint32 y, int winId); template Struct *pointer(const QVariant &id, QHash &container) { const auto handle = id.toInt(); if (!container.contains(handle - 1)) return nullptr; return &container[handle - 1]; } bool findSwapBuffers(const QSignalSpy &spy); signals: void command(const QString &name, const QVariantList ¶meters); void queryCommand(const QString &name, int id, const QVariantList ¶meters); public slots: void parseTextMessage(const QString &text); void parseBinaryMessage(const QByteArray &data); private slots: void initTestCase(); void init(); void cleanup(); void checkFunctionCount_data(); void checkFunctionCount(); void waitForSwapBuffers_data(); void waitForSwapBuffers(); void reload_data(); void reload(); void update_data(); void update(); }; void tst_WebGL::connectToQmlScene() { const QJsonDocument connectMessage { QJsonObject { { QLatin1String("type"), QLatin1String("connect") }, { QLatin1String("width"), 1920 }, { QLatin1String("height"), 1080 }, { QLatin1String("physicalWidth"), 531.3 }, { QLatin1String("physicalHeight"), 298.9 } } }; QSignalSpy connected(&webSocket, &QWebSocket::connected); webSocket.open(QUrl(QString::fromLatin1("ws://localhost:%1").arg(websocketPort))); QVERIFY(connected.wait()); webSocket.sendTextMessage(connectMessage.toJson()); QVERIFY(webSocket.state() == QAbstractSocket::ConnectedState); } void tst_WebGL::sendMouseEvent(Qt::MouseButtons buttons, quint32 x, quint32 y, const int winId) { const QJsonDocument message { QJsonObject { { QLatin1String("type"), QLatin1String("mouse") }, { QLatin1String("buttons"), int(buttons) }, { QLatin1String("layerX"), int(x) }, { QLatin1String("layerY"), int(y) }, { QLatin1String("clientX"), int(x) }, { QLatin1String("clientY"), int(y) }, { QLatin1String("time"), QDateTime::currentDateTime().toMSecsSinceEpoch() }, { QLatin1String("name"), winId } } }; webSocket.sendTextMessage(message.toJson()); } void tst_WebGL::sendMouseClick(quint32 x, quint32 y, int winId) { sendMouseEvent(Qt::LeftButton, x, y, winId); sendMouseEvent(Qt::NoButton, x, y, winId); } bool tst_WebGL::findSwapBuffers(const QSignalSpy &spy) { return std::find_if(spy.cbegin(), spy.cend(), [](const QList &list) { // Our connect message changed the scene's size, forcing a swapBuffers() call. return list.first() == QLatin1String("swapBuffers"); }) != spy.cend(); } void tst_WebGL::parseTextMessage(const QString &text) { const auto document = QJsonDocument::fromJson(text.toUtf8()); if (document["type"].toString() == "connect") { const auto supportedFunctions = document["supportedFunctions"].toArray(); functions.clear(); for (const auto &function : supportedFunctions) functions.append(function.toString()); } else if (document["type"] == "create_canvas") { const QJsonDocument defaultValuesMessage { QJsonObject { { QLatin1String("type"), QLatin1String("default_context_parameters") }, { QString::number(GL_EXTENSIONS), QLatin1String("GL_OES_element_index_uint " "GL_OES_standard_derivatives " "GL_OES_depth_texture GL_OES_packed_depth_stencil") }, { QString::number(GL_BLEND), false }, { QString::number(GL_DEPTH_TEST), false }, { QString::number(GL_MAX_TEXTURE_SIZE), 512 }, { QString::number(GL_MAX_VERTEX_ATTRIBS), 16}, { QString::number(GL_RENDERER), "Test WebGL"}, { QString::number(GL_SCISSOR_TEST), false }, { QString::number(GL_STENCIL_TEST), false }, { QString::number(GL_UNPACK_ALIGNMENT), 4 }, { QString::number(GL_VENDOR), "Qt" }, { QString::number(GL_VERSION), "WebGL 1.0" }, { QString::number(GL_VIEWPORT), QJsonArray{ 0, 0, 640, 480 } }, { QLatin1String("name"), document["winId"] } }, }; webSocket.sendTextMessage(defaultValuesMessage.toJson()); contexts.insert(contexts.size(), Context(document["winId"].toInt())); } } // This function gets called inside a QTRY_* that is subject to // a QEXPECT_FAIL, which treats QCOMPARE and QVERIFY // specially, so we have to avoid using those. void tst_WebGL::parseBinaryMessage(const QByteArray &data) { const QSet commandsNeedingResponse { QLatin1String("swapBuffers"), QLatin1String("checkFramebufferStatus"), QLatin1String("createProgram"), QLatin1String("createShader"), QLatin1String("genBuffers"), QLatin1String("genFramebuffers"), QLatin1String("genRenderbuffers"), QLatin1String("genTextures"), QLatin1String("getAttachedShaders"), QLatin1String("getAttribLocation"), QLatin1String("getBooleanv"), QLatin1String("getError"), QLatin1String("getFramebufferAttachmentParameteriv"), QLatin1String("getIntegerv"), QLatin1String("getParameter"), QLatin1String("getProgramInfoLog"), QLatin1String("getProgramiv"), QLatin1String("getRenderbufferParameteriv"), QLatin1String("getShaderiv"), QLatin1String("getShaderPrecisionFormat"), QLatin1String("getString"), QLatin1String("getTexParameterfv"), QLatin1String("getTexParameteriv"), QLatin1String("getUniformfv"), QLatin1String("getUniformLocation"), QLatin1String("getUniformiv"), QLatin1String("getVertexAttribfv"), QLatin1String("getVertexAttribiv"), QLatin1String("getShaderSource"), QLatin1String("getShaderInfoLog"), QLatin1String("isRenderbuffer") }; quint32 offset = 0; QString function; int id = -1; QDataStream stream(data); { quint8 functionIndex; stream >> functionIndex; offset += sizeof(functionIndex); function = functions[functionIndex]; if (commandsNeedingResponse.contains(function)) { stream >> id; offset += sizeof(id); } } const auto parameters = Parameters::read(data, stream, offset); { quint32 magic = 0; stream >> magic; offset += sizeof(magic); if (magic != 0xbaadf00d) { QFAIL(qPrintable(QStringLiteral("Magic token is 0x%1 not 0x%2") .arg(magic, 0, 16).arg(0xbaadf00d, 0, 16))); } } if (int(offset) != data.size()) QFAIL(qPrintable(QStringLiteral("Offset is %1 not %2").arg(offset).arg(data.size()))); if (id == -1) { emit command(function, parameters); if (function == "attachShader") { const auto shader = pointer(parameters[1], shaders); if (!shader) QFAIL("Null pointer"); auto program = pointer(parameters[0], programs); if (!program) QFAIL("Null pointer"); program->attached[shader->type] = shader; } else if (function == "bindBuffer") { auto buffer = pointer(parameters[1], buffers); if (parameters[0].toUInt() == GL_ARRAY_BUFFER) currentContext->arrayBuffer = buffer; else if (parameters[0].toUInt() == GL_ELEMENT_ARRAY_BUFFER) currentContext->elementArrayBuffer = buffer; else QTest::qFail("Unsupported buffer type", __FILE__, __LINE__); } else if (function == "compileShader") { auto shader = pointer(parameters[0], shaders); if (!shader) QFAIL("Null pointer"); shader->compiled = true; } else if (function == "linkProgram") { auto program = pointer(parameters[0], programs); if (!program) QFAIL("Null pointer"); program->linked = true; } else if (function == "shaderSource") { auto shader = pointer(parameters[0], shaders); if (!shader) QFAIL("Null pointer"); shader->source = parameters[1].toString(); } else if (function == "makeCurrent") { currentContext = pointer(parameters[3].toInt(), contexts); } else if (function == "useProgram") { currentContext->program = pointer(parameters[0], programs); } } else { emit queryCommand(function, id, parameters); QJsonValue retval; static QMap nextIds; if (function == "createProgram") { programs.insert(programs.size(), Program{}); retval = programs.size(); } else if (function == "createShader") { shaders.insert(shaders.size(), parameters[0].toUInt()); retval = shaders.size(); } else if (function == "genBuffers") { QJsonArray array; for (int i = 0, count = parameters.first().toInt(); i < count; ++i) { buffers.insert(buffers.size(), Buffer{}); array.append(buffers.size()); } retval = array; } else if (function == "genTextures") { QJsonArray array; for (int i = 0, count = parameters.first().toInt(); i < count; ++i) { textures.insert(textures.size(), Texture{}); array.append(textures.size()); } retval = array; } else if (function == "getError") { retval = ""; } else if (function == "getProgramiv") { const auto program = pointer(parameters[0], programs); retval = program ? program->linked : false; } else if (function == "getShaderiv") { const auto shader = pointer(parameters[0], shaders); retval = shader ? shader->compiled : false; } else if (function == "getUniformLocation") { const auto shaders = currentContext->program->attached[GL_VERTEX_SHADER]->source + currentContext->program->attached[GL_FRAGMENT_SHADER] ->source; const QRegularExpression rx(R"rx(uniform +(?:(?:high|medium|low)p +)?\w+ +(\w+) *;)rx"); auto m = rx.globalMatch(shaders); for (int i = 0; m.hasNext() && retval.isNull(); ++i) { if (m.next().captured(1) == parameters[1].toString()) retval = i; } } else if (function == "linkProgram") { auto program = pointer(parameters[0], programs); if (program) program->linked = true; } else if (function == "swapBuffers") { // do nothing } else { QFAIL("Function not handled"); } const QJsonDocument answer { QJsonObject { { QLatin1String("type"), QLatin1String("gl_response") }, { QLatin1String("id"), id }, { QLatin1String("value"), retval } } }; webSocket.sendTextMessage(answer.toJson()); } } void tst_WebGL::initTestCase() { connect(&webSocket, &QWebSocket::binaryMessageReceived, this, &tst_WebGL::parseBinaryMessage); connect(&webSocket, &QWebSocket::textMessageReceived, this, &tst_WebGL::parseTextMessage); } void tst_WebGL::init() { QFETCH(QString, scene); contexts.clear(); buffers.clear(); programs.clear(); shaders.clear(); textures.clear(); currentContext = nullptr; const auto tryToConnect = [=](quint16 port = PORT) { QTcpSocket socket; socket.connectToHost("localhost", port); QTRY_LOOP_IMPL(socket.state() == QTcpSocket::ConnectedState || socket.state() == QTcpSocket::UnconnectedState, 1000, 50); return socket.state() == QTcpSocket::ConnectedState; }; QVERIFY2(!tryToConnect(), "An application is listening on port " PORTSTRING); QString executableName = QLatin1String("qmlscene"); #if defined(Q_OS_WIN) executableName += QString::fromLatin1(".exe"); #endif process.setProcessChannelMode(QProcess::MergedChannels); process.setProgram(QLibraryInfo::location(QLibraryInfo::BinariesPath) + QChar('/') + executableName); process.setArguments(QStringList { QDir::toNativeSeparators(scene) }); process.setEnvironment(QProcess::systemEnvironment() << "QT_QPA_PLATFORM=webgl:port=" PORTSTRING); process.start(); process.waitForStarted(); QVERIFY(process.isOpen()); #if defined(QT_DEBUG) connect(&process, &QProcess::readyReadStandardOutput, [=]() { while (process.bytesAvailable()) qDebug() << process.pid() << process.readLine(); }); #endif // defined(QT_DEBUG) QTRY_VERIFY(tryToConnect()); auto reply = manager.get(QNetworkRequest(QUrl("http://localhost:" PORTSTRING "/webqt.js"))); QSignalSpy replyFinishedSpy(reply, &QNetworkReply::finished); QTRY_VERIFY(!replyFinishedSpy.isEmpty()); reply->readLine(); const auto portString = reply->readLine().trimmed(); QVERIFY(portString.size()); const QRegularExpression rx("var port = (\\d+);"); const auto match = rx.match(portString); QVERIFY(!match.captured(1).isEmpty()); QVERIFY(match.captured(1).toUInt() <= std::numeric_limits::max()); websocketPort = match.captured(1).toInt(); QVERIFY(websocketPort != 0); connectToQmlScene(); } void tst_WebGL::cleanup() { webSocket.close(); process.kill(); process.waitForFinished(); } void tst_WebGL::checkFunctionCount_data() { QTest::addColumn("scene"); // Fetched in tst_WebGL::init QTest::newRow("Basic scene") << QFINDTESTDATA("basic_scene.qml"); } void tst_WebGL::checkFunctionCount() { QCOMPARE(functions.size(), 147); } void tst_WebGL::waitForSwapBuffers_data() { QTest::addColumn("scene"); // Fetched in tst_WebGL::init QTest::newRow("Basic scene") << QFINDTESTDATA("basic_scene.qml"); } void tst_WebGL::waitForSwapBuffers() { QSignalSpy spy(this, &tst_WebGL::queryCommand); QTRY_VERIFY(findSwapBuffers(spy)); } void tst_WebGL::reload_data() { QTest::addColumn("scene"); // Fetched in tst_WebGL::init QTest::newRow("Basic scene") << QFINDTESTDATA("basic_scene.qml"); QTest::newRow("Colors") << QFINDTESTDATA("colors.qml"); } void tst_WebGL::reload() { for (int i = 0; i < 2; ++i) { QSignalSpy spy(this, &tst_WebGL::queryCommand); QTRY_VERIFY(findSwapBuffers(spy)); QVERIFY(!QTest::currentTestFailed()); webSocket.close(); connectToQmlScene(); QVERIFY(!QTest::currentTestFailed()); } } void tst_WebGL::update_data() { QTest::addColumn("scene"); // Fetched in tst_WebGL::init QTest::newRow("Colors") << QFINDTESTDATA("colors.qml"); QTest::newRow("Launcher") << QFINDTESTDATA("launcher.qml"); } void tst_WebGL::update() { { QSignalSpy spy(this, &tst_WebGL::queryCommand); QTRY_VERIFY(findSwapBuffers(spy)); QVERIFY(!QTest::currentTestFailed()); } sendMouseClick(0, 0, currentContext->winId); { QSignalSpy spy(this, &tst_WebGL::queryCommand); QTRY_VERIFY(findSwapBuffers(spy)); } } QTEST_MAIN(tst_WebGL) #include "tst_webgl.moc"