image_cache_test.dart 26.1 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:ui' as ui;

7
import 'package:flutter/foundation.dart';
8
import 'package:flutter/painting.dart';
9
import 'package:flutter/scheduler.dart';
10
import 'package:flutter_test/flutter_test.dart';
11

12
import '../rendering/rendering_tester.dart';
13 14 15
import 'mocks_for_image_cache.dart';

void main() {
16
  TestRenderingFlutterBinding.ensureInitialized();
17

18
  tearDown(() {
19
    imageCache
20 21 22 23
      ..clear()
      ..clearLiveImages()
      ..maximumSize = 1000
      ..maximumSizeBytes = 10485760;
24
  });
25

26
  test('maintains cache size', () async {
27
    imageCache.maximumSize = 3;
28

29
    final TestImageInfo a = await extractOneFrame(TestImageProvider(1, 1, image: await createTestImage()).resolve(ImageConfiguration.empty)) as TestImageInfo;
30
    expect(a.value, equals(1));
31
    final TestImageInfo b = await extractOneFrame(TestImageProvider(1, 2, image: await createTestImage()).resolve(ImageConfiguration.empty)) as TestImageInfo;
32
    expect(b.value, equals(1));
33
    final TestImageInfo c = await extractOneFrame(TestImageProvider(1, 3, image: await createTestImage()).resolve(ImageConfiguration.empty)) as TestImageInfo;
34
    expect(c.value, equals(1));
35
    final TestImageInfo d = await extractOneFrame(TestImageProvider(1, 4, image: await createTestImage()).resolve(ImageConfiguration.empty)) as TestImageInfo;
36
    expect(d.value, equals(1));
37
    final TestImageInfo e = await extractOneFrame(TestImageProvider(1, 5, image: await createTestImage()).resolve(ImageConfiguration.empty)) as TestImageInfo;
38
    expect(e.value, equals(1));
39
    final TestImageInfo f = await extractOneFrame(TestImageProvider(1, 6, image: await createTestImage()).resolve(ImageConfiguration.empty)) as TestImageInfo;
40
    expect(f.value, equals(1));
41

42
    expect(f, equals(a));
43

44
    // cache still only has one entry in it: 1(1)
45

46
    final TestImageInfo g = await extractOneFrame(TestImageProvider(2, 7, image: await createTestImage()).resolve(ImageConfiguration.empty)) as TestImageInfo;
47
    expect(g.value, equals(7));
48

49
    // cache has two entries in it: 1(1), 2(7)
50

51
    final TestImageInfo h = await extractOneFrame(TestImageProvider(1, 8, image: await createTestImage()).resolve(ImageConfiguration.empty)) as TestImageInfo;
52
    expect(h.value, equals(1));
53

54
    // cache still has two entries in it: 2(7), 1(1)
55

56
    final TestImageInfo i = await extractOneFrame(TestImageProvider(3, 9, image: await createTestImage()).resolve(ImageConfiguration.empty)) as TestImageInfo;
57
    expect(i.value, equals(9));
58

59
    // cache has three entries in it: 2(7), 1(1), 3(9)
60

61
    final TestImageInfo j = await extractOneFrame(TestImageProvider(1, 10, image: await createTestImage()).resolve(ImageConfiguration.empty)) as TestImageInfo;
62
    expect(j.value, equals(1));
63

64
    // cache still has three entries in it: 2(7), 3(9), 1(1)
65

66
    final TestImageInfo k = await extractOneFrame(TestImageProvider(4, 11, image: await createTestImage()).resolve(ImageConfiguration.empty)) as TestImageInfo;
67
    expect(k.value, equals(11));
68

69
    // cache has three entries: 3(9), 1(1), 4(11)
70

71
    final TestImageInfo l = await extractOneFrame(TestImageProvider(1, 12, image: await createTestImage()).resolve(ImageConfiguration.empty)) as TestImageInfo;
72
    expect(l.value, equals(1));
73

74
    // cache has three entries: 3(9), 4(11), 1(1)
75

76
    final TestImageInfo m = await extractOneFrame(TestImageProvider(2, 13, image: await createTestImage()).resolve(ImageConfiguration.empty)) as TestImageInfo;
77
    expect(m.value, equals(13));
78

79
    // cache has three entries: 4(11), 1(1), 2(13)
80

81
    final TestImageInfo n = await extractOneFrame(TestImageProvider(3, 14, image: await createTestImage()).resolve(ImageConfiguration.empty)) as TestImageInfo;
82
    expect(n.value, equals(14));
83

84
    // cache has three entries: 1(1), 2(13), 3(14)
85

86
    final TestImageInfo o = await extractOneFrame(TestImageProvider(4, 15, image: await createTestImage()).resolve(ImageConfiguration.empty)) as TestImageInfo;
87
    expect(o.value, equals(15));
88

89
    // cache has three entries: 2(13), 3(14), 4(15)
90

91
    final TestImageInfo p = await extractOneFrame(TestImageProvider(1, 16, image: await createTestImage()).resolve(ImageConfiguration.empty)) as TestImageInfo;
92
    expect(p.value, equals(16));
93

94 95
    // cache has three entries: 3(14), 4(15), 1(16)
  });
96

97
  test('clear removes all images and resets cache size', () async {
98
    final ui.Image testImage = await createTestImage(width: 8, height: 8);
99

100 101
    expect(imageCache.currentSize, 0);
    expect(imageCache.currentSizeBytes, 0);
102

103 104
    await extractOneFrame(TestImageProvider(1, 1, image: testImage).resolve(ImageConfiguration.empty));
    await extractOneFrame(TestImageProvider(2, 2, image: testImage).resolve(ImageConfiguration.empty));
105

106 107
    expect(imageCache.currentSize, 2);
    expect(imageCache.currentSizeBytes, 256 * 2);
108

109
    imageCache.clear();
110

111 112
    expect(imageCache.currentSize, 0);
    expect(imageCache.currentSizeBytes, 0);
113
  });
114

115
  test('evicts individual images', () async {
116 117 118
    final ui.Image testImage = await createTestImage(width: 8, height: 8);
    await extractOneFrame(TestImageProvider(1, 1, image: testImage).resolve(ImageConfiguration.empty));
    await extractOneFrame(TestImageProvider(2, 2, image: testImage).resolve(ImageConfiguration.empty));
119

120 121 122 123 124
    expect(imageCache.currentSize, 2);
    expect(imageCache.currentSizeBytes, 256 * 2);
    expect(imageCache.evict(1), true);
    expect(imageCache.currentSize, 1);
    expect(imageCache.currentSizeBytes, 256);
125
  });
126

127
  test('Do not cache large images', () async {
128
    final ui.Image testImage = await createTestImage(width: 8, height: 8);
129

130
    imageCache.maximumSizeBytes = 1;
131
    await extractOneFrame(TestImageProvider(1, 1, image: testImage).resolve(ImageConfiguration.empty));
132 133 134
    expect(imageCache.currentSize, 0);
    expect(imageCache.currentSizeBytes, 0);
    expect(imageCache.maximumSizeBytes, 1);
135
  });
136

137
  test('Returns null if an error is caught resolving an image', () {
138 139
    Future<ui.Codec> basicDecoder(ui.ImmutableBuffer bytes, {int? cacheWidth, int? cacheHeight, bool? allowUpscaling}) {
      return PaintingBinding.instance.instantiateImageCodecFromBuffer(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight, allowUpscaling: allowUpscaling ?? false);
140
    }
141
    final ErrorImageProvider errorImage = ErrorImageProvider();
142
    expect(() => imageCache.putIfAbsent(errorImage, () => errorImage.loadBuffer(errorImage, basicDecoder)), throwsA(isA<Error>()));
143
    bool caughtError = false;
144
    final ImageStreamCompleter? result = imageCache.putIfAbsent(
145
      errorImage,
146
      () => errorImage.loadBuffer(errorImage, basicDecoder),
147 148 149 150
      onError: (dynamic error, StackTrace? stackTrace) {
       caughtError = true;
      },
    );
151 152 153
    expect(result, null);
    expect(caughtError, true);
  });
154

155
  test('already pending image is returned when it is put into the cache again', () async {
156
    final ui.Image testImage = await createTestImage(width: 8, height: 8);
157

158 159
    final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
    final TestImageStreamCompleter completer2 = TestImageStreamCompleter();
160

161
    final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () {
162
      return completer1;
163
    })! as TestImageStreamCompleter;
164
    final TestImageStreamCompleter resultingCompleter2 = imageCache.putIfAbsent(testImage, () {
165
      return completer2;
166
    })! as TestImageStreamCompleter;
167

168 169 170
    expect(resultingCompleter1, completer1);
    expect(resultingCompleter2, completer1);
  });
171

172
  test('pending image is removed when cache is cleared', () async {
173
    final ui.Image testImage = await createTestImage(width: 8, height: 8);
174

175 176
    final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
    final TestImageStreamCompleter completer2 = TestImageStreamCompleter();
177

178
    final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () {
179
      return completer1;
180
    })! as TestImageStreamCompleter;
181

182 183 184 185
    // Make the image seem live.
    final ImageStreamListener listener = ImageStreamListener((_, __) {});
    completer1.addListener(listener);

186 187 188 189 190 191 192 193
    expect(imageCache.statusForKey(testImage).pending, true);
    expect(imageCache.statusForKey(testImage).live, true);
    imageCache.clear();
    expect(imageCache.statusForKey(testImage).pending, false);
    expect(imageCache.statusForKey(testImage).live, true);
    imageCache.clearLiveImages();
    expect(imageCache.statusForKey(testImage).pending, false);
    expect(imageCache.statusForKey(testImage).live, false);
194

195
    final TestImageStreamCompleter resultingCompleter2 = imageCache.putIfAbsent(testImage, () {
196
      return completer2;
197
    })! as TestImageStreamCompleter;
198

199 200 201
    expect(resultingCompleter1, completer1);
    expect(resultingCompleter2, completer2);
  });
202

203
  test('pending image is removed when image is evicted', () async {
204
    final ui.Image testImage = await createTestImage(width: 8, height: 8);
205

206 207
    final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
    final TestImageStreamCompleter completer2 = TestImageStreamCompleter();
208

209
    final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () {
210
      return completer1;
211
    })! as TestImageStreamCompleter;
212

213
    imageCache.evict(testImage);
214

215
    final TestImageStreamCompleter resultingCompleter2 = imageCache.putIfAbsent(testImage, () {
216
      return completer2;
217
    })! as TestImageStreamCompleter;
218

219 220 221
    expect(resultingCompleter1, completer1);
    expect(resultingCompleter2, completer2);
  });
