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 | |
44 | class tst_QQmlPreview : public QQmlDebugTest |
45 | { |
46 | Q_OBJECT |
47 | |
48 | private: |
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 | |
62 | private 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 | |
75 | QQmlDebugTest::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 | |
81 | void 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 | |
100 | QList<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 | |
116 | void tst_QQmlPreview::verifyProcessOutputContains(const QString &string) const |
117 | { |
118 | QTRY_VERIFY_WITH_TIMEOUT(m_process->output().contains(string), 30000); |
119 | } |
120 | |
121 | void 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 | |
128 | void 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 | |
148 | void 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 | |
162 | void 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 | |
184 | void 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 | |
202 | void 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 | |
283 | void 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 | |
292 | static 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 | |
308 | static void verifyZoomFactor(const QQmlDebugProcess *process, float factor) |
309 | { |
310 | QTRY_VERIFY_WITH_TIMEOUT(qFuzzyCompare(parseZoomFactor(process->output()), factor), 30000); |
311 | } |
312 | |
313 | void 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 | |
334 | void 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 | |
355 | void 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 | |
363 | QTEST_MAIN(tst_QQmlPreview) |
364 | |
365 | #include "tst_qqmlpreview.moc" |
366 | |