Unverified Commit 33b183e6 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Fix extra blank lines in logger output (#81607)

parent 96e4b47f
......@@ -323,7 +323,6 @@ class AndroidGradleBuilder implements AndroidBuilder {
final Status status = _logger.startProgress(
"Running Gradle task '$assembleTask'...",
multilineOutput: true,
);
final List<String> command = <String>[
......@@ -636,7 +635,6 @@ class AndroidGradleBuilder implements AndroidBuilder {
final String aarTask = getAarTaskFor(buildInfo);
final Status status = _logger.startProgress(
"Running Gradle task '$aarTask'...",
multilineOutput: true,
);
final String flutterRoot = _fileSystem.path.absolute(Cache.flutterRoot);
......
......@@ -21,7 +21,7 @@ class ToolExit implements Exception {
final int? exitCode;
@override
String toString() => 'Exception: $message';
String toString() => 'Exception: $message'; // TODO(ianh): Really this should say "Error".
}
/// Indicates to the linter that the given future is intentionally not awaited.
......
......@@ -120,16 +120,16 @@ abstract class Logger {
/// The `progressId` argument provides an ID that can be used to identify
/// this type of progress (e.g. `hot.reload`, `hot.restart`).
///
/// The `progressIndicatorPadding` can optionally be used to specify spacing
/// between the `message` and the progress indicator, if any.
/// The `progressIndicatorPadding` can optionally be used to specify the width
/// of the space into which the `message` is placed before the progress
/// indicator, if any. It is ignored if the message is longer.
Status startProgress(
String message, {
String? progressId,
bool multilineOutput = false,
int progressIndicatorPadding = kDefaultStatusPadding,
});
/// A [SilentStatus] or an [AnsiSpinner] (depending on whether the
/// A [SilentStatus] or an [AnonymousSpinnerStatus] (depending on whether the
/// terminal is fancy enough), already started.
Status startSpinner({ VoidCallback? onFinish });
......@@ -223,12 +223,10 @@ class DelegatingLogger implements Logger {
@override
Status startProgress(String message, {
String? progressId,
bool multilineOutput = false,
int progressIndicatorPadding = kDefaultStatusPadding,
}) {
return _delegate.startProgress(message,
progressId: progressId,
multilineOutput: multilineOutput,
progressIndicatorPadding: progressIndicatorPadding,
);
}
......@@ -363,20 +361,17 @@ class StdoutLogger extends Logger {
Status startProgress(
String message, {
String? progressId,
bool multilineOutput = false,
int progressIndicatorPadding = kDefaultStatusPadding,
}) {
if (_status != null) {
// Ignore nested progresses; return a no-op status object.
return SilentStatus(
onFinish: _clearStatus,
stopwatch: _stopwatchFactory.createStopwatch(),
)..start();
}
if (supportsColor) {
_status = AnsiStatus(
_status = SpinnerStatus(
message: message,
multilineOutput: multilineOutput,
padding: progressIndicatorPadding,
onFinish: _clearStatus,
stdio: _stdio,
......@@ -397,18 +392,24 @@ class StdoutLogger extends Logger {
@override
Status startSpinner({ VoidCallback? onFinish }) {
if (terminal.supportsColor) {
return AnsiSpinner(
if (_status != null || !supportsColor) {
return SilentStatus(
onFinish: onFinish,
stopwatch: _stopwatchFactory.createStopwatch(),
terminal: terminal,
stdio: _stdio,
)..start();
}
return SilentStatus(
onFinish: onFinish,
_status = AnonymousSpinnerStatus(
onFinish: () {
if (onFinish != null) {
onFinish();
}
_clearStatus();
},
stdio: _stdio,
stopwatch: _stopwatchFactory.createStopwatch(),
terminal: terminal,
)..start();
return _status!;
}
void _clearStatus() {
......@@ -561,7 +562,6 @@ class BufferLogger extends Logger {
Status startProgress(
String message, {
String? progressId,
bool multilineOutput = false,
int progressIndicatorPadding = kDefaultStatusPadding,
}) {
assert(progressIndicatorPadding != null);
......@@ -661,7 +661,6 @@ class VerboseLogger extends DelegatingLogger {
Status startProgress(
String message, {
String? progressId,
bool multilineOutput = false,
int progressIndicatorPadding = kDefaultStatusPadding,
}) {
assert(progressIndicatorPadding != null);
......@@ -758,12 +757,11 @@ typedef SlowWarningCallback = String Function();
///
/// The [SilentStatus] class never has any output.
///
/// The [AnsiSpinner] subclass shows a spinner, and replaces it with a single
/// space character when stopped or canceled.
/// The [SpinnerStatus] subclass shows a message with a spinner, and replaces it
/// with timing information when stopped. When canceled, the information isn't
/// shown. In either case, a newline is printed.
///
/// The [AnsiStatus] subclass shows a spinner, and replaces it with timing
/// information when stopped. When canceled, the information isn't shown. In
/// either case, a newline is printed.
/// The [AnonymousSpinnerStatus] subclass just shows a spinner.
///
/// The [SummaryStatus] subclass shows only a static message (without an
/// indicator), then updates it when the operation ends.
......@@ -835,6 +833,8 @@ class SilentStatus extends Status {
}
}
const int _kTimePadding = 8; // should fit "99,999ms"
/// Constructor writes [message] to [stdout]. On [cancel] or [stop], will call
/// [onFinish]. On [stop], will additionally print out summary information.
class SummaryStatus extends Status {
......@@ -876,7 +876,8 @@ class SummaryStatus extends Status {
_printMessage();
}
super.stop();
writeSummaryInformation();
assert(_messageShowingOnCurrentLine);
_writeToStdOut(elapsedTime.padLeft(_kTimePadding));
_writeToStdOut('\n');
}
......@@ -888,57 +889,91 @@ class SummaryStatus extends Status {
}
}
/// Prints a (minimum) 8 character padded time.
void writeSummaryInformation() {
assert(_messageShowingOnCurrentLine);
_writeToStdOut(elapsedTime.padLeft(_kTimePadding));
}
@override
void pause() {
super.pause();
_writeToStdOut('\n');
_messageShowingOnCurrentLine = false;
if (_messageShowingOnCurrentLine) {
_writeToStdOut('\n');
_messageShowingOnCurrentLine = false;
}
}
}
/// An [AnsiSpinner] is a simple animation that does nothing but implement a
/// terminal spinner. When stopped or canceled, the animation erases itself.
class AnsiSpinner extends Status {
AnsiSpinner({
required Stopwatch stopwatch,
required Terminal terminal,
/// A kind of animated [Status] that has no message.
///
/// Call [pause] before outputting any text while this is running.
class AnonymousSpinnerStatus extends Status {
AnonymousSpinnerStatus({
VoidCallback? onFinish,
required Stopwatch stopwatch,
required Stdio stdio,
required Terminal terminal,
}) : _stdio = stdio,
_terminal = terminal,
_animation = _selectAnimation(terminal),
super(
onFinish: onFinish,
stopwatch: stopwatch,
);
final String _backspaceChar = '\b';
final String _clearChar = ' ';
final Stdio _stdio;
final Terminal _terminal;
bool timedOut = false;
static const String _backspaceChar = '\b';
static const String _clearChar = ' ';
static const List<String> _emojiAnimations = <String>[
'⣾⣽⣻⢿⡿⣟⣯⣷', // counter-clockwise
'⣾⣷⣯⣟⡿⢿⣻⣽', // clockwise
'⣾⣷⣯⣟⡿⢿⣻⣽⣷⣾⣽⣻⢿⡿⣟⣯⣷', // bouncing clockwise and counter-clockwise
'⣾⣷⣯⣽⣻⣟⡿⢿⣻⣟⣯⣽', // snaking
'⣾⣽⣻⢿⣿⣷⣯⣟⡿⣿', // alternating rain
'⣀⣠⣤⣦⣶⣾⣿⡿⠿⠻⠛⠋⠉⠙⠛⠟⠿⢿⣿⣷⣶⣴⣤⣄', // crawl up and down, large
'⠙⠚⠖⠦⢤⣠⣄⡤⠴⠲⠓⠋', // crawl up and down, small
'⣀⡠⠤⠔⠒⠊⠉⠑⠒⠢⠤⢄', // crawl up and down, tiny
'⡀⣄⣦⢷⠻⠙⠈⠀⠁⠋⠟⡾⣴⣠⢀⠀', // slide up and down
'⠙⠸⢰⣠⣄⡆⠇⠋', // clockwise line
'⠁⠈⠐⠠⢀⡀⠄⠂', // clockwise dot
'⢇⢣⢱⡸⡜⡎', // vertical wobble up
'⡇⡎⡜⡸⢸⢱⢣⢇', // vertical wobble down
'⡀⣀⣐⣒⣖⣶⣾⣿⢿⠿⠯⠭⠩⠉⠁⠀', // swirl
'⠁⠐⠄⢀⢈⢂⢠⣀⣁⣐⣄⣌⣆⣤⣥⣴⣼⣶⣷⣿⣾⣶⣦⣤⣠⣀⡀⠀⠀', // snowing and melting
'⠁⠋⠞⡴⣠⢀⠀⠈⠙⠻⢷⣦⣄⡀⠀⠉⠛⠲⢤⢀⠀', // falling water
'⠄⡢⢑⠈⠀⢀⣠⣤⡶⠞⠋⠁⠀⠈⠙⠳⣆⡀⠀⠆⡷⣹⢈⠀⠐⠪⢅⡀⠀', // fireworks
'⠐⢐⢒⣒⣲⣶⣷⣿⡿⡷⡧⠧⠇⠃⠁⠀⡀⡠⡡⡱⣱⣳⣷⣿⢿⢯⢧⠧⠣⠃⠂⠀⠈⠨⠸⠺⡺⡾⡿⣿⡿⡷⡗⡇⡅⡄⠄⠀⡀⡐⣐⣒⣓⣳⣻⣿⣾⣼⡼⡸⡘⡈⠈⠀', // fade
'⢸⡯⠭⠅⢸⣇⣀⡀⢸⣇⣸⡇⠈⢹⡏⠁⠈⢹⡏⠁⢸⣯⣭⡅⢸⡯⢕⡂⠀⠀', // text crawl
];
static const List<String> _asciiAnimations = <String>[
r'-\|/',
];
static List<String> _selectAnimation(Terminal terminal) {
final List<String> animations = terminal.supportsEmoji ? _emojiAnimations : _asciiAnimations;
return animations[terminal.preferredStyle % animations.length]
.runes
.map<String>((int scalar) => String.fromCharCode(scalar))
.toList();
}
int ticks = 0;
Timer? timer;
final List<String> _animation;
// Windows console font has a limited set of Unicode characters.
List<String> get _animation => !_terminal.supportsEmoji
? const <String>[r'-', r'\', r'|', r'/']
: const <String>['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'];
Timer? timer;
int ticks = 0;
int _lastAnimationFrameLength = 0;
String get _currentAnimationFrame => _animation[ticks % _animation.length];
int get _currentLength => _currentAnimationFrame.length;
String get _backspace => _backspaceChar * (spinnerIndent + _currentLength);
String get _clear => _clearChar * (spinnerIndent + _currentLength);
int get _currentLineLength => _lastAnimationFrameLength;
@protected
int get spinnerIndent => 0;
void _writeToStdOut(String message) => _stdio.stdoutWrite(message);
void _clear(int length) {
_writeToStdOut(
'${_backspaceChar * length}'
'${_clearChar * length}'
'${_backspaceChar * length}'
);
}
@override
void start() {
......@@ -947,10 +982,7 @@ class AnsiSpinner extends Status {
_startSpinner();
}
void _writeToStdOut(String message) => _stdio.stdoutWrite(message);
void _startSpinner() {
_writeToStdOut(_clear); // for _callback to backspace over
timer = Timer.periodic(const Duration(milliseconds: 100), _callback);
_callback(timer!);
}
......@@ -959,30 +991,23 @@ class AnsiSpinner extends Status {
assert(this.timer == timer);
assert(timer != null);
assert(timer.isActive);
_writeToStdOut(_backspace);
_writeToStdOut(_backspaceChar * _lastAnimationFrameLength);
ticks += 1;
_writeToStdOut('${_clearChar * spinnerIndent}$_currentAnimationFrame');
}
@override
void finish() {
assert(timer != null);
assert(timer!.isActive);
timer?.cancel();
timer = null;
_clearSpinner();
super.finish();
}
void _clearSpinner() {
_writeToStdOut('$_backspace$_clear$_backspace');
final String newFrame = _currentAnimationFrame;
_lastAnimationFrameLength = newFrame.runes.length;
_writeToStdOut(newFrame);
}
@override
void pause() {
assert(timer != null);
assert(timer!.isActive);
_clearSpinner();
if (_terminal.supportsColor) {
_writeToStdOut('\r\x1B[K'); // go to start of line and clear line
} else {
_clear(_currentLineLength);
}
_lastAnimationFrameLength = 0;
timer?.cancel();
}
......@@ -992,101 +1017,90 @@ class AnsiSpinner extends Status {
assert(!timer!.isActive);
_startSpinner();
}
}
const int _kTimePadding = 8; // should fit "99,999ms"
@override
void finish() {
assert(timer != null);
assert(timer!.isActive);
timer?.cancel();
timer = null;
_clear(_lastAnimationFrameLength);
_lastAnimationFrameLength = 0;
super.finish();
}
}
/// Constructor writes [message] to [stdout] with padding, then starts an
/// indeterminate progress indicator animation (it's a subclass of
/// [AnsiSpinner]).
/// An animated version of [Status].
///
/// The constructor writes [message] to [stdout] with padding, then starts an
/// indeterminate progress indicator animation.
///
/// On [cancel] or [stop], will call [onFinish]. On [stop], will
/// additionally print out summary information.
class AnsiStatus extends AnsiSpinner {
AnsiStatus({
this.message = '',
this.multilineOutput = false,
///
/// Call [pause] before outputting any text while this is running.
class SpinnerStatus extends AnonymousSpinnerStatus {
SpinnerStatus({
required this.message,
this.padding = kDefaultStatusPadding,
required Stopwatch stopwatch,
required Terminal terminal,
VoidCallback? onFinish,
required Stopwatch stopwatch,
required Stdio stdio,
}) : assert(message != null),
assert(multilineOutput != null),
assert(padding != null),
super(
required Terminal terminal,
}) : super(
onFinish: onFinish,
stdio: stdio,
stopwatch: stopwatch,
stdio: stdio,
terminal: terminal,
);
final String message;
final bool multilineOutput;
final int padding;
static const String _margin = ' ';
@override
int get spinnerIndent => _kTimePadding - 1;
static final String _margin = AnonymousSpinnerStatus._clearChar * (5 + _kTimePadding - 1);
int _totalMessageLength = 0;
@override
int get _currentLineLength => _totalMessageLength + super._currentLineLength;
@override
void start() {
_startStatus();
_printStatus();
super.start();
}
void _startStatus() {
void _printStatus() {
final String line = '${message.padRight(padding)}$_margin';
_totalMessageLength = line.length;
_writeToStdOut(line);
}
@override
void stop() {
super.stop();
writeSummaryInformation();
_writeToStdOut('\n');
void pause() {
super.pause();
_totalMessageLength = 0;
}
@override
void cancel() {
super.cancel();
_writeToStdOut('\n');
}
/// Print summary information when a task is done.
///
/// If [multilineOutput] is false, replaces the spinner with the summary message.
///
/// If [multilineOutput] is true, then it prints the message again on a new
/// line before writing the elapsed time.
void writeSummaryInformation() {
if (multilineOutput) {
_writeToStdOut('\n${'$message Done'.padRight(padding)}$_margin');
}
_writeToStdOut(elapsedTime.padLeft(_kTimePadding));
}
void _clearStatus() {
_writeToStdOut(
'${_backspaceChar * _totalMessageLength}'
'${_clearChar * _totalMessageLength}'
'${_backspaceChar * _totalMessageLength}',
);
void resume() {
_printStatus();
super.resume();
}
@override
void pause() {
super.pause();
_clearStatus();
void stop() {
super.stop(); // calls finish, which clears the spinner
assert(_totalMessageLength > _kTimePadding);
_writeToStdOut(AnonymousSpinnerStatus._backspaceChar * (_kTimePadding - 1));
_writeToStdOut(elapsedTime.padLeft(_kTimePadding));
_writeToStdOut('\n');
}
@override
void resume() {
_startStatus();
super.resume();
void cancel() {
super.cancel(); // calls finish, which clears the spinner
assert(_totalMessageLength > 0);
_writeToStdOut('\n');
}
}
......@@ -81,6 +81,10 @@ abstract class Terminal {
/// Whether the current terminal can display emoji.
bool get supportsEmoji;
/// When we have a choice of styles (e.g. animated spinners), this selects the
/// style to use.
int get preferredStyle;
/// Whether we are interacting with the flutter tool via the terminal.
///
/// If not set, defaults to false.
......@@ -143,12 +147,15 @@ class AnsiTerminal implements Terminal {
AnsiTerminal({
required io.Stdio stdio,
required Platform platform,
DateTime? now, // Time used to determine preferredStyle. Defaults to 0001-01-01 00:00.
})
: _stdio = stdio,
_platform = platform;
_platform = platform,
_now = now ?? DateTime(1);
final io.Stdio _stdio;
final Platform _platform;
final DateTime _now;
static const String bold = '\u001B[1m';
static const String resetAll = '\u001B[0m';
......@@ -187,6 +194,15 @@ class AnsiTerminal implements Terminal {
bool get supportsEmoji => !_platform.isWindows
|| _platform.environment.containsKey('WT_SESSION');
@override
int get preferredStyle {
const int workdays = DateTime.friday;
if (_now.weekday <= workdays) {
return _now.weekday - 1;
}
return _now.hour + workdays;
}
final RegExp _boldControls = RegExp(
'(${RegExp.escape(resetBold)}|${RegExp.escape(bold)})',
);
......@@ -355,6 +371,9 @@ class _TestTerminal implements Terminal {
@override
final bool supportsEmoji;
@override
int get preferredStyle => 0;
@override
bool get stdinHasTerminal => false;
......
......@@ -163,6 +163,7 @@ AnsiTerminal get terminal {
final AnsiTerminal _defaultAnsiTerminal = AnsiTerminal(
stdio: stdio,
platform: platform,
now: DateTime.now(),
);
/// The global Stdio wrapper.
......
......@@ -192,17 +192,14 @@ void main() {
);
const String progressId = 'progressId';
const bool multilineOutput = true;
const int progressIndicatorPadding = kDefaultStatusPadding * 2;
expect(
() => delegatingLogger.startProgress(message,
progressId: progressId,
multilineOutput: multilineOutput,
progressIndicatorPadding: progressIndicatorPadding,
),
_throwsInvocationFor(() => fakeLogger.startProgress(message,
progressId: progressId,
multilineOutput: multilineOutput,
progressIndicatorPadding: progressIndicatorPadding,
)),
);
......@@ -399,7 +396,7 @@ void main() {
Platform ansiPlatform;
AnsiTerminal terminal;
AnsiTerminal coloredTerminal;
AnsiStatus ansiStatus;
SpinnerStatus spinnerStatus;
setUp(() {
platform = FakePlatform(stdoutSupportsAnsi: false);
......@@ -414,7 +411,7 @@ void main() {
platform: ansiPlatform,
);
ansiStatus = AnsiStatus(
spinnerStatus = SpinnerStatus(
message: 'Hello world',
padding: 20,
onFinish: () => called += 1,
......@@ -424,35 +421,35 @@ void main() {
);
});
testWithoutContext('AnsiSpinner works (1)', () async {
testWithoutContext('AnonymousSpinnerStatus works (1)', () async {
bool done = false;
mockStopwatch = FakeStopwatch();
FakeAsync().run((FakeAsync time) {
final AnsiSpinner ansiSpinner = AnsiSpinner(
final AnonymousSpinnerStatus spinner = AnonymousSpinnerStatus(
stdio: mockStdio,
stopwatch: stopwatchFactory.createStopwatch(),
terminal: terminal,
)..start();
doWhileAsync(time, () => ansiSpinner.ticks < 10);
doWhileAsync(time, () => spinner.ticks < 10);
List<String> lines = outputStdout();
expect(lines[0], startsWith(
terminal.supportsEmoji
? ' \b\b⣻\b⢿\b⡿\b⣟\b⣯\b⣷\b⣾\b⣽\b⣻'
: ' \b\\\b|\b/\b-\b\\\b|\b/\b-'
? '⣽\b⣻\b⢿\b⡿\b⣟\b⣯\b⣷\b⣾\b⣽\b⣻'
: '\\\b|\b/\b-\b\\\b|\b/\b-'
),
);
expect(lines[0].endsWith('\n'), isFalse);
expect(lines.length, equals(1));
ansiSpinner.stop();
spinner.stop();
lines = outputStdout();
expect(lines[0], endsWith('\b \b'));
expect(lines.length, equals(1));
// Verify that stopping or canceling multiple times throws.
expect(ansiSpinner.stop, throwsAssertionError);
expect(ansiSpinner.cancel, throwsAssertionError);
expect(spinner.stop, throwsAssertionError);
expect(spinner.cancel, throwsAssertionError);
done = true;
});
expect(done, isTrue);
......@@ -472,12 +469,13 @@ void main() {
);
expect(outputStderr().length, equals(1));
expect(outputStderr().first, isEmpty);
// the 5 below is the margin that is always included between the message and the time.
// the 4 below is the margin that is always included between the message and the time.
// the 8 below is the space left for the time.
expect(
outputStdout().join('\n'),
matches(terminal.supportsEmoji
? r'^Hello {15} {5} {8}[\b]{8} {7}⣽$'
: r'^Hello {15} {5} {8}[\b]{8} {7}\\$'),
? r'^Hello {15} {4} {8}⣽$'
: r'^Hello {15} {4} {8}\\$'),
);
mockStopwatch.elapsed = const Duration(seconds: 4, milliseconds: 100);
status.stop();
......@@ -485,8 +483,8 @@ void main() {
outputStdout().join('\n'),
matches(
terminal.supportsEmoji
? r'^Hello {15} {5} {8}[\b]{8} {7}⣽[\b]{8} {8}[\b]{8}[\d, ]{4}[\d]\.[\d]s[\n]$'
: r'^Hello {15} {5} {8}[\b]{8} {7}\\[\b]{8} {8}[\b]{8}[\d, ]{4}[\d]\.[\d]s[\n]$',
? r'^Hello {15} {4} {8}⣽[\b] [\b]{8}[\d, ]{4}[\d]\.[\d]s[\n]$'
: r'^Hello {15} {4} {8}\\[\b] [\b]{8}[\d, ]{4}[\d]\.[\d]s[\n]$',
),
);
});
......@@ -501,32 +499,30 @@ void main() {
outputPreferences: OutputPreferences.test(showColor: true),
stopwatchFactory: stopwatchFactory,
);
const String message = "Knock Knock, Who's There";
final Status status = logger.startProgress(
"Knock Knock, Who's There",
progressIndicatorPadding: 10,
message,
progressIndicatorPadding: 10, // ignored
);
logger.printStatus('Rude Interrupting Cow');
status.stop();
final String a = terminal.supportsEmoji ? '⣽' : r'\';
final String b = terminal.supportsEmoji ? '⣻' : '|';
const String blankLine = '\r\x1B[K';
expect(
outputStdout().join('\n'),
"Knock Knock, Who's There " // initial message
' ' // placeholder so that spinner can backspace on its first tick
'$message' // initial message
'${" " * 4}${" " * 8}' // margin (4) and space for the time at the end (8)
// ignore: missing_whitespace_between_adjacent_strings
'\b\b\b\b\b\b\b\b $a' // first tick
'\b\b\b\b\b\b\b\b ' // clearing the spinner
'\b\b\b\b\b\b\b\b' // clearing the clearing of the spinner
'\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b ' // clearing the message
'\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b' // clearing the clearing of the message
'$a' // first tick
'$blankLine' // clearing the line
'Rude Interrupting Cow\n' // message
"Knock Knock, Who's There " // message restoration
' ' // placeholder so that spinner can backspace on its second tick
'$message' // message restoration
'${" " * 4}${" " * 8}' // margin (4) and space for the time at the end (8)
'$b' // second tick
// ignore: missing_whitespace_between_adjacent_strings
'\b\b\b\b\b\b\b\b $b' // second tick
'\b\b\b\b\b\b\b\b ' // clearing the spinner to put the time
'\b\b\b\b\b\b\b\b' // clearing the clearing of the spinner
'\b \b' // backspace the tick, wipe the tick, backspace the wipe
'\b\b\b\b\b\b\b' // backspace the space for the time
' 5.0s\n', // replacing it with the time
);
done = true;
......@@ -534,66 +530,96 @@ void main() {
expect(done, isTrue);
});
testWithoutContext('AnsiStatus works when canceled', () async {
testWithoutContext('Stdout startProgress on non-colored terminal pauses', () async {
bool done = false;
FakeAsync().run((FakeAsync time) {
mockStopwatch.elapsed = const Duration(seconds: 5);
final Logger logger = StdoutLogger(
terminal: terminal,
stdio: mockStdio,
outputPreferences: OutputPreferences.test(showColor: true),
stopwatchFactory: stopwatchFactory,
);
const String message = "Knock Knock, Who's There";
final Status status = logger.startProgress(
message,
progressIndicatorPadding: 10, // ignored
);
logger.printStatus('Rude Interrupting Cow');
status.stop();
expect(
outputStdout().join('\n'),
'$message' // initial message
' ' // margin
'\n' // clearing the line
'Rude Interrupting Cow\n' // message
'$message 5.0s\n' // message restoration
);
done = true;
});
expect(done, isTrue);
});
testWithoutContext('SpinnerStatus works when canceled', () async {
bool done = false;
FakeAsync().run((FakeAsync time) {
ansiStatus.start();
spinnerStatus.start();
mockStopwatch.elapsed = const Duration(seconds: 1);
doWhileAsync(time, () => ansiStatus.ticks < 10);
doWhileAsync(time, () => spinnerStatus.ticks < 10);
List<String> lines = outputStdout();
expect(lines[0], startsWith(
terminal.supportsEmoji
? 'Hello world \b\b\b\b\b\b\b\b ⣽\b\b\b\b\b\b\b\b ⣻\b\b\b\b\b\b\b\b ⢿\b\b\b\b\b\b\b\b ⡿\b\b\b\b\b\b\b\b ⣟\b\b\b\b\b\b\b\b ⣯\b\b\b\b\b\b\b\b ⣷\b\b\b\b\b\b\b\b ⣾\b\b\b\b\b\b\b\b ⣽\b\b\b\b\b\b\b\b ⣻'
: 'Hello world \b\b\b\b\b\b\b\b \\\b\b\b\b\b\b\b\b |\b\b\b\b\b\b\b\b /\b\b\b\b\b\b\b\b -\b\b\b\b\b\b\b\b \\\b\b\b\b\b\b\b\b |\b\b\b\b\b\b\b\b /\b\b\b\b\b\b\b\b -\b\b\b\b\b\b\b\b \\\b\b\b\b\b\b\b\b |',
? 'Hello world \b⣻\b⢿\b⡿\b⣟\b⣯\b⣷\b⣾\b⣽\b⣻'
: 'Hello world \\\b|\b/\b-\b\\\b|\b/\b-\b\\\b|'
));
expect(lines.length, equals(1));
expect(lines[0].endsWith('\n'), isFalse);
// Verify a cancel does _not_ print the time and prints a newline.
ansiStatus.cancel();
spinnerStatus.cancel();
lines = outputStdout();
final List<Match> matches = secondDigits.allMatches(lines[0]).toList();
expect(matches, isEmpty);
final String leading = terminal.supportsEmoji ? '⣻' : '|';
expect(lines[0], endsWith('$leading\b\b\b\b\b\b\b\b \b\b\b\b\b\b\b\b'));
expect(lines[0], endsWith('$leading\b \b'));
expect(called, equals(1));
expect(lines.length, equals(2));
expect(lines[1], equals(''));
// Verify that stopping or canceling multiple times throws.
expect(ansiStatus.cancel, throwsAssertionError);
expect(ansiStatus.stop, throwsAssertionError);
expect(spinnerStatus.cancel, throwsAssertionError);
expect(spinnerStatus.stop, throwsAssertionError);
done = true;
});
expect(done, isTrue);
});
testWithoutContext('AnsiStatus works when stopped', () async {
testWithoutContext('SpinnerStatus works when stopped', () async {
bool done = false;
FakeAsync().run((FakeAsync time) {
ansiStatus.start();
spinnerStatus.start();
mockStopwatch.elapsed = const Duration(seconds: 1);
doWhileAsync(time, () => ansiStatus.ticks < 10);
doWhileAsync(time, () => spinnerStatus.ticks < 10);
List<String> lines = outputStdout();
expect(lines, hasLength(1));
expect(
lines[0],
terminal.supportsEmoji
? 'Hello world \b\b\b\b\b\b\b\b ⣽\b\b\b\b\b\b\b\b ⣻\b\b\b\b\b\b\b\b ⢿\b\b\b\b\b\b\b\b ⡿\b\b\b\b\b\b\b\b ⣟\b\b\b\b\b\b\b\b ⣯\b\b\b\b\b\b\b\b ⣷\b\b\b\b\b\b\b\b ⣾\b\b\b\b\b\b\b\b ⣽\b\b\b\b\b\b\b\b ⣻'
: 'Hello world \b\b\b\b\b\b\b\b \\\b\b\b\b\b\b\b\b |\b\b\b\b\b\b\b\b /\b\b\b\b\b\b\b\b -\b\b\b\b\b\b\b\b \\\b\b\b\b\b\b\b\b |\b\b\b\b\b\b\b\b /\b\b\b\b\b\b\b\b -\b\b\b\b\b\b\b\b \\\b\b\b\b\b\b\b\b |',
? 'Hello world \b⣻\b⢿\b⡿\b⣟\b⣯\b⣷\b⣾\b⣽\b⣻'
: 'Hello world \\\b|\b/\b-\b\\\b|\b/\b-\b\\\b|'
);
// Verify a stop prints the time.
ansiStatus.stop();
spinnerStatus.stop();
lines = outputStdout();
expect(lines, hasLength(2));
expect(lines[0], matches(
terminal.supportsEmoji
? r'Hello world {8}[\b]{8} {7}⣽[\b]{8} {7}⣻[\b]{8} {7}⢿[\b]{8} {7}⡿[\b]{8} {7}⣟[\b]{8} {7}⣯[\b]{8} {7}⣷[\b]{8} {7}⣾[\b]{8} {7}⣽[\b]{8} {7}⣻[\b]{8} {7} [\b]{8}[\d., ]{5}[\d]ms$'
: r'Hello world {8}[\b]{8} {7}\\[\b]{8} {7}|[\b]{8} {7}/[\b]{8} {7}-[\b]{8} {7}\\[\b]{8} {7}|[\b]{8} {7}/[\b]{8} {7}-[\b]{8} {7}\\[\b]{8} {7}|[\b]{8} {7} [\b]{8}[\d., ]{6}[\d]ms$',
? r'Hello world ⣽[\b]⣻[\b]⢿[\b]⡿[\b]⣟[\b]⣯[\b]⣷[\b]⣾[\b]⣽[\b]⣻[\b] [\b]{8}[\d., ]{5}[\d]ms$'
: r'Hello world \\[\b]|[\b]/[\b]-[\b]\\[\b]|[\b]/[\b]-[\b]\\[\b]|[\b] [\b]{8}[\d., ]{5}[\d]ms$'
));
expect(lines[1], isEmpty);
final List<Match> times = secondDigits.allMatches(lines[0]).toList();
......@@ -607,8 +633,8 @@ void main() {
expect(lines[1], equals(''));
// Verify that stopping or canceling multiple times throws.
expect(ansiStatus.stop, throwsAssertionError);
expect(ansiStatus.cancel, throwsAssertionError);
expect(spinnerStatus.stop, throwsAssertionError);
expect(spinnerStatus.cancel, throwsAssertionError);
done = true;
});
expect(done, isTrue);
......
......@@ -173,12 +173,53 @@ void main() {
..stdinHasTerminal = false;
final AnsiTerminal ansiTerminal = AnsiTerminal(
stdio: stdio,
platform: const LocalPlatform()
platform: const LocalPlatform(),
);
expect(() => ansiTerminal.singleCharMode = true, returnsNormally);
});
});
testWithoutContext('AnsiTerminal.preferredStyle', () {
final Stdio stdio = FakeStdio();
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform()).preferredStyle, 0); // Defaults to 0 for backwards compatibility.
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 1)).preferredStyle, 0);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 2)).preferredStyle, 1);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 3)).preferredStyle, 2);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 4)).preferredStyle, 3);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 5)).preferredStyle, 4);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 6)).preferredStyle, 5);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 7)).preferredStyle, 5);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 8)).preferredStyle, 0);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 9)).preferredStyle, 1);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 10)).preferredStyle, 2);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 11)).preferredStyle, 3);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 1, 1)).preferredStyle, 0);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 2, 1)).preferredStyle, 1);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 3, 1)).preferredStyle, 2);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 4, 1)).preferredStyle, 3);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 5, 1)).preferredStyle, 4);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 6, 1)).preferredStyle, 6);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 7, 1)).preferredStyle, 6);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 8, 1)).preferredStyle, 0);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 9, 1)).preferredStyle, 1);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 10, 1)).preferredStyle, 2);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 11, 1)).preferredStyle, 3);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 1, 23)).preferredStyle, 0);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 2, 23)).preferredStyle, 1);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 3, 23)).preferredStyle, 2);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 4, 23)).preferredStyle, 3);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 5, 23)).preferredStyle, 4);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 6, 23)).preferredStyle, 28);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 7, 23)).preferredStyle, 28);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 8, 23)).preferredStyle, 0);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 9, 23)).preferredStyle, 1);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 10, 23)).preferredStyle, 2);
expect(AnsiTerminal(stdio: stdio, platform: const LocalPlatform(), now: DateTime(2018, 1, 11, 23)).preferredStyle, 3);
});
}
late Stream<String> mockStdInStream;
......@@ -187,7 +228,8 @@ class TestTerminal extends AnsiTerminal {
TestTerminal({
Stdio? stdio,
Platform platform = const LocalPlatform(),
}) : super(stdio: stdio ?? Stdio(), platform: platform);
DateTime? now,
}) : super(stdio: stdio ?? Stdio(), platform: platform, now: now ?? DateTime(2018));
@override
Stream<String> get keystrokes {
......@@ -195,6 +237,9 @@ class TestTerminal extends AnsiTerminal {
}
bool singleCharMode = false;
@override
int get preferredStyle => 0;
}
class FakeStdio extends Fake implements Stdio {
......
......@@ -682,4 +682,7 @@ class TestTerminal extends AnsiTerminal {
Stream<String> get keystrokes {
return mockTerminalStdInStream;
}
@override
int get preferredStyle => 0;
}
......@@ -280,6 +280,8 @@ Future<ProcessTestResult> runFlutter(
return ProcessTestResult(exitCode, stdoutLog, stderrLog);
}
const int progressMessageWidth = 64;
void main() {
testWithoutContext('flutter run writes and clears pidfile appropriately', () async {
final String tempDirectory = fileSystem.systemTempDirectory.createTempSync('flutter_overall_experience_test.').resolveSymbolicLinksSync();
......@@ -326,18 +328,18 @@ void main() {
<String>['run', '-dflutter-tester', '--report-ready', '--pid-file', pidFile, '--no-devtools', testScript],
testDirectory,
<Transition>[
Barrier('Flutter run key commands.', handler: (String line) {
Multiple(<Pattern>['Flutter run key commands.', 'called paint'], handler: (String line) {
pid = int.parse(fileSystem.file(pidFile).readAsStringSync());
processManager.killPid(pid, ProcessSignal.sigusr1);
return null;
}),
Barrier(RegExp(r'^Performing hot reload\.\.\.'), logging: true), // sometimes this includes the "called reassemble" message
Multiple(<Pattern>[RegExp(r'^Reloaded 0 libraries in [0-9]+ms\.$'), /*'called reassemble', (see TODO below)*/ 'called paint'], handler: (String line) {
Barrier('Performing hot reload...'.padRight(progressMessageWidth), logging: true),
Multiple(<Pattern>[RegExp(r'^Reloaded 0 libraries in [0-9]+ms\.$'), 'called reassemble', 'called paint'], handler: (String line) {
processManager.killPid(pid, ProcessSignal.sigusr2);
return null;
}),
Barrier(RegExp(r'^Performing hot restart\.\.\.')), // sametimes this includes the "called main" message
Multiple(<Pattern>[RegExp(r'^Restarted application in [0-9]+ms.$'), /*'called main', (see TODO below)*/ 'called paint'], handler: (String line) {
Barrier('Performing hot restart...'.padRight(progressMessageWidth)),
Multiple(<Pattern>[RegExp(r'^Restarted application in [0-9]+ms.$'), 'called main', 'called paint'], handler: (String line) {
return 'q';
}),
const Barrier('Application finished.'),
......@@ -347,25 +349,20 @@ void main() {
// We check the output from the app (all starts with "called ...") and the output from the tool
// (everything else) separately, because their relative timing isn't guaranteed. Their rough timing
// is verified by the expected transitions above.
// TODO(ianh): Fix the tool so that the output isn't garbled (right now we're putting debug output from
// the app on the line where we're spinning the busy signal, rather than adding a newline).
expect(result.stdout.where((String line) => line.startsWith('called ') &&
line != 'called reassemble' /* see todo above*/ &&
line != 'called main' /* see todo above*/), <Object>[
expect(result.stdout.where((String line) => line.startsWith('called ')), <Object>[
// logs start after we receive the response to sending SIGUSR1
// SIGUSR1:
// 'called reassemble', // see todo above, this only sometimes gets included, other times it's on the "performing..." line
'called reassemble',
'called paint',
// SIGUSR2:
// 'called main', // see todo above, this is sometimes on the "performing..." line
'called main',
'called paint',
]);
expect(result.stdout.where((String line) => !line.startsWith('called ')), <Object>[
// logs start after we receive the response to sending SIGUSR1
startsWith('Performing hot reload...'), // see todo above, this sometimes ends with "called reassemble"
'', // this newline is probably the misplaced one for the reassemble; see todo above
'Performing hot reload...'.padRight(progressMessageWidth),
startsWith('Reloaded 0 libraries in '),
'Performing hot restart... ',
'Performing hot restart...'.padRight(progressMessageWidth),
startsWith('Restarted application in '),
'', // this newline is the one for after we hit "q"
'Application finished.',
......@@ -386,14 +383,14 @@ void main() {
<String>['run', '-dflutter-tester', '--report-ready', '--no-devtools', testScript],
testDirectory,
<Transition>[
Multiple(<Pattern>['Flutter run key commands.', 'called main'], handler: (String line) {
Multiple(<Pattern>['Flutter run key commands.', 'called main', 'called paint'], handler: (String line) {
return 'r';
}),
Barrier(RegExp(r'^Performing hot reload\.\.\.'), logging: true),
Multiple(<Pattern>['ready', /*'reassemble', (see todo below)*/ 'called paint'], handler: (String line) {
Barrier('Performing hot reload...'.padRight(progressMessageWidth), logging: true),
Multiple(<Pattern>['ready', 'called reassemble', 'called paint'], handler: (String line) {
return 'R';
}),
Barrier(RegExp(r'^Performing hot restart\.\.\.')),
Barrier('Performing hot restart...'.padRight(progressMessageWidth)),
Multiple(<Pattern>['ready', 'called main', 'called paint'], handler: (String line) {
return 'p';
}),
......@@ -410,11 +407,10 @@ void main() {
// We check the output from the app (all starts with "called ...") and the output from the tool
// (everything else) separately, because their relative timing isn't guaranteed. Their rough timing
// is verified by the expected transitions above.
// TODO(ianh): Fix the tool so that the output isn't garbled (right now we're putting debug output from
// the app on the line where we're spinning the busy signal, rather than adding a newline).
expect(result.stdout.where((String line) => line.startsWith('called ') && line != 'called reassemble' /* see todo above*/), <Object>[
expect(result.stdout.where((String line) => line.startsWith('called ')), <Object>[
// logs start after we initiate the hot reload
// hot reload:
// 'called reassemble', // see todo above, this sometimes gets placed on the "Performing hot reload..." line
'called reassemble',
'called paint',
// hot restart:
'called main',
......@@ -427,12 +423,11 @@ void main() {
]);
expect(result.stdout.where((String line) => !line.startsWith('called ')), <Object>[
// logs start after we receive the response to hitting "r"
startsWith('Performing hot reload...'), // see todo above, this sometimes ends with "called reassemble"
'', // this newline is probably the misplaced one for the reassemble; see todo above
'Performing hot reload...'.padRight(progressMessageWidth),
startsWith('Reloaded 0 libraries in '),
'ready',
'', // this newline is the one for after we hit "R"
'Performing hot restart... ',
'Performing hot restart...'.padRight(progressMessageWidth),
startsWith('Restarted application in '),
'ready',
'', // newline for after we hit "p" the first time
......@@ -462,7 +457,7 @@ void main() {
Barrier(RegExp(r'^The Flutter DevTools debugger and profiler on Flutter test device is available at: '), handler: (String line) {
return 'r';
}),
Barrier(RegExp(r'^Performing hot reload\.\.\.'), logging: true),
Barrier('Performing hot reload...'.padRight(progressMessageWidth), logging: true),
Barrier(RegExp(r'^Reloaded 0 libraries in [0-9]+ms.'), handler: (String line) {
return 'q';
}),
......@@ -472,6 +467,7 @@ void main() {
expect(result.exitCode, 0);
expect(result.stdout, <Object>[
startsWith('Performing hot reload...'),
'',
'══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞═════════════════════════════════════════════════════════',
'The following assertion was thrown during layout:',
'A RenderFlex overflowed by 69200 pixels on the right.',
......@@ -506,7 +502,6 @@ void main() {
' verticalDirection: down',
'◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤',
'════════════════════════════════════════════════════════════════════════════════════════════════════',
'',
startsWith('Reloaded 0 libraries in '),
'',
'Application finished.',
......
......@@ -36,7 +36,7 @@ final Map<Type, Generator> _testbedDefaults = <Type, Generator>{
FileSystem: () => MemoryFileSystem(style: globals.platform.isWindows ? FileSystemStyle.windows : FileSystemStyle.posix),
ProcessManager: () => FakeProcessManager.any(),
Logger: () => BufferLogger(
terminal: AnsiTerminal(stdio: globals.stdio, platform: globals.platform), // Danger, using real stdio.
terminal: AnsiTerminal(stdio: globals.stdio, platform: globals.platform), // Danger, using real stdio.
outputPreferences: OutputPreferences.test(),
), // Allows reading logs and prevents stdout.
OperatingSystemUtils: () => FakeOperatingSystemUtils(),
......
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