| 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 |  |