Unverified Commit 7caa6594 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

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