diff --git a/packages/flutter_tools/lib/src/commands/packages.dart b/packages/flutter_tools/lib/src/commands/packages.dart index a75de92ec75..9ed504f69a5 100644 --- a/packages/flutter_tools/lib/src/commands/packages.dart +++ b/packages/flutter_tools/lib/src/commands/packages.dart @@ -103,7 +103,7 @@ class PackagesTestCommand extends FlutterCommand { } @override - Future runCommand() => pub(['run', 'test']..addAll(argResults.rest)); + Future runCommand() => pub(['run', 'test']..addAll(argResults.rest), retry: false); } class PackagesPassthroughCommand extends FlutterCommand { @@ -124,5 +124,5 @@ class PackagesPassthroughCommand extends FlutterCommand { } @override - Future runCommand() => pub(argResults.rest); + Future runCommand() => pub(argResults.rest, retry: false); } diff --git a/packages/flutter_tools/lib/src/dart/pub.dart b/packages/flutter_tools/lib/src/dart/pub.dart index 86898eb810c..c113c9ce320 100644 --- a/packages/flutter_tools/lib/src/dart/pub.dart +++ b/packages/flutter_tools/lib/src/dart/pub.dart @@ -4,6 +4,8 @@ import 'dart:async'; +import 'package:meta/meta.dart'; + import '../base/common.dart'; import '../base/file_system.dart'; import '../base/logger.dart'; @@ -55,7 +57,13 @@ Future pubGet({ if (offline) args.add('--offline'); try { - await pub(args, directory: directory, filter: _filterOverrideWarnings, failureMessage: 'pub $command failed'); + await pub( + args, + directory: directory, + filter: _filterOverrideWarnings, + failureMessage: 'pub $command failed', + retry: true, + ); } finally { status.stop(); } @@ -73,15 +81,29 @@ typedef String MessageFilter(String message); Future pub(List arguments, { String directory, MessageFilter filter, - String failureMessage: 'pub failed' + String failureMessage: 'pub failed', + @required bool retry, }) async { final List command = [ sdkBinaryName('pub') ]..addAll(arguments); - final int code = await runCommandAndStreamOutput( - command, - workingDirectory: directory, - mapFunction: filter, - environment: { 'FLUTTER_ROOT': Cache.flutterRoot, _pubEnvironmentKey: _getPubEnvironmentValue() } - ); + int attempts = 0; + int duration = 1; + int code; + while (true) { + attempts += 1; + code = await runCommandAndStreamOutput( + command, + workingDirectory: directory, + mapFunction: filter, + environment: { 'FLUTTER_ROOT': Cache.flutterRoot, _pubEnvironmentKey: _getPubEnvironmentValue() } + ); + if (code != 69) // UNAVAILABLE in https://github.com/dart-lang/pub/blob/master/lib/src/exit_codes.dart + break; + printStatus('$failureMessage ($code) -- attempting retry $attempts in $duration second${ duration == 1 ? "" : "s"}...'); + await new Future.delayed(new Duration(seconds: duration)); + if (duration < 64) + duration *= 2; + } + assert(code != null); if (code != 0) throwToolExit('$failureMessage ($code)', exitCode: code); } diff --git a/packages/flutter_tools/test/dart/pub_get_test.dart b/packages/flutter_tools/test/dart/pub_get_test.dart new file mode 100644 index 00000000000..cb4acd27a98 --- /dev/null +++ b/packages/flutter_tools/test/dart/pub_get_test.dart @@ -0,0 +1,171 @@ +// Copyright 2016 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:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_tools/src/base/io.dart'; +import 'package:flutter_tools/src/dart/pub.dart'; +import 'package:mockito/mockito.dart'; +import 'package:process/process.dart'; +import 'package:quiver/testing/async.dart'; +import 'package:test/test.dart'; + +import '../src/context.dart'; + +void main() { + testUsingContext('pub get 69', () async { + String error; + new FakeAsync().run((FakeAsync time) { + pubGet(checkLastModified: false).then((Null value) { + error = 'test completed unexpectedly'; + }, onError: (dynamic error) { + error = 'test failed unexpectedly'; + }); + expect(testLogger.statusText, ''); + time.elapse(const Duration(milliseconds: 500)); + expect(testLogger.statusText, + 'Running "flutter packages get" in /...\n' + 'pub get failed (69) -- attempting retry 1 in 1 second...\n' + ); + time.elapse(const Duration(milliseconds: 500)); + expect(testLogger.statusText, + 'Running "flutter packages get" in /...\n' + 'pub get failed (69) -- attempting retry 1 in 1 second...\n' + 'pub get failed (69) -- attempting retry 2 in 2 seconds...\n' + ); + time.elapse(const Duration(seconds: 1)); + expect(testLogger.statusText, + 'Running "flutter packages get" in /...\n' + 'pub get failed (69) -- attempting retry 1 in 1 second...\n' + 'pub get failed (69) -- attempting retry 2 in 2 seconds...\n' + ); + time.elapse(const Duration(seconds: 100)); // from t=0 to t=100 + expect(testLogger.statusText, + 'Running "flutter packages get" in /...\n' + 'pub get failed (69) -- attempting retry 1 in 1 second...\n' + 'pub get failed (69) -- attempting retry 2 in 2 seconds...\n' + 'pub get failed (69) -- attempting retry 3 in 4 seconds...\n' // at t=1 + 'pub get failed (69) -- attempting retry 4 in 8 seconds...\n' // at t=5 + 'pub get failed (69) -- attempting retry 5 in 16 seconds...\n' // at t=13 + 'pub get failed (69) -- attempting retry 6 in 32 seconds...\n' // at t=29 + 'pub get failed (69) -- attempting retry 7 in 64 seconds...\n' // at t=61 + ); + time.elapse(const Duration(seconds: 200)); // from t=0 to t=200 + expect(testLogger.statusText, + 'Running "flutter packages get" in /...\n' + 'pub get failed (69) -- attempting retry 1 in 1 second...\n' + 'pub get failed (69) -- attempting retry 2 in 2 seconds...\n' + 'pub get failed (69) -- attempting retry 3 in 4 seconds...\n' + 'pub get failed (69) -- attempting retry 4 in 8 seconds...\n' + 'pub get failed (69) -- attempting retry 5 in 16 seconds...\n' + 'pub get failed (69) -- attempting retry 6 in 32 seconds...\n' + 'pub get failed (69) -- attempting retry 7 in 64 seconds...\n' + 'pub get failed (69) -- attempting retry 8 in 64 seconds...\n' // at t=39 + 'pub get failed (69) -- attempting retry 9 in 64 seconds...\n' // at t=103 + 'pub get failed (69) -- attempting retry 10 in 64 seconds...\n' // at t=167 + ); + }); + expect(testLogger.errorText, isEmpty); + expect(error, isNull); + }, overrides: { + ProcessManager: () => new MockProcessManager(69), + FileSystem: () => new MockFileSystem(), + }); +} + +typedef void StartCallback(List command); + +class MockProcessManager implements ProcessManager { + MockProcessManager(this.fakeExitCode); + + final int fakeExitCode; + + @override + Future start( + List command, { + String workingDirectory, + Map environment, + bool includeParentEnvironment: true, + bool runInShell: false, + ProcessStartMode mode: ProcessStartMode.NORMAL, + }) { + return new Future.value(new MockProcess(fakeExitCode)); + } + + @override + dynamic noSuchMethod(Invocation invocation) => null; +} + +class MockProcess implements Process { + MockProcess(this.fakeExitCode); + + final int fakeExitCode; + + @override + Stream> get stdout => new MockStream>(); + + @override + Stream> get stderr => new MockStream>(); + + @override + Future get exitCode => new Future.value(fakeExitCode); + + @override + dynamic noSuchMethod(Invocation invocation) => null; +} + +class MockStream implements Stream { + @override + Stream transform(StreamTransformer streamTransformer) => new MockStream(); + + @override + Stream where(bool test(T event)) => new MockStream(); + + @override + StreamSubscription listen(void onData(T event), {Function onError, void onDone(), bool cancelOnError}) { + return new MockStreamSubscription(); + } + + @override + dynamic noSuchMethod(Invocation invocation) => null; +} + +class MockStreamSubscription implements StreamSubscription { + @override + Future asFuture([E futureValue]) => new Future.value(); + + @override + Future cancel() => null; + + @override + dynamic noSuchMethod(Invocation invocation) => null; +} + + +class MockFileSystem extends MemoryFileSystem { + @override + File file(dynamic path) { + return new MockFile(); + } +} + +class MockFile implements File { + @override + Future open({FileMode mode: FileMode.READ}) async { + return new MockRandomAccessFile(); + } + + @override + bool existsSync() => true; + + @override + DateTime lastModifiedSync() => new DateTime(0); + + @override + dynamic noSuchMethod(Invocation invocation) => null; +} + +class MockRandomAccessFile extends Mock implements RandomAccessFile {} diff --git a/packages/flutter_tools/test/dart_dependencies_test.dart b/packages/flutter_tools/test/dart_dependencies_test.dart index 81fbdb8516a..c8e84b9cbff 100644 --- a/packages/flutter_tools/test/dart_dependencies_test.dart +++ b/packages/flutter_tools/test/dart_dependencies_test.dart @@ -74,7 +74,7 @@ void main() { } }); - testUsingContext('does not change ASCI casing of path', () { + testUsingContext('does not change ASCII casing of path', () { final String testPath = fs.path.join(dataPath, 'asci_casing'); final String mainPath = fs.path.join(testPath, 'main.dart'); final String packagesPath = fs.path.join(testPath, '.packages');