// 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' show jsonEncode; 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:mockito/mockito.dart'; import 'package:quiver/testing/async.dart'; import '../../src/common.dart'; import '../../src/mocks.dart' as mocks; final Platform _kNoAnsiPlatform = FakePlatform(stdoutSupportsAnsi: false); 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); class MockStdout extends Mock implements Stdout {} void main() { group('AppContext', () { FakeStopwatch fakeStopWatch; setUp(() { fakeStopWatch = FakeStopwatch(); }); testWithoutContext('error', () async { final BufferLogger mockLogger = BufferLogger.test( outputPreferences: OutputPreferences.test(showColor: false), ); final VerboseLogger verboseLogger = VerboseLogger( mockLogger, stopwatchFactory: FakeStopwatchFactory(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| )\] 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$')); }); testWithoutContext('ANSI colored errors', () async { final BufferLogger mockLogger = BufferLogger( terminal: AnsiTerminal( stdio: mocks.MockStdio(), platform: FakePlatform(stdoutSupportsAnsi: true), ), outputPreferences: OutputPreferences.test(showColor: true), ); final VerboseLogger verboseLogger = VerboseLogger( mockLogger, stopwatchFactory: FakeStopwatchFactory(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('Logger does not throw when stdio write throws synchronously', () async { final MockStdout stdout = MockStdout(); final MockStdout stderr = MockStdout(); final Stdio stdio = Stdio.test(stdout: stdout, stderr: stderr); bool stdoutThrew = false; bool stderrThrew = false; final Completer stdoutError = Completer(); final Completer stderrError = Completer(); when(stdout.write(any)).thenAnswer((_) { stdoutThrew = true; throw 'Error'; }); when(stderr.write(any)).thenAnswer((_) { stderrThrew = true; throw 'Error'; }); when(stdout.done).thenAnswer((_) => stdoutError.future); when(stderr.done).thenAnswer((_) => stderrError.future); final Logger logger = StdoutLogger( terminal: AnsiTerminal( stdio: stdio, platform: _kNoAnsiPlatform, ), stdio: stdio, outputPreferences: OutputPreferences.test(), timeoutConfiguration: const TimeoutConfiguration(), ); logger.printStatus('message'); logger.printError('error message'); expect(stdoutThrew, true); expect(stderrThrew, true); }); testWithoutContext('Logger does not throw when stdio write throws asynchronously', () async { final MockStdout stdout = MockStdout(); final MockStdout stderr = MockStdout(); final Stdio stdio = Stdio.test(stdout: stdout, stderr: stderr); final Completer stdoutError = Completer(); final Completer stderrError = Completer(); bool stdoutThrew = false; bool stderrThrew = false; final Completer stdoutCompleter = Completer(); final Completer stderrCompleter = Completer(); when(stdout.write(any)).thenAnswer((_) { Zone.current.runUnaryGuarded((_) { stdoutThrew = true; stdoutCompleter.complete(); throw 'Error'; }, null); }); when(stderr.write(any)).thenAnswer((_) { Zone.current.runUnaryGuarded((_) { stderrThrew = true; stderrCompleter.complete(); throw 'Error'; }, null); }); when(stdout.done).thenAnswer((_) => stdoutError.future); when(stderr.done).thenAnswer((_) => stderrError.future); final Logger logger = StdoutLogger( terminal: AnsiTerminal( stdio: stdio, platform: _kNoAnsiPlatform, ), stdio: stdio, outputPreferences: OutputPreferences.test(), timeoutConfiguration: const TimeoutConfiguration(), ); logger.printStatus('message'); logger.printError('error message'); await stdoutCompleter.future; await stderrCompleter.future; expect(stdoutThrew, true); expect(stderrThrew, true); }); testWithoutContext('Logger does not throw when stdio completes done with an error', () async { final MockStdout stdout = MockStdout(); final MockStdout stderr = MockStdout(); final Stdio stdio = Stdio.test(stdout: stdout, stderr: stderr); final Completer stdoutError = Completer(); final Completer stderrError = Completer(); final Completer stdoutCompleter = Completer(); final Completer stderrCompleter = Completer(); when(stdout.write(any)).thenAnswer((_) { Zone.current.runUnaryGuarded((_) { stdoutError.completeError(Exception('Some pipe error')); stdoutCompleter.complete(); }, null); }); when(stderr.write(any)).thenAnswer((_) { Zone.current.runUnaryGuarded((_) { stderrError.completeError(Exception('Some pipe error')); stderrCompleter.complete(); }, null); }); when(stdout.done).thenAnswer((_) => stdoutError.future); when(stderr.done).thenAnswer((_) => stderrError.future); final Logger logger = StdoutLogger( terminal: AnsiTerminal( stdio: stdio, platform: _kNoAnsiPlatform, ), stdio: stdio, outputPreferences: OutputPreferences.test(), timeoutConfiguration: const TimeoutConfiguration(), ); logger.printStatus('message'); logger.printError('error message'); await stdoutCompleter.future; await stderrCompleter.future; }); group('Spinners', () { mocks.MockStdio mockStdio; FakeStopwatch mockStopwatch; FakeStopwatchFactory stopwatchFactory; int called; final List testPlatforms = [ FakePlatform( operatingSystem: 'linux', environment: {}, executableArguments: [], ), FakePlatform( operatingSystem: 'macos', environment: {}, executableArguments: [], ), FakePlatform( operatingSystem: 'windows', environment: {}, executableArguments: [], ), FakePlatform( operatingSystem: 'windows', environment: {'WT_SESSION': ''}, executableArguments: [], ), FakePlatform( operatingSystem: 'fuchsia', environment: {}, executableArguments: [], ), ]; final RegExp secondDigits = RegExp(r'[0-9,.]*[0-9]m?s'); setUp(() { mockStopwatch = FakeStopwatch(); mockStdio = mocks.MockStdio(); called = 0; stopwatchFactory = FakeStopwatchFactory(mockStopwatch); }); List outputStdout() => mockStdio.writtenToStdout.join('').split('\n'); List outputStderr() => mockStdio.writtenToStderr.join('').split('\n'); void doWhileAsync(FakeAsync time, bool 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})', () { Platform platform; Platform ansiPlatform; AnsiTerminal terminal; AnsiTerminal coloredTerminal; AnsiStatus ansiStatus; setUp(() { platform = FakePlatform(stdoutSupportsAnsi: false); ansiPlatform = FakePlatform(stdoutSupportsAnsi: true); terminal = AnsiTerminal( stdio: mockStdio, platform: platform, ); coloredTerminal = AnsiTerminal( stdio: mockStdio, platform: ansiPlatform, ); ansiStatus = AnsiStatus( message: 'Hello world', timeout: const Duration(seconds: 2), padding: 20, onFinish: () => called += 1, stdio: mockStdio, timeoutConfiguration: const TimeoutConfiguration(), stopwatch: stopwatchFactory.createStopwatch(), terminal: terminal, ); }); testWithoutContext('AnsiSpinner works (1)', () async { bool done = false; mockStopwatch = FakeStopwatch(); FakeAsync().run((FakeAsync time) { final AnsiSpinner ansiSpinner = AnsiSpinner( timeout: const Duration(hours: 10), stdio: mockStdio, timeoutConfiguration: const TimeoutConfiguration(), stopwatch: stopwatchFactory.createStopwatch(), terminal: terminal, )..start(); doWhileAsync(time, () => ansiSpinner.ticks < 10); List 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-' ), ); expect(lines[0].endsWith('\n'), isFalse); expect(lines.length, equals(1)); ansiSpinner.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); done = true; }); expect(done, isTrue); }); testWithoutContext('AnsiSpinner works (2)', () async { bool done = false; mockStopwatch = FakeStopwatch(); FakeAsync().run((FakeAsync time) { final AnsiSpinner ansiSpinner = AnsiSpinner( timeout: const Duration(seconds: 2), stdio: mockStdio, timeoutConfiguration: const TimeoutConfiguration(), stopwatch: mockStopwatch, terminal: terminal, )..start(); mockStopwatch.elapsed = const Duration(seconds: 1); doWhileAsync(time, () => ansiSpinner.ticks < 10); // one second expect(ansiSpinner.seemsSlow, isFalse); expect(outputStdout().join('\n'), isNot(contains('This is taking an unexpectedly long time.'))); mockStopwatch.elapsed = const Duration(seconds: 3); doWhileAsync(time, () => ansiSpinner.ticks < 30); // three seconds expect(ansiSpinner.seemsSlow, isTrue); // Check the 2nd line to verify there's a newline before the warning expect(outputStdout()[1], contains('This is taking an unexpectedly long time.')); ansiSpinner.stop(); expect(outputStdout().join('\n'), isNot(contains('(!)'))); done = true; }); expect(done, isTrue); }); testWithoutContext('Stdout startProgress on colored terminal', () async { bool done = false; FakeAsync().run((FakeAsync time) { final Logger logger = StdoutLogger( terminal: coloredTerminal, stdio: mockStdio, outputPreferences: OutputPreferences.test(showColor: true), timeoutConfiguration: const TimeoutConfiguration(), stopwatchFactory: stopwatchFactory, ); final Status status = logger.startProgress( 'Hello', progressId: null, timeout: const TimeoutConfiguration().slowOperation, 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(terminal.supportsEmoji ? r'^Hello {15} {5} {8}[\b]{8} {7}⣽$' : r'^Hello {15} {5} {8}[\b]{8} {7}\\$'), ); status.stop(); expect( 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]$', ), ); done = true; }); expect(done, isTrue); }); 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), timeoutConfiguration: const TimeoutConfiguration(), stopwatchFactory: stopwatchFactory, ); final Status status = logger.startProgress( "Knock Knock, Who's There", timeout: const Duration(days: 10), progressIndicatorPadding: 10, ); logger.printStatus('Rude Interrupting Cow'); status.stop(); final String a = terminal.supportsEmoji ? '⣽' : r'\'; final String b = terminal.supportsEmoji ? '⣻' : '|'; expect( outputStdout().join('\n'), "Knock Knock, Who's There " // initial message ' ' // placeholder so that spinner can backspace on its first tick '\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 'Rude Interrupting Cow\n' // message "Knock Knock, Who's There " // message restoration ' ' // placeholder so that spinner can backspace on its second tick '\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 ' 5.0s\n', // replacing it with the time ); done = true; }); expect(done, isTrue); }); testWithoutContext('AnsiStatus works', () { bool done = false; FakeAsync().run((FakeAsync time) { ansiStatus.start(); mockStopwatch.elapsed = const Duration(seconds: 1); doWhileAsync(time, () => ansiStatus.ticks < 10); // one second expect(ansiStatus.seemsSlow, isFalse); expect(outputStdout().join('\n'), isNot(contains('This is taking an unexpectedly long time.'))); expect(outputStdout().join('\n'), isNot(contains('(!)'))); mockStopwatch.elapsed = const Duration(seconds: 3); doWhileAsync(time, () => ansiStatus.ticks < 30); // three seconds expect(ansiStatus.seemsSlow, isTrue); expect(outputStdout().join('\n'), contains('This is taking an unexpectedly long time.')); // Test that the number of '\b' is correct. for (final String line in outputStdout()) { int currLength = 0; for (int i = 0; i < line.length; i += 1) { currLength += line[i] == '\b' ? -1 : 1; expect(currLength, isNonNegative, reason: 'The following line has overflow backtraces:\n' + jsonEncode(line)); } } ansiStatus.stop(); expect(outputStdout().join('\n'), contains('(!)')); done = true; }); expect(done, isTrue); }); testWithoutContext('AnsiStatus works when canceled', () async { bool done = false; FakeAsync().run((FakeAsync time) { ansiStatus.start(); mockStopwatch.elapsed = const Duration(seconds: 1); doWhileAsync(time, () => ansiStatus.ticks < 10); List 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 |', )); 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(); lines = outputStdout(); final List 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(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); done = true; }); expect(done, isTrue); }); testWithoutContext('AnsiStatus works when stopped', () async { bool done = false; FakeAsync().run((FakeAsync time) { ansiStatus.start(); mockStopwatch.elapsed = const Duration(seconds: 1); doWhileAsync(time, () => ansiStatus.ticks < 10); List 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 |', ); // Verify a stop prints the time. ansiStatus.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$', )); expect(lines[1], isEmpty); final List 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(ansiStatus.stop, throwsAssertionError); expect(ansiStatus.cancel, throwsAssertionError); done = true; }); expect(done, isTrue); }); }); } }); group('Output format', () { mocks.MockStdio mockStdio; SummaryStatus summaryStatus; int called; final RegExp secondDigits = RegExp(r'[^\b]\b\b\b\b\b[0-9]+[.][0-9]+(?:s|ms)'); setUp(() { mockStdio = mocks.MockStdio(); called = 0; summaryStatus = SummaryStatus( message: 'Hello world', timeout: const TimeoutConfiguration().slowOperation, padding: 20, onFinish: () => called++, stdio: mockStdio, timeoutConfiguration: const TimeoutConfiguration(), stopwatch: FakeStopwatch(), ); }); List outputStdout() => mockStdio.writtenToStdout.join('').split('\n'); List outputStderr() => mockStdio.writtenToStderr.join('').split('\n'); testWithoutContext('Error logs are wrapped', () async { final Logger logger = StdoutLogger( terminal: AnsiTerminal( stdio: mockStdio, platform: _kNoAnsiPlatform, ), stdio: mockStdio, outputPreferences: OutputPreferences.test(wrapText: true, wrapColumn: 40, showColor: false), timeoutConfiguration: const TimeoutConfiguration(), ); logger.printError('0123456789' * 15); final List 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('Error logs are wrapped and can be indented.', () async { final Logger logger = StdoutLogger( terminal: AnsiTerminal( stdio: mockStdio, platform: _kNoAnsiPlatform, ), stdio: mockStdio, outputPreferences: OutputPreferences.test(wrapText: true, wrapColumn: 40, showColor: false), timeoutConfiguration: const TimeoutConfiguration(), ); logger.printError('0123456789' * 15, indent: 5); final List 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: mockStdio, platform: _kNoAnsiPlatform, ), stdio: mockStdio, outputPreferences: OutputPreferences.test(wrapText: true, wrapColumn: 40, showColor: false), timeoutConfiguration: const TimeoutConfiguration(), ); logger.printError('0123456789' * 15, hangingIndent: 5); final List 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: mockStdio, platform: _kNoAnsiPlatform, ), stdio: mockStdio, outputPreferences: OutputPreferences.test(wrapText: true, wrapColumn: 40, showColor: false), timeoutConfiguration: const TimeoutConfiguration(), ); logger.printError('0123456789' * 15, indent: 4, hangingIndent: 5); final List 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: mockStdio, platform: _kNoAnsiPlatform, ), stdio: mockStdio, outputPreferences: OutputPreferences.test(wrapText: true, wrapColumn: 40, showColor: false), timeoutConfiguration: const TimeoutConfiguration(), ); logger.printStatus('0123456789' * 15); final List 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: mockStdio, platform: _kNoAnsiPlatform, ), stdio: mockStdio, outputPreferences: OutputPreferences.test(wrapText: true, wrapColumn: 40, showColor: false), timeoutConfiguration: const TimeoutConfiguration(), ); logger.printStatus('0123456789' * 15, indent: 5); final List 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: mockStdio, platform: _kNoAnsiPlatform, ), stdio: mockStdio, outputPreferences: OutputPreferences.test(wrapText: true, wrapColumn: 40, showColor: false), timeoutConfiguration: const TimeoutConfiguration(), ); logger.printStatus('0123456789' * 15, hangingIndent: 5); final List 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: mockStdio, platform: _kNoAnsiPlatform, ), stdio: mockStdio, outputPreferences: OutputPreferences.test(wrapText: true, wrapColumn: 40, showColor: false), timeoutConfiguration: const TimeoutConfiguration(), ); logger.printStatus('0123456789' * 15, indent: 4, hangingIndent: 5); final List 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: mockStdio, platform: FakePlatform(stdoutSupportsAnsi: true), ), stdio: mockStdio, outputPreferences: OutputPreferences.test(showColor: true), timeoutConfiguration: const TimeoutConfiguration(), ); logger.printError('Pants on fire!'); final List 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: mockStdio, platform: FakePlatform(), ), stdio: mockStdio, outputPreferences: OutputPreferences.test(showColor: true), timeoutConfiguration: const TimeoutConfiguration(), ); logger.printStatus('All good.'); final List lines = outputStdout(); expect(outputStderr().length, equals(1)); expect(outputStderr().first, isEmpty); expect(lines[0], equals('All good.')); }); testWithoutContext('Stdout printStatus handle null inputs on colored terminal', () async { final Logger logger = StdoutLogger( terminal: AnsiTerminal( stdio: mockStdio, platform: FakePlatform(), ), stdio: mockStdio, outputPreferences: OutputPreferences.test(showColor: true), timeoutConfiguration: const TimeoutConfiguration(), ); logger.printStatus( null, emphasis: null, color: null, newline: null, indent: null, ); final List lines = outputStdout(); expect(outputStderr().length, equals(1)); expect(outputStderr().first, isEmpty); expect(lines[0], equals('')); }); testWithoutContext('Stdout printStatus handle null inputs on non-color terminal', () async { final Logger logger = StdoutLogger( terminal: AnsiTerminal( stdio: mockStdio, platform: _kNoAnsiPlatform, ), stdio: mockStdio, outputPreferences: OutputPreferences.test(showColor: false), timeoutConfiguration: const TimeoutConfiguration(), ); logger.printStatus( null, emphasis: null, color: null, newline: null, indent: null, ); final List lines = outputStdout(); expect(outputStderr().length, equals(1)); expect(outputStderr().first, isEmpty); expect(lines[0], equals('')); }); testWithoutContext('Stdout startProgress on non-color terminal', () async { bool done = false; FakeAsync().run((FakeAsync time) { final Logger logger = StdoutLogger( terminal: AnsiTerminal( stdio: mockStdio, platform: _kNoAnsiPlatform, ), stdio: mockStdio, outputPreferences: OutputPreferences.test(showColor: false), timeoutConfiguration: const TimeoutConfiguration(), ); final Status status = logger.startProgress( 'Hello', progressId: null, timeout: const TimeoutConfiguration().slowOperation, 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}$')); status.stop(); expect(outputStdout().join('\n'), matches(r'^Hello {15} {5}[\d, ]{4}[\d]\.[\d]s[\n]$')); done = true; }); expect(done, isTrue); }); testWithoutContext('SummaryStatus works when canceled', () async { final SummaryStatus summaryStatus = SummaryStatus( message: 'Hello world', timeout: const TimeoutConfiguration().slowOperation, padding: 20, onFinish: () => called++, stdio: mockStdio, timeoutConfiguration: const TimeoutConfiguration(), stopwatch: FakeStopwatch(), ); summaryStatus.start(); List lines = outputStdout(); expect(lines[0], startsWith('Hello world ')); expect(lines.length, equals(1)); expect(lines[0].endsWith('\n'), isFalse); // Verify a cancel does _not_ print the time and prints a newline. summaryStatus.cancel(); lines = outputStdout(); final List matches = secondDigits.allMatches(lines[0]).toList(); expect(matches, isEmpty); expect(lines[0], endsWith(' ')); expect(called, equals(1)); expect(lines.length, equals(2)); expect(lines[1], equals('')); // Verify that stopping or canceling multiple times throws. expect(summaryStatus.cancel, throwsAssertionError); expect(summaryStatus.stop, throwsAssertionError); }); testWithoutContext('SummaryStatus works when stopped', () async { summaryStatus.start(); List lines = outputStdout(); expect(lines[0], startsWith('Hello world ')); expect(lines.length, equals(1)); // Verify a stop prints the time. summaryStatus.stop(); lines = outputStdout(); final List matches = secondDigits.allMatches(lines[0]).toList(); expect(matches, isNotNull); expect(matches, hasLength(1)); final Match match = matches.first; expect(lines[0], endsWith(match.group(0))); expect(called, equals(1)); expect(lines.length, equals(2)); expect(lines[1], equals('')); // Verify that stopping or canceling multiple times throws. expect(summaryStatus.stop, throwsAssertionError); expect(summaryStatus.cancel, throwsAssertionError); }); testWithoutContext('sequential startProgress calls with StdoutLogger', () async { final Logger logger = StdoutLogger( terminal: AnsiTerminal( stdio: mockStdio, platform: _kNoAnsiPlatform, ), stdio: mockStdio, outputPreferences: OutputPreferences.test(showColor: false), timeoutConfiguration: const TimeoutConfiguration(), ); logger.startProgress('AAA', timeout: const TimeoutConfiguration().fastOperation).stop(); logger.startProgress('BBB', timeout: const TimeoutConfiguration().fastOperation).stop(); final List 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: mockStdio, platform: _kNoAnsiPlatform, ), stdio: mockStdio, outputPreferences: OutputPreferences.test(), timeoutConfiguration: const TimeoutConfiguration(), ), stopwatchFactory: FakeStopwatchFactory(), ); logger.startProgress('AAA', timeout: const TimeoutConfiguration().fastOperation).stop(); logger.startProgress('BBB', timeout: const TimeoutConfiguration().fastOperation).stop(); expect(outputStdout(), [ 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: mockStdio, platform: _kNoAnsiPlatform, ), outputPreferences: OutputPreferences.test(), ); logger.startProgress('AAA', timeout: const TimeoutConfiguration().fastOperation).stop(); logger.startProgress('BBB', timeout: const TimeoutConfiguration().fastOperation).stop(); expect(logger.statusText, 'AAA\nBBB\n'); }); }); } class FakeStopwatch implements Stopwatch { @override bool get isRunning => _isRunning; bool _isRunning = false; @override void start() => _isRunning = true; @override void stop() => _isRunning = false; @override Duration elapsed = Duration.zero; @override int get elapsedMicroseconds => elapsed.inMicroseconds; @override int get elapsedMilliseconds => elapsed.inMilliseconds; @override int get elapsedTicks => elapsed.inMilliseconds; @override int get frequency => 1000; @override void reset() { _isRunning = false; elapsed = Duration.zero; } @override String toString() => '$runtimeType $elapsed $isRunning'; } class FakeStopwatchFactory implements StopwatchFactory { FakeStopwatchFactory([this.stopwatch]); Stopwatch stopwatch; @override Stopwatch createStopwatch() { return stopwatch ?? FakeStopwatch(); } }