blob: c4b7ed5c03c92b64f2f1d2a9d820cabf3cd2034d [file] [log] [blame]
// Copyright 2014 The Flutter 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 'dart:async';
import 'dart:convert';
import 'package:fake_async/fake_async.dart';
import 'package:flutter_tools/executable.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/commands/daemon.dart';
import 'package:test/fake.dart';
import '../../src/common.dart';
import '../../src/fakes.dart';
final Platform _kNoAnsiPlatform = FakePlatform();
final String red = RegExp.escape(AnsiTerminal.red);
final String bold = RegExp.escape(AnsiTerminal.bold);
final String resetBold = RegExp.escape(AnsiTerminal.resetBold);
final String resetColor = RegExp.escape(AnsiTerminal.resetColor);
void main() {
testWithoutContext('correct logger instance is created', () {
final LoggerFactory loggerFactory = LoggerFactory(
terminal: Terminal.test(),
stdio: FakeStdio(),
outputPreferences: OutputPreferences.test(),
);
expect(loggerFactory.createLogger(
verbose: false,
prefixedErrors: false,
machine: false,
daemon: false,
windows: false,
), isA<StdoutLogger>());
expect(loggerFactory.createLogger(
verbose: false,
prefixedErrors: false,
machine: false,
daemon: false,
windows: true,
), isA<WindowsStdoutLogger>());
expect(loggerFactory.createLogger(
verbose: true,
prefixedErrors: false,
machine: false,
daemon: false,
windows: true,
), isA<VerboseLogger>());
expect(loggerFactory.createLogger(
verbose: true,
prefixedErrors: false,
machine: false,
daemon: false,
windows: false,
), isA<VerboseLogger>());
expect(loggerFactory.createLogger(
verbose: false,
prefixedErrors: true,
machine: false,
daemon: false,
windows: false,
), isA<PrefixedErrorLogger>());
expect(loggerFactory.createLogger(
verbose: false,
prefixedErrors: false,
machine: false,
daemon: true,
windows: false,
), isA<NotifyingLogger>());
expect(loggerFactory.createLogger(
verbose: false,
prefixedErrors: false,
machine: true,
daemon: false,
windows: false,
), isA<AppRunLogger>());
});
testWithoutContext('WindowsStdoutLogger rewrites emojis when terminal does not support emoji', () {
final FakeStdio stdio = FakeStdio();
final WindowsStdoutLogger logger = WindowsStdoutLogger(
outputPreferences: OutputPreferences.test(),
stdio: stdio,
terminal: Terminal.test(),
);
logger.printStatus('🔥🖼️✗✓🔨💪✏️');
expect(stdio.writtenToStdout, <String>['X√\n']);
});
testWithoutContext('WindowsStdoutLogger does not rewrite emojis when terminal does support emoji', () {
final FakeStdio stdio = FakeStdio();
final WindowsStdoutLogger logger = WindowsStdoutLogger(
outputPreferences: OutputPreferences.test(),
stdio: stdio,
terminal: Terminal.test(supportsColor: true, supportsEmoji: true),
);
logger.printStatus('🔥🖼️✗✓🔨💪✏️');
expect(stdio.writtenToStdout, <String>['🔥🖼️✗✓🔨💪✏️\n']);
});
testWithoutContext('DelegatingLogger delegates', () {
final FakeLogger fakeLogger = FakeLogger();
final DelegatingLogger delegatingLogger = DelegatingLogger(fakeLogger);
expect(
() => delegatingLogger.quiet,
_throwsInvocationFor(() => fakeLogger.quiet),
);
expect(
() => delegatingLogger.quiet = true,
_throwsInvocationFor(() => fakeLogger.quiet = true),
);
expect(
() => delegatingLogger.hasTerminal,
_throwsInvocationFor(() => fakeLogger.hasTerminal),
);
expect(
() => delegatingLogger.isVerbose,
_throwsInvocationFor(() => fakeLogger.isVerbose),
);
const String message = 'message';
final StackTrace stackTrace = StackTrace.current;
const bool emphasis = true;
const TerminalColor color = TerminalColor.cyan;
const int indent = 88;
const int hangingIndent = 52;
const bool wrap = true;
const bool newline = true;
expect(
() => delegatingLogger.printError(message,
stackTrace: stackTrace,
emphasis: emphasis,
color: color,
indent: indent,
hangingIndent: hangingIndent,
wrap: wrap,
),
_throwsInvocationFor(() => fakeLogger.printError(message,
stackTrace: stackTrace,
emphasis: emphasis,
color: color,
indent: indent,
hangingIndent: hangingIndent,
wrap: wrap,
)),
);
expect(
() => delegatingLogger.printStatus(message,
emphasis: emphasis,
color: color,
newline: newline,
indent: indent,
hangingIndent: hangingIndent,
wrap: wrap,
),
_throwsInvocationFor(() => fakeLogger.printStatus(message,
emphasis: emphasis,
color: color,
newline: newline,
indent: indent,
hangingIndent: hangingIndent,
wrap: wrap,
)),
);
expect(
() => delegatingLogger.printTrace(message),
_throwsInvocationFor(() => fakeLogger.printTrace(message)),
);
final Map<String, dynamic> eventArgs = <String, dynamic>{};
expect(
() => delegatingLogger.sendEvent(message, eventArgs),
_throwsInvocationFor(() => fakeLogger.sendEvent(message, eventArgs)),
);
const String progressId = 'progressId';
const int progressIndicatorPadding = kDefaultStatusPadding * 2;
expect(
() => delegatingLogger.startProgress(message,
progressId: progressId,
progressIndicatorPadding: progressIndicatorPadding,
),
_throwsInvocationFor(() => fakeLogger.startProgress(message,
progressId: progressId,
progressIndicatorPadding: progressIndicatorPadding,
)),
);
expect(
() => delegatingLogger.supportsColor,
_throwsInvocationFor(() => fakeLogger.supportsColor),
);
expect(
() => delegatingLogger.clear(),
_throwsInvocationFor(() => fakeLogger.clear()),
);
});
testWithoutContext('asLogger finds the correct delegate', () async {
final FakeLogger fakeLogger = FakeLogger();
final VerboseLogger verboseLogger = VerboseLogger(fakeLogger);
final NotifyingLogger notifyingLogger =
NotifyingLogger(verbose: true, parent: verboseLogger);
expect(asLogger<Logger>(notifyingLogger), notifyingLogger);
expect(asLogger<NotifyingLogger>(notifyingLogger), notifyingLogger);
expect(asLogger<VerboseLogger>(notifyingLogger), verboseLogger);
expect(asLogger<FakeLogger>(notifyingLogger), fakeLogger);
expect(
() => asLogger<AppRunLogger>(notifyingLogger),
throwsStateError,
);
});
group('AppContext', () {
late FakeStopwatch fakeStopWatch;
setUp(() {
fakeStopWatch = FakeStopwatch();
});
testWithoutContext('error', () async {
final BufferLogger mockLogger = BufferLogger.test(
outputPreferences: OutputPreferences.test(),
);
final VerboseLogger verboseLogger = VerboseLogger(
mockLogger,
stopwatchFactory: FakeStopwatchFactory(stopwatch: fakeStopWatch),
);
verboseLogger.printStatus('Hey Hey Hey Hey');
verboseLogger.printTrace('Oooh, I do I do I do');
final StackTrace stackTrace = StackTrace.current;
verboseLogger.printError('Helpless!', stackTrace: stackTrace);
expect(mockLogger.statusText, matches(r'^\[ (?: {0,2}\+[0-9]{1,4} ms| )\] Hey Hey Hey Hey\n'
r'\[ (?: {0,2}\+[0-9]{1,4} ms| )\] Oooh, I do I do I do\n$'));
expect(mockLogger.traceText, '');
expect(mockLogger.errorText, matches( r'^\[ (?: {0,2}\+[0-9]{1,4} ms| )\] Helpless!\n'));
final String lastLine = LineSplitter.split(stackTrace.toString()).toList().last;
expect(mockLogger.errorText, endsWith('$lastLine\n\n'));
});
testWithoutContext('ANSI colored errors', () async {
final BufferLogger mockLogger = BufferLogger(
terminal: AnsiTerminal(
stdio: FakeStdio(),
platform: FakePlatform(stdoutSupportsAnsi: true),
),
outputPreferences: OutputPreferences.test(showColor: true),
);
final VerboseLogger verboseLogger = VerboseLogger(
mockLogger, stopwatchFactory: FakeStopwatchFactory(stopwatch: fakeStopWatch),
);
verboseLogger.printStatus('Hey Hey Hey Hey');
verboseLogger.printTrace('Oooh, I do I do I do');
verboseLogger.printError('Helpless!');
expect(
mockLogger.statusText,
matches(r'^\[ (?: {0,2}\+[0-9]{1,4} ms| )\] ' '${bold}Hey Hey Hey Hey$resetBold'
r'\n\[ (?: {0,2}\+[0-9]{1,4} ms| )\] Oooh, I do I do I do\n$'));
expect(mockLogger.traceText, '');
expect(
mockLogger.errorText,
matches('^$red' r'\[ (?: {0,2}\+[0-9]{1,4} ms| )\] ' '${bold}Helpless!$resetBold$resetColor' r'\n$'));
});
testWithoutContext('printBox', () {
final BufferLogger mockLogger = BufferLogger(
terminal: AnsiTerminal(
stdio: FakeStdio(),
platform: FakePlatform(stdoutSupportsAnsi: true),
),
outputPreferences: OutputPreferences.test(showColor: true),
);
final VerboseLogger verboseLogger = VerboseLogger(
mockLogger, stopwatchFactory: FakeStopwatchFactory(stopwatch: fakeStopWatch),
);
verboseLogger.printBox('This is the box message', title: 'Sample title');
expect(
mockLogger.statusText,
contains('[ ] \x1B[1m\x1B[22m\n'
'\x1B[1m ┌─ Sample title ──────────┐\x1B[22m\n'
'\x1B[1m │ This is the box message │\x1B[22m\n'
'\x1B[1m └─────────────────────────┘\x1B[22m\n'
'\x1B[1m \x1B[22m\n'
),
);
});
});
testWithoutContext('Logger does not throw when stdio write throws synchronously', () async {
final FakeStdout stdout = FakeStdout(syncError: true);
final FakeStdout stderr = FakeStdout(syncError: true);
final Stdio stdio = Stdio.test(stdout: stdout, stderr: stderr);
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: stdio,
platform: _kNoAnsiPlatform,
),
stdio: stdio,
outputPreferences: OutputPreferences.test(),
);
logger.printStatus('message');
logger.printError('error message');
});
testWithoutContext('Logger does not throw when stdio write throws asynchronously', () async {
final FakeStdout stdout = FakeStdout(syncError: false);
final FakeStdout stderr = FakeStdout(syncError: false);
final Stdio stdio = Stdio.test(stdout: stdout, stderr: stderr);
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: stdio,
platform: _kNoAnsiPlatform,
),
stdio: stdio,
outputPreferences: OutputPreferences.test(),
);
logger.printStatus('message');
logger.printError('error message');
await stdout.done;
await stderr.done;
});
testWithoutContext('Logger does not throw when stdio completes done with an error', () async {
final FakeStdout stdout = FakeStdout(syncError: false, completeWithError: true);
final FakeStdout stderr = FakeStdout(syncError: false, completeWithError: true);
final Stdio stdio = Stdio.test(stdout: stdout, stderr: stderr);
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: stdio,
platform: _kNoAnsiPlatform,
),
stdio: stdio,
outputPreferences: OutputPreferences.test(),
);
logger.printStatus('message');
logger.printError('error message');
expect(() async => stdout.done, throwsException);
expect(() async => stderr.done, throwsException);
});
group('Spinners', () {
late FakeStdio mockStdio;
late FakeStopwatch mockStopwatch;
late FakeStopwatchFactory stopwatchFactory;
late int called;
final List<Platform> testPlatforms = <Platform>[
FakePlatform(
environment: <String, String>{},
executableArguments: <String>[],
),
FakePlatform(
operatingSystem: 'macos',
environment: <String, String>{},
executableArguments: <String>[],
),
FakePlatform(
operatingSystem: 'windows',
environment: <String, String>{},
executableArguments: <String>[],
),
FakePlatform(
operatingSystem: 'windows',
environment: <String, String>{'WT_SESSION': ''},
executableArguments: <String>[],
),
FakePlatform(
operatingSystem: 'fuchsia',
environment: <String, String>{},
executableArguments: <String>[],
),
];
final RegExp secondDigits = RegExp(r'[0-9,.]*[0-9]m?s');
setUp(() {
mockStopwatch = FakeStopwatch();
mockStdio = FakeStdio();
called = 0;
stopwatchFactory = FakeStopwatchFactory(stopwatch: mockStopwatch);
});
List<String> outputStdout() => mockStdio.writtenToStdout.join().split('\n');
List<String> outputStderr() => mockStdio.writtenToStderr.join().split('\n');
void doWhileAsync(FakeAsync time, bool Function() doThis) {
do {
mockStopwatch.elapsed += const Duration(milliseconds: 1);
time.elapse(const Duration(milliseconds: 1));
} while (doThis());
}
for (final Platform testPlatform in testPlatforms) {
group('(${testPlatform.operatingSystem})', () {
late Platform platform;
late Platform ansiPlatform;
late AnsiTerminal terminal;
late AnsiTerminal coloredTerminal;
late SpinnerStatus spinnerStatus;
setUp(() {
platform = FakePlatform();
ansiPlatform = FakePlatform(stdoutSupportsAnsi: true);
terminal = AnsiTerminal(
stdio: mockStdio,
platform: platform,
);
coloredTerminal = AnsiTerminal(
stdio: mockStdio,
platform: ansiPlatform,
);
spinnerStatus = SpinnerStatus(
message: 'Hello world',
padding: 20,
onFinish: () => called += 1,
stdio: mockStdio,
stopwatch: stopwatchFactory.createStopwatch(),
terminal: terminal,
);
});
testWithoutContext('AnonymousSpinnerStatus works (1)', () async {
bool done = false;
mockStopwatch = FakeStopwatch();
FakeAsync().run((FakeAsync time) {
final AnonymousSpinnerStatus spinner = AnonymousSpinnerStatus(
stdio: mockStdio,
stopwatch: mockStopwatch,
terminal: terminal,
)..start();
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-'
),
);
expect(lines[0].endsWith('\n'), isFalse);
expect(lines.length, equals(1));
spinner.stop();
lines = outputStdout();
expect(lines[0], endsWith('\b \b'));
expect(lines.length, equals(1));
// Verify that stopping or canceling multiple times throws.
expect(spinner.stop, throwsAssertionError);
expect(spinner.cancel, throwsAssertionError);
done = true;
});
expect(done, isTrue);
});
testWithoutContext('AnonymousSpinnerStatus logs warning after timeout without color support', () async {
mockStopwatch = FakeStopwatch();
const String warningMessage = 'a warning message.';
final bool done = FakeAsync().run<bool>((FakeAsync time) {
final AnonymousSpinnerStatus spinner = AnonymousSpinnerStatus(
stdio: mockStdio,
stopwatch: mockStopwatch,
terminal: terminal,
slowWarningCallback: () => warningMessage,
warningColor: TerminalColor.red,
timeout: const Duration(milliseconds: 100),
)..start();
// must be greater than the spinner timer duration
const Duration timeLapse = Duration(milliseconds: 101);
mockStopwatch.elapsed += timeLapse;
time.elapse(timeLapse);
List<String> lines = outputStdout();
expect(lines.join().contains(RegExp(red)), isFalse);
expect(lines.join(), '⣽\ba warning message.⣻');
spinner.stop();
lines = outputStdout();
return true;
});
expect(done, isTrue);
});
testWithoutContext('AnonymousSpinnerStatus logs warning after timeout with color support', () async {
mockStopwatch = FakeStopwatch();
const String warningMessage = 'a warning message.';
final bool done = FakeAsync().run<bool>((FakeAsync time) {
final AnonymousSpinnerStatus spinner = AnonymousSpinnerStatus(
stdio: mockStdio,
stopwatch: mockStopwatch,
terminal: coloredTerminal,
slowWarningCallback: () => warningMessage,
warningColor: TerminalColor.red,
timeout: const Duration(milliseconds: 100),
)..start();
// must be greater than the spinner timer duration
const Duration timeLapse = Duration(milliseconds: 101);
mockStopwatch.elapsed += timeLapse;
time.elapse(timeLapse);
List<String> lines = outputStdout();
expect(lines.join().contains(RegExp(red)), isTrue);
expect(lines.join(), '⣽\b${AnsiTerminal.red}a warning message.${AnsiTerminal.resetColor}⣻');
expect(lines.join(), matches('$red$warningMessage$resetColor'));
spinner.stop();
lines = outputStdout();
return true;
});
expect(done, isTrue);
});
testWithoutContext('Stdout startProgress on colored terminal', () async {
final Logger logger = StdoutLogger(
terminal: coloredTerminal,
stdio: mockStdio,
outputPreferences: OutputPreferences.test(showColor: true),
stopwatchFactory: stopwatchFactory,
);
final Status status = logger.startProgress(
'Hello',
progressIndicatorPadding: 20, // this minus the "Hello" equals the 15 below.
);
expect(outputStderr().length, equals(1));
expect(outputStderr().first, isEmpty);
// 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} {4} {8}⣽$'
: r'^Hello {15} {4} {8}\\$'),
);
mockStopwatch.elapsed = const Duration(seconds: 4, milliseconds: 100);
status.stop();
expect(
outputStdout().join('\n'),
matches(
terminal.supportsEmoji
? 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]$',
),
);
});
testWithoutContext('Stdout startProgress on colored terminal pauses', () async {
bool done = false;
FakeAsync().run((FakeAsync time) {
mockStopwatch.elapsed = const Duration(seconds: 5);
final Logger logger = StdoutLogger(
terminal: coloredTerminal,
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();
final String a = terminal.supportsEmoji ? '⣽' : r'\';
final String b = terminal.supportsEmoji ? '⣻' : '|';
const String blankLine = '\r\x1B[K';
expect(
outputStdout().join('\n'),
'$message' // initial message
'${" " * 4}${" " * 8}' // margin (4) and space for the time at the end (8)
'$a' // first tick
'$blankLine' // clearing the line
'Rude Interrupting Cow\n' // message
'$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' // 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;
});
expect(done, isTrue);
});
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) {
spinnerStatus.start();
mockStopwatch.elapsed = const Duration(seconds: 1);
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⣻'
: '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.
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'));
expect(called, equals(1));
expect(lines.length, equals(2));
expect(lines[1], equals(''));
// Verify that stopping or canceling multiple times throws.
expect(spinnerStatus.cancel, throwsAssertionError);
expect(spinnerStatus.stop, throwsAssertionError);
done = true;
});
expect(done, isTrue);
});
testWithoutContext('SpinnerStatus works when stopped', () async {
bool done = false;
FakeAsync().run((FakeAsync time) {
spinnerStatus.start();
mockStopwatch.elapsed = const Duration(seconds: 1);
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⣻'
: 'Hello world \\\b|\b/\b-\b\\\b|\b/\b-\b\\\b|'
);
// Verify a stop prints the time.
spinnerStatus.stop();
lines = outputStdout();
expect(lines, hasLength(2));
expect(lines[0], matches(
terminal.supportsEmoji
? 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();
expect(times, isNotNull);
expect(times, hasLength(1));
final Match match = times.single;
expect(lines[0], endsWith(match.group(0)!));
expect(called, equals(1));
expect(lines.length, equals(2));
expect(lines[1], equals(''));
// Verify that stopping or canceling multiple times throws.
expect(spinnerStatus.stop, throwsAssertionError);
expect(spinnerStatus.cancel, throwsAssertionError);
done = true;
});
expect(done, isTrue);
});
});
}
});
group('Output format', () {
late FakeStdio fakeStdio;
late SummaryStatus summaryStatus;
late int called;
setUp(() {
fakeStdio = FakeStdio();
called = 0;
summaryStatus = SummaryStatus(
message: 'Hello world',
padding: 20,
onFinish: () => called++,
stdio: fakeStdio,
stopwatch: FakeStopwatch(),
);
});
List<String> outputStdout() => fakeStdio.writtenToStdout.join().split('\n');
List<String> outputStderr() => fakeStdio.writtenToStderr.join().split('\n');
testWithoutContext('Error logs are wrapped', () async {
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: fakeStdio,
platform: _kNoAnsiPlatform,
),
stdio: fakeStdio,
outputPreferences: OutputPreferences.test(wrapText: true, wrapColumn: 40),
);
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));
});
testWithoutContext('AppRunLogger writes plain text statuses when no app is active', () async {
final BufferLogger buffer = BufferLogger.test();
final AppRunLogger logger = AppRunLogger(parent: buffer);
logger.startProgress('Test status...').stop();
expect(buffer.statusText.trim(), equals('Test status...'));
});
testWithoutContext('Error logs are wrapped and can be indented.', () async {
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: fakeStdio,
platform: _kNoAnsiPlatform,
),
stdio: fakeStdio,
outputPreferences: OutputPreferences.test(wrapText: true, wrapColumn: 40),
);
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);
});
testWithoutContext('Error logs are wrapped and can have hanging indent.', () async {
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: fakeStdio,
platform: _kNoAnsiPlatform,
),
stdio: fakeStdio,
outputPreferences: OutputPreferences.test(wrapText: true, wrapColumn: 40),
);
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);
});
testWithoutContext('Error logs are wrapped, indented, and can have hanging indent.', () async {
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: fakeStdio,
platform: _kNoAnsiPlatform,
),
stdio: fakeStdio,
outputPreferences: OutputPreferences.test(wrapText: true, wrapColumn: 40),
);
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);
});
testWithoutContext('Stdout logs are wrapped', () async {
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: fakeStdio,
platform: _kNoAnsiPlatform,
),
stdio: fakeStdio,
outputPreferences: OutputPreferences.test(wrapText: true, wrapColumn: 40),
);
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));
});
testWithoutContext('Stdout logs are wrapped and can be indented.', () async {
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: fakeStdio,
platform: _kNoAnsiPlatform,
),
stdio: fakeStdio,
outputPreferences: OutputPreferences.test(wrapText: true, wrapColumn: 40),
);
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);
});
testWithoutContext('Stdout logs are wrapped and can have hanging indent.', () async {
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: fakeStdio,
platform: _kNoAnsiPlatform,
),
stdio: fakeStdio,
outputPreferences: OutputPreferences.test(wrapText: true, wrapColumn: 40)
);
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);
});
testWithoutContext('Stdout logs are wrapped, indented, and can have hanging indent.', () async {
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: fakeStdio,
platform: _kNoAnsiPlatform,
),
stdio: fakeStdio,
outputPreferences: OutputPreferences.test(wrapText: true, wrapColumn: 40),
);
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);
});
testWithoutContext('Error logs are red', () async {
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: fakeStdio,
platform: FakePlatform(stdoutSupportsAnsi: true),
),
stdio: fakeStdio,
outputPreferences: OutputPreferences.test(showColor: true),
);
logger.printError('Pants on fire!');
final List<String> lines = outputStderr();
expect(outputStdout().length, equals(1));
expect(outputStdout().first, isEmpty);
expect(lines[0], equals('${AnsiTerminal.red}Pants on fire!${AnsiTerminal.resetColor}'));
});
testWithoutContext('Stdout logs are not colored', () async {
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: fakeStdio,
platform: FakePlatform(),
),
stdio: fakeStdio,
outputPreferences: OutputPreferences.test(showColor: true),
);
logger.printStatus('All good.');
final List<String> lines = outputStdout();
expect(outputStderr().length, equals(1));
expect(outputStderr().first, isEmpty);
expect(lines[0], equals('All good.'));
});
testWithoutContext('Stdout printBox puts content inside a box', () {
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: fakeStdio,
platform: FakePlatform(),
),
stdio: fakeStdio,
outputPreferences: OutputPreferences.test(showColor: true),
);
logger.printBox('Hello world', title: 'Test title');
final String stdout = fakeStdio.writtenToStdout.join();
expect(stdout,
contains(
'\n'
'┌─ Test title ┐\n'
'│ Hello world │\n'
'└─────────────┘\n'
),
);
});
testWithoutContext('Stdout printBox does not require title', () {
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: fakeStdio,
platform: FakePlatform(),
),
stdio: fakeStdio,
outputPreferences: OutputPreferences.test(showColor: true),
);
logger.printBox('Hello world');
final String stdout = fakeStdio.writtenToStdout.join();
expect(stdout,
contains(
'\n'
'┌─────────────┐\n'
'│ Hello world │\n'
'└─────────────┘\n'
),
);
});
testWithoutContext('Stdout printBox handles new lines', () {
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: fakeStdio,
platform: FakePlatform(),
),
stdio: fakeStdio,
outputPreferences: OutputPreferences.test(showColor: true),
);
logger.printBox('Hello world\nThis is a new line', title: 'Test title');
final String stdout = fakeStdio.writtenToStdout.join();
expect(stdout,
contains(
'\n'
'┌─ Test title ───────┐\n'
'│ Hello world │\n'
'│ This is a new line │\n'
'└────────────────────┘\n'
),
);
});
testWithoutContext('Stdout printBox handles content with ANSI escape characters', () {
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: fakeStdio,
platform: FakePlatform(),
),
stdio: fakeStdio,
outputPreferences: OutputPreferences.test(showColor: true),
);
const String bold = '\u001B[1m';
const String clear = '\u001B[2J\u001B[H';
logger.printBox('${bold}Hello world$clear', title: 'Test title');
final String stdout = fakeStdio.writtenToStdout.join();
expect(stdout,
contains(
'\n'
'┌─ Test title ┐\n'
'│ ${bold}Hello world$clear │\n'
'└─────────────┘\n'
),
);
});
testWithoutContext('Stdout printBox handles column limit', () {
const int columnLimit = 14;
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: fakeStdio,
platform: FakePlatform(),
),
stdio: fakeStdio,
outputPreferences: OutputPreferences.test(showColor: true, wrapColumn: columnLimit),
);
logger.printBox('This line is longer than $columnLimit characters', title: 'Test');
final String stdout = fakeStdio.writtenToStdout.join();
final List<String> stdoutLines = stdout.split('\n');
expect(stdoutLines.length, greaterThan(1));
expect(stdoutLines[1].length, equals(columnLimit));
expect(stdout,
contains(
'\n'
'┌─ Test ─────┐\n'
'│ This line │\n'
'│ is longer │\n'
'│ than 14 │\n'
'│ characters │\n'
'└────────────┘\n'
),
);
});
testWithoutContext('Stdout printBox handles column limit and respects new lines', () {
const int columnLimit = 14;
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: fakeStdio,
platform: FakePlatform(),
),
stdio: fakeStdio,
outputPreferences: OutputPreferences.test(showColor: true, wrapColumn: columnLimit),
);
logger.printBox('This\nline is longer than\n\n$columnLimit characters', title: 'Test');
final String stdout = fakeStdio.writtenToStdout.join();
final List<String> stdoutLines = stdout.split('\n');
expect(stdoutLines.length, greaterThan(1));
expect(stdoutLines[1].length, equals(columnLimit));
expect(stdout,
contains(
'\n'
'┌─ Test ─────┐\n'
'│ This │\n'
'│ line is │\n'
'│ longer │\n'
'│ than │\n'
'│ │\n'
'│ 14 │\n'
'│ characters │\n'
'└────────────┘\n'
),
);
});
testWithoutContext('Stdout printBox breaks long words that exceed the column limit', () {
const int columnLimit = 14;
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: fakeStdio,
platform: FakePlatform(),
),
stdio: fakeStdio,
outputPreferences: OutputPreferences.test(showColor: true, wrapColumn: columnLimit),
);
logger.printBox('Thiswordislongerthan${columnLimit}characters', title: 'Test');
final String stdout = fakeStdio.writtenToStdout.join();
final List<String> stdoutLines = stdout.split('\n');
expect(stdoutLines.length, greaterThan(1));
expect(stdoutLines[1].length, equals(columnLimit));
expect(stdout,
contains(
'\n'
'┌─ Test ─────┐\n'
'│ Thiswordis │\n'
'│ longerthan │\n'
'│ 14characte │\n'
'│ rs │\n'
'└────────────┘\n'
),
);
});
testWithoutContext('Stdout startProgress on non-color terminal', () async {
final FakeStopwatch fakeStopwatch = FakeStopwatch();
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: fakeStdio,
platform: _kNoAnsiPlatform,
),
stdio: fakeStdio,
outputPreferences: OutputPreferences.test(),
stopwatchFactory: FakeStopwatchFactory(stopwatch: fakeStopwatch),
);
final Status status = logger.startProgress(
'Hello',
progressIndicatorPadding: 20, // this minus the "Hello" equals the 15 below.
);
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.
expect(outputStdout().join('\n'), matches(r'^Hello {15} {5}$'));
fakeStopwatch.elapsed = const Duration(seconds: 4, milliseconds: 123);
status.stop();
expect(outputStdout(), <String>['Hello 4.1s', '']);
});
testWithoutContext('SummaryStatus works when canceled', () async {
final SummaryStatus summaryStatus = SummaryStatus(
message: 'Hello world',
padding: 20,
onFinish: () => called++,
stdio: fakeStdio,
stopwatch: FakeStopwatch(),
);
summaryStatus.start();
final List<String> lines = outputStdout();
expect(lines[0], startsWith('Hello world '));
expect(lines.length, equals(1));
expect(lines[0].endsWith('\n'), isFalse);
// Verify a cancel does _not_ print the time and prints a newline.
summaryStatus.cancel();
expect(outputStdout(), <String>[
'Hello world ',
'',
]);
// Verify that stopping or canceling multiple times throws.
expect(summaryStatus.cancel, throwsAssertionError);
expect(summaryStatus.stop, throwsAssertionError);
});
testWithoutContext('SummaryStatus works when stopped', () async {
summaryStatus.start();
final List<String> lines = outputStdout();
expect(lines[0], startsWith('Hello world '));
expect(lines.length, equals(1));
// Verify a stop prints the time.
summaryStatus.stop();
expect(outputStdout(), <String>[
'Hello world 0ms',
'',
]);
// Verify that stopping or canceling multiple times throws.
expect(summaryStatus.stop, throwsAssertionError);
expect(summaryStatus.cancel, throwsAssertionError);
});
testWithoutContext('sequential startProgress calls with StdoutLogger', () async {
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: fakeStdio,
platform: _kNoAnsiPlatform,
),
stdio: fakeStdio,
outputPreferences: OutputPreferences.test(),
);
logger.startProgress('AAA').stop();
logger.startProgress('BBB').stop();
final List<String> output = outputStdout();
expect(output.length, equals(3));
// There's 61 spaces at the start: 59 (padding default) - 3 (length of AAA) + 5 (margin).
// Then there's a left-padded "0ms" 8 characters wide, so 5 spaces then "0ms"
// (except sometimes it's randomly slow so we handle up to "99,999ms").
expect(output[0], matches(RegExp(r'AAA[ ]{61}[\d, ]{5}[\d]ms')));
expect(output[1], matches(RegExp(r'BBB[ ]{61}[\d, ]{5}[\d]ms')));
});
testWithoutContext('sequential startProgress calls with VerboseLogger and StdoutLogger', () async {
final Logger logger = VerboseLogger(
StdoutLogger(
terminal: AnsiTerminal(
stdio: fakeStdio,
platform: _kNoAnsiPlatform,
),
stdio: fakeStdio,
outputPreferences: OutputPreferences.test(),
),
stopwatchFactory: FakeStopwatchFactory(),
);
logger.startProgress('AAA').stop();
logger.startProgress('BBB').stop();
expect(outputStdout(), <Matcher>[
matches(r'^\[ (?: {0,2}\+[0-9]{1,4} ms| )\] AAA$'),
matches(r'^\[ (?: {0,2}\+[0-9]{1,4} ms| )\] AAA \(completed.*\)$'),
matches(r'^\[ (?: {0,2}\+[0-9]{1,4} ms| )\] BBB$'),
matches(r'^\[ (?: {0,2}\+[0-9]{1,4} ms| )\] BBB \(completed.*\)$'),
matches(r'^$'),
]);
});
testWithoutContext('sequential startProgress calls with BufferLogger', () async {
final BufferLogger logger = BufferLogger(
terminal: AnsiTerminal(
stdio: fakeStdio,
platform: _kNoAnsiPlatform,
),
outputPreferences: OutputPreferences.test(),
);
logger.startProgress('AAA').stop();
logger.startProgress('BBB').stop();
expect(logger.statusText, 'AAA\nBBB\n');
});
testWithoutContext('BufferLogger prints status, trace, error', () async {
final BufferLogger mockLogger = BufferLogger.test(
outputPreferences: OutputPreferences.test(),
);
mockLogger.printStatus('Hey Hey Hey Hey');
mockLogger.printTrace('Oooh, I do I do I do');
final StackTrace stackTrace = StackTrace.current;
mockLogger.printError('Helpless!', stackTrace: stackTrace);
expect(mockLogger.statusText, 'Hey Hey Hey Hey\n');
expect(mockLogger.traceText, 'Oooh, I do I do I do\n');
expect(mockLogger.errorText, 'Helpless!\n$stackTrace\n');
});
testWithoutContext('Animations are disabled when, uh, disabled.', () async {
final Logger logger = StdoutLogger(
terminal: AnsiTerminal(
stdio: fakeStdio,
platform: _kNoAnsiPlatform,
defaultCliAnimationEnabled: false,
),
stdio: fakeStdio,
stopwatchFactory: FakeStopwatchFactory(stopwatch: FakeStopwatch()),
outputPreferences: OutputPreferences.test(wrapText: true, wrapColumn: 40),
);
logger.startProgress('po').stop();
expect(outputStderr(), <String>['']);
expect(outputStdout(), <String>[
'po 0ms',
'',
]);
logger.startProgress('ta')
..pause()
..resume()
..stop();
expect(outputStderr(), <String>['']);
expect(outputStdout(), <String>[
'po 0ms',
'ta ',
'ta 0ms',
'',
]);
logger.startSpinner()
..pause()
..resume()
..stop();
expect(outputStderr(), <String>['']);
expect(outputStdout(), <String>[
'po 0ms',
'ta ',
'ta 0ms',
'',
]);
});
});
}
/// A fake [Logger] that throws the [Invocation] for any method call.
class FakeLogger implements Logger {
@override
dynamic noSuchMethod(Invocation invocation) => throw invocation; // ignore: only_throw_errors
}
/// Returns the [Invocation] thrown from a call to [FakeLogger].
Invocation _invocationFor(dynamic Function() fakeCall) {
try {
fakeCall();
} on Invocation catch (invocation) {
return invocation;
}
throw UnsupportedError('_invocationFor can be used only with Fake objects '
'that throw Invocations');
}
/// Returns a [Matcher] that matches against an expected [Invocation].
Matcher _matchesInvocation(Invocation expected) {
return const TypeMatcher<Invocation>()
// Compare Symbol strings instead of comparing Symbols directly for a nicer failure message.
.having((Invocation actual) => actual.memberName.toString(), 'memberName', expected.memberName.toString())
.having((Invocation actual) => actual.isGetter, 'isGetter', expected.isGetter)
.having((Invocation actual) => actual.isSetter, 'isSetter', expected.isSetter)
.having((Invocation actual) => actual.isMethod, 'isMethod', expected.isMethod)
.having((Invocation actual) => actual.typeArguments, 'typeArguments', expected.typeArguments)
.having((Invocation actual) => actual.positionalArguments, 'positionalArguments', expected.positionalArguments)
.having((Invocation actual) => actual.namedArguments, 'namedArguments', expected.namedArguments);
}
/// Returns a [Matcher] that matches against an [Invocation] thrown from a call
/// to [FakeLogger].
Matcher _throwsInvocationFor(dynamic Function() fakeCall) =>
throwsA(_matchesInvocation(_invocationFor(fakeCall)));
class FakeStdout extends Fake implements Stdout {
FakeStdout({required this.syncError, this.completeWithError = false});
final bool syncError;
final bool completeWithError;
final Completer<void> _completer = Completer<void>();
@override
void write(Object? object) {
if (syncError) {
throw Exception('Error!');
}
Zone.current.runUnaryGuarded<void>((_) {
if (completeWithError) {
_completer.completeError(Exception('Some pipe error'));
} else {
_completer.complete();
throw Exception('Error!');
}
}, null);
}
@override
Future<void> get done => _completer.future;
}