From 3cb79079bf6a1abb6a583df68765f5c3c5d4f0c2 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Thu, 7 May 2020 18:19:03 -0700 Subject: [PATCH] Fix silent test failure in image cache tests (#56492) --- .../flutter/lib/src/painting/image_cache.dart | 7 +- .../lib/src/painting/image_provider.dart | 8 +- .../painting/image_cache_clearing_test.dart | 31 + .../painting/image_cache_resize_test.dart | 99 ++- .../test/painting/image_cache_test.dart | 798 +++++++++--------- .../image_provider_and_image_cache_test.dart | 142 ++++ .../image_provider_network_image_test.dart | 224 +++++ .../image_provider_resize_image_test.dart | 134 +++ .../test/painting/image_provider_test.dart | 574 ++----------- .../test/rendering/rendering_tester.dart | 12 +- 10 files changed, 1062 insertions(+), 967 deletions(-) create mode 100644 packages/flutter/test/painting/image_cache_clearing_test.dart create mode 100644 packages/flutter/test/painting/image_provider_and_image_cache_test.dart create mode 100644 packages/flutter/test/painting/image_provider_network_image_test.dart create mode 100644 packages/flutter/test/painting/image_provider_resize_image_test.dart diff --git a/packages/flutter/lib/src/painting/image_cache.dart b/packages/flutter/lib/src/painting/image_cache.dart index 24d894f45bc..dbdf7b0c82a 100644 --- a/packages/flutter/lib/src/painting/image_cache.dart +++ b/packages/flutter/lib/src/painting/image_cache.dart @@ -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); diff --git a/packages/flutter/lib/src/painting/image_provider.dart b/packages/flutter/lib/src/painting/image_provider.dart index e75a5820f51..c6ffb7f5982 100644 --- a/packages/flutter/lib/src/painting/image_provider.dart +++ b/packages/flutter/lib/src/painting/image_provider.dart @@ -347,10 +347,10 @@ abstract class ImageProvider { stack: stack, context: ErrorDescription('while resolving an image'), silent: true, // could be a network error or whatnot - informationCollector: collector - ); - }, - ); + informationCollector: collector, + ); + }, + ); return stream; } diff --git a/packages/flutter/test/painting/image_cache_clearing_test.dart b/packages/flutter/test/painting/image_cache_clearing_test.dart new file mode 100644 index 00000000000..beb913b8411 --- /dev/null +++ b/packages/flutter/test/painting/image_cache_clearing_test.dart @@ -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 completer = Completer(); + FlutterError.onError = (FlutterErrorDetails error) { completer.completeError(error.exception, error.stack); }; + stream.addListener(ImageStreamListener( + (ImageInfo image, bool synchronousCall) { + completer.complete(); + } + )); + imageCache.clearLiveImages(); + await completer.future; + }); +} diff --git a/packages/flutter/test/painting/image_cache_resize_test.dart b/packages/flutter/test/painting/image_cache_resize_test.dart index eab9291e00e..867ff4c5960 100644 --- a/packages/flutter/test/painting/image_cache_resize_test.dart +++ b/packages/flutter/test/painting/image_cache_resize_test.dart @@ -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)); }); } diff --git a/packages/flutter/test/painting/image_cache_test.dart b/packages/flutter/test/painting/image_cache_test.dart index ad72ade1edc..1ddba9b66c0 100644 --- a/packages/flutter/test/painting/image_cache_test.dart +++ b/packages/flutter/test/painting/image_cache_test.dart @@ -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())); + 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())); - 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); }); } diff --git a/packages/flutter/test/painting/image_provider_and_image_cache_test.dart b/packages/flutter/test/painting/image_provider_and_image_cache_test.dart new file mode 100644 index 00000000000..53724c69403 --- /dev/null +++ b/packages/flutter/test/painting/image_provider_and_image_cache_test.dart @@ -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 error = Completer(); + 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 error = Completer(); + 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 completer = Completer(); + 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 completer = Completer(); + final Completer cacheCompleter = Completer(); + stream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) { + completer.complete(); + })); + cacheStream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) { + cacheCompleter.complete(); + })); + await Future.wait(>[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 caughtError = Completer(); + 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 load(String key) async { + return null; + } +} diff --git a/packages/flutter/test/painting/image_provider_network_image_test.dart b/packages/flutter/test/painting/image_provider_network_image_test.dart new file mode 100644 index 00000000000..3fda11b5a94 --- /dev/null +++ b/packages/flutter/test/painting/image_provider_network_image_test.dart @@ -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.value(request)); + when(request.close()).thenAnswer((_) => Future.value(response)); + when(response.statusCode).thenReturn(errorStatusCode); + + final Completer caughtError = Completer(); + + 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() + .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 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 capturedErrors = []; + + Future 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.value(); + } + + await loadNetworkImage(); + expect(capturedErrors, ['client1']); + final _MockHttpClient client2 = _MockHttpClient(); + when(client2.getUrl(any)).thenThrow('client2'); + debugNetworkImageHttpClientProvider = () => client2; + await loadNetworkImage(); + expect(capturedErrors, ['client1', 'client2']); + }, skip: isBrowser); // Browser implementation does not use HTTP client but an 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 caughtError = Completer(); + 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 chunks = [ + for (int offset = 0; offset < kTransparentImage.length; offset += chunkSize) + Uint8List.fromList(kTransparentImage.skip(offset).take(chunkSize).toList()), + ]; + final Completer imageAvailable = Completer(); + final _MockHttpClientRequest request = _MockHttpClientRequest(); + final _MockHttpClientResponse response = _MockHttpClientResponse(); + when(httpClient.getUrl(any)).thenAnswer((_) => Future.value(request)); + when(request.close()).thenAnswer((_) => Future.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) onData = invocation.positionalArguments[0] as void Function(List); + 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.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 events = []; + 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 caughtError = Completer(); + result.addListener(ImageStreamListener( + (ImageInfo info, bool syncCall) {}, + onError: (dynamic error, StackTrace stackTrace) { + caughtError.complete(error); + }, + )); + + final dynamic err = await caughtError.future; + + expect(err, isA()); + + 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 {} diff --git a/packages/flutter/test/painting/image_provider_resize_image_test.dart b/packages/flutter/test/painting/image_provider_resize_image_test.dart new file mode 100644 index 00000000000..6f8d268c576 --- /dev/null +++ b/packages/flutter/test/painting/image_provider_resize_image_test.dart @@ -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 _resolveAndGetSize(ImageProvider imageProvider, + {ImageConfiguration configuration = ImageConfiguration.empty}) async { + final ImageStream stream = imageProvider.resolve(configuration); + final Completer completer = Completer(); + 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 obtainKey(ImageConfiguration configuration) { + return Future(() => this); + } +} diff --git a/packages/flutter/test/painting/image_provider_test.dart b/packages/flutter/test/painting/image_provider_test.dart index 34b5b9776bb..a342d6a08b1 100644 --- a/packages/flutter/test/painting/image_provider_test.dart +++ b/packages/flutter/test/painting/image_provider_test.dart @@ -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 caughtError = Completer(); + 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 error = Completer(); - 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 caughtError = Completer(); + 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 error = Completer(); - 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 completer = Completer(); - 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 completer = Completer(); - final Completer cacheCompleter = Completer(); - stream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) { - completer.complete(); - })); - cacheStream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) { - cacheCompleter.complete(); - })); - await Future.wait(>[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 caughtError = Completer(); - 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 caughtError = Completer(); 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 caughtError = Completer(); 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 caughtError = Completer(); - 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 caughtError = Completer(); - 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 error = Completer(); - 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.value(request)); - when(request.close()).thenAnswer((_) => Future.value(response)); - when(response.statusCode).thenReturn(errorStatusCode); - - final Completer caughtError = Completer(); - - 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() - .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 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 capturedErrors = []; - - Future 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.value(); - } - - await loadNetworkImage(); - expect(capturedErrors, ['client1']); - final MockHttpClient client2 = MockHttpClient(); - when(client2.getUrl(any)).thenThrow('client2'); - debugNetworkImageHttpClientProvider = () => client2; - await loadNetworkImage(); - expect(capturedErrors, ['client1', 'client2']); - }, skip: isBrowser); // Browser implementation does not use HTTP client but an 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 caughtError = Completer(); - 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 chunks = [ - for (int offset = 0; offset < kTransparentImage.length; offset += chunkSize) - Uint8List.fromList(kTransparentImage.skip(offset).take(chunkSize).toList()), - ]; - final Completer imageAvailable = Completer(); - final MockHttpClientRequest request = MockHttpClientRequest(); - final MockHttpClientResponse response = MockHttpClientResponse(); - when(httpClient.getUrl(any)).thenAnswer((_) => Future.value(request)); - when(request.close()).thenAnswer((_) => Future.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) onData = invocation.positionalArguments[0] as void Function(List); - 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.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 events = []; - 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 caughtError = Completer(); - result.addListener(ImageStreamListener( - (ImageInfo info, bool syncCall) {}, - onError: (dynamic error, StackTrace stackTrace) { - caughtError.complete(error); - }, - )); - - final dynamic err = await caughtError.future; - - expect(err, isA()); - - 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 error = Completer(); + 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 _resolveAndGetSize(ImageProvider imageProvider, - {ImageConfiguration configuration = ImageConfiguration.empty}) async { - final ImageStream stream = imageProvider.resolve(configuration); - final Completer completer = Completer(); - 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 obtainKey(ImageConfiguration configuration) { - return Future(() => 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 load(String key) async { - return null; - } -} diff --git a/packages/flutter/test/rendering/rendering_tester.dart b/packages/flutter/test/rendering/rendering_tester.dart index 289c1b2ce80..6d24df8562c 100644 --- a/packages/flutter/test/rendering/rendering_tester.dart +++ b/packages/flutter/test/rendering/rendering_tester.dart @@ -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 _errors = [];