From 7caa65943fa5d28ed17fa3ed7d80cee2c70b576b Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Wed, 19 Sep 2018 15:22:43 -0700 Subject: [PATCH] Added more extensive ANSI color printing support on terminals. (#20958) This adds support to AnsiTerminal for colored output, and makes all tool output written to stderr (with the printError function) colored red. No color codes are sent if the terminal doesn't support color (or isn't a terminal). Also makes "progress" output print the elapsed time when not connected to a terminal, so that redirected output and terminal output match (redirected output doesn't print the spinner, however). Addresses #17307 --- packages/flutter_tools/lib/runner.dart | 17 +- .../flutter_tools/lib/src/base/logger.dart | 201 +++++++++++++----- .../flutter_tools/lib/src/base/terminal.dart | 62 +++++- .../lib/src/commands/daemon.dart | 34 ++- .../lib/src/commands/fuchsia_reload.dart | 8 +- packages/flutter_tools/lib/src/compile.dart | 3 +- packages/flutter_tools/lib/src/globals.dart | 42 ++-- packages/flutter_tools/lib/src/ios/mac.dart | 3 +- packages/flutter_tools/lib/src/run_hot.dart | 13 +- .../lib/src/test/flutter_platform.dart | 9 +- .../flutter_tools/test/base/logger_test.dart | 179 ++++++++++++++-- packages/flutter_tools/test/src/context.dart | 2 +- packages/flutter_tools/test/src/mocks.dart | 5 + 13 files changed, 450 insertions(+), 128 deletions(-) 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 {