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
46class tst_qquickpixmapcache : public QQmlDataTest
47{
48 Q_OBJECT
49public:
50 tst_qquickpixmapcache() {}
51
52private 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
70private:
71 QQmlEngine engine;
72 TestHTTPServer server;
73};
74
75static int slotters=0;
76
77class Slotter : public QObject
78{
79 Q_OBJECT
80public:
81 Slotter()
82 {
83 gotslot = false;
84 slotters++;
85 }
86 bool gotslot;
87
88public 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
99static const bool localfile_optimized = true;
100#else
101static const bool localfile_optimized = false;
102#endif
103
104void 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
122void 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
139void 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
186void 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
227void 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
298void 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
339void 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
347class MyPixmapProvider : public QQuickImageProvider
348{
349public:
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
363QRgb MyPixmapProvider::fillColor = qRgb(r: 255, g: 0, b: 0);
364
365// QTBUG-13345
366void 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
379void 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
389void 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
409void 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
428void 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
478void 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>
490class DataLeakView : public QQuickView
491{
492 Q_OBJECT
493
494public:
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
506signals:
507 void ready();
508};
509
510// QTBUG-22742
511Q_GLOBAL_STATIC(QQuickPixmap, dataLeakPixmap)
512void 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
538QTEST_MAIN(tst_qquickpixmapcache)
539
540#include "tst_qquickpixmapcache.moc"
541

source code of qtdeclarative/tests/auto/quick/qquickpixmapcache/tst_qquickpixmapcache.cpp