Commit bd929c4a authored by Alexander Aprelev's avatar Alexander Aprelev Committed by Kaushik Iska

Revert "Use separate isolate for image loading. (#34188)" (#40984)

This reverts commit b12bdd0e as it
breaks existing tests that expect image loaded after certaing number of
pupms. With image loading done on separate isolate pumping is not
guaranteed to get image loaded.
parent 57c319a9
......@@ -5,7 +5,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data';
/// Signature for getting notified when chunks of bytes are received while
......@@ -23,11 +22,11 @@ import 'dart:typed_data';
/// returned to the client and the total size of the response may not be known
/// until the request has been fully processed).
///
/// This is used in [getHttpClientResponseBytes].
/// This is used in [consolidateHttpClientResponseBytes].
typedef BytesReceivedCallback = void Function(int cumulative, int total);
/// Efficiently converts the response body of an [HttpClientResponse] into a
/// [TransferableTypedData].
/// [Uint8List].
///
/// The future returned will forward any error emitted by `response`.
///
......@@ -44,13 +43,13 @@ typedef BytesReceivedCallback = void Function(int cumulative, int total);
/// bytes from this method (assuming the response is sending compressed bytes),
/// set both [HttpClient.autoUncompress] to false and the `autoUncompress`
/// parameter to false.
Future<TransferableTypedData> getHttpClientResponseBytes(
Future<Uint8List> consolidateHttpClientResponseBytes(
HttpClientResponse response, {
bool autoUncompress = true,
BytesReceivedCallback onBytesReceived,
}) {
assert(autoUncompress != null);
final Completer<TransferableTypedData> completer = Completer<TransferableTypedData>.sync();
final Completer<Uint8List> completer = Completer<Uint8List>.sync();
final _OutputBuffer output = _OutputBuffer();
ByteConversionSink sink = output;
......@@ -90,54 +89,41 @@ Future<TransferableTypedData> getHttpClientResponseBytes(
}
}, onDone: () {
sink.close();
completer.complete(TransferableTypedData.fromList(output.chunks));
completer.complete(output.bytes);
}, onError: completer.completeError, cancelOnError: true);
return completer.future;
}
/// Efficiently converts the response body of an [HttpClientResponse] into a
/// [Uint8List].
///
/// (This method is deprecated - use [getHttpClientResponseBytes] instead.)
///
/// The future returned will forward any error emitted by `response`.
///
/// The `onBytesReceived` callback, if specified, will be invoked for every
/// chunk of bytes that is received while consolidating the response bytes.
/// If the callback throws an error, processing of the response will halt, and
/// the returned future will complete with the error that was thrown by the
/// callback. For more information on how to interpret the parameters to the
/// callback, see the documentation on [BytesReceivedCallback].
///
/// If the `response` is gzipped and the `autoUncompress` parameter is true,
/// this will automatically un-compress the bytes in the returned list if it
/// hasn't already been done via [HttpClient.autoUncompress]. To get compressed
/// bytes from this method (assuming the response is sending compressed bytes),
/// set both [HttpClient.autoUncompress] to false and the `autoUncompress`
/// parameter to false.
@Deprecated('Use getHttpClientResponseBytes instead')
Future<Uint8List> consolidateHttpClientResponseBytes(
HttpClientResponse response, {
bool autoUncompress = true,
BytesReceivedCallback onBytesReceived,
}) async {
final TransferableTypedData bytes = await getHttpClientResponseBytes(
response,
autoUncompress: autoUncompress,
onBytesReceived: onBytesReceived,
);
return bytes.materialize().asUint8List();
}
class _OutputBuffer extends ByteConversionSinkBase {
final List<Uint8List> chunks = <Uint8List>[];
List<List<int>> _chunks = <List<int>>[];
int _contentLength = 0;
Uint8List _bytes;
@override
void add(List<int> chunk) {
chunks.add(chunk);
assert(_bytes == null);
_chunks.add(chunk);
_contentLength += chunk.length;
}
@override
void close() {}
void close() {
if (_bytes != null) {
// We've already been closed; this is a no-op
return;
}
_bytes = Uint8List(_contentLength);
int offset = 0;
for (List<int> chunk in _chunks) {
_bytes.setRange(offset, offset + chunk.length, chunk);
offset += chunk.length;
}
_chunks = null;
}
Uint8List get bytes {
assert(_bytes != null);
return _bytes;
}
}
......@@ -4,7 +4,6 @@
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data';
import 'dart:ui' as ui;
......@@ -15,7 +14,7 @@ import 'debug.dart';
import 'image_provider.dart' as image_provider;
import 'image_stream.dart';
/// The dart:io implementation of [image_provider.NetworkImage].
/// The dart:io implemenation of [image_provider.NetworkImage].
class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkImage> implements image_provider.NetworkImage {
/// Creates an object that fetches the image at the given URL.
///
......@@ -40,9 +39,9 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
@override
ImageStreamCompleter load(image_provider.NetworkImage key) {
// Ownership of this controller is handed off to [_loadAsync];
// it is that method's responsibility to close the controller's stream when
// the image has been loaded or an error is thrown.
// Ownership of this controller is handed off to [_loadAsync]; it is that
// method's responsibility to close the controller's stream when the image
// has been loaded or an error is thrown.
final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter(
......@@ -58,130 +57,54 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
);
}
// For [_pendingLoader] we don't need the value(worker isolate), just the future
// itself that is used as an indicator that successive load requests should be
// added to the list of pending load requests [_pendingLoadRequests].
static Future<void> _pendingLoader;
static RawReceivePort _loaderErrorHandler;
static List<_DownloadRequest> _pendingLoadRequests;
static SendPort _requestPort;
// Do not access this field directly; use [_httpClient] instead.
// We set `autoUncompress` to false to ensure that we can trust the value of
// the `Content-Length` HTTP header. We automatically uncompress the content
// in our call to [consolidateHttpClientResponseBytes].
static final HttpClient _sharedHttpClient = HttpClient()..autoUncompress = false;
static HttpClient get _httpClient {
HttpClient client = _sharedHttpClient;
assert(() {
if (debugNetworkImageHttpClientProvider != null)
client = debugNetworkImageHttpClientProvider();
return true;
}());
return client;
}
Future<ui.Codec> _loadAsync(
NetworkImage key,
StreamController<ImageChunkEvent> chunkEvents,
) async {
RawReceivePort downloadResponseHandler;
try {
assert(key == this);
final Uri resolved = Uri.base.resolve(key.url);
final Completer<TransferableTypedData> bytesCompleter = Completer<TransferableTypedData>();
downloadResponseHandler = RawReceivePort((_DownloadResponse response) {
if (response.bytes != null) {
if (bytesCompleter.isCompleted) {
// If an uncaught error occurred in the worker isolate, we'll have
// already completed our bytes completer.
return;
}
bytesCompleter.complete(response.bytes);
} else if (response.chunkEvent != null) {
chunkEvents.add(response.chunkEvent);
} else if (response.error != null) {
bytesCompleter.completeError(response.error);
} else {
assert(false);
}
final HttpClientRequest request = await _httpClient.getUrl(resolved);
headers?.forEach((String name, String value) {
request.headers.add(name, value);
});
final HttpClientResponse response = await request.close();
if (response.statusCode != HttpStatus.ok)
throw image_provider.NetworkImageLoadException(statusCode: response.statusCode, uri: resolved);
// This will keep references to [debugNetworkImageHttpClientProvider] tree-shaken
// out of release builds.
HttpClientProvider httpClientProvider;
assert(() { httpClientProvider = debugNetworkImageHttpClientProvider; return true; }());
final _DownloadRequest downloadRequest = _DownloadRequest(
downloadResponseHandler.sendPort,
resolved,
headers,
httpClientProvider,
final Uint8List bytes = await consolidateHttpClientResponseBytes(
response,
onBytesReceived: (int cumulative, int total) {
chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded: cumulative,
expectedTotalBytes: total,
));
},
);
if (_requestPort != null) {
// If worker isolate is properly set up ([_requestPort] is holding
// initialized [SendPort]), then just send download request down to it.
_requestPort.send(downloadRequest);
} else {
if (_pendingLoader == null) {
// If worker isolate creation was not started, start creation now.
_spawnAndSetupIsolate();
}
// Record download request so it can either send a request when isolate is ready or handle errors.
_pendingLoadRequests.add(downloadRequest);
}
final TransferableTypedData transferable = await bytesCompleter.future;
final Uint8List bytes = transferable.materialize().asUint8List();
if (bytes.isEmpty)
if (bytes.lengthInBytes == 0)
throw Exception('NetworkImage is an empty file: $resolved');
return PaintingBinding.instance.instantiateImageCodec(bytes);
} finally {
chunkEvents.close();
downloadResponseHandler?.close();
}
}
void _spawnAndSetupIsolate() {
assert(_pendingLoadRequests == null);
assert(_loaderErrorHandler == null);
assert(_pendingLoader == null);
_pendingLoadRequests = <_DownloadRequest>[];
_pendingLoader = _spawnIsolate()..then((Isolate isolate) {
_loaderErrorHandler = RawReceivePort((List<dynamic> errorAndStackTrace) {
_cleanupDueToError(errorAndStackTrace[0]);
});
isolate.addErrorListener(_loaderErrorHandler.sendPort);
isolate.resume(isolate.pauseCapability);
}).catchError((dynamic error, StackTrace stackTrace) {
_cleanupDueToError(error);
});
}
void _cleanupDueToError(dynamic error) {
for (_DownloadRequest request in _pendingLoadRequests) {
request.handleError(error);
}
_pendingLoadRequests = null;
_pendingLoader = null;
_loaderErrorHandler.close();
_loaderErrorHandler = null;
}
Future<Isolate> _spawnIsolate() {
// Once worker isolate is up and running it sends it's [sendPort] over
// [communicationBootstrapHandler] receive port.
// If [sendPort] is [null], it indicates that worker isolate exited after
// being idle.
final RawReceivePort communicationBootstrapHandler = RawReceivePort((SendPort sendPort) {
_requestPort = sendPort;
if (sendPort == null) {
assert(_pendingLoadRequests.isEmpty);
_pendingLoader = null;
_pendingLoadRequests = null;
_loaderErrorHandler.close();
_loaderErrorHandler = null;
return;
}
// When we received [SendPort] for the worker isolate, we send all
// pending requests that were accumulated before worker isolate provided
// it's port (before [_requestPort] was populated).
_pendingLoadRequests.forEach(sendPort.send);
_pendingLoadRequests.clear();
});
return Isolate.spawn<SendPort>(_initializeWorkerIsolate,
communicationBootstrapHandler.sendPort, paused: true);
}
@override
......@@ -189,7 +112,8 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
if (other.runtimeType != runtimeType)
return false;
final NetworkImage typedOther = other;
return url == typedOther.url && scale == typedOther.scale;
return url == typedOther.url
&& scale == typedOther.scale;
}
@override
......@@ -198,95 +122,3 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
@override
String toString() => '$runtimeType("$url", scale: $scale)';
}
@immutable
class _DownloadResponse {
const _DownloadResponse.bytes(this.bytes) : assert(bytes != null), chunkEvent = null, error = null;
const _DownloadResponse.chunkEvent(this.chunkEvent) : assert(chunkEvent != null), bytes = null, error = null;
const _DownloadResponse.error(this.error) : assert(error != null), bytes = null, chunkEvent = null;
final TransferableTypedData bytes;
final ImageChunkEvent chunkEvent;
final dynamic error;
}
@immutable
class _DownloadRequest {
const _DownloadRequest(this.sendPort, this.uri, this.headers, this.httpClientProvider) :
assert(sendPort != null), assert(uri != null);
final SendPort sendPort;
final Uri uri;
final Map<String, String> headers;
final HttpClientProvider httpClientProvider;
void handleError(dynamic error) { sendPort.send(_DownloadResponse.error(error)); }
}
// We set `autoUncompress` to false to ensure that we can trust the value of
// the `Content-Length` HTTP header. We automatically uncompress the content
// in our call to [getHttpClientResponseBytes].
final HttpClient _sharedHttpClient = HttpClient()..autoUncompress = false;
const Duration _idleDuration = Duration(seconds: 60);
/// Sets up the worker isolate to listen for incoming [_DownloadRequest]s from
/// the main isolate.
///
/// This method runs on a worker isolate.
///
/// The `handshakeSendPort` argument is this worker isolate's communications
/// link back to the main isolate. It is used to set-up the channel with which
/// the main isolate sends download requests to the worker isolate.
void _initializeWorkerIsolate(SendPort handshakeSendPort) {
int ongoingRequests = 0;
Timer idleTimer;
RawReceivePort downloadRequestHandler;
// Sets up a handler that processes download requests messages.
downloadRequestHandler = RawReceivePort((_DownloadRequest downloadRequest) async {
ongoingRequests++;
idleTimer?.cancel();
final HttpClient httpClient = downloadRequest.httpClientProvider != null
? downloadRequest.httpClientProvider()
: _sharedHttpClient;
try {
final HttpClientRequest request = await httpClient.getUrl(downloadRequest.uri);
downloadRequest.headers?.forEach((String name, String value) {
request.headers.add(name, value);
});
final HttpClientResponse response = await request.close();
if (response.statusCode != HttpStatus.ok) {
throw image_provider.NetworkImageLoadException(
statusCode: response?.statusCode,
uri: downloadRequest.uri,
);
}
final TransferableTypedData transferable = await getHttpClientResponseBytes(
response,
onBytesReceived: (int cumulative, int total) {
downloadRequest.sendPort.send(_DownloadResponse.chunkEvent(
ImageChunkEvent(
cumulativeBytesLoaded: cumulative,
expectedTotalBytes: total,
),
));
},
);
downloadRequest.sendPort.send(_DownloadResponse.bytes(transferable));
} catch (error) {
downloadRequest.sendPort.send(_DownloadResponse.error(error));
}
ongoingRequests--;
if (ongoingRequests == 0) {
idleTimer = Timer(_idleDuration, () {
assert(ongoingRequests == 0);
// [null] indicates that worker is going down.
handshakeSendPort.send(null);
downloadRequestHandler.close();
});
}
});
handshakeSendPort.send(downloadRequestHandler.sendPort);
}
......@@ -5,7 +5,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
......@@ -121,8 +120,8 @@ class NetworkAssetBundle extends AssetBundle {
'Unable to load asset: $key\n'
'HTTP status code: ${response.statusCode}'
);
final TransferableTypedData transferable = await getHttpClientResponseBytes(response);
return transferable.materialize().asByteData();
final Uint8List bytes = await consolidateHttpClientResponseBytes(response);
return bytes.buffer.asByteData();
}
/// Retrieve a string from the asset bundle, parse it with the given function,
......
......@@ -6,7 +6,6 @@
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
......@@ -15,7 +14,7 @@ import 'package:mockito/mockito.dart';
import '../flutter_test_alternative.dart';
void main() {
group(getHttpClientResponseBytes, () {
group(consolidateHttpClientResponseBytes, () {
final Uint8List chunkOne = Uint8List.fromList(<int>[0, 1, 2, 3, 4, 5]);
final Uint8List chunkTwo = Uint8List.fromList(<int>[6, 7, 8, 9, 10]);
MockHttpClientResponse response;
......@@ -47,24 +46,24 @@ void main() {
test('Converts an HttpClientResponse with contentLength to bytes', () async {
when(response.contentLength)
.thenReturn(chunkOne.length + chunkTwo.length);
final List<int> bytes = (await getHttpClientResponseBytes(response))
.materialize().asUint8List();
final List<int> bytes =
await consolidateHttpClientResponseBytes(response);
expect(bytes, <int>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
});
test('Converts a compressed HttpClientResponse with contentLength to bytes', () async {
when(response.contentLength).thenReturn(chunkOne.length);
final List<int> bytes = (await getHttpClientResponseBytes(response))
.materialize().asUint8List();
final List<int> bytes =
await consolidateHttpClientResponseBytes(response);
expect(bytes, <int>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
});
test('Converts an HttpClientResponse without contentLength to bytes', () async {
when(response.contentLength).thenReturn(-1);
final List<int> bytes = (await getHttpClientResponseBytes(response))
.materialize().asUint8List();
final List<int> bytes =
await consolidateHttpClientResponseBytes(response);
expect(bytes, <int>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
});
......@@ -73,7 +72,7 @@ void main() {
final int syntheticTotal = (chunkOne.length + chunkTwo.length) * 2;
when(response.contentLength).thenReturn(syntheticTotal);
final List<int> records = <int>[];
await getHttpClientResponseBytes(
await consolidateHttpClientResponseBytes(
response,
onBytesReceived: (int cumulative, int total) {
records.addAll(<int>[cumulative, total]);
......@@ -111,13 +110,13 @@ void main() {
});
when(response.contentLength).thenReturn(-1);
expect(getHttpClientResponseBytes(response),
expect(consolidateHttpClientResponseBytes(response),
throwsA(isInstanceOf<Exception>()));
});
test('Propagates error to Future return value if onBytesReceived throws', () async {
when(response.contentLength).thenReturn(-1);
final Future<TransferableTypedData> result = getHttpClientResponseBytes(
final Future<List<int>> result = consolidateHttpClientResponseBytes(
response,
onBytesReceived: (int cumulative, int total) {
throw 'misbehaving callback';
......@@ -158,14 +157,14 @@ void main() {
test('Uncompresses GZIP bytes if autoUncompress is true and response.compressionState is compressed', () async {
when(response.compressionState).thenReturn(HttpClientResponseCompressionState.compressed);
when(response.contentLength).thenReturn(gzipped.length);
final List<int> bytes = (await getHttpClientResponseBytes(response)).materialize().asUint8List();
final List<int> bytes = await consolidateHttpClientResponseBytes(response);
expect(bytes, <int>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
});
test('returns gzipped bytes if autoUncompress is false and response.compressionState is compressed', () async {
when(response.compressionState).thenReturn(HttpClientResponseCompressionState.compressed);
when(response.contentLength).thenReturn(gzipped.length);
final List<int> bytes = (await getHttpClientResponseBytes(response, autoUncompress: false)).materialize().asUint8List();
final List<int> bytes = await consolidateHttpClientResponseBytes(response, autoUncompress: false);
expect(bytes, gzipped);
});
......@@ -173,7 +172,7 @@ void main() {
when(response.compressionState).thenReturn(HttpClientResponseCompressionState.compressed);
when(response.contentLength).thenReturn(gzipped.length);
final List<int> records = <int>[];
await getHttpClientResponseBytes(
await consolidateHttpClientResponseBytes(
response,
onBytesReceived: (int cumulative, int total) {
records.addAll(<int>[cumulative, total]);
......@@ -193,7 +192,7 @@ void main() {
when(response.compressionState).thenReturn(HttpClientResponseCompressionState.decompressed);
when(response.contentLength).thenReturn(syntheticTotal);
final List<int> records = <int>[];
await getHttpClientResponseBytes(
await consolidateHttpClientResponseBytes(
response,
onBytesReceived: (int cumulative, int total) {
records.addAll(<int>[cumulative, total]);
......
......@@ -144,11 +144,26 @@ void main() {
});
group(NetworkImage, () {
MockHttpClient httpClient;
setUp(() {
httpClient = MockHttpClient();
debugNetworkImageHttpClientProvider = () => httpClient;
});
tearDown(() {
debugNetworkImageHttpClientProvider = null;
});
test('Expect thrown exception with statusCode', () async {
final int errorStatusCode = HttpStatus.notFound;
const String requestUrl = 'foo-url';
debugNetworkImageHttpClientProvider = returnErrorStatusCode;
final MockHttpClientRequest request = MockHttpClientRequest();
final MockHttpClientResponse response = MockHttpClientResponse();
when(httpClient.getUrl(any)).thenAnswer((_) => Future<HttpClientRequest>.value(request));
when(request.close()).thenAnswer((_) => Future<HttpClientResponse>.value(response));
when(response.statusCode).thenReturn(errorStatusCode);
final Completer<dynamic> caughtError = Completer<dynamic>();
......@@ -173,40 +188,32 @@ void main() {
});
test('Uses the HttpClient provided by debugNetworkImageHttpClientProvider if set', () async {
debugNetworkImageHttpClientProvider = throwOnAnyClient1;
when(httpClient.getUrl(any)).thenThrow('client1');
final List<dynamic> capturedErrors = <dynamic>[];
Future<void> loadNetworkImage() async {
final NetworkImage networkImage = NetworkImage(nonconst('foo'));
final Completer<bool> completer = Completer<bool>();
networkImage.load(networkImage).addListener(ImageStreamListener(
(ImageInfo image, bool synchronousCall) {
completer.complete(true);
},
final ImageStreamCompleter completer = networkImage.load(networkImage);
completer.addListener(ImageStreamListener(
(ImageInfo image, bool synchronousCall) { },
onError: (dynamic error, StackTrace stackTrace) {
capturedErrors.add(error);
completer.complete(false);
},
));
await completer.future;
await Future<void>.value();
}
await loadNetworkImage();
expect(capturedErrors, isNotNull);
expect(capturedErrors.length, 1);
expect(capturedErrors[0], equals('client1'));
debugNetworkImageHttpClientProvider = throwOnAnyClient2;
expect(capturedErrors, <dynamic>['client1']);
final MockHttpClient client2 = MockHttpClient();
when(client2.getUrl(any)).thenThrow('client2');
debugNetworkImageHttpClientProvider = () => client2;
await loadNetworkImage();
expect(capturedErrors, isNotNull);
expect(capturedErrors.length, 2);
expect(capturedErrors[0], equals('client1'));
expect(capturedErrors[1], equals('client2'));
expect(capturedErrors, <dynamic>['client1', 'client2']);
}, skip: isBrowser);
test('Propagates http client errors during resolve()', () async {
debugNetworkImageHttpClientProvider = throwErrorOnAny;
when(httpClient.getUrl(any)).thenThrow(Error());
bool uncaught = false;
await runZoned(() async {
......@@ -230,145 +237,14 @@ void main() {
});
test('Notifies listeners of chunk events', () async {
debugNetworkImageHttpClientProvider = respondOnAny;
const int chunkSize = 8;
final List<Uint8List> chunks = createChunks(chunkSize);
final List<Uint8List> chunks = <Uint8List>[
for (int offset = 0; offset < kTransparentImage.length; offset += chunkSize)
Uint8List.fromList(kTransparentImage.skip(offset).take(chunkSize).toList()),
];
final Completer<void> imageAvailable = Completer<void>();
final ImageProvider imageProvider = NetworkImage(nonconst('foo'));
final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
final List<ImageChunkEvent> events = <ImageChunkEvent>[];
result.addListener(ImageStreamListener(
(ImageInfo image, bool synchronousCall) {
imageAvailable.complete();
},
onChunk: (ImageChunkEvent event) {
events.add(event);
},
onError: (dynamic error, StackTrace stackTrace) {
imageAvailable.completeError(error, stackTrace);
},
));
await imageAvailable.future;
expect(events.length, chunks.length);
for (int i = 0; i < events.length; i++) {
expect(events[i].cumulativeBytesLoaded, math.min((i + 1) * chunkSize, kTransparentImage.length));
expect(events[i].expectedTotalBytes, kTransparentImage.length);
}
}, skip: isBrowser);
test('Uses http request headers', () async {
debugNetworkImageHttpClientProvider = respondOnAnyWithHeaders;
final Completer<bool> imageAvailable = Completer<bool>();
final ImageProvider imageProvider = NetworkImage(nonconst('foo'),
headers: const <String, String>{'flutter': 'flutter'},
);
final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
result.addListener(ImageStreamListener(
(ImageInfo image, bool synchronousCall) {
imageAvailable.complete(true);
},
onError: (dynamic error, StackTrace stackTrace) {
imageAvailable.completeError(error, stackTrace);
},
));
expect(await imageAvailable.future, isTrue);
}, skip: isBrowser);
test('Handles http stream errors', () async {
debugNetworkImageHttpClientProvider = respondErrorOnAny;
final Completer<String> imageAvailable = Completer<String>();
final ImageProvider imageProvider = NetworkImage(nonconst('bar'));
final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
final List<ImageChunkEvent> events = <ImageChunkEvent>[];
result.addListener(ImageStreamListener(
(ImageInfo image, bool synchronousCall) {
imageAvailable.complete(null);
},
onChunk: (ImageChunkEvent event) {
events.add(event);
},
onError: (dynamic error, StackTrace stackTrace) {
imageAvailable.complete(error);
},
));
final String error = await imageAvailable.future;
expect(error, 'failed chunk');
}, skip: isBrowser);
test('Handles http connection errors', () async {
debugNetworkImageHttpClientProvider = respondErrorOnConnection;
final Completer<dynamic> imageAvailable = Completer<dynamic>();
final ImageProvider imageProvider = NetworkImage(nonconst('baz'));
final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
result.addListener(ImageStreamListener(
(ImageInfo image, bool synchronousCall) {
imageAvailable.complete(null);
},
onError: (dynamic error, StackTrace stackTrace) {
imageAvailable.complete(error);
},
));
final dynamic err = await imageAvailable.future;
expect(err, const TypeMatcher<NetworkImageLoadException>()
.having((NetworkImageLoadException e) => e.toString(), 'e', startsWith('HTTP request failed'))
.having((NetworkImageLoadException e) => e.statusCode, 'statusCode', HttpStatus.badGateway)
.having((NetworkImageLoadException e) => e.uri.toString(), 'uri', endsWith('/baz')));
}, skip: isBrowser);
});
});
}
class MockHttpClient extends Mock implements HttpClient {}
class MockHttpClientRequest extends Mock implements HttpClientRequest {}
class MockHttpClientResponse extends Mock implements HttpClientResponse {}
class MockHttpHeaders extends Mock implements HttpHeaders {}
HttpClient returnErrorStatusCode() {
final int errorStatusCode = HttpStatus.notFound;
debugNetworkImageHttpClientProvider = returnErrorStatusCode;
final MockHttpClientRequest request = MockHttpClientRequest();
final MockHttpClientResponse response = MockHttpClientResponse();
final MockHttpClient httpClient = MockHttpClient();
when(httpClient.getUrl(any)).thenAnswer((_) => Future<HttpClientRequest>.value(request));
when(request.close()).thenAnswer((_) => Future<HttpClientResponse>.value(response));
when(response.statusCode).thenReturn(errorStatusCode);
return httpClient;
}
HttpClient throwOnAnyClient1() {
final MockHttpClient httpClient = MockHttpClient();
when(httpClient.getUrl(any)).thenThrow('client1');
return httpClient;
}
HttpClient throwOnAnyClient2() {
final MockHttpClient httpClient = MockHttpClient();
when(httpClient.getUrl(any)).thenThrow('client2');
return httpClient;
}
HttpClient throwErrorOnAny() {
final MockHttpClient httpClient = MockHttpClient();
when(httpClient.getUrl(any)).thenThrow(Exception());
return httpClient;
}
HttpClient respondOnAny() {
const int chunkSize = 8;
final List<Uint8List> chunks = createChunks(chunkSize);
final MockHttpClientRequest request = MockHttpClientRequest();
final MockHttpClientResponse response = MockHttpClientResponse();
final MockHttpClient httpClient = MockHttpClient();
when(httpClient.getUrl(any)).thenAnswer((_) => Future<HttpClientRequest>.value(request));
when(request.close()).thenAnswer((_) => Future<HttpClientResponse>.value(response));
when(response.statusCode).thenReturn(HttpStatus.ok);
......@@ -379,7 +255,7 @@ HttpClient respondOnAny() {
onError: anyNamed('onError'),
cancelOnError: anyNamed('cancelOnError'),
)).thenAnswer((Invocation invocation) {
final void Function(Uint8List) onData = invocation.positionalArguments[0];
final void Function(List<int>) onData = invocation.positionalArguments[0];
final void Function(Object) onError = invocation.namedArguments[#onError];
final void Function() onDone = invocation.namedArguments[#onDone];
final bool cancelOnError = invocation.namedArguments[#cancelOnError];
......@@ -391,106 +267,32 @@ HttpClient respondOnAny() {
cancelOnError: cancelOnError,
);
});
return httpClient;
}
HttpClient respondOnAnyWithHeaders() {
final List<Invocation> invocations = <Invocation>[];
const int chunkSize = 8;
final List<Uint8List> chunks = createChunks(chunkSize);
final MockHttpClientRequest request = MockHttpClientRequest();
final MockHttpClientResponse response = MockHttpClientResponse();
final MockHttpClient httpClient = MockHttpClient();
final MockHttpHeaders headers = MockHttpHeaders();
when(httpClient.getUrl(any)).thenAnswer((_) => Future<HttpClientRequest>.value(request));
when(request.headers).thenReturn(headers);
when(headers.add(any, any)).thenAnswer((Invocation invocation) {
invocations.add(invocation);
});
when(request.close()).thenAnswer((Invocation invocation) {
if (invocations.length == 1 &&
invocations[0].positionalArguments.length == 2 &&
invocations[0].positionalArguments[0] == 'flutter' &&
invocations[0].positionalArguments[1] == 'flutter') {
return Future<HttpClientResponse>.value(response);
} else {
return Future<HttpClientResponse>.value(null);
final ImageProvider imageProvider = NetworkImage(nonconst('foo'));
final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
final List<ImageChunkEvent> events = <ImageChunkEvent>[];
result.addListener(ImageStreamListener(
(ImageInfo image, bool synchronousCall) {
imageAvailable.complete();
},
onChunk: (ImageChunkEvent event) {
events.add(event);
},
onError: (dynamic error, StackTrace stackTrace) {
imageAvailable.completeError(error, stackTrace);
},
));
await imageAvailable.future;
expect(events.length, chunks.length);
for (int i = 0; i < events.length; i++) {
expect(events[i].cumulativeBytesLoaded, math.min((i + 1) * chunkSize, kTransparentImage.length));
expect(events[i].expectedTotalBytes, kTransparentImage.length);
}
}, skip: isBrowser);
});
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(Uint8List) onData = invocation.positionalArguments[0];
final void Function(Object) onError = invocation.namedArguments[#onError];
final void Function() onDone = invocation.namedArguments[#onDone];
final bool cancelOnError = invocation.namedArguments[#cancelOnError];
return Stream<Uint8List>.fromIterable(chunks).listen(
onData,
onDone: onDone,
onError: onError,
cancelOnError: cancelOnError,
);
});
return httpClient;
}
HttpClient respondErrorOnConnection() {
final MockHttpClientRequest request = MockHttpClientRequest();
final MockHttpClientResponse response = MockHttpClientResponse();
final MockHttpClient httpClient = MockHttpClient();
when(httpClient.getUrl(any)).thenAnswer((_) => Future<HttpClientRequest>.value(request));
when(request.close()).thenAnswer((_) => Future<HttpClientResponse>.value(response));
when(response.statusCode).thenReturn(HttpStatus.badGateway);
return httpClient;
}
HttpClient respondErrorOnAny() {
const int chunkSize = 8;
final MockHttpClientRequest request = MockHttpClientRequest();
final MockHttpClientResponse response = MockHttpClientResponse();
final MockHttpClient httpClient = MockHttpClient();
when(httpClient.getUrl(any)).thenAnswer((_) => Future<HttpClientRequest>.value(request));
when(request.close()).thenAnswer((_) => Future<HttpClientResponse>.value(response));
when(response.statusCode).thenReturn(HttpStatus.ok);
when(response.contentLength).thenReturn(kTransparentImage.length);
when(response.listen(
any,
onDone: anyNamed('onDone'),
onError: anyNamed('onError'),
cancelOnError: anyNamed('cancelOnError'),
)).thenAnswer((Invocation invocation) {
final void Function(Uint8List) onData = invocation.positionalArguments[0];
final void Function(Object) onError = invocation.namedArguments[#onError];
final void Function() onDone = invocation.namedArguments[#onDone];
final bool cancelOnError = invocation.namedArguments[#cancelOnError];
return createRottenChunks(chunkSize).listen(
onData,
onDone: onDone,
onError: onError,
cancelOnError: cancelOnError,
);
});
return httpClient;
}
List<Uint8List> createChunks(int chunkSize) {
final List<Uint8List> chunks = <Uint8List>[
for (int offset = 0; offset < kTransparentImage.length; offset += chunkSize)
Uint8List.fromList(kTransparentImage.skip(offset).take(chunkSize).toList()),
];
return chunks;
}
Stream<Uint8List> createRottenChunks(int chunkSize) async* {
yield Uint8List.fromList(kTransparentImage.take(chunkSize).toList());
throw 'failed chunk';
}
class MockHttpClient extends Mock implements HttpClient {}
class MockHttpClientRequest extends Mock implements HttpClientRequest {}
class MockHttpClientResponse extends Mock implements HttpClientResponse {}
// Copyright 2017 The Chromium 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 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import '../painting/image_data.dart';
void main() {
final MockHttpClient client = MockHttpClient();
final MockHttpClientRequest request = MockHttpClientRequest();
final MockHttpClientResponse response = MockHttpClientResponse();
final MockHttpHeaders headers = MockHttpHeaders();
testWidgets('Headers', (WidgetTester tester) async {
HttpOverrides.runZoned<Future<void>>(() async {
await tester.pumpWidget(Image.network(
'https://www.example.com/images/frame.png',
headers: const <String, String>{'flutter': 'flutter'},
));
verify(headers.add('flutter', 'flutter')).called(1);
}, createHttpClient: (SecurityContext _) {
when(client.getUrl(any)).thenAnswer((_) => Future<HttpClientRequest>.value(request));
when(request.headers).thenReturn(headers);
when(request.close()).thenAnswer((_) => Future<HttpClientResponse>.value(response));
when(response.contentLength).thenReturn(kTransparentImage.length);
when(response.statusCode).thenReturn(HttpStatus.ok);
when(response.listen(any)).thenAnswer((Invocation invocation) {
final void Function(List<int>) onData = invocation.positionalArguments[0];
final void Function() onDone = invocation.namedArguments[#onDone];
final void Function(Object, [ StackTrace ]) onError = invocation.namedArguments[#onError];
final bool cancelOnError = invocation.namedArguments[#cancelOnError];
return Stream<List<int>>.fromIterable(<List<int>>[kTransparentImage]).listen(onData, onDone: onDone, onError: onError, cancelOnError: cancelOnError);
});
return client;
});
}, skip: isBrowser);
}
class MockHttpClient extends Mock implements HttpClient {}
class MockHttpClientRequest extends Mock implements HttpClientRequest {}
class MockHttpClientResponse extends Mock implements HttpClientResponse {}
class MockHttpHeaders extends Mock implements HttpHeaders {}
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