mirror of
https://github.com/flutter/flutter
synced 2024-10-13 19:52:53 +00:00
Fix silent test failure in image cache tests (#56492)
This commit is contained in:
parent
0a4f6cdef0
commit
3cb79079bf
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 {}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>[];
|
||||
|
||||
|
|
Loading…
Reference in a new issue