222

223
  test("failed image can successfully be removed from the cache's pending images", () async {
224
    final ui.Image testImage = await createTestImage(width: 8, height: 8);
225

226
    FailingTestImageProvider(1, 1, image: testImage)
227 228
        .resolve(ImageConfiguration.empty)
        .addListener(ImageStreamListener(
229 230 231
          (ImageInfo image, bool synchronousCall) {
            fail('Image should not complete successfully');
           },
232
          onError: (dynamic exception, StackTrace? stackTrace) {
233
            final bool evictionResult = imageCache.evict(1);
234
            expect(evictionResult, isTrue);
235 236
          },
        ));
237 238
    // yield an event turn so that async work can complete.
    await null;
239
  });
240

241
  test('containsKey - pending', () async {
242
    final ui.Image testImage = await createTestImage(width: 8, height: 8);
243

244
    final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
245

246
    final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () {
247
      return completer1;
248
    })! as TestImageStreamCompleter;
249

250
    expect(resultingCompleter1, completer1);
251
    expect(imageCache.containsKey(testImage), true);
252
  });
253

254
  test('containsKey - completed', () async {
255
    final ui.Image testImage = await createTestImage(width: 8, height: 8);
256

257
    final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
258

259
    final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () {
260
      return completer1;
261
    })! as TestImageStreamCompleter;
262

263 264
    // Mark as complete
    completer1.testSetImage(testImage);
265

266
    expect(resultingCompleter1, completer1);
267
    expect(imageCache.containsKey(testImage), true);
268
  });
Dan Field's avatar
Dan Field committed
269

270
  test('putIfAbsent updates LRU properties of a live image', () async {
271
    imageCache.maximumSize = 1;
272 273
    final ui.Image testImage = await createTestImage(width: 8, height: 8);
    final ui.Image testImage2 = await createTestImage(width: 10, height: 10);
Dan Field's avatar
Dan Field committed
274

275 276
    final TestImageStreamCompleter completer1 = TestImageStreamCompleter()..testSetImage(testImage);
    final TestImageStreamCompleter completer2 = TestImageStreamCompleter()..testSetImage(testImage2);
Dan Field's avatar
Dan Field committed
277

278
    completer1.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {}));
Dan Field's avatar
Dan Field committed
279

280
    final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () {
281
      return completer1;
282
    })! as TestImageStreamCompleter;
