flutter_flutter/packages/flutter/test/painting/image_cache_test.dart
Kate Lovett 9d96df2364
Modernize framework lints (#179089)
WIP

Commits separated as follows:
- Update lints in analysis_options files
- Run `dart fix --apply`
- Clean up leftover analysis issues 
- Run `dart format .` in the right places.

Local analysis and testing passes. Checking CI now.

Part of https://github.com/flutter/flutter/issues/178827
- Adoption of flutter_lints in examples/api coming in a separate change
(cc @loic-sharma)

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-11-26 01:10:39 +00:00

818 lines
28 KiB
Dart

// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/rendering_tester.dart';
import 'mocks_for_image_cache.dart';
void main() {
TestRenderingFlutterBinding.ensureInitialized();
tearDown(() {
imageCache
..clear()
..clearLiveImages()
..maximumSize = 1000
..maximumSizeBytes = 10485760;
});
test('maintains cache size', () async {
imageCache.maximumSize = 3;
final a =
await extractOneFrame(
TestImageProvider(
1,
1,
image: await createTestImage(),
).resolve(ImageConfiguration.empty),
)
as TestImageInfo;
expect(a.value, equals(1));
final b =
await extractOneFrame(
TestImageProvider(
1,
2,
image: await createTestImage(),
).resolve(ImageConfiguration.empty),
)
as TestImageInfo;
expect(b.value, equals(1));
final c =
await extractOneFrame(
TestImageProvider(
1,
3,
image: await createTestImage(),
).resolve(ImageConfiguration.empty),
)
as TestImageInfo;
expect(c.value, equals(1));
final d =
await extractOneFrame(
TestImageProvider(
1,
4,
image: await createTestImage(),
).resolve(ImageConfiguration.empty),
)
as TestImageInfo;
expect(d.value, equals(1));
final e =
await extractOneFrame(
TestImageProvider(
1,
5,
image: await createTestImage(),
).resolve(ImageConfiguration.empty),
)
as TestImageInfo;
expect(e.value, equals(1));
final f =
await extractOneFrame(
TestImageProvider(
1,
6,
image: await createTestImage(),
).resolve(ImageConfiguration.empty),
)
as TestImageInfo;
expect(f.value, equals(1));
expect(f, equals(a));
// cache still only has one entry in it: 1(1)
final g =
await extractOneFrame(
TestImageProvider(
2,
7,
image: await createTestImage(),
).resolve(ImageConfiguration.empty),
)
as TestImageInfo;
expect(g.value, equals(7));
// cache has two entries in it: 1(1), 2(7)
final h =
await extractOneFrame(
TestImageProvider(
1,
8,
image: await createTestImage(),
).resolve(ImageConfiguration.empty),
)
as TestImageInfo;
expect(h.value, equals(1));
// cache still has two entries in it: 2(7), 1(1)
final i =
await extractOneFrame(
TestImageProvider(
3,
9,
image: await createTestImage(),
).resolve(ImageConfiguration.empty),
)
as TestImageInfo;
expect(i.value, equals(9));
// cache has three entries in it: 2(7), 1(1), 3(9)
final j =
await extractOneFrame(
TestImageProvider(
1,
10,
image: await createTestImage(),
).resolve(ImageConfiguration.empty),
)
as TestImageInfo;
expect(j.value, equals(1));
// cache still has three entries in it: 2(7), 3(9), 1(1)
final k =
await extractOneFrame(
TestImageProvider(
4,
11,
image: await createTestImage(),
).resolve(ImageConfiguration.empty),
)
as TestImageInfo;
expect(k.value, equals(11));
// cache has three entries: 3(9), 1(1), 4(11)
final l =
await extractOneFrame(
TestImageProvider(
1,
12,
image: await createTestImage(),
).resolve(ImageConfiguration.empty),
)
as TestImageInfo;
expect(l.value, equals(1));
// cache has three entries: 3(9), 4(11), 1(1)
final m =
await extractOneFrame(
TestImageProvider(
2,
13,
image: await createTestImage(),
).resolve(ImageConfiguration.empty),
)
as TestImageInfo;
expect(m.value, equals(13));
// cache has three entries: 4(11), 1(1), 2(13)
final n =
await extractOneFrame(
TestImageProvider(
3,
14,
image: await createTestImage(),
).resolve(ImageConfiguration.empty),
)
as TestImageInfo;
expect(n.value, equals(14));
// cache has three entries: 1(1), 2(13), 3(14)
final o =
await extractOneFrame(
TestImageProvider(
4,
15,
image: await createTestImage(),
).resolve(ImageConfiguration.empty),
)
as TestImageInfo;
expect(o.value, equals(15));
// cache has three entries: 2(13), 3(14), 4(15)
final p =
await extractOneFrame(
TestImageProvider(
1,
16,
image: await createTestImage(),
).resolve(ImageConfiguration.empty),
)
as TestImageInfo;
expect(p.value, equals(16));
// cache has three entries: 3(14), 4(15), 1(16)
});
test('clear removes all images and resets cache size', () async {
final ui.Image testImage = await createTestImage(width: 8, height: 8);
expect(imageCache.currentSize, 0);
expect(imageCache.currentSizeBytes, 0);
await extractOneFrame(
TestImageProvider(1, 1, image: testImage).resolve(ImageConfiguration.empty),
);
await extractOneFrame(
TestImageProvider(2, 2, image: testImage).resolve(ImageConfiguration.empty),
);
expect(imageCache.currentSize, 2);
expect(imageCache.currentSizeBytes, 256 * 2);
imageCache.clear();
expect(imageCache.currentSize, 0);
expect(imageCache.currentSizeBytes, 0);
});
test('evicts individual images', () async {
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),
);
expect(imageCache.currentSize, 2);
expect(imageCache.currentSizeBytes, 256 * 2);
expect(imageCache.evict(1), true);
expect(imageCache.currentSize, 1);
expect(imageCache.currentSizeBytes, 256);
});
test('Do not cache large images', () async {
final ui.Image testImage = await createTestImage(width: 8, height: 8);
imageCache.maximumSizeBytes = 1;
await extractOneFrame(
TestImageProvider(1, 1, image: testImage).resolve(ImageConfiguration.empty),
);
expect(imageCache.currentSize, 0);
expect(imageCache.currentSizeBytes, 0);
expect(imageCache.maximumSizeBytes, 1);
});
test('Returns null if an error is caught resolving an image', () {
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,
);
}
final errorImage = ErrorImageProvider();
expect(
() =>
imageCache.putIfAbsent(errorImage, () => errorImage.loadBuffer(errorImage, basicDecoder)),
throwsA(isA<Error>()),
);
var caughtError = false;
final ImageStreamCompleter? result = imageCache.putIfAbsent(
errorImage,
() => errorImage.loadBuffer(errorImage, basicDecoder),
onError: (dynamic error, StackTrace? stackTrace) {
caughtError = true;
},
);
expect(result, null);
expect(caughtError, true);
});
test('already pending image is returned when it is put into the cache again', () async {
final ui.Image testImage = await createTestImage(width: 8, height: 8);
final completer1 = TestImageStreamCompleter();
final completer2 = TestImageStreamCompleter();
final resultingCompleter1 =
imageCache.putIfAbsent(testImage, () {
return completer1;
})!
as TestImageStreamCompleter;
final resultingCompleter2 =
imageCache.putIfAbsent(testImage, () {
return completer2;
})!
as TestImageStreamCompleter;
expect(resultingCompleter1, completer1);
expect(resultingCompleter2, completer1);
});
test('pending image is removed when cache is cleared', () async {
final ui.Image testImage = await createTestImage(width: 8, height: 8);
final completer1 = TestImageStreamCompleter();
final completer2 = TestImageStreamCompleter();
final resultingCompleter1 =
imageCache.putIfAbsent(testImage, () {
return completer1;
})!
as TestImageStreamCompleter;
// Make the image seem live.
final listener = ImageStreamListener((_, _) {});
completer1.addListener(listener);
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);
final resultingCompleter2 =
imageCache.putIfAbsent(testImage, () {
return completer2;
})!
as TestImageStreamCompleter;
expect(resultingCompleter1, completer1);
expect(resultingCompleter2, completer2);
});
test('pending image is removed when image is evicted', () async {
final ui.Image testImage = await createTestImage(width: 8, height: 8);
final completer1 = TestImageStreamCompleter();
final completer2 = TestImageStreamCompleter();
final resultingCompleter1 =
imageCache.putIfAbsent(testImage, () {
return completer1;
})!
as TestImageStreamCompleter;
imageCache.evict(testImage);
final resultingCompleter2 =
imageCache.putIfAbsent(testImage, () {
return completer2;
})!
as TestImageStreamCompleter;
expect(resultingCompleter1, completer1);
expect(resultingCompleter2, completer2);
});
test("failed image can successfully be removed from the cache's pending images", () async {
final ui.Image testImage = await createTestImage(width: 8, height: 8);
FailingTestImageProvider(1, 1, image: testImage)
.resolve(ImageConfiguration.empty)
.addListener(
ImageStreamListener(
(ImageInfo image, bool synchronousCall) {
fail('Image should not complete successfully');
},
onError: (dynamic exception, StackTrace? stackTrace) {
final bool evictionResult = imageCache.evict(1);
expect(evictionResult, isTrue);
},
),
);
// yield an event turn so that async work can complete.
await null;
});
test('containsKey - pending', () async {
final ui.Image testImage = await createTestImage(width: 8, height: 8);
final completer1 = TestImageStreamCompleter();
final resultingCompleter1 =
imageCache.putIfAbsent(testImage, () {
return completer1;
})!
as TestImageStreamCompleter;
expect(resultingCompleter1, completer1);
expect(imageCache.containsKey(testImage), true);
});
test('containsKey - completed', () async {
final ui.Image testImage = await createTestImage(width: 8, height: 8);
final completer1 = TestImageStreamCompleter();
final resultingCompleter1 =
imageCache.putIfAbsent(testImage, () {
return completer1;
})!
as TestImageStreamCompleter;
// Mark as complete
completer1.testSetImage(testImage);
expect(resultingCompleter1, completer1);
expect(imageCache.containsKey(testImage), true);
});
test('putIfAbsent updates LRU properties of a live image', () async {
imageCache.maximumSize = 1;
final ui.Image testImage = await createTestImage(width: 8, height: 8);
final ui.Image testImage2 = await createTestImage(width: 10, height: 10);
final completer1 = TestImageStreamCompleter()..testSetImage(testImage);
final completer2 = TestImageStreamCompleter()..testSetImage(testImage2);
completer1.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {}));
final resultingCompleter1 =
imageCache.putIfAbsent(testImage, () {
return completer1;
})!
as TestImageStreamCompleter;
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 resultingCompleter2 =
imageCache.putIfAbsent(testImage2, () {
return completer2;
})!
as TestImageStreamCompleter;
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
expect(resultingCompleter1, completer1);
expect(resultingCompleter2, completer2);
});
test('Live image cache avoids leaks of unlistened streams', () async {
imageCache.maximumSize = 3;
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);
// wait an event loop to let image resolution process.
await null;
expect(imageCache.currentSize, 3);
expect(imageCache.liveImageCount, 0);
});
test('Disabled image cache does not leak live images', () async {
imageCache.maximumSize = 0;
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);
// wait an event loop to let image resolution process.
await null;
expect(imageCache.currentSize, 0);
expect(imageCache.liveImageCount, 0);
});
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 completer1 = TestImageStreamCompleter();
final 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);
expect(imageCache.statusForKey(testImage1).live, false);
completer1.testSetImage(testImage1);
expect(imageCache.statusForKey(testImage1).keepAlive, false);
expect(imageCache.statusForKey(testImage1).live, false);
imageCache.putIfAbsent(testImage2, () => completer2);
expect(imageCache.statusForKey(testImage1).tracked, false); // evicted
expect(imageCache.statusForKey(testImage2).tracked, true);
});
test('Evicting a pending image clears the live image by default', () async {
final ui.Image testImage = await createTestImage(width: 8, height: 8);
final completer1 = TestImageStreamCompleter();
imageCache.putIfAbsent(testImage, () => completer1);
expect(imageCache.statusForKey(testImage).pending, true);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, false);
imageCache.evict(testImage);
expect(imageCache.statusForKey(testImage).untracked, true);
});
test(
'Evicting a pending image does clear the live image when includeLive is false and only cache listening',
() async {
final ui.Image testImage = await createTestImage(width: 8, height: 8);
final completer1 = TestImageStreamCompleter();
imageCache.putIfAbsent(testImage, () => completer1);
expect(imageCache.statusForKey(testImage).pending, true);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, false);
imageCache.evict(testImage, includeLive: false);
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, false);
expect(imageCache.statusForKey(testImage).keepAlive, false);
},
);
test(
'Evicting a pending image does clear the live image when includeLive is false and some other listener',
() async {
final ui.Image testImage = await createTestImage(width: 8, height: 8);
final completer1 = TestImageStreamCompleter();
imageCache.putIfAbsent(testImage, () => completer1);
expect(imageCache.statusForKey(testImage).pending, true);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, false);
completer1.addListener(ImageStreamListener((_, _) {}));
imageCache.evict(testImage, includeLive: false);
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, false);
},
);
test('Evicting a completed image does clear the live image by default', () async {
final ui.Image testImage = await createTestImage(width: 8, height: 8);
final completer1 = TestImageStreamCompleter()
..testSetImage(testImage)
..addListener(ImageStreamListener((ImageInfo info, bool syncCall) {}));
imageCache.putIfAbsent(testImage, () => completer1);
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, true);
imageCache.evict(testImage);
expect(imageCache.statusForKey(testImage).untracked, true);
});
test(
'Evicting a completed image does not clear the live image when includeLive is set to false',
() async {
final ui.Image testImage = await createTestImage(width: 8, height: 8);
final completer1 = TestImageStreamCompleter()
..testSetImage(testImage)
..addListener(ImageStreamListener((ImageInfo info, bool syncCall) {}));
imageCache.putIfAbsent(testImage, () => completer1);
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, true);
imageCache.evict(testImage, includeLive: false);
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, false);
},
);
test('Clearing liveImages removes callbacks', () async {
final ui.Image testImage = await createTestImage(width: 8, height: 8);
final listener = ImageStreamListener((ImageInfo info, bool syncCall) {});
final completer1 = TestImageStreamCompleter()
..testSetImage(testImage)
..addListener(listener);
final completer2 = TestImageStreamCompleter()
..testSetImage(testImage)
..addListener(listener);
imageCache.putIfAbsent(testImage, () => completer1);
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, true);
imageCache.clear();
imageCache.clearLiveImages();
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, false);
expect(imageCache.statusForKey(testImage).keepAlive, false);
imageCache.putIfAbsent(testImage, () => completer2);
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, true);
completer1.removeListener(listener);
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, true);
});
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.
final ui.Image testImage = await createTestImage(width: 8, height: 8);
const int testImageSize = 8 * 8 * 4;
final listener = ImageStreamListener((ImageInfo info, bool syncCall) {});
final completer1 = TestImageStreamCompleter()..addListener(listener);
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);
completer1.testSetImage(testImage);
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, true);
expect(imageCache.currentSizeBytes, testImageSize);
imageCache.evict(testImage, includeLive: false);
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, false);
expect(imageCache.currentSizeBytes, 0);
imageCache.putIfAbsent(testImage, () => completer1);
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, true);
expect(imageCache.currentSizeBytes, testImageSize);
});
test('Image is obtained and disposed of properly for cache', () async {
const key = 1;
final ui.Image testImage = await createTestImage(width: 8, height: 8, cache: false);
expect(testImage.debugGetOpenHandleStackTraces()!.length, 1);
late ImageInfo imageInfo;
final listener = ImageStreamListener((ImageInfo info, bool syncCall) {
imageInfo = info;
});
final completer = TestImageStreamCompleter();
completer.addListener(listener);
imageCache.putIfAbsent(key, () => completer);
expect(testImage.debugGetOpenHandleStackTraces()!.length, 1);
// This should cause keepAlive to be set to true.
completer.testSetImage(testImage);
expect(imageInfo, isNotNull);
// +1 ImageStreamCompleter
expect(testImage.debugGetOpenHandleStackTraces()!.length, 2);
completer.removeListener(listener);
// Force us to the end of the frame.
SchedulerBinding.instance.scheduleFrame();
await SchedulerBinding.instance.endOfFrame;
expect(testImage.debugGetOpenHandleStackTraces()!.length, 2);
expect(imageCache.evict(key), true);
// Force us to the end of the frame.
SchedulerBinding.instance.scheduleFrame();
await SchedulerBinding.instance.endOfFrame;
// -1 _CachedImage
// -1 ImageStreamCompleter
expect(testImage.debugGetOpenHandleStackTraces()!.length, 1);
imageInfo.dispose();
expect(testImage.debugGetOpenHandleStackTraces()!.length, 0);
}, skip: kIsWeb); // https://github.com/flutter/flutter/issues/87442
test(
'Image is obtained and disposed of properly for cache when listener is still active',
() async {
const key = 1;
final ui.Image testImage = await createTestImage(width: 8, height: 8, cache: false);
expect(testImage.debugGetOpenHandleStackTraces()!.length, 1);
late ImageInfo imageInfo;
final listener = ImageStreamListener((ImageInfo info, bool syncCall) {
imageInfo = info;
});
final completer = TestImageStreamCompleter();
completer.addListener(listener);
imageCache.putIfAbsent(key, () => completer);
expect(testImage.debugGetOpenHandleStackTraces()!.length, 1);
// This should cause keepAlive to be set to true.
completer.testSetImage(testImage);
expect(imageInfo, isNotNull);
// Just our imageInfo and the completer.
expect(testImage.debugGetOpenHandleStackTraces()!.length, 2);
expect(imageCache.evict(key), true);
// Force us to the end of the frame.
SchedulerBinding.instance.scheduleFrame();
await SchedulerBinding.instance.endOfFrame;
// Live image still around since there's still a listener, and the listener
// should be holding a handle.
expect(testImage.debugGetOpenHandleStackTraces()!.length, 2);
completer.removeListener(listener);
expect(testImage.debugGetOpenHandleStackTraces()!.length, 1);
imageInfo.dispose();
expect(testImage.debugGetOpenHandleStackTraces()!.length, 0);
},
skip: kIsWeb, // https://github.com/flutter/flutter/issues/87442
);
test('clear does not leave pending images stuck', () async {
final ui.Image testImage = await createTestImage(width: 8, height: 8);
final 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);
});
}