mirror of
https://github.com/flutter/flutter
synced 2024-10-13 11:42:54 +00:00
Use separate isolate for image loading. (#34188)
* Use separate isolate for image loading. Use TransferableTypedData to const-cost receive bytes from that isolate.
This commit is contained in:
parent
3cf88fed6d
commit
b12bdd0ea1
|
@ -5,6 +5,7 @@
|
||||||
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
|
||||||
|
@ -22,11 +23,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 [consolidateHttpClientResponseBytes].
|
/// This is used in [getHttpClientResponseBytes].
|
||||||
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
|
||||||
/// [Uint8List].
|
/// [TransferableTypedData].
|
||||||
///
|
///
|
||||||
/// The future returned will forward any error emitted by `response`.
|
/// The future returned will forward any error emitted by `response`.
|
||||||
///
|
///
|
||||||
|
@ -43,13 +44,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<Uint8List> consolidateHttpClientResponseBytes(
|
Future<TransferableTypedData> getHttpClientResponseBytes(
|
||||||
HttpClientResponse response, {
|
HttpClientResponse response, {
|
||||||
bool autoUncompress = true,
|
bool autoUncompress = true,
|
||||||
BytesReceivedCallback onBytesReceived,
|
BytesReceivedCallback onBytesReceived,
|
||||||
}) {
|
}) {
|
||||||
assert(autoUncompress != null);
|
assert(autoUncompress != null);
|
||||||
final Completer<Uint8List> completer = Completer<Uint8List>.sync();
|
final Completer<TransferableTypedData> completer = Completer<TransferableTypedData>.sync();
|
||||||
|
|
||||||
final _OutputBuffer output = _OutputBuffer();
|
final _OutputBuffer output = _OutputBuffer();
|
||||||
ByteConversionSink sink = output;
|
ByteConversionSink sink = output;
|
||||||
|
@ -89,41 +90,54 @@ Future<Uint8List> consolidateHttpClientResponseBytes(
|
||||||
}
|
}
|
||||||
}, onDone: () {
|
}, onDone: () {
|
||||||
sink.close();
|
sink.close();
|
||||||
completer.complete(output.bytes);
|
completer.complete(TransferableTypedData.fromList(output.chunks));
|
||||||
}, 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 {
|
||||||
List<List<int>> _chunks = <List<int>>[];
|
final List<Uint8List> chunks = <Uint8List>[];
|
||||||
int _contentLength = 0;
|
|
||||||
Uint8List _bytes;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void add(List<int> chunk) {
|
void add(List<int> chunk) {
|
||||||
assert(_bytes == null);
|
chunks.add(chunk);
|
||||||
_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,6 +4,7 @@
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
|
@ -14,7 +15,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 implemenation of [image_provider.NetworkImage].
|
/// The dart:io implementation 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.
|
||||||
///
|
///
|
||||||
|
@ -39,9 +40,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]; it is that
|
// Ownership of this controller is handed off to [_loadAsync];
|
||||||
// method's responsibility to close the controller's stream when the image
|
// it is that method's responsibility to close the controller's stream when
|
||||||
// has been loaded or an error is thrown.
|
// the image has been loaded or an error is thrown.
|
||||||
final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
|
final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
|
||||||
|
|
||||||
return MultiFrameImageStreamCompleter(
|
return MultiFrameImageStreamCompleter(
|
||||||
|
@ -57,63 +58,138 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not access this field directly; use [_httpClient] instead.
|
// For [_pendingLoader] we don't need the value(worker isolate), just the future
|
||||||
// We set `autoUncompress` to false to ensure that we can trust the value of
|
// itself that is used as an indicator that successive load requests should be
|
||||||
// the `Content-Length` HTTP header. We automatically uncompress the content
|
// added to the list of pending load requests [_pendingLoadRequests].
|
||||||
// in our call to [consolidateHttpClientResponseBytes].
|
static Future<void> _pendingLoader;
|
||||||
static final HttpClient _sharedHttpClient = HttpClient()..autoUncompress = false;
|
static RawReceivePort _loaderErrorHandler;
|
||||||
|
static List<_DownloadRequest> _pendingLoadRequests;
|
||||||
static HttpClient get _httpClient {
|
static SendPort _requestPort;
|
||||||
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);
|
|
||||||
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);
|
|
||||||
|
|
||||||
final Uint8List bytes = await consolidateHttpClientResponseBytes(
|
final Completer<TransferableTypedData> bytesCompleter = Completer<TransferableTypedData>();
|
||||||
response,
|
downloadResponseHandler = RawReceivePort((_DownloadResponse response) {
|
||||||
onBytesReceived: (int cumulative, int total) {
|
if (response.bytes != null) {
|
||||||
chunkEvents.add(ImageChunkEvent(
|
if (bytesCompleter.isCompleted) {
|
||||||
cumulativeBytesLoaded: cumulative,
|
// If an uncaught error occurred in the worker isolate, we'll have
|
||||||
expectedTotalBytes: total,
|
// 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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,
|
||||||
);
|
);
|
||||||
if (bytes.lengthInBytes == 0)
|
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)
|
||||||
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
|
||||||
bool operator ==(dynamic other) {
|
bool operator ==(dynamic other) {
|
||||||
if (other.runtimeType != runtimeType)
|
if (other.runtimeType != runtimeType)
|
||||||
return false;
|
return false;
|
||||||
final NetworkImage typedOther = other;
|
final NetworkImage typedOther = other;
|
||||||
return url == typedOther.url
|
return url == typedOther.url && scale == typedOther.scale;
|
||||||
&& scale == typedOther.scale;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -122,3 +198,95 @@ 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,6 +5,7 @@
|
||||||
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';
|
||||||
|
@ -120,8 +121,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 Uint8List bytes = await consolidateHttpClientResponseBytes(response);
|
final TransferableTypedData transferable = await getHttpClientResponseBytes(response);
|
||||||
return bytes.buffer.asByteData();
|
return transferable.materialize().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,6 +6,7 @@
|
||||||
|
|
||||||
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';
|
||||||
|
@ -14,7 +15,7 @@ import 'package:mockito/mockito.dart';
|
||||||
import '../flutter_test_alternative.dart';
|
import '../flutter_test_alternative.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group(consolidateHttpClientResponseBytes, () {
|
group(getHttpClientResponseBytes, () {
|
||||||
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;
|
||||||
|
@ -46,24 +47,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 =
|
final List<int> bytes = (await getHttpClientResponseBytes(response))
|
||||||
await consolidateHttpClientResponseBytes(response);
|
.materialize().asUint8List();
|
||||||
|
|
||||||
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 =
|
final List<int> bytes = (await getHttpClientResponseBytes(response))
|
||||||
await consolidateHttpClientResponseBytes(response);
|
.materialize().asUint8List();
|
||||||
|
|
||||||
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 =
|
final List<int> bytes = (await getHttpClientResponseBytes(response))
|
||||||
await consolidateHttpClientResponseBytes(response);
|
.materialize().asUint8List();
|
||||||
|
|
||||||
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]);
|
||||||
});
|
});
|
||||||
|
@ -72,7 +73,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 consolidateHttpClientResponseBytes(
|
await getHttpClientResponseBytes(
|
||||||
response,
|
response,
|
||||||
onBytesReceived: (int cumulative, int total) {
|
onBytesReceived: (int cumulative, int total) {
|
||||||
records.addAll(<int>[cumulative, total]);
|
records.addAll(<int>[cumulative, total]);
|
||||||
|
@ -110,13 +111,13 @@ void main() {
|
||||||
});
|
});
|
||||||
when(response.contentLength).thenReturn(-1);
|
when(response.contentLength).thenReturn(-1);
|
||||||
|
|
||||||
expect(consolidateHttpClientResponseBytes(response),
|
expect(getHttpClientResponseBytes(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<List<int>> result = consolidateHttpClientResponseBytes(
|
final Future<TransferableTypedData> result = getHttpClientResponseBytes(
|
||||||
response,
|
response,
|
||||||
onBytesReceived: (int cumulative, int total) {
|
onBytesReceived: (int cumulative, int total) {
|
||||||
throw 'misbehaving callback';
|
throw 'misbehaving callback';
|
||||||
|
@ -157,14 +158,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 consolidateHttpClientResponseBytes(response);
|
final List<int> bytes = (await getHttpClientResponseBytes(response)).materialize().asUint8List();
|
||||||
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 consolidateHttpClientResponseBytes(response, autoUncompress: false);
|
final List<int> bytes = (await getHttpClientResponseBytes(response, autoUncompress: false)).materialize().asUint8List();
|
||||||
expect(bytes, gzipped);
|
expect(bytes, gzipped);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -172,7 +173,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 consolidateHttpClientResponseBytes(
|
await getHttpClientResponseBytes(
|
||||||
response,
|
response,
|
||||||
onBytesReceived: (int cumulative, int total) {
|
onBytesReceived: (int cumulative, int total) {
|
||||||
records.addAll(<int>[cumulative, total]);
|
records.addAll(<int>[cumulative, total]);
|
||||||
|
@ -192,7 +193,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 consolidateHttpClientResponseBytes(
|
await getHttpClientResponseBytes(
|
||||||
response,
|
response,
|
||||||
onBytesReceived: (int cumulative, int total) {
|
onBytesReceived: (int cumulative, int total) {
|
||||||
records.addAll(<int>[cumulative, total]);
|
records.addAll(<int>[cumulative, total]);
|
||||||
|
|
|
@ -144,26 +144,11 @@ 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';
|
||||||
|
|
||||||
final MockHttpClientRequest request = MockHttpClientRequest();
|
debugNetworkImageHttpClientProvider = returnErrorStatusCode;
|
||||||
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>();
|
||||||
|
|
||||||
|
@ -188,32 +173,40 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Uses the HttpClient provided by debugNetworkImageHttpClientProvider if set', () async {
|
test('Uses the HttpClient provided by debugNetworkImageHttpClientProvider if set', () async {
|
||||||
when(httpClient.getUrl(any)).thenThrow('client1');
|
debugNetworkImageHttpClientProvider = throwOnAnyClient1;
|
||||||
|
|
||||||
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 ImageStreamCompleter completer = networkImage.load(networkImage);
|
final Completer<bool> completer = Completer<bool>();
|
||||||
completer.addListener(ImageStreamListener(
|
networkImage.load(networkImage).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 Future<void>.value();
|
await completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadNetworkImage();
|
await loadNetworkImage();
|
||||||
expect(capturedErrors, <dynamic>['client1']);
|
expect(capturedErrors, isNotNull);
|
||||||
final MockHttpClient client2 = MockHttpClient();
|
expect(capturedErrors.length, 1);
|
||||||
when(client2.getUrl(any)).thenThrow('client2');
|
expect(capturedErrors[0], equals('client1'));
|
||||||
debugNetworkImageHttpClientProvider = () => client2;
|
|
||||||
|
debugNetworkImageHttpClientProvider = throwOnAnyClient2;
|
||||||
await loadNetworkImage();
|
await loadNetworkImage();
|
||||||
expect(capturedErrors, <dynamic>['client1', 'client2']);
|
expect(capturedErrors, isNotNull);
|
||||||
|
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 {
|
||||||
when(httpClient.getUrl(any)).thenThrow(Error());
|
debugNetworkImageHttpClientProvider = throwErrorOnAny;
|
||||||
bool uncaught = false;
|
bool uncaught = false;
|
||||||
|
|
||||||
await runZoned(() async {
|
await runZoned(() async {
|
||||||
|
@ -237,40 +230,16 @@ 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 = <Uint8List>[
|
final List<Uint8List> chunks = createChunks(chunkSize);
|
||||||
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();
|
||||||
|
@ -289,6 +258,69 @@ 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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -296,3 +328,169 @@ 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';
|
||||||
|
}
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
// 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 {}
|
|
Loading…
Reference in a new issue