Dan Field's avatar
Dan Field committed
283

284 285 286 287 288
    expect(imageCache.statusForKey(testImage).pending, false);
    expect(imageCache.statusForKey(testImage).keepAlive, true);
    expect(imageCache.statusForKey(testImage).live, true);
    expect(imageCache.statusForKey(testImage2).untracked, true);
    final TestImageStreamCompleter resultingCompleter2 = imageCache.putIfAbsent(testImage2, () {
289
      return completer2;
290
    })! as TestImageStreamCompleter;
Dan Field's avatar
Dan Field committed
291 292


293 294 295 296 297 298
    expect(imageCache.statusForKey(testImage).pending, false);
    expect(imageCache.statusForKey(testImage).keepAlive, false); // evicted
    expect(imageCache.statusForKey(testImage).live, true);
    expect(imageCache.statusForKey(testImage2).pending, false);
    expect(imageCache.statusForKey(testImage2).keepAlive, true); // took the LRU spot.
    expect(imageCache.statusForKey(testImage2).live, false); // no listeners
Dan Field's avatar
Dan Field committed
299

300 301 302
    expect(resultingCompleter1, completer1);
    expect(resultingCompleter2, completer2);
  });
Dan Field's avatar
Dan Field committed
303

304
  test('Live image cache avoids leaks of unlistened streams', () async {
305
    imageCache.maximumSize = 3;
Dan Field's avatar
Dan Field committed
306

307 308 309 310 311 312
    TestImageProvider(1, 1, image: await createTestImage()).resolve(ImageConfiguration.empty);
    TestImageProvider(2, 2, image: await createTestImage()).resolve(ImageConfiguration.empty);
    TestImageProvider(3, 3, image: await createTestImage()).resolve(ImageConfiguration.empty);
    TestImageProvider(4, 4, image: await createTestImage()).resolve(ImageConfiguration.empty);
    TestImageProvider(5, 5, image: await createTestImage()).resolve(ImageConfiguration.empty);
    TestImageProvider(6, 6, image: await createTestImage()).resolve(ImageConfiguration.empty);
Dan Field's avatar
Dan Field committed
313

314 315
    // wait an event loop to let image resolution process.
    await null;
Dan Field's avatar
Dan Field committed
316

317 318
    expect(imageCache.currentSize, 3);
    expect(imageCache.liveImageCount, 0);
319
  });
Dan Field's avatar
Dan Field committed
320

321
  test('Disabled image cache does not leak live images', () async {
322
    imageCache.maximumSize = 0;
Dan Field's avatar
Dan Field committed
323

324 325 326 327 328 329
    TestImageProvider(1, 1, image: await createTestImage()).resolve(ImageConfiguration.empty);
    TestImageProvider(2, 2, image: await createTestImage()).resolve(ImageConfiguration.empty);
    TestImageProvider(3, 3, image: await createTestImage()).resolve(ImageConfiguration.empty);
    TestImageProvider(4, 4, image: await createTestImage()).resolve(ImageConfiguration.empty);
    TestImageProvider(5, 5, image: await createTestImage()).resolve(ImageConfiguration.empty);
    TestImageProvider(6, 6, image: await createTestImage()).resolve(ImageConfiguration.empty);
Dan Field's avatar
Dan Field committed
330

331 332
    // wait an event loop to let image resolution process.
    await null;
Dan Field's avatar
Dan Field committed
333

334 335
    expect(imageCache.currentSize, 0);
    expect(imageCache.liveImageCount, 0);
336
  });
Dan Field's avatar
Dan Field committed
337

338 339 340 341 342 343 344 345 346 347 348 349 350 351 352
  test('Clearing image cache does not leak live images', () async {
    imageCache.maximumSize = 1;

    final ui.Image testImage1 = await createTestImage(width: 8, height: 8);
    final ui.Image testImage2 = await createTestImage(width: 10, height: 10);

    final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
    final TestImageStreamCompleter completer2 = TestImageStreamCompleter()..testSetImage(testImage2);

    imageCache.putIfAbsent(testImage1, () => completer1);
    expect(imageCache.statusForKey(testImage1).pending, true);
    expect(imageCache.statusForKey(testImage1).live, true);

    imageCache.clear();
    expect(imageCache.statusForKey(testImage1).pending, false);
353
    expect(imageCache.statusForKey(testImage1).live, false);
354 355

    completer1.testSetImage(testImage1);
356
    expect(imageCache.statusForKey(testImage1).keepAlive, false);
357 358 359 360 361 362 363 364
    expect(imageCache.statusForKey(testImage1).live, false);

    imageCache.putIfAbsent(testImage2, () => completer2);
    expect(imageCache.statusForKey(testImage1).tracked, false); // evicted
    expect(imageCache.statusForKey(testImage2).tracked, true);
  });


