// 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', () 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, 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(warningMessage), ); 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'); }); }); } /// 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; }