diff --git a/dev/devicelab/README.md b/dev/devicelab/README.md index dd3e1c322ff..4450ee06a82 100644 --- a/dev/devicelab/README.md +++ b/dev/devicelab/README.md @@ -106,6 +106,18 @@ You must set the `ANDROID_HOME` environment variable to run tests on Android. If you have a local build of the Flutter engine, then you have a copy of the Android SDK at `.../engine/src/third_party/android_tools/sdk`. +You can find where your Android SDK is using `flutter doctor`. + +## Running all tests + +To run all tests defined in `manifest.yaml`, use option `-a` (`--all`): + +```sh +dart bin/run.dart -a +``` + +## Running specific tests + To run a test, use option `-t` (`--task`): ```sh @@ -127,20 +139,15 @@ To run multiple tests, repeat option `-t` (`--task`) multiple times: dart bin/run.dart -t test1 -t test2 -t test3 ``` -To run all tests defined in `manifest.yaml`, use option `-a` (`--all`): +To run tests from a specific stage, use option `-s` (`--stage`). +Currently there are only three stages defined, `devicelab`, +`devicelab_ios` and `devicelab_win`. -```sh -dart bin/run.dart -a -``` - -To run tests from a specific stage, use option `-s` (`--stage`): ```sh dart bin/run.dart -s {NAME_OF_STAGE} ``` -Currently there are only three stages defined, `devicelab`, `devicelab_ios` and `devicelab_win`. - # Reproducing broken builds locally To reproduce the breakage locally `git checkout` the corresponding Flutter diff --git a/dev/devicelab/bin/tasks/flutter_attach_test.dart b/dev/devicelab/bin/tasks/flutter_attach_test.dart index e5e8699a34a..a3defc58956 100644 --- a/dev/devicelab/bin/tasks/flutter_attach_test.dart +++ b/dev/devicelab/bin/tasks/flutter_attach_test.dart @@ -31,14 +31,13 @@ Future testReload(Process process, { Future Function() onListening } .listen((String line) { print('attach:stdout: $line'); stdout.add(line); - if (line.contains('Waiting') && onListening != null) { + if (line.contains('Waiting') && onListening != null) listening.complete(onListening()); - } if (line.contains('To quit, press "q".')) ready.complete(); if (line.contains('Reloaded ')) reloaded.complete(); - if (line.contains('Restarted app in ')) + if (line.contains('Restarted application in ')) restarted.complete(); if (line.contains('Application finished')) finished.complete(); @@ -91,7 +90,7 @@ void main() { await device.unlock(); final Directory appDir = dir(path.join(flutterDirectory.path, 'dev/integration_tests/ui')); await inDirectory(appDir, () async { - section('Build: starting...'); + section('Building'); final String buildStdout = await eval( path.join(flutterDirectory.path, 'bin', 'flutter'), ['--suppress-analytics', 'build', 'apk', '--debug', 'lib/main.dart'], @@ -105,7 +104,7 @@ void main() { await device.adb(['install', '-r', apkPath]); try { - section('Launching attach.'); + section('Launching `flutter attach`'); Process attachProcess = await startProcess( path.join(flutterDirectory.path, 'bin', 'flutter'), ['--suppress-analytics', 'attach', '-d', device.deviceId], @@ -113,7 +112,6 @@ void main() { ); await testReload(attachProcess, onListening: () async { - section('Launching app.'); await device.shellExec('am', ['start', '-n', kActivityId]); }); @@ -124,15 +122,16 @@ void main() { final String currentTime = (await device.shellEval('date', ['"+%F %R:%S.000"'])).trim(); print('Start time on device: $currentTime'); - section('Launching app'); + section('Relaunching application'); await device.shellExec('am', ['start', '-n', kActivityId]); + // If the next line fails, your device may not support regexp search. final String observatoryLine = await device.adb(['logcat', '-e', 'Observatory listening on http:', '-m', '1', '-T', currentTime]); print('Found observatory line: $observatoryLine'); final String observatoryPort = new RegExp(r'Observatory listening on http://.*:([0-9]+)').firstMatch(observatoryLine)[1]; print('Extracted observatory port: $observatoryPort'); - section('Launching attach with given port.'); + section('Launching attach with given port'); attachProcess = await startProcess( path.join(flutterDirectory.path, 'bin', 'flutter'), ['--suppress-analytics', 'attach', '--debug-port', observatoryPort, '-d', device.deviceId], diff --git a/dev/devicelab/bin/tasks/run_release_test.dart b/dev/devicelab/bin/tasks/run_release_test.dart index dedd300966c..87094bc998d 100644 --- a/dev/devicelab/bin/tasks/run_release_test.dart +++ b/dev/devicelab/bin/tasks/run_release_test.dart @@ -58,6 +58,8 @@ void main() { stdout.removeAt(0); if (stdout.first == 'Initializing gradle...') stdout.removeAt(0); + if (stdout.first == 'Resolving dependencies...') + stdout.removeAt(0); if (!(stdout.first.startsWith('Launching lib/main.dart on ') && stdout.first.endsWith(' in release mode...'))) throw 'flutter run --release had unexpected first line: ${stdout.first}'; stdout.removeAt(0); diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart index d1a0cc39161..4fa661b183f 100644 --- a/packages/flutter_tools/lib/src/android/gradle.dart +++ b/packages/flutter_tools/lib/src/android/gradle.dart @@ -93,29 +93,34 @@ Future _readGradleProject() async { final FlutterProject flutterProject = new FlutterProject(fs.currentDirectory); final String gradle = await _ensureGradle(flutterProject); await updateLocalProperties(project: flutterProject); + final Status status = logger.startProgress('Resolving dependencies...', expectSlowOperation: true); + GradleProject project; try { - final Status status = logger.startProgress('Resolving dependencies...', expectSlowOperation: true); final RunResult runResult = await runCheckedAsync( [gradle, 'app:properties'], workingDirectory: flutterProject.android.directory.path, environment: _gradleEnv, ); final String properties = runResult.stdout.trim(); - final GradleProject project = new GradleProject.fromAppProperties(properties); - status.stop(); - return project; - } catch (e) { + project = new GradleProject.fromAppProperties(properties); + } catch (exception) { if (getFlutterPluginVersion(flutterProject.android) == FlutterPluginVersion.managed) { + status.cancel(); // Handle known exceptions. This will exit if handled. - handleKnownGradleExceptions(e); + handleKnownGradleExceptions(exception); // Print a general Gradle error and exit. - printError('* Error running Gradle:\n$e\n'); + printError('* Error running Gradle:\n$exception\n'); throwToolExit('Please review your Gradle project setup in the android/ folder.'); } + // Fall back to the default + project = new GradleProject( + ['debug', 'profile', 'release'], + [], flutterProject.android.gradleAppOutV1Directory, + ); } - // Fall back to the default - return new GradleProject(['debug', 'profile', 'release'], [], flutterProject.android.gradleAppOutV1Directory); + status.stop(); + return project; } void handleKnownGradleExceptions(String exceptionString) { diff --git a/packages/flutter_tools/lib/src/base/logger.dart b/packages/flutter_tools/lib/src/base/logger.dart index 7248c7793ed..dcab62c15a5 100644 --- a/packages/flutter_tools/lib/src/base/logger.dart +++ b/packages/flutter_tools/lib/src/base/logger.dart @@ -13,6 +13,8 @@ import 'utils.dart'; const int kDefaultStatusPadding = 59; +typedef void VoidCallback(); + abstract class Logger { bool get isVerbose => false; @@ -53,8 +55,6 @@ abstract class Logger { }); } -typedef void _FinishCallback(); - class StdoutLogger extends Logger { Status _status; @@ -66,7 +66,6 @@ class StdoutLogger extends Logger { void printError(String message, { StackTrace stackTrace, bool emphasis = false }) { _status?.cancel(); _status = null; - if (emphasis) message = terminal.bolden(message); stderr.writeln(message); @@ -109,16 +108,25 @@ class StdoutLogger extends Logger { }) { if (_status != null) { // Ignore nested progresses; return a no-op status object. - return new Status()..start(); + return new Status(onFinish: _clearStatus)..start(); } if (terminal.supportsColor) { - _status = new AnsiStatus(message, expectSlowOperation, () { _status = null; }, progressIndicatorPadding)..start(); + _status = new AnsiStatus( + message: message, + expectSlowOperation: expectSlowOperation, + padding: progressIndicatorPadding, + onFinish: _clearStatus, + )..start(); } else { printStatus(message); - _status = new Status()..start(); + _status = new Status(onFinish: _clearStatus)..start(); } return _status; } + + void _clearStatus() { + _status = null; + } } /// A [StdoutLogger] which replaces Unicode characters that cannot be printed to @@ -180,7 +188,7 @@ class BufferLogger extends Logger { int progressIndicatorPadding = kDefaultStatusPadding, }) { printStatus(message); - return new Status(); + return new Status()..start(); } /// Clears all buffers. @@ -230,7 +238,9 @@ class VerboseLogger extends Logger { int progressIndicatorPadding = kDefaultStatusPadding, }) { printStatus(message); - return new Status(); + return new Status(onFinish: () { + printTrace('$message (completed)'); + })..start(); } void _emit(_LogType type, String message, [StackTrace stackTrace]) { @@ -275,75 +285,91 @@ enum _LogType { /// A [Status] class begins when start is called, and may produce progress /// information asynchronously. /// -/// When stop is called, summary information supported by this class is printed. -/// If cancel is called, no summary information is displayed. -/// The base class displays nothing at all. +/// The [Status] class itself never has any output. +/// +/// The [AnsiSpinner] subclass shows a spinner, and replaces it with a single +/// space character when stopped or canceled. +/// +/// The [AnsiStatus] subclass shows a spinner, and replaces it with timing +/// information when stopped. When canceled, the information isn't shown. In +/// either case, a newline is printed. +/// +/// Generally, consider `logger.startProgress` instead of directly creating +/// a [Status] or one of its subclasses. class Status { - Status(); + Status({ this.onFinish }); + + /// A straight [Status] or an [AnsiSpinner] (depending on whether the + /// terminal is fancy enough), already started. + factory Status.withSpinner({ VoidCallback onFinish }) { + if (terminal.supportsColor) + return new AnsiSpinner(onFinish: onFinish)..start(); + return new Status(onFinish: onFinish)..start(); + } + + final VoidCallback onFinish; bool _isStarted = false; - factory Status.withSpinner() { - if (terminal.supportsColor) - return new AnsiSpinner()..start(); - return new Status()..start(); - } - - /// Display summary information for this spinner; called by [stop]. - void summaryInformation() {} - - /// Call to start spinning. Call this method via super at the beginning - /// of a subclass [start] method. + /// Call to start spinning. void start() { + assert(!_isStarted); _isStarted = true; } - /// Call to stop spinning and delete the spinner. Print summary information, - /// if applicable to the spinner. + /// Call to stop spinning after success. void stop() { - if (_isStarted) { - cancel(); - summaryInformation(); - } + assert(_isStarted); + _isStarted = false; + if (onFinish != null) + onFinish(); } - /// Call to cancel the spinner without printing any summary output. Call - /// this method via super at the end of a subclass [cancel] method. + /// Call to cancel the spinner after failure or cancelation. void cancel() { + assert(_isStarted); _isStarted = false; + if (onFinish != null) + onFinish(); } } /// An [AnsiSpinner] is a simple animation that does nothing but implement an -/// ASCII spinner. When stopped or canceled, the animation erases itself. +/// ASCII spinner. When stopped or canceled, the animation erases itself. class AnsiSpinner extends Status { + AnsiSpinner({ VoidCallback onFinish }) : super(onFinish: onFinish); + int ticks = 0; Timer timer; - static final List _progress = ['-', r'\', '|', r'/']; + static final List _progress = [r'-', r'\', r'|', r'/']; - void _callback(Timer _) { + void _callback(Timer timer) { stdout.write('\b${_progress[ticks++ % _progress.length]}'); } @override void start() { super.start(); + assert(timer == null); stdout.write(' '); - _callback(null); timer = new Timer.periodic(const Duration(milliseconds: 100), _callback); + _callback(timer); + } + + @override + void stop() { + assert(timer.isActive); + timer.cancel(); + stdout.write('\b \b'); + super.stop(); } @override - /// Clears the spinner. After cancel, the cursor will be one space right - /// of where it was when [start] was called (assuming no other input). void cancel() { - if (timer?.isActive == true) { - timer.cancel(); - // Many terminals do not interpret backspace as deleting a character, - // but rather just moving the cursor back one. - stdout.write('\b \b'); - } + assert(timer.isActive); + timer.cancel(); + stdout.write('\b \b'); super.cancel(); } } @@ -353,59 +379,50 @@ class AnsiSpinner extends Status { /// On [stop], will additionally print out summary information in /// milliseconds if [expectSlowOperation] is false, as seconds otherwise. class AnsiStatus extends AnsiSpinner { - AnsiStatus(this.message, this.expectSlowOperation, this.onFinish, this.padding); + AnsiStatus({ + this.message, + this.expectSlowOperation, + this.padding, + VoidCallback onFinish, + }) : super(onFinish: onFinish); final String message; final bool expectSlowOperation; - final _FinishCallback onFinish; final int padding; Stopwatch stopwatch; - bool _finished = false; @override - /// Writes [message] to [stdout] with padding, then begins spinning. void start() { stopwatch = new Stopwatch()..start(); stdout.write('${message.padRight(padding)} '); - assert(!_finished); super.start(); } @override - /// Calls onFinish. void stop() { - if (!_finished) { - onFinish(); - _finished = true; - super.cancel(); - summaryInformation(); - } + super.stop(); + writeSummaryInformation(); + stdout.write('\n'); } @override + void cancel() { + super.cancel(); + stdout.write('\n'); + } + /// Backs up 4 characters and prints a (minimum) 5 character padded time. If /// [expectSlowOperation] is true, the time is in seconds; otherwise, /// milliseconds. Only backs up 4 characters because [super.cancel] backs /// up one. /// /// Example: '\b\b\b\b 0.5s', '\b\b\b\b150ms', '\b\b\b\b1600ms' - void summaryInformation() { + void writeSummaryInformation() { if (expectSlowOperation) { - stdout.writeln('\b\b\b\b${getElapsedAsSeconds(stopwatch.elapsed).padLeft(5)}'); + stdout.write('\b\b\b\b${getElapsedAsSeconds(stopwatch.elapsed).padLeft(5)}'); } else { - stdout.writeln('\b\b\b\b${getElapsedAsMilliseconds(stopwatch.elapsed).padLeft(5)}'); - } - } - - @override - /// Calls [onFinish]. - void cancel() { - if (!_finished) { - onFinish(); - _finished = true; - super.cancel(); - stdout.write('\n'); + stdout.write('\b\b\b\b${getElapsedAsMilliseconds(stopwatch.elapsed).padLeft(5)}'); } } } diff --git a/packages/flutter_tools/lib/src/cache.dart b/packages/flutter_tools/lib/src/cache.dart index ccfeb15b437..326c24e02bb 100644 --- a/packages/flutter_tools/lib/src/cache.dart +++ b/packages/flutter_tools/lib/src/cache.dart @@ -295,11 +295,15 @@ abstract class CachedArtifact { return _withDownloadFile('${flattenNameSubdirs(url)}', (File tempFile) async { if (!verifier(tempFile)) { final Status status = logger.startProgress(message, expectSlowOperation: true); - await _downloadFile(url, tempFile).then((_) { + try { + await _downloadFile(url, tempFile); status.stop(); - }).whenComplete(status.cancel); + } catch (exception) { + status.cancel(); + rethrow; + } } else { - logger.printStatus('$message(cached)'); + logger.printTrace('$message (cached)'); } _ensureExists(location); extractor(tempFile, location); diff --git a/packages/flutter_tools/lib/src/commands/build_aot.dart b/packages/flutter_tools/lib/src/commands/build_aot.dart index 5bf9bb827dd..f28a1d27d10 100644 --- a/packages/flutter_tools/lib/src/commands/build_aot.dart +++ b/packages/flutter_tools/lib/src/commands/build_aot.dart @@ -74,8 +74,10 @@ class BuildAotCommand extends BuildSubCommand { Status status; if (!argResults['quiet']) { final String typeName = artifacts.getEngineType(platform, buildMode); - status = logger.startProgress('Building AOT snapshot in ${getModeName(getBuildMode())} mode ($typeName)...', - expectSlowOperation: true); + status = logger.startProgress( + 'Building AOT snapshot in ${getModeName(getBuildMode())} mode ($typeName)...', + expectSlowOperation: true, + ); } final String outputPath = argResults['output-dir'] ?? getAotBuildDirectory(); try { @@ -120,8 +122,6 @@ class BuildAotCommand extends BuildSubCommand { buildSharedLibrary: false, extraGenSnapshotOptions: argResults[FlutterOptions.kExtraGenSnapshotOptions], ).then((int buildExitCode) { - if (buildExitCode != 0) - printError('Snapshotting ($iosArch) exited with non-zero exit code: $buildExitCode'); return buildExitCode; }); }); @@ -134,6 +134,12 @@ class BuildAotCommand extends BuildSubCommand { ..addAll(dylibs) ..addAll(['-create', '-output', fs.path.join(outputPath, 'App.framework', 'App')]), ); + } else { + status?.cancel(); + exitCodes.forEach((IOSArch iosArch, Future exitCodeFuture) async { + final int buildExitCode = await exitCodeFuture; + printError('Snapshotting ($iosArch) exited with non-zero exit code: $buildExitCode'); + }); } } else { // Android AOT snapshot. @@ -148,12 +154,14 @@ class BuildAotCommand extends BuildSubCommand { extraGenSnapshotOptions: argResults[FlutterOptions.kExtraGenSnapshotOptions], ); if (snapshotExitCode != 0) { + status?.cancel(); printError('Snapshotting exited with non-zero exit code: $snapshotExitCode'); return; } } } on String catch (error) { // Catch the String exceptions thrown from the `runCheckedSync` methods below. + status?.cancel(); printError(error); return; } diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index 202fbf2dacd..eaf51f70c0e 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -901,7 +901,14 @@ class _AppRunLogger extends Logger { 'message': message, }); - _status = new _AppLoggerStatus(this, id, progressId); + _status = new Status(onFinish: () { + _status = null; + _sendProgressEvent({ + 'id': id.toString(), + 'progressId': progressId, + 'finished': true + }); + }); return _status; } @@ -924,37 +931,6 @@ class _AppRunLogger extends Logger { } } -class _AppLoggerStatus extends Status { - _AppLoggerStatus(this.logger, this.id, this.progressId); - - final _AppRunLogger logger; - final int id; - final String progressId; - - @override - void start() {} - - @override - void stop() { - logger._status = null; - _sendFinished(); - } - - @override - void cancel() { - logger._status = null; - _sendFinished(); - } - - void _sendFinished() { - logger._sendProgressEvent({ - 'id': id.toString(), - 'progressId': progressId, - 'finished': true - }); - } -} - class LogMessage { final String level; final String message; diff --git a/packages/flutter_tools/lib/src/commands/update_packages.dart b/packages/flutter_tools/lib/src/commands/update_packages.dart index 7106f639b0a..aca3411fcad 100644 --- a/packages/flutter_tools/lib/src/commands/update_packages.dart +++ b/packages/flutter_tools/lib/src/commands/update_packages.dart @@ -84,7 +84,10 @@ class UpdatePackagesCommand extends FlutterCommand { final bool hidden; Future _downloadCoverageData() async { - final Status status = logger.startProgress('Downloading lcov data for package:flutter...', expectSlowOperation: true); + final Status status = logger.startProgress( + 'Downloading lcov data for package:flutter...', + expectSlowOperation: true, + ); final String urlBase = platform.environment['FLUTTER_STORAGE_BASE_URL'] ?? 'https://storage.googleapis.com'; final List data = await fetchUrl(Uri.parse('$urlBase/flutter_infra/flutter/coverage/lcov.info')); final String coverageDir = fs.path.join(Cache.flutterRoot, 'packages/flutter/coverage'); diff --git a/packages/flutter_tools/lib/src/dart/pub.dart b/packages/flutter_tools/lib/src/dart/pub.dart index d633374aa0a..90ef41b0e48 100644 --- a/packages/flutter_tools/lib/src/dart/pub.dart +++ b/packages/flutter_tools/lib/src/dart/pub.dart @@ -109,8 +109,10 @@ Future pubGet({ failureMessage: 'pub $command failed', retry: true, ); - } finally { status.stop(); + } catch (exception) { + status.cancel(); + rethrow; } } diff --git a/packages/flutter_tools/lib/src/doctor.dart b/packages/flutter_tools/lib/src/doctor.dart index 9d9dc90473d..f97f2ff701e 100644 --- a/packages/flutter_tools/lib/src/doctor.dart +++ b/packages/flutter_tools/lib/src/doctor.dart @@ -145,9 +145,13 @@ class Doctor { for (ValidatorTask validatorTask in startValidatorTasks()) { final DoctorValidator validator = validatorTask.validator; final Status status = new Status.withSpinner(); - await (validatorTask.result).then((_) { - status.stop(); - }).whenComplete(status.cancel); + try { + await validatorTask.result; + } catch (exception) { + status.cancel(); + rethrow; + } + status.stop(); final ValidationResult result = await validatorTask.result; if (result.type == ValidationType.missing) { diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index 8099bd278fd..3ea48a1974a 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -299,7 +299,6 @@ class IOSDevice extends Device { bundlePath: bundle.path, launchArguments: launchArguments, ); - installStatus.stop(); } else { // Debugging is enabled, look for the observatory server port post launch. printTrace('Debugging is enabled, connecting to observatory'); diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index ca167809449..a18ea9dd116 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -385,7 +385,7 @@ class FlutterDevice { }) async { final Status devFSStatus = logger.startProgress( 'Syncing files to device ${device.name}...', - expectSlowOperation: true + expectSlowOperation: true, ); int bytes = 0; try { @@ -554,8 +554,9 @@ abstract class ResidentRunner { for (FlutterView view in device.views) await view.uiIsolate.flutterDebugAllowBanner(false); } catch (error) { - status.stop(); + status.cancel(); printError('Error communicating with Flutter on the device: $error'); + return; } } try { @@ -566,8 +567,9 @@ abstract class ResidentRunner { for (FlutterView view in device.views) await view.uiIsolate.flutterDebugAllowBanner(true); } catch (error) { - status.stop(); + status.cancel(); printError('Error communicating with Flutter on the device: $error'); + return; } } } @@ -575,7 +577,7 @@ abstract class ResidentRunner { status.stop(); printStatus('Screenshot written to ${fs.path.relative(outputFile.path)} (${sizeKB}kB).'); } catch (error) { - status.stop(); + status.cancel(); printError('Error taking screenshot: $error'); } } diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart index f28cf4947b8..b051dafaf6b 100644 --- a/packages/flutter_tools/lib/src/run_hot.dart +++ b/packages/flutter_tools/lib/src/run_hot.dart @@ -441,8 +441,7 @@ class HotRunner extends ResidentRunner { : mainPath; await _launchFromDevFS(launchPath); restartTimer.stop(); - printTrace('Restart performed in ' - '${getElapsedAsMilliseconds(restartTimer.elapsed)}.'); + printTrace('Hot restart performed in ${getElapsedAsMilliseconds(restartTimer.elapsed)}.'); // We are now running from sources. _runningFromSnapshot = false; _addBenchmarkData('hotRestartMillisecondsToFrame', @@ -494,26 +493,21 @@ class HotRunner extends ResidentRunner { @override Future restart({ bool fullRestart = false, bool pauseAfterRestart = false }) async { + final Stopwatch timer = new Stopwatch()..start(); if (fullRestart) { final Status status = logger.startProgress( 'Performing hot restart...', - progressId: 'hot.restart' + progressId: 'hot.restart', ); try { - final Stopwatch timer = new Stopwatch()..start(); - if (!(await hotRunnerConfig.setupHotRestart())) { - status.cancel(); + if (!(await hotRunnerConfig.setupHotRestart())) return new OperationResult(1, 'setupHotRestart failed'); - } await _restartFromSources(); - timer.stop(); + } finally { status.cancel(); - printStatus('Restarted app in ${getElapsedAsMilliseconds(timer.elapsed)}.'); - return OperationResult.ok; - } catch (error) { - status.cancel(); - rethrow; } + printStatus('Restarted application in ${getElapsedAsMilliseconds(timer.elapsed)}.'); + return OperationResult.ok; } else { final bool reloadOnTopOfSnapshot = _runningFromSnapshot; final String progressPrefix = reloadOnTopOfSnapshot ? 'Initializing' : 'Performing'; @@ -521,20 +515,17 @@ class HotRunner extends ResidentRunner { '$progressPrefix hot reload...', progressId: 'hot.reload' ); + OperationResult result; try { - final Stopwatch timer = new Stopwatch()..start(); - final OperationResult result = await _reloadSources(pause: pauseAfterRestart); - timer.stop(); + result = await _reloadSources(pause: pauseAfterRestart); + } finally { status.cancel(); - if (result.isOk) - printStatus('${result.message} in ${getElapsedAsMilliseconds(timer.elapsed)}.'); - if (result.hintMessage != null) - printStatus('\n${result.hintMessage}'); - return result; - } catch (error) { - status.cancel(); - rethrow; } + if (result.isOk) + printStatus('${result.message} in ${getElapsedAsMilliseconds(timer.elapsed)}.'); + if (result.hintMessage != null) + printStatus('\n${result.hintMessage}'); + return result; } } @@ -637,9 +628,11 @@ class HotRunner extends ResidentRunner { final int errorCode = error['code']; final String errorMessage = error['message']; if (errorCode == Isolate.kIsolateReloadBarred) { - printError('Unable to hot reload app due to an unrecoverable error in ' - 'the source code. Please address the error and then use ' - '"R" to restart the app.'); + printError( + 'Unable to hot reload application due to an unrecoverable error in ' + 'the source code. Please address the error and then use "R" to ' + 'restart the app.' + ); flutterUsage.sendEvent('hot', 'reload-barred'); return new OperationResult(errorCode, errorMessage); } @@ -714,8 +707,7 @@ class HotRunner extends ResidentRunner { reassembleTimer.elapsed.inMilliseconds); reloadTimer.stop(); - printTrace('Hot reload performed in ' - '${getElapsedAsMilliseconds(reloadTimer.elapsed)}.'); + printTrace('Hot reload performed in ${getElapsedAsMilliseconds(reloadTimer.elapsed)}.'); // Record complete time it took for the reload. _addBenchmarkData('hotReloadMillisecondsToFrame', reloadTimer.elapsed.inMilliseconds); diff --git a/packages/flutter_tools/test/base/logger_test.dart b/packages/flutter_tools/test/base/logger_test.dart index 5fa9778d8ea..cf1fbf126d4 100644 --- a/packages/flutter_tools/test/base/logger_test.dart +++ b/packages/flutter_tools/test/base/logger_test.dart @@ -4,6 +4,7 @@ import 'dart:async'; +import 'package:flutter_tools/src/base/context.dart'; import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:test/test.dart'; @@ -23,9 +24,9 @@ void main() { verboseLogger.printError('Helpless!'); expect(mockLogger.statusText, matches(r'^\[ (?: {0,2}\+[0-9]{1,3} ms| )\] Hey Hey Hey Hey\n' - r'\[ (?: {0,2}\+[0-9]{1,3} ms| )\] Oooh, I do I do I do\n$')); + r'\[ (?: {0,2}\+[0-9]{1,3} ms| )\] Oooh, I do I do I do\n$')); expect(mockLogger.traceText, ''); - expect(mockLogger.errorText, matches(r'^\[ (?: {0,2}\+[0-9]{1,3} ms| )\] Helpless!\n$')); + expect(mockLogger.errorText, matches( r'^\[ (?: {0,2}\+[0-9]{1,3} ms| )\] Helpless!\n$')); }); }); @@ -40,23 +41,27 @@ void main() { mockStdio = new MockStdio(); ansiSpinner = new AnsiSpinner(); called = 0; - ansiStatus = new AnsiStatus('Hello world', true, () => called++, 20); + ansiStatus = new AnsiStatus( + message: 'Hello world', + expectSlowOperation: true, + padding: 20, + onFinish: () => called++, + ); }); List outputLines() => mockStdio.writtenToStdout.join('').split('\n'); - Future doWhile(bool doThis()) async { - return Future.doWhile(() async { - // Future.doWhile() isn't enough by itself, because the VM never gets - // around to scheduling the other tasks for some reason. - await new Future.delayed(const Duration(milliseconds: 0)); - return doThis(); + Future doWhileAsync(bool doThis()) async { + return Future.doWhile(() { + // We want to let other tasks run at the same time, so we schedule these + // using a timer rather than a microtask. + return Future.delayed(Duration.zero, doThis); }); } testUsingContext('AnsiSpinner works', () async { ansiSpinner.start(); - await doWhile(() => ansiSpinner.ticks < 10); + await doWhileAsync(() => ansiSpinner.ticks < 10); List lines = outputLines(); expect(lines[0], startsWith(' \b-\b\\\b|\b/\b-\b\\\b|\b/')); expect(lines[0].endsWith('\n'), isFalse); @@ -66,44 +71,37 @@ void main() { expect(lines[0], endsWith('\b \b')); expect(lines.length, equals(1)); - // Verify that stopping multiple times doesn't clear multiple times. - ansiSpinner.stop(); - lines = outputLines(); - expect(lines[0].endsWith('\b \b '), isFalse); - expect(lines.length, equals(1)); - ansiSpinner.cancel(); - lines = outputLines(); - expect(lines[0].endsWith('\b \b '), isFalse); - expect(lines.length, equals(1)); + // Verify that stopping or canceling multiple times throws. + expect(() { ansiSpinner.stop(); }, throwsA(const isInstanceOf())); + expect(() { ansiSpinner.cancel(); }, throwsA(const isInstanceOf())); }, overrides: {Stdio: () => mockStdio}); testUsingContext('AnsiStatus works when cancelled', () async { ansiStatus.start(); - await doWhile(() => ansiStatus.ticks < 10); + await doWhileAsync(() => ansiStatus.ticks < 10); List lines = outputLines(); expect(lines[0], startsWith('Hello world \b-\b\\\b|\b/\b-\b\\\b|\b/\b-')); - expect(lines[0].endsWith('\n'), isFalse); expect(lines.length, equals(1)); + expect(lines[0].endsWith('\n'), isFalse); + + // Verify a cancel does _not_ print the time and prints a newline. ansiStatus.cancel(); lines = outputLines(); + final List matches = secondDigits.allMatches(lines[0]).toList(); + expect(matches, isEmpty); expect(lines[0], endsWith('\b \b')); - expect(lines.length, equals(2)); expect(called, equals(1)); - ansiStatus.cancel(); - lines = outputLines(); - expect(lines[0].endsWith('\b \b\b \b'), isFalse); expect(lines.length, equals(2)); - expect(called, equals(1)); - ansiStatus.stop(); - lines = outputLines(); - expect(lines[0].endsWith('\b \b\b \b'), isFalse); - expect(lines.length, equals(2)); - expect(called, equals(1)); + expect(lines[1], equals('')); + + // Verify that stopping or canceling multiple times throws. + expect(() { ansiStatus.cancel(); }, throwsA(const isInstanceOf())); + expect(() { ansiStatus.stop(); }, throwsA(const isInstanceOf())); }, overrides: {Stdio: () => mockStdio}); testUsingContext('AnsiStatus works when stopped', () async { ansiStatus.start(); - await doWhile(() => ansiStatus.ticks < 10); + await doWhileAsync(() => ansiStatus.ticks < 10); List lines = outputLines(); expect(lines[0], startsWith('Hello world \b-\b\\\b|\b/\b-\b\\\b|\b/\b-')); expect(lines.length, equals(1)); @@ -111,55 +109,55 @@ void main() { // Verify a stop prints the time. ansiStatus.stop(); lines = outputLines(); - List matches = secondDigits.allMatches(lines[0]).toList(); + final List matches = secondDigits.allMatches(lines[0]).toList(); expect(matches, isNotNull); expect(matches, hasLength(1)); - Match match = matches.first; + final Match match = matches.first; expect(lines[0], endsWith(match.group(0))); - final String initialTime = match.group(0); expect(called, equals(1)); expect(lines.length, equals(2)); expect(lines[1], equals('')); - // Verify stopping more than once generates no additional output. - ansiStatus.stop(); - lines = outputLines(); - matches = secondDigits.allMatches(lines[0]).toList(); - expect(matches, hasLength(1)); - match = matches.first; - expect(lines[0], endsWith(initialTime)); - expect(called, equals(1)); - expect(lines.length, equals(2)); - expect(lines[1], equals('')); + // Verify that stopping or canceling multiple times throws. + expect(() { ansiStatus.stop(); }, throwsA(const isInstanceOf())); + expect(() { ansiStatus.cancel(); }, throwsA(const isInstanceOf())); }, overrides: {Stdio: () => mockStdio}); - testUsingContext('AnsiStatus works when cancelled', () async { - ansiStatus.start(); - await doWhile(() => ansiStatus.ticks < 10); - List lines = outputLines(); - expect(lines[0], startsWith('Hello world \b-\b\\\b|\b/\b-\b\\\b|\b/\b-')); - expect(lines.length, equals(1)); + testUsingContext('sequential startProgress calls with StdoutLogger', () async { + context[Logger].startProgress('AAA')..stop(); + context[Logger].startProgress('BBB')..stop(); + expect(outputLines(), [ + 'AAA', + 'BBB', + '', + ]); + }, overrides: { + Stdio: () => mockStdio, + Logger: () => new StdoutLogger(), + }); - // Verify a cancel does _not_ print the time and prints a newline. - ansiStatus.cancel(); - lines = outputLines(); - List matches = secondDigits.allMatches(lines[0]).toList(); - expect(matches, isEmpty); - expect(lines[0], endsWith('\b \b')); - expect(called, equals(1)); - // TODO(jcollins-g): Consider having status objects print the newline - // when canceled, or never printing a newline at all. - expect(lines.length, equals(2)); + testUsingContext('sequential startProgress calls with VerboseLogger and StdoutLogger', () async { + context[Logger].startProgress('AAA')..stop(); + context[Logger].startProgress('BBB')..stop(); + expect(outputLines(), [ + matches(r'^\[ (?: {0,2}\+[0-9]{1,3} ms| )\] AAA$'), + matches(r'^\[ (?: {0,2}\+[0-9]{1,3} ms| )\] AAA \(completed\)$'), + matches(r'^\[ (?: {0,2}\+[0-9]{1,3} ms| )\] BBB$'), + matches(r'^\[ (?: {0,2}\+[0-9]{1,3} ms| )\] BBB \(completed\)$'), + matches(r'^$'), + ]); + }, overrides: { + Stdio: () => mockStdio, + Logger: () => new VerboseLogger(new StdoutLogger()), + }); - // Verifying calling stop after cancel doesn't print anything weird. - ansiStatus.stop(); - lines = outputLines(); - matches = secondDigits.allMatches(lines[0]).toList(); - expect(matches, isEmpty); - expect(lines[0], endsWith('\b \b')); - expect(called, equals(1)); - expect(lines[0], isNot(endsWith('\b \b\b \b'))); - expect(lines.length, equals(2)); - }, overrides: {Stdio: () => mockStdio}); + testUsingContext('sequential startProgress calls with BufferLogger', () async { + context[Logger].startProgress('AAA')..stop(); + context[Logger].startProgress('BBB')..stop(); + final BufferLogger logger = context[Logger]; + expect(logger.statusText, 'AAA\nBBB\n'); + }, overrides: { + Logger: () => new BufferLogger(), + }); }); }