365
  test('Evicting a pending image clears the live image by default', () async {
366
    final ui.Image testImage = await createTestImage(width: 8, height: 8);
Dan Field's avatar
Dan Field committed
367

368
    final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
Dan Field's avatar
Dan Field committed
369

370 371 372 373
    imageCache.putIfAbsent(testImage, () => completer1);
    expect(imageCache.statusForKey(testImage).pending, true);
    expect(imageCache.statusForKey(testImage).live, true);
    expect(imageCache.statusForKey(testImage).keepAlive, false);
Dan Field's avatar
Dan Field committed
374

375 376
    imageCache.evict(testImage);
    expect(imageCache.statusForKey(testImage).untracked, true);
377
  });
Dan Field's avatar
Dan Field committed
378

379
  test('Evicting a pending image does clear the live image when includeLive is false and only cache listening', () async {
380
    final ui.Image testImage = await createTestImage(width: 8, height: 8);
Dan Field's avatar
Dan Field committed
381

382
    final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
Dan Field's avatar
Dan Field committed
383

384 385 386 387
    imageCache.putIfAbsent(testImage, () => completer1);
    expect(imageCache.statusForKey(testImage).pending, true);
    expect(imageCache.statusForKey(testImage).live, true);
    expect(imageCache.statusForKey(testImage).keepAlive, false);
Dan Field's avatar
Dan Field committed
388

389 390 391 392
    imageCache.evict(testImage, includeLive: false);
    expect(imageCache.statusForKey(testImage).pending, false);
    expect(imageCache.statusForKey(testImage).live, false);
    expect(imageCache.statusForKey(testImage).keepAlive, false);
393
  });
Dan Field's avatar
Dan Field committed
394

395
  test('Evicting a pending image does clear the live image when includeLive is false and some other listener', () async {
396
    final ui.Image testImage = await createTestImage(width: 8, height: 8);
Dan Field's avatar
Dan Field committed
397

398
    final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
Dan Field's avatar
Dan Field committed
399

400 401 402 403
    imageCache.putIfAbsent(testImage, () => completer1);
    expect(imageCache.statusForKey(testImage).pending, true);
    expect(imageCache.statusForKey(testImage).live, true);
    expect(imageCache.statusForKey(testImage).keepAlive, false);
Dan Field's avatar
Dan Field committed
404

405
    completer1.addListener(ImageStreamListener((_, __) {}));
406 407 408 409
    imageCache.evict(testImage, includeLive: false);
    expect(imageCache.statusForKey(testImage).pending, false);
    expect(imageCache.statusForKey(testImage).live, true);
    expect(imageCache.statusForKey(testImage).keepAlive, false);
410
  });
Dan Field's avatar
Dan Field committed
411

412
  test('Evicting a completed image does clear the live image by default', () async {
413
    final ui.Image testImage = await createTestImage(width: 8, height: 8);
Dan Field's avatar
Dan Field committed
414

415 416 417
    final TestImageStreamCompleter completer1 = TestImageStreamCompleter()
      ..testSetImage(testImage)
      ..addListener(ImageStreamListener((ImageInfo info, bool syncCall) {}));
Dan Field's avatar
Dan Field committed
418

419 420 421 422
    imageCache.putIfAbsent(testImage, () => completer1);
    expect(imageCache.statusForKey(testImage).pending, false);
    expect(imageCache.statusForKey(testImage).live, true);
    expect(imageCache.statusForKey(testImage).keepAlive, true);
Dan Field's avatar
Dan Field committed
423

424 425
    imageCache.evict(testImage);
    expect(imageCache.statusForKey(testImage).untracked, true);
426
  });
Dan Field's avatar
Dan Field committed
427

