Unverified Commit 2b15b244 authored by Todd Volkert's avatar Todd Volkert Committed by GitHub

Add debugNetworkImageHttpClientProvider (#32857)

Currently, the fact that NetworkImage uses a static HttpClient
makes it impossible to properly test, as a mock in one test will
be reused in another test. This change fixes that.

https://github.com/flutter/flutter/issues/32374
parent 950493ff
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
/// Whether to replace all shadows with solid color blocks. /// Whether to replace all shadows with solid color blocks.
...@@ -11,6 +13,22 @@ import 'package:flutter/foundation.dart'; ...@@ -11,6 +13,22 @@ import 'package:flutter/foundation.dart';
/// version to version (or even from run to run). /// version to version (or even from run to run).
bool debugDisableShadows = false; bool debugDisableShadows = false;
/// Signature for a method that returns an [HttpClient].
///
/// Used by [debugNetworkImageHttpClientProvider].
typedef HttpClientProvider = HttpClient Function();
/// Provider from which [NetworkImage] will get its [HttpClient] in debug builds.
///
/// If this value is unset, [NetworkImage] will use its own internally-managed
/// [HttpClient].
///
/// This setting can be overridden for testing to ensure that each test receives
/// a mock client that hasn't been affected by other tests.
///
/// This value is ignored in non-debug builds.
HttpClientProvider debugNetworkImageHttpClientProvider;
/// Returns true if none of the painting library debug variables have been changed. /// Returns true if none of the painting library debug variables have been changed.
/// ///
/// This function is used by the test framework to ensure that debug variables /// This function is used by the test framework to ensure that debug variables
...@@ -24,7 +42,8 @@ bool debugDisableShadows = false; ...@@ -24,7 +42,8 @@ bool debugDisableShadows = false;
/// test framework itself overrides this value in some cases.) /// test framework itself overrides this value in some cases.)
bool debugAssertAllPaintingVarsUnset(String reason, { bool debugDisableShadowsOverride = false }) { bool debugAssertAllPaintingVarsUnset(String reason, { bool debugDisableShadowsOverride = false }) {
assert(() { assert(() {
if (debugDisableShadows != debugDisableShadowsOverride) { if (debugDisableShadows != debugDisableShadowsOverride ||
debugNetworkImageHttpClientProvider != null) {
throw FlutterError(reason); throw FlutterError(reason);
} }
return true; return true;
......
...@@ -12,6 +12,7 @@ import 'package:flutter/foundation.dart'; ...@@ -12,6 +12,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'binding.dart'; import 'binding.dart';
import 'debug.dart';
import 'image_cache.dart'; import 'image_cache.dart';
import 'image_stream.dart'; import 'image_stream.dart';
...@@ -510,7 +511,18 @@ class NetworkImage extends ImageProvider<NetworkImage> { ...@@ -510,7 +511,18 @@ class NetworkImage extends ImageProvider<NetworkImage> {
); );
} }
static final HttpClient _httpClient = HttpClient(); // Do not access this field directly; use [_httpClient] instead.
static final HttpClient _sharedHttpClient = HttpClient();
static HttpClient get _httpClient {
HttpClient client = _sharedHttpClient;
assert(() {
if (debugNetworkImageHttpClientProvider != null)
client = debugNetworkImageHttpClientProvider();
return true;
}());
return client;
}
Future<ui.Codec> _loadAsync(NetworkImage key) async { Future<ui.Codec> _loadAsync(NetworkImage key) async {
assert(key == this); assert(key == this);
......
...@@ -16,50 +16,64 @@ import 'image_data.dart'; ...@@ -16,50 +16,64 @@ import 'image_data.dart';
import 'mocks_for_image_cache.dart'; import 'mocks_for_image_cache.dart';
void main() { void main() {
TestRenderingFlutterBinding(); // initializes the imageCache
group(ImageProvider, () { group(ImageProvider, () {
tearDown(() { setUpAll(() {
imageCache.clear(); TestRenderingFlutterBinding(); // initializes the imageCache
}); });
test('NetworkImage non-null url test', () { group('Image cache', () {
expect(() { tearDown(() {
NetworkImage(nonconst(null)); imageCache.clear();
}, throwsAssertionError); });
});
test('ImageProvider can evict images', () async { test('ImageProvider can evict images', () async {
final Uint8List bytes = Uint8List.fromList(kTransparentImage); final Uint8List bytes = Uint8List.fromList(kTransparentImage);
final MemoryImage imageProvider = MemoryImage(bytes); final MemoryImage imageProvider = MemoryImage(bytes);
final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty); final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
final Completer<void> completer = Completer<void>(); final Completer<void> completer = Completer<void>();
stream.addListener((ImageInfo info, bool syncCall) => completer.complete()); stream.addListener((ImageInfo info, bool syncCall) => completer.complete());
await completer.future; await completer.future;
expect(imageCache.currentSize, 1);
expect(await MemoryImage(bytes).evict(), true);
expect(imageCache.currentSize, 0);
});
expect(imageCache.currentSize, 1); test('ImageProvider.evict respects the provided ImageCache', () async {
expect(await MemoryImage(bytes).evict(), true); final ImageCache otherCache = ImageCache();
expect(imageCache.currentSize, 0); final Uint8List bytes = Uint8List.fromList(kTransparentImage);
}); final MemoryImage imageProvider = MemoryImage(bytes);
otherCache.putIfAbsent(imageProvider, () => imageProvider.load(imageProvider));
final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
final Completer<void> completer = Completer<void>();
stream.addListener((ImageInfo info, bool syncCall) => completer.complete());
await completer.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.evict respects the provided ImageCache', () async { test('ImageProvider errors can always be caught', () async {
final ImageCache otherCache = ImageCache(); final ErrorImageProvider imageProvider = ErrorImageProvider();
final Uint8List bytes = Uint8List.fromList(kTransparentImage); final Completer<bool> caughtError = Completer<bool>();
final MemoryImage imageProvider = MemoryImage(bytes); FlutterError.onError = (FlutterErrorDetails details) {
otherCache.putIfAbsent(imageProvider, () => imageProvider.load(imageProvider)); caughtError.complete(false);
final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty); };
final Completer<void> completer = Completer<void>(); final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
stream.addListener((ImageInfo info, bool syncCall) => completer.complete()); stream.addListener((ImageInfo info, bool syncCall) {
await completer.future; caughtError.complete(false);
}, onError: (dynamic error, StackTrace stackTrace) {
expect(otherCache.currentSize, 1); caughtError.complete(true);
expect(imageCache.currentSize, 1); });
expect(await imageProvider.evict(cache: otherCache), true); expect(await caughtError.future, true);
expect(otherCache.currentSize, 0); });
expect(imageCache.currentSize, 1);
}); });
test('ImageProvider errors can always be caught', () async { test('obtainKey errors will be caught', () async {
final ErrorImageProvider imageProvider = ErrorImageProvider(); final ImageProvider imageProvider = ObtainKeyErrorImageProvider();
final Completer<bool> caughtError = Completer<bool>(); final Completer<bool> caughtError = Completer<bool>();
FlutterError.onError = (FlutterErrorDetails details) { FlutterError.onError = (FlutterErrorDetails details) {
caughtError.complete(false); caughtError.complete(false);
...@@ -72,93 +86,121 @@ void main() { ...@@ -72,93 +86,121 @@ void main() {
}); });
expect(await caughtError.future, true); expect(await caughtError.future, true);
}); });
});
test('ImageProvide.obtainKey errors will be caught', () async { test('resolve sync errors will be caught', () async {
final ImageProvider imageProvider = ObtainKeyErrorImageProvider(); bool uncaught = false;
final Completer<bool> caughtError = Completer<bool>(); final Zone testZone = Zone.current.fork(specification: ZoneSpecification(
FlutterError.onError = (FlutterErrorDetails details) { handleUncaughtError: (Zone zone, ZoneDelegate zoneDelegate, Zone parent, Object error, StackTrace stackTrace) {
caughtError.complete(false); uncaught = true;
}; },
final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty); ));
stream.addListener((ImageInfo info, bool syncCall) { await testZone.run(() async {
caughtError.complete(false); final ImageProvider imageProvider = LoadErrorImageProvider();
}, onError: (dynamic error, StackTrace stackTrace) { final Completer<bool> caughtError = Completer<bool>();
caughtError.complete(true); FlutterError.onError = (FlutterErrorDetails details) {
throw Error();
};
final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
result.addListener((ImageInfo info, bool syncCall) {
}, onError: (dynamic error, StackTrace stackTrace) {
caughtError.complete(true);
});
expect(await caughtError.future, true);
});
expect(uncaught, false);
}); });
expect(await caughtError.future, true);
});
test('ImageProvider.resolve sync errors will be caught', () async { test('resolve errors in the completer will be caught', () async {
bool uncaught = false; bool uncaught = false;
final Zone testZone = Zone.current.fork(specification: ZoneSpecification( final Zone testZone = Zone.current.fork(specification: ZoneSpecification(
handleUncaughtError: (Zone zone, ZoneDelegate zoneDelegate, Zone parent, Object error, StackTrace stackTrace) { handleUncaughtError: (Zone zone, ZoneDelegate zoneDelegate, Zone parent, Object error, StackTrace stackTrace) {
uncaught = true; uncaught = true;
} },
)); ));
await testZone.run(() async { await testZone.run(() async {
final ImageProvider imageProvider = LoadErrorImageProvider(); final ImageProvider imageProvider = LoadErrorCompleterImageProvider();
final Completer<bool> caughtError = Completer<bool>(); final Completer<bool> caughtError = Completer<bool>();
FlutterError.onError = (FlutterErrorDetails details) { FlutterError.onError = (FlutterErrorDetails details) {
throw Error(); throw Error();
}; };
final ImageStream result = imageProvider.resolve(ImageConfiguration.empty); final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
result.addListener((ImageInfo info, bool syncCall) { result.addListener((ImageInfo info, bool syncCall) {
}, onError: (dynamic error, StackTrace stackTrace) { }, onError: (dynamic error, StackTrace stackTrace) {
caughtError.complete(true); caughtError.complete(true);
});
expect(await caughtError.future, true);
}); });
expect(await caughtError.future, true); expect(uncaught, false);
}); });
expect(uncaught, false);
});
test('ImageProvider.resolve errors in the completer will be caught', () async { group(NetworkImage, () {
bool uncaught = false; MockHttpClient httpClient;
final Zone testZone = Zone.current.fork(specification: ZoneSpecification(
handleUncaughtError: (Zone zone, ZoneDelegate zoneDelegate, Zone parent, Object error, StackTrace stackTrace) { setUp(() {
uncaught = true; httpClient = MockHttpClient();
} debugNetworkImageHttpClientProvider = () => httpClient;
));
await testZone.run(() async {
final ImageProvider imageProvider = LoadErrorCompleterImageProvider();
final Completer<bool> caughtError = Completer<bool>();
FlutterError.onError = (FlutterErrorDetails details) {
throw Error();
};
final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
result.addListener((ImageInfo info, bool syncCall) {
}, onError: (dynamic error, StackTrace stackTrace) {
caughtError.complete(true);
}); });
expect(await caughtError.future, true);
});
expect(uncaught, false);
});
test('ImageProvider.resolve errors in the http client will be caught', () async { tearDown(() {
bool uncaught = false; debugNetworkImageHttpClientProvider = null;
final HttpClientMock httpClientMock = HttpClientMock(); });
when(httpClientMock.getUrl(any)).thenThrow(Error());
await HttpOverrides.runZoned(() async { test('Disallows null urls', () {
const ImageProvider imageProvider = NetworkImage('asdasdasdas'); expect(() {
final Completer<bool> caughtError = Completer<bool>(); NetworkImage(nonconst(null));
FlutterError.onError = (FlutterErrorDetails details) { }, throwsAssertionError);
throw Error();
};
final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
result.addListener((ImageInfo info, bool syncCall) {
}, onError: (dynamic error, StackTrace stackTrace) {
caughtError.complete(true);
}); });
expect(await caughtError.future, true);
}, createHttpClient: (SecurityContext context) => httpClientMock, zoneSpecification: ZoneSpecification( test('Uses the HttpClient provided by debugNetworkImageHttpClientProvider if set', () async {
handleUncaughtError: (Zone zone, ZoneDelegate zoneDelegate, Zone parent, Object error, StackTrace stackTrace) { when(httpClient.getUrl(any)).thenThrow('client1');
uncaught = true; final List<dynamic> capturedErrors = <dynamic>[];
}
)); Future<void> loadNetworkImage() async {
expect(uncaught, false); final NetworkImage networkImage = NetworkImage(nonconst('foo'));
final ImageStreamCompleter completer = networkImage.load(networkImage);
completer.addListener(
(ImageInfo image, bool synchronousCall) { },
onError: (dynamic error, StackTrace stackTrace) {
capturedErrors.add(error);
},
);
await Future<void>.value();
}
await loadNetworkImage();
expect(capturedErrors, <dynamic>['client1']);
final MockHttpClient client2 = MockHttpClient();
when(client2.getUrl(any)).thenThrow('client2');
debugNetworkImageHttpClientProvider = () => client2;
await loadNetworkImage();
expect(capturedErrors, <dynamic>['client1', 'client2']);
});
test('Propagates http client errors during resolve()', () async {
when(httpClient.getUrl(any)).thenThrow(Error());
bool uncaught = false;
await runZoned(() async {
const ImageProvider imageProvider = NetworkImage('asdasdasdas');
final Completer<bool> caughtError = Completer<bool>();
FlutterError.onError = (FlutterErrorDetails details) {
throw Error();
};
final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
result.addListener((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);
});
});
}); });
} }
class HttpClientMock extends Mock implements HttpClient {} class MockHttpClient extends Mock implements HttpClient {}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment