1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 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 | #include <qtest.h> |
29 | #include <QtTest/QtTest> |
30 | #include <QtQuick/private/qquickpixmapcache_p.h> |
31 | #include <QtQml/qqmlengine.h> |
32 | #include <QtQuick/qquickimageprovider.h> |
33 | #include <QtQml/QQmlComponent> |
34 | #include <QNetworkReply> |
35 | #include "../../shared/util.h" |
36 | #include "testhttpserver.h" |
37 | #include <QtNetwork/QNetworkConfigurationManager> |
38 | |
39 | #if QT_CONFIG(concurrent) |
40 | #include <qtconcurrentrun.h> |
41 | #include <qfuture.h> |
42 | #endif |
43 | |
44 | #define PIXMAP_DATA_LEAK_TEST 0 |
45 | |
46 | class tst_qquickpixmapcache : public QQmlDataTest |
47 | { |
48 | Q_OBJECT |
49 | public: |
50 | tst_qquickpixmapcache() {} |
51 | |
52 | private slots: |
53 | void initTestCase(); |
54 | void single(); |
55 | void single_data(); |
56 | void parallel(); |
57 | void parallel_data(); |
58 | void massive(); |
59 | void cancelcrash(); |
60 | void shrinkcache(); |
61 | #if QT_CONFIG(concurrent) |
62 | void networkCrash(); |
63 | #endif |
64 | void lockingCrash(); |
65 | void uncached(); |
66 | void asynchronousNoCache(); |
67 | #if PIXMAP_DATA_LEAK_TEST |
68 | void dataLeak(); |
69 | #endif |
70 | private: |
71 | QQmlEngine engine; |
72 | TestHTTPServer server; |
73 | }; |
74 | |
75 | static int slotters=0; |
76 | |
77 | class Slotter : public QObject |
78 | { |
79 | Q_OBJECT |
80 | public: |
81 | Slotter() |
82 | { |
83 | gotslot = false; |
84 | slotters++; |
85 | } |
86 | bool gotslot; |
87 | |
88 | public slots: |
89 | void got() |
90 | { |
91 | gotslot = true; |
92 | --slotters; |
93 | if (slotters==0) |
94 | QTestEventLoop::instance().exitLoop(); |
95 | } |
96 | }; |
97 | |
98 | #ifndef QT_NO_LOCALFILE_OPTIMIZED_QML |
99 | static const bool localfile_optimized = true; |
100 | #else |
101 | static const bool localfile_optimized = false; |
102 | #endif |
103 | |
104 | void tst_qquickpixmapcache::initTestCase() |
105 | { |
106 | QQmlDataTest::initTestCase(); |
107 | |
108 | QVERIFY2(server.listen(), qPrintable(server.errorString())); |
109 | |
110 | #if QT_CONFIG(bearermanagement) |
111 | // This avoids a race condition/deadlock bug in network config |
112 | // manager when it is accessed by the HTTP server thread before |
113 | // anything else. Bug report can be found at: |
114 | // QTBUG-26355 |
115 | QNetworkConfigurationManager cm; |
116 | cm.updateConfigurations(); |
117 | #endif |
118 | |
119 | server.serveDirectory(testFile(fileName: "http" )); |
120 | } |
121 | |
122 | void tst_qquickpixmapcache::single_data() |
123 | { |
124 | // Note, since QQuickPixmapCache is shared, tests affect each other! |
125 | // so use different files fore all test functions. |
126 | |
127 | QTest::addColumn<QUrl>(name: "target" ); |
128 | QTest::addColumn<bool>(name: "incache" ); |
129 | QTest::addColumn<bool>(name: "exists" ); |
130 | QTest::addColumn<bool>(name: "neterror" ); |
131 | |
132 | // File URLs are optimized |
133 | QTest::newRow(dataTag: "local" ) << testFileUrl(fileName: "exists.png" ) << localfile_optimized << true << false; |
134 | QTest::newRow(dataTag: "local" ) << testFileUrl(fileName: "notexists.png" ) << localfile_optimized << false << false; |
135 | QTest::newRow(dataTag: "remote" ) << server.url(documentPath: "/exists.png" ) << false << true << false; |
136 | QTest::newRow(dataTag: "remote" ) << server.url(documentPath: "/notexists.png" ) << false << false << true; |
137 | } |
138 | |
139 | void tst_qquickpixmapcache::single() |
140 | { |
141 | QFETCH(QUrl, target); |
142 | QFETCH(bool, incache); |
143 | QFETCH(bool, exists); |
144 | QFETCH(bool, neterror); |
145 | |
146 | QString expectedError; |
147 | if (neterror) { |
148 | expectedError = "Error transferring " + target.toString() + " - server replied: Not found" ; |
149 | } else if (!exists) { |
150 | expectedError = "Cannot open: " + target.toString(); |
151 | } |
152 | |
153 | QQuickPixmap pixmap; |
154 | QVERIFY(pixmap.width() <= 0); // Check Qt assumption |
155 | |
156 | pixmap.load(&engine, target); |
157 | |
158 | if (incache) { |
159 | QCOMPARE(pixmap.error(), expectedError); |
160 | if (exists) { |
161 | QCOMPARE(pixmap.status(), QQuickPixmap::Ready); |
162 | QVERIFY(pixmap.width() > 0); |
163 | } else { |
164 | QCOMPARE(pixmap.status(), QQuickPixmap::Error); |
165 | QVERIFY(pixmap.width() <= 0); |
166 | } |
167 | } else { |
168 | QVERIFY(pixmap.width() <= 0); |
169 | |
170 | Slotter getter; |
171 | pixmap.connectFinished(&getter, SLOT(got())); |
172 | QTestEventLoop::instance().enterLoop(secs: 10); |
173 | QVERIFY(!QTestEventLoop::instance().timeout()); |
174 | QVERIFY(getter.gotslot); |
175 | if (exists) { |
176 | QCOMPARE(pixmap.status(), QQuickPixmap::Ready); |
177 | QVERIFY(pixmap.width() > 0); |
178 | } else { |
179 | QCOMPARE(pixmap.status(), QQuickPixmap::Error); |
180 | QVERIFY(pixmap.width() <= 0); |
181 | } |
182 | QCOMPARE(pixmap.error(), expectedError); |
183 | } |
184 | } |
185 | |
186 | void tst_qquickpixmapcache::parallel_data() |
187 | { |
188 | // Note, since QQuickPixmapCache is shared, tests affect each other! |
189 | // so use different files fore all test functions. |
190 | |
191 | QTest::addColumn<QUrl>(name: "target1" ); |
192 | QTest::addColumn<QUrl>(name: "target2" ); |
193 | QTest::addColumn<int>(name: "incache" ); |
194 | QTest::addColumn<int>(name: "cancel" ); // which one to cancel |
195 | |
196 | QTest::newRow(dataTag: "local" ) |
197 | << testFileUrl(fileName: "exists1.png" ) |
198 | << testFileUrl(fileName: "exists2.png" ) |
199 | << (localfile_optimized ? 2 : 0) |
200 | << -1; |
201 | |
202 | QTest::newRow(dataTag: "remote" ) |
203 | << server.url(documentPath: "/exists2.png" ) |
204 | << server.url(documentPath: "/exists3.png" ) |
205 | << 0 |
206 | << -1; |
207 | |
208 | QTest::newRow(dataTag: "remoteagain" ) |
209 | << server.url(documentPath: "/exists2.png" ) |
210 | << server.url(documentPath: "/exists3.png" ) |
211 | << 2 |
212 | << -1; |
213 | |
214 | QTest::newRow(dataTag: "remotecopy" ) |
215 | << server.url(documentPath: "/exists4.png" ) |
216 | << server.url(documentPath: "/exists4.png" ) |
217 | << 0 |
218 | << -1; |
219 | |
220 | QTest::newRow(dataTag: "remotecopycancel" ) |
221 | << server.url(documentPath: "/exists5.png" ) |
222 | << server.url(documentPath: "/exists5.png" ) |
223 | << 0 |
224 | << 0; |
225 | } |
226 | |
227 | void tst_qquickpixmapcache::parallel() |
228 | { |
229 | QFETCH(QUrl, target1); |
230 | QFETCH(QUrl, target2); |
231 | QFETCH(int, incache); |
232 | QFETCH(int, cancel); |
233 | |
234 | QList<QUrl> targets; |
235 | targets << target1 << target2; |
236 | |
237 | QList<QQuickPixmap *> pixmaps; |
238 | QList<bool> pending; |
239 | QList<Slotter*> getters; |
240 | |
241 | for (int i=0; i<targets.count(); ++i) { |
242 | QUrl target = targets.at(i); |
243 | QQuickPixmap *pixmap = new QQuickPixmap; |
244 | |
245 | pixmap->load(&engine, target); |
246 | |
247 | QVERIFY(pixmap->status() != QQuickPixmap::Error); |
248 | pixmaps.append(t: pixmap); |
249 | if (pixmap->isReady()) { |
250 | QVERIFY(pixmap->width() > 0); |
251 | getters.append(t: 0); |
252 | pending.append(t: false); |
253 | } else { |
254 | QVERIFY(pixmap->width() <= 0); |
255 | getters.append(t: new Slotter); |
256 | pixmap->connectFinished(getters[i], SLOT(got())); |
257 | pending.append(t: true); |
258 | } |
259 | } |
260 | |
261 | if (incache + slotters != targets.count()) |
262 | QFAIL(QString::fromLatin1("pixmap counts don't add up: %1 incache, %2 slotters, %3 total" ) |
263 | .arg(incache).arg(slotters).arg(targets.count()).toLatin1().constData()); |
264 | |
265 | if (cancel >= 0) { |
266 | pixmaps.at(i: cancel)->clear(getters[cancel]); |
267 | slotters--; |
268 | } |
269 | |
270 | if (slotters) { |
271 | QTestEventLoop::instance().enterLoop(secs: 10); |
272 | QVERIFY(!QTestEventLoop::instance().timeout()); |
273 | } |
274 | |
275 | for (int i=0; i<targets.count(); ++i) { |
276 | QQuickPixmap *pixmap = pixmaps[i]; |
277 | |
278 | if (i == cancel) { |
279 | QVERIFY(!getters[i]->gotslot); |
280 | } else { |
281 | if (pending[i]) |
282 | QVERIFY(getters[i]->gotslot); |
283 | |
284 | if (!pixmap->isReady()) { |
285 | QFAIL(QString::fromLatin1("pixmap %1 not ready, status %2: %3" ) |
286 | .arg(pixmap->url().toString()).arg(pixmap->status()) |
287 | .arg(pixmap->error()).toLatin1().constData()); |
288 | |
289 | } |
290 | QVERIFY(pixmap->width() > 0); |
291 | delete getters[i]; |
292 | } |
293 | } |
294 | |
295 | qDeleteAll(c: pixmaps); |
296 | } |
297 | |
298 | void tst_qquickpixmapcache::massive() |
299 | { |
300 | QQmlEngine engine; |
301 | QUrl url = testFileUrl(fileName: "massive.png" ); |
302 | |
303 | // Confirm that massive images remain in the cache while they are |
304 | // in use by the application. |
305 | { |
306 | qint64 cachekey = 0; |
307 | QQuickPixmap p(&engine, url); |
308 | QVERIFY(p.isReady()); |
309 | QVERIFY(p.image().size() == QSize(10000, 1000)); |
310 | cachekey = p.image().cacheKey(); |
311 | |
312 | QQuickPixmap p2(&engine, url); |
313 | QVERIFY(p2.isReady()); |
314 | QVERIFY(p2.image().size() == QSize(10000, 1000)); |
315 | |
316 | QCOMPARE(p2.image().cacheKey(), cachekey); |
317 | } |
318 | |
319 | // Confirm that massive images are removed from the cache when |
320 | // they become unused |
321 | { |
322 | qint64 cachekey = 0; |
323 | { |
324 | QQuickPixmap p(&engine, url); |
325 | QVERIFY(p.isReady()); |
326 | QVERIFY(p.image().size() == QSize(10000, 1000)); |
327 | cachekey = p.image().cacheKey(); |
328 | } |
329 | |
330 | QQuickPixmap p2(&engine, url); |
331 | QVERIFY(p2.isReady()); |
332 | QVERIFY(p2.image().size() == QSize(10000, 1000)); |
333 | |
334 | QVERIFY(p2.image().cacheKey() != cachekey); |
335 | } |
336 | } |
337 | |
338 | // QTBUG-12729 |
339 | void tst_qquickpixmapcache::cancelcrash() |
340 | { |
341 | QUrl url = server.url(documentPath: "/cancelcrash_notexist.png" ); |
342 | for (int ii = 0; ii < 1000; ++ii) { |
343 | QQuickPixmap pix(&engine, url); |
344 | } |
345 | } |
346 | |
347 | class MyPixmapProvider : public QQuickImageProvider |
348 | { |
349 | public: |
350 | MyPixmapProvider() |
351 | : QQuickImageProvider(Pixmap) {} |
352 | |
353 | virtual QPixmap requestPixmap(const QString &d, QSize *, const QSize &) { |
354 | Q_UNUSED(d) |
355 | QPixmap pix(800, 600); |
356 | pix.fill(fillColor); |
357 | return pix; |
358 | } |
359 | |
360 | static QRgb fillColor; |
361 | }; |
362 | |
363 | QRgb MyPixmapProvider::fillColor = qRgb(r: 255, g: 0, b: 0); |
364 | |
365 | // QTBUG-13345 |
366 | void tst_qquickpixmapcache::shrinkcache() |
367 | { |
368 | QQmlEngine engine; |
369 | engine.addImageProvider(id: QLatin1String("mypixmaps" ), new MyPixmapProvider); |
370 | |
371 | for (int ii = 0; ii < 4000; ++ii) { |
372 | QUrl url("image://mypixmaps/" + QString::number(ii)); |
373 | QQuickPixmap p(&engine, url); |
374 | } |
375 | } |
376 | |
377 | #if QT_CONFIG(concurrent) |
378 | |
379 | void createNetworkServer(TestHTTPServer *server) |
380 | { |
381 | QEventLoop eventLoop; |
382 | server->serveDirectory(QQmlDataTest::instance()->testFile(fileName: "http" )); |
383 | QTimer::singleShot(msec: 100, receiver: &eventLoop, SLOT(quit())); |
384 | eventLoop.exec(); |
385 | } |
386 | |
387 | #if QT_CONFIG(concurrent) |
388 | // QT-3957 |
389 | void tst_qquickpixmapcache::networkCrash() |
390 | { |
391 | TestHTTPServer server; |
392 | QVERIFY2(server.listen(), qPrintable(server.errorString())); |
393 | QFuture<void> future = QtConcurrent::run(functionPointer: createNetworkServer, arg1: &server); |
394 | QQmlEngine engine; |
395 | for (int ii = 0; ii < 100 ; ++ii) { |
396 | QQuickPixmap* pixmap = new QQuickPixmap; |
397 | pixmap->load(&engine, server.url(documentPath: "/exists.png" )); |
398 | QTest::qSleep(ms: 1); |
399 | pixmap->clear(); |
400 | delete pixmap; |
401 | } |
402 | future.cancel(); |
403 | } |
404 | #endif |
405 | |
406 | #endif |
407 | |
408 | // QTBUG-22125 |
409 | void tst_qquickpixmapcache::lockingCrash() |
410 | { |
411 | TestHTTPServer server; |
412 | QVERIFY2(server.listen(), qPrintable(server.errorString())); |
413 | server.serveDirectory(testFile(fileName: "http" ), TestHTTPServer::Delay); |
414 | |
415 | { |
416 | QQuickPixmap* p = new QQuickPixmap; |
417 | { |
418 | QQmlEngine e; |
419 | p->load(&e, server.url(documentPath: "/exists6.png" )); |
420 | } |
421 | p->clear(); |
422 | QVERIFY(p->isNull()); |
423 | delete p; |
424 | server.sendDelayedItem(); |
425 | } |
426 | } |
427 | |
428 | void tst_qquickpixmapcache::uncached() |
429 | { |
430 | QQmlEngine engine; |
431 | engine.addImageProvider(id: QLatin1String("mypixmaps" ), new MyPixmapProvider); |
432 | |
433 | QUrl url("image://mypixmaps/mypix" ); |
434 | { |
435 | QQuickPixmap p; |
436 | p.load(&engine, url, options: QQuickPixmap::Options{}); |
437 | QImage img = p.image(); |
438 | QCOMPARE(img.pixel(0,0), qRgb(255, 0, 0)); |
439 | } |
440 | |
441 | // uncached, so we will get a different colored image |
442 | MyPixmapProvider::fillColor = qRgb(r: 0, g: 255, b: 0); |
443 | { |
444 | QQuickPixmap p; |
445 | p.load(&engine, url, options: QQuickPixmap::Options{}); |
446 | QImage img = p.image(); |
447 | QCOMPARE(img.pixel(0,0), qRgb(0, 255, 0)); |
448 | } |
449 | |
450 | // Load the image with cache enabled |
451 | MyPixmapProvider::fillColor = qRgb(r: 0, g: 0, b: 255); |
452 | { |
453 | QQuickPixmap p; |
454 | p.load(&engine, url, options: QQuickPixmap::Cache); |
455 | QImage img = p.image(); |
456 | QCOMPARE(img.pixel(0,0), qRgb(0, 0, 255)); |
457 | } |
458 | |
459 | // We should not get the cached version if we request uncached |
460 | MyPixmapProvider::fillColor = qRgb(r: 255, g: 0, b: 255); |
461 | { |
462 | QQuickPixmap p; |
463 | p.load(&engine, url, options: QQuickPixmap::Options{}); |
464 | QImage img = p.image(); |
465 | QCOMPARE(img.pixel(0,0), qRgb(255, 0, 255)); |
466 | } |
467 | |
468 | // If we again load the image with cache enabled, we should get the previously cached version |
469 | MyPixmapProvider::fillColor = qRgb(r: 0, g: 255, b: 255); |
470 | { |
471 | QQuickPixmap p; |
472 | p.load(&engine, url, options: QQuickPixmap::Cache); |
473 | QImage img = p.image(); |
474 | QCOMPARE(img.pixel(0,0), qRgb(0, 0, 255)); |
475 | } |
476 | } |
477 | |
478 | void tst_qquickpixmapcache::asynchronousNoCache() |
479 | { |
480 | QQmlEngine engine; |
481 | QQmlComponent component(&engine, testFileUrl(fileName: "asynchronousNoCache.qml" )); |
482 | QScopedPointer<QObject> root {component.create()}; // should not crash |
483 | } |
484 | |
485 | |
486 | #if PIXMAP_DATA_LEAK_TEST |
487 | // This test should not be enabled by default as it |
488 | // produces spurious output in the expected case. |
489 | #include <QtQuick/QQuickView> |
490 | class DataLeakView : public QQuickView |
491 | { |
492 | Q_OBJECT |
493 | |
494 | public: |
495 | explicit DataLeakView() : QQuickView() |
496 | { |
497 | setSource(testFileUrl("dataLeak.qml" )); |
498 | } |
499 | |
500 | void showFor2Seconds() |
501 | { |
502 | showFullScreen(); |
503 | QTimer::singleShot(2000, this, SIGNAL(ready())); |
504 | } |
505 | |
506 | signals: |
507 | void ready(); |
508 | }; |
509 | |
510 | // QTBUG-22742 |
511 | Q_GLOBAL_STATIC(QQuickPixmap, dataLeakPixmap) |
512 | void tst_qquickpixmapcache::dataLeak() |
513 | { |
514 | // Should not leak cached QQuickPixmapData. |
515 | // Unfortunately, since the QQuickPixmapStore |
516 | // is a global static, and it releases the cache |
517 | // entries on dtor (application exit), we must use |
518 | // valgrind to determine whether it leaks or not. |
519 | QQuickPixmap *p1 = new QQuickPixmap; |
520 | QQuickPixmap *p2 = new QQuickPixmap; |
521 | { |
522 | QScopedPointer<DataLeakView> test(new DataLeakView); |
523 | test->showFor2Seconds(); |
524 | dataLeakPixmap()->load(test->engine(), testFileUrl("exists.png" )); |
525 | p1->load(test->engine(), testFileUrl("exists.png" )); |
526 | p2->load(test->engine(), testFileUrl("exists2.png" )); |
527 | QTest::qWait(2005); // 2 seconds + a few more millis. |
528 | } |
529 | |
530 | // When the (global static) dataLeakPixmap is deleted, it |
531 | // shouldn't attempt to dereference a QQuickPixmapData |
532 | // which has been deleted by the QQuickPixmapStore |
533 | // destructor. |
534 | } |
535 | #endif |
536 | #undef PIXMAP_DATA_LEAK_TEST |
537 | |
538 | QTEST_MAIN(tst_qquickpixmapcache) |
539 | |
540 | #include "tst_qquickpixmapcache.moc" |
541 | |