428
  test('Evicting a completed image does not clear the live image when includeLive is set to false', () async {
429
    final ui.Image testImage = await createTestImage(width: 8, height: 8);
Dan Field's avatar
Dan Field committed
430

431 432 433
    final TestImageStreamCompleter completer1 = TestImageStreamCompleter()
      ..testSetImage(testImage)
      ..addListener(ImageStreamListener((ImageInfo info, bool syncCall) {}));
Dan Field's avatar
Dan Field committed
434

435 436 437 438
    imageCache.putIfAbsent(testImage, () => completer1);
    expect(imageCache.statusForKey(testImage).pending, false);
    expect(imageCache.statusForKey(testImage).live, true);
    expect(imageCache.statusForKey(testImage).keepAlive, true);
439

440 441 442 443
    imageCache.evict(testImage, includeLive: false);
    expect(imageCache.statusForKey(testImage).pending, false);
    expect(imageCache.statusForKey(testImage).live, true);
    expect(imageCache.statusForKey(testImage).keepAlive, false);
444
  });
445

446
  test('Clearing liveImages removes callbacks', () async {
447
    final ui.Image testImage = await createTestImage(width: 8, height: 8);
448

449
    final ImageStreamListener listener = ImageStreamListener((ImageInfo info, bool syncCall) {});
450

451 452 453
    final TestImageStreamCompleter completer1 = TestImageStreamCompleter()
      ..testSetImage(testImage)
      ..addListener(listener);
454

455 456 457
    final TestImageStreamCompleter completer2 = TestImageStreamCompleter()
      ..testSetImage(testImage)
      ..addListener(listener);
458

459 460 461 462
    imageCache.putIfAbsent(testImage, () => completer1);
    expect(imageCache.statusForKey(testImage).pending, false);
    expect(imageCache.statusForKey(testImage).live, true);
    expect(imageCache.statusForKey(testImage).keepAlive, true);
463

464 465 466 467 468
    imageCache.clear();
    imageCache.clearLiveImages();
    expect(imageCache.statusForKey(testImage).pending, false);
    expect(imageCache.statusForKey(testImage).live, false);
    expect(imageCache.statusForKey(testImage).keepAlive, false);
469

470 471 472 473
    imageCache.putIfAbsent(testImage, () => completer2);
    expect(imageCache.statusForKey(testImage).pending, false);
    expect(imageCache.statusForKey(testImage).live, true);
    expect(imageCache.statusForKey(testImage).keepAlive, true);
474

475
    completer1.removeListener(listener);
476

477 478 479
    expect(imageCache.statusForKey(testImage).pending, false);
    expect(imageCache.statusForKey(testImage).live, true);
    expect(imageCache.statusForKey(testImage).keepAlive, true);
480
  });
481

482 483 484 485 486 487 488
  test('Live image gets size updated', () async {
    // Add an image to the cache in pending state
    // Complete it once it is in there as live
    // Evict it but leave the live one.
    // Add it again.
    // If the live image did not track the size properly, the last line of
    // this test will fail.
489

490
    final ui.Image testImage = await createTestImage(width: 8, height: 8);
491
    const int testImageSize = 8 * 8 * 4;
492

493
    final ImageStreamListener listener = ImageStreamListener((ImageInfo info, bool syncCall) {});
494

495 496
    final TestImageStreamCompleter completer1 = TestImageStreamCompleter()
      ..addListener(listener);
497

498 499 500 501 502
    imageCache.putIfAbsent(testImage, () => completer1);
    expect(imageCache.statusForKey(testImage).pending, true);
    expect(imageCache.statusForKey(testImage).live, true);
    expect(imageCache.statusForKey(testImage).keepAlive, false);
    expect(imageCache.currentSizeBytes, 0);
503

504
    completer1.testSetImage(testImage);
505

506 507 508 509
    expect(imageCache.statusForKey(testImage).pending, false);
    expect(imageCache.statusForKey(testImage).live, true);
    expect(imageCache.statusForKey(testImage).keepAlive, true);
    expect(imageCache.currentSizeBytes, testImageSize);
510

511
    imageCache.evict(testImage, includeLive: false);
512

513 514 515 516
    expect(imageCache.statusForKey(testImage).pending, false);
    expect(imageCache.statusForKey(testImage).live, true);
    expect(imageCache.statusForKey(testImage).keepAlive, false);
    expect(imageCache.currentSizeBytes, 0);
517

518
    imageCache.putIfAbsent(testImage, () => completer1);
519

520 521 522 523
    expect(imageCache.statusForKey(testImage).pending, false);
    expect(imageCache.statusForKey(testImage).live, true);
    expect(imageCache.statusForKey(testImage).keepAlive, true);
    expect(imageCache.currentSizeBytes, testImageSize);
524
  });
