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 test suite of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:GPL-EXCEPT$
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 as published by the Free Software
20** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
21** included in the packaging of this file. Please review the following
22** information to ensure the GNU General Public License requirements will
23** be met: https://www.gnu.org/licenses/gpl-3.0.html.
24**
25** $QT_END_LICENSE$
26**
27****************************************************************************/
28
29#include "qqmldebugprocess_p.h"
30#include "debugutil_p.h"
31#include "qqmlpreviewblacklist.h"
32
33#include <QtTest/qtest.h>
34#include <QtTest/qsignalspy.h>
35#include <QtCore/qtimer.h>
36#include <QtCore/qdebug.h>
37#include <QtCore/qthread.h>
38#include <QtCore/qlibraryinfo.h>
39#include <QtNetwork/qhostaddress.h>
40
41#include <private/qqmldebugconnection_p.h>
42#include <private/qqmlpreviewclient_p.h>
43
44class tst_QQmlPreview : public QQmlDebugTest
45{
46 Q_OBJECT
47
48private:
49 ConnectResult startQmlProcess(const QString &qmlFile);
50 void serveRequest(const QString &path);
51 QList<QQmlDebugClient *> createClients() override;
52 void verifyProcessOutputContains(const QString &string) const;
53
54 QPointer<QQmlPreviewClient> m_client;
55
56 QStringList m_files;
57 QStringList m_filesNotFound;
58 QStringList m_directories;
59 QStringList m_serviceErrors;
60 QQmlPreviewClient::FpsInfo m_frameStats;
61
62private slots:
63 void cleanup() final;
64
65 void connect();
66 void load();
67 void rerun();
68 void blacklist();
69 void error();
70 void zoom();
71 void fps();
72 void language();
73};
74
75QQmlDebugTest::ConnectResult tst_QQmlPreview::startQmlProcess(const QString &qmlFile)
76{
77 return QQmlDebugTest::connectTo(executable: QLibraryInfo::location(QLibraryInfo::BinariesPath) + "/qml",
78 QStringLiteral("QmlPreview"), extraArgs: testFile(fileName: qmlFile), block: true);
79}
80
81void tst_QQmlPreview::serveRequest(const QString &path)
82{
83 QFileInfo info(path);
84
85 if (info.isDir()) {
86 m_directories.append(t: path);
87 m_client->sendDirectory(path, entries: QDir(path).entryList());
88 } else {
89 QFile file(path);
90 if (file.open(flags: QIODevice::ReadOnly)) {
91 m_files.append(t: path);
92 m_client->sendFile(path, contents: file.readAll());
93 } else {
94 m_filesNotFound.append(t: path);
95 m_client->sendError(path);
96 }
97 }
98}
99
100QList<QQmlDebugClient *> tst_QQmlPreview::createClients()
101{
102 m_client = new QQmlPreviewClient(m_connection);
103
104 QObject::connect(sender: m_client.data(), signal: &QQmlPreviewClient::request, receiver: this, slot: &tst_QQmlPreview::serveRequest);
105 QObject::connect(sender: m_client.data(), signal: &QQmlPreviewClient::error, context: this, slot: [this](const QString &error) {
106 m_serviceErrors.append(t: error);
107 });
108 QObject::connect(sender: m_client.data(), signal: &QQmlPreviewClient::fps,
109 context: this, slot: [this](const QQmlPreviewClient::FpsInfo &info) {
110 m_frameStats = info;
111 });
112
113 return QList<QQmlDebugClient *>({m_client});
114}
115
116void tst_QQmlPreview::verifyProcessOutputContains(const QString &string) const
117{
118 QTRY_VERIFY_WITH_TIMEOUT(m_process->output().contains(string), 30000);
119}
120
121void checkFiles(const QStringList &files)
122{
123 QVERIFY(!files.contains("/etc/localtime"));
124 QVERIFY(!files.contains("/etc/timezome"));
125 QVERIFY(!files.contains(":/qgradient/webgradients.binaryjson"));
126}
127
128void tst_QQmlPreview::cleanup()
129{
130 // Use a separate function so that we don't return early from cleanup() on failure.
131 checkFiles(files: m_files);
132
133 QQmlDebugTest::cleanup();
134 if (QTest::currentTestFailed()) {
135 qDebug() << "Files loaded:" << m_files;
136 qDebug() << "Files not loaded:" << m_filesNotFound;
137 qDebug() << "Directories loaded:" << m_directories;
138 qDebug() << "Errors reported:" << m_serviceErrors;
139 }
140
141 m_directories.clear();
142 m_files.clear();
143 m_filesNotFound.clear();
144 m_serviceErrors.clear();
145 m_frameStats = QQmlPreviewClient::FpsInfo();
146}
147
148void tst_QQmlPreview::connect()
149{
150 const QString file("window.qml");
151 QCOMPARE(startQmlProcess(file), ConnectSuccess);
152 QVERIFY(m_client);
153 QTRY_COMPARE(m_client->state(), QQmlDebugClient::Enabled);
154 m_client->triggerLoad(url: testFileUrl(fileName: file));
155 QTRY_VERIFY(m_files.contains(testFile(file)));
156 verifyProcessOutputContains(string: file);
157 m_process->stop();
158 QTRY_COMPARE(m_client->state(), QQmlDebugClient::NotConnected);
159 QVERIFY(m_serviceErrors.isEmpty());
160}
161
162void tst_QQmlPreview::load()
163{
164 const QString file("qtquick2.qml");
165 QCOMPARE(startQmlProcess(file), ConnectSuccess);
166 QVERIFY(m_client);
167 QTRY_COMPARE(m_client->state(), QQmlDebugClient::Enabled);
168 m_client->triggerLoad(url: testFileUrl(fileName: file));
169 QTRY_VERIFY(m_files.contains(testFile(file)));
170 verifyProcessOutputContains(string: "ms/degrees");
171
172 const QStringList files({"window2.qml", "window1.qml", "window.qml"});
173 for (const QString &newFile : files) {
174 m_client->triggerLoad(url: testFileUrl(fileName: newFile));
175 QTRY_VERIFY(m_files.contains(testFile(newFile)));
176 verifyProcessOutputContains(string: newFile);
177 }
178
179 m_process->stop();
180 QTRY_COMPARE(m_client->state(), QQmlDebugClient::NotConnected);
181 QVERIFY(m_serviceErrors.isEmpty());
182}
183
184void tst_QQmlPreview::rerun()
185{
186 const QString file("window.qml");
187 QCOMPARE(startQmlProcess(file), ConnectSuccess);
188 QVERIFY(m_client);
189 m_client->triggerLoad(url: testFileUrl(fileName: file));
190 const QLatin1String message("window.qml");
191 verifyProcessOutputContains(string: message);
192 const int pos = m_process->output().lastIndexOf(s: message) + message.size();
193 QVERIFY(pos >= 0);
194
195 m_client->triggerRerun();
196 QTRY_VERIFY_WITH_TIMEOUT(m_process->output().indexOf(message, pos) >= pos, 30000);
197
198 m_process->stop();
199 QVERIFY(m_serviceErrors.isEmpty());
200}
201
202void tst_QQmlPreview::blacklist()
203{
204 QQmlPreviewBlacklist blacklist;
205
206 QStringList strings({
207 "lalala", "lulul", "trakdkd", "suppe", "zack"
208 });
209
210 for (const QString &string : strings)
211 QVERIFY(!blacklist.isBlacklisted(string));
212
213 for (const QString &string : strings)
214 blacklist.blacklist(path: string);
215
216 for (const QString &string : strings) {
217 QVERIFY(blacklist.isBlacklisted(string));
218 QVERIFY(!blacklist.isBlacklisted(string.left(string.size() / 2)));
219 QVERIFY(!blacklist.isBlacklisted(string + "45"));
220 QVERIFY(!blacklist.isBlacklisted(" " + string));
221 QVERIFY(blacklist.isBlacklisted(string + "/45"));
222 }
223
224 for (auto begin = strings.begin(), it = begin, end = strings.end(); it != end; ++it) {
225 std::rotate(first: begin, middle: it, last: end);
226 QString path = "/" + strings.join(sep: '/');
227 blacklist.blacklist(path);
228 QVERIFY(blacklist.isBlacklisted(path));
229 QVERIFY(blacklist.isBlacklisted(path + "/file"));
230 QVERIFY(!blacklist.isBlacklisted(path + "more"));
231 path.chop(n: 1);
232 QVERIFY(!blacklist.isBlacklisted(path));
233 std::reverse(first: begin, last: end);
234 }
235
236 blacklist.clear();
237 for (const QString &string : strings)
238 QVERIFY(!blacklist.isBlacklisted(string));
239
240 blacklist.blacklist(path: ":/qt-project.org");
241 QVERIFY(blacklist.isBlacklisted(":/qt-project.org/QmlRuntime/conf/configuration.qml"));
242 QVERIFY(!blacklist.isBlacklisted(":/qt-project.orgQmlRuntime/conf/configuration.qml"));
243
244 QQmlPreviewBlacklist blacklist2;
245
246 blacklist2.blacklist(path: ":/qt-project.org");
247 blacklist2.blacklist(path: ":/QtQuick/Controls/Styles");
248 blacklist2.blacklist(path: ":/ExtrasImports/QtQuick/Controls/Styles");
249 blacklist2.blacklist(path: QLibraryInfo::location(QLibraryInfo::Qml2ImportsPath));
250 blacklist2.blacklist(path: "/home/ulf/.local/share/QtProject/Qml Runtime/configuration.qml");
251 blacklist2.blacklist(path: "/usr/share");
252 blacklist2.blacklist(path: "/usr/share/QtProject/Qml Runtime/configuration.qml");
253 QVERIFY(blacklist2.isBlacklisted(QLibraryInfo::location(QLibraryInfo::Qml2ImportsPath)));
254 blacklist2.blacklist(path: "/usr/local/share/QtProject/Qml Runtime/configuration.qml");
255 blacklist2.blacklist(path: "qml");
256 blacklist2.blacklist(path: ""); // This should not remove all other paths.
257
258 QVERIFY(blacklist2.isBlacklisted(QLibraryInfo::location(QLibraryInfo::Qml2ImportsPath) +
259 "/QtQuick/Window.2.0"));
260 QVERIFY(blacklist2.isBlacklisted(QLibraryInfo::location(QLibraryInfo::Qml2ImportsPath)));
261 QVERIFY(blacklist2.isBlacklisted("/usr/share/QtProject/Qml Runtime/configuration.qml"));
262 QVERIFY(blacklist2.isBlacklisted("/usr/share/stuff"));
263 QVERIFY(blacklist2.isBlacklisted(""));
264
265 QQmlPreviewBlacklist blacklist3;
266 blacklist3.blacklist(path: "/usr/share");
267 blacklist3.blacklist(path: "/usr");
268 blacklist3.blacklist(path: "/usrdings");
269 QVERIFY(blacklist3.isBlacklisted("/usrdings"));
270 QVERIFY(blacklist3.isBlacklisted("/usr/src"));
271 QVERIFY(!blacklist3.isBlacklisted("/opt/share"));
272 QVERIFY(!blacklist3.isBlacklisted("/opt"));
273
274 blacklist3.whitelist(path: "/usr/share");
275 QVERIFY(blacklist3.isBlacklisted("/usrdings"));
276 QVERIFY(!blacklist3.isBlacklisted("/usr"));
277 QVERIFY(!blacklist3.isBlacklisted("/usr/share"));
278 QVERIFY(!blacklist3.isBlacklisted("/usr/src"));
279 QVERIFY(!blacklist3.isBlacklisted("/opt/share"));
280 QVERIFY(!blacklist3.isBlacklisted("/opt"));
281}
282
283void tst_QQmlPreview::error()
284{
285 QCOMPARE(startQmlProcess("window.qml"), ConnectSuccess);
286 QVERIFY(m_client);
287 m_client->triggerLoad(url: testFileUrl(fileName: "broken.qml"));
288 QTRY_COMPARE_WITH_TIMEOUT(m_serviceErrors.count(), 1, 10000);
289 QVERIFY(m_serviceErrors.first().contains("broken.qml:32 Expected token `}'"));
290}
291
292static float parseZoomFactor(const QString &output)
293{
294 const QString prefix("zoom ");
295 const int start = output.lastIndexOf(s: prefix) + prefix.length();
296 if (start < 0)
297 return -1;
298 const int end = output.indexOf(c: '\n', from: start);
299 if (end < 0)
300 return -1;
301 bool ok = false;
302 const float zoomFactor = output.mid(position: start, n: end - start).toFloat(ok: &ok);
303 if (!ok)
304 return -1;
305 return zoomFactor;
306}
307
308static void verifyZoomFactor(const QQmlDebugProcess *process, float factor)
309{
310 QTRY_VERIFY_WITH_TIMEOUT(qFuzzyCompare(parseZoomFactor(process->output()), factor), 30000);
311}
312
313void tst_QQmlPreview::zoom()
314{
315 const QString file("zoom.qml");
316 QCOMPARE(startQmlProcess(file), ConnectSuccess);
317 QVERIFY(m_client);
318 m_client->triggerLoad(url: testFileUrl(fileName: file));
319 QTRY_VERIFY(m_files.contains(testFile(file)));
320 float baseZoomFactor = -1;
321 QTRY_VERIFY_WITH_TIMEOUT((baseZoomFactor = parseZoomFactor(m_process->output())) > 0, 30000);
322
323 for (auto testZoomFactor : {2.0f, 1.5f, 0.5f}) {
324 m_client->triggerZoom(factor: testZoomFactor);
325 verifyZoomFactor(process: m_process, factor: testZoomFactor);
326 }
327
328 m_client->triggerZoom(factor: -1.0f);
329 verifyZoomFactor(process: m_process, factor: baseZoomFactor);
330 m_process->stop();
331 QVERIFY(m_serviceErrors.isEmpty());
332}
333
334void tst_QQmlPreview::fps()
335{
336 const QString file("qtquick2.qml");
337 QCOMPARE(startQmlProcess(file), ConnectSuccess);
338 QVERIFY(m_client);
339 m_client->triggerLoad(url: testFileUrl(fileName: file));
340 if (QGuiApplication::platformName() != "offscreen") {
341 QTRY_VERIFY_WITH_TIMEOUT(m_frameStats.numSyncs > 10, 10000);
342 QVERIFY(m_frameStats.minSync <= m_frameStats.maxSync);
343 QVERIFY(m_frameStats.totalSync / m_frameStats.numSyncs >= m_frameStats.minSync - 1);
344 QVERIFY(m_frameStats.totalSync / m_frameStats.numSyncs <= m_frameStats.maxSync);
345
346 QVERIFY(m_frameStats.numRenders > 0);
347 QVERIFY(m_frameStats.minRender <= m_frameStats.maxRender);
348 QVERIFY(m_frameStats.totalRender / m_frameStats.numRenders >= m_frameStats.minRender - 1);
349 QVERIFY(m_frameStats.totalRender / m_frameStats.numRenders <= m_frameStats.maxRender);
350 } else {
351 QSKIP("offscreen rendering doesn't produce any frames");
352 }
353}
354
355void tst_QQmlPreview::language()
356{
357 QCOMPARE(startQmlProcess("window.qml"), ConnectSuccess);
358 QVERIFY(m_client);
359 m_client->triggerLanguage(url: dataDirectoryUrl(), locale: "fr_FR");
360 QTRY_VERIFY_WITH_TIMEOUT(m_files.contains(testFile("i18n/qml_fr_FR.qm")), 30000);
361}
362
363QTEST_MAIN(tst_QQmlPreview)
364
365#include "tst_qqmlpreview.moc"
366

source code of qtdeclarative/tests/auto/qml/debugger/qqmlpreview/tst_qqmlpreview.cpp