Fix silent test failure in image cache tests (#56492)

This commit is contained in:
Ian Hickson 2020-05-07 18:19:03 -07:00 committed by GitHub
parent 0a4f6cdef0
commit 3cb79079bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 1062 additions and 967 deletions

View file

@ -286,10 +286,9 @@ class ImageCache {
}
}
void _trackLiveImage(Object key, _LiveImage image, { bool debugPutOk = true }) {
void _trackLiveImage(Object key, _LiveImage image) {
// Avoid adding unnecessary callbacks to the completer.
_liveImages.putIfAbsent(key, () {
assert(debugPutOk);
// Even if no callers to ImageProvider.resolve have listened to the stream,
// the cache is listening to the stream and will remove itself once the
// image completes to move it from pending to keepAlive.
@ -400,10 +399,6 @@ class ImageCache {
imageSize,
() => _liveImages.remove(key),
),
// This should result in a put if `loader()` above executed
// synchronously, in which case syncCall is true and we arrived here
// before we got a chance to track the image otherwise.
debugPutOk: syncCall,
);
final _PendingImage pendingImage = untrackedPendingImage ?? _pendingImages.remove(key);

View file

@ -347,10 +347,10 @@ abstract class ImageProvider<T> {
stack: stack,
context: ErrorDescription('while resolving an image'),
silent: true, // could be a network error or whatnot
informationCollector: collector
);
},
);
informationCollector: collector,
);
},
);
return stream;
}

View file

@ -0,0 +1,31 @@
// 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:async';
import 'dart:typed_data';
import 'package:flutter/painting.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/rendering_tester.dart';
import 'image_data.dart';
void main() {
TestRenderingFlutterBinding();
test('Clearing images while they\'re pending does not crash', () async {
final Uint8List bytes = Uint8List.fromList(kTransparentImage);
final MemoryImage memoryImage = MemoryImage(bytes);
final ImageStream stream = memoryImage.resolve(ImageConfiguration.empty);
final Completer<void> completer = Completer<void>();
FlutterError.onError = (FlutterErrorDetails error) { completer.completeError(error.exception, error.stack); };
stream.addListener(ImageStreamListener(
(ImageInfo image, bool synchronousCall) {
completer.complete();
}
));
imageCache.clearLiveImages();
await completer.future;
});
}

View file

