Unverified Commit 3cb79079 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

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

parent 0a4f6cde
...@@ -286,10 +286,9 @@ class ImageCache { ...@@ -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. // Avoid adding unnecessary callbacks to the completer.
_liveImages.putIfAbsent(key, () { _liveImages.putIfAbsent(key, () {
assert(debugPutOk);
// Even if no callers to ImageProvider.resolve have listened to the stream, // 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 // the cache is listening to the stream and will remove itself once the
// image completes to move it from pending to keepAlive. // image completes to move it from pending to keepAlive.
...@@ -400,10 +399,6 @@ class ImageCache { ...@@ -400,10 +399,6 @@ class ImageCache {
imageSize, imageSize,
() => _liveImages.remove(key), () => _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); final _PendingImage pendingImage = untrackedPendingImage ?? _pendingImages.remove(key);
......
...@@ -347,10 +347,10 @@ abstract class ImageProvider<T> { ...@@ -347,10 +347,10 @@ abstract class ImageProvider<T> {
stack: stack, stack: stack,
context: ErrorDescription('while resolving an image'), context: ErrorDescription('while resolving an image'),
silent: true, // could be a network error or whatnot silent: true, // could be a network error or whatnot
informationCollector: collector informationCollector: collector,
); );
}, },
); );
return stream; return stream;
} }
......
// 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;
});
}
...@@ -9,71 +9,70 @@ import '../rendering/rendering_tester.dart'; ...@@ -9,71 +9,70 @@ import '../rendering/rendering_tester.dart';
import 'mocks_for_image_cache.dart'; import 'mocks_for_image_cache.dart';
void main() { void main() {
TestRenderingFlutterBinding(); // initializes the imageCache TestRenderingFlutterBinding();
group(ImageCache, () {
tearDown(() {
imageCache.clear();
imageCache.maximumSize = 1000;
imageCache.maximumSizeBytes = 10485760;
});
test('Image cache resizing based on count', () async { tearDown(() {
imageCache.maximumSize = 2; imageCache.clear();
imageCache.maximumSize = 1000;
imageCache.maximumSizeBytes = 10485760;
});
test('Image cache resizing based on count', () async {
imageCache.maximumSize = 2;
final TestImageInfo a = await extractOneFrame(const TestImageProvider(1, 1).resolve(ImageConfiguration.empty)) as TestImageInfo; 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 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 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; final TestImageInfo d = await extractOneFrame(const TestImageProvider(1, 4).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(a.value, equals(1)); expect(a.value, equals(1));
expect(b.value, equals(2)); expect(b.value, equals(2));
expect(c.value, equals(3)); expect(c.value, equals(3));
expect(d.value, equals(4)); expect(d.value, equals(4));
imageCache.maximumSize = 0; imageCache.maximumSize = 0;
final TestImageInfo e = await extractOneFrame(const TestImageProvider(1, 5).resolve(ImageConfiguration.empty)) as TestImageInfo; final TestImageInfo e = await extractOneFrame(const TestImageProvider(1, 5).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(e.value, equals(5)); expect(e.value, equals(5));
final TestImageInfo f = await extractOneFrame(const TestImageProvider(1, 6).resolve(ImageConfiguration.empty)) as TestImageInfo; final TestImageInfo f = await extractOneFrame(const TestImageProvider(1, 6).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(f.value, equals(6)); expect(f.value, equals(6));
imageCache.maximumSize = 3; imageCache.maximumSize = 3;
final TestImageInfo g = await extractOneFrame(const TestImageProvider(1, 7).resolve(ImageConfiguration.empty)) as TestImageInfo; final TestImageInfo g = await extractOneFrame(const TestImageProvider(1, 7).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(g.value, equals(7)); expect(g.value, equals(7));
final TestImageInfo h = await extractOneFrame(const TestImageProvider(1, 8).resolve(ImageConfiguration.empty)) as TestImageInfo; final TestImageInfo h = await extractOneFrame(const TestImageProvider(1, 8).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(h.value, equals(7)); expect(h.value, equals(7));
}); });
test('Image cache resizing based on size', () async { test('Image cache resizing based on size', () async {
const TestImage testImage = TestImage(width: 8, height: 8); // 256 B. const TestImage testImage = TestImage(width: 8, height: 8); // 256 B.
imageCache.maximumSizeBytes = 256 * 2; imageCache.maximumSizeBytes = 256 * 2;
final TestImageInfo a = await extractOneFrame(const TestImageProvider(1, 1, image: testImage).resolve(ImageConfiguration.empty)) as TestImageInfo; 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 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 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; final TestImageInfo d = await extractOneFrame(const TestImageProvider(1, 4, image: testImage).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(a.value, equals(1)); expect(a.value, equals(1));
expect(b.value, equals(2)); expect(b.value, equals(2));
expect(c.value, equals(3)); expect(c.value, equals(3));
expect(d.value, equals(4)); expect(d.value, equals(4));
imageCache.maximumSizeBytes = 0; imageCache.maximumSizeBytes = 0;
final TestImageInfo e = await extractOneFrame(const TestImageProvider(1, 5, image: testImage).resolve(ImageConfiguration.empty)) as TestImageInfo; final TestImageInfo e = await extractOneFrame(const TestImageProvider(1, 5, image: testImage).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(e.value, equals(5)); expect(e.value, equals(5));
final TestImageInfo f = await extractOneFrame(const TestImageProvider(1, 6, image: testImage).resolve(ImageConfiguration.empty)) as TestImageInfo; final TestImageInfo f = await extractOneFrame(const TestImageProvider(1, 6, image: testImage).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(f.value, equals(6)); expect(f.value, equals(6));
imageCache.maximumSizeBytes = 256 * 3; imageCache.maximumSizeBytes = 256 * 3;
final TestImageInfo g = await extractOneFrame(const TestImageProvider(1, 7, image: testImage).resolve(ImageConfiguration.empty)) as TestImageInfo; final TestImageInfo g = await extractOneFrame(const TestImageProvider(1, 7, image: testImage).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(g.value, equals(7)); expect(g.value, equals(7));
final TestImageInfo h = await extractOneFrame(const TestImageProvider(1, 8, image: testImage).resolve(ImageConfiguration.empty)) as TestImageInfo; final TestImageInfo h = await extractOneFrame(const TestImageProvider(1, 8, image: testImage).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(h.value, equals(7)); expect(h.value, equals(7));
});
}); });
} }
...@@ -9,475 +9,471 @@ import '../rendering/rendering_tester.dart'; ...@@ -9,475 +9,471 @@ import '../rendering/rendering_tester.dart';
import 'mocks_for_image_cache.dart'; import 'mocks_for_image_cache.dart';
void main() { void main() {
group('ImageCache', () { TestRenderingFlutterBinding();
setUpAll(() {
TestRenderingFlutterBinding(); // initializes the imageCache
});
tearDown(() { tearDown(() {
imageCache.clear(); imageCache.clear();
imageCache.clearLiveImages(); imageCache.clearLiveImages();
imageCache.maximumSize = 1000; imageCache.maximumSize = 1000;
imageCache.maximumSizeBytes = 10485760; imageCache.maximumSizeBytes = 10485760;
}); });
test('maintains cache size', () async { test('maintains cache size', () async {
imageCache.maximumSize = 3; imageCache.maximumSize = 3;
final TestImageInfo a = await extractOneFrame(const TestImageProvider(1, 1).resolve(ImageConfiguration.empty)) as TestImageInfo; final TestImageInfo a = await extractOneFrame(const TestImageProvider(1, 1).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(a.value, equals(1)); expect(a.value, equals(1));
final TestImageInfo b = await extractOneFrame(const TestImageProvider(1, 2).resolve(ImageConfiguration.empty)) as TestImageInfo; final TestImageInfo b = await extractOneFrame(const TestImageProvider(1, 2).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(b.value, equals(1)); expect(b.value, equals(1));
final TestImageInfo c = await extractOneFrame(const TestImageProvider(1, 3).resolve(ImageConfiguration.empty)) as TestImageInfo; final TestImageInfo c = await extractOneFrame(const TestImageProvider(1, 3).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(c.value, equals(1)); expect(c.value, equals(1));
final TestImageInfo d = await extractOneFrame(const TestImageProvider(1, 4).resolve(ImageConfiguration.empty)) as TestImageInfo; final TestImageInfo d = await extractOneFrame(const TestImageProvider(1, 4).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(d.value, equals(1)); expect(d.value, equals(1));
final TestImageInfo e = await extractOneFrame(const TestImageProvider(1, 5).resolve(ImageConfiguration.empty)) as TestImageInfo; final TestImageInfo e = await extractOneFrame(const TestImageProvider(1, 5).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(e.value, equals(1)); expect(e.value, equals(1));
final TestImageInfo f = await extractOneFrame(const TestImageProvider(1, 6).resolve(ImageConfiguration.empty)) as TestImageInfo; final TestImageInfo f = await extractOneFrame(const TestImageProvider(1, 6).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(f.value, equals(1)); expect(f.value, equals(1));
expect(f, equals(a)); expect(f, equals(a));
// cache still only has one entry in it: 1(1) // cache still only has one entry in it: 1(1)
final TestImageInfo g = await extractOneFrame(const TestImageProvider(2, 7).resolve(ImageConfiguration.empty)) as TestImageInfo; final TestImageInfo g = await extractOneFrame(const TestImageProvider(2, 7).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(g.value, equals(7)); expect(g.value, equals(7));
// cache has two entries in it: 1(1), 2(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; final TestImageInfo h = await extractOneFrame(const TestImageProvider(1, 8).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(h.value, equals(1)); expect(h.value, equals(1));
// cache still has two entries in it: 2(7), 1(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; final TestImageInfo i = await extractOneFrame(const TestImageProvider(3, 9).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(i.value, equals(9)); expect(i.value, equals(9));
// cache has three entries in it: 2(7), 1(1), 3(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; final TestImageInfo j = await extractOneFrame(const TestImageProvider(1, 10).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(j.value, equals(1)); expect(j.value, equals(1));
// cache still has three entries in it: 2(7), 3(9), 1(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; final TestImageInfo k = await extractOneFrame(const TestImageProvider(4, 11).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(k.value, equals(11)); expect(k.value, equals(11));
// cache has three entries: 3(9), 1(1), 4(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; final TestImageInfo l = await extractOneFrame(const TestImageProvider(1, 12).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(l.value, equals(1)); expect(l.value, equals(1));
// cache has three entries: 3(9), 4(11), 1(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; final TestImageInfo m = await extractOneFrame(const TestImageProvider(2, 13).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(m.value, equals(13)); expect(m.value, equals(13));
// cache has three entries: 4(11), 1(1), 2(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; final TestImageInfo n = await extractOneFrame(const TestImageProvider(3, 14).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(n.value, equals(14)); expect(n.value, equals(14));
// cache has three entries: 1(1), 2(13), 3(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; final TestImageInfo o = await extractOneFrame(const TestImageProvider(4, 15).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(o.value, equals(15)); expect(o.value, equals(15));
// cache has three entries: 2(13), 3(14), 4(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; final TestImageInfo p = await extractOneFrame(const TestImageProvider(1, 16).resolve(ImageConfiguration.empty)) as TestImageInfo;
expect(p.value, equals(16)); expect(p.value, equals(16));
// cache has three entries: 3(14), 4(15), 1(16) // cache has three entries: 3(14), 4(15), 1(16)
}); });
test('clear removes all images and resets cache size', () async { test('clear removes all images and resets cache size', () async {
const TestImage testImage = TestImage(width: 8, height: 8); const TestImage testImage = TestImage(width: 8, height: 8);
expect(imageCache.currentSize, 0); expect(imageCache.currentSize, 0);
expect(imageCache.currentSizeBytes, 0); expect(imageCache.currentSizeBytes, 0);
await extractOneFrame(const TestImageProvider(1, 1, image: testImage).resolve(ImageConfiguration.empty)); await extractOneFrame(const TestImageProvider(1, 1, image: testImage).resolve(ImageConfiguration.empty));
await extractOneFrame(const TestImageProvider(2, 2, image: testImage).resolve(ImageConfiguration.empty)); await extractOneFrame(const TestImageProvider(2, 2, image: testImage).resolve(ImageConfiguration.empty));
expect(imageCache.currentSize, 2); expect(imageCache.currentSize, 2);
expect(imageCache.currentSizeBytes, 256 * 2); expect(imageCache.currentSizeBytes, 256 * 2);
imageCache.clear(); imageCache.clear();
expect(imageCache.currentSize, 0); expect(imageCache.currentSize, 0);
expect(imageCache.currentSizeBytes, 0); expect(imageCache.currentSizeBytes, 0);
}); });
test('evicts individual images', () async { test('evicts individual images', () async {
const TestImage testImage = TestImage(width: 8, height: 8); const TestImage testImage = TestImage(width: 8, height: 8);
await extractOneFrame(const TestImageProvider(1, 1, image: testImage).resolve(ImageConfiguration.empty)); await extractOneFrame(const TestImageProvider(1, 1, image: testImage).resolve(ImageConfiguration.empty));
await extractOneFrame(const TestImageProvider(2, 2, image: testImage).resolve(ImageConfiguration.empty)); await extractOneFrame(const TestImageProvider(2, 2, image: testImage).resolve(ImageConfiguration.empty));
expect(imageCache.currentSize, 2); expect(imageCache.currentSize, 2);
expect(imageCache.currentSizeBytes, 256 * 2); expect(imageCache.currentSizeBytes, 256 * 2);
expect(imageCache.evict(1), true); expect(imageCache.evict(1), true);
expect(imageCache.currentSize, 1); expect(imageCache.currentSize, 1);
expect(imageCache.currentSizeBytes, 256); expect(imageCache.currentSizeBytes, 256);
}); });
test('Do not cache large images', () async { test('Do not cache large images', () async {
const TestImage testImage = TestImage(width: 8, height: 8); const TestImage testImage = TestImage(width: 8, height: 8);
imageCache.maximumSizeBytes = 1; imageCache.maximumSizeBytes = 1;
await extractOneFrame(const TestImageProvider(1, 1, image: testImage).resolve(ImageConfiguration.empty)); await extractOneFrame(const TestImageProvider(1, 1, image: testImage).resolve(ImageConfiguration.empty));
expect(imageCache.currentSize, 0); expect(imageCache.currentSize, 0);
expect(imageCache.currentSizeBytes, 0); expect(imageCache.currentSizeBytes, 0);
expect(imageCache.maximumSizeBytes, 1); expect(imageCache.maximumSizeBytes, 1);
}); });
test('Returns null if an error is caught resolving an image', () { test('Returns null if an error is caught resolving an image', () {
final ErrorImageProvider errorImage = ErrorImageProvider(); final ErrorImageProvider errorImage = ErrorImageProvider();
expect(() => imageCache.putIfAbsent(errorImage, () => errorImage.load(errorImage, null)), throwsA(isA<Error>())); expect(() => imageCache.putIfAbsent(errorImage, () => errorImage.load(errorImage, null)), throwsA(isA<Error>()));
bool caughtError = false; bool caughtError = false;
final ImageStreamCompleter result = imageCache.putIfAbsent(errorImage, () => errorImage.load(errorImage, null), onError: (dynamic error, StackTrace stackTrace) { final ImageStreamCompleter result = imageCache.putIfAbsent(errorImage, () => errorImage.load(errorImage, null), onError: (dynamic error, StackTrace stackTrace) {
caughtError = true; caughtError = true;
});
expect(result, null);
expect(caughtError, true);
}); });
expect(result, null);
expect(caughtError, true);
});
test('already pending image is returned when it is put into the cache again', () async { test('already pending image is returned when it is put into the cache again', () async {
const TestImage testImage = TestImage(width: 8, height: 8); const TestImage testImage = TestImage(width: 8, height: 8);
final TestImageStreamCompleter completer1 = TestImageStreamCompleter(); final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
final TestImageStreamCompleter completer2 = TestImageStreamCompleter(); final TestImageStreamCompleter completer2 = TestImageStreamCompleter();
final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () { final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () {
return completer1; return completer1;
}) as TestImageStreamCompleter; }) as TestImageStreamCompleter;
final TestImageStreamCompleter resultingCompleter2 = imageCache.putIfAbsent(testImage, () { final TestImageStreamCompleter resultingCompleter2 = imageCache.putIfAbsent(testImage, () {
return completer2; return completer2;
}) as TestImageStreamCompleter; }) as TestImageStreamCompleter;
expect(resultingCompleter1, completer1); expect(resultingCompleter1, completer1);
expect(resultingCompleter2, completer1); expect(resultingCompleter2, completer1);
}); });
test('pending image is removed when cache is cleared', () async { test('pending image is removed when cache is cleared', () async {
const TestImage testImage = TestImage(width: 8, height: 8); const TestImage testImage = TestImage(width: 8, height: 8);
final TestImageStreamCompleter completer1 = TestImageStreamCompleter(); final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
final TestImageStreamCompleter completer2 = TestImageStreamCompleter(); final TestImageStreamCompleter completer2 = TestImageStreamCompleter();
final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () { final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () {
return completer1; return completer1;
}) as TestImageStreamCompleter; }) as TestImageStreamCompleter;
expect(imageCache.statusForKey(testImage).pending, true); expect(imageCache.statusForKey(testImage).pending, true);
expect(imageCache.statusForKey(testImage).live, true); expect(imageCache.statusForKey(testImage).live, true);
imageCache.clear(); imageCache.clear();
expect(imageCache.statusForKey(testImage).pending, false); expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true); expect(imageCache.statusForKey(testImage).live, true);
imageCache.clearLiveImages(); imageCache.clearLiveImages();
expect(imageCache.statusForKey(testImage).pending, false); expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, false); expect(imageCache.statusForKey(testImage).live, false);
final TestImageStreamCompleter resultingCompleter2 = imageCache.putIfAbsent(testImage, () { final TestImageStreamCompleter resultingCompleter2 = imageCache.putIfAbsent(testImage, () {
return completer2; return completer2;
}) as TestImageStreamCompleter; }) as TestImageStreamCompleter;
expect(resultingCompleter1, completer1); expect(resultingCompleter1, completer1);
expect(resultingCompleter2, completer2); expect(resultingCompleter2, completer2);
}); });
test('pending image is removed when image is evicted', () async { test('pending image is removed when image is evicted', () async {
const TestImage testImage = TestImage(width: 8, height: 8); const TestImage testImage = TestImage(width: 8, height: 8);
final TestImageStreamCompleter completer1 = TestImageStreamCompleter(); final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
final TestImageStreamCompleter completer2 = TestImageStreamCompleter(); final TestImageStreamCompleter completer2 = TestImageStreamCompleter();
final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () { final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () {
return completer1; return completer1;
}) as TestImageStreamCompleter; }) as TestImageStreamCompleter;
imageCache.evict(testImage); imageCache.evict(testImage);
final TestImageStreamCompleter resultingCompleter2 = imageCache.putIfAbsent(testImage, () { final TestImageStreamCompleter resultingCompleter2 = imageCache.putIfAbsent(testImage, () {
return completer2; return completer2;
}) as TestImageStreamCompleter; }) as TestImageStreamCompleter;
expect(resultingCompleter1, completer1); expect(resultingCompleter1, completer1);
expect(resultingCompleter2, completer2); expect(resultingCompleter2, completer2);
}); });
test("failed image can successfully be removed from the cache's pending images", () async { test("failed image can successfully be removed from the cache's pending images", () async {
const TestImage testImage = TestImage(width: 8, height: 8); const TestImage testImage = TestImage(width: 8, height: 8);
const FailingTestImageProvider(1, 1, image: testImage) const FailingTestImageProvider(1, 1, image: testImage)
.resolve(ImageConfiguration.empty) .resolve(ImageConfiguration.empty)
.addListener(ImageStreamListener( .addListener(ImageStreamListener(
(ImageInfo image, bool synchronousCall) { }, (ImageInfo image, bool synchronousCall) { },
onError: (dynamic exception, StackTrace stackTrace) { onError: (dynamic exception, StackTrace stackTrace) {
final bool evicationResult = imageCache.evict(1); final bool evicationResult = imageCache.evict(1);
expect(evicationResult, isTrue); expect(evicationResult, isTrue);
}, },
)); ));
}); });
test('containsKey - pending', () async { test('containsKey - pending', () async {
const TestImage testImage = TestImage(width: 8, height: 8); const TestImage testImage = TestImage(width: 8, height: 8);
final TestImageStreamCompleter completer1 = TestImageStreamCompleter(); final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () { final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () {
return completer1; return completer1;
}) as TestImageStreamCompleter; }) as TestImageStreamCompleter;
expect(resultingCompleter1, completer1); expect(resultingCompleter1, completer1);
expect(imageCache.containsKey(testImage), true); expect(imageCache.containsKey(testImage), true);
}); });
test('containsKey - completed', () async { test('containsKey - completed', () async {
const TestImage testImage = TestImage(width: 8, height: 8); const TestImage testImage = TestImage(width: 8, height: 8);
final TestImageStreamCompleter completer1 = TestImageStreamCompleter(); final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () { final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () {
return completer1; return completer1;
}) as TestImageStreamCompleter; }) as TestImageStreamCompleter;
// Mark as complete // Mark as complete
completer1.testSetImage(testImage); completer1.testSetImage(testImage);
expect(resultingCompleter1, completer1); expect(resultingCompleter1, completer1);
expect(imageCache.containsKey(testImage), true); expect(imageCache.containsKey(testImage), true);
}); });
test('putIfAbsent updates LRU properties of a live image', () async { test('putIfAbsent updates LRU properties of a live image', () async {
imageCache.maximumSize = 1; imageCache.maximumSize = 1;
const TestImage testImage = TestImage(width: 8, height: 8); const TestImage testImage = TestImage(width: 8, height: 8);
const TestImage testImage2 = TestImage(width: 10, height: 10); const TestImage testImage2 = TestImage(width: 10, height: 10);
final TestImageStreamCompleter completer1 = TestImageStreamCompleter()..testSetImage(testImage); final TestImageStreamCompleter completer1 = TestImageStreamCompleter()..testSetImage(testImage);
final TestImageStreamCompleter completer2 = TestImageStreamCompleter()..testSetImage(testImage2); final TestImageStreamCompleter completer2 = TestImageStreamCompleter()..testSetImage(testImage2);
completer1.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {})); completer1.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {}));
final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () { final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () {
return completer1; return completer1;
}) as TestImageStreamCompleter; }) as TestImageStreamCompleter;
expect(imageCache.statusForKey(testImage).pending, false); expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).keepAlive, true); expect(imageCache.statusForKey(testImage).keepAlive, true);
expect(imageCache.statusForKey(testImage).live, true); expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage2).untracked, true); expect(imageCache.statusForKey(testImage2).untracked, true);
final TestImageStreamCompleter resultingCompleter2 = imageCache.putIfAbsent(testImage2, () { final TestImageStreamCompleter resultingCompleter2 = imageCache.putIfAbsent(testImage2, () {
return completer2; return completer2;
}) as TestImageStreamCompleter; }) as TestImageStreamCompleter;
expect(imageCache.statusForKey(testImage).pending, false); expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).keepAlive, false); // evicted expect(imageCache.statusForKey(testImage).keepAlive, false); // evicted
expect(imageCache.statusForKey(testImage).live, true); expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage2).pending, false); expect(imageCache.statusForKey(testImage2).pending, false);
expect(imageCache.statusForKey(testImage2).keepAlive, true); // took the LRU spot. expect(imageCache.statusForKey(testImage2).keepAlive, true); // took the LRU spot.
expect(imageCache.statusForKey(testImage2).live, false); // no listeners expect(imageCache.statusForKey(testImage2).live, false); // no listeners
expect(resultingCompleter1, completer1); expect(resultingCompleter1, completer1);
expect(resultingCompleter2, completer2); expect(resultingCompleter2, completer2);
}); });
test('Live image cache avoids leaks of unlistened streams', () async { test('Live image cache avoids leaks of unlistened streams', () async {
imageCache.maximumSize = 3; imageCache.maximumSize = 3;
const TestImageProvider(1, 1).resolve(ImageConfiguration.empty); const TestImageProvider(1, 1).resolve(ImageConfiguration.empty);
const TestImageProvider(2, 2).resolve(ImageConfiguration.empty); const TestImageProvider(2, 2).resolve(ImageConfiguration.empty);
const TestImageProvider(3, 3).resolve(ImageConfiguration.empty); const TestImageProvider(3, 3).resolve(ImageConfiguration.empty);
const TestImageProvider(4, 4).resolve(ImageConfiguration.empty); const TestImageProvider(4, 4).resolve(ImageConfiguration.empty);
const TestImageProvider(5, 5).resolve(ImageConfiguration.empty); const TestImageProvider(5, 5).resolve(ImageConfiguration.empty);
const TestImageProvider(6, 6).resolve(ImageConfiguration.empty); const TestImageProvider(6, 6).resolve(ImageConfiguration.empty);
// wait an event loop to let image resolution process. // wait an event loop to let image resolution process.
await null; await null;
expect(imageCache.currentSize, 3); expect(imageCache.currentSize, 3);
expect(imageCache.liveImageCount, 0); expect(imageCache.liveImageCount, 0);
}); });
test('Disabled image cache does not leak live images', () async { test('Disabled image cache does not leak live images', () async {
imageCache.maximumSize = 0; imageCache.maximumSize = 0;
const TestImageProvider(1, 1).resolve(ImageConfiguration.empty); const TestImageProvider(1, 1).resolve(ImageConfiguration.empty);
const TestImageProvider(2, 2).resolve(ImageConfiguration.empty); const TestImageProvider(2, 2).resolve(ImageConfiguration.empty);
const TestImageProvider(3, 3).resolve(ImageConfiguration.empty); const TestImageProvider(3, 3).resolve(ImageConfiguration.empty);
const TestImageProvider(4, 4).resolve(ImageConfiguration.empty); const TestImageProvider(4, 4).resolve(ImageConfiguration.empty);
const TestImageProvider(5, 5).resolve(ImageConfiguration.empty); const TestImageProvider(5, 5).resolve(ImageConfiguration.empty);
const TestImageProvider(6, 6).resolve(ImageConfiguration.empty); const TestImageProvider(6, 6).resolve(ImageConfiguration.empty);
// wait an event loop to let image resolution process. // wait an event loop to let image resolution process.
await null; await null;
expect(imageCache.currentSize, 0); expect(imageCache.currentSize, 0);
expect(imageCache.liveImageCount, 0); expect(imageCache.liveImageCount, 0);
}); });
test('Evicting a pending image clears the live image by default', () async { test('Evicting a pending image clears the live image by default', () async {
const TestImage testImage = TestImage(width: 8, height: 8); const TestImage testImage = TestImage(width: 8, height: 8);
final TestImageStreamCompleter completer1 = TestImageStreamCompleter(); final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
imageCache.putIfAbsent(testImage, () => completer1); imageCache.putIfAbsent(testImage, () => completer1);
expect(imageCache.statusForKey(testImage).pending, true); expect(imageCache.statusForKey(testImage).pending, true);
expect(imageCache.statusForKey(testImage).live, true); expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, false); expect(imageCache.statusForKey(testImage).keepAlive, false);
imageCache.evict(testImage); imageCache.evict(testImage);
expect(imageCache.statusForKey(testImage).untracked, true); 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 { 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); const TestImage testImage = TestImage(width: 8, height: 8);
final TestImageStreamCompleter completer1 = TestImageStreamCompleter(); final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
imageCache.putIfAbsent(testImage, () => completer1); imageCache.putIfAbsent(testImage, () => completer1);
expect(imageCache.statusForKey(testImage).pending, true); expect(imageCache.statusForKey(testImage).pending, true);
expect(imageCache.statusForKey(testImage).live, true); expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, false); expect(imageCache.statusForKey(testImage).keepAlive, false);
imageCache.evict(testImage, includeLive: false); imageCache.evict(testImage, includeLive: false);
expect(imageCache.statusForKey(testImage).pending, false); expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, false); expect(imageCache.statusForKey(testImage).live, false);
expect(imageCache.statusForKey(testImage).keepAlive, 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 { 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); const TestImage testImage = TestImage(width: 8, height: 8);
final TestImageStreamCompleter completer1 = TestImageStreamCompleter(); final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
imageCache.putIfAbsent(testImage, () => completer1); imageCache.putIfAbsent(testImage, () => completer1);
expect(imageCache.statusForKey(testImage).pending, true); expect(imageCache.statusForKey(testImage).pending, true);
expect(imageCache.statusForKey(testImage).live, true); expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, false); expect(imageCache.statusForKey(testImage).keepAlive, false);
completer1.addListener(ImageStreamListener((_, __) {})); completer1.addListener(ImageStreamListener((_, __) {}));
imageCache.evict(testImage, includeLive: false); imageCache.evict(testImage, includeLive: false);
expect(imageCache.statusForKey(testImage).pending, false); expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true); expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, false); expect(imageCache.statusForKey(testImage).keepAlive, false);
}); });
test('Evicting a completed image does clear the live image by default', () async { test('Evicting a completed image does clear the live image by default', () async {
const TestImage testImage = TestImage(width: 8, height: 8); const TestImage testImage = TestImage(width: 8, height: 8);
final TestImageStreamCompleter completer1 = TestImageStreamCompleter() final TestImageStreamCompleter completer1 = TestImageStreamCompleter()
..testSetImage(testImage) ..testSetImage(testImage)
..addListener(ImageStreamListener((ImageInfo info, bool syncCall) {})); ..addListener(ImageStreamListener((ImageInfo info, bool syncCall) {}));
imageCache.putIfAbsent(testImage, () => completer1); imageCache.putIfAbsent(testImage, () => completer1);
expect(imageCache.statusForKey(testImage).pending, false); expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true); expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, true); expect(imageCache.statusForKey(testImage).keepAlive, true);
imageCache.evict(testImage); imageCache.evict(testImage);
expect(imageCache.statusForKey(testImage).untracked, true); expect(imageCache.statusForKey(testImage).untracked, true);
}); });
test('Evicting a completed image does not clear the live image when includeLive is set to false', () async { 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); const TestImage testImage = TestImage(width: 8, height: 8);
final TestImageStreamCompleter completer1 = TestImageStreamCompleter() final TestImageStreamCompleter completer1 = TestImageStreamCompleter()
..testSetImage(testImage) ..testSetImage(testImage)
..addListener(ImageStreamListener((ImageInfo info, bool syncCall) {})); ..addListener(ImageStreamListener((ImageInfo info, bool syncCall) {}));
imageCache.putIfAbsent(testImage, () => completer1); imageCache.putIfAbsent(testImage, () => completer1);
expect(imageCache.statusForKey(testImage).pending, false); expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true); expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, true); expect(imageCache.statusForKey(testImage).keepAlive, true);
imageCache.evict(testImage, includeLive: false); imageCache.evict(testImage, includeLive: false);
expect(imageCache.statusForKey(testImage).pending, false); expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true); expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, false); expect(imageCache.statusForKey(testImage).keepAlive, false);
}); });
test('Clearing liveImages removes callbacks', () async { test('Clearing liveImages removes callbacks', () async {
const TestImage testImage = TestImage(width: 8, height: 8); const TestImage testImage = TestImage(width: 8, height: 8);
final ImageStreamListener listener = ImageStreamListener((ImageInfo info, bool syncCall) {}); final ImageStreamListener listener = ImageStreamListener((ImageInfo info, bool syncCall) {});
final TestImageStreamCompleter completer1 = TestImageStreamCompleter() final TestImageStreamCompleter completer1 = TestImageStreamCompleter()
..testSetImage(testImage) ..testSetImage(testImage)
..addListener(listener); ..addListener(listener);
final TestImageStreamCompleter completer2 = TestImageStreamCompleter() final TestImageStreamCompleter completer2 = TestImageStreamCompleter()
..testSetImage(testImage) ..testSetImage(testImage)
..addListener(listener); ..addListener(listener);
imageCache.putIfAbsent(testImage, () => completer1); imageCache.putIfAbsent(testImage, () => completer1);
expect(imageCache.statusForKey(testImage).pending, false); expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true); expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, true); expect(imageCache.statusForKey(testImage).keepAlive, true);
imageCache.clear(); imageCache.clear();
imageCache.clearLiveImages(); imageCache.clearLiveImages();
expect(imageCache.statusForKey(testImage).pending, false); expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, false); expect(imageCache.statusForKey(testImage).live, false);
expect(imageCache.statusForKey(testImage).keepAlive, false); expect(imageCache.statusForKey(testImage).keepAlive, false);
imageCache.putIfAbsent(testImage, () => completer2); imageCache.putIfAbsent(testImage, () => completer2);
expect(imageCache.statusForKey(testImage).pending, false); expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true); expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, true); expect(imageCache.statusForKey(testImage).keepAlive, true);
completer1.removeListener(listener); completer1.removeListener(listener);
expect(imageCache.statusForKey(testImage).pending, false); expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true); expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, true); expect(imageCache.statusForKey(testImage).keepAlive, true);
}); });
test('Live image gets size updated', () async { test('Live image gets size updated', () async {
// Add an image to the cache in pending state // Add an image to the cache in pending state
// Complete it once it is in there as live // Complete it once it is in there as live
// Evict it but leave the live one. // Evict it but leave the live one.
// Add it again. // Add it again.
// If the live image did not track the size properly, the last line of // If the live image did not track the size properly, the last line of
// this test will fail. // this test will fail.
const TestImage testImage = TestImage(width: 8, height: 8); const TestImage testImage = TestImage(width: 8, height: 8);
const int testImageSize = 8 * 8 * 4; const int testImageSize = 8 * 8 * 4;
final ImageStreamListener listener = ImageStreamListener((ImageInfo info, bool syncCall) {}); final ImageStreamListener listener = ImageStreamListener((ImageInfo info, bool syncCall) {});
final TestImageStreamCompleter completer1 = TestImageStreamCompleter() final TestImageStreamCompleter completer1 = TestImageStreamCompleter()
..addListener(listener); ..addListener(listener);
imageCache.putIfAbsent(testImage, () => completer1); imageCache.putIfAbsent(testImage, () => completer1);
expect(imageCache.statusForKey(testImage).pending, true); expect(imageCache.statusForKey(testImage).pending, true);
expect(imageCache.statusForKey(testImage).live, true); expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, false); expect(imageCache.statusForKey(testImage).keepAlive, false);
expect(imageCache.currentSizeBytes, 0); expect(imageCache.currentSizeBytes, 0);
completer1.testSetImage(testImage); completer1.testSetImage(testImage);
expect(imageCache.statusForKey(testImage).pending, false); expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true); expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, true); expect(imageCache.statusForKey(testImage).keepAlive, true);
expect(imageCache.currentSizeBytes, testImageSize); expect(imageCache.currentSizeBytes, testImageSize);
imageCache.evict(testImage, includeLive: false); imageCache.evict(testImage, includeLive: false);
expect(imageCache.statusForKey(testImage).pending, false); expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true); expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, false); expect(imageCache.statusForKey(testImage).keepAlive, false);
expect(imageCache.currentSizeBytes, 0); expect(imageCache.currentSizeBytes, 0);
imageCache.putIfAbsent(testImage, () => completer1); imageCache.putIfAbsent(testImage, () => completer1);
expect(imageCache.statusForKey(testImage).pending, false); expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true); expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, true); expect(imageCache.statusForKey(testImage).keepAlive, true);
expect(imageCache.currentSizeBytes, testImageSize); expect(imageCache.currentSizeBytes, testImageSize);
});
}); });
} }
// 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;
}
}
// 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 {}
// 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);
}
}
...@@ -4,29 +4,17 @@ ...@@ -4,29 +4,17 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:math' as math;
import 'dart:typed_data';
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import '../rendering/rendering_tester.dart'; import '../rendering/rendering_tester.dart';
import 'image_data.dart';
import 'mocks_for_image_cache.dart'; import 'mocks_for_image_cache.dart';
void main() { void main() {
TestRenderingFlutterBinding();
final DecoderCallback basicDecoder = (Uint8List bytes, {int cacheWidth, int cacheHeight}) {
return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight);
};
setUpAll(() {
TestRenderingFlutterBinding(); // initializes the imageCache
});
FlutterExceptionHandler oldError; FlutterExceptionHandler oldError;
setUp(() { setUp(() {
...@@ -39,485 +27,98 @@ void main() { ...@@ -39,485 +27,98 @@ void main() {
PaintingBinding.instance.imageCache.clearLiveImages(); PaintingBinding.instance.imageCache.clearLiveImages();
}); });
group('ImageProvider', () { test('obtainKey errors will be caught', () async {
group('Image cache', () { final ImageProvider imageProvider = ObtainKeyErrorImageProvider();
tearDown(() { final Completer<bool> caughtError = Completer<bool>();
imageCache.clear(); FlutterError.onError = (FlutterErrorDetails details) {
}); caughtError.complete(false);
};
test('AssetImageProvider - evicts on failure to load', () async { final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
final Completer<FlutterError> error = Completer<FlutterError>(); stream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
FlutterError.onError = (FlutterErrorDetails details) { caughtError.complete(false);
error.complete(details.exception as FlutterError); }, onError: (dynamic error, StackTrace stackTrace) {
}; caughtError.complete(true);
}));
const ImageProvider provider = ExactAssetImage('does-not-exist'); expect(await caughtError.future, true);
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); test('obtainKey errors will be caught - check location', () async {
expect(imageCache.currentSize, 1); final ImageProvider imageProvider = ObtainKeyErrorImageProvider();
expect(await imageProvider.evict(cache: otherCache), true); final Completer<bool> caughtError = Completer<bool>();
expect(otherCache.currentSize, 0); FlutterError.onError = (FlutterErrorDetails details) {
expect(imageCache.currentSize, 1); caughtError.complete(true);
}); };
await imageProvider.obtainCacheStatus(configuration: ImageConfiguration.empty);
test('ImageProvider errors can always be caught', () async { expect(await caughtError.future, true);
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 { test('resolve sync errors will be caught', () async {
final ImageProvider imageProvider = ObtainKeyErrorImageProvider(); 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>(); final Completer<bool> caughtError = Completer<bool>();
FlutterError.onError = (FlutterErrorDetails details) { FlutterError.onError = (FlutterErrorDetails details) {
caughtError.complete(false); throw Error();
}; };
final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty); final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
stream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) { result.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
caughtError.complete(false);
}, onError: (dynamic error, StackTrace stackTrace) { }, onError: (dynamic error, StackTrace stackTrace) {
caughtError.complete(true); caughtError.complete(true);
})); }));
expect(await caughtError.future, true); expect(await caughtError.future, true);
}); });
expect(uncaught, false);
});
test('obtainKey errors will be caught - check location', () async { test('resolve errors in the completer will be caught', () async {
final ImageProvider imageProvider = ObtainKeyErrorImageProvider(); 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>(); final Completer<bool> caughtError = Completer<bool>();
FlutterError.onError = (FlutterErrorDetails details) { 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); expect(await caughtError.future, true);
}); });
expect(uncaught, false);
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.
});
}); });
test('ResizeImage resizes to the correct dimensions', () async { test('File image with empty file throws expected error and evicts from cache', () async {
final Uint8List bytes = Uint8List.fromList(kTransparentImage); final Completer<StateError> error = Completer<StateError>();
final MemoryImage imageProvider = MemoryImage(bytes); FlutterError.onError = (FlutterErrorDetails details) {
final Size rawImageSize = await _resolveAndGetSize(imageProvider); error.complete(details.exception as StateError);
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);
}; };
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);
bool isAsync = false; provider.resolve(ImageConfiguration.empty);
resizeImage.obtainKey(ImageConfiguration.empty).then((Object key) {
expect(isAsync, false);
});
isAsync = true;
expect(isAsync, true);
});
test('ResizeImage handles async obtainKey', () async { expect(imageCache.statusForKey(provider).pending, true);
final Uint8List bytes = Uint8List.fromList(kTransparentImage); expect(imageCache.pendingImageCount, 1);
final AsyncKeyMemoryImage memoryImage = AsyncKeyMemoryImage(bytes);
final ResizeImage resizeImage = ResizeImage(memoryImage, width: 123, height: 321);
bool isAsync = false; expect(await error.future, isStateError);
resizeImage.obtainKey(ImageConfiguration.empty).then((Object key) { expect(imageCache.statusForKey(provider).untracked, true);
expect(isAsync, true); expect(imageCache.pendingImageCount, 0);
});
isAsync = true;
expect(isAsync, true);
}); });
test('File image with empty file throws expected error (load)', () async { test('File image with empty file throws expected error (load)', () async {
...@@ -534,40 +135,3 @@ void main() { ...@@ -534,40 +135,3 @@ void main() {
expect(await error.future, isStateError); 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;
}
}
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
...@@ -19,7 +21,15 @@ class TestRenderingFlutterBinding extends BindingBase with SchedulerBinding, Ser ...@@ -19,7 +21,15 @@ class TestRenderingFlutterBinding extends BindingBase with SchedulerBinding, Ser
/// while drawing the frame. If [onErrors] is null and [FlutterError] caught at least /// 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 /// one error, this function fails the test. A test may override [onErrors] and
/// inspect errors using [takeFlutterErrorDetails]. /// 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>[]; final List<FlutterErrorDetails> _errors = <FlutterErrorDetails>[];
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment