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 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:io';
import 'package:flutter/foundation.dart';
/// Whether to replace all shadows with solid color blocks.
......@@ -11,6 +13,22 @@ import 'package:flutter/foundation.dart';
/// version to version (or even from run to run).
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.
///
/// This function is used by the test framework to ensure that debug variables
......@@ -24,7 +42,8 @@ bool debugDisableShadows = false;
/// test framework itself overrides this value in some cases.)
bool debugAssertAllPaintingVarsUnset(String reason, { bool debugDisableShadowsOverride = false }) {
assert(() {
if (debugDisableShadows != debugDisableShadowsOverride) {
if (debugDisableShadows != debugDisableShadowsOverride ||
debugNetworkImageHttpClientProvider != null) {
throw FlutterError(reason);
}
return true;
......
......@@ -12,6 +12,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'binding.dart';
import 'debug.dart';
import 'image_cache.dart';
import 'image_stream.dart';
......@@ -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 {
assert(key == this);
......
......@@ -16,16 +16,14 @@ import 'image_data.dart';
import 'mocks_for_image_cache.dart';
void main() {
TestRenderingFlutterBinding(); // initializes the imageCache
group(ImageProvider, () {
tearDown(() {
imageCache.clear();
setUpAll(() {
TestRenderingFlutterBinding(); // initializes the imageCache
});
test('NetworkImage non-null url test', () {
expect(() {
NetworkImage(nonconst(null));
}, throwsAssertionError);
group('Image cache', () {
tearDown(() {
imageCache.clear();
});
test('ImageProvider can evict images', () async {
......@@ -74,7 +72,7 @@ void main() {
});
});
test('ImageProvide.obtainKey errors will be caught', () async {
test('obtainKey errors will be caught', () async {
final ImageProvider imageProvider = ObtainKeyErrorImageProvider();
final Completer<bool> caughtError = Completer<bool>();
FlutterError.onError = (FlutterErrorDetails details) {
......@@ -89,12 +87,12 @@ void main() {
expect(await caughtError.future, true);
});
test('ImageProvider.resolve sync errors will be caught', () async {
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();
......@@ -112,12 +110,12 @@ void main() {
expect(uncaught, false);
});
test('ImageProvider.resolve errors in the completer will be caught', () async {
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();
......@@ -135,12 +133,54 @@ void main() {
expect(uncaught, false);
});
test('ImageProvider.resolve errors in the http client will be caught', () async {
group(NetworkImage, () {
MockHttpClient httpClient;
setUp(() {
httpClient = MockHttpClient();
debugNetworkImageHttpClientProvider = () => httpClient;
});
tearDown(() {
debugNetworkImageHttpClientProvider = null;
});
test('Disallows null urls', () {
expect(() {
NetworkImage(nonconst(null));
}, throwsAssertionError);
});
test('Uses the HttpClient provided by debugNetworkImageHttpClientProvider if set', () async {
when(httpClient.getUrl(any)).thenThrow('client1');
final List<dynamic> capturedErrors = <dynamic>[];
Future<void> loadNetworkImage() async {
final NetworkImage networkImage = NetworkImage(nonconst('foo'));
final ImageStreamCompleter completer = networkImage.load(networkImage);
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;
final HttpClientMock httpClientMock = HttpClientMock();
when(httpClientMock.getUrl(any)).thenThrow(Error());
await HttpOverrides.runZoned(() async {
await runZoned(() async {
const ImageProvider imageProvider = NetworkImage('asdasdasdas');
final Completer<bool> caughtError = Completer<bool>();
FlutterError.onError = (FlutterErrorDetails details) {
......@@ -152,13 +192,15 @@ void main() {
caughtError.complete(true);
});
expect(await caughtError.future, true);
}, createHttpClient: (SecurityContext context) => httpClientMock, zoneSpecification: ZoneSpecification(
}, 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