525 526 527 528

  test('Image is obtained and disposed of properly for cache', () async {
    const int key = 1;
    final ui.Image testImage = await createTestImage(width: 8, height: 8, cache: false);
529
    expect(testImage.debugGetOpenHandleStackTraces()!.length, 1);
530

531
    late ImageInfo imageInfo;
532 533 534 535 536 537 538
    final ImageStreamListener listener = ImageStreamListener((ImageInfo info, bool syncCall) {
      imageInfo = info;
    });

    final TestImageStreamCompleter completer = TestImageStreamCompleter();

    completer.addListener(listener);
539
    imageCache.putIfAbsent(key, () => completer);
540

541
    expect(testImage.debugGetOpenHandleStackTraces()!.length, 1);
542 543 544 545 546

    // This should cause keepAlive to be set to true.
    completer.testSetImage(testImage);
    expect(imageInfo, isNotNull);
    // +1 ImageStreamCompleter
547
    expect(testImage.debugGetOpenHandleStackTraces()!.length, 2);
548 549 550 551

    completer.removeListener(listener);

    // Force us to the end of the frame.
552 553
    SchedulerBinding.instance.scheduleFrame();
    await SchedulerBinding.instance.endOfFrame;
554

555
    expect(testImage.debugGetOpenHandleStackTraces()!.length, 2);
556

557
    expect(imageCache.evict(key), true);
558 559

    // Force us to the end of the frame.
560 561
    SchedulerBinding.instance.scheduleFrame();
    await SchedulerBinding.instance.endOfFrame;
562 563 564

    // -1 _CachedImage
    // -1 ImageStreamCompleter
565
    expect(testImage.debugGetOpenHandleStackTraces()!.length, 1);
566 567

    imageInfo.dispose();
568
    expect(testImage.debugGetOpenHandleStackTraces()!.length, 0);
569
  }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/87442
570 571 572 573

  test('Image is obtained and disposed of properly for cache when listener is still active', () async {
    const int key = 1;
    final ui.Image testImage = await createTestImage(width: 8, height: 8, cache: false);
574
    expect(testImage.debugGetOpenHandleStackTraces()!.length, 1);
575

576
    late ImageInfo imageInfo;
577 578 579 580 581 582 583
    final ImageStreamListener listener = ImageStreamListener((ImageInfo info, bool syncCall) {
      imageInfo = info;
    });

    final TestImageStreamCompleter completer = TestImageStreamCompleter();

    completer.addListener(listener);
584
    imageCache.putIfAbsent(key, () => completer);
585

586
    expect(testImage.debugGetOpenHandleStackTraces()!.length, 1);
587 588 589 590 591

    // This should cause keepAlive to be set to true.
    completer.testSetImage(testImage);
    expect(imageInfo, isNotNull);
    // Just our imageInfo and the completer.
592
    expect(testImage.debugGetOpenHandleStackTraces()!.length, 2);
593

594
    expect(imageCache.evict(key), true);
595 596

    // Force us to the end of the frame.
597 598
    SchedulerBinding.instance.scheduleFrame();
    await SchedulerBinding.instance.endOfFrame;
599 600 601

    // Live image still around since there's still a listener, and the listener
    // should be holding a handle.
602
    expect(testImage.debugGetOpenHandleStackTraces()!.length, 2);
603 604
    completer.removeListener(listener);

605
    expect(testImage.debugGetOpenHandleStackTraces()!.length, 1);
606
    imageInfo.dispose();
607
    expect(testImage.debugGetOpenHandleStackTraces()!.length, 0);
608
  }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/87442
609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631

  test('clear does not leave pending images stuck', () async {
    final ui.Image testImage = await createTestImage(width: 8, height: 8);

    final TestImageStreamCompleter completer1 = TestImageStreamCompleter();

    imageCache.putIfAbsent(testImage, () {
      return completer1;
    });

    expect(imageCache.statusForKey(testImage).pending, true);
    expect(imageCache.statusForKey(testImage).live, true);
    expect(imageCache.statusForKey(testImage).keepAlive, false);

    imageCache.clear();

    // No one else is listening to the completer. It should not be considered
    // live anymore.

    expect(imageCache.statusForKey(testImage).pending, false);
    expect(imageCache.statusForKey(testImage).live, false);
    expect(imageCache.statusForKey(testImage).keepAlive, false);
  });
632
}