Turn on line wrapping in usage and status messages, adds ANSI color to doctor and analysis messages. (#22656)

This turns on text wrapping for usage messages and status messages. When on a terminal, wraps to the width of the terminal. When writing to a non-terminal, wrap lines at a default column width (currently defined to be 100 chars). If --no-wrap is specified, then no wrapping occurs. If --wrap-column is specified, wraps to that column (if --wrap is on).

Adds ANSI color to the doctor and analysis output on terminals. This is in this PR with the wrapping, since wrapping needs to know how to count visible characters in the presence of ANSI sequences. (This is just one more step towards re-implementing all of Curses for Flutter. :-)) Will not print ANSI sequences when sent to a non-terminal, or of --no-color is specified.

Fixes ANSI color and bold sequences so that they can be combined (bold, colored text), and a small bug in indentation calculation for wrapping.

Since wrapping is now turned on, also removed many redundant '\n's in the code.
This commit is contained in:
Greg Spencer 2018-10-05 20:00:11 -07:00 committed by GitHub
parent cdf1cec902
commit e438632165
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 1118 additions and 216 deletions

View file

@ -223,7 +223,7 @@ linter:
print('Found $sampleCodeSections sample code sections.');
final Process process = await Process.start(
_flutter,
<String>['analyze', '--no-preamble', '--no-congratulate', mainDart.parent.path],
<String>['--no-wrap', 'analyze', '--no-preamble', '--no-congratulate', mainDart.parent.path],
workingDirectory: tempDir.path,
);
final List<String> errors = <String>[];

View file

@ -80,7 +80,7 @@ class NoAndroidStudioValidator extends DoctorValidator {
'Android Studio not found; download from https://developer.android.com/studio/index.html\n'
'(or visit https://flutter.io/setup/#android-setup for detailed instructions).'));
return ValidationResult(ValidationType.missing, messages,
return ValidationResult(ValidationType.notAvailable, messages,
statusInfo: 'not installed');
}
}

View file

@ -3,7 +3,6 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert' show LineSplitter;
import 'package:meta/meta.dart';
@ -28,26 +27,57 @@ abstract class Logger {
bool get hasTerminal => stdio.hasTerminal;
/// Display an error level message to the user. Commands should use this if they
/// Display an error [message] to the user. Commands should use this if they
/// fail in some way.
///
/// The [message] argument is printed to the stderr in red by default.
/// The [stackTrace] argument is the stack trace that will be printed if
/// supplied.
/// The [emphasis] argument will cause the output message be printed in bold text.
/// The [color] argument will print the message in the supplied color instead
/// of the default of red. Colors will not be printed if the output terminal
/// doesn't support them.
/// The [indent] argument specifies the number of spaces to indent the overall
/// message. If wrapping is enabled in [outputPreferences], then the wrapped
/// lines will be indented as well.
/// If [hangingIndent] is specified, then any wrapped lines will be indented
/// by this much more than the first line, if wrapping is enabled in
/// [outputPreferences].
void printError(
String message, {
StackTrace stackTrace,
bool emphasis,
TerminalColor color,
int indent,
int hangingIndent,
});
/// 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".
/// The [message] argument is printed to the stderr in red by default.
/// The [stackTrace] argument is the stack trace that will be printed if
/// supplied.
/// If the [emphasis] argument is true, it will cause the output message be
/// printed in bold text. Defaults to false.
/// The [color] argument will print the message in the supplied color instead
/// of the default of red. Colors will not be printed if the output terminal
/// doesn't support them.
/// If [newline] is true, then a newline will be added after printing the
/// status. Defaults to true.
/// The [indent] argument specifies the number of spaces to indent the overall
/// message. If wrapping is enabled in [outputPreferences], then the wrapped
/// lines will be indented as well.
/// If [hangingIndent] is specified, then any wrapped lines will be indented
/// by this much more than the first line, if wrapping is enabled in
/// [outputPreferences].
void printStatus(
String message, {
bool emphasis,
TerminalColor color,
bool newline,
int indent,
int hangingIndent,
});
/// Use this for verbose tracing output. Users can turn this output on in order
@ -82,8 +112,11 @@ class StdoutLogger extends Logger {
StackTrace stackTrace,
bool emphasis,
TerminalColor color,
int indent,
int hangingIndent,
}) {
message ??= '';
message = wrapText(message, indent: indent, hangingIndent: hangingIndent);
_status?.cancel();
_status = null;
if (emphasis == true)
@ -102,19 +135,16 @@ class StdoutLogger extends Logger {
TerminalColor color,
bool newline,
int indent,
int hangingIndent,
}) {
message ??= '';
message = wrapText(message, indent: indent, hangingIndent: hangingIndent);
_status?.cancel();
_status = null;
if (emphasis == true)
message = terminal.bolden(message);
if (color != null)
message = terminal.color(message, color);
if (indent != null && indent > 0) {
message = LineSplitter.split(message)
.map<String>((String line) => ' ' * indent + line)
.join('\n');
}
if (newline != false)
message = '$message\n';
writeToStdOut(message);
@ -203,8 +233,13 @@ class BufferLogger extends Logger {
StackTrace stackTrace,
bool emphasis,
TerminalColor color,
int indent,
int hangingIndent,
}) {
_error.writeln(terminal.color(message, color ?? TerminalColor.red));
_error.writeln(terminal.color(
wrapText(message, indent: indent, hangingIndent: hangingIndent),
color ?? TerminalColor.red,
));
}
@override
@ -214,11 +249,12 @@ class BufferLogger extends Logger {
TerminalColor color,
bool newline,
int indent,
int hangingIndent,
}) {
if (newline != false)
_status.writeln(message);
_status.writeln(wrapText(message, indent: indent, hangingIndent: hangingIndent));
else
_status.write(message);
_status.write(wrapText(message, indent: indent, hangingIndent: hangingIndent));
}
@override
@ -262,8 +298,14 @@ class VerboseLogger extends Logger {
StackTrace stackTrace,
bool emphasis,
TerminalColor color,
int indent,
int hangingIndent,
}) {
_emit(_LogType.error, message, stackTrace);
_emit(
_LogType.error,
wrapText(message, indent: indent, hangingIndent: hangingIndent),
stackTrace,
);
}
@override
@ -273,8 +315,9 @@ class VerboseLogger extends Logger {
TerminalColor color,
bool newline,
int indent,
int hangingIndent,
}) {
_emit(_LogType.status, message);
_emit(_LogType.status, wrapText(message, indent: indent, hangingIndent: hangingIndent));
}
@override

View file