@ -9,71 +9,70 @@ import '../rendering/rendering_tester.dart';
import 'mocks_for_image_cache.dart';
void main() {
TestRenderingFlutterBinding(); // initializes the imageCache
group(ImageCache, () {
tearDown(() {
imageCache.clear();
imageCache.maximumSize = 1000;
imageCache.maximumSizeBytes = 10485760;
});
TestRenderingFlutterBinding();
test('Image cache resizing based on count', () async {
imageCache.maximumSize = 2;
tearDown(() {
imageCache.clear();
imageCache.maximumSize = 1000;
imageCache.maximumSizeBytes = 10485760;
});
final TestImageInfo a = await extractOneFrame(const TestImageProvider(1, 1).resolve(ImageConfiguration.empty)) as TestImageInfo;
final TestImageInfo b = await extractOneFrame(const TestImageProvider(2, 2).resolve(ImageConfiguration.empty)) as TestImageInfo;
final TestImageInfo c = await extractOneFrame(const TestImageProvider(3, 3).resolve(ImageConfiguration.empty)) as TestImageInfo;
final TestImageInfo d = await extractOneFrame(const TestImageProvider(1, 4).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(a.value, equals(1));
expect(b.value, equals(2));
expect(c.value, equals(3));
expect(d.value, equals(4));
test('Image cache resizing based on count', () async {
imageCache.maximumSize = 2;
imageCache.maximumSize = 0;
final TestImageInfo a = await extractOneFrame(const TestImageProvider(1, 1).resolve(ImageConfiguration.empty)) as TestImageInfo;
final TestImageInfo b = await extractOneFrame(const TestImageProvider(2, 2).resolve(ImageConfiguration.empty)) as TestImageInfo;
final TestImageInfo c = await extractOneFrame(const TestImageProvider(3, 3).resolve(ImageConfiguration.empty)) as TestImageInfo;
final TestImageInfo d = await extractOneFrame(const TestImageProvider(1, 4).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(a.value, equals(1));
expect(b.value, equals(2));
expect(c.value, equals(3));
expect(d.value, equals(4));
final TestImageInfo e = await extractOneFrame(const TestImageProvider(1, 5).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(e.value, equals(5));
imageCache.maximumSize = 0;
final TestImageInfo f = await extractOneFrame(const TestImageProvider(1, 6).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(f.value, equals(6));
final TestImageInfo e = await extractOneFrame(const TestImageProvider(1, 5).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(e.value, equals(5));
imageCache.maximumSize = 3;
final TestImageInfo f = await extractOneFrame(const TestImageProvider(1, 6).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(f.value, equals(6));
final TestImageInfo g = await extractOneFrame(const TestImageProvider(1, 7).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(g.value, equals(7));
imageCache.maximumSize = 3;
final TestImageInfo h = await extractOneFrame(const TestImageProvider(1, 8).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(h.value, equals(7));
});
final TestImageInfo g = await extractOneFrame(const TestImageProvider(1, 7).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(g.value, equals(7));
test('Image cache resizing based on size', () async {
const TestImage testImage = TestImage(width: 8, height: 8); // 256 B.
imageCache.maximumSizeBytes = 256 * 2;
final TestImageInfo h = await extractOneFrame(const TestImageProvider(1, 8).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(h.value, equals(7));
});
final TestImageInfo a = await extractOneFrame(const TestImageProvider(1, 1, image: testImage).resolve(ImageConfiguration.empty)) as TestImageInfo;
final TestImageInfo b = await extractOneFrame(const TestImageProvider(2, 2, image: testImage).resolve(ImageConfiguration.empty)) as TestImageInfo;
final TestImageInfo c = await extractOneFrame(const TestImageProvider(3, 3, image: testImage).resolve(ImageConfiguration.empty)) as TestImageInfo;
final TestImageInfo d = await extractOneFrame(const TestImageProvider(1, 4, image: testImage).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(a.value, equals(1));
expect(b.value, equals(2));
expect(c.value, equals(3));
expect(d.value, equals(4));
test('Image cache resizing based on size', () async {
const TestImage testImage = TestImage(width: 8, height: 8); // 256 B.
imageCache.maximumSizeBytes = 256 * 2;
imageCache.maximumSizeBytes = 0;
final TestImageInfo a = await extractOneFrame(const TestImageProvider(1, 1, image: testImage).resolve(ImageConfiguration.empty)) as TestImageInfo;
final TestImageInfo b = await extractOneFrame(const TestImageProvider(2, 2, image: testImage).resolve(ImageConfiguration.empty)) as TestImageInfo;
final TestImageInfo c = await extractOneFrame(const TestImageProvider(3, 3, image: testImage).resolve(ImageConfiguration.empty)) as TestImageInfo;
final TestImageInfo d = await extractOneFrame(const TestImageProvider(1, 4, image: testImage).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(a.value, equals(1));
expect(b.value, equals(2));
expect(c.value, equals(3));
expect(d.value, equals(4));
final TestImageInfo e = await extractOneFrame(const TestImageProvider(1, 5, image: testImage).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(e.value, equals(5));
imageCache.maximumSizeBytes = 0;
final TestImageInfo f = await extractOneFrame(const TestImageProvider(1, 6, image: testImage).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(f.value, equals(6));
final TestImageInfo e = await extractOneFrame(const TestImageProvider(1, 5, image: testImage).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(e.value, equals(5));
imageCache.maximumSizeBytes = 256 * 3;
final TestImageInfo f = await extractOneFrame(const TestImageProvider(1, 6, image: testImage).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(f.value, equals(6));
final TestImageInfo g = await extractOneFrame(const TestImageProvider(1, 7, image: testImage).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(g.value, equals(7));
imageCache.maximumSizeBytes = 256 * 3;
final TestImageInfo h = await extractOneFrame(const TestImageProvider(1, 8, image: testImage).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(h.value, equals(7));
});
final TestImageInfo g = await extractOneFrame(const TestImageProvider(1, 7, image: testImage).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(g.value, equals(7));
final TestImageInfo h = await extractOneFrame(const TestImageProvider(1, 8, image: testImage).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(h.value, equals(7));
});
}

View file

@ -9,475 +9,471 @@ import '../rendering/rendering_tester.dart';
import 'mocks_for_image_cache.dart';
void main() {
group('ImageCache', () {
setUpAll(() {
TestRenderingFlutterBinding(); // initializes the imageCache
TestRenderingFlutterBinding();
tearDown(() {
imageCache.clear();
imageCache.clearLiveImages();
imageCache.maximumSize = 1000;
imageCache.maximumSizeBytes = 10485760;
});
test('maintains cache size', () async {
imageCache.maximumSize = 3;
final TestImageInfo a = await extractOneFrame(const TestImageProvider(1, 1).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(a.value, equals(1));
final TestImageInfo b = await extractOneFrame(const TestImageProvider(1, 2).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(b.value, equals(1));
final TestImageInfo c = await extractOneFrame(const TestImageProvider(1, 3).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(c.value, equals(1));
final TestImageInfo d = await extractOneFrame(const TestImageProvider(1, 4).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(d.value, equals(1));
final TestImageInfo e = await extractOneFrame(const TestImageProvider(1, 5).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(e.value, equals(1));
final TestImageInfo f = await extractOneFrame(const TestImageProvider(1, 6).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 TestImageInfo g = await extractOneFrame(const TestImageProvider(2, 7).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(g.value, equals(7));
// cache has two entries in it: 1(1), 2(7)
final TestImageInfo h = await extractOneFrame(const TestImageProvider(1, 8).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(h.value, equals(1));
// cache still has two entries in it: 2(7), 1(1)
final TestImageInfo i = await extractOneFrame(const TestImageProvider(3, 9).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(i.value, equals(9));
// cache has three entries in it: 2(7), 1(1), 3(9)
final TestImageInfo j = await extractOneFrame(const TestImageProvider(1, 10).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(j.value, equals(1));
// cache still has three entries in it: 2(7), 3(9), 1(1)
final TestImageInfo k = await extractOneFrame(const TestImageProvider(4, 11).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(k.value, equals(11));
// cache has three entries: 3(9), 1(1), 4(11)
final TestImageInfo l = await extractOneFrame(const TestImageProvider(1, 12).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(l.value, equals(1));
// cache has three entries: 3(9), 4(11), 1(1)
final TestImageInfo m = await extractOneFrame(const TestImageProvider(2, 13).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(m.value, equals(13));
// cache has three entries: 4(11), 1(1), 2(13)
final TestImageInfo n = await extractOneFrame(const TestImageProvider(3, 14).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(n.value, equals(14));
// cache has three entries: 1(1), 2(13), 3(14)
final TestImageInfo o = await extractOneFrame(const TestImageProvider(4, 15).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(o.value, equals(15));
// cache has three entries: 2(13), 3(14), 4(15)
final TestImageInfo p = await extractOneFrame(const TestImageProvider(1, 16).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 {
const TestImage testImage = TestImage(width: 8, height: 8);
expect(imageCache.currentSize, 0);
expect(imageCache.currentSizeBytes, 0);
await extractOneFrame(const TestImageProvider(1, 1, image: testImage).resolve(ImageConfiguration.empty));
await extractOneFrame(const 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 {
const TestImage testImage = TestImage(width: 8, height: 8);
await extractOneFrame(const TestImageProvider(1, 1, image: testImage).resolve(ImageConfiguration.empty));
await extractOneFrame(const 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 {
const TestImage testImage = TestImage(width: 8, height: 8);
imageCache.maximumSizeBytes = 1;
await extractOneFrame(const 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', () {
final ErrorImageProvider errorImage = ErrorImageProvider();
expect(() => imageCache.putIfAbsent(errorImage, () => errorImage.load(errorImage, null)), throwsA(isA<Error>()));
bool caughtError = false;
final ImageStreamCompleter result = imageCache.putIfAbsent(errorImage, () => errorImage.load(errorImage, null), onError: (dynamic error, StackTrace stackTrace) {
caughtError = true;
});
expect(result, null);
expect(caughtError, true);
});
tearDown(() {
imageCache.clear();
imageCache.clearLiveImages();
imageCache.maximumSize = 1000;
imageCache.maximumSizeBytes = 10485760;
});
test('already pending image is returned when it is put into the cache again', () async {
const TestImage testImage = TestImage(width: 8, height: 8);
test('maintains cache size', () async {
imageCache.maximumSize = 3;
final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
final TestImageStreamCompleter completer2 = TestImageStreamCompleter();
final TestImageInfo a = await extractOneFrame(const TestImageProvider(1, 1).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(a.value, equals(1));
final TestImageInfo b = await extractOneFrame(const TestImageProvider(1, 2).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(b.value, equals(1));
final TestImageInfo c = await extractOneFrame(const TestImageProvider(1, 3).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(c.value, equals(1));
final TestImageInfo d = await extractOneFrame(const TestImageProvider(1, 4).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(d.value, equals(1));
final TestImageInfo e = await extractOneFrame(const TestImageProvider(1, 5).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(e.value, equals(1));
final TestImageInfo f = await extractOneFrame(const TestImageProvider(1, 6).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(f.value, equals(1));
final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () {
return completer1;
}) as TestImageStreamCompleter;
final TestImageStreamCompleter resultingCompleter2 = imageCache.putIfAbsent(testImage, () {
return completer2;
}) as TestImageStreamCompleter;
expect(f, equals(a));
expect(resultingCompleter1, completer1);
expect(resultingCompleter2, completer1);
});
// cache still only has one entry in it: 1(1)
test('pending image is removed when cache is cleared', () async {
const TestImage testImage = TestImage(width: 8, height: 8);
final TestImageInfo g = await extractOneFrame(const TestImageProvider(2, 7).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(g.value, equals(7));
final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
final TestImageStreamCompleter completer2 = TestImageStreamCompleter();
// cache has two entries in it: 1(1), 2(7)
final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () {
return completer1;
}) as TestImageStreamCompleter;
final TestImageInfo h = await extractOneFrame(const TestImageProvider(1, 8).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(h.value, equals(1));
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 TestImageStreamCompleter resultingCompleter2 = imageCache.putIfAbsent(testImage, () {
return completer2;
}) as TestImageStreamCompleter;
expect(resultingCompleter1, completer1);
expect(resultingCompleter2, completer2);
});
// cache still has two entries in it: 2(7), 1(1)
test('pending image is removed when image is evicted', () async {
const TestImage testImage = TestImage(width: 8, height: 8);
final TestImageInfo i = await extractOneFrame(const TestImageProvider(3, 9).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(i.value, equals(9));
final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
final TestImageStreamCompleter completer2 = TestImageStreamCompleter();
final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () {
return completer1;
}) as TestImageStreamCompleter;
// cache has three entries in it: 2(7), 1(1), 3(9)
imageCache.evict(testImage);
final TestImageStreamCompleter 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 {
const TestImage testImage = TestImage(width: 8, height: 8);
const FailingTestImageProvider(1, 1, image: testImage)
.resolve(ImageConfiguration.empty)
.addListener(ImageStreamListener(
(ImageInfo image, bool synchronousCall) { },
onError: (dynamic exception, StackTrace stackTrace) {
final bool evicationResult = imageCache.evict(1);
expect(evicationResult, isTrue);
},
));
});
final TestImageInfo j = await extractOneFrame(const TestImageProvider(1, 10).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(j.value, equals(1));
test('containsKey - pending', () async {
const TestImage testImage = TestImage(width: 8, height: 8);
// cache still has three entries in it: 2(7), 3(9), 1(1)
final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
final TestImageInfo k = await extractOneFrame(const TestImageProvider(4, 11).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(k.value, equals(11));
final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () {
return completer1;
}) as TestImageStreamCompleter;
// cache has three entries: 3(9), 1(1), 4(11)
expect(resultingCompleter1, completer1);
expect(imageCache.containsKey(testImage), true);
});
final TestImageInfo l = await extractOneFrame(const TestImageProvider(1, 12).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(l.value, equals(1));
test('containsKey - completed', () async {
const TestImage testImage = TestImage(width: 8, height: 8);
// cache has three entries: 3(9), 4(11), 1(1)
final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
final TestImageInfo m = await extractOneFrame(const TestImageProvider(2, 13).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(m.value, equals(13));
final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () {
return completer1;
}) as TestImageStreamCompleter;
// cache has three entries: 4(11), 1(1), 2(13)
// Mark as complete
completer1.testSetImage(testImage);
final TestImageInfo n = await extractOneFrame(const TestImageProvider(3, 14).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(n.value, equals(14));
expect(resultingCompleter1, completer1);
expect(imageCache.containsKey(testImage), true);
});
test('putIfAbsent updates LRU properties of a live image', () async {
imageCache.maximumSize = 1;
const TestImage testImage = TestImage(width: 8, height: 8);
const TestImage testImage2 = TestImage(width: 10, height: 10);
// cache has three entries: 1(1), 2(13), 3(14)
final TestImageStreamCompleter completer1 = TestImageStreamCompleter()..testSetImage(testImage);
final TestImageStreamCompleter completer2 = TestImageStreamCompleter()..testSetImage(testImage2);
final TestImageInfo o = await extractOneFrame(const TestImageProvider(4, 15).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(o.value, equals(15));
completer1.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {}));
// cache has three entries: 2(13), 3(14), 4(15)
final TestImageStreamCompleter 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 TestImageStreamCompleter resultingCompleter2 = imageCache.putIfAbsent(testImage2, () {
return completer2;
}) as TestImageStreamCompleter;
final TestImageInfo p = await extractOneFrame(const TestImageProvider(1, 16).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(p.value, equals(16));
// cache has three entries: 3(14), 4(15), 1(16)
});
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
test('clear removes all images and resets cache size', () async {
const TestImage testImage = TestImage(width: 8, height: 8);
expect(resultingCompleter1, completer1);
expect(resultingCompleter2, completer2);
});
expect(imageCache.currentSize, 0);
expect(imageCache.currentSizeBytes, 0);
test('Live image cache avoids leaks of unlistened streams', () async {
imageCache.maximumSize = 3;
await extractOneFrame(const TestImageProvider(1, 1, image: testImage).resolve(ImageConfiguration.empty));
await extractOneFrame(const TestImageProvider(2, 2, image: testImage).resolve(ImageConfiguration.empty));
const TestImageProvider(1, 1).resolve(ImageConfiguration.empty);
const TestImageProvider(2, 2).resolve(ImageConfiguration.empty);
const TestImageProvider(3, 3).resolve(ImageConfiguration.empty);
const TestImageProvider(4, 4).resolve(ImageConfiguration.empty);
const TestImageProvider(5, 5).resolve(ImageConfiguration.empty);
const TestImageProvider(6, 6).resolve(ImageConfiguration.empty);
expect(imageCache.currentSize, 2);
expect(imageCache.currentSizeBytes, 256 * 2);
// wait an event loop to let image resolution process.
await null;
imageCache.clear();
expect(imageCache.currentSize, 3);
expect(imageCache.liveImageCount, 0);
});
test('Disabled image cache does not leak live images', () async {
imageCache.maximumSize = 0;
expect(imageCache.currentSize, 0);
expect(imageCache.currentSizeBytes, 0);
});
const TestImageProvider(1, 1).resolve(ImageConfiguration.empty);
const TestImageProvider(2, 2).resolve(ImageConfiguration.empty);
const TestImageProvider(3, 3).resolve(ImageConfiguration.empty);
const TestImageProvider(4, 4).resolve(ImageConfiguration.empty);
const TestImageProvider(5, 5).resolve(ImageConfiguration.empty);
const TestImageProvider(6, 6).resolve(ImageConfiguration.empty);
// wait an event loop to let image resolution process.
await null;
test('evicts individual images', () async {
const TestImage testImage = TestImage(width: 8, height: 8);
await extractOneFrame(const TestImageProvider(1, 1, image: testImage).resolve(ImageConfiguration.empty));
await extractOneFrame(const TestImageProvider(2, 2, image: testImage).resolve(ImageConfiguration.empty));
expect(imageCache.currentSize, 0);
expect(imageCache.liveImageCount, 0);
});
expect(imageCache.currentSize, 2);
expect(imageCache.currentSizeBytes, 256 * 2);
expect(imageCache.evict(1), true);
expect(imageCache.currentSize, 1);
expect(imageCache.currentSizeBytes, 256);
});
test('Evicting a pending image clears the live image by default', () async {
const TestImage testImage = TestImage(width: 8, height: 8);
test('Do not cache large images', () async {
const TestImage testImage = TestImage(width: 8, height: 8);
final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
imageCache.maximumSizeBytes = 1;
await extractOneFrame(const TestImageProvider(1, 1, image: testImage).resolve(ImageConfiguration.empty));
expect(imageCache.currentSize, 0);
expect(imageCache.currentSizeBytes, 0);
expect(imageCache.maximumSizeBytes, 1);
});
imageCache.putIfAbsent(testImage, () => completer1);
expect(imageCache.statusForKey(testImage).pending, true);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, false);
test('Returns null if an error is caught resolving an image', () {
final ErrorImageProvider errorImage = ErrorImageProvider();
expect(() => imageCache.putIfAbsent(errorImage, () => errorImage.load(errorImage, null)), throwsA(isA<Error>()));
bool caughtError = false;
final ImageStreamCompleter result = imageCache.putIfAbsent(errorImage, () => errorImage.load(errorImage, null), onError: (dynamic error, StackTrace stackTrace) {
caughtError = true;
});
expect(result, null);
expect(caughtError, true);
});
imageCache.evict(testImage);
expect(imageCache.statusForKey(testImage).untracked, true);
});
test('already pending image is returned when it is put into the cache again', () async {
const TestImage testImage = TestImage(width: 8, height: 8);
test('Evicting a pending image does clear the live image when includeLive is false and only cache listening', () async {
const TestImage testImage = TestImage(width: 8, height: 8);
final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
final TestImageStreamCompleter completer2 = TestImageStreamCompleter();
final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () {
return completer1;
}) as TestImageStreamCompleter;
final TestImageStreamCompleter resultingCompleter2 = imageCache.putIfAbsent(testImage, () {
return completer2;
}) as 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 {
const TestImage testImage = TestImage(width: 8, height: 8);
final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
expect(resultingCompleter1, completer1);
expect(resultingCompleter2, completer1);
});
imageCache.putIfAbsent(testImage, () => completer1);
expect(imageCache.statusForKey(testImage).pending, true);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, false);
test('pending image is removed when cache is cleared', () async {
const TestImage testImage = TestImage(width: 8, height: 8);
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 {
const TestImage testImage = TestImage(width: 8, height: 8);
final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
final TestImageStreamCompleter completer2 = TestImageStreamCompleter();
final TestImageStreamCompleter completer1 = TestImageStreamCompleter()
..testSetImage(testImage)
..addListener(ImageStreamListener((ImageInfo info, bool syncCall) {}));
final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () {
return completer1;
}) as TestImageStreamCompleter;
imageCache.putIfAbsent(testImage, () => completer1);
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, true);
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);
imageCache.evict(testImage);
expect(imageCache.statusForKey(testImage).untracked, true);
});
final TestImageStreamCompleter resultingCompleter2 = imageCache.putIfAbsent(testImage, () {
return completer2;
}) as TestImageStreamCompleter;
expect(resultingCompleter1, completer1);
expect(resultingCompleter2, completer2);
});
test('pending image is removed when image is evicted', () async {
const TestImage testImage = TestImage(width: 8, height: 8);
final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
final TestImageStreamCompleter completer2 = TestImageStreamCompleter();
final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () {
return completer1;
}) as TestImageStreamCompleter;
imageCache.evict(testImage);
final TestImageStreamCompleter 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 {
const TestImage testImage = TestImage(width: 8, height: 8);
const FailingTestImageProvider(1, 1, image: testImage)
.resolve(ImageConfiguration.empty)
.addListener(ImageStreamListener(
(ImageInfo image, bool synchronousCall) { },
onError: (dynamic exception, StackTrace stackTrace) {
final bool evicationResult = imageCache.evict(1);
expect(evicationResult, isTrue);
},
));
});
test('containsKey - pending', () async {
const TestImage testImage = TestImage(width: 8, height: 8);
final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () {
return completer1;
}) as TestImageStreamCompleter;
expect(resultingCompleter1, completer1);
expect(imageCache.containsKey(testImage), true);
});
test('containsKey - completed', () async {
const TestImage testImage = TestImage(width: 8, height: 8);
test('Evicting a completed image does not clear the live image when includeLive is set to false', () async {
const TestImage testImage = TestImage(width: 8, height: 8);
final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
final TestImageStreamCompleter completer1 = TestImageStreamCompleter()
..testSetImage(testImage)
..addListener(ImageStreamListener((ImageInfo info, bool syncCall) {}));
final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () {
return completer1;
}) as TestImageStreamCompleter;
imageCache.putIfAbsent(testImage, () => completer1);
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, true);
// Mark as complete
completer1.testSetImage(testImage);
imageCache.evict(testImage, includeLive: false);
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, false);
});
expect(resultingCompleter1, completer1);
expect(imageCache.containsKey(testImage), true);
});
test('putIfAbsent updates LRU properties of a live image', () async {
imageCache.maximumSize = 1;
const TestImage testImage = TestImage(width: 8, height: 8);
const TestImage testImage2 = TestImage(width: 10, height: 10);
test('Clearing liveImages removes callbacks', () async {
const TestImage testImage = TestImage(width: 8, height: 8);
final TestImageStreamCompleter completer1 = TestImageStreamCompleter()..testSetImage(testImage);
final TestImageStreamCompleter completer2 = TestImageStreamCompleter()..testSetImage(testImage2);
final ImageStreamListener listener = ImageStreamListener((ImageInfo info, bool syncCall) {});
completer1.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {}));
final TestImageStreamCompleter completer1 = TestImageStreamCompleter()
..testSetImage(testImage)
..addListener(listener);
final TestImageStreamCompleter 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 TestImageStreamCompleter resultingCompleter2 = imageCache.putIfAbsent(testImage2, () {
return completer2;
}) as TestImageStreamCompleter;
final TestImageStreamCompleter 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);
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
imageCache.clear();
imageCache.clearLiveImages();
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, false);
expect(imageCache.statusForKey(testImage).keepAlive, false);
expect(resultingCompleter1, completer1);
expect(resultingCompleter2, completer2);
});
imageCache.putIfAbsent(testImage, () => completer2);
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, true);
test('Live image cache avoids leaks of unlistened streams', () async {
imageCache.maximumSize = 3;
completer1.removeListener(listener);
const TestImageProvider(1, 1).resolve(ImageConfiguration.empty);
const TestImageProvider(2, 2).resolve(ImageConfiguration.empty);
const TestImageProvider(3, 3).resolve(ImageConfiguration.empty);
const TestImageProvider(4, 4).resolve(ImageConfiguration.empty);
const TestImageProvider(5, 5).resolve(ImageConfiguration.empty);
const TestImageProvider(6, 6).resolve(ImageConfiguration.empty);
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, true);
});
// wait an event loop to let image resolution process.
await null;
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.
const TestImage testImage = TestImage(width: 8, height: 8);
const int testImageSize = 8 * 8 * 4;
expect(imageCache.currentSize, 3);
expect(imageCache.liveImageCount, 0);
});
test('Disabled image cache does not leak live images', () async {
imageCache.maximumSize = 0;
final ImageStreamListener listener = ImageStreamListener((ImageInfo info, bool syncCall) {});
const TestImageProvider(1, 1).resolve(ImageConfiguration.empty);
const TestImageProvider(2, 2).resolve(ImageConfiguration.empty);
const TestImageProvider(3, 3).resolve(ImageConfiguration.empty);
const TestImageProvider(4, 4).resolve(ImageConfiguration.empty);
const TestImageProvider(5, 5).resolve(ImageConfiguration.empty);
const TestImageProvider(6, 6).resolve(ImageConfiguration.empty);
// wait an event loop to let image resolution process.
await null;
final TestImageStreamCompleter completer1 = TestImageStreamCompleter()
..addListener(listener);
expect(imageCache.currentSize, 0);
expect(imageCache.liveImageCount, 0);
});
test('Evicting a pending image clears the live image by default', () async {
const TestImage testImage = TestImage(width: 8, height: 8);
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);
final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
completer1.testSetImage(testImage);
imageCache.putIfAbsent(testImage, () => completer1);
expect(imageCache.statusForKey(testImage).pending, true);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, false);
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);
expect(imageCache.statusForKey(testImage).untracked, true);
});
imageCache.evict(testImage, includeLive: false);
test('Evicting a pending image does clear the live image when includeLive is false and only cache listening', () async {
const TestImage testImage = TestImage(width: 8, height: 8);
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, false);
expect(imageCache.currentSizeBytes, 0);
final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
imageCache.putIfAbsent(testImage, () => completer1);
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 {
const TestImage testImage = TestImage(width: 8, height: 8);
final TestImageStreamCompleter 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 {
const TestImage testImage = TestImage(width: 8, height: 8);
final TestImageStreamCompleter 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 {
const TestImage testImage = TestImage(width: 8, height: 8);
final TestImageStreamCompleter 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 {
const TestImage testImage = TestImage(width: 8, height: 8);
final ImageStreamListener listener = ImageStreamListener((ImageInfo info, bool syncCall) {});
final TestImageStreamCompleter completer1 = TestImageStreamCompleter()
..testSetImage(testImage)
..addListener(listener);
final TestImageStreamCompleter 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.
const TestImage testImage = TestImage(width: 8, height: 8);
const int testImageSize = 8 * 8 * 4;
final ImageStreamListener listener = ImageStreamListener((ImageInfo info, bool syncCall) {});
final TestImageStreamCompleter 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);
});
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, true);
expect(imageCache.currentSizeBytes, testImageSize);
});
}

View file

@ -0,0 +1,142 @@
// 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:async';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/rendering_tester.dart';
import 'image_data.dart';
import 'mocks_for_image_cache.dart';
void main() {
TestRenderingFlutterBinding();
final DecoderCallback _basicDecoder = (Uint8List bytes, {int cacheWidth, int cacheHeight}) {
return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight);
};
FlutterExceptionHandler oldError;
setUp(() {
oldError = FlutterError.onError;
});
tearDown(() {
FlutterError.onError = oldError;
PaintingBinding.instance.imageCache.clear();
PaintingBinding.instance.imageCache.clearLiveImages();
});
tearDown(() {
imageCache.clear();
});
test('AssetImageProvider - evicts on failure to load', () async {
final Completer<FlutterError> error = Completer<FlutterError>();
FlutterError.onError = (FlutterErrorDetails details) {
error.complete(details.exception as FlutterError);
};
const ImageProvider provider = ExactAssetImage('does-not-exist');
final Object key = await provider.obtainKey(ImageConfiguration.empty);
expect(imageCache.statusForKey(provider).untracked, true);
expect(imageCache.pendingImageCount, 0);
provider.resolve(ImageConfiguration.empty);
expect(imageCache.statusForKey(key).pending, true);
expect(imageCache.pendingImageCount, 1);
await error.future;
expect(imageCache.statusForKey(provider).untracked, true);
expect(imageCache.pendingImageCount, 0);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/56314
test('AssetImageProvider - evicts on null load', () async {
final Completer<StateError> error = Completer<StateError>();
FlutterError.onError = (FlutterErrorDetails details) {
error.complete(details.exception as StateError);
};
final ImageProvider provider = ExactAssetImage('does-not-exist', bundle: _TestAssetBundle());
final Object key = await provider.obtainKey(ImageConfiguration.empty);
expect(imageCache.statusForKey(provider).untracked, true);
expect(imageCache.pendingImageCount, 0);
provider.resolve(ImageConfiguration.empty);
expect(imageCache.statusForKey(key).pending, true);
expect(imageCache.pendingImageCount, 1);
await error.future;
expect(imageCache.statusForKey(provider).untracked, true);
expect(imageCache.pendingImageCount, 0);
});
test('ImageProvider can evict images', () async {
final Uint8List bytes = Uint8List.fromList(kTransparentImage);
final MemoryImage imageProvider = MemoryImage(bytes);
final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
final Completer<void> completer = Completer<void>();
stream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) => completer.complete()));
await completer.future;
expect(imageCache.currentSize, 1);
expect(await MemoryImage(bytes).evict(), true);
expect(imageCache.currentSize, 0);
});
test('ImageProvider.evict respects the provided ImageCache', () async {
final ImageCache otherCache = ImageCache();
final Uint8List bytes = Uint8List.fromList(kTransparentImage);
final MemoryImage imageProvider = MemoryImage(bytes);
final ImageStreamCompleter cacheStream = otherCache.putIfAbsent(
imageProvider, () => imageProvider.load(imageProvider, _basicDecoder),
);
final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
final Completer<void> completer = Completer<void>();
final Completer<void> cacheCompleter = Completer<void>();
stream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
completer.complete();
}));
cacheStream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
cacheCompleter.complete();
}));
await Future.wait(<Future<void>>[completer.future, cacheCompleter.future]);
expect(otherCache.currentSize, 1);
expect(imageCache.currentSize, 1);
expect(await imageProvider.evict(cache: otherCache), true);
expect(otherCache.currentSize, 0);
expect(imageCache.currentSize, 1);
});
test('ImageProvider errors can always be caught', () async {
final ErrorImageProvider imageProvider = ErrorImageProvider();
final Completer<bool> caughtError = Completer<bool>();
FlutterError.onError = (FlutterErrorDetails details) {
caughtError.complete(false);
};
final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
stream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
caughtError.complete(false);
}, onError: (dynamic error, StackTrace stackTrace) {
caughtError.complete(true);
}));
expect(await caughtError.future, true);
});
}
class _TestAssetBundle extends CachingAssetBundle {
@override
Future<ByteData> load(String key) async {
return null;
}
}

View file

@ -0,0 +1,224 @@
// 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:async';
import 'dart:io';
import 'dart:math' as math;
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import '../rendering/rendering_tester.dart';
import 'image_data.dart';
void main() {
TestRenderingFlutterBinding();
final DecoderCallback _basicDecoder = (Uint8List bytes, {int cacheWidth, int cacheHeight}) {
return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight);
};
_MockHttpClient httpClient;
setUp(() {
httpClient = _MockHttpClient();
debugNetworkImageHttpClientProvider = () => httpClient;
});
tearDown(() {
debugNetworkImageHttpClientProvider = null;
PaintingBinding.instance.imageCache.clear();
PaintingBinding.instance.imageCache.clearLiveImages();
});
test('Expect thrown exception with statusCode - evicts from cache', () async {
final int errorStatusCode = HttpStatus.notFound;
const String requestUrl = 'foo-url';
final _MockHttpClientRequest request = _MockHttpClientRequest();
final _MockHttpClientResponse response = _MockHttpClientResponse();
when(httpClient.getUrl(any)).thenAnswer((_) => Future<HttpClientRequest>.value(request));
when(request.close()).thenAnswer((_) => Future<HttpClientResponse>.value(response));
when(response.statusCode).thenReturn(errorStatusCode);
final Completer<dynamic> caughtError = Completer<dynamic>();
final ImageProvider imageProvider = NetworkImage(nonconst(requestUrl));
expect(imageCache.pendingImageCount, 0);
expect(imageCache.statusForKey(imageProvider).untracked, true);
final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
expect(imageCache.pendingImageCount, 1);
expect(imageCache.statusForKey(imageProvider).pending, true);
result.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
}, onError: (dynamic error, StackTrace stackTrace) {
caughtError.complete(error);
}));
final dynamic err = await caughtError.future;
expect(imageCache.pendingImageCount, 0);
expect(imageCache.statusForKey(imageProvider).untracked, true);
expect(
err,
isA<NetworkImageLoadException>()
.having((NetworkImageLoadException e) => e.statusCode, 'statusCode', errorStatusCode)
.having((NetworkImageLoadException e) => e.uri, 'uri', Uri.base.resolve(requestUrl)),
);
}, skip: isBrowser); // Browser implementation does not use HTTP client but an <img> tag.
test('Disallows null urls', () {
expect(() {
NetworkImage(nonconst(null));
}, throwsAssertionError);
});
test('Uses the HttpClient provided by debugNetworkImageHttpClientProvider if set', () async {
when(httpClient.getUrl(any)).thenThrow('client1');
final List<dynamic> capturedErrors = <dynamic>[];
Future<void> loadNetworkImage() async {
final NetworkImage networkImage = NetworkImage(nonconst('foo'));
final ImageStreamCompleter completer = networkImage.load(networkImage, _basicDecoder);
completer.addListener(ImageStreamListener(
(ImageInfo image, bool synchronousCall) { },
onError: (dynamic error, StackTrace stackTrace) {
capturedErrors.add(error);
},
));
await Future<void>.value();
}
await loadNetworkImage();
expect(capturedErrors, <dynamic>['client1']);
final _MockHttpClient client2 = _MockHttpClient();
when(client2.getUrl(any)).thenThrow('client2');
debugNetworkImageHttpClientProvider = () => client2;
await loadNetworkImage();
expect(capturedErrors, <dynamic>['client1', 'client2']);
}, skip: isBrowser); // Browser implementation does not use HTTP client but an <img> tag.
test('Propagates http client errors during resolve()', () async {
when(httpClient.getUrl(any)).thenThrow(Error());
bool uncaught = false;
final FlutterExceptionHandler oldError = FlutterError.onError;
await runZoned(() async {
const ImageProvider imageProvider = NetworkImage('asdasdasdas');
final Completer<bool> caughtError = Completer<bool>();
FlutterError.onError = (FlutterErrorDetails details) {
throw Error();
};
final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
result.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
}, onError: (dynamic error, StackTrace stackTrace) {
caughtError.complete(true);
}));
expect(await caughtError.future, true);
}, zoneSpecification: ZoneSpecification(
handleUncaughtError: (Zone zone, ZoneDelegate zoneDelegate, Zone parent, Object error, StackTrace stackTrace) {
uncaught = true;
},
));
expect(uncaught, false);
FlutterError.onError = oldError;
});
test('Notifies listeners of chunk events', () async {
const int chunkSize = 8;
final List<Uint8List> chunks = <Uint8List>[
for (int offset = 0; offset < kTransparentImage.length; offset += chunkSize)
Uint8List.fromList(kTransparentImage.skip(offset).take(chunkSize).toList()),
];
final Completer<void> imageAvailable = Completer<void>();
final _MockHttpClientRequest request = _MockHttpClientRequest();
final _MockHttpClientResponse response = _MockHttpClientResponse();
when(httpClient.getUrl(any)).thenAnswer((_) => Future<HttpClientRequest>.value(request));
when(request.close()).thenAnswer((_) => Future<HttpClientResponse>.value(response));
when(response.statusCode).thenReturn(HttpStatus.ok);
when(response.contentLength).thenReturn(kTransparentImage.length);
when(response.listen(
any,
onDone: anyNamed('onDone'),
onError: anyNamed('onError'),
cancelOnError: anyNamed('cancelOnError'),
)).thenAnswer((Invocation invocation) {
final void Function(List<int>) onData = invocation.positionalArguments[0] as void Function(List<int>);
final void Function(Object) onError = invocation.namedArguments[#onError] as void Function(Object);
final VoidCallback onDone = invocation.namedArguments[#onDone] as VoidCallback;
final bool cancelOnError = invocation.namedArguments[#cancelOnError] as bool;
return Stream<Uint8List>.fromIterable(chunks).listen(
onData,
onDone: onDone,
onError: onError,
cancelOnError: cancelOnError,
);
});
final ImageProvider imageProvider = NetworkImage(nonconst('foo'));
final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
final List<ImageChunkEvent> events = <ImageChunkEvent>[];
result.addListener(ImageStreamListener(
(ImageInfo image, bool synchronousCall) {
imageAvailable.complete();
},
onChunk: (ImageChunkEvent event) {
events.add(event);
},
onError: (dynamic error, StackTrace stackTrace) {
imageAvailable.completeError(error, stackTrace);
},
));
await imageAvailable.future;
expect(events.length, chunks.length);
for (int i = 0; i < events.length; i++) {
expect(events[i].cumulativeBytesLoaded, math.min((i + 1) * chunkSize, kTransparentImage.length));
expect(events[i].expectedTotalBytes, kTransparentImage.length);
}
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/56317
test('NetworkImage is evicted from cache on SocketException', () async {
final _MockHttpClient mockHttpClient = _MockHttpClient();
when(mockHttpClient.getUrl(any)).thenAnswer((_) => throw const SocketException('test exception'));
debugNetworkImageHttpClientProvider = () => mockHttpClient;
final ImageProvider imageProvider = NetworkImage(nonconst('testing.url'));
expect(imageCache.pendingImageCount, 0);
expect(imageCache.statusForKey(imageProvider).untracked, true);
final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
expect(imageCache.pendingImageCount, 1);
expect(imageCache.statusForKey(imageProvider).pending, true);
final Completer<dynamic> caughtError = Completer<dynamic>();
result.addListener(ImageStreamListener(
(ImageInfo info, bool syncCall) {},
onError: (dynamic error, StackTrace stackTrace) {
caughtError.complete(error);
},
));
final dynamic err = await caughtError.future;
expect(err, isA<SocketException>());
expect(imageCache.pendingImageCount, 0);
expect(imageCache.statusForKey(imageProvider).untracked, true);
expect(imageCache.containsKey(result), isFalse);
debugNetworkImageHttpClientProvider = null;
}, skip: isBrowser); // Browser does not resolve images this way.
}
class _MockHttpClient extends Mock implements HttpClient {}
class _MockHttpClientRequest extends Mock implements HttpClientRequest {}
class _MockHttpClientResponse extends Mock implements HttpClientResponse {}

View file

@ -0,0 +1,134 @@
// 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:async';
import 'dart:typed_data';
import 'package:flutter/painting.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/rendering_tester.dart';
import 'image_data.dart';
void main() {
TestRenderingFlutterBinding();
tearDown(() {
PaintingBinding.instance.imageCache.clear();
PaintingBinding.instance.imageCache.clearLiveImages();
});
test('ResizeImage resizes to the correct dimensions', () async {
final Uint8List bytes = Uint8List.fromList(kTransparentImage);
final MemoryImage imageProvider = MemoryImage(bytes);
final Size rawImageSize = await _resolveAndGetSize(imageProvider);
expect(rawImageSize, const Size(1, 1));
const Size resizeDims = Size(14, 7);
final ResizeImage resizedImage = ResizeImage(MemoryImage(bytes), width: resizeDims.width.round(), height: resizeDims.height.round());
const ImageConfiguration resizeConfig = ImageConfiguration(size: resizeDims);
final Size resizedImageSize = await _resolveAndGetSize(resizedImage, configuration: resizeConfig);
expect(resizedImageSize, resizeDims);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/56312
test('ResizeImage does not resize when no size is passed', () async {
final Uint8List bytes = Uint8List.fromList(kTransparentImage);
final MemoryImage imageProvider = MemoryImage(bytes);
final Size rawImageSize = await _resolveAndGetSize(imageProvider);
expect(rawImageSize, const Size(1, 1));
// Cannot pass in two null arguments for cache dimensions, so will use the regular
// MemoryImage
final MemoryImage resizedImage = MemoryImage(bytes);
final Size resizedImageSize = await _resolveAndGetSize(resizedImage);
expect(resizedImageSize, const Size(1, 1));
});
test('ResizeImage stores values', () async {
final Uint8List bytes = Uint8List.fromList(kTransparentImage);
final MemoryImage memoryImage = MemoryImage(bytes);
memoryImage.resolve(ImageConfiguration.empty);
final ResizeImage resizeImage = ResizeImage(memoryImage, width: 10, height: 20);
expect(resizeImage.width, 10);
expect(resizeImage.height, 20);
expect(resizeImage.imageProvider, memoryImage);
expect(memoryImage.resolve(ImageConfiguration.empty) != resizeImage.resolve(ImageConfiguration.empty), true);
});
test('ResizeImage takes one dim', () async {
final Uint8List bytes = Uint8List.fromList(kTransparentImage);
final MemoryImage memoryImage = MemoryImage(bytes);
final ResizeImage resizeImage = ResizeImage(memoryImage, width: 10, height: null);
expect(resizeImage.width, 10);
expect(resizeImage.height, null);
expect(resizeImage.imageProvider, memoryImage);
expect(memoryImage.resolve(ImageConfiguration.empty) != resizeImage.resolve(ImageConfiguration.empty), true);
});
test('ResizeImage forms closure', () async {
final Uint8List bytes = Uint8List.fromList(kTransparentImage);
final MemoryImage memoryImage = MemoryImage(bytes);
final ResizeImage resizeImage = ResizeImage(memoryImage, width: 123, height: 321);
final DecoderCallback decode = (Uint8List bytes, {int cacheWidth, int cacheHeight}) {
expect(cacheWidth, 123);
expect(cacheHeight, 321);
return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight);
};
resizeImage.load(await resizeImage.obtainKey(ImageConfiguration.empty), decode);
});
test('ResizeImage handles sync obtainKey', () async {
final Uint8List bytes = Uint8List.fromList(kTransparentImage);
final MemoryImage memoryImage = MemoryImage(bytes);
final ResizeImage resizeImage = ResizeImage(memoryImage, width: 123, height: 321);
bool isAsync = false;
resizeImage.obtainKey(ImageConfiguration.empty).then((Object key) {
expect(isAsync, false);
});
isAsync = true;
expect(isAsync, true);
});
test('ResizeImage handles async obtainKey', () async {
final Uint8List bytes = Uint8List.fromList(kTransparentImage);
final _AsyncKeyMemoryImage memoryImage = _AsyncKeyMemoryImage(bytes);
final ResizeImage resizeImage = ResizeImage(memoryImage, width: 123, height: 321);
bool isAsync = false;
resizeImage.obtainKey(ImageConfiguration.empty).then((Object key) {
expect(isAsync, true);
});
isAsync = true;
expect(isAsync, true);
});
}
Future<Size> _resolveAndGetSize(ImageProvider imageProvider,
{ImageConfiguration configuration = ImageConfiguration.empty}) async {
final ImageStream stream = imageProvider.resolve(configuration);
final Completer<Size> completer = Completer<Size>();
final ImageStreamListener listener =
ImageStreamListener((ImageInfo image, bool synchronousCall) {
final int height = image.image.height;
final int width = image.image.width;
completer.complete(Size(width.toDouble(), height.toDouble()));
}
);
stream.addListener(listener);
return await completer.future;
}
// This version of MemoryImage guarantees obtainKey returns a future that has not been
// completed synchronously.
class _AsyncKeyMemoryImage extends MemoryImage {
const _AsyncKeyMemoryImage(Uint8List bytes) : super(bytes);
@override
Future<MemoryImage> obtainKey(ImageConfiguration configuration) {
return Future<MemoryImage>(() => this);
}
}

View file

@ -4,29 +4,17 @@
import 'dart:async';
import 'dart:io';
import 'dart:math' as math;
import 'dart:typed_data';
import 'package:file/memory.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import '../rendering/rendering_tester.dart';
import 'image_data.dart';
import 'mocks_for_image_cache.dart';
void main() {
final DecoderCallback basicDecoder = (Uint8List bytes, {int cacheWidth, int cacheHeight}) {
return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight);
};
setUpAll(() {
TestRenderingFlutterBinding(); // initializes the imageCache
});
TestRenderingFlutterBinding();
FlutterExceptionHandler oldError;
setUp(() {
@ -39,485 +27,98 @@ void main() {
PaintingBinding.instance.imageCache.clearLiveImages();
});
group('ImageProvider', () {
group('Image cache', () {
tearDown(() {
imageCache.clear();
});
test('obtainKey errors will be caught', () async {
final ImageProvider imageProvider = ObtainKeyErrorImageProvider();
final Completer<bool> caughtError = Completer<bool>();
FlutterError.onError = (FlutterErrorDetails details) {
caughtError.complete(false);
};
final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
stream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
caughtError.complete(false);
}, onError: (dynamic error, StackTrace stackTrace) {
caughtError.complete(true);
}));
expect(await caughtError.future, true);
});
test('AssetImageProvider - evicts on failure to load', () async {
final Completer<FlutterError> error = Completer<FlutterError>();
FlutterError.onError = (FlutterErrorDetails details) {
error.complete(details.exception as FlutterError);
};
test('obtainKey errors will be caught - check location', () async {
final ImageProvider imageProvider = ObtainKeyErrorImageProvider();
final Completer<bool> caughtError = Completer<bool>();
FlutterError.onError = (FlutterErrorDetails details) {
caughtError.complete(true);
};
await imageProvider.obtainCacheStatus(configuration: ImageConfiguration.empty);
const ImageProvider provider = ExactAssetImage('does-not-exist');
final Object key = await provider.obtainKey(ImageConfiguration.empty);
expect(imageCache.statusForKey(provider).untracked, true);
expect(imageCache.pendingImageCount, 0);
expect(await caughtError.future, true);
});
provider.resolve(ImageConfiguration.empty);
expect(imageCache.statusForKey(key).pending, true);
expect(imageCache.pendingImageCount, 1);
await error.future;
expect(imageCache.statusForKey(provider).untracked, true);
expect(imageCache.pendingImageCount, 0);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/56314
test('AssetImageProvider - evicts on null load', () async {
final Completer<StateError> error = Completer<StateError>();
FlutterError.onError = (FlutterErrorDetails details) {
error.complete(details.exception as StateError);
};
final ImageProvider provider = ExactAssetImage('does-not-exist', bundle: TestAssetBundle());
final Object key = await provider.obtainKey(ImageConfiguration.empty);
expect(imageCache.statusForKey(provider).untracked, true);
expect(imageCache.pendingImageCount, 0);
provider.resolve(ImageConfiguration.empty);
expect(imageCache.statusForKey(key).pending, true);
expect(imageCache.pendingImageCount, 1);
await error.future;
expect(imageCache.statusForKey(provider).untracked, true);
expect(imageCache.pendingImageCount, 0);
});
test('ImageProvider can evict images', () async {
final Uint8List bytes = Uint8List.fromList(kTransparentImage);
final MemoryImage imageProvider = MemoryImage(bytes);
final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
final Completer<void> completer = Completer<void>();
stream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) => completer.complete()));
await completer.future;
expect(imageCache.currentSize, 1);
expect(await MemoryImage(bytes).evict(), true);
expect(imageCache.currentSize, 0);
});
test('ImageProvider.evict respects the provided ImageCache', () async {
final ImageCache otherCache = ImageCache();
final Uint8List bytes = Uint8List.fromList(kTransparentImage);
final MemoryImage imageProvider = MemoryImage(bytes);
final ImageStreamCompleter cacheStream = otherCache.putIfAbsent(
imageProvider, () => imageProvider.load(imageProvider, basicDecoder),
);
final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
final Completer<void> completer = Completer<void>();
final Completer<void> cacheCompleter = Completer<void>();
stream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
completer.complete();
}));
cacheStream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
cacheCompleter.complete();
}));
await Future.wait(<Future<void>>[completer.future, cacheCompleter.future]);
expect(otherCache.currentSize, 1);
expect(imageCache.currentSize, 1);
expect(await imageProvider.evict(cache: otherCache), true);
expect(otherCache.currentSize, 0);
expect(imageCache.currentSize, 1);
});
test('ImageProvider errors can always be caught', () async {
final ErrorImageProvider imageProvider = ErrorImageProvider();
final Completer<bool> caughtError = Completer<bool>();
FlutterError.onError = (FlutterErrorDetails details) {
caughtError.complete(false);
};
final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
stream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
caughtError.complete(false);
}, onError: (dynamic error, StackTrace stackTrace) {
caughtError.complete(true);
}));
expect(await caughtError.future, true);
});
});
test('obtainKey errors will be caught', () async {
final ImageProvider imageProvider = ObtainKeyErrorImageProvider();
test('resolve sync errors will be caught', () async {
bool uncaught = false;
final Zone testZone = Zone.current.fork(specification: ZoneSpecification(
handleUncaughtError: (Zone zone, ZoneDelegate zoneDelegate, Zone parent, Object error, StackTrace stackTrace) {
uncaught = true;
},
));
await testZone.run(() async {
final ImageProvider imageProvider = LoadErrorImageProvider();
final Completer<bool> caughtError = Completer<bool>();
FlutterError.onError = (FlutterErrorDetails details) {
caughtError.complete(false);
throw Error();
};
final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
stream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
caughtError.complete(false);
final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
result.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
}, onError: (dynamic error, StackTrace stackTrace) {
caughtError.complete(true);
}));
expect(await caughtError.future, true);
});
expect(uncaught, false);
});
test('obtainKey errors will be caught - check location', () async {
final ImageProvider imageProvider = ObtainKeyErrorImageProvider();
test('resolve errors in the completer will be caught', () async {
bool uncaught = false;
final Zone testZone = Zone.current.fork(specification: ZoneSpecification(
handleUncaughtError: (Zone zone, ZoneDelegate zoneDelegate, Zone parent, Object error, StackTrace stackTrace) {
uncaught = true;
},
));
await testZone.run(() async {
final ImageProvider imageProvider = LoadErrorCompleterImageProvider();
final Completer<bool> caughtError = Completer<bool>();
FlutterError.onError = (FlutterErrorDetails details) {
caughtError.complete(true);
throw Error();
};
await imageProvider.obtainCacheStatus(configuration: ImageConfiguration.empty);
final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
result.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
}, onError: (dynamic error, StackTrace stackTrace) {
caughtError.complete(true);
}));
expect(await caughtError.future, true);
});
test('resolve sync errors will be caught', () async {
bool uncaught = false;
final Zone testZone = Zone.current.fork(specification: ZoneSpecification(
handleUncaughtError: (Zone zone, ZoneDelegate zoneDelegate, Zone parent, Object error, StackTrace stackTrace) {
uncaught = true;
},
));
await testZone.run(() async {
final ImageProvider imageProvider = LoadErrorImageProvider();
final Completer<bool> caughtError = Completer<bool>();
FlutterError.onError = (FlutterErrorDetails details) {
throw Error();
};
final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
result.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
}, onError: (dynamic error, StackTrace stackTrace) {
caughtError.complete(true);
}));
expect(await caughtError.future, true);
});
expect(uncaught, false);
});
test('resolve errors in the completer will be caught', () async {
bool uncaught = false;
final Zone testZone = Zone.current.fork(specification: ZoneSpecification(
handleUncaughtError: (Zone zone, ZoneDelegate zoneDelegate, Zone parent, Object error, StackTrace stackTrace) {
uncaught = true;
},
));
await testZone.run(() async {
final ImageProvider imageProvider = LoadErrorCompleterImageProvider();
final Completer<bool> caughtError = Completer<bool>();
FlutterError.onError = (FlutterErrorDetails details) {
throw Error();
};
final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
result.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
}, onError: (dynamic error, StackTrace stackTrace) {
caughtError.complete(true);
}));
expect(await caughtError.future, true);
});
expect(uncaught, false);
});
test('File image with empty file throws expected error and evicts from cache', () async {
final Completer<StateError> error = Completer<StateError>();
FlutterError.onError = (FlutterErrorDetails details) {
error.complete(details.exception as StateError);
};
final MemoryFileSystem fs = MemoryFileSystem();
final File file = fs.file('/empty.png')..createSync(recursive: true);
final FileImage provider = FileImage(file);
expect(imageCache.statusForKey(provider).untracked, true);
expect(imageCache.pendingImageCount, 0);
provider.resolve(ImageConfiguration.empty);
expect(imageCache.statusForKey(provider).pending, true);
expect(imageCache.pendingImageCount, 1);
expect(await error.future, isStateError);
expect(imageCache.statusForKey(provider).untracked, true);
expect(imageCache.pendingImageCount, 0);
});
group('NetworkImage', () {
MockHttpClient httpClient;
setUp(() {
httpClient = MockHttpClient();
debugNetworkImageHttpClientProvider = () => httpClient;
});
tearDown(() {
debugNetworkImageHttpClientProvider = null;
});
test('Expect thrown exception with statusCode - evicts from cache', () async {
final int errorStatusCode = HttpStatus.notFound;
const String requestUrl = 'foo-url';
final MockHttpClientRequest request = MockHttpClientRequest();
final MockHttpClientResponse response = MockHttpClientResponse();
when(httpClient.getUrl(any)).thenAnswer((_) => Future<HttpClientRequest>.value(request));
when(request.close()).thenAnswer((_) => Future<HttpClientResponse>.value(response));
when(response.statusCode).thenReturn(errorStatusCode);
final Completer<dynamic> caughtError = Completer<dynamic>();
final ImageProvider imageProvider = NetworkImage(nonconst(requestUrl));
expect(imageCache.pendingImageCount, 0);
expect(imageCache.statusForKey(imageProvider).untracked, true);
final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
expect(imageCache.pendingImageCount, 1);
expect(imageCache.statusForKey(imageProvider).pending, true);
result.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
}, onError: (dynamic error, StackTrace stackTrace) {
caughtError.complete(error);
}));
final dynamic err = await caughtError.future;
expect(imageCache.pendingImageCount, 0);
expect(imageCache.statusForKey(imageProvider).untracked, true);
expect(
err,
isA<NetworkImageLoadException>()
.having((NetworkImageLoadException e) => e.statusCode, 'statusCode', errorStatusCode)
.having((NetworkImageLoadException e) => e.uri, 'uri', Uri.base.resolve(requestUrl)),
);
}, skip: isBrowser); // Browser implementation does not use HTTP client but an <img> tag.
test('Disallows null urls', () {
expect(() {
NetworkImage(nonconst(null));
}, throwsAssertionError);
});
test('Uses the HttpClient provided by debugNetworkImageHttpClientProvider if set', () async {
when(httpClient.getUrl(any)).thenThrow('client1');
final List<dynamic> capturedErrors = <dynamic>[];
Future<void> loadNetworkImage() async {
final NetworkImage networkImage = NetworkImage(nonconst('foo'));
final ImageStreamCompleter completer = networkImage.load(networkImage, basicDecoder);
completer.addListener(ImageStreamListener(
(ImageInfo image, bool synchronousCall) { },
onError: (dynamic error, StackTrace stackTrace) {
capturedErrors.add(error);
},
));
await Future<void>.value();
}
await loadNetworkImage();
expect(capturedErrors, <dynamic>['client1']);
final MockHttpClient client2 = MockHttpClient();
when(client2.getUrl(any)).thenThrow('client2');
debugNetworkImageHttpClientProvider = () => client2;
await loadNetworkImage();
expect(capturedErrors, <dynamic>['client1', 'client2']);
}, skip: isBrowser); // Browser implementation does not use HTTP client but an <img> tag.
test('Propagates http client errors during resolve()', () async {
when(httpClient.getUrl(any)).thenThrow(Error());
bool uncaught = false;
await runZoned(() async {
const ImageProvider imageProvider = NetworkImage('asdasdasdas');
final Completer<bool> caughtError = Completer<bool>();
FlutterError.onError = (FlutterErrorDetails details) {
throw Error();
};
final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
result.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
}, onError: (dynamic error, StackTrace stackTrace) {
caughtError.complete(true);
}));
expect(await caughtError.future, true);
}, zoneSpecification: ZoneSpecification(
handleUncaughtError: (Zone zone, ZoneDelegate zoneDelegate, Zone parent, Object error, StackTrace stackTrace) {
uncaught = true;
},
));
expect(uncaught, false);
});
test('Notifies listeners of chunk events', () async {
const int chunkSize = 8;
final List<Uint8List> chunks = <Uint8List>[
for (int offset = 0; offset < kTransparentImage.length; offset += chunkSize)
Uint8List.fromList(kTransparentImage.skip(offset).take(chunkSize).toList()),
];
final Completer<void> imageAvailable = Completer<void>();
final MockHttpClientRequest request = MockHttpClientRequest();
final MockHttpClientResponse response = MockHttpClientResponse();
when(httpClient.getUrl(any)).thenAnswer((_) => Future<HttpClientRequest>.value(request));
when(request.close()).thenAnswer((_) => Future<HttpClientResponse>.value(response));
when(response.statusCode).thenReturn(HttpStatus.ok);
when(response.contentLength).thenReturn(kTransparentImage.length);
when(response.listen(
any,
onDone: anyNamed('onDone'),
onError: anyNamed('onError'),
cancelOnError: anyNamed('cancelOnError'),
)).thenAnswer((Invocation invocation) {
final void Function(List<int>) onData = invocation.positionalArguments[0] as void Function(List<int>);
final void Function(Object) onError = invocation.namedArguments[#onError] as void Function(Object);
final VoidCallback onDone = invocation.namedArguments[#onDone] as VoidCallback;
final bool cancelOnError = invocation.namedArguments[#cancelOnError] as bool;
return Stream<Uint8List>.fromIterable(chunks).listen(
onData,
onDone: onDone,
onError: onError,
cancelOnError: cancelOnError,
);
});
final ImageProvider imageProvider = NetworkImage(nonconst('foo'));
final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
final List<ImageChunkEvent> events = <ImageChunkEvent>[];
result.addListener(ImageStreamListener(
(ImageInfo image, bool synchronousCall) {
imageAvailable.complete();
},
onChunk: (ImageChunkEvent event) {
events.add(event);
},
onError: (dynamic error, StackTrace stackTrace) {
imageAvailable.completeError(error, stackTrace);
},
));
await imageAvailable.future;
expect(events.length, chunks.length);
for (int i = 0; i < events.length; i++) {
expect(events[i].cumulativeBytesLoaded, math.min((i + 1) * chunkSize, kTransparentImage.length));
expect(events[i].expectedTotalBytes, kTransparentImage.length);
}
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/56317
test('NetworkImage is evicted from cache on SocketException', () async {
final MockHttpClient mockHttpClient = MockHttpClient();
when(mockHttpClient.getUrl(any)).thenAnswer((_) => throw const SocketException('test exception'));
debugNetworkImageHttpClientProvider = () => mockHttpClient;
final ImageProvider imageProvider = NetworkImage(nonconst('testing.url'));
expect(imageCache.pendingImageCount, 0);
expect(imageCache.statusForKey(imageProvider).untracked, true);
final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
expect(imageCache.pendingImageCount, 1);
expect(imageCache.statusForKey(imageProvider).pending, true);
final Completer<dynamic> caughtError = Completer<dynamic>();
result.addListener(ImageStreamListener(
(ImageInfo info, bool syncCall) {},
onError: (dynamic error, StackTrace stackTrace) {
caughtError.complete(error);
},
));
final dynamic err = await caughtError.future;
expect(err, isA<SocketException>());
expect(imageCache.pendingImageCount, 0);
expect(imageCache.statusForKey(imageProvider).untracked, true);
expect(imageCache.containsKey(result), isFalse);
debugNetworkImageHttpClientProvider = null;
}, skip: isBrowser); // Browser does not resolve images this way.
});
expect(uncaught, false);
});
test('ResizeImage resizes to the correct dimensions', () async {
final Uint8List bytes = Uint8List.fromList(kTransparentImage);
final MemoryImage imageProvider = MemoryImage(bytes);
final Size rawImageSize = await _resolveAndGetSize(imageProvider);
expect(rawImageSize, const Size(1, 1));
const Size resizeDims = Size(14, 7);
final ResizeImage resizedImage = ResizeImage(MemoryImage(bytes), width: resizeDims.width.round(), height: resizeDims.height.round());
const ImageConfiguration resizeConfig = ImageConfiguration(size: resizeDims);
final Size resizedImageSize = await _resolveAndGetSize(resizedImage, configuration: resizeConfig);
expect(resizedImageSize, resizeDims);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/56312
test('ResizeImage does not resize when no size is passed', () async {
final Uint8List bytes = Uint8List.fromList(kTransparentImage);
final MemoryImage imageProvider = MemoryImage(bytes);
final Size rawImageSize = await _resolveAndGetSize(imageProvider);
expect(rawImageSize, const Size(1, 1));
// Cannot pass in two null arguments for cache dimensions, so will use the regular
// MemoryImage
final MemoryImage resizedImage = MemoryImage(bytes);
final Size resizedImageSize = await _resolveAndGetSize(resizedImage);
expect(resizedImageSize, const Size(1, 1));
});
test('ResizeImage stores values', () async {
final Uint8List bytes = Uint8List.fromList(kTransparentImage);
final MemoryImage memoryImage = MemoryImage(bytes);
final ResizeImage resizeImage = ResizeImage(memoryImage, width: 10, height: 20);
expect(resizeImage.width, 10);
expect(resizeImage.height, 20);
expect(resizeImage.imageProvider, memoryImage);
expect(memoryImage.resolve(ImageConfiguration.empty) != resizeImage.resolve(ImageConfiguration.empty), true);
});
test('ResizeImage takes one dim', () async {
final Uint8List bytes = Uint8List.fromList(kTransparentImage);
final MemoryImage memoryImage = MemoryImage(bytes);
final ResizeImage resizeImage = ResizeImage(memoryImage, width: 10, height: null);
expect(resizeImage.width, 10);
expect(resizeImage.height, null);
expect(resizeImage.imageProvider, memoryImage);
expect(memoryImage.resolve(ImageConfiguration.empty) != resizeImage.resolve(ImageConfiguration.empty), true);
});
test('ResizeImage forms closure', () async {
final Uint8List bytes = Uint8List.fromList(kTransparentImage);
final MemoryImage memoryImage = MemoryImage(bytes);
final ResizeImage resizeImage = ResizeImage(memoryImage, width: 123, height: 321);
final DecoderCallback decode = (Uint8List bytes, {int cacheWidth, int cacheHeight}) {
expect(cacheWidth, 123);
expect(cacheHeight, 321);
return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight);
test('File image with empty file throws expected error and evicts from cache', () async {
final Completer<StateError> error = Completer<StateError>();
FlutterError.onError = (FlutterErrorDetails details) {
error.complete(details.exception as StateError);
};
final MemoryFileSystem fs = MemoryFileSystem();
final File file = fs.file('/empty.png')..createSync(recursive: true);
final FileImage provider = FileImage(file);
resizeImage.load(await resizeImage.obtainKey(ImageConfiguration.empty), decode);
});
expect(imageCache.statusForKey(provider).untracked, true);
expect(imageCache.pendingImageCount, 0);
test('ResizeImage handles sync obtainKey', () async {
final Uint8List bytes = Uint8List.fromList(kTransparentImage);
final MemoryImage memoryImage = MemoryImage(bytes);
final ResizeImage resizeImage = ResizeImage(memoryImage, width: 123, height: 321);
provider.resolve(ImageConfiguration.empty);
bool isAsync = false;
resizeImage.obtainKey(ImageConfiguration.empty).then((Object key) {
expect(isAsync, false);
});
isAsync = true;
expect(isAsync, true);
});
expect(imageCache.statusForKey(provider).pending, true);
expect(imageCache.pendingImageCount, 1);
test('ResizeImage handles async obtainKey', () async {
final Uint8List bytes = Uint8List.fromList(kTransparentImage);
final AsyncKeyMemoryImage memoryImage = AsyncKeyMemoryImage(bytes);
final ResizeImage resizeImage = ResizeImage(memoryImage, width: 123, height: 321);
bool isAsync = false;
resizeImage.obtainKey(ImageConfiguration.empty).then((Object key) {
expect(isAsync, true);
});
isAsync = true;
expect(isAsync, true);
expect(await error.future, isStateError);
expect(imageCache.statusForKey(provider).untracked, true);
expect(imageCache.pendingImageCount, 0);
});
test('File image with empty file throws expected error (load)', () async {
@ -534,40 +135,3 @@ void main() {
expect(await error.future, isStateError);
});
}
Future<Size> _resolveAndGetSize(ImageProvider imageProvider,
{ImageConfiguration configuration = ImageConfiguration.empty}) async {
final ImageStream stream = imageProvider.resolve(configuration);
final Completer<Size> completer = Completer<Size>();
final ImageStreamListener listener =
ImageStreamListener((ImageInfo image, bool synchronousCall) {
final int height = image.image.height;
final int width = image.image.width;
completer.complete(Size(width.toDouble(), height.toDouble()));
}
);
stream.addListener(listener);
return await completer.future;
}
// This version of MemoryImage guarantees obtainKey returns a future that has not been
// completed synchronously.
class AsyncKeyMemoryImage extends MemoryImage {
const AsyncKeyMemoryImage(Uint8List bytes) : super(bytes);
@override
Future<MemoryImage> obtainKey(ImageConfiguration configuration) {
return Future<MemoryImage>(() => this);
}
}
class MockHttpClient extends Mock implements HttpClient {}
class MockHttpClientRequest extends Mock implements HttpClientRequest {}
class MockHttpClientResponse extends Mock implements HttpClientResponse {}
class TestAssetBundle extends CachingAssetBundle {
@override
Future<ByteData> load(String key) async {
return null;
}
}

View file

@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
@ -19,7 +21,15 @@ class TestRenderingFlutterBinding extends BindingBase with SchedulerBinding, Ser
/// while drawing the frame. If [onErrors] is null and [FlutterError] caught at least
/// one error, this function fails the test. A test may override [onErrors] and
/// inspect errors using [takeFlutterErrorDetails].
TestRenderingFlutterBinding({ this.onErrors });
///
/// Errors caught between frames will cause the test to fail unless
/// [FlutterError.onError] has been overridden.
TestRenderingFlutterBinding({ this.onErrors }) {
FlutterError.onError = (FlutterErrorDetails details) {
FlutterError.dumpErrorToConsole(details);
Zone.current.parent.handleUncaughtError(details.exception, details.stack);
};
}
final List<FlutterErrorDetails> _errors = <FlutterErrorDetails>[];