diff --git a/packages/flutter_tools/lib/runner.dart b/packages/flutter_tools/lib/runner.dart index a8f7a9a775..d727c70bf8 100644 --- a/packages/flutter_tools/lib/runner.dart +++ b/packages/flutter_tools/lib/runner.dart @@ -75,22 +75,15 @@ Future _handleToolError( String getFlutterVersion(), ) async { if (error is UsageException) { - stderr.writeln(error.message); - stderr.writeln(); - stderr.writeln( - "Run 'flutter -h' (or 'flutter -h') for available " - 'flutter commands and options.' - ); + printError('${error.message}\n'); + printError("Run 'flutter -h' (or 'flutter -h') for available flutter commands and options."); // Argument error exit code. return _exit(64); } else if (error is ToolExit) { if (error.message != null) - stderr.writeln(error.message); - if (verbose) { - stderr.writeln(); - stderr.writeln(stackTrace.toString()); - stderr.writeln(); - } + printError(error.message); + if (verbose) + printError('\n$stackTrace\n'); return _exit(error.exitCode ?? 1); } else if (error is ProcessExit) { // We've caught an exit code. diff --git a/packages/flutter_tools/lib/src/base/logger.dart b/packages/flutter_tools/lib/src/base/logger.dart index a9d09d46bd..e97d222ca6 100644 --- a/packages/flutter_tools/lib/src/base/logger.dart +++ b/packages/flutter_tools/lib/src/base/logger.dart @@ -27,14 +27,25 @@ abstract class Logger { /// Display an error level message to the user. Commands should use this if they /// fail in some way. - void printError(String message, { StackTrace stackTrace, bool emphasis = false }); + void printError( + String message, { + StackTrace stackTrace, + bool emphasis, + TerminalColor color, + }); /// Display normal output of the command. This should be used for things like /// progress messages, success messages, or just normal command output. + /// + /// If [newline] is null, then it defaults to "true". If [emphasis] is null, + /// then it defaults to "false". void printStatus( - String message, - { bool emphasis = false, bool newline = true, String ansiAlternative, int indent } - ); + String message, { + bool emphasis, + TerminalColor color, + bool newline, + int indent, + }); /// Use this for verbose tracing output. Users can turn this output on in order /// to help diagnose issues with the toolchain or with their setup. @@ -50,43 +61,57 @@ abstract class Logger { Status startProgress( String message, { String progressId, - bool expectSlowOperation = false, - int progressIndicatorPadding = kDefaultStatusPadding, + bool expectSlowOperation, + int progressIndicatorPadding, }); } class StdoutLogger extends Logger { - Status _status; @override bool get isVerbose => false; @override - void printError(String message, { StackTrace stackTrace, bool emphasis = false }) { + void printError( + String message, { + StackTrace stackTrace, + bool emphasis, + TerminalColor color, + }) { + message ??= ''; _status?.cancel(); _status = null; - if (emphasis) + if (emphasis == true) message = terminal.bolden(message); + message = terminal.color(message, color ?? TerminalColor.red); stderr.writeln(message); - if (stackTrace != null) + if (stackTrace != null) { stderr.writeln(stackTrace.toString()); + } } @override void printStatus( - String message, - { bool emphasis = false, bool newline = true, String ansiAlternative, int indent } - ) { + String message, { + bool emphasis, + TerminalColor color, + bool newline, + int indent, + }) { + message ??= ''; _status?.cancel(); _status = null; - if (terminal.supportsColor && ansiAlternative != null) - message = ansiAlternative; - if (emphasis) + if (emphasis == true) message = terminal.bolden(message); - if (indent != null && indent > 0) - message = LineSplitter.split(message).map((String line) => ' ' * indent + line).join('\n'); - if (newline) + if (color != null) + message = terminal.color(message, color); + if (indent != null && indent > 0) { + message = LineSplitter.split(message) + .map((String line) => ' ' * indent + line) + .join('\n'); + } + if (newline != false) message = '$message\n'; writeToStdOut(message); } @@ -97,15 +122,17 @@ class StdoutLogger extends Logger { } @override - void printTrace(String message) { } + void printTrace(String message) {} @override Status startProgress( String message, { String progressId, - bool expectSlowOperation = false, - int progressIndicatorPadding = 59, + bool expectSlowOperation, + int progressIndicatorPadding, }) { + expectSlowOperation ??= false; + progressIndicatorPadding ??= kDefaultStatusPadding; if (_status != null) { // Ignore nested progresses; return a no-op status object. return Status(onFinish: _clearStatus)..start(); @@ -118,8 +145,12 @@ class StdoutLogger extends Logger { onFinish: _clearStatus, )..start(); } else { - printStatus(message); - _status = Status(onFinish: _clearStatus)..start(); + _status = SummaryStatus( + message: message, + expectSlowOperation: expectSlowOperation, + padding: progressIndicatorPadding, + onFinish: _clearStatus, + )..start(); } return _status; } @@ -138,7 +169,6 @@ class StdoutLogger extends Logger { /// fonts, should be replaced by this class with printable symbols. Otherwise, /// they will show up as the unrepresentable character symbol '�'. class WindowsStdoutLogger extends StdoutLogger { - @override void writeToStdOut(String message) { // TODO(jcollins-g): wrong abstraction layer for this, move to [Stdio]. @@ -162,16 +192,24 @@ class BufferLogger extends Logger { String get traceText => _trace.toString(); @override - void printError(String message, { StackTrace stackTrace, bool emphasis = false }) { - _error.writeln(message); + void printError( + String message, { + StackTrace stackTrace, + bool emphasis, + TerminalColor color, + }) { + _error.writeln(terminal.color(message, color ?? TerminalColor.red)); } @override void printStatus( - String message, - { bool emphasis = false, bool newline = true, String ansiAlternative, int indent } - ) { - if (newline) + String message, { + bool emphasis, + TerminalColor color, + bool newline, + int indent, + }) { + if (newline != false) _status.writeln(message); else _status.write(message); @@ -184,8 +222,8 @@ class BufferLogger extends Logger { Status startProgress( String message, { String progressId, - bool expectSlowOperation = false, - int progressIndicatorPadding = kDefaultStatusPadding, + bool expectSlowOperation, + int progressIndicatorPadding, }) { printStatus(message); return Status()..start(); @@ -200,8 +238,7 @@ class BufferLogger extends Logger { } class VerboseLogger extends Logger { - VerboseLogger(this.parent) - : assert(terminal != null) { + VerboseLogger(this.parent) : assert(terminal != null) { stopwatch.start(); } @@ -213,15 +250,23 @@ class VerboseLogger extends Logger { bool get isVerbose => true; @override - void printError(String message, { StackTrace stackTrace, bool emphasis = false }) { + void printError( + String message, { + StackTrace stackTrace, + bool emphasis, + TerminalColor color, + }) { _emit(_LogType.error, message, stackTrace); } @override void printStatus( - String message, - { bool emphasis = false, bool newline = true, String ansiAlternative, int indent } - ) { + String message, { + bool emphasis, + TerminalColor color, + bool newline, + int indent, + }) { _emit(_LogType.status, message); } @@ -234,8 +279,8 @@ class VerboseLogger extends Logger { Status startProgress( String message, { String progressId, - bool expectSlowOperation = false, - int progressIndicatorPadding = kDefaultStatusPadding, + bool expectSlowOperation, + int progressIndicatorPadding, }) { printStatus(message); return Status(onFinish: () { @@ -276,11 +321,7 @@ class VerboseLogger extends Logger { } } -enum _LogType { - error, - status, - trace -} +enum _LogType { error, status, trace } /// A [Status] class begins when start is called, and may produce progress /// information asynchronously. @@ -297,7 +338,7 @@ enum _LogType { /// Generally, consider `logger.startProgress` instead of directly creating /// a [Status] or one of its subclasses. class Status { - Status({ this.onFinish }); + Status({this.onFinish}); /// A straight [Status] or an [AnsiSpinner] (depending on whether the /// terminal is fancy enough), already started. @@ -337,7 +378,7 @@ class Status { /// An [AnsiSpinner] is a simple animation that does nothing but implement an /// ASCII spinner. When stopped or canceled, the animation erases itself. class AnsiSpinner extends Status { - AnsiSpinner({ VoidCallback onFinish }) : super(onFinish: onFinish); + AnsiSpinner({VoidCallback onFinish}) : super(onFinish: onFinish); int ticks = 0; Timer timer; @@ -380,11 +421,14 @@ class AnsiSpinner extends Status { /// milliseconds if [expectSlowOperation] is false, as seconds otherwise. class AnsiStatus extends AnsiSpinner { AnsiStatus({ - this.message, - this.expectSlowOperation, - this.padding, + String message, + bool expectSlowOperation, + int padding, VoidCallback onFinish, - }) : super(onFinish: onFinish); + }) : message = message ?? '', + padding = padding ?? 0, + expectSlowOperation = expectSlowOperation ?? false, + super(onFinish: onFinish); final String message; final bool expectSlowOperation; @@ -426,3 +470,56 @@ class AnsiStatus extends AnsiSpinner { } } } + +/// Constructor writes [message] to [stdout]. On [cancel] or [stop], will call +/// [onFinish]. On [stop], will additionally print out summary information in +/// milliseconds if [expectSlowOperation] is false, as seconds otherwise. +class SummaryStatus extends Status { + SummaryStatus({ + String message, + bool expectSlowOperation, + int padding, + VoidCallback onFinish, + }) : message = message ?? '', + padding = padding ?? 0, + expectSlowOperation = expectSlowOperation ?? false, + super(onFinish: onFinish); + + final String message; + final bool expectSlowOperation; + final int padding; + + Stopwatch stopwatch; + + @override + void start() { + stopwatch = Stopwatch()..start(); + stdout.write('${message.padRight(padding)} '); + super.start(); + } + + @override + void stop() { + super.stop(); + writeSummaryInformation(); + stdout.write('\n'); + } + + @override + void cancel() { + super.cancel(); + stdout.write('\n'); + } + + /// Prints a (minimum) 5 character padded time. If [expectSlowOperation] is + /// true, the time is in seconds; otherwise, milliseconds. + /// + /// Example: ' 0.5s', '150ms', '1600ms' + void writeSummaryInformation() { + if (expectSlowOperation) { + stdout.write(getElapsedAsSeconds(stopwatch.elapsed).padLeft(5)); + } else { + stdout.write(getElapsedAsMilliseconds(stopwatch.elapsed).padLeft(5)); + } + } +} diff --git a/packages/flutter_tools/lib/src/base/terminal.dart b/packages/flutter_tools/lib/src/base/terminal.dart index c6c21b9d77..8e40c59a77 100644 --- a/packages/flutter_tools/lib/src/base/terminal.dart +++ b/packages/flutter_tools/lib/src/base/terminal.dart @@ -20,19 +20,50 @@ AnsiTerminal get terminal { : context[AnsiTerminal]; } -class AnsiTerminal { - static const String _bold = '\u001B[1m'; - static const String _reset = '\u001B[0m'; - static const String _clear = '\u001B[2J\u001B[H'; +enum TerminalColor { + red, + green, + blue, + cyan, + yellow, + magenta, + grey, +} - bool supportsColor = platform.stdoutSupportsAnsi; +class AnsiTerminal { + static const String bold = '\u001B[1m'; + static const String reset = '\u001B[0m'; + static const String clear = '\u001B[2J\u001B[H'; + + static const String red = '\u001b[31m'; + static const String green = '\u001b[32m'; + static const String blue = '\u001b[34m'; + static const String cyan = '\u001b[36m'; + static const String magenta = '\u001b[35m'; + static const String yellow = '\u001b[33m'; + static const String grey = '\u001b[1;30m'; + + static const Map _colorMap = { + TerminalColor.red: red, + TerminalColor.green: green, + TerminalColor.blue: blue, + TerminalColor.cyan: cyan, + TerminalColor.magenta: magenta, + TerminalColor.yellow: yellow, + TerminalColor.grey: grey, + }; + + static String colorCode(TerminalColor color) => _colorMap[color]; + + bool supportsColor = platform.stdoutSupportsAnsi ?? false; String bolden(String message) { - if (!supportsColor) + assert(message != null); + if (!supportsColor || message.isEmpty) return message; final StringBuffer buffer = StringBuffer(); for (String line in message.split('\n')) - buffer.writeln('$_bold$line$_reset'); + buffer.writeln('$bold$line$reset'); final String result = buffer.toString(); // avoid introducing a new newline to the emboldened text return (!message.endsWith('\n') && result.endsWith('\n')) @@ -40,7 +71,21 @@ class AnsiTerminal { : result; } - String clearScreen() => supportsColor ? _clear : '\n\n'; + String color(String message, TerminalColor color) { + assert(message != null); + if (!supportsColor || color == null || message.isEmpty) + return message; + final StringBuffer buffer = StringBuffer(); + for (String line in message.split('\n')) + buffer.writeln('${_colorMap[color]}$line$reset'); + final String result = buffer.toString(); + // avoid introducing a new newline to the colored text + return (!message.endsWith('\n') && result.endsWith('\n')) + ? result.substring(0, result.length - 1) + : result; + } + + String clearScreen() => supportsColor ? clear : '\n\n'; set singleCharMode(bool value) { final Stream> stdin = io.stdin; @@ -113,4 +158,3 @@ class AnsiTerminal { return choice; } } - diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index 81337c9eb2..be3b2b6481 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -13,6 +13,7 @@ import '../base/context.dart'; import '../base/file_system.dart'; import '../base/io.dart'; import '../base/logger.dart'; +import '../base/terminal.dart'; import '../base/utils.dart'; import '../build_info.dart'; import '../cache.dart'; @@ -746,15 +747,18 @@ class NotifyingLogger extends Logger { Stream get onMessage => _messageController.stream; @override - void printError(String message, { StackTrace stackTrace, bool emphasis = false }) { + void printError(String message, { StackTrace stackTrace, bool emphasis = false, TerminalColor color }) { _messageController.add(LogMessage('error', message, stackTrace)); } @override void printStatus( - String message, - { bool emphasis = false, bool newline = true, String ansiAlternative, int indent } - ) { + String message, { + bool emphasis = false, + TerminalColor color, + bool newline = true, + int indent, + }) { _messageController.add(LogMessage('status', message)); } @@ -868,7 +872,7 @@ class _AppRunLogger extends Logger { int _nextProgressId = 0; @override - void printError(String message, { StackTrace stackTrace, bool emphasis = false }) { + void printError(String message, { StackTrace stackTrace, bool emphasis, TerminalColor color}) { if (parent != null) { parent.printError(message, stackTrace: stackTrace, emphasis: emphasis); } else { @@ -889,14 +893,22 @@ class _AppRunLogger extends Logger { @override void printStatus( - String message, { - bool emphasis = false, bool newline = true, String ansiAlternative, int indent - }) { + String message, { + bool emphasis = false, + TerminalColor color, + bool newline = true, + int indent, + }) { if (parent != null) { - parent.printStatus(message, emphasis: emphasis, newline: newline, - ansiAlternative: ansiAlternative, indent: indent); + parent.printStatus( + message, + emphasis: emphasis, + color: color, + newline: newline, + indent: indent, + ); } else { - _sendLogEvent({ 'log': message }); + _sendLogEvent({'log': message}); } } diff --git a/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart b/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart index 10ed98a488..8dd0ab878b 100644 --- a/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart +++ b/packages/flutter_tools/lib/src/commands/fuchsia_reload.dart @@ -10,6 +10,7 @@ import '../base/common.dart'; import '../base/file_system.dart'; import '../base/io.dart'; import '../base/process_manager.dart'; +import '../base/terminal.dart'; import '../base/utils.dart'; import '../bundle.dart' as bundle; import '../cache.dart'; @@ -188,9 +189,6 @@ class FuchsiaReloadCommand extends FlutterCommand { return result; } - static const String _bold = '\u001B[0;1m'; - static const String _reset = '\u001B[0m'; - String _vmServiceToString(VMService vmService, {int tabDepth = 0}) { final Uri addr = vmService.httpAddress; final String embedder = vmService.vm.embedder; @@ -218,7 +216,7 @@ class FuchsiaReloadCommand extends FlutterCommand { final String tabs = '\t' * tabDepth; final String extraTabs = '\t' * (tabDepth + 1); final StringBuffer stringBuffer = StringBuffer( - '$tabs$_bold$embedder at $addr$_reset\n' + '$tabs${terminal.bolden('$embedder at $addr')}\n' '${extraTabs}RSS: $maxRSS\n' '${extraTabs}Native allocations: $heapSize\n' '${extraTabs}New Spaces: $newUsed of $newCap\n' @@ -257,7 +255,7 @@ class FuchsiaReloadCommand extends FlutterCommand { final String tabs = '\t' * tabDepth; final String extraTabs = '\t' * (tabDepth + 1); return - '$tabs$_bold$shortName$_reset\n' + '$tabs${terminal.bolden(shortName)}\n' '${extraTabs}Isolate number: $number\n' '${extraTabs}Observatory: $isolateAddr\n' '${extraTabs}Debugger: $debuggerAddr\n' diff --git a/packages/flutter_tools/lib/src/compile.dart b/packages/flutter_tools/lib/src/compile.dart index a98644caf2..3039c2c1b0 100644 --- a/packages/flutter_tools/lib/src/compile.dart +++ b/packages/flutter_tools/lib/src/compile.dart @@ -13,11 +13,12 @@ import 'base/context.dart'; import 'base/fingerprint.dart'; import 'base/io.dart'; import 'base/process_manager.dart'; +import 'base/terminal.dart'; import 'globals.dart'; KernelCompiler get kernelCompiler => context[KernelCompiler]; -typedef CompilerMessageConsumer = void Function(String message); +typedef CompilerMessageConsumer = void Function(String message, {bool emphasis, TerminalColor color}); class CompilerOutput { final String outputFilename; diff --git a/packages/flutter_tools/lib/src/globals.dart b/packages/flutter_tools/lib/src/globals.dart index b36eb1b851..ca8f3345d6 100644 --- a/packages/flutter_tools/lib/src/globals.dart +++ b/packages/flutter_tools/lib/src/globals.dart @@ -6,6 +6,7 @@ import 'artifacts.dart'; import 'base/config.dart'; import 'base/context.dart'; import 'base/logger.dart'; +import 'base/terminal.dart'; import 'cache.dart'; Logger get logger => context[Logger]; @@ -16,9 +17,21 @@ Artifacts get artifacts => Artifacts.instance; /// Display an error level message to the user. Commands should use this if they /// fail in some way. /// -/// Set `emphasis` to true to make the output bold if it's supported. -void printError(String message, { StackTrace stackTrace, bool emphasis = false }) { - logger.printError(message, stackTrace: stackTrace, emphasis: emphasis); +/// Set [emphasis] to true to make the output bold if it's supported. +/// Set [color] to a [TerminalColor] to color the output, if the logger +/// supports it. The [color] defaults to [TerminalColor.red]. +void printError( + String message, { + StackTrace stackTrace, + bool emphasis, + TerminalColor color, +}) { + logger.printError( + message, + stackTrace: stackTrace, + emphasis: emphasis ?? false, + color: color, + ); } /// Display normal output of the command. This should be used for things like @@ -28,20 +41,21 @@ void printError(String message, { StackTrace stackTrace, bool emphasis = false } /// /// Set `newline` to false to skip the trailing linefeed. /// -/// If `ansiAlternative` is provided, and the terminal supports color, that -/// string will be printed instead of the message. -/// -/// If `indent` is provided, each line of the message will be prepended by the specified number of -/// whitespaces. +/// If `indent` is provided, each line of the message will be prepended by the +/// specified number of whitespaces. void printStatus( - String message, - { bool emphasis = false, bool newline = true, String ansiAlternative, int indent }) { + String message, { + bool emphasis, + bool newline, + TerminalColor color, + int indent, +}) { logger.printStatus( message, - emphasis: emphasis, - newline: newline, - ansiAlternative: ansiAlternative, - indent: indent + emphasis: emphasis ?? false, + color: color, + newline: newline ?? true, + indent: indent, ); } diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index b51f8b4ddc..5cbac19a0d 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -455,8 +455,7 @@ Future buildXcodeProject({ // Free pipe file. tempDir?.deleteSync(recursive: true); printStatus( - 'Xcode build done.', - ansiAlternative: 'Xcode build done.'.padRight(kDefaultStatusPadding + 1) + 'Xcode build done.'.padRight(kDefaultStatusPadding + 1) + '${getElapsedAsSeconds(buildStopwatch.elapsed).padLeft(5)}', ); diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart index f1c1a50bc8..f973c897e7 100644 --- a/packages/flutter_tools/lib/src/run_hot.dart +++ b/packages/flutter_tools/lib/src/run_hot.dart @@ -13,6 +13,7 @@ import 'base/common.dart'; import 'base/context.dart'; import 'base/file_system.dart'; import 'base/logger.dart'; +import 'base/terminal.dart'; import 'base/utils.dart'; import 'build_info.dart'; import 'compile.dart'; @@ -740,14 +741,12 @@ class HotRunner extends ResidentRunner { @override void printHelp({ @required bool details }) { const String fire = '🔥'; - const String red = '\u001B[31m'; - const String bold = '\u001B[0;1m'; - const String reset = '\u001B[0m'; - printStatus( - '$fire To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".', - ansiAlternative: '$red$fire$bold To hot reload changes while running, press "r". ' - 'To hot restart (and rebuild state), press "R".$reset' + final String message = terminal.color( + fire + terminal.bolden(' To hot reload changes while running, press "r". ' + 'To hot restart (and rebuild state), press "R".'), + TerminalColor.red, ); + printStatus(message); for (FlutterDevice device in flutterDevices) { final String dname = device.device.name; for (Uri uri in device.observatoryUris) diff --git a/packages/flutter_tools/lib/src/test/flutter_platform.dart b/packages/flutter_tools/lib/src/test/flutter_platform.dart index 695b235083..28572f70d4 100644 --- a/packages/flutter_tools/lib/src/test/flutter_platform.dart +++ b/packages/flutter_tools/lib/src/test/flutter_platform.dart @@ -18,6 +18,7 @@ import '../base/common.dart'; import '../base/file_system.dart'; import '../base/io.dart'; import '../base/process_manager.dart'; +import '../base/terminal.dart'; import '../build_info.dart'; import '../compile.dart'; import '../dart/package_map.dart'; @@ -212,13 +213,17 @@ class _Compiler { printTrace('Compiler will use the following file as its incremental dill file: ${outputDill.path}'); bool suppressOutput = false; - void reportCompilerMessage(String message) { + void reportCompilerMessage(String message, {bool emphasis, TerminalColor color}) { if (suppressOutput) return; if (message.startsWith('compiler message: Error: Could not resolve the package \'test\'')) { printTrace(message); - printError('\n\nFailed to load test harness. Are you missing a dependency on flutter_test?\n'); + printError( + '\n\nFailed to load test harness. Are you missing a dependency on flutter_test?\n', + emphasis: emphasis, + color: color, + ); suppressOutput = true; return; } diff --git a/packages/flutter_tools/test/base/logger_test.dart b/packages/flutter_tools/test/base/logger_test.dart index fd8a1bdf8a..ebf116cf49 100644 --- a/packages/flutter_tools/test/base/logger_test.dart +++ b/packages/flutter_tools/test/base/logger_test.dart @@ -7,12 +7,17 @@ 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:flutter_tools/src/base/terminal.dart'; import '../src/common.dart'; import '../src/context.dart'; import '../src/mocks.dart'; void main() { + final String red = RegExp.escape(AnsiTerminal.red); + final String bold = RegExp.escape(AnsiTerminal.bold); + final String reset = RegExp.escape(AnsiTerminal.reset); + group('AppContext', () { test('error', () async { final BufferLogger mockLogger = BufferLogger(); @@ -28,12 +33,32 @@ void main() { expect(mockLogger.traceText, ''); expect(mockLogger.errorText, matches( r'^\[ (?: {0,2}\+[0-9]{1,3} ms| )\] Helpless!\n$')); }); + + test('ANSI colored errors', () async { + final BufferLogger mockLogger = BufferLogger(); + final VerboseLogger verboseLogger = VerboseLogger(mockLogger); + verboseLogger.supportsColor = true; + + verboseLogger.printStatus('Hey Hey Hey Hey'); + verboseLogger.printTrace('Oooh, I do I do I do'); + verboseLogger.printError('Helpless!'); + + expect( + mockLogger.statusText, + matches(r'^\[ (?: {0,2}\+[0-9]{1,3} ms| )\] ' '${bold}Hey Hey Hey Hey$reset' + r'\n\[ (?: {0,2}\+[0-9]{1,3} ms| )\] Oooh, I do I do I do\n$')); + expect(mockLogger.traceText, ''); + expect( + mockLogger.errorText, + matches('^$red' r'\[ (?: {0,2}\+[0-9]{1,3} ms| )\] ' '${bold}Helpless!$reset$reset' r'\n$')); + }); }); group('Spinners', () { MockStdio mockStdio; AnsiSpinner ansiSpinner; AnsiStatus ansiStatus; + SummaryStatus summaryStatus; int called; final RegExp secondDigits = RegExp(r'[^\b]\b\b\b\b\b[0-9]+[.][0-9]+(?:s|ms)'); @@ -47,9 +72,16 @@ void main() { padding: 20, onFinish: () => called++, ); + summaryStatus = SummaryStatus( + message: 'Hello world', + expectSlowOperation: true, + padding: 20, + onFinish: () => called++, + ); }); - List outputLines() => mockStdio.writtenToStdout.join('').split('\n'); + List outputStdout() => mockStdio.writtenToStdout.join('').split('\n'); + List outputStderr() => mockStdio.writtenToStderr.join('').split('\n'); Future doWhileAsync(bool doThis()) async { return Future.doWhile(() { @@ -62,12 +94,12 @@ void main() { testUsingContext('AnsiSpinner works', () async { ansiSpinner.start(); await doWhileAsync(() => ansiSpinner.ticks < 10); - List lines = outputLines(); + List lines = outputStdout(); expect(lines[0], startsWith(' \b-\b\\\b|\b/\b-\b\\\b|\b/')); expect(lines[0].endsWith('\n'), isFalse); expect(lines.length, equals(1)); ansiSpinner.stop(); - lines = outputLines(); + lines = outputStdout(); expect(lines[0], endsWith('\b \b')); expect(lines.length, equals(1)); @@ -76,17 +108,95 @@ void main() { expect(() { ansiSpinner.cancel(); }, throwsA(isInstanceOf())); }, overrides: {Stdio: () => mockStdio}); + testUsingContext('Error logs are red', () async { + context[Logger].printError('Pants on fire!'); + final List lines = outputStderr(); + expect(outputStdout().length, equals(1)); + expect(outputStdout().first, isEmpty); + expect(lines[0], equals('${AnsiTerminal.red}Pants on fire!${AnsiTerminal.reset}')); + }, overrides: { + Stdio: () => mockStdio, + Logger: () => StdoutLogger()..supportsColor = true, + }); + + testUsingContext('Stdout logs are not colored', () async { + context[Logger].printStatus('All good.'); + final List lines = outputStdout(); + expect(outputStderr().length, equals(1)); + expect(outputStderr().first, isEmpty); + expect(lines[0], equals('All good.')); + }, overrides: { + Stdio: () => mockStdio, + Logger: () => StdoutLogger()..supportsColor = true, + }); + + testUsingContext('Stdout printStatus handle null inputs on colored terminal', () async { + context[Logger].printStatus(null, emphasis: null, + color: null, + newline: null, + indent: null); + final List lines = outputStdout(); + expect(outputStderr().length, equals(1)); + expect(outputStderr().first, isEmpty); + expect(lines[0], equals('')); + }, overrides: { + Stdio: () => mockStdio, + Logger: () => StdoutLogger()..supportsColor = true, + }); + + testUsingContext('Stdout startProgress handle null inputs on colored terminal', () async { + context[Logger].startProgress(null, progressId: null, + expectSlowOperation: null, + progressIndicatorPadding: null, + ); + final List lines = outputStdout(); + expect(outputStderr().length, equals(1)); + expect(outputStderr().first, isEmpty); + expect(lines[0], equals(' \b-')); + }, overrides: { + Stdio: () => mockStdio, + Logger: () => StdoutLogger()..supportsColor = true, + }); + + testUsingContext('Stdout printStatus handle null inputs on regular terminal', () async { + context[Logger].printStatus(null, emphasis: null, + color: null, + newline: null, + indent: null); + final List lines = outputStdout(); + expect(outputStderr().length, equals(1)); + expect(outputStderr().first, isEmpty); + expect(lines[0], equals('')); + }, overrides: { + Stdio: () => mockStdio, + Logger: () => StdoutLogger()..supportsColor = false, + }); + + testUsingContext('Stdout startProgress handle null inputs on regular terminal', () async { + context[Logger].startProgress(null, progressId: null, + expectSlowOperation: null, + progressIndicatorPadding: null, + ); + final List lines = outputStdout(); + expect(outputStderr().length, equals(1)); + expect(outputStderr().first, isEmpty); + expect(lines[0], equals(' ')); + }, overrides: { + Stdio: () => mockStdio, + Logger: () => StdoutLogger()..supportsColor = false, + }); + testUsingContext('AnsiStatus works when cancelled', () async { ansiStatus.start(); await doWhileAsync(() => ansiStatus.ticks < 10); - List lines = outputLines(); + List lines = outputStdout(); expect(lines[0], startsWith('Hello world \b-\b\\\b|\b/\b-\b\\\b|\b/\b-')); 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(); + lines = outputStdout(); final List matches = secondDigits.allMatches(lines[0]).toList(); expect(matches, isEmpty); expect(lines[0], endsWith('\b \b')); @@ -102,13 +212,13 @@ void main() { testUsingContext('AnsiStatus works when stopped', () async { ansiStatus.start(); await doWhileAsync(() => ansiStatus.ticks < 10); - List lines = outputLines(); + List lines = outputStdout(); expect(lines[0], startsWith('Hello world \b-\b\\\b|\b/\b-\b\\\b|\b/\b-')); expect(lines.length, equals(1)); // Verify a stop prints the time. ansiStatus.stop(); - lines = outputLines(); + lines = outputStdout(); final List matches = secondDigits.allMatches(lines[0]).toList(); expect(matches, isNotNull); expect(matches, hasLength(1)); @@ -123,23 +233,68 @@ void main() { expect(() { ansiStatus.cancel(); }, throwsA(isInstanceOf())); }, overrides: {Stdio: () => mockStdio}); + testUsingContext('SummaryStatus works when cancelled', () async { + summaryStatus.start(); + List lines = outputStdout(); + expect(lines[0], startsWith('Hello world ')); + expect(lines.length, equals(1)); + expect(lines[0].endsWith('\n'), isFalse); + + // Verify a cancel does _not_ print the time and prints a newline. + summaryStatus.cancel(); + lines = outputStdout(); + final List matches = secondDigits.allMatches(lines[0]).toList(); + expect(matches, isEmpty); + expect(lines[0], endsWith(' ')); + expect(called, equals(1)); + expect(lines.length, equals(2)); + expect(lines[1], equals('')); + + // Verify that stopping or canceling multiple times throws. + expect(() { summaryStatus.cancel(); }, throwsA(isInstanceOf())); + expect(() { summaryStatus.stop(); }, throwsA(isInstanceOf())); + }, overrides: {Stdio: () => mockStdio}); + + testUsingContext('SummaryStatus works when stopped', () async { + summaryStatus.start(); + List lines = outputStdout(); + expect(lines[0], startsWith('Hello world ')); + expect(lines.length, equals(1)); + + // Verify a stop prints the time. + summaryStatus.stop(); + lines = outputStdout(); + final List matches = secondDigits.allMatches(lines[0]).toList(); + expect(matches, isNotNull); + expect(matches, hasLength(1)); + final Match match = matches.first; + expect(lines[0], endsWith(match.group(0))); + expect(called, equals(1)); + expect(lines.length, equals(2)); + expect(lines[1], equals('')); + + // Verify that stopping or canceling multiple times throws. + expect(() { summaryStatus.stop(); }, throwsA(isInstanceOf())); + expect(() { summaryStatus.cancel(); }, throwsA(isInstanceOf())); + }, overrides: {Stdio: () => mockStdio}); + testUsingContext('sequential startProgress calls with StdoutLogger', () async { context[Logger].startProgress('AAA')..stop(); context[Logger].startProgress('BBB')..stop(); - expect(outputLines(), [ - 'AAA', - 'BBB', + expect(outputStdout(), [ + 'AAA 0ms', + 'BBB 0ms', '', ]); }, overrides: { Stdio: () => mockStdio, - Logger: () => StdoutLogger(), + Logger: () => StdoutLogger()..supportsColor = false, }); testUsingContext('sequential startProgress calls with VerboseLogger and StdoutLogger', () async { context[Logger].startProgress('AAA')..stop(); context[Logger].startProgress('BBB')..stop(); - expect(outputLines(), [ + expect(outputStdout(), [ 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$'), diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart index 743b623884..b20bac2a6d 100644 --- a/packages/flutter_tools/test/src/context.dart +++ b/packages/flutter_tools/test/src/context.dart @@ -76,7 +76,7 @@ void testUsingContext(String description, dynamic testMethod(), { when(mock.getAttachedDevices()).thenReturn([]); return mock; }, - Logger: () => BufferLogger(), + Logger: () => BufferLogger()..supportsColor = false, OperatingSystemUtils: () => MockOperatingSystemUtils(), SimControl: () => MockSimControl(), Usage: () => MockUsage(), diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart index 408d111b7b..d21cc91921 100644 --- a/packages/flutter_tools/test/src/mocks.dart +++ b/packages/flutter_tools/test/src/mocks.dart @@ -287,11 +287,15 @@ class MemoryIOSink implements IOSink { /// A Stdio that collects stdout and supports simulated stdin. class MockStdio extends Stdio { final MemoryIOSink _stdout = MemoryIOSink(); + final MemoryIOSink _stderr = MemoryIOSink(); final StreamController> _stdin = StreamController>(); @override IOSink get stdout => _stdout; + @override + IOSink get stderr => _stderr; + @override Stream> get stdin => _stdin.stream; @@ -300,6 +304,7 @@ class MockStdio extends Stdio { } List get writtenToStdout => _stdout.writes.map(_stdout.encoding.decode).toList(); + List get writtenToStderr => _stderr.writes.map(_stderr.encoding.decode).toList(); } class MockPollingDeviceDiscovery extends PollingDeviceDiscovery {