Retry on failed download. (#12293)

diff --git a/packages/flutter_tools/test/base/net_test.dart b/packages/flutter_tools/test/base/net_test.dart
new file mode 100644
index 0000000..3840750
--- /dev/null
+++ b/packages/flutter_tools/test/base/net_test.dart
@@ -0,0 +1,115 @@
+// 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 'package:flutter_tools/src/base/io.dart' as io;
+import 'package:flutter_tools/src/base/net.dart';
+import 'package:quiver/testing/async.dart';
+import 'package:test/test.dart';
+
+import '../src/context.dart';
+
+void main() {
+  testUsingContext('retry from 500', () async {
+    String error;
+    new FakeAsync().run((FakeAsync time) {
+      fetchUrl(Uri.parse('http://example.invalid/')).then((List<int> value) {
+        error = 'test completed unexpectedly';
+      }, onError: (dynamic error) {
+        error = 'test failed unexpectedly';
+      });
+      expect(testLogger.statusText, '');
+      time.elapse(const Duration(milliseconds: 10000));
+      expect(testLogger.statusText,
+        'Download failed -- attempting retry 1 in 1 second...\n'
+        'Download failed -- attempting retry 2 in 2 seconds...\n'
+        'Download failed -- attempting retry 3 in 4 seconds...\n'
+        'Download failed -- attempting retry 4 in 8 seconds...\n'
+      );
+    });
+    expect(testLogger.errorText, isEmpty);
+    expect(error, isNull);
+  }, overrides: <Type, Generator>{
+    HttpClientFactory: () => () => new MockHttpClient(500),
+  });
+
+  testUsingContext('retry from network error', () async {
+    String error;
+    new FakeAsync().run((FakeAsync time) {
+      fetchUrl(Uri.parse('http://example.invalid/')).then((List<int> value) {
+        error = 'test completed unexpectedly';
+      }, onError: (dynamic error) {
+        error = 'test failed unexpectedly';
+      });
+      expect(testLogger.statusText, '');
+      time.elapse(const Duration(milliseconds: 10000));
+      expect(testLogger.statusText,
+        'Download failed -- attempting retry 1 in 1 second...\n'
+        'Download failed -- attempting retry 2 in 2 seconds...\n'
+        'Download failed -- attempting retry 3 in 4 seconds...\n'
+        'Download failed -- attempting retry 4 in 8 seconds...\n'
+      );
+    });
+    expect(testLogger.errorText, isEmpty);
+    expect(error, isNull);
+  }, overrides: <Type, Generator>{
+    HttpClientFactory: () => () => new MockHttpClient(200),
+  });
+}
+
+class MockHttpClient implements io.HttpClient {
+  MockHttpClient(this.statusCode);
+
+  final int statusCode;
+
+  @override
+  Future<io.HttpClientRequest> getUrl(Uri url) async {
+    return new MockHttpClientRequest(statusCode);
+  }
+
+  @override
+  dynamic noSuchMethod(Invocation invocation) {
+    throw 'io.HttpClient - $invocation';
+  }
+}
+
+class MockHttpClientRequest implements io.HttpClientRequest {
+  MockHttpClientRequest(this.statusCode);
+
+  final int statusCode;
+
+  @override
+  Future<io.HttpClientResponse> close() async {
+    return new MockHttpClientResponse(statusCode);
+  }
+
+  @override
+  dynamic noSuchMethod(Invocation invocation) {
+    throw 'io.HttpClientRequest - $invocation';
+  }
+}
+
+class MockHttpClientResponse implements io.HttpClientResponse {
+  MockHttpClientResponse(this.statusCode);
+
+  @override
+  final int statusCode;
+
+  @override
+  String get reasonPhrase => '<reason phrase>';
+
+  @override
+  StreamSubscription<List<int>> listen(void onData(List<int> event), {
+    Function onError, void onDone(), bool cancelOnError
+  }) {
+    return new Stream<List<int>>.fromFuture(new Future<List<int>>.error(const io.SocketException('test')))
+      .listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError);
+  }
+
+  @override
+  dynamic noSuchMethod(Invocation invocation) {
+    throw 'io.HttpClientResponse - $invocation';
+  }
+}
diff --git a/packages/flutter_tools/test/cache_test.dart b/packages/flutter_tools/test/cache_test.dart
index b921260..a4ef941 100644
--- a/packages/flutter_tools/test/cache_test.dart
+++ b/packages/flutter_tools/test/cache_test.dart
@@ -48,6 +48,7 @@
       Platform: () => new FakePlatform()..environment = <String, String>{'FLUTTER_ALREADY_LOCKED': 'true'},
     });
   });
+
   group('Cache', () {
     test('should not be up to date, if some cached artifact is not', () {
       final CachedArtifact artifact1 = new MockCachedArtifact();
diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart
index 6d89f62..8913488 100644
--- a/packages/flutter_tools/test/src/context.dart
+++ b/packages/flutter_tools/test/src/context.dart
@@ -60,7 +60,8 @@
     ..putIfAbsent(SimControl, () => new MockSimControl())
     ..putIfAbsent(Usage, () => new MockUsage())
     ..putIfAbsent(FlutterVersion, () => new MockFlutterVersion())
-    ..putIfAbsent(Clock, () => const Clock());
+    ..putIfAbsent(Clock, () => const Clock())
+    ..putIfAbsent(HttpClient, () => new MockHttpClient());
 }
 
 void testUsingContext(String description, dynamic testMethod(), {
@@ -244,3 +245,5 @@
 class MockFlutterVersion extends Mock implements FlutterVersion {}
 
 class MockClock extends Mock implements Clock {}
+
+class MockHttpClient extends Mock implements HttpClient {}