@ -11,6 +11,7 @@ import '../globals.dart';
import 'context.dart';
import 'io.dart' as io;
import 'platform.dart';
import 'utils.dart';
final AnsiTerminal _kAnsiTerminal = AnsiTerminal();
@ -30,6 +31,45 @@ enum TerminalColor {
grey,
}
final OutputPreferences _kOutputPreferences = OutputPreferences();
OutputPreferences get outputPreferences => (context == null || context[OutputPreferences] == null)
? _kOutputPreferences
: context[OutputPreferences];
/// A class that contains the context settings for command text output to the
/// console.
class OutputPreferences {
OutputPreferences({
bool wrapText,
int wrapColumn,
bool showColor,
}) : wrapText = wrapText ?? true,
wrapColumn = wrapColumn ?? const io.Stdio().terminalColumns ?? kDefaultTerminalColumns,
showColor = showColor ?? platform.stdoutSupportsAnsi ?? false;
/// If [wrapText] is true, then output text sent to the context's [Logger]
/// instance (e.g. from the [printError] or [printStatus] functions) will be
/// wrapped to be no longer than the [wrapColumn] specifies. Defaults to true.
final bool wrapText;
/// The column at which any output sent to the context's [Logger] instance
/// (e.g. from the [printError] or [printStatus] functions) will be wrapped.
/// Ignored if [wrapText] is false. Defaults to the width of the output
/// terminal, or to [kDefaultTerminalColumns] if not writing to a terminal.
final int wrapColumn;
/// Whether or not to output ANSI color codes when writing to the output
/// terminal. Defaults to whatever [platform.stdoutSupportsAnsi] says if
/// writing to a terminal, and false otherwise.
final bool showColor;
@override
String toString() {
return '$runtimeType[wrapText: $wrapText, wrapColumn: $wrapColumn, showColor: $showColor]';
}
}
class AnsiTerminal {
static const String bold = '\u001B[1m';
static const String reset = '\u001B[0m';
@ -62,8 +102,16 @@ class AnsiTerminal {
if (!supportsColor || message.isEmpty)
return message;
final StringBuffer buffer = StringBuffer();
for (String line in message.split('\n'))
for (String line in message.split('\n')) {
// If there were resets in the string before, then keep them, but
// restart the bold right after. This prevents embedded resets from
// stopping the boldness.
line = line.replaceAll(reset, '$reset$bold');
// Remove all codes at the end of the string, since we're just going
// to reset them, and they would either be redundant, or have no effect.
line = line.replaceAll(RegExp('(\u001b\[[0-9;]+m)+\$'), '');
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'))
@ -76,8 +124,17 @@ class AnsiTerminal {
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 colorCodes = _colorMap[color];
for (String line in message.split('\n')) {
// If there were resets in the string before, then keep them, but
// restart the color right after. This prevents embedded resets from
// stopping the colors.
line = line.replaceAll(reset, '$reset$colorCodes');
// Remove any extra codes at the end of the string, since we're just going
// to reset them.
line = line.replaceAll(RegExp('(\u001b\[[0-9;]*m)+\$'), '');
buffer.writeln('$colorCodes$line$reset');
}
final String result = buffer.toString();
// avoid introducing a new newline to the colored text
return (!message.endsWith('\n') && result.endsWith('\n'))

View file

@ -4,7 +4,7 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math' show Random;
import 'dart:math' show Random, max;
import 'package:crypto/crypto.dart';
import 'package:intl/intl.dart';
@ -13,7 +13,9 @@ import 'package:quiver/time.dart';
import '../globals.dart';
import 'context.dart';
import 'file_system.dart';
import 'io.dart' as io;
import 'platform.dart';
import 'terminal.dart';
const BotDetector _kBotDetector = BotDetector();
@ -300,3 +302,229 @@ class Poller {
Future<List<T>> waitGroup<T>(Iterable<Future<T>> futures) {
return Future.wait<T>(futures.where((Future<T> future) => future != null));
}
/// The terminal width used by the [wrapText] function if there is no terminal
/// attached to [io.Stdio], --wrap is on, and --wrap-columns was not specified.
const int kDefaultTerminalColumns = 100;
/// Smallest column that will be used for text wrapping. If the requested column
/// width is smaller than this, then this is what will be used.
const int kMinColumnWidth = 10;
/// Wraps a block of text into lines no longer than [columnWidth].
///
/// Tries to split at whitespace, but if that's not good enough to keep it
/// under the limit, then it splits in the middle of a word.
///
/// Preserves indentation (leading whitespace) for each line (delimited by '\n')
/// in the input, and will indent wrapped lines that same amount, adding
/// [indent] spaces in addition to any existing indent.
///
/// If [hangingIndent] is supplied, then that many additional spaces will be
/// added to each line, except for the first line. The [hangingIndent] is added
/// to the specified [indent], if any. This is useful for wrapping
/// text with a heading prefix (e.g. "Usage: "):
///
/// ```dart
/// String prefix = "Usage: ";
/// print(prefix + wrapText(invocation, indent: 2, hangingIndent: prefix.length, columnWidth: 40));
/// ```
///
/// yields:
/// ```
/// Usage: app main_command <subcommand>
/// [arguments]
/// ```
///
/// If [columnWidth] is not specified, then the column width will be the
/// [outputPreferences.wrapColumn], which is set with the --wrap-column option.
///
/// If [outputPreferences.wrapText] is false, then the text will be returned
/// unchanged.
///
/// The [indent] and [hangingIndent] must be smaller than [columnWidth] when
/// added together.
String wrapText(String text, {int columnWidth, int hangingIndent, int indent}) {
if (text == null || text.isEmpty) {
return '';
}
indent ??= 0;
columnWidth ??= outputPreferences.wrapColumn;
columnWidth -= indent;
assert(columnWidth >= 0);
hangingIndent ??= 0;
final List<String> splitText = text.split('\n');
final List<String> result = <String>[];
for (String line in splitText) {
String trimmedText = line.trimLeft();
final String leadingWhitespace = line.substring(0, line.length - trimmedText.length);
List<String> notIndented;
if (hangingIndent != 0) {
// When we have a hanging indent, we want to wrap the first line at one
// width, and the rest at another (offset by hangingIndent), so we wrap
// them twice and recombine.
final List<String> firstLineWrap = _wrapTextAsLines(
trimmedText,
columnWidth: columnWidth - leadingWhitespace.length,
);
notIndented = <String>[firstLineWrap.removeAt(0)];
trimmedText = trimmedText.substring(notIndented[0].length).trimLeft();
if (firstLineWrap.isNotEmpty) {
notIndented.addAll(_wrapTextAsLines(
trimmedText,
columnWidth: columnWidth - leadingWhitespace.length - hangingIndent,
));
}
} else {
notIndented = _wrapTextAsLines(
trimmedText,
columnWidth: columnWidth - leadingWhitespace.length,
);
}
String hangingIndentString;
final String indentString = ' ' * indent;
result.addAll(notIndented.map(
(String line) {
// Don't return any lines with just whitespace on them.
if (line.isEmpty) {
return '';
}
final String result = '$indentString${hangingIndentString ?? ''}$leadingWhitespace$line';
hangingIndentString ??= ' ' * hangingIndent;
return result;
},
));
}
return result.join('\n');
}
// Used to represent a run of ANSI control sequences next to a visible
// character.
class _AnsiRun {
_AnsiRun(this.original, this.character);
String original;
String character;
}
/// Wraps a block of text into lines no longer than [columnWidth], starting at the
/// [start] column, and returning the result as a list of strings.
///
/// Tries to split at whitespace, but if that's not good enough to keep it
/// under the limit, then splits in the middle of a word. Preserves embedded
/// newlines, but not indentation (it trims whitespace from each line).
///
/// If [columnWidth] is not specified, then the column width will be the width of the
/// terminal window by default. If the stdout is not a terminal window, then the
/// default will be [outputPreferences.wrapColumn].
///
/// If [outputPreferences.wrapText] is false, then the text will be returned
/// simply split at the newlines, but not wrapped.
List<String> _wrapTextAsLines(String text, {int start = 0, int columnWidth}) {
if (text == null || text.isEmpty) {
return <String>[''];
}
columnWidth ??= const io.Stdio().terminalColumns ?? kDefaultTerminalColumns;
assert(columnWidth >= 0);
assert(start >= 0);
/// Returns true if the code unit at [index] in [text] is a whitespace
/// character.
///
/// Based on: https://en.wikipedia.org/wiki/Whitespace_character#Unicode
bool isWhitespace(_AnsiRun run) {
final int rune = run.character.isNotEmpty ? run.character.codeUnitAt(0) : 0x0;
return rune >= 0x0009 && rune <= 0x000D ||
rune == 0x0020 ||
rune == 0x0085 ||
rune == 0x1680 ||
rune == 0x180E ||
rune >= 0x2000 && rune <= 0x200A ||
rune == 0x2028 ||
rune == 0x2029 ||
rune == 0x202F ||
rune == 0x205F ||
rune == 0x3000 ||
rune == 0xFEFF;
}
// Splits a string so that the resulting list has the same number of elements
// as there are visible characters in the string, but elements may include one
// or more adjacent ANSI sequences. Joining the list elements again will
// reconstitute the original string. This is useful for manipulating "visible"
// characters in the presence of ANSI control codes.
List<_AnsiRun> splitWithCodes(String input) {
final RegExp characterOrCode = RegExp('(\u001b\[[0-9;]*m|.)', multiLine: true);
List<_AnsiRun> result = <_AnsiRun>[];
final StringBuffer current = StringBuffer();
for (Match match in characterOrCode.allMatches(input)) {
current.write(match[0]);
if (match[0].length < 4) {
// This is a regular character, write it out.
result.add(_AnsiRun(current.toString(), match[0]));
current.clear();
}
}
// If there's something accumulated, then it must be an ANSI sequence, so
// add it to the end of the last entry so that we don't lose it.
if (current.isNotEmpty) {
if (result.isNotEmpty) {
result.last.original += current.toString();
} else {
// If there is nothing in the string besides control codes, then just
// return them as the only entry.
result = <_AnsiRun>[_AnsiRun(current.toString(), '')];
}
}
return result;
}
String joinRun(List<_AnsiRun> list, int start, [int end]) {
return list.sublist(start, end).map<String>((_AnsiRun run) => run.original).join().trim();
}
final List<String> result = <String>[];
final int effectiveLength = max(columnWidth - start, kMinColumnWidth);
for (String line in text.split('\n')) {
// If the line is short enough, even with ANSI codes, then we can just add
// add it and move on.
if (line.length <= effectiveLength || !outputPreferences.wrapText) {
result.add(line);
continue;
}
final List<_AnsiRun> splitLine = splitWithCodes(line);
if (splitLine.length <= effectiveLength) {
result.add(line);
continue;
}
int currentLineStart = 0;
int lastWhitespace;
// Find the start of the current line.
for (int index = 0; index < splitLine.length; ++index) {
if (splitLine[index].character.isNotEmpty && isWhitespace(splitLine[index])) {
lastWhitespace = index;
}
if (index - currentLineStart >= effectiveLength) {
// Back up to the last whitespace, unless there wasn't any, in which
// case we just split where we are.
if (lastWhitespace != null) {
index = lastWhitespace;
}
result.add(joinRun(splitLine, currentLineStart, index));
// Skip any intervening whitespace.
while (isWhitespace(splitLine[index]) && index < splitLine.length) {
index++;
}
currentLineStart = index;
lastWhitespace = null;
}
}
result.add(joinRun(splitLine, currentLineStart));
}
return result;
}

View file

@ -20,7 +20,7 @@ class AnalyzeCommand extends FlutterCommand {
help: 'Analyze the current project, if applicable.', defaultsTo: true);
argParser.addFlag('dartdocs',
negatable: false,
help: 'List every public member that is lacking documentation.\n'
help: 'List every public member that is lacking documentation. '
'(The public_member_api_docs lint must be enabled in analysis_options.yaml)',
hide: !verboseHelp);
argParser.addFlag('watch',
@ -45,7 +45,7 @@ class AnalyzeCommand extends FlutterCommand {
// Not used by analyze --watch
argParser.addFlag('congratulate',
help: 'Show output even when there are no errors, warnings, hints, or lints.\n'
help: 'Show output even when there are no errors, warnings, hints, or lints. '
'Ignored if --watch is specified.',
defaultsTo: true);
argParser.addFlag('preamble',

View file

@ -132,7 +132,7 @@ class AnalyzeOnce extends AnalyzeBase {
printStatus('');
errors.sort();
for (AnalysisError error in errors)
printStatus(error.toString());
printStatus(error.toString(), hangingIndent: 7);
final String seconds = (timer.elapsedMilliseconds / 1000.0).toStringAsFixed(1);

View file

@ -50,7 +50,7 @@ class AttachCommand extends FlutterCommand {
)..addFlag('machine',
hide: !verboseHelp,
negatable: false,
help: 'Handle machine structured JSON command input and provide output\n'
help: 'Handle machine structured JSON command input and provide output '
'and progress in machine friendly format.',
);
hotRunnerFactory ??= HotRunnerFactory();

View file

@ -34,8 +34,8 @@ class BuildApkCommand extends BuildSubCommand {
@override
final String description = 'Build an Android APK file from your app.\n\n'
'This command can build debug and release versions of your application. \'debug\' builds support\n'
'debugging and a quick development cycle. \'release\' builds don\'t support debugging and are\n'
'This command can build debug and release versions of your application. \'debug\' builds support '
'debugging and a quick development cycle. \'release\' builds don\'t support debugging and are '
'suitable for deploying to app stores.';
@override

View file

@ -35,19 +35,19 @@ class BuildBundleCommand extends BuildSubCommand {
)
..addOption('precompile',
hide: !verboseHelp,
help: 'Precompile functions specified in input file. This flag is only\n'
'allowed when using --dynamic. It takes a Dart compilation trace\n'
'file produced by the training run of the application. With this\n'
'flag, instead of using default Dart VM snapshot provided by the\n'
'engine, the application will use its own snapshot that includes\n'
help: 'Precompile functions specified in input file. This flag is only '
'allowed when using --dynamic. It takes a Dart compilation trace '
'file produced by the training run of the application. With this '
'flag, instead of using default Dart VM snapshot provided by the '
'engine, the application will use its own snapshot that includes '
'additional compiled functions.'
)
..addFlag('hotupdate',
hide: !verboseHelp,
help: 'Build differential snapshot based on the last state of the build\n'
'tree and any changes to the application source code since then.\n'
'This flag is only allowed when using --dynamic. With this flag,\n'
'a partial VM snapshot is generated that is loaded on top of the\n'
help: 'Build differential snapshot based on the last state of the build '
'tree and any changes to the application source code since then. '
'This flag is only allowed when using --dynamic. With this flag, '
'a partial VM snapshot is generated that is loaded on top of the '
'original VM snapshot that contains precompiled code.'
)
..addMultiOption(FlutterOptions.kExtraFrontEndOptions,

View file

@ -35,7 +35,7 @@ class ConfigCommand extends FlutterCommand {
final String description =
'Configure Flutter settings.\n\n'
'To remove a setting, configure it to an empty string.\n\n'
'The Flutter tool anonymously reports feature usage statistics and basic crash reports to help improve\n'
'The Flutter tool anonymously reports feature usage statistics and basic crash reports to help improve '
'Flutter tools over time. See Google\'s privacy policy: https://www.google.com/intl/en/policies/privacy/';
@override

View file

@ -109,7 +109,7 @@ class CreateCommand extends FlutterCommand {
argParser.addOption(
'org',
defaultsTo: 'com.example',
help: 'The organization responsible for your new Flutter project, in reverse domain name notation.\n'
help: 'The organization responsible for your new Flutter project, in reverse domain name notation. '
'This string is used in Java package names and as prefix in the iOS bundle identifier.'
);
argParser.addOption(
@ -191,7 +191,7 @@ class CreateCommand extends FlutterCommand {
}
if (Cache.flutterRoot == null)
throwToolExit('Neither the --flutter-root command line flag nor the FLUTTER_ROOT environment\n'
throwToolExit('Neither the --flutter-root command line flag nor the FLUTTER_ROOT environment '
'variable was specified. Unable to find package:flutter.', exitCode: 2);
await Cache.instance.updateAll();
@ -247,7 +247,7 @@ class CreateCommand extends FlutterCommand {
organization = existingOrganizations.first;
} else if (1 < existingOrganizations.length) {
throwToolExit(
'Ambiguous organization in existing files: $existingOrganizations.\n'
'Ambiguous organization in existing files: $existingOrganizations. '
'The --org command line argument must be specified to recreate project.'
);
}
@ -571,7 +571,7 @@ String _validateProjectName(String projectName) {
/// if we should disallow the directory name.
String _validateProjectDir(String dirPath, { String flutterRoot }) {
if (fs.path.isWithin(flutterRoot, dirPath)) {
return 'Cannot create a project within the Flutter SDK.\n'
return 'Cannot create a project within the Flutter SDK. '
"Target directory '$dirPath' is within the Flutter SDK at '$flutterRoot'.";
}

View file

@ -748,18 +748,26 @@ class NotifyingLogger extends Logger {
Stream<LogMessage> get onMessage => _messageController.stream;
@override
void printError(String message, { StackTrace stackTrace, bool emphasis = false, TerminalColor color }) {
void printError(
String message, {
StackTrace stackTrace,
bool emphasis = false,
TerminalColor color,
int indent,
int hangingIndent,
}) {
_messageController.add(LogMessage('error', message, stackTrace));
}
@override
void printStatus(
String message, {
bool emphasis = false,
TerminalColor color,
bool newline = true,
int indent,
}) {
bool emphasis = false,
TerminalColor color,
bool newline = true,
int indent,
int hangingIndent,
}) {
_messageController.add(LogMessage('status', message));
}
@ -874,9 +882,22 @@ class _AppRunLogger extends Logger {
int _nextProgressId = 0;
@override
void printError(String message, { StackTrace stackTrace, bool emphasis, TerminalColor color}) {
void printError(
String message, {
StackTrace stackTrace,
bool emphasis,
TerminalColor color,
int indent,
int hangingIndent,
}) {
if (parent != null) {
parent.printError(message, stackTrace: stackTrace, emphasis: emphasis);
parent.printError(
message,
stackTrace: stackTrace,
emphasis: emphasis,
indent: indent,
hangingIndent: hangingIndent,
);
} else {
if (stackTrace != null) {
_sendLogEvent(<String, dynamic>{
@ -896,11 +917,12 @@ class _AppRunLogger extends Logger {
@override
void printStatus(
String message, {
bool emphasis = false,
TerminalColor color,
bool newline = true,
int indent,
}) {
bool emphasis = false,
TerminalColor color,
bool newline = true,
int indent,
int hangingIndent,
}) {
if (parent != null) {
parent.printStatus(
message,
@ -908,6 +930,7 @@ class _AppRunLogger extends Logger {
color: color,
newline: newline,
indent: indent,
hangingIndent: hangingIndent,
);
} else {
_sendLogEvent(<String, dynamic>{'log': message});

View file

@ -33,13 +33,13 @@ class DevicesCommand extends FlutterCommand {
printStatus(
'No devices detected.\n\n'
"Run 'flutter emulators' to list and start any available device emulators.\n\n"
'Or, if you expected your device to be detected, please run "flutter doctor" to diagnose\n'
'Or, if you expected your device to be detected, please run "flutter doctor" to diagnose '
'potential issues, or visit https://flutter.io/setup/ for troubleshooting tips.');
final List<String> diagnostics = await deviceManager.getDeviceDiagnostics();
if (diagnostics.isNotEmpty) {
printStatus('');
for (String diagnostic in diagnostics) {
printStatus('${diagnostic.replaceAll('\n', '\n ')}');
printStatus('$diagnostic', hangingIndent: 2);
}
}
} else {

View file

@ -45,23 +45,24 @@ class DriveCommand extends RunCommandBase {
..addFlag('keep-app-running',
defaultsTo: null,
help: 'Will keep the Flutter application running when done testing.\n'
'By default, "flutter drive" stops the application after tests are finished,\n'
'and --keep-app-running overrides this. On the other hand, if --use-existing-app\n'
'is specified, then "flutter drive" instead defaults to leaving the application\n'
'By default, "flutter drive" stops the application after tests are finished, '
'and --keep-app-running overrides this. On the other hand, if --use-existing-app '
'is specified, then "flutter drive" instead defaults to leaving the application '
'running, and --no-keep-app-running overrides it.',
)
..addOption('use-existing-app',
help: 'Connect to an already running instance via the given observatory URL.\n'
'If this option is given, the application will not be automatically started,\n'
help: 'Connect to an already running instance via the given observatory URL. '
'If this option is given, the application will not be automatically started, '
'and it will only be stopped if --no-keep-app-running is explicitly set.',
valueHelp: 'url',
)
..addOption('driver',
help: 'The test file to run on the host (as opposed to the target file to run on\n'
'the device). By default, this file has the same base name as the target\n'
'file, but in the "test_driver/" directory instead, and with "_test" inserted\n'
'just before the extension, so e.g. if the target is "lib/main.dart", the\n'
'driver will be "test_driver/main_test.dart".',
help: 'The test file to run on the host (as opposed to the target file to run on '
'the device).\n'
'By default, this file has the same base name as the target file, but in the '
'"test_driver/" directory instead, and with "_test" inserted just before the '
'extension, so e.g. if the target is "lib/main.dart", the driver will be '
'"test_driver/main_test.dart".',
valueHelp: 'path',
);
}

View file

@ -105,10 +105,10 @@ class PackagesTestCommand extends FlutterCommand {
@override
String get description {
return 'Run the "test" package.\n'
'This is similar to "flutter test", but instead of hosting the tests in the\n'
'flutter environment it hosts the tests in a pure Dart environment. The main\n'
'differences are that the "dart:ui" library is not available and that tests\n'
'run faster. This is helpful for testing libraries that do not depend on any\n'
'This is similar to "flutter test", but instead of hosting the tests in the '
'flutter environment it hosts the tests in a pure Dart environment. The main '
'differences are that the "dart:ui" library is not available and that tests '
'run faster. This is helpful for testing libraries that do not depend on any '
'packages from the Flutter SDK. It is equivalent to "pub run test".';
}

View file

@ -32,7 +32,7 @@ abstract class RunCommandBase extends FlutterCommand {
..addFlag('ipv6',
hide: true,
negatable: false,
help: 'Binds to IPv6 localhost instead of IPv4 when the flutter tool\n'
help: 'Binds to IPv6 localhost instead of IPv4 when the flutter tool '
'forwards the host port to a device port.',
)
..addOption('route',
@ -83,27 +83,27 @@ class RunCommand extends RunCommandBase {
)
..addFlag('enable-software-rendering',
negatable: false,
help: 'Enable rendering using the Skia software backend. This is useful\n'
'when testing Flutter on emulators. By default, Flutter will\n'
'attempt to either use OpenGL or Vulkan and fall back to software\n'
'when neither is available.',
help: 'Enable rendering using the Skia software backend. '
'This is useful when testing Flutter on emulators. By default, '
'Flutter will attempt to either use OpenGL or Vulkan and fall back '
'to software when neither is available.',
)
..addFlag('skia-deterministic-rendering',
negatable: false,
help: 'When combined with --enable-software-rendering, provides 100%\n'
help: 'When combined with --enable-software-rendering, provides 100% '
'deterministic Skia rendering.',
)
..addFlag('trace-skia',
negatable: false,
help: 'Enable tracing of Skia code. This is useful when debugging\n'
help: 'Enable tracing of Skia code. This is useful when debugging '
'the GPU thread. By default, Flutter will not log skia code.',
)
..addFlag('use-test-fonts',
negatable: true,
help: 'Enable (and default to) the "Ahem" font. This is a special font\n'
'used in tests to remove any dependencies on the font metrics. It\n'
'is enabled when you use "flutter test". Set this flag when running\n'
'a test using "flutter run" for debugging purposes. This flag is\n'
help: 'Enable (and default to) the "Ahem" font. This is a special font '
'used in tests to remove any dependencies on the font metrics. It '
'is enabled when you use "flutter test". Set this flag when running '
'a test using "flutter run" for debugging purposes. This flag is '
'only available when running in debug mode.',
)
..addFlag('build',
@ -116,19 +116,19 @@ class RunCommand extends RunCommandBase {
)
..addOption('precompile',
hide: !verboseHelp,
help: 'Precompile functions specified in input file. This flag is only\n'
'allowed when using --dynamic. It takes a Dart compilation trace\n'
'file produced by the training run of the application. With this\n'
'flag, instead of using default Dart VM snapshot provided by the\n'
'engine, the application will use its own snapshot that includes\n'
help: 'Precompile functions specified in input file. This flag is only '
'allowed when using --dynamic. It takes a Dart compilation trace '
'file produced by the training run of the application. With this '
'flag, instead of using default Dart VM snapshot provided by the '
'engine, the application will use its own snapshot that includes '
'additional functions.'
)
..addFlag('hotupdate',
hide: !verboseHelp,
help: 'Build differential snapshot based on the last state of the build\n'
'tree and any changes to the application source code since then.\n'
'This flag is only allowed when using --dynamic. With this flag,\n'
'a partial VM snapshot is generated that is loaded on top of the\n'
help: 'Build differential snapshot based on the last state of the build '
'tree and any changes to the application source code since then. '
'This flag is only allowed when using --dynamic. With this flag, '
'a partial VM snapshot is generated that is loaded on top of the '
'original VM snapshot that contains precompiled code.'
)
..addFlag('track-widget-creation',
@ -142,7 +142,7 @@ class RunCommand extends RunCommandBase {
..addFlag('machine',
hide: !verboseHelp,
negatable: false,
help: 'Handle machine structured JSON command input and provide output\n'
help: 'Handle machine structured JSON command input and provide output '
'and progress in machine friendly format.',
)
..addFlag('hot',
@ -151,8 +151,8 @@ class RunCommand extends RunCommandBase {
help: 'Run with support for hot reloading.',
)
..addOption('pid-file',
help: 'Specify a file to write the process id to.\n'
'You can send SIGUSR1 to trigger a hot reload\n'
help: 'Specify a file to write the process id to. '
'You can send SIGUSR1 to trigger a hot reload '
'and SIGUSR2 to trigger a hot restart.',
)
..addFlag('resident',
@ -164,9 +164,9 @@ class RunCommand extends RunCommandBase {
..addFlag('benchmark',
negatable: false,
hide: !verboseHelp,
help: 'Enable a benchmarking mode. This will run the given application,\n'
'measure the startup time and the app restart time, write the\n'
'results out to "refresh_benchmark.json", and exit. This flag is\n'
help: 'Enable a benchmarking mode. This will run the given application, '
'measure the startup time and the app restart time, write the '
'results out to "refresh_benchmark.json", and exit. This flag is '
'intended for use in generating automated flutter benchmarks.',
)
..addOption(FlutterOptions.kExtraFrontEndOptions, hide: true)

View file

@ -33,7 +33,7 @@ class ScreenshotCommand extends FlutterCommand {
valueHelp: 'port',
help: 'The observatory port to connect to.\n'
'This is required when --$_kType is "$_kSkiaType" or "$_kRasterizerType".\n'
'To find the observatory port number, use "flutter run --verbose"\n'
'To find the observatory port number, use "flutter run --verbose" '
'and look for "Forwarded host port ... for Observatory" in the output.',
);
argParser.addOption(
@ -42,8 +42,8 @@ class ScreenshotCommand extends FlutterCommand {
help: 'The type of screenshot to retrieve.',
allowed: const <String>[_kDeviceType, _kSkiaType, _kRasterizerType],
allowedHelp: const <String, String>{
_kDeviceType: 'Delegate to the device\'s native screenshot capabilities. This\n'
'screenshots the entire screen currently being displayed (including content\n'
_kDeviceType: 'Delegate to the device\'s native screenshot capabilities. This '
'screenshots the entire screen currently being displayed (including content '
'not rendered by Flutter, like the device status bar).',
_kSkiaType: 'Render the Flutter app as a Skia picture. Requires --$_kObservatoryPort',
_kRasterizerType: 'Render the Flutter app using the rasterizer. Requires --$_kObservatoryPort',

View file

@ -26,9 +26,9 @@ class ShellCompletionCommand extends FlutterCommand {
@override
final String description = 'Output command line shell completion setup scripts.\n\n'
'This command prints the flutter command line completion setup script for Bash and Zsh. To\n'
'use it, specify an output file and follow the instructions in the generated output file to\n'
'install it in your shell environment. Once it is sourced, your shell will be able to\n'
'This command prints the flutter command line completion setup script for Bash and Zsh. To '
'use it, specify an output file and follow the instructions in the generated output file to '
'install it in your shell environment. Once it is sourced, your shell will be able to '
'complete flutter commands and options.';
@override

View file

@ -35,7 +35,7 @@ class TestCommand extends FlutterCommand {
negatable: false,
help: 'Start in a paused mode and wait for a debugger to connect.\n'
'You must specify a single test file to run, explicitly.\n'
'Instructions for connecting with a debugger and printed to the\n'
'Instructions for connecting with a debugger and printed to the '
'console once the test has started.',
)
..addFlag('coverage',
@ -72,7 +72,7 @@ class TestCommand extends FlutterCommand {
)
..addFlag('update-goldens',
negatable: false,
help: 'Whether matchesGoldenFile() calls within your test methods should\n'
help: 'Whether matchesGoldenFile() calls within your test methods should '
'update the golden files rather than test for an existing match.',
)
..addOption('concurrency',
@ -94,8 +94,8 @@ class TestCommand extends FlutterCommand {
if (!fs.isFileSync('pubspec.yaml')) {
throwToolExit(
'Error: No pubspec.yaml file found in the current working directory.\n'
'Run this command from the root of your project. Test files must be\n'
'called *_test.dart and must reside in the package\'s \'test\'\n'
'Run this command from the root of your project. Test files must be '
'called *_test.dart and must reside in the package\'s \'test\' '
'directory (or one of its subdirectories).');
}
}

View file

@ -23,7 +23,7 @@ class TraceCommand extends FlutterCommand {
argParser.addFlag('stop', negatable: false, help: 'Stop tracing. Implied if --start is also omitted.');
argParser.addOption('duration',
abbr: 'd',
help: 'Time to wait after starting (if --start is specified or implied) and before\n'
help: 'Time to wait after starting (if --start is specified or implied) and before '
'stopping (if --stop is specified or implied).\n'
'Defaults to ten seconds if --stop is specified or implied, zero otherwise.',
);
@ -38,8 +38,8 @@ class TraceCommand extends FlutterCommand {
@override
final String usageFooter =
'\`trace\` called without the --start or --stop flags will automatically start tracing,\n'
'delay a set amount of time (controlled by --duration), and stop tracing. To explicitly\n'
'\`trace\` called without the --start or --stop flags will automatically start tracing, '
'delay a set amount of time (controlled by --duration), and stop tracing. To explicitly '
'control tracing, call trace with --start and later with --stop.\n'
'The --debug-port argument is required.';

View file

@ -4,12 +4,14 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math' as math;
import '../base/file_system.dart' hide IOSink;
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/platform.dart';
import '../base/process_manager.dart';
import '../base/terminal.dart';
import '../base/utils.dart';
import '../globals.dart';
@ -145,13 +147,20 @@ class AnalysisServer {
}
}
enum _AnalysisSeverity {
error,
warning,
info,
none,
}
class AnalysisError implements Comparable<AnalysisError> {
AnalysisError(this.json);
static final Map<String, int> _severityMap = <String, int>{
'ERROR': 3,
'WARNING': 2,
'INFO': 1
static final Map<String, _AnalysisSeverity> _severityMap = <String, _AnalysisSeverity>{
'INFO': _AnalysisSeverity.info,
'WARNING': _AnalysisSeverity.warning,
'ERROR': _AnalysisSeverity.error,
};
static final String _separator = platform.isWindows ? '-' : '';
@ -162,7 +171,19 @@ class AnalysisError implements Comparable<AnalysisError> {
Map<String, dynamic> json;
String get severity => json['severity'];
int get severityLevel => _severityMap[severity] ?? 0;
String get colorSeverity {
switch(_severityLevel) {
case _AnalysisSeverity.error:
return terminal.color(severity, TerminalColor.red);
case _AnalysisSeverity.warning:
return terminal.color(severity, TerminalColor.yellow);
case _AnalysisSeverity.info:
case _AnalysisSeverity.none:
return severity;
}
return null;
}
_AnalysisSeverity get _severityLevel => _severityMap[severity] ?? _AnalysisSeverity.none;
String get type => json['type'];
String get message => json['message'];
String get code => json['code'];
@ -189,7 +210,7 @@ class AnalysisError implements Comparable<AnalysisError> {
if (offset != other.offset)
return offset - other.offset;
final int diff = other.severityLevel - severityLevel;
final int diff = other._severityLevel.index - _severityLevel.index;
if (diff != 0)
return diff;
@ -198,7 +219,10 @@ class AnalysisError implements Comparable<AnalysisError> {
@override
String toString() {
return '${severity.toLowerCase().padLeft(7)} $_separator '
// Can't use "padLeft" because of ANSI color sequences in the colorized
// severity.
final String padding = ' ' * math.max(0, 7 - severity.length);
return '$padding${colorSeverity.toLowerCase()} $_separator '
'$messageSentenceFragment $_separator '
'${fs.path.relative(file)}:$startLine:$startColumn $_separator '
'$code';

View file

@ -14,6 +14,8 @@ import 'base/logger.dart';
import 'base/os.dart';
import 'base/platform.dart';
import 'base/process_manager.dart';
import 'base/terminal.dart';
import 'base/utils.dart';
import 'base/version.dart';
import 'cache.dart';
import 'device.dart';
@ -122,26 +124,28 @@ class Doctor {
bool allGood = true;
for (DoctorValidator validator in validators) {
final StringBuffer lineBuffer = StringBuffer();
final ValidationResult result = await validator.validate();
buffer.write('${result.leadingBox} ${validator.title} is ');
lineBuffer.write('${result.coloredLeadingBox} ${validator.title} is ');
switch (result.type) {
case ValidationType.missing:
buffer.write('not installed.');
lineBuffer.write('not installed.');
break;
case ValidationType.partial:
buffer.write('partially installed; more components are available.');
lineBuffer.write('partially installed; more components are available.');
break;
case ValidationType.notAvailable:
buffer.write('not available.');
lineBuffer.write('not available.');
break;
case ValidationType.installed:
buffer.write('fully installed.');
lineBuffer.write('fully installed.');
break;
}
if (result.statusInfo != null)
buffer.write(' (${result.statusInfo})');
lineBuffer.write(' (${result.statusInfo})');
buffer.write(wrapText(lineBuffer.toString(), hangingIndent: result.leadingBox.length + 1));
buffer.writeln();
if (result.type != ValidationType.installed)
@ -192,20 +196,23 @@ class Doctor {
break;
}
if (result.statusInfo != null)
printStatus('${result.leadingBox} ${validator.title} (${result.statusInfo})');
else
printStatus('${result.leadingBox} ${validator.title}');
if (result.statusInfo != null) {
printStatus('${result.coloredLeadingBox} ${validator.title} (${result.statusInfo})',
hangingIndent: result.leadingBox.length + 1);
} else {
printStatus('${result.coloredLeadingBox} ${validator.title}',
hangingIndent: result.leadingBox.length + 1);
}
for (ValidationMessage message in result.messages) {
if (message.isError || message.isHint || verbose == true) {
final String text = message.message.replaceAll('\n', '\n ');
if (message.isError) {
printStatus('$text', emphasis: true);
} else if (message.isHint) {
printStatus(' ! $text');
} else {
printStatus('$text');
if (message.type != ValidationMessageType.information || verbose == true) {
int hangingIndent = 2;
int indent = 4;
for (String line in '${message.coloredIndicator} ${message.message}'.split('\n')) {
printStatus(line, hangingIndent: hangingIndent, indent: indent, emphasis: true);
// Only do hanging indent for the first line.
hangingIndent = 0;
indent = 6;
}
}
}
@ -216,10 +223,11 @@ class Doctor {
// Make sure there's always one line before the summary even when not verbose.
if (!verbose)
printStatus('');
if (issues > 0) {
printStatus('! Doctor found issues in $issues categor${issues > 1 ? "ies" : "y"}.');
printStatus('${terminal.color('!', TerminalColor.yellow)} Doctor found issues in $issues categor${issues > 1 ? "ies" : "y"}.', hangingIndent: 2);
} else {
printStatus(' No issues found!');
printStatus('${terminal.color('', TerminalColor.green)} No issues found!', hangingIndent: 2);
}
return doctorResult;
@ -256,6 +264,12 @@ enum ValidationType {
installed,
}
enum ValidationMessageType {
error,
hint,
information,
}
abstract class DoctorValidator {
const DoctorValidator(this.title);
@ -344,17 +358,56 @@ class ValidationResult {
}
return null;
}
String get coloredLeadingBox {
assert(type != null);
switch (type) {
case ValidationType.missing:
return terminal.color(leadingBox, TerminalColor.red);
case ValidationType.installed:
return terminal.color(leadingBox, TerminalColor.green);
case ValidationType.notAvailable:
case ValidationType.partial:
return terminal.color(leadingBox, TerminalColor.yellow);
}
return null;
}
}
class ValidationMessage {
ValidationMessage(this.message) : isError = false, isHint = false;
ValidationMessage.error(this.message) : isError = true, isHint = false;
ValidationMessage.hint(this.message) : isError = false, isHint = true;
ValidationMessage(this.message) : type = ValidationMessageType.information;
ValidationMessage.error(this.message) : type = ValidationMessageType.error;
ValidationMessage.hint(this.message) : type = ValidationMessageType.hint;
final bool isError;
final bool isHint;
final ValidationMessageType type;
bool get isError => type == ValidationMessageType.error;
bool get isHint => type == ValidationMessageType.hint;
final String message;
String get indicator {
switch (type) {
case ValidationMessageType.error:
return '';
case ValidationMessageType.hint:
return '!';
case ValidationMessageType.information:
return '';
}
return null;
}
String get coloredIndicator {
switch (type) {
case ValidationMessageType.error:
return terminal.color(indicator, TerminalColor.red);
case ValidationMessageType.hint:
return terminal.color(indicator, TerminalColor.yellow);
case ValidationMessageType.information:
return terminal.color(indicator, TerminalColor.green);
}
return null;
}
@override
String toString() => message;
}

View file

@ -25,12 +25,16 @@ void printError(
StackTrace stackTrace,
bool emphasis,
TerminalColor color,
int indent,
int hangingIndent,
}) {
logger.printError(
message,
stackTrace: stackTrace,
emphasis: emphasis ?? false,
color: color,
indent: indent,
hangingIndent: hangingIndent,
);
}
@ -49,6 +53,7 @@ void printStatus(
bool newline,
TerminalColor color,
int indent,
int hangingIndent,
}) {
logger.printStatus(
message,
@ -56,6 +61,7 @@ void printStatus(
color: color,
newline: newline ?? true,
indent: indent,
hangingIndent: hangingIndent,
);
}

View file

@ -100,7 +100,7 @@ abstract class FlutterCommand extends Command<void> {
abbr: 't',
defaultsTo: bundle.defaultMainPath,
help: 'The main entry-point file of the application, as run on the device.\n'
'If the --target option is omitted, but a file name is provided on\n'
'If the --target option is omitted, but a file name is provided on '
'the command line, then that is used instead.',
valueHelp: 'path');
_usesTargetOption = true;

View file

@ -17,11 +17,13 @@ import '../base/common.dart';
import '../base/context.dart';
import '../base/file_system.dart';
import '../base/flags.dart';
import '../base/io.dart' as io;
import '../base/logger.dart';
import '../base/os.dart';
import '../base/platform.dart';
import '../base/process.dart';
import '../base/process_manager.dart';
import '../base/terminal.dart';
import '../base/utils.dart';
import '../cache.dart';
import '../dart/package_map.dart';
@ -60,6 +62,16 @@ class FlutterCommandRunner extends CommandRunner<void> {
negatable: false,
hide: !verboseHelp,
help: 'Reduce the amount of output from some commands.');
argParser.addFlag('wrap',
negatable: true,
hide: !verboseHelp,
help: 'Toggles output word wrapping, regardless of whether or not the output is a terminal.',
defaultsTo: true);
argParser.addOption('wrap-column',
hide: !verboseHelp,
help: 'Sets the output wrap column. If not set, uses the width of the terminal, or 100 if '
'the output is not a terminal. Use --no-wrap to turn off wrapping entirely.',
defaultsTo: null);
argParser.addOption('device-id',
abbr: 'd',
help: 'Target device id or name (prefixes allowed).');
@ -73,7 +85,8 @@ class FlutterCommandRunner extends CommandRunner<void> {
argParser.addFlag('color',
negatable: true,
hide: !verboseHelp,
help: 'Whether to use terminal colors (requires support for ANSI escape sequences).');
help: 'Whether to use terminal colors (requires support for ANSI escape sequences).',
defaultsTo: true);
argParser.addFlag('version-check',
negatable: true,
defaultsTo: true,
@ -103,8 +116,8 @@ class FlutterCommandRunner extends CommandRunner<void> {
argParser.addOption('flutter-root',
hide: !verboseHelp,
help: 'The root directory of the Flutter repository.\n'
'Defaults to \$$kFlutterRootEnvironmentVariableName if set, otherwise uses the parent of the\n'
'directory that the "flutter" script itself is in.');
'Defaults to \$$kFlutterRootEnvironmentVariableName if set, otherwise uses the parent '
'of the directory that the "flutter" script itself is in.');
if (verboseHelp)
argParser.addSeparator('Local build selection options (not normally required):');
@ -112,9 +125,10 @@ class FlutterCommandRunner extends CommandRunner<void> {
argParser.addOption('local-engine-src-path',
hide: !verboseHelp,
help: 'Path to your engine src directory, if you are building Flutter locally.\n'
'Defaults to \$$kFlutterEngineEnvironmentVariableName if set, otherwise defaults to the path given in your pubspec.yaml\n'
'dependency_overrides for $kFlutterEnginePackageName, if any, or, failing that, tries to guess at the location\n'
'based on the value of the --flutter-root option.');
'Defaults to \$$kFlutterEngineEnvironmentVariableName if set, otherwise defaults to '
'the path given in your pubspec.yaml dependency_overrides for $kFlutterEnginePackageName, '
'if any, or, failing that, tries to guess at the location based on the value of the '
'--flutter-root option.');
argParser.addOption('local-engine',
hide: !verboseHelp,
@ -127,15 +141,15 @@ class FlutterCommandRunner extends CommandRunner<void> {
argParser.addOption('record-to',
hide: !verboseHelp,
help: 'Enables recording of process invocations (including stdout and stderr of all such invocations),\n'
help: 'Enables recording of process invocations (including stdout and stderr of all such invocations), '
'and file system access (reads and writes).\n'
'Serializes that recording to a directory with the path specified in this flag. If the\n'
'Serializes that recording to a directory with the path specified in this flag. If the '
'directory does not already exist, it will be created.');
argParser.addOption('replay-from',
hide: !verboseHelp,
help: 'Enables mocking of process invocations by replaying their stdout, stderr, and exit code from\n'
'the specified recording (obtained via --record-to). The path specified in this flag must refer\n'
'to a directory that holds serialized process invocations structured according to the output of\n'
help: 'Enables mocking of process invocations by replaying their stdout, stderr, and exit code from '
'the specified recording (obtained via --record-to). The path specified in this flag must refer '
'to a directory that holds serialized process invocations structured according to the output of '
'--record-to.');
argParser.addFlag('show-test-device',
negatable: false,
@ -146,11 +160,20 @@ class FlutterCommandRunner extends CommandRunner<void> {
@override
ArgParser get argParser => _argParser;
final ArgParser _argParser = ArgParser(allowTrailingOptions: false);
final ArgParser _argParser = ArgParser(
allowTrailingOptions: false,
usageLineLength: outputPreferences.wrapText ? outputPreferences.wrapColumn : null,
);
@override
String get usageFooter {
return 'Run "flutter help -v" for verbose help output, including less commonly used options.';
return wrapText('Run "flutter help -v" for verbose help output, including less commonly used options.');
}
@override
String get usage {
final String usageWithoutDescription = super.usage.substring(description.length + 2);
return '${wrapText(description)}\n\n$usageWithoutDescription';
}
static String get _defaultFlutterRoot {
@ -223,6 +246,26 @@ class FlutterCommandRunner extends CommandRunner<void> {
contextOverrides[Logger] = VerboseLogger(logger);
}
int wrapColumn = const io.Stdio().terminalColumns ?? kDefaultTerminalColumns;
if (topLevelResults['wrap-column'] != null) {
try {
wrapColumn = int.parse(topLevelResults['wrap-column']);
if (wrapColumn < 0) {
throwToolExit('Argument to --wrap-column must be a positive integer. '
'You supplied ${topLevelResults['wrap-column']}.');
}
} on FormatException {
throwToolExit('Unable to parse argument '
'--wrap-column=${topLevelResults['wrap-column']}. Must be a positive integer.');
}
}
contextOverrides[OutputPreferences] = OutputPreferences(
wrapText: topLevelResults['wrap'],
showColor: topLevelResults['color'],
wrapColumn: wrapColumn,
);
if (topLevelResults['show-test-device'] ||
topLevelResults['device-id'] == FlutterTesterDevices.kTesterDeviceId) {
FlutterTesterDevices.showFlutterTesterDevice = true;
@ -307,9 +350,6 @@ class FlutterCommandRunner extends CommandRunner<void> {
body: () async {
logger.quiet = topLevelResults['quiet'];
if (topLevelResults.wasParsed('color'))
logger.supportsColor = topLevelResults['color'];
if (platform.environment['FLUTTER_ALREADY_LOCKED'] != 'true')
await Cache.lock();
@ -377,8 +417,8 @@ class FlutterCommandRunner extends CommandRunner<void> {
if (engineSourcePath == null) {
throwToolExit('Unable to detect local Flutter engine build directory.\n'
'Either specify a dependency_override for the $kFlutterEnginePackageName package in your pubspec.yaml and\n'
'ensure --package-root is set if necessary, or set the \$$kFlutterEngineEnvironmentVariableName environment variable, or\n'
'Either specify a dependency_override for the $kFlutterEnginePackageName package in your pubspec.yaml and '
'ensure --package-root is set if necessary, or set the \$$kFlutterEngineEnvironmentVariableName environment variable, or '
'use --local-engine-src-path to specify the path to the root of your flutter/engine repository.',
exitCode: 2);
}
@ -386,7 +426,7 @@ class FlutterCommandRunner extends CommandRunner<void> {
if (engineSourcePath != null && _tryEnginePath(engineSourcePath) == null) {
throwToolExit('Unable to detect a Flutter engine build directory in $engineSourcePath.\n'
'Please ensure that $engineSourcePath is a Flutter engine \'src\' directory and that\n'
'Please ensure that $engineSourcePath is a Flutter engine \'src\' directory and that '
'you have compiled the engine in that directory, which should produce an \'out\' directory',
exitCode: 2);
}
@ -469,7 +509,7 @@ class FlutterCommandRunner extends CommandRunner<void> {
'Warning: the \'flutter\' tool you are currently running is not the one from the current directory:\n'
' running Flutter : ${Cache.flutterRoot}\n'
' current directory: $directory\n'
'This can happen when you have multiple copies of flutter installed. Please check your system path to verify\n'
'This can happen when you have multiple copies of flutter installed. Please check your system path to verify '
'that you\'re running the expected version (run \'flutter --version\' to see which flutter is on your path).\n'
);
}
@ -495,25 +535,25 @@ class FlutterCommandRunner extends CommandRunner<void> {
if (!fs.isDirectorySync(flutterPath)) {
printError(
'Warning! This package referenced a Flutter repository via the .packages file that is\n'
'no longer available. The repository from which the \'flutter\' tool is currently\n'
'Warning! This package referenced a Flutter repository via the .packages file that is '
'no longer available. The repository from which the \'flutter\' tool is currently '
'executing will be used instead.\n'
' running Flutter tool: ${Cache.flutterRoot}\n'
' previous reference : $flutterPath\n'
'This can happen if you deleted or moved your copy of the Flutter repository, or\n'
'if it was on a volume that is no longer mounted or has been mounted at a\n'
'different location. Please check your system path to verify that you are running\n'
'This can happen if you deleted or moved your copy of the Flutter repository, or '
'if it was on a volume that is no longer mounted or has been mounted at a '
'different location. Please check your system path to verify that you are running '
'the expected version (run \'flutter --version\' to see which flutter is on your path).\n'
);
} else if (!_compareResolvedPaths(flutterPath, Cache.flutterRoot)) {
printError(
'Warning! The \'flutter\' tool you are currently running is from a different Flutter\n'
'repository than the one last used by this package. The repository from which the\n'
'Warning! The \'flutter\' tool you are currently running is from a different Flutter '
'repository than the one last used by this package. The repository from which the '
'\'flutter\' tool is currently executing will be used instead.\n'
' running Flutter tool: ${Cache.flutterRoot}\n'
' previous reference : $flutterPath\n'
'This can happen when you have multiple copies of flutter installed. Please check\n'
'your system path to verify that you are running the expected version (run\n'
'This can happen when you have multiple copies of flutter installed. Please check '
'your system path to verify that you are running the expected version (run '
'\'flutter --version\' to see which flutter is on your path).\n'
);
}

View file

@ -1,3 +1,7 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/android_studio.dart';
import 'package:flutter_tools/src/base/file_system.dart';

View file

@ -0,0 +1,30 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_tools/src/doctor.dart';
import 'package:flutter_tools/src/android/android_studio_validator.dart';
import 'package:flutter_tools/src/base/platform.dart';
import '../src/common.dart';
import '../src/context.dart';
const String home = '/home/me';
Platform linuxPlatform() {
return FakePlatform.fromPlatform(const LocalPlatform())
..operatingSystem = 'linux'
..environment = <String, String>{'HOME': home};
}
void main() {
group('NoAndroidStudioValidator', () {
testUsingContext('shows Android Studio as "not available" when not available.', () async {
final NoAndroidStudioValidator validator = NoAndroidStudioValidator();
expect((await validator.validate()).type, equals(ValidationType.notAvailable));
}, overrides: <Type, Generator>{
// Note that custom home paths are not supported on macOS nor Windows yet:
Platform: () => linuxPlatform(),
});
});
}

View file

@ -51,7 +51,7 @@ void main() {
expect(mockLogger.traceText, '');
expect(
mockLogger.errorText,
matches('^$red' r'\[ (?: {0,2}\+[0-9]{1,3} ms| )\] ' '${bold}Helpless!$reset$reset' r'\n$'));
matches('^$red' r'\[ (?: {0,2}\+[0-9]{1,3} ms| )\] ' '${bold}Helpless!$reset' r'\n$'));
});
});
@ -59,7 +59,6 @@ void main() {
MockStdio mockStdio;
AnsiSpinner ansiSpinner;
AnsiStatus ansiStatus;
SummaryStatus summaryStatus;
int called;
const List<String> testPlatforms = <String>['linux', 'macos', 'windows', 'fuchsia'];
final RegExp secondDigits = RegExp(r'[^\b]\b\b\b\b\b[0-9]+[.][0-9]+(?:s|ms)');
@ -74,12 +73,6 @@ void main() {
padding: 20,
onFinish: () => called++,
);
summaryStatus = SummaryStatus(
message: 'Hello world',
expectSlowOperation: true,
padding: 20,
onFinish: () => called++,
);
});
List<String> outputStdout() => mockStdio.writtenToStdout.join('').split('\n');
@ -121,7 +114,9 @@ void main() {
});
testUsingContext('Stdout startProgress handle null inputs on colored terminal for $testOs', () async {
context[Logger].startProgress(null, progressId: null,
context[Logger].startProgress(
null,
progressId: null,
expectSlowOperation: null,
progressIndicatorPadding: null,
);
@ -192,6 +187,164 @@ void main() {
Platform: () => FakePlatform(operatingSystem: testOs),
});
}
});
group('Output format', () {
MockStdio mockStdio;
SummaryStatus summaryStatus;
int called;
final RegExp secondDigits = RegExp(r'[^\b]\b\b\b\b\b[0-9]+[.][0-9]+(?:s|ms)');
setUp(() {
mockStdio = MockStdio();
called = 0;
summaryStatus = SummaryStatus(
message: 'Hello world',
expectSlowOperation: true,
padding: 20,
onFinish: () => called++,
);
});
List<String> outputStdout() => mockStdio.writtenToStdout.join('').split('\n');
List<String> outputStderr() => mockStdio.writtenToStderr.join('').split('\n');
testUsingContext('Error logs are wrapped', () async {
context[Logger].printError('0123456789' * 15);
final List<String> lines = outputStderr();
expect(outputStdout().length, equals(1));
expect(outputStdout().first, isEmpty);
expect(lines[0], equals('0123456789' * 4));
expect(lines[1], equals('0123456789' * 4));
expect(lines[2], equals('0123456789' * 4));
expect(lines[3], equals('0123456789' * 3));
}, overrides: <Type, Generator>{
Stdio: () => mockStdio,
OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 40),
Logger: () => StdoutLogger()..supportsColor = false,
});
testUsingContext('Error logs are wrapped and can be indented.', () async {
context[Logger].printError('0123456789' * 15, indent: 5);
final List<String> lines = outputStderr();
expect(outputStdout().length, equals(1));
expect(outputStdout().first, isEmpty);
expect(lines.length, equals(6));
expect(lines[0], equals(' 01234567890123456789012345678901234'));
expect(lines[1], equals(' 56789012345678901234567890123456789'));
expect(lines[2], equals(' 01234567890123456789012345678901234'));
expect(lines[3], equals(' 56789012345678901234567890123456789'));
expect(lines[4], equals(' 0123456789'));
expect(lines[5], isEmpty);
}, overrides: <Type, Generator>{
Stdio: () => mockStdio,
OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 40),
Logger: () => StdoutLogger()..supportsColor = false,
});
testUsingContext('Error logs are wrapped and can have hanging indent.', () async {
context[Logger].printError('0123456789' * 15, hangingIndent: 5);
final List<String> lines = outputStderr();
expect(outputStdout().length, equals(1));
expect(outputStdout().first, isEmpty);
expect(lines.length, equals(6));
expect(lines[0], equals('0123456789012345678901234567890123456789'));
expect(lines[1], equals(' 01234567890123456789012345678901234'));
expect(lines[2], equals(' 56789012345678901234567890123456789'));
expect(lines[3], equals(' 01234567890123456789012345678901234'));
expect(lines[4], equals(' 56789'));
expect(lines[5], isEmpty);
}, overrides: <Type, Generator>{
Stdio: () => mockStdio,
OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 40),
Logger: () => StdoutLogger()..supportsColor = false,
});
testUsingContext('Error logs are wrapped, indented, and can have hanging indent.', () async {
context[Logger].printError('0123456789' * 15, indent: 4, hangingIndent: 5);
final List<String> lines = outputStderr();
expect(outputStdout().length, equals(1));
expect(outputStdout().first, isEmpty);
expect(lines.length, equals(6));
expect(lines[0], equals(' 012345678901234567890123456789012345'));
expect(lines[1], equals(' 6789012345678901234567890123456'));
expect(lines[2], equals(' 7890123456789012345678901234567'));
expect(lines[3], equals(' 8901234567890123456789012345678'));
expect(lines[4], equals(' 901234567890123456789'));
expect(lines[5], isEmpty);
}, overrides: <Type, Generator>{
Stdio: () => mockStdio,
OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 40),
Logger: () => StdoutLogger()..supportsColor = false,
});
testUsingContext('Stdout logs are wrapped', () async {
context[Logger].printStatus('0123456789' * 15);
final List<String> lines = outputStdout();
expect(outputStderr().length, equals(1));
expect(outputStderr().first, isEmpty);
expect(lines[0], equals('0123456789' * 4));
expect(lines[1], equals('0123456789' * 4));
expect(lines[2], equals('0123456789' * 4));
expect(lines[3], equals('0123456789' * 3));
}, overrides: <Type, Generator>{
Stdio: () => mockStdio,
OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 40),
Logger: () => StdoutLogger()..supportsColor = false,
});
testUsingContext('Stdout logs are wrapped and can be indented.', () async {
context[Logger].printStatus('0123456789' * 15, indent: 5);
final List<String> lines = outputStdout();
expect(outputStderr().length, equals(1));
expect(outputStderr().first, isEmpty);
expect(lines.length, equals(6));
expect(lines[0], equals(' 01234567890123456789012345678901234'));
expect(lines[1], equals(' 56789012345678901234567890123456789'));
expect(lines[2], equals(' 01234567890123456789012345678901234'));
expect(lines[3], equals(' 56789012345678901234567890123456789'));
expect(lines[4], equals(' 0123456789'));
expect(lines[5], isEmpty);
}, overrides: <Type, Generator>{
Stdio: () => mockStdio,
OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 40),
Logger: () => StdoutLogger()..supportsColor = false,
});
testUsingContext('Stdout logs are wrapped and can have hanging indent.', () async {
context[Logger].printStatus('0123456789' * 15, hangingIndent: 5);
final List<String> lines = outputStdout();
expect(outputStderr().length, equals(1));
expect(outputStderr().first, isEmpty);
expect(lines.length, equals(6));
expect(lines[0], equals('0123456789012345678901234567890123456789'));
expect(lines[1], equals(' 01234567890123456789012345678901234'));
expect(lines[2], equals(' 56789012345678901234567890123456789'));
expect(lines[3], equals(' 01234567890123456789012345678901234'));
expect(lines[4], equals(' 56789'));
expect(lines[5], isEmpty);
}, overrides: <Type, Generator>{
Stdio: () => mockStdio,
OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 40),
Logger: () => StdoutLogger()..supportsColor = false,
});
testUsingContext('Stdout logs are wrapped, indented, and can have hanging indent.', () async {
context[Logger].printStatus('0123456789' * 15, indent: 4, hangingIndent: 5);
final List<String> lines = outputStdout();
expect(outputStderr().length, equals(1));
expect(outputStderr().first, isEmpty);
expect(lines.length, equals(6));
expect(lines[0], equals(' 012345678901234567890123456789012345'));
expect(lines[1], equals(' 6789012345678901234567890123456'));
expect(lines[2], equals(' 7890123456789012345678901234567'));
expect(lines[3], equals(' 8901234567890123456789012345678'));
expect(lines[4], equals(' 901234567890123456789'));
expect(lines[5], isEmpty);
}, overrides: <Type, Generator>{
Stdio: () => mockStdio,
OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 40),
Logger: () => StdoutLogger()..supportsColor = false,
});
testUsingContext('Error logs are red', () async {
context[Logger].printError('Pants on fire!');
@ -216,10 +369,7 @@ void main() {
});
testUsingContext('Stdout printStatus handle null inputs on colored terminal', () async {
context[Logger].printStatus(null, emphasis: null,
color: null,
newline: null,
indent: null);
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);
@ -230,10 +380,7 @@ void main() {
});
testUsingContext('Stdout printStatus handle null inputs on regular terminal', () async {
context[Logger].printStatus(null, emphasis: null,
color: null,
newline: null,
indent: null);
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);
@ -244,7 +391,9 @@ void main() {
});
testUsingContext('Stdout startProgress handle null inputs on regular terminal', () async {
context[Logger].startProgress(null, progressId: null,
context[Logger].startProgress(
null,
progressId: null,
expectSlowOperation: null,
progressIndicatorPadding: null,
);

View file

@ -4,12 +4,31 @@
import 'dart:async';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/globals.dart';
import '../src/common.dart';
import '../src/context.dart';
void main() {
group('output preferences', () {
testUsingContext('can wrap output', () async {
printStatus('0123456789' * 8);
expect(testLogger.statusText, equals(('0123456789' * 4 + '\n') * 2));
}, overrides: <Type, Generator>{
OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 40),
});
testUsingContext('can turn off wrapping', () async {
final String testString = '0123456789' * 20;
printStatus(testString);
expect(testLogger.statusText, equals('$testString\n'));
}, overrides: <Type, Generator>{
Platform: () => FakePlatform()..stdoutSupportsAnsi = true,
OutputPreferences: () => OutputPreferences(wrapText: false),
});
});
group('character input prompt', () {
AnsiTerminal terminalUnderTest;
@ -23,38 +42,34 @@ void main() {
Future<String>.value('\n'), // Not in accepted list
Future<String>.value('b'),
]).asBroadcastStream();
final String choice =
await terminalUnderTest.promptForCharInput(
<String>['a', 'b', 'c'],
prompt: 'Please choose something',
);
final String choice = await terminalUnderTest.promptForCharInput(
<String>['a', 'b', 'c'],
prompt: 'Please choose something',
);
expect(choice, 'b');
expect(
testLogger.statusText,
'Please choose something [a|b|c]: d\n'
'Please choose something [a|b|c]: \n'
'\n'
'Please choose something [a|b|c]: b\n'
);
testLogger.statusText,
'Please choose something [a|b|c]: d\n'
'Please choose something [a|b|c]: \n'
'\n'
'Please choose something [a|b|c]: b\n');
});
testUsingContext('default character choice without displayAcceptedCharacters', () async {
mockStdInStream = Stream<String>.fromFutures(<Future<String>>[
Future<String>.value('\n'), // Not in accepted list
]).asBroadcastStream();
final String choice =
await terminalUnderTest.promptForCharInput(
<String>['a', 'b', 'c'],
prompt: 'Please choose something',
displayAcceptedCharacters: false,
defaultChoiceIndex: 1, // which is b.
);
final String choice = await terminalUnderTest.promptForCharInput(
<String>['a', 'b', 'c'],
prompt: 'Please choose something',
displayAcceptedCharacters: false,
defaultChoiceIndex: 1, // which is b.
);
expect(choice, 'b');
expect(
testLogger.statusText,
'Please choose something: \n'
'\n'
);
testLogger.statusText,
'Please choose something: \n'
'\n');
});
});
}

View file

@ -41,7 +41,7 @@ void main() {
testUsingContext('flutter create', () async {
await runCommand(
command: CreateCommand(),
arguments: <String>['create', projectPath],
arguments: <String>['--no-wrap', 'create', projectPath],
statusTextContains: <String>[
'All done!',
'Your application code is in ${fs.path.normalize(fs.path.join(fs.path.relative(projectPath), 'lib', 'main.dart'))}',

View file

@ -375,7 +375,7 @@ void main() {
]);
}, timeout: allowForRemotePubInvocation);
testUsingContext('has correct content and formatting with applicaiton template', () async {
testUsingContext('has correct content and formatting with application template', () async {
Cache.flutterRoot = '../..';
when(mockFlutterVersion.frameworkRevision).thenReturn(frameworkRevision);
when(mockFlutterVersion.channel).thenReturn(frameworkChannel);

View file

@ -5,6 +5,7 @@
import 'dart:async';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/doctor.dart';
import 'package:flutter_tools/src/vscode/vscode.dart';
import 'package:flutter_tools/src/vscode/vscode_validator.dart';
@ -132,7 +133,7 @@ void main() {
'[!] Partial Validator with only a Hint\n'
' ! There is a hint here\n'
'[!] Partial Validator with Errors\n'
' ✗ A error message indicating partial installation\n'
' ✗ An error message indicating partial installation\n'
' ! Maybe a hint will help the user\n'
'[✓] Another Passing Validator (with statusInfo)\n'
'\n'
@ -154,7 +155,7 @@ void main() {
'[!] Partial Validator with only a Hint\n'
' ! There is a hint here\n'
'[!] Partial Validator with Errors\n'
' ✗ A error message indicating partial installation\n'
' ✗ An error message indicating partial installation\n'
' ! Maybe a hint will help the user\n'
'\n'
'! Doctor found issues in 4 categories.\n'
@ -183,7 +184,7 @@ void main() {
' • But there is no error\n'
'\n'
'[!] Partial Validator with Errors\n'
' ✗ A error message indicating partial installation\n'
' ✗ An error message indicating partial installation\n'
' ! Maybe a hint will help the user\n'
' • An extra message with some verbose details\n'
'\n'
@ -192,6 +193,84 @@ void main() {
});
});
testUsingContext('validate non-verbose output wrapping', () async {
expect(await FakeDoctor().diagnose(verbose: false), isFalse);
expect(testLogger.statusText, equals(
'Doctor summary (to see all\n'
'details, run flutter doctor\n'
'-v):\n'
'[✓] Passing Validator (with\n'
' statusInfo)\n'
'[✗] Missing Validator\n'
' ✗ A useful error message\n'
' ! A hint message\n'
'[!] Not Available Validator\n'
' ✗ A useful error message\n'
' ! A hint message\n'
'[!] Partial Validator with\n'
' only a Hint\n'
' ! There is a hint here\n'
'[!] Partial Validator with\n'
' Errors\n'
' ✗ An error message\n'
' indicating partial\n'
' installation\n'
' ! Maybe a hint will help\n'
' the user\n'
'\n'
'! Doctor found issues in 4\n'
' categories.\n'
''
));
}, overrides: <Type, Generator>{
OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 30),
});
testUsingContext('validate verbose output wrapping', () async {
expect(await FakeDoctor().diagnose(verbose: true), isFalse);
expect(testLogger.statusText, equals(
'[✓] Passing Validator (with\n'
' statusInfo)\n'
' • A helpful message\n'
' • A second, somewhat\n'
' longer helpful message\n'
'\n'
'[✗] Missing Validator\n'
' ✗ A useful error message\n'
' • A message that is not an\n'
' error\n'
' ! A hint message\n'
'\n'
'[!] Not Available Validator\n'
' ✗ A useful error message\n'
' • A message that is not an\n'
' error\n'
' ! A hint message\n'
'\n'
'[!] Partial Validator with\n'
' only a Hint\n'
' ! There is a hint here\n'
' • But there is no error\n'
'\n'
'[!] Partial Validator with\n'
' Errors\n'
' ✗ An error message\n'
' indicating partial\n'
' installation\n'
' ! Maybe a hint will help\n'
' the user\n'
' • An extra message with\n'
' some verbose details\n'
'\n'
'! Doctor found issues in 4\n'
' categories.\n'
''
));
}, overrides: <Type, Generator>{
OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 30),
});
group('doctor with grouped validators', () {
testUsingContext('validate diagnose combines validator output', () async {
expect(await FakeGroupedDoctor().diagnose(), isTrue);
@ -328,7 +407,7 @@ class PartialValidatorWithErrors extends DoctorValidator {
@override
Future<ValidationResult> validate() async {
final List<ValidationMessage> messages = <ValidationMessage>[];
messages.add(ValidationMessage.error('A error message indicating partial installation'));
messages.add(ValidationMessage.error('An error message indicating partial installation'));
messages.add(ValidationMessage.hint('Maybe a hint will help the user'));
messages.add(ValidationMessage('An extra message with some verbose details'));
return ValidationResult(ValidationType.partial, messages);

View file

@ -123,6 +123,7 @@ void main() {
final String appFile = fs.path.join(tempDir.dirname, 'other_app', 'app.dart');
fs.file(appFile).createSync(recursive: true);
final List<String> args = <String>[
'--no-wrap',
'drive',
'--target=$appFile',
];
@ -143,6 +144,7 @@ void main() {
final String appFile = fs.path.join(tempDir.path, 'main.dart');
fs.file(appFile).createSync(recursive: true);
final List<String> args = <String>[
'--no-wrap',
'drive',
'--target=$appFile',
];

View file

@ -53,6 +53,8 @@ void main() {
expect(testLogger.statusText, equals(
'Automatically signing iOS for device deployment using specified development team in Xcode project: abc\n'
));
}, overrides: <Type, Generator>{
OutputPreferences: () => OutputPreferences(wrapText: false),
});
testUsingContext('No auto-sign if security or openssl not available', () async {
@ -88,6 +90,7 @@ void main() {
},
overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
OutputPreferences: () => OutputPreferences(wrapText: false),
});
testUsingContext('Test single identity and certificate organization works', () async {
@ -147,6 +150,7 @@ void main() {
},
overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
OutputPreferences: () => OutputPreferences(wrapText: false),
});
testUsingContext('Test Google cert also manually selects a provisioning profile', () async {
@ -210,6 +214,7 @@ void main() {
},
overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
OutputPreferences: () => OutputPreferences(wrapText: false),
});
testUsingContext('Test multiple identity and certificate organization works', () async {
@ -284,6 +289,7 @@ void main() {
ProcessManager: () => mockProcessManager,
Config: () => mockConfig,
AnsiTerminal: () => testTerminal,
OutputPreferences: () => OutputPreferences(wrapText: false),
});
testUsingContext('Test multiple identity in machine mode works', () async {
@ -352,6 +358,7 @@ void main() {
ProcessManager: () => mockProcessManager,
Config: () => mockConfig,
AnsiTerminal: () => testTerminal,
OutputPreferences: () => OutputPreferences(wrapText: false),
});
testUsingContext('Test saved certificate used', () async {
@ -422,6 +429,7 @@ void main() {
overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
Config: () => mockConfig,
OutputPreferences: () => OutputPreferences(wrapText: false),
});
testUsingContext('Test invalid saved certificate shows error and prompts again', () async {

View file

@ -6,6 +6,7 @@ import 'dart:async';
import 'package:flutter_tools/src/base/utils.dart';
import 'package:flutter_tools/src/base/version.dart';
import 'package:flutter_tools/src/base/terminal.dart';
import 'src/common.dart';
@ -179,4 +180,143 @@ baz=qux
expect(snakeCase('ABC'), equals('a_b_c'));
});
});
group('text wrapping', () {
const int _lineLength = 40;
const String _longLine = 'This is a long line that needs to be wrapped.';
final String _longLineWithNewlines = 'This is a long line with newlines that\n'
'needs to be wrapped.\n\n' +
'0123456789' * 5;
final String _longAnsiLineWithNewlines = '${AnsiTerminal.red}This${AnsiTerminal.reset} is a long line with newlines that\n'
'needs to be wrapped.\n\n'
'${AnsiTerminal.green}0123456789${AnsiTerminal.reset}' +
'0123456789' * 3 +
'${AnsiTerminal.green}0123456789${AnsiTerminal.reset}';
const String _onlyAnsiSequences = '${AnsiTerminal.red}${AnsiTerminal.reset}';
final String _indentedLongLineWithNewlines = ' This is an indented long line with newlines that\n'
'needs to be wrapped.\n\tAnd preserves tabs.\n \n ' +
'0123456789' * 5;
const String _shortLine = 'Short line.';
const String _indentedLongLine = ' This is an indented long line that needs to be '
'wrapped and indentation preserved.';
test('does not wrap short lines.', () {
expect(wrapText(_shortLine, columnWidth: _lineLength), equals(_shortLine));
});
test('does not wrap at all if not given a length', () {
expect(wrapText(_longLine), equals(_longLine));
});
test('able to wrap long lines', () {
expect(wrapText(_longLine, columnWidth: _lineLength), equals('''
This is a long line that needs to be
wrapped.'''));
});
test('wrap long lines with no whitespace', () {
expect(wrapText('0123456789' * 5, columnWidth: _lineLength), equals('''
0123456789012345678901234567890123456789
0123456789'''));
});
test('refuses to wrap to a column smaller than 10 characters', () {
expect(wrapText('$_longLine ' + '0123456789' * 4, columnWidth: 1), equals('''
This is a
long line
that needs
to be
wrapped.
0123456789
0123456789
0123456789
0123456789'''));
});
test('preserves indentation', () {
expect(wrapText(_indentedLongLine, columnWidth: _lineLength), equals('''
This is an indented long line that
needs to be wrapped and indentation
preserved.'''));
});
test('preserves indentation and stripping trailing whitespace', () {
expect(wrapText('$_indentedLongLine ', columnWidth: _lineLength), equals('''
This is an indented long line that
needs to be wrapped and indentation
preserved.'''));
});
test('wraps text with newlines', () {
expect(wrapText(_longLineWithNewlines, columnWidth: _lineLength), equals('''
This is a long line with newlines that
needs to be wrapped.
0123456789012345678901234567890123456789
0123456789'''));
});
test('wraps text with ANSI sequences embedded', () {
expect(wrapText(_longAnsiLineWithNewlines, columnWidth: _lineLength), equals('''
${AnsiTerminal.red}This${AnsiTerminal.reset} is a long line with newlines that
needs to be wrapped.
${AnsiTerminal.green}0123456789${AnsiTerminal.reset}012345678901234567890123456789
${AnsiTerminal.green}0123456789${AnsiTerminal.reset}'''));
});
test('wraps text with only ANSI sequences', () {
expect(wrapText(_onlyAnsiSequences, columnWidth: _lineLength),
equals('${AnsiTerminal.red}${AnsiTerminal.reset}'));
});
test('preserves indentation in the presence of newlines', () {
expect(wrapText(_indentedLongLineWithNewlines, columnWidth: _lineLength), equals('''
This is an indented long line with
newlines that
needs to be wrapped.
\tAnd preserves tabs.
01234567890123456789012345678901234567
890123456789'''));
});
test('removes trailing whitespace when wrapping', () {
expect(wrapText('$_longLine \t', columnWidth: _lineLength), equals('''
This is a long line that needs to be
wrapped.'''));
});
test('honors hangingIndent parameter', () {
expect(wrapText(_longLine, columnWidth: _lineLength, hangingIndent: 6), equals('''
This is a long line that needs to be
wrapped.'''));
});
test('handles hangingIndent with a single unwrapped line.', () {
expect(wrapText(_shortLine, columnWidth: _lineLength, hangingIndent: 6), equals('''
Short line.'''));
});
test('handles hangingIndent with two unwrapped lines and the second is empty.', () {
expect(wrapText('$_shortLine\n', columnWidth: _lineLength, hangingIndent: 6), equals('''
Short line.
'''));
});
test('honors hangingIndent parameter on already indented line.', () {
expect(wrapText(_indentedLongLine, columnWidth: _lineLength, hangingIndent: 6), equals('''
This is an indented long line that
needs to be wrapped and
indentation preserved.'''));
});
test('honors hangingIndent and indent parameters at the same time.', () {
expect(wrapText(_indentedLongLine, columnWidth: _lineLength, indent: 6, hangingIndent: 6), equals('''
This is an indented long line
that needs to be wrapped
and indentation
preserved.'''));
});
test('honors indent parameter on already indented line.', () {
expect(wrapText(_indentedLongLine, columnWidth: _lineLength, indent: 6), equals('''
This is an indented long line
that needs to be wrapped and
indentation preserved.'''));
});
test('honors hangingIndent parameter on already indented line.', () {
expect(wrapText(_indentedLongLineWithNewlines, columnWidth: _lineLength, hangingIndent: 6), equals('''
This is an indented long line with
newlines that
needs to be wrapped.
And preserves tabs.
01234567890123456789012345678901234567
890123456789'''));
});
});
}