1/****************************************************************************
2**
3** Copyright (C) 2018 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the Qt WebGL module of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:GPL$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU
19** General Public License version 3 or (at your option) any later version
20** approved by the KDE Free Qt Foundation. The licenses are as published by
21** the Free Software Foundation and appearing in the file LICENSE.GPL3
22** included in the packaging of this file. Please review the following
23** information to ensure the GNU General Public License requirements will
24** be met: https://www.gnu.org/licenses/gpl-3.0.html.
25**
26** $QT_END_LICENSE$
27**
28****************************************************************************/
29
30#include <QtTest/qtest.h>
31#include <QtTest/qsignalspy.h>
32
33#include <QtCore/qcoreapplication.h>
34#include <QtCore/qlibraryinfo.h>
35#include <QtCore/qjsonarray.h>
36#include <QtCore/qjsondocument.h>
37#include <QtCore/qjsonobject.h>
38#include <QtCore/qobject.h>
39#include <QtCore/qprocess.h>
40#include <QtCore/qregularexpression.h>
41#include <QtGui/qguiapplication.h>
42#include <QtGui/qopengl.h>
43#include <QtNetwork/qnetworkaccessmanager.h>
44#include <QtNetwork/qnetworkreply.h>
45#include <QtNetwork/qtcpsocket.h>
46#include <QtWebSockets/qwebsocket.h>
47
48#include "parameters.h"
49
50#include <memory>
51
52#define PORT 29836
53#define PORTSTRING QT_STRINGIFY(PORT)
54
55class tst_WebGL : public QObject
56{
57 Q_OBJECT
58
59 struct GLObject {};
60
61 struct Buffer : GLObject {};
62 struct Shader : GLObject
63 {
64 Shader(GLuint type = GL_INVALID_VALUE) : type(type) {}
65 GLuint type;
66 QString source;
67 bool compiled = false;
68 };
69
70 struct Program : GLObject
71 {
72 QMap<GLuint, Shader *> attached;
73 bool linked = false;
74 };
75
76 struct Texture : GLObject {};
77
78 struct Context
79 {
80 Context(int winId = -1) : winId(winId) {}
81
82 int winId;
83 Buffer *arrayBuffer = nullptr;
84 Buffer *elementArrayBuffer = nullptr;
85 Program *program = nullptr;
86 };
87
88 QList<Context> contexts;
89 QList<Buffer> buffers;
90 QList<Program> programs;
91 QList<Shader> shaders;
92 QList<Texture> textures;
93 Context *currentContext = nullptr;
94
95 QNetworkAccessManager manager;
96 QWebSocket webSocket;
97 QStringList functions;
98 QProcess process;
99 qintptr websocketPort;
100
101 void connectToQmlScene();
102 void sendMouseEvent(Qt::MouseButtons buttons, quint32 x, quint32 y, int winId);
103 void sendMouseClick(quint32 x, quint32 y, int winId);
104
105 template <typename Struct>
106 Struct *pointer(const QVariant &id, QList<Struct> &container)
107 {
108 const auto handle = id.toInt();
109 if (handle > 0 && handle <= container.size())
110 return &container[handle - 1];
111 return nullptr;
112 }
113
114 bool findSwapBuffers(const QSignalSpy &spy);
115
116signals:
117 void command(const QString &name, const QVariantList &parameters);
118 void queryCommand(const QString &name, int id, const QVariantList &parameters);
119
120public slots:
121 void parseTextMessage(const QString &text);
122 void parseBinaryMessage(const QByteArray &data);
123
124private slots:
125 void initTestCase();
126
127 void init();
128 void cleanup();
129
130 void checkFunctionCount_data();
131 void checkFunctionCount();
132
133 void waitForSwapBuffers_data();
134 void waitForSwapBuffers();
135
136 void reload_data();
137 void reload();
138
139 void update_data();
140 void update();
141};
142
143void tst_WebGL::connectToQmlScene()
144{
145 const QJsonDocument connectMessage {
146 QJsonObject {
147 { QLatin1String("type"), QLatin1String("connect") },
148 { QLatin1String("width"), 1920 },
149 { QLatin1String("height"), 1080 },
150 { QLatin1String("physicalWidth"), 531.3 },
151 { QLatin1String("physicalHeight"), 298.9 }
152 }
153 };
154
155 QSignalSpy connected(&webSocket, &QWebSocket::connected);
156 webSocket.open(url: QUrl(QString::fromLatin1(str: "ws://localhost:%1").arg(a: websocketPort)));
157 QVERIFY(connected.wait());
158 webSocket.sendTextMessage(message: connectMessage.toJson());
159 QVERIFY(webSocket.state() == QAbstractSocket::ConnectedState);
160}
161
162void tst_WebGL::sendMouseEvent(Qt::MouseButtons buttons, quint32 x, quint32 y, const int winId)
163{
164 const QJsonDocument message {
165 QJsonObject {
166 { QLatin1String("type"), QLatin1String("mouse") },
167 { QLatin1String("buttons"), int(buttons) },
168 { QLatin1String("layerX"), int(x) },
169 { QLatin1String("layerY"), int(y) },
170 { QLatin1String("clientX"), int(x) },
171 { QLatin1String("clientY"), int(y) },
172 { QLatin1String("time"), QDateTime::currentDateTime().toMSecsSinceEpoch() },
173 { QLatin1String("name"), winId }
174 }
175 };
176 webSocket.sendTextMessage(message: message.toJson());
177}
178
179void tst_WebGL::sendMouseClick(quint32 x, quint32 y, int winId)
180{
181 sendMouseEvent(buttons: Qt::LeftButton, x, y, winId);
182 sendMouseEvent(buttons: Qt::NoButton, x, y, winId);
183}
184
185bool tst_WebGL::findSwapBuffers(const QSignalSpy &spy)
186{
187 return std::find_if(first: spy.cbegin(), last: spy.cend(), pred: [](const QList<QVariant> &list) {
188 // Our connect message changed the scene's size, forcing a swapBuffers() call.
189 return list.first() == QLatin1String("swapBuffers");
190 }) != spy.cend();
191}
192
193void tst_WebGL::parseTextMessage(const QString &text)
194{
195 const auto document = QJsonDocument::fromJson(json: text.toUtf8());
196 if (document["type"].toString() == "connect") {
197 const auto supportedFunctions = document["supportedFunctions"].toArray();
198 functions.clear();
199 for (const auto &function : supportedFunctions)
200 functions.append(t: function.toString());
201 } else if (document["type"] == "create_canvas") {
202 const QJsonDocument defaultValuesMessage {
203 QJsonObject {
204 { QLatin1String("type"), QLatin1String("default_context_parameters") },
205 { QString::number(GL_EXTENSIONS),
206 QLatin1String("GL_OES_element_index_uint "
207 "GL_OES_standard_derivatives "
208 "GL_OES_depth_texture GL_OES_packed_depth_stencil") },
209 { QString::number(GL_BLEND), false },
210 { QString::number(GL_DEPTH_TEST), false },
211 { QString::number(GL_MAX_TEXTURE_SIZE), 512 },
212 { QString::number(GL_MAX_VERTEX_ATTRIBS), 16},
213 { QString::number(GL_RENDERER), "Test WebGL"},
214 { QString::number(GL_SCISSOR_TEST), false },
215 { QString::number(GL_STENCIL_TEST), false },
216 { QString::number(GL_UNPACK_ALIGNMENT), 4 },
217 { QString::number(GL_VENDOR), "Qt" },
218 { QString::number(GL_VERSION), "WebGL 1.0" },
219 { QString::number(GL_VIEWPORT), QJsonArray{ 0, 0, 640, 480 } },
220 { QLatin1String("name"), document["winId"] }
221 },
222 };
223 webSocket.sendTextMessage(message: defaultValuesMessage.toJson());
224 contexts.append(t: Context(document["winId"].toInt()));
225 }
226}
227
228// This function gets called inside a QTRY_* that is subject to
229// a QEXPECT_FAIL, which treats QCOMPARE and QVERIFY
230// specially, so we have to avoid using those.
231void tst_WebGL::parseBinaryMessage(const QByteArray &data)
232{
233 const QSet<QString> commandsNeedingResponse {
234 QLatin1String("swapBuffers"),
235 QLatin1String("checkFramebufferStatus"),
236 QLatin1String("createProgram"),
237 QLatin1String("createShader"),
238 QLatin1String("genBuffers"),
239 QLatin1String("genFramebuffers"),
240 QLatin1String("genRenderbuffers"),
241 QLatin1String("genTextures"),
242 QLatin1String("getAttachedShaders"),
243 QLatin1String("getAttribLocation"),
244 QLatin1String("getBooleanv"),
245 QLatin1String("getError"),
246 QLatin1String("getFramebufferAttachmentParameteriv"),
247 QLatin1String("getIntegerv"),
248 QLatin1String("getParameter"),
249 QLatin1String("getProgramInfoLog"),
250 QLatin1String("getProgramiv"),
251 QLatin1String("getRenderbufferParameteriv"),
252 QLatin1String("getShaderiv"),
253 QLatin1String("getShaderPrecisionFormat"),
254 QLatin1String("getString"),
255 QLatin1String("getTexParameterfv"),
256 QLatin1String("getTexParameteriv"),
257 QLatin1String("getUniformfv"),
258 QLatin1String("getUniformLocation"),
259 QLatin1String("getUniformiv"),
260 QLatin1String("getVertexAttribfv"),
261 QLatin1String("getVertexAttribiv"),
262 QLatin1String("getShaderSource"),
263 QLatin1String("getShaderInfoLog"),
264 QLatin1String("isRenderbuffer")
265 };
266
267 quint32 offset = 0;
268 QString function;
269 int id = -1;
270 QDataStream stream(data);
271 {
272 quint8 functionIndex;
273 stream >> functionIndex;
274 offset += sizeof(functionIndex);
275 function = functions[functionIndex];
276 if (commandsNeedingResponse.contains(value: function)) {
277 stream >> id;
278 offset += sizeof(id);
279 }
280 }
281 const auto parameters = Parameters::read(data, stream, offset);
282 {
283 quint32 magic = 0;
284 stream >> magic;
285 offset += sizeof(magic);
286 if (magic != 0xbaadf00d) {
287 QFAIL(qPrintable(QStringLiteral("Magic token is 0x%1 not 0x%2")
288 .arg(magic, 0, 16).arg(0xbaadf00d, 0, 16)));
289 }
290 }
291
292 if (int(offset) != data.size())
293 QFAIL(qPrintable(QStringLiteral("Offset is %1 not %2").arg(offset).arg(data.size())));
294
295 if (id == -1) {
296 emit command(name: function, parameters);
297
298 if (function == "attachShader") {
299 const auto shader = pointer(id: parameters[1], container&: shaders);
300 if (!shader) QFAIL("Null pointer");
301 auto program = pointer(id: parameters[0], container&: programs);
302 if (!program) QFAIL("Null pointer");
303 program->attached[shader->type] = shader;
304 } else if (function == "bindBuffer") {
305 auto buffer = pointer(id: parameters[1], container&: buffers);
306 if (parameters[0].toUInt() == GL_ARRAY_BUFFER)
307 currentContext->arrayBuffer = buffer;
308 else if (parameters[0].toUInt() == GL_ELEMENT_ARRAY_BUFFER)
309 currentContext->elementArrayBuffer = buffer;
310 else
311 QTest::qFail(statementStr: "Unsupported buffer type", __FILE__, __LINE__);
312 } else if (function == "compileShader") {
313 auto shader = pointer(id: parameters[0], container&: shaders);
314 if (!shader) QFAIL("Null pointer");
315 shader->compiled = true;
316 } else if (function == "linkProgram") {
317 auto program = pointer(id: parameters[0], container&: programs);
318 if (!program) QFAIL("Null pointer");
319 program->linked = true;
320 } else if (function == "shaderSource") {
321 auto shader = pointer(id: parameters[0], container&: shaders);
322 if (!shader) QFAIL("Null pointer");
323 shader->source = parameters[1].toString();
324 } else if (function == "makeCurrent") {
325 currentContext = pointer(id: parameters[3].toInt(), container&: contexts);
326 } else if (function == "useProgram") {
327 currentContext->program = pointer(id: parameters[0], container&: programs);
328 }
329 } else {
330 emit queryCommand(name: function, id, parameters);
331
332 QJsonValue retval;
333 static QMap<QString, int> nextIds;
334
335 if (function == "createProgram") {
336 programs.append(t: Program{});
337 retval = programs.size();
338 } else if (function == "createShader") {
339 shaders.append(t: Shader(parameters[0].toUInt()));
340 retval = shaders.size();
341 } else if (function == "genBuffers") {
342 QJsonArray array;
343 for (int i = 0, count = parameters.first().toInt(); i < count; ++i) {
344 buffers.append(t: Buffer{});
345 array.append(value: buffers.size());
346 }
347 retval = array;
348 } else if (function == "genTextures") {
349 QJsonArray array;
350 for (int i = 0, count = parameters.first().toInt(); i < count; ++i) {
351 textures.append(t: Texture{});
352 array.append(value: textures.size());
353 }
354 retval = array;
355 } else if (function == "getError") {
356 retval = "";
357 } else if (function == "getProgramiv") {
358 const auto program = pointer(id: parameters[0], container&: programs);
359 retval = program ? program->linked : false;
360 } else if (function == "getShaderiv") {
361 const auto shader = pointer(id: parameters[0], container&: shaders);
362 retval = shader ? shader->compiled : false;
363 } else if (function == "getUniformLocation") {
364 const auto shaders = currentContext->program->attached[GL_VERTEX_SHADER]->source
365 + currentContext->program->attached[GL_FRAGMENT_SHADER] ->source;
366 const QRegularExpression rx(R"rx(uniform +(?:(?:high|medium|low)p +)?\w+ +(\w+) *;)rx");
367 auto m = rx.globalMatch(subject: shaders);
368 for (int i = 0; m.hasNext() && retval.isNull(); ++i) {
369 if (m.next().captured(nth: 1) == parameters[1].toString())
370 retval = i;
371 }
372 } else if (function == "linkProgram") {
373 auto program = pointer(id: parameters[0], container&: programs);
374 if (program)
375 program->linked = true;
376 } else if (function == "swapBuffers") {
377 // do nothing
378 } else {
379 QFAIL("Function not handled");
380 }
381 const QJsonDocument answer {
382 QJsonObject {
383 { QLatin1String("type"), QLatin1String("gl_response") },
384 { QLatin1String("id"), id },
385 { QLatin1String("value"), retval }
386 }
387 };
388 webSocket.sendTextMessage(message: answer.toJson());
389 }
390}
391
392void tst_WebGL::initTestCase()
393{
394 connect(sender: &webSocket, signal: &QWebSocket::binaryMessageReceived, receiver: this, slot: &tst_WebGL::parseBinaryMessage);
395 connect(sender: &webSocket, signal: &QWebSocket::textMessageReceived, receiver: this, slot: &tst_WebGL::parseTextMessage);
396}
397
398void tst_WebGL::init()
399{
400 QFETCH(QString, scene);
401
402 contexts.clear();
403 buffers.clear();
404 programs.clear();
405 shaders.clear();
406 textures.clear();
407 currentContext = nullptr;
408
409 const auto tryToConnect = [=](quint16 port = PORT) {
410 QTcpSocket socket;
411 socket.connectToHost(hostName: "localhost", port);
412 QTRY_LOOP_IMPL(socket.state() == QTcpSocket::ConnectedState ||
413 socket.state() == QTcpSocket::UnconnectedState, 1000, 50);
414 return socket.state() == QTcpSocket::ConnectedState;
415 };
416
417 QVERIFY2(!tryToConnect(), "An application is listening on port " PORTSTRING);
418
419 QString executableName = QLatin1String("qmlscene");
420#if defined(Q_OS_WIN)
421 executableName += QString::fromLatin1(".exe");
422#endif
423
424 process.setProcessChannelMode(QProcess::MergedChannels);
425 process.setProgram(QLibraryInfo::location(QLibraryInfo::BinariesPath) + QChar('/')
426 + executableName);
427 process.setArguments(QStringList { QDir::toNativeSeparators(pathName: scene) });
428 process.setEnvironment(QProcess::systemEnvironment()
429 << "QT_QPA_PLATFORM=webgl:port=" PORTSTRING);
430 process.start();
431 process.waitForStarted();
432 QVERIFY(process.isOpen());
433#if defined(QT_DEBUG)
434 connect(sender: &process, signal: &QProcess::readyReadStandardOutput, slot: [=]() {
435 while (process.bytesAvailable())
436 qDebug() << process.pid() << process.readLine();
437 });
438#endif // defined(QT_DEBUG)
439 QTRY_VERIFY(tryToConnect());
440
441 auto reply = manager.get(request: QNetworkRequest(QUrl("http://localhost:" PORTSTRING "/webqt.js")));
442 QSignalSpy replyFinishedSpy(reply, &QNetworkReply::finished);
443 QTRY_VERIFY(!replyFinishedSpy.isEmpty());
444 reply->readLine();
445 const auto portString = reply->readLine().trimmed();
446 QVERIFY(portString.size());
447 const QRegularExpression rx("var port = (\\d+);");
448 const auto match = rx.match(subject: portString);
449 QVERIFY(!match.captured(1).isEmpty());
450 QVERIFY(match.captured(1).toUInt() <= std::numeric_limits<quint16>::max());
451 websocketPort = match.captured(nth: 1).toInt();
452
453 QVERIFY(websocketPort != 0);
454
455 connectToQmlScene();
456}
457
458void tst_WebGL::cleanup()
459{
460 webSocket.close();
461
462 process.kill();
463 process.waitForFinished();
464}
465
466void tst_WebGL::checkFunctionCount_data()
467{
468 QTest::addColumn<QString>(name: "scene"); // Fetched in tst_WebGL::init
469 QTest::newRow(dataTag: "Basic scene") << QFINDTESTDATA("basic_scene.qml");
470}
471
472void tst_WebGL::checkFunctionCount()
473{
474 QCOMPARE(functions.size(), 147);
475}
476
477void tst_WebGL::waitForSwapBuffers_data()
478{
479 QTest::addColumn<QString>(name: "scene"); // Fetched in tst_WebGL::init
480 QTest::newRow(dataTag: "Basic scene") << QFINDTESTDATA("basic_scene.qml");
481}
482
483void tst_WebGL::waitForSwapBuffers()
484{
485 QSignalSpy spy(this, &tst_WebGL::queryCommand);
486 QTRY_VERIFY(findSwapBuffers(spy));
487}
488
489void tst_WebGL::reload_data()
490{
491 QTest::addColumn<QString>(name: "scene"); // Fetched in tst_WebGL::init
492 QTest::newRow(dataTag: "Basic scene") << QFINDTESTDATA("basic_scene.qml");
493 QTest::newRow(dataTag: "Colors") << QFINDTESTDATA("colors.qml");;
494}
495
496void tst_WebGL::reload()
497{
498 for (int i = 0; i < 2; ++i) {
499 QSignalSpy spy(this, &tst_WebGL::queryCommand);
500 QTRY_VERIFY(findSwapBuffers(spy));
501 QVERIFY(!QTest::currentTestFailed());
502 webSocket.close();
503 connectToQmlScene();
504 QVERIFY(!QTest::currentTestFailed());
505 }
506}
507
508void tst_WebGL::update_data()
509{
510 QTest::addColumn<QString>(name: "scene"); // Fetched in tst_WebGL::init
511 QTest::newRow(dataTag: "Colors") << QFINDTESTDATA("colors.qml");
512 QTest::newRow(dataTag: "Launcher") << QFINDTESTDATA("launcher.qml");
513}
514
515void tst_WebGL::update()
516{
517 {
518 QSignalSpy spy(this, &tst_WebGL::queryCommand);
519 QTRY_VERIFY(findSwapBuffers(spy));
520 QVERIFY(!QTest::currentTestFailed());
521 }
522 sendMouseClick(x: 0, y: 0, winId: currentContext->winId);
523 {
524 QSignalSpy spy(this, &tst_WebGL::queryCommand);
525 QTRY_VERIFY(findSwapBuffers(spy));
526 }
527}
528
529QTEST_MAIN(tst_WebGL)
530
531#include "tst_webgl.moc"
532

source code of qtwebglplugin/tests/plugins/platforms/webgl/tst_webgl.cpp