mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
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
818 lines
28 KiB
Dart
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);
|
|
});
|
|
}
|