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 <QtQml/qqmlengine.h> |
31 | #include <QtQuick/qquickimageprovider.h> |
32 | #include <private/qquickimage_p.h> |
33 | #include <QImageReader> |
34 | #include <QWaitCondition> |
35 | #include <QThreadPool> |
36 | #include <private/qqmlengine_p.h> |
37 | |
38 | Q_DECLARE_METATYPE(QQuickImageProvider*); |
39 | |
40 | class tst_qquickimageprovider : public QObject |
41 | { |
42 | Q_OBJECT |
43 | public: |
44 | tst_qquickimageprovider() |
45 | { |
46 | } |
47 | |
48 | private slots: |
49 | void requestImage_sync_data(); |
50 | void requestImage_sync(); |
51 | void requestImage_async_data(); |
52 | void requestImage_async(); |
53 | void requestImage_async_forced_data(); |
54 | void requestImage_async_forced(); |
55 | |
56 | void requestPixmap_sync_data(); |
57 | void requestPixmap_sync(); |
58 | void requestPixmap_async(); |
59 | |
60 | void removeProvider_data(); |
61 | void removeProvider(); |
62 | |
63 | void imageProviderId_data(); |
64 | void imageProviderId(); |
65 | |
66 | void threadTest(); |
67 | |
68 | void asyncTextureTest(); |
69 | void instantAsyncTextureTest(); |
70 | |
71 | void asyncImageThreadSafety(); |
72 | |
73 | private: |
74 | QString newImageFileName() const; |
75 | void fillRequestTestsData(const char *id); |
76 | void runTest(bool async, QQuickImageProvider *provider); |
77 | }; |
78 | |
79 | |
80 | class TestQImageProvider : public QQuickImageProvider |
81 | { |
82 | public: |
83 | TestQImageProvider(bool *deleteWatch = nullptr, bool forceAsync = false) |
84 | : QQuickImageProvider(Image, (forceAsync ? ForceAsynchronousImageLoading : Flags())) |
85 | , deleteWatch(deleteWatch) |
86 | { |
87 | } |
88 | |
89 | ~TestQImageProvider() |
90 | { |
91 | if (deleteWatch) |
92 | *deleteWatch = true; |
93 | } |
94 | |
95 | QImage requestImage(const QString &id, QSize *size, const QSize& requestedSize) |
96 | { |
97 | lastImageId = id; |
98 | |
99 | if (id == QLatin1String("no-such-file.png" )) |
100 | return QImage(); |
101 | |
102 | int width = 100; |
103 | int height = 100; |
104 | QImage image(width, height, QImage::Format_RGB32); |
105 | if (size) |
106 | *size = QSize(width, height); |
107 | if (requestedSize.isValid()) |
108 | image = image.scaled(s: requestedSize); |
109 | return image; |
110 | } |
111 | |
112 | bool *deleteWatch; |
113 | QString lastImageId; |
114 | }; |
115 | Q_DECLARE_METATYPE(TestQImageProvider*); |
116 | |
117 | |
118 | class TestQPixmapProvider : public QQuickImageProvider |
119 | { |
120 | public: |
121 | TestQPixmapProvider(bool *deleteWatch = nullptr) |
122 | : QQuickImageProvider(Pixmap), deleteWatch(deleteWatch) |
123 | { |
124 | } |
125 | |
126 | ~TestQPixmapProvider() |
127 | { |
128 | if (deleteWatch) |
129 | *deleteWatch = true; |
130 | } |
131 | |
132 | QPixmap requestPixmap(const QString &id, QSize *size, const QSize& requestedSize) |
133 | { |
134 | lastImageId = id; |
135 | |
136 | if (id == QLatin1String("no-such-file.png" )) |
137 | return QPixmap(); |
138 | |
139 | int width = 100; |
140 | int height = 100; |
141 | QPixmap image(width, height); |
142 | if (size) |
143 | *size = QSize(width, height); |
144 | if (requestedSize.isValid()) |
145 | image = image.scaled(s: requestedSize); |
146 | return image; |
147 | } |
148 | |
149 | bool *deleteWatch; |
150 | QString lastImageId; |
151 | }; |
152 | Q_DECLARE_METATYPE(TestQPixmapProvider*); |
153 | |
154 | |
155 | QString tst_qquickimageprovider::newImageFileName() const |
156 | { |
157 | // need to generate new filenames each time or else images are loaded |
158 | // from cache and we won't get loading status changes when testing |
159 | // async loading |
160 | static int count = 0; |
161 | return QString("image://test/image-%1.png" ).arg(a: count++); |
162 | } |
163 | |
164 | void tst_qquickimageprovider::fillRequestTestsData(const char *id) |
165 | { |
166 | QTest::addColumn<QString>(name: "source" ); |
167 | QTest::addColumn<QString>(name: "imageId" ); |
168 | QTest::addColumn<QString>(name: "properties" ); |
169 | QTest::addColumn<QSize>(name: "size" ); |
170 | QTest::addColumn<QString>(name: "error" ); |
171 | |
172 | QString fileName = newImageFileName(); |
173 | QTest::addRow(format: "%s simple test" , id) |
174 | << "image://test/" + fileName << fileName << "" << QSize(100,100) << "" ; |
175 | |
176 | fileName = newImageFileName(); |
177 | QTest::addRow(format: "%s simple test with capitalization" , id)//As it's a URL, should make no difference |
178 | << "image://Test/" + fileName << fileName << "" << QSize(100,100) << "" ; |
179 | |
180 | fileName = newImageFileName(); |
181 | QTest::addRow(format: "%s url with no id" , id) |
182 | << "image://test/" + fileName << "" + fileName << "" << QSize(100,100) << "" ; |
183 | |
184 | fileName = newImageFileName(); |
185 | QTest::addRow(format: "%s url with path" , id) |
186 | << "image://test/test/path" + fileName << "test/path" + fileName << "" << QSize(100,100) << "" ; |
187 | |
188 | fileName = newImageFileName(); |
189 | QTest::addRow(format: "%s url with fragment" , id) |
190 | << "image://test/faq.html?#question13" + fileName << "faq.html?#question13" + fileName << "" << QSize(100,100) << "" ; |
191 | |
192 | fileName = newImageFileName(); |
193 | QTest::addRow(format: "%s url with query" , id) |
194 | << "image://test/cgi-bin/drawgraph.cgi?type=pie&color=green" + fileName << "cgi-bin/drawgraph.cgi?type=pie&color=green" + fileName |
195 | << "" << QSize(100,100) << "" ; |
196 | |
197 | fileName = newImageFileName(); |
198 | QTest::addRow(format: "%s scaled image" , id) |
199 | << "image://test/" + fileName << fileName << "sourceSize: \"80x30\"" << QSize(80,30) << "" ; |
200 | |
201 | QTest::addRow(format: "%s missing" , id) |
202 | << "image://test/no-such-file.png" << "no-such-file.png" << "" << QSize(100,100) |
203 | << "<Unknown File>:2:1: QML Image: Failed to get image from provider: image://test/no-such-file.png" ; |
204 | |
205 | QTest::addRow(format: "%s unknown provider" , id) |
206 | << "image://bogus/exists.png" << "" << "" << QSize() |
207 | << "<Unknown File>:2:1: QML Image: Invalid image provider: image://bogus/exists.png" ; |
208 | } |
209 | |
210 | void tst_qquickimageprovider::runTest(bool async, QQuickImageProvider *provider) |
211 | { |
212 | QFETCH(QString, source); |
213 | QFETCH(QString, imageId); |
214 | QFETCH(QString, properties); |
215 | QFETCH(QSize, size); |
216 | QFETCH(QString, error); |
217 | |
218 | if (!error.isEmpty()) |
219 | QTest::ignoreMessage(type: QtWarningMsg, message: error.toUtf8()); |
220 | |
221 | QQmlEngine engine; |
222 | |
223 | engine.addImageProvider(id: "test" , provider); |
224 | QVERIFY(engine.imageProvider("test" ) != nullptr); |
225 | |
226 | QString componentStr = "import QtQuick 2.0\nImage { source: \"" + source + "\"; " |
227 | + (async ? "asynchronous: true; " : "" ) |
228 | + properties + " }" ; |
229 | QQmlComponent component(&engine); |
230 | component.setData(componentStr.toLatin1(), baseUrl: QUrl::fromLocalFile(localfile: "" )); |
231 | QQuickImage *obj = qobject_cast<QQuickImage*>(object: component.create()); |
232 | QVERIFY(obj != nullptr); |
233 | |
234 | // From this point on, treat forced async providers as async behaviour-wise |
235 | if (engine.imageProvider(id: QUrl(source).host()) == provider) |
236 | async |= (provider->flags() & QQuickImageProvider::ForceAsynchronousImageLoading) != 0; |
237 | |
238 | if (async) |
239 | QTRY_COMPARE(obj->status(), QQuickImage::Loading); |
240 | |
241 | QCOMPARE(obj->source(), QUrl(source)); |
242 | |
243 | if (error.isEmpty()) { |
244 | if (async) |
245 | QTRY_COMPARE(obj->status(), QQuickImage::Ready); |
246 | else |
247 | QCOMPARE(obj->status(), QQuickImage::Ready); |
248 | if (QByteArray(QTest::currentDataTag()).startsWith(c: "qimage" )) |
249 | QCOMPARE(static_cast<TestQImageProvider*>(provider)->lastImageId, imageId); |
250 | else |
251 | QCOMPARE(static_cast<TestQPixmapProvider*>(provider)->lastImageId, imageId); |
252 | |
253 | QCOMPARE(obj->width(), qreal(size.width())); |
254 | QCOMPARE(obj->height(), qreal(size.height())); |
255 | QCOMPARE(obj->fillMode(), QQuickImage::Stretch); |
256 | QCOMPARE(obj->progress(), 1.0); |
257 | } else { |
258 | if (async) |
259 | QTRY_COMPARE(obj->status(), QQuickImage::Error); |
260 | else |
261 | QCOMPARE(obj->status(), QQuickImage::Error); |
262 | } |
263 | |
264 | delete obj; |
265 | } |
266 | |
267 | void tst_qquickimageprovider::requestImage_sync_data() |
268 | { |
269 | fillRequestTestsData(id: "qimage|sync" ); |
270 | } |
271 | |
272 | void tst_qquickimageprovider::requestImage_sync() |
273 | { |
274 | bool deleteWatch = false; |
275 | runTest(async: false, provider: new TestQImageProvider(&deleteWatch)); |
276 | QVERIFY(deleteWatch); |
277 | } |
278 | |
279 | void tst_qquickimageprovider::requestImage_async_data() |
280 | { |
281 | fillRequestTestsData(id: "qimage|async" ); |
282 | } |
283 | |
284 | void tst_qquickimageprovider::requestImage_async() |
285 | { |
286 | bool deleteWatch = false; |
287 | runTest(async: true, provider: new TestQImageProvider(&deleteWatch)); |
288 | QVERIFY(deleteWatch); |
289 | } |
290 | |
291 | void tst_qquickimageprovider::requestImage_async_forced_data() |
292 | { |
293 | fillRequestTestsData(id: "qimage|async_forced" ); |
294 | } |
295 | |
296 | void tst_qquickimageprovider::requestImage_async_forced() |
297 | { |
298 | bool deleteWatch = false; |
299 | runTest(async: false, provider: new TestQImageProvider(&deleteWatch, true)); |
300 | QVERIFY(deleteWatch); |
301 | } |
302 | |
303 | void tst_qquickimageprovider::requestPixmap_sync_data() |
304 | { |
305 | fillRequestTestsData(id: "qpixmap" ); |
306 | } |
307 | |
308 | void tst_qquickimageprovider::requestPixmap_sync() |
309 | { |
310 | bool deleteWatch = false; |
311 | runTest(async: false, provider: new TestQPixmapProvider(&deleteWatch)); |
312 | QVERIFY(deleteWatch); |
313 | } |
314 | |
315 | void tst_qquickimageprovider::requestPixmap_async() |
316 | { |
317 | QQmlEngine engine; |
318 | QQuickImageProvider *provider = new TestQPixmapProvider(); |
319 | |
320 | engine.addImageProvider(id: "test" , provider); |
321 | QVERIFY(engine.imageProvider("test" ) != nullptr); |
322 | |
323 | // pixmaps are loaded synchronously regardless of 'asynchronous' value |
324 | QString componentStr = "import QtQuick 2.0\nImage { asynchronous: true; source: \"image://test/pixmap-async-test.png\" }" ; |
325 | QQmlComponent component(&engine); |
326 | component.setData(componentStr.toLatin1(), baseUrl: QUrl::fromLocalFile(localfile: "" )); |
327 | QQuickImage *obj = qobject_cast<QQuickImage*>(object: component.create()); |
328 | QVERIFY(obj != nullptr); |
329 | |
330 | delete obj; |
331 | } |
332 | |
333 | void tst_qquickimageprovider::removeProvider_data() |
334 | { |
335 | QTest::addColumn<QQuickImageProvider*>(name: "provider" ); |
336 | |
337 | QTest::newRow(dataTag: "qimage" ) << static_cast<QQuickImageProvider*>(new TestQImageProvider); |
338 | QTest::newRow(dataTag: "qpixmap" ) << static_cast<QQuickImageProvider*>(new TestQPixmapProvider); |
339 | } |
340 | |
341 | void tst_qquickimageprovider::removeProvider() |
342 | { |
343 | QFETCH(QQuickImageProvider*, provider); |
344 | |
345 | QQmlEngine engine; |
346 | |
347 | engine.addImageProvider(id: "test" , provider); |
348 | QVERIFY(engine.imageProvider("test" ) != nullptr); |
349 | |
350 | // add provider, confirm it works |
351 | QString componentStr = "import QtQuick 2.0\nImage { source: \"" + newImageFileName() + "\" }" ; |
352 | QQmlComponent component(&engine); |
353 | component.setData(componentStr.toLatin1(), baseUrl: QUrl::fromLocalFile(localfile: "" )); |
354 | QQuickImage *obj = qobject_cast<QQuickImage*>(object: component.create()); |
355 | QVERIFY(obj != nullptr); |
356 | |
357 | QCOMPARE(obj->status(), QQuickImage::Ready); |
358 | |
359 | // remove the provider and confirm |
360 | QString fileName = newImageFileName(); |
361 | QString error("<Unknown File>:2:1: QML Image: Invalid image provider: " + fileName); |
362 | QTest::ignoreMessage(type: QtWarningMsg, message: error.toUtf8()); |
363 | |
364 | engine.removeImageProvider(id: "test" ); |
365 | |
366 | obj->setSource(QUrl(fileName)); |
367 | QCOMPARE(obj->status(), QQuickImage::Error); |
368 | |
369 | delete obj; |
370 | } |
371 | |
372 | void tst_qquickimageprovider::imageProviderId_data() |
373 | { |
374 | QTest::addColumn<QString>(name: "providerId" ); |
375 | |
376 | QTest::newRow(dataTag: "lowercase" ) << QStringLiteral("imageprovider" ); |
377 | QTest::newRow(dataTag: "CamelCase" ) << QStringLiteral("ImageProvider" ); |
378 | QTest::newRow(dataTag: "UPPERCASE" ) << QStringLiteral("IMAGEPROVIDER" ); |
379 | } |
380 | |
381 | void tst_qquickimageprovider::imageProviderId() |
382 | { |
383 | QFETCH(QString, providerId); |
384 | |
385 | QQmlEngine engine; |
386 | |
387 | bool deleteWatch = false; |
388 | TestQImageProvider *provider = new TestQImageProvider(&deleteWatch); |
389 | |
390 | engine.addImageProvider(id: providerId, provider); |
391 | QVERIFY(engine.imageProvider(providerId) != nullptr); |
392 | |
393 | engine.removeImageProvider(id: providerId); |
394 | QVERIFY(deleteWatch); |
395 | } |
396 | |
397 | class TestThreadProvider : public QQuickImageProvider |
398 | { |
399 | public: |
400 | TestThreadProvider() : QQuickImageProvider(Image) {} |
401 | |
402 | ~TestThreadProvider() {} |
403 | |
404 | QImage requestImage(const QString &id, QSize *size, const QSize& requestedSize) |
405 | { |
406 | mutex.lock(); |
407 | if (!ok) |
408 | cond.wait(lockedMutex: &mutex); |
409 | mutex.unlock(); |
410 | QVector<int> v; |
411 | for (int i = 0; i < 10000; i++) |
412 | v.prepend(t: i); //do some computation |
413 | QImage image(50,50, QImage::Format_RGB32); |
414 | image.fill(pixel: QColor(id).rgb()); |
415 | if (size) |
416 | *size = image.size(); |
417 | if (requestedSize.isValid()) |
418 | image = image.scaled(s: requestedSize); |
419 | return image; |
420 | } |
421 | |
422 | QWaitCondition cond; |
423 | QMutex mutex; |
424 | bool ok = false; |
425 | }; |
426 | |
427 | |
428 | void tst_qquickimageprovider::threadTest() |
429 | { |
430 | QQmlEngine engine; |
431 | |
432 | TestThreadProvider *provider = new TestThreadProvider; |
433 | |
434 | engine.addImageProvider(id: "test_thread" , provider); |
435 | QVERIFY(engine.imageProvider("test_thread" ) != nullptr); |
436 | |
437 | QString componentStr = "import QtQuick 2.0\nItem { \n" |
438 | "Image { source: \"image://test_thread/blue\"; asynchronous: true; }\n" |
439 | "Image { source: \"image://test_thread/red\"; asynchronous: true; }\n" |
440 | "Image { source: \"image://test_thread/green\"; asynchronous: true; }\n" |
441 | "Image { source: \"image://test_thread/yellow\"; asynchronous: true; }\n" |
442 | " }" ; |
443 | QQmlComponent component(&engine); |
444 | component.setData(componentStr.toLatin1(), baseUrl: QUrl::fromLocalFile(localfile: "" )); |
445 | QObject *obj = component.create(); |
446 | //MUST not deadlock |
447 | QVERIFY(obj != nullptr); |
448 | QList<QQuickImage *> images = obj->findChildren<QQuickImage *>(); |
449 | QCOMPARE(images.count(), 4); |
450 | QTest::qWait(ms: 100); |
451 | foreach (QQuickImage *img, images) { |
452 | QCOMPARE(img->status(), QQuickImage::Loading); |
453 | } |
454 | { |
455 | QMutexLocker lock(&provider->mutex); |
456 | provider->ok = true; |
457 | provider->cond.wakeAll(); |
458 | } |
459 | QTest::qWait(ms: 250); |
460 | foreach (QQuickImage *img, images) { |
461 | QTRY_COMPARE(img->status(), QQuickImage::Ready); |
462 | } |
463 | } |
464 | |
465 | class TestImageResponseRunner : public QObject, public QRunnable { |
466 | |
467 | Q_OBJECT |
468 | |
469 | public: |
470 | Q_SIGNAL void finished(QQuickTextureFactory *texture); |
471 | TestImageResponseRunner(QMutex *lock, QWaitCondition *condition, bool *ok, const QString &id, const QSize &requestedSize) |
472 | : m_lock(lock), m_condition(condition), m_ok(ok), m_id(id), m_requestedSize(requestedSize) {} |
473 | void run() |
474 | { |
475 | m_lock->lock(); |
476 | if (!(*m_ok)) { |
477 | m_condition->wait(lockedMutex: m_lock); |
478 | } |
479 | m_lock->unlock(); |
480 | QImage image(50, 50, QImage::Format_RGB32); |
481 | image.fill(pixel: QColor(m_id).rgb()); |
482 | if (m_requestedSize.isValid()) |
483 | image = image.scaled(s: m_requestedSize); |
484 | emit finished(texture: QQuickTextureFactory::textureFactoryForImage(image)); |
485 | } |
486 | |
487 | private: |
488 | QMutex *m_lock; |
489 | QWaitCondition *m_condition; |
490 | bool *m_ok; |
491 | QString m_id; |
492 | QSize m_requestedSize; |
493 | }; |
494 | |
495 | class TestImageResponse : public QQuickImageResponse |
496 | { |
497 | public: |
498 | TestImageResponse(QMutex *lock, QWaitCondition *condition, bool *ok, const QString &id, const QSize &requestedSize, QThreadPool *pool) |
499 | : m_lock(lock), m_condition(condition), m_ok(ok), m_id(id), m_requestedSize(requestedSize), m_texture(nullptr) |
500 | { |
501 | auto runnable = new TestImageResponseRunner(m_lock, m_condition, m_ok, m_id, m_requestedSize); |
502 | QObject::connect(sender: runnable, signal: &TestImageResponseRunner::finished, receiver: this, slot: &TestImageResponse::handleResponse); |
503 | pool->start(runnable); |
504 | } |
505 | |
506 | QQuickTextureFactory *textureFactory() const |
507 | { |
508 | return m_texture; |
509 | } |
510 | |
511 | void handleResponse(QQuickTextureFactory *factory) { |
512 | this->m_texture = factory; |
513 | emit finished(); |
514 | } |
515 | |
516 | QMutex *m_lock; |
517 | QWaitCondition *m_condition; |
518 | bool *m_ok; |
519 | QString m_id; |
520 | QSize m_requestedSize; |
521 | QQuickTextureFactory *m_texture; |
522 | }; |
523 | |
524 | class TestAsyncProvider : public QQuickAsyncImageProvider |
525 | { |
526 | public: |
527 | TestAsyncProvider() |
528 | { |
529 | pool.setMaxThreadCount(4); |
530 | } |
531 | |
532 | ~TestAsyncProvider() {} |
533 | |
534 | QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) |
535 | { |
536 | TestImageResponse *response = new TestImageResponse(&lock, &condition, &ok, id, requestedSize, &pool); |
537 | return response; |
538 | } |
539 | |
540 | QThreadPool pool; |
541 | QMutex lock; |
542 | QWaitCondition condition; |
543 | bool ok = false; |
544 | }; |
545 | |
546 | |
547 | void tst_qquickimageprovider::asyncTextureTest() |
548 | { |
549 | QQmlEngine engine; |
550 | |
551 | TestAsyncProvider *provider = new TestAsyncProvider; |
552 | |
553 | engine.addImageProvider(id: "test_async" , provider); |
554 | QVERIFY(engine.imageProvider("test_async" ) != nullptr); |
555 | |
556 | QString componentStr = "import QtQuick 2.0\nItem { \n" |
557 | "Image { source: \"image://test_async/blue\"; }\n" |
558 | "Image { source: \"image://test_async/red\"; }\n" |
559 | "Image { source: \"image://test_async/green\"; }\n" |
560 | "Image { source: \"image://test_async/yellow\"; }\n" |
561 | " }" ; |
562 | QQmlComponent component(&engine); |
563 | component.setData(componentStr.toLatin1(), baseUrl: QUrl::fromLocalFile(localfile: "" )); |
564 | QObject *obj = component.create(); |
565 | //MUST not deadlock |
566 | QVERIFY(obj != nullptr); |
567 | QList<QQuickImage *> images = obj->findChildren<QQuickImage *>(); |
568 | QCOMPARE(images.count(), 4); |
569 | |
570 | QTRY_COMPARE(provider->pool.activeThreadCount(), 4); |
571 | foreach (QQuickImage *img, images) { |
572 | QTRY_COMPARE(img->status(), QQuickImage::Loading); |
573 | } |
574 | { |
575 | QMutexLocker lock(&provider->lock); |
576 | provider->ok = true; |
577 | provider->condition.wakeAll(); |
578 | } |
579 | foreach (QQuickImage *img, images) { |
580 | QTRY_COMPARE(img->status(), QQuickImage::Ready); |
581 | } |
582 | } |
583 | |
584 | class InstantAsyncImageResponse : public QQuickImageResponse |
585 | { |
586 | public: |
587 | InstantAsyncImageResponse(const QString &id, const QSize &requestedSize) |
588 | { |
589 | QImage image(50, 50, QImage::Format_RGB32); |
590 | image.fill(pixel: QColor(id).rgb()); |
591 | if (requestedSize.isValid()) |
592 | image = image.scaled(s: requestedSize); |
593 | m_texture = QQuickTextureFactory::textureFactoryForImage(image); |
594 | emit finished(); |
595 | } |
596 | |
597 | QQuickTextureFactory *textureFactory() const |
598 | { |
599 | return m_texture; |
600 | } |
601 | |
602 | QQuickTextureFactory *m_texture; |
603 | }; |
604 | |
605 | class InstancAsyncProvider : public QQuickAsyncImageProvider |
606 | { |
607 | public: |
608 | InstancAsyncProvider() |
609 | { |
610 | } |
611 | |
612 | ~InstancAsyncProvider() {} |
613 | |
614 | QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) |
615 | { |
616 | return new InstantAsyncImageResponse(id, requestedSize); |
617 | } |
618 | }; |
619 | |
620 | void tst_qquickimageprovider::instantAsyncTextureTest() |
621 | { |
622 | QQmlEngine engine; |
623 | |
624 | InstancAsyncProvider *provider = new InstancAsyncProvider; |
625 | |
626 | engine.addImageProvider(id: "test_instantasync" , provider); |
627 | QVERIFY(engine.imageProvider("test_instantasync" ) != nullptr); |
628 | |
629 | QString componentStr = "import QtQuick 2.0\nItem { \n" |
630 | "Image { source: \"image://test_instantasync/blue\"; }\n" |
631 | "Image { source: \"image://test_instantasync/red\"; }\n" |
632 | "Image { source: \"image://test_instantasync/green\"; }\n" |
633 | "Image { source: \"image://test_instantasync/yellow\"; }\n" |
634 | " }" ; |
635 | QQmlComponent component(&engine); |
636 | component.setData(componentStr.toLatin1(), baseUrl: QUrl::fromLocalFile(localfile: "" )); |
637 | QScopedPointer<QObject> obj(component.create()); |
638 | |
639 | QVERIFY(!obj.isNull()); |
640 | const QList<QQuickImage *> images = obj->findChildren<QQuickImage *>(); |
641 | QCOMPARE(images.count(), 4); |
642 | |
643 | for (QQuickImage *img: images) { |
644 | QTRY_COMPARE(img->status(), QQuickImage::Ready); |
645 | } |
646 | } |
647 | |
648 | |
649 | class WaitingAsyncImageResponse : public QQuickImageResponse, public QRunnable |
650 | { |
651 | public: |
652 | WaitingAsyncImageResponse(QMutex *providerRemovedMutex, QWaitCondition *providerRemovedCond, bool *providerRemoved, QMutex *imageRequestedMutex, QWaitCondition *imageRequestedCond, bool *imageRequested) |
653 | : m_providerRemovedMutex(providerRemovedMutex), m_providerRemovedCond(providerRemovedCond), m_providerRemoved(providerRemoved), |
654 | m_imageRequestedMutex(imageRequestedMutex), m_imageRequestedCondition(imageRequestedCond), m_imageRequested(imageRequested) |
655 | { |
656 | setAutoDelete(false); |
657 | } |
658 | |
659 | void run() override |
660 | { |
661 | m_imageRequestedMutex->lock(); |
662 | *m_imageRequested = true; |
663 | m_imageRequestedCondition->wakeAll(); |
664 | m_imageRequestedMutex->unlock(); |
665 | m_providerRemovedMutex->lock(); |
666 | while (!*m_providerRemoved) |
667 | m_providerRemovedCond->wait(lockedMutex: m_providerRemovedMutex); |
668 | m_providerRemovedMutex->unlock(); |
669 | emit finished(); |
670 | } |
671 | |
672 | QQuickTextureFactory *textureFactory() const override |
673 | { |
674 | QImage image(50, 50, QImage::Format_RGB32); |
675 | auto texture = QQuickTextureFactory::textureFactoryForImage(image); |
676 | return texture; |
677 | } |
678 | |
679 | QMutex *m_providerRemovedMutex; |
680 | QWaitCondition *m_providerRemovedCond; |
681 | bool *m_providerRemoved; |
682 | QMutex *m_imageRequestedMutex; |
683 | QWaitCondition *m_imageRequestedCondition; |
684 | bool *m_imageRequested; |
685 | |
686 | }; |
687 | |
688 | class WaitingAsyncProvider : public QQuickAsyncImageProvider |
689 | { |
690 | public: |
691 | WaitingAsyncProvider(QMutex *providerRemovedMutex, QWaitCondition *providerRemovedCond, bool *providerRemoved, QMutex *imageRequestedMutex, QWaitCondition *imageRequestedCond, bool *imageRequested) |
692 | : m_providerRemovedMutex(providerRemovedMutex), m_providerRemovedCond(providerRemovedCond), m_providerRemoved(providerRemoved), |
693 | m_imageRequestedMutex(imageRequestedMutex), m_imageRequestedCondition(imageRequestedCond), m_imageRequested(imageRequested) |
694 | { |
695 | } |
696 | |
697 | ~WaitingAsyncProvider() {} |
698 | |
699 | QQuickImageResponse *requestImageResponse(const QString & /* id */, const QSize & /* requestedSize */) |
700 | { |
701 | auto response = new WaitingAsyncImageResponse(m_providerRemovedMutex, m_providerRemovedCond, m_providerRemoved, m_imageRequestedMutex, m_imageRequestedCondition, m_imageRequested); |
702 | pool.start(runnable: response); |
703 | return response; |
704 | } |
705 | |
706 | QMutex *m_providerRemovedMutex; |
707 | QWaitCondition *m_providerRemovedCond; |
708 | bool *m_providerRemoved; |
709 | QMutex *m_imageRequestedMutex; |
710 | QWaitCondition *m_imageRequestedCondition; |
711 | bool *m_imageRequested; |
712 | QThreadPool pool; |
713 | }; |
714 | |
715 | |
716 | // QTBUG-76527 |
717 | void tst_qquickimageprovider::asyncImageThreadSafety() |
718 | { |
719 | QQmlEngine engine; |
720 | QMutex providerRemovedMutex; |
721 | bool providerRemoved = false; |
722 | QWaitCondition providerRemovedCond; |
723 | QMutex imageRequestedMutex; |
724 | bool imageRequested = false; |
725 | QWaitCondition imageRequestedCond; |
726 | auto imageProvider = new WaitingAsyncProvider(&providerRemovedMutex, &providerRemovedCond, &providerRemoved, &imageRequestedMutex, &imageRequestedCond, &imageRequested); |
727 | engine.addImageProvider(id: "test_waiting" , imageProvider); |
728 | QVERIFY(engine.imageProvider("test_waiting" ) != nullptr); |
729 | auto privateEngine = QQmlEnginePrivate::get(e: &engine); |
730 | |
731 | QString componentStr = "import QtQuick 2.0\nItem { \n" |
732 | "Image { source: \"image://test_waiting/blue\"; }\n" |
733 | " }" ; |
734 | QQmlComponent component(&engine); |
735 | component.setData(componentStr.toLatin1(), baseUrl: QUrl::fromLocalFile(localfile: "" )); |
736 | QScopedPointer<QObject> obj(component.create()); |
737 | QVERIFY(!obj.isNull()); |
738 | QWeakPointer<QQmlImageProviderBase> observer = privateEngine->imageProvider(providerId: "test_waiting" ).toWeakRef(); |
739 | QVERIFY(!observer.isNull()); // engine still own the object |
740 | imageRequestedMutex.lock(); |
741 | while (!imageRequested) |
742 | imageRequestedCond.wait(lockedMutex: &imageRequestedMutex); |
743 | imageRequestedMutex.unlock(); |
744 | engine.removeImageProvider(id: "test_waiting" ); |
745 | |
746 | QVERIFY(engine.imageProvider("test_waiting" ) == nullptr); |
747 | QVERIFY(!observer.isNull()); // lifetime has been extended |
748 | |
749 | providerRemovedMutex.lock(); |
750 | providerRemoved = true; |
751 | providerRemovedCond.wakeAll(); |
752 | providerRemovedMutex.unlock(); |
753 | |
754 | QTRY_VERIFY(observer.isNull()); // once the reply has finished, the imageprovider gets deleted |
755 | } |
756 | |
757 | |
758 | QTEST_MAIN(tst_qquickimageprovider) |
759 | |
760 | #include "tst_qquickimageprovider.moc" |
761 | |