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 @@ ...@@ -5,7 +5,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data'; import 'dart:typed_data';
/// Signature for getting notified when chunks of bytes are received while /// Signature for getting notified when chunks of bytes are received while
...@@ -23,11 +22,11 @@ import 'dart:typed_data'; ...@@ -23,11 +22,11 @@ import 'dart:typed_data';
/// returned to the client and the total size of the response may not be known /// returned to the client and the total size of the response may not be known
/// until the request has been fully processed). /// 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); typedef BytesReceivedCallback = void Function(int cumulative, int total);
/// Efficiently converts the response body of an [HttpClientResponse] into a /// Efficiently converts the response body of an [HttpClientResponse] into a
/// [TransferableTypedData]. /// [Uint8List].
/// ///
/// The future returned will forward any error emitted by `response`. /// The future returned will forward any error emitted by `response`.
/// ///
...@@ -44,13 +43,13 @@ typedef BytesReceivedCallback = void Function(int cumulative, int total); ...@@ -44,13 +43,13 @@ typedef BytesReceivedCallback = void Function(int cumulative, int total);
/// bytes from this method (assuming the response is sending compressed bytes), /// bytes from this method (assuming the response is sending compressed bytes),
/// set both [HttpClient.autoUncompress] to false and the `autoUncompress` /// set both [HttpClient.autoUncompress] to false and the `autoUncompress`
/// parameter to false. /// parameter to false.
Future<TransferableTypedData> getHttpClientResponseBytes( Future<Uint8List> consolidateHttpClientResponseBytes(
HttpClientResponse response, { HttpClientResponse response, {
bool autoUncompress = true, bool autoUncompress = true,
BytesReceivedCallback onBytesReceived, BytesReceivedCallback onBytesReceived,
}) { }) {
assert(autoUncompress != null); assert(autoUncompress != null);
final Completer<TransferableTypedData> completer = Completer<TransferableTypedData>.sync(); final Completer<Uint8List> completer = Completer<Uint8List>.sync();
final _OutputBuffer output = _OutputBuffer(); final _OutputBuffer output = _OutputBuffer();
ByteConversionSink sink = output; ByteConversionSink sink = output;
...@@ -90,54 +89,41 @@ Future<TransferableTypedData> getHttpClientResponseBytes( ...@@ -90,54 +89,41 @@ Future<TransferableTypedData> getHttpClientResponseBytes(
} }
}, onDone: () { }, onDone: () {
sink.close(); sink.close();
completer.complete(TransferableTypedData.fromList(output.chunks)); completer.complete(output.bytes);
}, onError: completer.completeError, cancelOnError: true); }, onError: completer.completeError, cancelOnError: true);
return completer.future; 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 { class _OutputBuffer extends ByteConversionSinkBase {
final List<Uint8List> chunks = <Uint8List>[]; List<List<int>> _chunks = <List<int>>[];
int _contentLength = 0;
Uint8List _bytes;
@override @override
void add(List<int> chunk) { void add(List<int> chunk) {
chunks.add(chunk); assert(_bytes == null);
_chunks.add(chunk);
_contentLength += chunk.length;
} }
@override @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 @@ ...@@ -4,7 +4,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data'; import 'dart:typed_data';
import 'dart:ui' as ui; import 'dart:ui' as ui;
...@@ -15,7 +14,7 @@ import 'debug.dart'; ...@@ -15,7 +14,7 @@ import 'debug.dart';
import 'image_provider.dart' as image_provider; import 'image_provider.dart' as image_provider;
import 'image_stream.dart'; 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 { class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkImage> implements image_provider.NetworkImage {
/// Creates an object that fetches the image at the given URL. /// Creates an object that fetches the image at the given URL.
/// ///
...@@ -40,9 +39,9 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm ...@@ -40,9 +39,9 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
@override @override
ImageStreamCompleter load(image_provider.NetworkImage key) { ImageStreamCompleter load(image_provider.NetworkImage key) {
// Ownership of this controller is handed off to [_loadAsync]; // Ownership of this controller is handed off to [_loadAsync]; it is that
// it is that method's responsibility to close the controller's stream when // method's responsibility to close the controller's stream when the image
// the image has been loaded or an error is thrown. // has been loaded or an error is thrown.
final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>(); final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
return MultiFrameImageStreamCompleter( return MultiFrameImageStreamCompleter(
...@@ -58,130 +57,54 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm ...@@ -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 // Do not access this field directly; use [_httpClient] instead.
// itself that is used as an indicator that successive load requests should be // We set `autoUncompress` to false to ensure that we can trust the value of
// added to the list of pending load requests [_pendingLoadRequests]. // the `Content-Length` HTTP header. We automatically uncompress the content
static Future<void> _pendingLoader; // in our call to [consolidateHttpClientResponseBytes].
static RawReceivePort _loaderErrorHandler; static final HttpClient _sharedHttpClient = HttpClient()..autoUncompress = false;
static List<_DownloadRequest> _pendingLoadRequests;
static SendPort _requestPort; static HttpClient get _httpClient {
HttpClient client = _sharedHttpClient;
assert(() {
if (debugNetworkImageHttpClientProvider != null)
client = debugNetworkImageHttpClientProvider();
return true;
}());
return client;
}
Future<ui.Codec> _loadAsync( Future<ui.Codec> _loadAsync(
NetworkImage key, NetworkImage key,
StreamController<ImageChunkEvent> chunkEvents, StreamController<ImageChunkEvent> chunkEvents,
) async { ) async {
RawReceivePort downloadResponseHandler;
try { try {
assert(key == this); assert(key == this);
final Uri resolved = Uri.base.resolve(key.url); final Uri resolved = Uri.base.resolve(key.url);
final HttpClientRequest request = await _httpClient.getUrl(resolved);
final Completer<TransferableTypedData> bytesCompleter = Completer<TransferableTypedData>(); headers?.forEach((String name, String value) {
downloadResponseHandler = RawReceivePort((_DownloadResponse response) { request.headers.add(name, value);
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 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 final Uint8List bytes = await consolidateHttpClientResponseBytes(
// out of release builds. response,
HttpClientProvider httpClientProvider; onBytesReceived: (int cumulative, int total) {
assert(() { httpClientProvider = debugNetworkImageHttpClientProvider; return true; }()); chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded: cumulative,
final _DownloadRequest downloadRequest = _DownloadRequest( expectedTotalBytes: total,
downloadResponseHandler.sendPort, ));
resolved, },
headers,
httpClientProvider,
); );
if (_requestPort != null) { if (bytes.lengthInBytes == 0)
// 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)
throw Exception('NetworkImage is an empty file: $resolved'); throw Exception('NetworkImage is an empty file: $resolved');
return PaintingBinding.instance.instantiateImageCodec(bytes); return PaintingBinding.instance.instantiateImageCodec(bytes);
} finally { } finally {
chunkEvents.close(); 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 @override
...@@ -189,7 +112,8 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm ...@@ -189,7 +112,8 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
if (other.runtimeType != runtimeType) if (other.runtimeType != runtimeType)
return false; return false;
final NetworkImage typedOther = other; final NetworkImage typedOther = other;
return url == typedOther.url && scale == typedOther.scale; return url == typedOther.url
&& scale == typedOther.scale;
} }
@override @override
...@@ -198,95 +122,3 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm ...@@ -198,95 +122,3 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
@override @override
String toString() => '$runtimeType("$url", scale: $scale)'; 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 @@ ...@@ -5,7 +5,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
...@@ -121,8 +120,8 @@ class NetworkAssetBundle extends AssetBundle { ...@@ -121,8 +120,8 @@ class NetworkAssetBundle extends AssetBundle {
'Unable to load asset: $key\n' 'Unable to load asset: $key\n'
'HTTP status code: ${response.statusCode}' 'HTTP status code: ${response.statusCode}'
); );
final TransferableTypedData transferable = await getHttpClientResponseBytes(response); final Uint8List bytes = await consolidateHttpClientResponseBytes(response);
return transferable.materialize().asByteData(); return bytes.buffer.asByteData();
} }
/// Retrieve a string from the asset bundle, parse it with the given function, /// Retrieve a string from the asset bundle, parse it with the given function,
......
...@@ -6,7 +6,6 @@ ...@@ -6,7 +6,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
...@@ -15,7 +14,7 @@ import 'package:mockito/mockito.dart'; ...@@ -15,7 +14,7 @@ import 'package:mockito/mockito.dart';
import '../flutter_test_alternative.dart'; import '../flutter_test_alternative.dart';
void main() { void main() {
group(getHttpClientResponseBytes, () { group(consolidateHttpClientResponseBytes, () {
final Uint8List chunkOne = Uint8List.fromList(<int>[0, 1, 2, 3, 4, 5]); final Uint8List chunkOne = Uint8List.fromList(<int>[0, 1, 2, 3, 4, 5]);
final Uint8List chunkTwo = Uint8List.fromList(<int>[6, 7, 8, 9, 10]); final Uint8List chunkTwo = Uint8List.fromList(<int>[6, 7, 8, 9, 10]);
MockHttpClientResponse response; MockHttpClientResponse response;
...@@ -47,24 +46,24 @@ void main() { ...@@ -47,24 +46,24 @@ void main() {
test('Converts an HttpClientResponse with contentLength to bytes', () async { test('Converts an HttpClientResponse with contentLength to bytes', () async {
when(response.contentLength) when(response.contentLength)
.thenReturn(chunkOne.length + chunkTwo.length); .thenReturn(chunkOne.length + chunkTwo.length);
final List<int> bytes = (await getHttpClientResponseBytes(response)) final List<int> bytes =
.materialize().asUint8List(); await consolidateHttpClientResponseBytes(response);
expect(bytes, <int>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); expect(bytes, <int>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
}); });
test('Converts a compressed HttpClientResponse with contentLength to bytes', () async { test('Converts a compressed HttpClientResponse with contentLength to bytes', () async {
when(response.contentLength).thenReturn(chunkOne.length); when(response.contentLength).thenReturn(chunkOne.length);
final List<int> bytes = (await getHttpClientResponseBytes(response)) final List<int> bytes =
.materialize().asUint8List(); await consolidateHttpClientResponseBytes(response);
expect(bytes, <int>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); expect(bytes, <int>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
}); });
test('Converts an HttpClientResponse without contentLength to bytes', () async { test('Converts an HttpClientResponse without contentLength to bytes', () async {
when(response.contentLength).thenReturn(-1); when(response.contentLength).thenReturn(-1);
final List<int> bytes = (await getHttpClientResponseBytes(response)) final List<int> bytes =
.materialize().asUint8List(); await consolidateHttpClientResponseBytes(response);
expect(bytes, <int>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); expect(bytes, <int>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
}); });
...@@ -73,7 +72,7 @@ void main() { ...@@ -73,7 +72,7 @@ void main() {
final int syntheticTotal = (chunkOne.length + chunkTwo.length) * 2; final int syntheticTotal = (chunkOne.length + chunkTwo.length) * 2;
when(response.contentLength).thenReturn(syntheticTotal); when(response.contentLength).thenReturn(syntheticTotal);
final List<int> records = <int>[]; final List<int> records = <int>[];
await getHttpClientResponseBytes( await consolidateHttpClientResponseBytes(
response, response,
onBytesReceived: (int cumulative, int total) { onBytesReceived: (int cumulative, int total) {
records.addAll(<int>[cumulative, total]); records.addAll(<int>[cumulative, total]);
...@@ -111,13 +110,13 @@ void main() { ...@@ -111,13 +110,13 @@ void main() {
}); });
when(response.contentLength).thenReturn(-1); when(response.contentLength).thenReturn(-1);
expect(getHttpClientResponseBytes(response), expect(consolidateHttpClientResponseBytes(response),
throwsA(isInstanceOf<Exception>())); throwsA(isInstanceOf<Exception>()));
}); });
test('Propagates error to Future return value if onBytesReceived throws', () async { test('Propagates error to Future return value if onBytesReceived throws', () async {
when(response.contentLength).thenReturn(-1); when(response.contentLength).thenReturn(-1);
final Future<TransferableTypedData> result = getHttpClientResponseBytes( final Future<List<int>> result = consolidateHttpClientResponseBytes(
response, response,
onBytesReceived: (int cumulative, int total) { onBytesReceived: (int cumulative, int total) {
throw 'misbehaving callback'; throw 'misbehaving callback';
...@@ -158,14 +157,14 @@ void main() { ...@@ -158,14 +157,14 @@ void main() {
test('Uncompresses GZIP bytes if autoUncompress is true and response.compressionState is compressed', () async { test('Uncompresses GZIP bytes if autoUncompress is true and response.compressionState is compressed', () async {
when(response.compressionState).thenReturn(HttpClientResponseCompressionState.compressed); when(response.compressionState).thenReturn(HttpClientResponseCompressionState.compressed);
when(response.contentLength).thenReturn(gzipped.length); 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]); 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 { test('returns gzipped bytes if autoUncompress is false and response.compressionState is compressed', () async {
when(response.compressionState).thenReturn(HttpClientResponseCompressionState.compressed); when(response.compressionState).thenReturn(HttpClientResponseCompressionState.compressed);
when(response.contentLength).thenReturn(gzipped.length); 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); expect(bytes, gzipped);
}); });
...@@ -173,7 +172,7 @@ void main() { ...@@ -173,7 +172,7 @@ void main() {
when(response.compressionState).thenReturn(HttpClientResponseCompressionState.compressed); when(response.compressionState).thenReturn(HttpClientResponseCompressionState.compressed);
when(response.contentLength).thenReturn(gzipped.length); when(response.contentLength).thenReturn(gzipped.length);
final List<int> records = <int>[]; final List<int> records = <int>[];
await getHttpClientResponseBytes( await consolidateHttpClientResponseBytes(
response, response,
onBytesReceived: (int cumulative, int total) { onBytesReceived: (int cumulative, int total) {
records.addAll(<int>[cumulative, total]); records.addAll(<int>[cumulative, total]);
...@@ -193,7 +192,7 @@ void main() { ...@@ -193,7 +192,7 @@ void main() {
when(response.compressionState).thenReturn(HttpClientResponseCompressionState.decompressed); when(response.compressionState).thenReturn(HttpClientResponseCompressionState.decompressed);
when(response.contentLength).thenReturn(syntheticTotal); when(response.contentLength).thenReturn(syntheticTotal);
final List<int> records = <int>[]; final List<int> records = <int>[];
await getHttpClientResponseBytes( await consolidateHttpClientResponseBytes(
response, response,
onBytesReceived: (int cumulative, int total) { onBytesReceived: (int cumulative, int total) {
records.addAll(<int>[cumulative, total]); records.addAll(<int>[cumulative, total]);
......
...@@ -144,11 +144,26 @@ void main() { ...@@ -144,11 +144,26 @@ void main() {
}); });
group(NetworkImage, () { group(NetworkImage, () {
MockHttpClient httpClient;
setUp(() {
httpClient = MockHttpClient();
debugNetworkImageHttpClientProvider = () => httpClient;
});
tearDown(() {
debugNetworkImageHttpClientProvider = null;
});
test('Expect thrown exception with statusCode', () async { test('Expect thrown exception with statusCode', () async {
final int errorStatusCode = HttpStatus.notFound; final int errorStatusCode = HttpStatus.notFound;
const String requestUrl = 'foo-url'; 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>(); final Completer<dynamic> caughtError = Completer<dynamic>();
...@@ -173,40 +188,32 @@ void main() { ...@@ -173,40 +188,32 @@ void main() {
}); });
test('Uses the HttpClient provided by debugNetworkImageHttpClientProvider if set', () async { test('Uses the HttpClient provided by debugNetworkImageHttpClientProvider if set', () async {
debugNetworkImageHttpClientProvider = throwOnAnyClient1; when(httpClient.getUrl(any)).thenThrow('client1');
final List<dynamic> capturedErrors = <dynamic>[]; final List<dynamic> capturedErrors = <dynamic>[];
Future<void> loadNetworkImage() async { Future<void> loadNetworkImage() async {
final NetworkImage networkImage = NetworkImage(nonconst('foo')); final NetworkImage networkImage = NetworkImage(nonconst('foo'));
final Completer<bool> completer = Completer<bool>(); final ImageStreamCompleter completer = networkImage.load(networkImage);
networkImage.load(networkImage).addListener(ImageStreamListener( completer.addListener(ImageStreamListener(
(ImageInfo image, bool synchronousCall) { (ImageInfo image, bool synchronousCall) { },
completer.complete(true);
},
onError: (dynamic error, StackTrace stackTrace) { onError: (dynamic error, StackTrace stackTrace) {
capturedErrors.add(error); capturedErrors.add(error);
completer.complete(false);
}, },
)); ));
await completer.future; await Future<void>.value();
} }
await loadNetworkImage(); await loadNetworkImage();
expect(capturedErrors, isNotNull); expect(capturedErrors, <dynamic>['client1']);
expect(capturedErrors.length, 1); final MockHttpClient client2 = MockHttpClient();
expect(capturedErrors[0], equals('client1')); when(client2.getUrl(any)).thenThrow('client2');
debugNetworkImageHttpClientProvider = () => client2;
debugNetworkImageHttpClientProvider = throwOnAnyClient2;
await loadNetworkImage(); await loadNetworkImage();
expect(capturedErrors, isNotNull); expect(capturedErrors, <dynamic>['client1', 'client2']);
expect(capturedErrors.length, 2);
expect(capturedErrors[0], equals('client1'));
expect(capturedErrors[1], equals('client2'));
}, skip: isBrowser); }, skip: isBrowser);
test('Propagates http client errors during resolve()', () async { test('Propagates http client errors during resolve()', () async {
debugNetworkImageHttpClientProvider = throwErrorOnAny; when(httpClient.getUrl(any)).thenThrow(Error());
bool uncaught = false; bool uncaught = false;
await runZoned(() async { await runZoned(() async {
...@@ -230,16 +237,40 @@ void main() { ...@@ -230,16 +237,40 @@ void main() {
}); });
test('Notifies listeners of chunk events', () async { test('Notifies listeners of chunk events', () async {
debugNetworkImageHttpClientProvider = respondOnAny;
const int chunkSize = 8; 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 Completer<void> imageAvailable = Completer<void>();
final MockHttpClientRequest request = MockHttpClientRequest();
final MockHttpClientResponse response = MockHttpClientResponse();
when(httpClient.getUrl(any)).thenAnswer((_) => Future<HttpClientRequest>.value(request));
when(request.close()).thenAnswer((_) => Future<HttpClientResponse>.value(response));
when(response.statusCode).thenReturn(HttpStatus.ok);
when(response.contentLength).thenReturn(kTransparentImage.length);
when(response.listen(
any,
onDone: anyNamed('onDone'),
onError: anyNamed('onError'),
cancelOnError: anyNamed('cancelOnError'),
)).thenAnswer((Invocation invocation) {
final void Function(List<int>) onData = invocation.positionalArguments[0];
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,
);
});
final ImageProvider imageProvider = NetworkImage(nonconst('foo')); final ImageProvider imageProvider = NetworkImage(nonconst('foo'));
final ImageStream result = imageProvider.resolve(ImageConfiguration.empty); final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
final List<ImageChunkEvent> events = <ImageChunkEvent>[]; final List<ImageChunkEvent> events = <ImageChunkEvent>[];
result.addListener(ImageStreamListener( result.addListener(ImageStreamListener(
(ImageInfo image, bool synchronousCall) { (ImageInfo image, bool synchronousCall) {
imageAvailable.complete(); imageAvailable.complete();
...@@ -258,69 +289,6 @@ void main() { ...@@ -258,69 +289,6 @@ void main() {
expect(events[i].expectedTotalBytes, kTransparentImage.length); expect(events[i].expectedTotalBytes, kTransparentImage.length);
} }
}, skip: isBrowser); }, 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);
}); });
}); });
} }
...@@ -328,169 +296,3 @@ void main() { ...@@ -328,169 +296,3 @@ void main() {
class MockHttpClient extends Mock implements HttpClient {} class MockHttpClient extends Mock implements HttpClient {}
class MockHttpClientRequest extends Mock implements HttpClientRequest {} class MockHttpClientRequest extends Mock implements HttpClientRequest {}
class MockHttpClientResponse extends Mock implements HttpClientResponse {} 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);
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 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);
}
});
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';
}
// 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