Unverified Commit 0636c6fe authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Clean up startProgress logic. (#19695)

Disallow calling stop() or cancel() multiple times. This means that
when you use startProgress you have to more carefully think about what
exactly is going on.

Properly cancel startProgress in non-ANSI situations, so that
back-to-back startProgress calls all render to the console.
parent f516074a
...@@ -93,29 +93,34 @@ Future<GradleProject> _readGradleProject() async { ...@@ -93,29 +93,34 @@ Future<GradleProject> _readGradleProject() async {
final FlutterProject flutterProject = new FlutterProject(fs.currentDirectory); final FlutterProject flutterProject = new FlutterProject(fs.currentDirectory);
final String gradle = await _ensureGradle(flutterProject); final String gradle = await _ensureGradle(flutterProject);
await updateLocalProperties(project: flutterProject); await updateLocalProperties(project: flutterProject);
final Status status = logger.startProgress('Resolving dependencies...', expectSlowOperation: true);
GradleProject project;
try { try {
final Status status = logger.startProgress('Resolving dependencies...', expectSlowOperation: true);
final RunResult runResult = await runCheckedAsync( final RunResult runResult = await runCheckedAsync(
<String>[gradle, 'app:properties'], <String>[gradle, 'app:properties'],
workingDirectory: flutterProject.android.directory.path, workingDirectory: flutterProject.android.directory.path,
environment: _gradleEnv, environment: _gradleEnv,
); );
final String properties = runResult.stdout.trim(); final String properties = runResult.stdout.trim();
final GradleProject project = new GradleProject.fromAppProperties(properties); project = new GradleProject.fromAppProperties(properties);
status.stop(); } catch (exception) {
return project;
} catch (e) {
if (getFlutterPluginVersion(flutterProject.android) == FlutterPluginVersion.managed) { if (getFlutterPluginVersion(flutterProject.android) == FlutterPluginVersion.managed) {
status.cancel();
// Handle known exceptions. This will exit if handled. // Handle known exceptions. This will exit if handled.
handleKnownGradleExceptions(e); handleKnownGradleExceptions(exception);
// Print a general Gradle error and exit. // 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.'); throwToolExit('Please review your Gradle project setup in the android/ folder.');
} }
// Fall back to the default
project = new GradleProject(
<String>['debug', 'profile', 'release'],
<String>[], flutterProject.android.gradleAppOutV1Directory,
);
} }
// Fall back to the default status.stop();
return new GradleProject(<String>['debug', 'profile', 'release'], <String>[], flutterProject.android.gradleAppOutV1Directory); return project;
} }
void handleKnownGradleExceptions(String exceptionString) { void handleKnownGradleExceptions(String exceptionString) {
......
...@@ -13,6 +13,8 @@ import 'utils.dart'; ...@@ -13,6 +13,8 @@ import 'utils.dart';
const int kDefaultStatusPadding = 59; const int kDefaultStatusPadding = 59;
typedef void VoidCallback();
abstract class Logger { abstract class Logger {
bool get isVerbose => false; bool get isVerbose => false;
...@@ -53,8 +55,6 @@ abstract class Logger { ...@@ -53,8 +55,6 @@ abstract class Logger {
}); });
} }
typedef void _FinishCallback();
class StdoutLogger extends Logger { class StdoutLogger extends Logger {
Status _status; Status _status;
...@@ -66,7 +66,6 @@ class StdoutLogger extends Logger { ...@@ -66,7 +66,6 @@ class StdoutLogger extends Logger {
void printError(String message, { StackTrace stackTrace, bool emphasis = false }) { void printError(String message, { StackTrace stackTrace, bool emphasis = false }) {
_status?.cancel(); _status?.cancel();
_status = null; _status = null;
if (emphasis) if (emphasis)
message = terminal.bolden(message); message = terminal.bolden(message);
stderr.writeln(message); stderr.writeln(message);
...@@ -109,16 +108,25 @@ class StdoutLogger extends Logger { ...@@ -109,16 +108,25 @@ class StdoutLogger extends Logger {
}) { }) {
if (_status != null) { if (_status != null) {
// Ignore nested progresses; return a no-op status object. // Ignore nested progresses; return a no-op status object.
return new Status()..start(); return new Status(onFinish: _clearStatus)..start();
} }
if (terminal.supportsColor) { 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 { } else {
printStatus(message); printStatus(message);
_status = new Status()..start(); _status = new Status(onFinish: _clearStatus)..start();
} }
return _status; return _status;
} }
void _clearStatus() {
_status = null;
}
} }
/// A [StdoutLogger] which replaces Unicode characters that cannot be printed to /// A [StdoutLogger] which replaces Unicode characters that cannot be printed to
...@@ -180,7 +188,7 @@ class BufferLogger extends Logger { ...@@ -180,7 +188,7 @@ class BufferLogger extends Logger {
int progressIndicatorPadding = kDefaultStatusPadding, int progressIndicatorPadding = kDefaultStatusPadding,
}) { }) {
printStatus(message); printStatus(message);
return new Status(); return new Status()..start();
} }
/// Clears all buffers. /// Clears all buffers.
...@@ -230,7 +238,9 @@ class VerboseLogger extends Logger { ...@@ -230,7 +238,9 @@ class VerboseLogger extends Logger {
int progressIndicatorPadding = kDefaultStatusPadding, int progressIndicatorPadding = kDefaultStatusPadding,
}) { }) {
printStatus(message); printStatus(message);
return new Status(); return new Status(onFinish: () {
printTrace('$message (completed)');
})..start();
} }
void _emit(_LogType type, String message, [StackTrace stackTrace]) { void _emit(_LogType type, String message, [StackTrace stackTrace]) {
...@@ -275,75 +285,91 @@ enum _LogType { ...@@ -275,75 +285,91 @@ enum _LogType {
/// A [Status] class begins when start is called, and may produce progress /// A [Status] class begins when start is called, and may produce progress
/// information asynchronously. /// information asynchronously.
/// ///
/// When stop is called, summary information supported by this class is printed. /// The [Status] class itself never has any output.
/// If cancel is called, no summary information is displayed. ///
/// The base class displays nothing at all. /// 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 { class Status {
Status(); Status({ this.onFinish });
bool _isStarted = false; /// A straight [Status] or an [AnsiSpinner] (depending on whether the
/// terminal is fancy enough), already started.
factory Status.withSpinner() { factory Status.withSpinner({ VoidCallback onFinish }) {
if (terminal.supportsColor) if (terminal.supportsColor)
return new AnsiSpinner()..start(); return new AnsiSpinner(onFinish: onFinish)..start();
return new Status()..start(); return new Status(onFinish: onFinish)..start();
} }
/// Display summary information for this spinner; called by [stop]. final VoidCallback onFinish;
void summaryInformation() {}
bool _isStarted = false;
/// Call to start spinning. Call this method via super at the beginning /// Call to start spinning.
/// of a subclass [start] method.
void start() { void start() {
assert(!_isStarted);
_isStarted = true; _isStarted = true;
} }
/// Call to stop spinning and delete the spinner. Print summary information, /// Call to stop spinning after success.
/// if applicable to the spinner.
void stop() { void stop() {
if (_isStarted) { assert(_isStarted);
cancel(); _isStarted = false;
summaryInformation(); if (onFinish != null)
} onFinish();
} }
/// Call to cancel the spinner without printing any summary output. Call /// Call to cancel the spinner after failure or cancelation.
/// this method via super at the end of a subclass [cancel] method.
void cancel() { void cancel() {
assert(_isStarted);
_isStarted = false; _isStarted = false;
if (onFinish != null)
onFinish();
} }
} }
/// An [AnsiSpinner] is a simple animation that does nothing but implement an /// 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 { class AnsiSpinner extends Status {
AnsiSpinner({ VoidCallback onFinish }) : super(onFinish: onFinish);
int ticks = 0; int ticks = 0;
Timer timer; Timer timer;
static final List<String> _progress = <String>['-', r'\', '|', r'/']; static final List<String> _progress = <String>[r'-', r'\', r'|', r'/'];
void _callback(Timer _) { void _callback(Timer timer) {
stdout.write('\b${_progress[ticks++ % _progress.length]}'); stdout.write('\b${_progress[ticks++ % _progress.length]}');
} }
@override @override
void start() { void start() {
super.start(); super.start();
assert(timer == null);
stdout.write(' '); stdout.write(' ');
_callback(null);
timer = new Timer.periodic(const Duration(milliseconds: 100), _callback); 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 @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() { void cancel() {
if (timer?.isActive == true) { assert(timer.isActive);
timer.cancel(); timer.cancel();
// Many terminals do not interpret backspace as deleting a character, stdout.write('\b \b');
// but rather just moving the cursor back one.
stdout.write('\b \b');
}
super.cancel(); super.cancel();
} }
} }
...@@ -353,59 +379,50 @@ class AnsiSpinner extends Status { ...@@ -353,59 +379,50 @@ class AnsiSpinner extends Status {
/// On [stop], will additionally print out summary information in /// On [stop], will additionally print out summary information in
/// milliseconds if [expectSlowOperation] is false, as seconds otherwise. /// milliseconds if [expectSlowOperation] is false, as seconds otherwise.
class AnsiStatus extends AnsiSpinner { 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 String message;
final bool expectSlowOperation; final bool expectSlowOperation;
final _FinishCallback onFinish;
final int padding; final int padding;
Stopwatch stopwatch; Stopwatch stopwatch;
bool _finished = false;
@override @override
/// Writes [message] to [stdout] with padding, then begins spinning.
void start() { void start() {
stopwatch = new Stopwatch()..start(); stopwatch = new Stopwatch()..start();
stdout.write('${message.padRight(padding)} '); stdout.write('${message.padRight(padding)} ');
assert(!_finished);
super.start(); super.start();
} }
@override @override
/// Calls onFinish.
void stop() { void stop() {
if (!_finished) { super.stop();
onFinish(); writeSummaryInformation();
_finished = true; stdout.write('\n');
super.cancel();
summaryInformation();
}
} }
@override @override
void cancel() {
super.cancel();
stdout.write('\n');
}
/// Backs up 4 characters and prints a (minimum) 5 character padded time. If /// Backs up 4 characters and prints a (minimum) 5 character padded time. If
/// [expectSlowOperation] is true, the time is in seconds; otherwise, /// [expectSlowOperation] is true, the time is in seconds; otherwise,
/// milliseconds. Only backs up 4 characters because [super.cancel] backs /// milliseconds. Only backs up 4 characters because [super.cancel] backs
/// up one. /// up one.
/// ///
/// Example: '\b\b\b\b 0.5s', '\b\b\b\b150ms', '\b\b\b\b1600ms' /// Example: '\b\b\b\b 0.5s', '\b\b\b\b150ms', '\b\b\b\b1600ms'
void summaryInformation() { void writeSummaryInformation() {
if (expectSlowOperation) { 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 { } else {
stdout.writeln('\b\b\b\b${getElapsedAsMilliseconds(stopwatch.elapsed).padLeft(5)}'); stdout.write('\b\b\b\b${getElapsedAsMilliseconds(stopwatch.elapsed).padLeft(5)}');
}
}
@override
/// Calls [onFinish].
void cancel() {
if (!_finished) {
onFinish();
_finished = true;
super.cancel();
stdout.write('\n');
} }
} }
} }
...@@ -295,11 +295,15 @@ abstract class CachedArtifact { ...@@ -295,11 +295,15 @@ abstract class CachedArtifact {
return _withDownloadFile('${flattenNameSubdirs(url)}', (File tempFile) async { return _withDownloadFile('${flattenNameSubdirs(url)}', (File tempFile) async {
if (!verifier(tempFile)) { if (!verifier(tempFile)) {
final Status status = logger.startProgress(message, expectSlowOperation: true); final Status status = logger.startProgress(message, expectSlowOperation: true);
await _downloadFile(url, tempFile).then<Null>((_) { try {
await _downloadFile(url, tempFile);
status.stop(); status.stop();
}).whenComplete(status.cancel); } catch (exception) {
status.cancel();
rethrow;
}
} else { } else {
logger.printStatus('$message(cached)'); logger.printTrace('$message (cached)');
} }
_ensureExists(location); _ensureExists(location);
extractor(tempFile, location); extractor(tempFile, location);
......
...@@ -74,8 +74,10 @@ class BuildAotCommand extends BuildSubCommand { ...@@ -74,8 +74,10 @@ class BuildAotCommand extends BuildSubCommand {
Status status; Status status;
if (!argResults['quiet']) { if (!argResults['quiet']) {
final String typeName = artifacts.getEngineType(platform, buildMode); final String typeName = artifacts.getEngineType(platform, buildMode);
status = logger.startProgress('Building AOT snapshot in ${getModeName(getBuildMode())} mode ($typeName)...', status = logger.startProgress(
expectSlowOperation: true); 'Building AOT snapshot in ${getModeName(getBuildMode())} mode ($typeName)...',
expectSlowOperation: true,
);
} }
final String outputPath = argResults['output-dir'] ?? getAotBuildDirectory(); final String outputPath = argResults['output-dir'] ?? getAotBuildDirectory();
try { try {
...@@ -120,8 +122,6 @@ class BuildAotCommand extends BuildSubCommand { ...@@ -120,8 +122,6 @@ class BuildAotCommand extends BuildSubCommand {
buildSharedLibrary: false, buildSharedLibrary: false,
extraGenSnapshotOptions: argResults[FlutterOptions.kExtraGenSnapshotOptions], extraGenSnapshotOptions: argResults[FlutterOptions.kExtraGenSnapshotOptions],
).then((int buildExitCode) { ).then((int buildExitCode) {
if (buildExitCode != 0)
printError('Snapshotting ($iosArch) exited with non-zero exit code: $buildExitCode');
return buildExitCode; return buildExitCode;
}); });
}); });
...@@ -134,6 +134,12 @@ class BuildAotCommand extends BuildSubCommand { ...@@ -134,6 +134,12 @@ class BuildAotCommand extends BuildSubCommand {
..addAll(dylibs) ..addAll(dylibs)
..addAll(<String>['-create', '-output', fs.path.join(outputPath, 'App.framework', 'App')]), ..addAll(<String>['-create', '-output', fs.path.join(outputPath, 'App.framework', 'App')]),
); );
} else {
status?.cancel();
exitCodes.forEach((IOSArch iosArch, Future<int> exitCodeFuture) async {
final int buildExitCode = await exitCodeFuture;
printError('Snapshotting ($iosArch) exited with non-zero exit code: $buildExitCode');
});
} }
} else { } else {
// Android AOT snapshot. // Android AOT snapshot.
...@@ -148,12 +154,14 @@ class BuildAotCommand extends BuildSubCommand { ...@@ -148,12 +154,14 @@ class BuildAotCommand extends BuildSubCommand {
extraGenSnapshotOptions: argResults[FlutterOptions.kExtraGenSnapshotOptions], extraGenSnapshotOptions: argResults[FlutterOptions.kExtraGenSnapshotOptions],
); );
if (snapshotExitCode != 0) { if (snapshotExitCode != 0) {
status?.cancel();
printError('Snapshotting exited with non-zero exit code: $snapshotExitCode'); printError('Snapshotting exited with non-zero exit code: $snapshotExitCode');
return; return;
} }
} }
} on String catch (error) { } on String catch (error) {
// Catch the String exceptions thrown from the `runCheckedSync` methods below. // Catch the String exceptions thrown from the `runCheckedSync` methods below.
status?.cancel();
printError(error); printError(error);
return; return;
} }
......
...@@ -901,7 +901,14 @@ class _AppRunLogger extends Logger { ...@@ -901,7 +901,14 @@ class _AppRunLogger extends Logger {
'message': message, 'message': message,
}); });
_status = new _AppLoggerStatus(this, id, progressId); _status = new Status(onFinish: () {
_status = null;
_sendProgressEvent(<String, dynamic>{
'id': id.toString(),
'progressId': progressId,
'finished': true
});
});
return _status; return _status;
} }
...@@ -924,37 +931,6 @@ class _AppRunLogger extends Logger { ...@@ -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(<String, dynamic>{
'id': id.toString(),
'progressId': progressId,
'finished': true
});
}
}
class LogMessage { class LogMessage {
final String level; final String level;
final String message; final String message;
......
...@@ -84,7 +84,10 @@ class UpdatePackagesCommand extends FlutterCommand { ...@@ -84,7 +84,10 @@ class UpdatePackagesCommand extends FlutterCommand {
final bool hidden; final bool hidden;
Future<Null> _downloadCoverageData() async { Future<Null> _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 String urlBase = platform.environment['FLUTTER_STORAGE_BASE_URL'] ?? 'https://storage.googleapis.com';
final List<int> data = await fetchUrl(Uri.parse('$urlBase/flutter_infra/flutter/coverage/lcov.info')); final List<int> data = await fetchUrl(Uri.parse('$urlBase/flutter_infra/flutter/coverage/lcov.info'));
final String coverageDir = fs.path.join(Cache.flutterRoot, 'packages/flutter/coverage'); final String coverageDir = fs.path.join(Cache.flutterRoot, 'packages/flutter/coverage');
......
...@@ -109,8 +109,10 @@ Future<Null> pubGet({ ...@@ -109,8 +109,10 @@ Future<Null> pubGet({
failureMessage: 'pub $command failed', failureMessage: 'pub $command failed',
retry: true, retry: true,
); );
} finally {
status.stop(); status.stop();
} catch (exception) {
status.cancel();
rethrow;
} }
} }
......
...@@ -145,9 +145,13 @@ class Doctor { ...@@ -145,9 +145,13 @@ class Doctor {
for (ValidatorTask validatorTask in startValidatorTasks()) { for (ValidatorTask validatorTask in startValidatorTasks()) {
final DoctorValidator validator = validatorTask.validator; final DoctorValidator validator = validatorTask.validator;
final Status status = new Status.withSpinner(); final Status status = new Status.withSpinner();
await (validatorTask.result).then<void>((_) { try {
status.stop(); await validatorTask.result;
}).whenComplete(status.cancel); } catch (exception) {
status.cancel();
rethrow;
}
status.stop();
final ValidationResult result = await validatorTask.result; final ValidationResult result = await validatorTask.result;
if (result.type == ValidationType.missing) { if (result.type == ValidationType.missing) {
......
...@@ -299,7 +299,6 @@ class IOSDevice extends Device { ...@@ -299,7 +299,6 @@ class IOSDevice extends Device {
bundlePath: bundle.path, bundlePath: bundle.path,
launchArguments: launchArguments, launchArguments: launchArguments,
); );
installStatus.stop();
} else { } else {
// Debugging is enabled, look for the observatory server port post launch. // Debugging is enabled, look for the observatory server port post launch.
printTrace('Debugging is enabled, connecting to observatory'); printTrace('Debugging is enabled, connecting to observatory');
......
...@@ -385,7 +385,7 @@ class FlutterDevice { ...@@ -385,7 +385,7 @@ class FlutterDevice {
}) async { }) async {
final Status devFSStatus = logger.startProgress( final Status devFSStatus = logger.startProgress(
'Syncing files to device ${device.name}...', 'Syncing files to device ${device.name}...',
expectSlowOperation: true expectSlowOperation: true,
); );
int bytes = 0; int bytes = 0;
try { try {
...@@ -554,8 +554,9 @@ abstract class ResidentRunner { ...@@ -554,8 +554,9 @@ abstract class ResidentRunner {
for (FlutterView view in device.views) for (FlutterView view in device.views)
await view.uiIsolate.flutterDebugAllowBanner(false); await view.uiIsolate.flutterDebugAllowBanner(false);
} catch (error) { } catch (error) {
status.stop(); status.cancel();
printError('Error communicating with Flutter on the device: $error'); printError('Error communicating with Flutter on the device: $error');
return;
} }
} }
try { try {
...@@ -566,8 +567,9 @@ abstract class ResidentRunner { ...@@ -566,8 +567,9 @@ abstract class ResidentRunner {
for (FlutterView view in device.views) for (FlutterView view in device.views)
await view.uiIsolate.flutterDebugAllowBanner(true); await view.uiIsolate.flutterDebugAllowBanner(true);
} catch (error) { } catch (error) {
status.stop(); status.cancel();
printError('Error communicating with Flutter on the device: $error'); printError('Error communicating with Flutter on the device: $error');
return;
} }
} }
} }
...@@ -575,7 +577,7 @@ abstract class ResidentRunner { ...@@ -575,7 +577,7 @@ abstract class ResidentRunner {
status.stop(); status.stop();
printStatus('Screenshot written to ${fs.path.relative(outputFile.path)} (${sizeKB}kB).'); printStatus('Screenshot written to ${fs.path.relative(outputFile.path)} (${sizeKB}kB).');
} catch (error) { } catch (error) {
status.stop(); status.cancel();
printError('Error taking screenshot: $error'); printError('Error taking screenshot: $error');
} }
} }
......
...@@ -497,23 +497,20 @@ class HotRunner extends ResidentRunner { ...@@ -497,23 +497,20 @@ class HotRunner extends ResidentRunner {
if (fullRestart) { if (fullRestart) {
final Status status = logger.startProgress( final Status status = logger.startProgress(
'Performing hot restart...', 'Performing hot restart...',
progressId: 'hot.restart' progressId: 'hot.restart',
); );
try { try {
final Stopwatch timer = new Stopwatch()..start();
if (!(await hotRunnerConfig.setupHotRestart())) { if (!(await hotRunnerConfig.setupHotRestart())) {
status.cancel(); status.cancel();
return new OperationResult(1, 'setupHotRestart failed'); return new OperationResult(1, 'setupHotRestart failed');
} }
await _restartFromSources(); await _restartFromSources();
timer.stop();
status.cancel();
printStatus('Restarted app in ${getElapsedAsMilliseconds(timer.elapsed)}.');
return OperationResult.ok;
} catch (error) { } catch (error) {
status.cancel(); status.cancel();
rethrow; rethrow;
} }
status.stop(); // Prints timing information.
return OperationResult.ok;
} else { } else {
final bool reloadOnTopOfSnapshot = _runningFromSnapshot; final bool reloadOnTopOfSnapshot = _runningFromSnapshot;
final String progressPrefix = reloadOnTopOfSnapshot ? 'Initializing' : 'Performing'; final String progressPrefix = reloadOnTopOfSnapshot ? 'Initializing' : 'Performing';
...@@ -521,20 +518,21 @@ class HotRunner extends ResidentRunner { ...@@ -521,20 +518,21 @@ class HotRunner extends ResidentRunner {
'$progressPrefix hot reload...', '$progressPrefix hot reload...',
progressId: 'hot.reload' progressId: 'hot.reload'
); );
final Stopwatch timer = new Stopwatch()..start();
OperationResult result;
try { try {
final Stopwatch timer = new Stopwatch()..start(); result = await _reloadSources(pause: pauseAfterRestart);
final OperationResult result = await _reloadSources(pause: pauseAfterRestart);
timer.stop();
status.cancel();
if (result.isOk)
printStatus('${result.message} in ${getElapsedAsMilliseconds(timer.elapsed)}.');
if (result.hintMessage != null)
printStatus('\n${result.hintMessage}');
return result;
} catch (error) { } catch (error) {
status.cancel(); status?.cancel();
rethrow; rethrow;
} }
timer.stop();
status.cancel(); // Do not show summary information, since we show it in more detail below.
if (result.isOk)
printStatus('${result.message} in ${getElapsedAsMilliseconds(timer.elapsed)}.');
if (result.hintMessage != null)
printStatus('\n${result.hintMessage}');
return result;
} }
} }
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:async'; 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/io.dart';
import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/logger.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
...@@ -40,23 +41,27 @@ void main() { ...@@ -40,23 +41,27 @@ void main() {
mockStdio = new MockStdio(); mockStdio = new MockStdio();
ansiSpinner = new AnsiSpinner(); ansiSpinner = new AnsiSpinner();
called = 0; called = 0;
ansiStatus = new AnsiStatus('Hello world', true, () => called++, 20); ansiStatus = new AnsiStatus(
message: 'Hello world',
expectSlowOperation: true,
padding: 20,
onFinish: () => called++,
);
}); });
List<String> outputLines() => mockStdio.writtenToStdout.join('').split('\n'); List<String> outputLines() => mockStdio.writtenToStdout.join('').split('\n');
Future<void> doWhile(bool doThis()) async { Future<void> doWhileAsync(bool doThis()) async {
return Future.doWhile(() async { return Future.doWhile(() {
// Future.doWhile() isn't enough by itself, because the VM never gets // We want to let other tasks run at the same time, so we schedule these
// around to scheduling the other tasks for some reason. // using a timer rather than a microtask.
await new Future<void>.delayed(const Duration(milliseconds: 0)); return Future<bool>.delayed(Duration.zero, doThis);
return doThis();
}); });
} }
testUsingContext('AnsiSpinner works', () async { testUsingContext('AnsiSpinner works', () async {
ansiSpinner.start(); ansiSpinner.start();
await doWhile(() => ansiSpinner.ticks < 10); await doWhileAsync(() => ansiSpinner.ticks < 10);
List<String> lines = outputLines(); List<String> lines = outputLines();
expect(lines[0], startsWith(' \b-\b\\\b|\b/\b-\b\\\b|\b/')); expect(lines[0], startsWith(' \b-\b\\\b|\b/\b-\b\\\b|\b/'));
expect(lines[0].endsWith('\n'), isFalse); expect(lines[0].endsWith('\n'), isFalse);
...@@ -66,44 +71,37 @@ void main() { ...@@ -66,44 +71,37 @@ void main() {
expect(lines[0], endsWith('\b \b')); expect(lines[0], endsWith('\b \b'));
expect(lines.length, equals(1)); expect(lines.length, equals(1));
// Verify that stopping multiple times doesn't clear multiple times. // Verify that stopping or canceling multiple times throws.
ansiSpinner.stop(); expect(() { ansiSpinner.stop(); }, throwsA(const isInstanceOf<AssertionError>()));
lines = outputLines(); expect(() { ansiSpinner.cancel(); }, throwsA(const isInstanceOf<AssertionError>()));
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));
}, overrides: <Type, Generator>{Stdio: () => mockStdio}); }, overrides: <Type, Generator>{Stdio: () => mockStdio});
testUsingContext('AnsiStatus works when cancelled', () async { testUsingContext('AnsiStatus works when cancelled', () async {
ansiStatus.start(); ansiStatus.start();
await doWhile(() => ansiStatus.ticks < 10); await doWhileAsync(() => ansiStatus.ticks < 10);
List<String> lines = outputLines(); List<String> lines = outputLines();
expect(lines[0], startsWith('Hello world \b-\b\\\b|\b/\b-\b\\\b|\b/\b-')); 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.length, equals(1));
expect(lines[0].endsWith('\n'), isFalse);
// Verify a cancel does _not_ print the time and prints a newline.
ansiStatus.cancel(); ansiStatus.cancel();
lines = outputLines(); lines = outputLines();
final List<Match> matches = secondDigits.allMatches(lines[0]).toList();
expect(matches, isEmpty);
expect(lines[0], endsWith('\b \b')); 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)); expect(called, equals(1));
ansiStatus.stop();
lines = outputLines();
expect(lines[0].endsWith('\b \b\b \b'), isFalse);
expect(lines.length, equals(2)); 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<AssertionError>()));
expect(() { ansiStatus.stop(); }, throwsA(const isInstanceOf<AssertionError>()));
}, overrides: <Type, Generator>{Stdio: () => mockStdio}); }, overrides: <Type, Generator>{Stdio: () => mockStdio});
testUsingContext('AnsiStatus works when stopped', () async { testUsingContext('AnsiStatus works when stopped', () async {
ansiStatus.start(); ansiStatus.start();
await doWhile(() => ansiStatus.ticks < 10); await doWhileAsync(() => ansiStatus.ticks < 10);
List<String> lines = outputLines(); List<String> lines = outputLines();
expect(lines[0], startsWith('Hello world \b-\b\\\b|\b/\b-\b\\\b|\b/\b-')); expect(lines[0], startsWith('Hello world \b-\b\\\b|\b/\b-\b\\\b|\b/\b-'));
expect(lines.length, equals(1)); expect(lines.length, equals(1));
...@@ -111,55 +109,55 @@ void main() { ...@@ -111,55 +109,55 @@ void main() {
// Verify a stop prints the time. // Verify a stop prints the time.
ansiStatus.stop(); ansiStatus.stop();
lines = outputLines(); lines = outputLines();
List<Match> matches = secondDigits.allMatches(lines[0]).toList(); final List<Match> matches = secondDigits.allMatches(lines[0]).toList();
expect(matches, isNotNull); expect(matches, isNotNull);
expect(matches, hasLength(1)); expect(matches, hasLength(1));
Match match = matches.first; final Match match = matches.first;
expect(lines[0], endsWith(match.group(0))); expect(lines[0], endsWith(match.group(0)));
final String initialTime = match.group(0);
expect(called, equals(1)); expect(called, equals(1));
expect(lines.length, equals(2)); expect(lines.length, equals(2));
expect(lines[1], equals('')); expect(lines[1], equals(''));
// Verify stopping more than once generates no additional output. // Verify that stopping or canceling multiple times throws.
ansiStatus.stop(); expect(() { ansiStatus.stop(); }, throwsA(const isInstanceOf<AssertionError>()));
lines = outputLines(); expect(() { ansiStatus.cancel(); }, throwsA(const isInstanceOf<AssertionError>()));
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(''));
}, overrides: <Type, Generator>{Stdio: () => mockStdio}); }, overrides: <Type, Generator>{Stdio: () => mockStdio});
testUsingContext('AnsiStatus works when cancelled', () async { testUsingContext('sequential startProgress calls with StdoutLogger', () async {
ansiStatus.start(); context[Logger].startProgress('AAA')..stop();
await doWhile(() => ansiStatus.ticks < 10); context[Logger].startProgress('BBB')..stop();
List<String> lines = outputLines(); expect(outputLines(), <String>[
expect(lines[0], startsWith('Hello world \b-\b\\\b|\b/\b-\b\\\b|\b/\b-')); 'AAA',
expect(lines.length, equals(1)); 'BBB',
'',
]);
}, overrides: <Type, Generator>{
Stdio: () => mockStdio,
Logger: () => new StdoutLogger(),
});
// Verify a cancel does _not_ print the time and prints a newline. testUsingContext('sequential startProgress calls with VerboseLogger and StdoutLogger', () async {
ansiStatus.cancel(); context[Logger].startProgress('AAA')..stop();
lines = outputLines(); context[Logger].startProgress('BBB')..stop();
List<Match> matches = secondDigits.allMatches(lines[0]).toList(); expect(outputLines(), <String>[
expect(matches, isEmpty); '[ ] AAA',
expect(lines[0], endsWith('\b \b')); '[ ] AAA (completed)',
expect(called, equals(1)); '[ ] BBB',
// TODO(jcollins-g): Consider having status objects print the newline '[ ] BBB (completed)',
// when canceled, or never printing a newline at all. ''
expect(lines.length, equals(2)); ]);
}, overrides: <Type, Generator>{
Stdio: () => mockStdio,
Logger: () => new VerboseLogger(new StdoutLogger()),
});
// Verifying calling stop after cancel doesn't print anything weird. testUsingContext('sequential startProgress calls with BufferLogger', () async {
ansiStatus.stop(); context[Logger].startProgress('AAA')..stop();
lines = outputLines(); context[Logger].startProgress('BBB')..stop();
matches = secondDigits.allMatches(lines[0]).toList(); final BufferLogger logger = context[Logger];
expect(matches, isEmpty); expect(logger.statusText, 'AAA\nBBB\n');
expect(lines[0], endsWith('\b \b')); }, overrides: <Type, Generator>{
expect(called, equals(1)); Logger: () => new BufferLogger(),
expect(lines[0], isNot(endsWith('\b \b\b \b'))); });
expect(lines.length, equals(2));
}, overrides: <Type, Generator>{Stdio: () => mockStdio});
}); });
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment