1// Copyright 2017 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'dart:convert' show jsonEncode; 6 7import 'package:flutter_tools/src/base/context.dart'; 8import 'package:flutter_tools/src/base/io.dart'; 9import 'package:flutter_tools/src/base/logger.dart'; 10import 'package:flutter_tools/src/base/platform.dart'; 11import 'package:flutter_tools/src/base/terminal.dart'; 12import 'package:quiver/testing/async.dart'; 13 14import '../../src/common.dart'; 15import '../../src/context.dart'; 16import '../../src/mocks.dart'; 17 18final Generator _kNoAnsiPlatform = () => FakePlatform.fromPlatform(const LocalPlatform())..stdoutSupportsAnsi = false; 19 20void main() { 21 final String red = RegExp.escape(AnsiTerminal.red); 22 final String bold = RegExp.escape(AnsiTerminal.bold); 23 final String resetBold = RegExp.escape(AnsiTerminal.resetBold); 24 final String resetColor = RegExp.escape(AnsiTerminal.resetColor); 25 26 group('AppContext', () { 27 testUsingContext('error', () async { 28 final BufferLogger mockLogger = BufferLogger(); 29 final VerboseLogger verboseLogger = VerboseLogger(mockLogger); 30 31 verboseLogger.printStatus('Hey Hey Hey Hey'); 32 verboseLogger.printTrace('Oooh, I do I do I do'); 33 verboseLogger.printError('Helpless!'); 34 35 expect(mockLogger.statusText, matches(r'^\[ (?: {0,2}\+[0-9]{1,4} ms| )\] Hey Hey Hey Hey\n' 36 r'\[ (?: {0,2}\+[0-9]{1,4} ms| )\] Oooh, I do I do I do\n$')); 37 expect(mockLogger.traceText, ''); 38 expect(mockLogger.errorText, matches( r'^\[ (?: {0,2}\+[0-9]{1,4} ms| )\] Helpless!\n$')); 39 }, overrides: <Type, Generator>{ 40 OutputPreferences: () => OutputPreferences(showColor: false), 41 Platform: _kNoAnsiPlatform, 42 }); 43 44 testUsingContext('ANSI colored errors', () async { 45 final BufferLogger mockLogger = BufferLogger(); 46 final VerboseLogger verboseLogger = VerboseLogger(mockLogger); 47 48 verboseLogger.printStatus('Hey Hey Hey Hey'); 49 verboseLogger.printTrace('Oooh, I do I do I do'); 50 verboseLogger.printError('Helpless!'); 51 52 expect( 53 mockLogger.statusText, 54 matches(r'^\[ (?: {0,2}\+[0-9]{1,4} ms| )\] ' '${bold}Hey Hey Hey Hey$resetBold' 55 r'\n\[ (?: {0,2}\+[0-9]{1,4} ms| )\] Oooh, I do I do I do\n$')); 56 expect(mockLogger.traceText, ''); 57 expect( 58 mockLogger.errorText, 59 matches('^$red' r'\[ (?: {0,2}\+[0-9]{1,4} ms| )\] ' '${bold}Helpless!$resetBold$resetColor' r'\n$')); 60 }, overrides: <Type, Generator>{ 61 OutputPreferences: () => OutputPreferences(showColor: true), 62 Platform: () => FakePlatform()..stdoutSupportsAnsi = true, 63 }); 64 }); 65 66 group('Spinners', () { 67 MockStdio mockStdio; 68 FakeStopwatch mockStopwatch; 69 int called; 70 const List<String> testPlatforms = <String>['linux', 'macos', 'windows', 'fuchsia']; 71 final RegExp secondDigits = RegExp(r'[0-9,.]*[0-9]m?s'); 72 73 AnsiStatus _createAnsiStatus() { 74 mockStopwatch = FakeStopwatch(); 75 return AnsiStatus( 76 message: 'Hello world', 77 timeout: const Duration(seconds: 2), 78 padding: 20, 79 onFinish: () => called += 1, 80 ); 81 } 82 83 setUp(() { 84 mockStdio = MockStdio(); 85 called = 0; 86 }); 87 88 List<String> outputStdout() => mockStdio.writtenToStdout.join('').split('\n'); 89 List<String> outputStderr() => mockStdio.writtenToStderr.join('').split('\n'); 90 91 void doWhileAsync(FakeAsync time, bool doThis()) { 92 do { 93 time.elapse(const Duration(milliseconds: 1)); 94 } while (doThis()); 95 } 96 97 for (String testOs in testPlatforms) { 98 testUsingContext('AnsiSpinner works for $testOs (1)', () async { 99 bool done = false; 100 FakeAsync().run((FakeAsync time) { 101 final AnsiSpinner ansiSpinner = AnsiSpinner( 102 timeout: const Duration(hours: 10), 103 )..start(); 104 doWhileAsync(time, () => ansiSpinner.ticks < 10); 105 List<String> lines = outputStdout(); 106 expect(lines[0], startsWith( 107 platform.isWindows 108 ? ' \b\\\b|\b/\b-\b\\\b|\b/\b-' 109 : ' \b⣽\b⣻\b⢿\b⡿\b⣟\b⣯\b⣷\b⣾\b⣽\b⣻' 110 ), 111 ); 112 expect(lines[0].endsWith('\n'), isFalse); 113 expect(lines.length, equals(1)); 114 ansiSpinner.stop(); 115 lines = outputStdout(); 116 expect(lines[0], endsWith('\b \b')); 117 expect(lines.length, equals(1)); 118 119 // Verify that stopping or canceling multiple times throws. 120 expect(() { 121 ansiSpinner.stop(); 122 }, throwsA(isInstanceOf<AssertionError>())); 123 expect(() { 124 ansiSpinner.cancel(); 125 }, throwsA(isInstanceOf<AssertionError>())); 126 done = true; 127 }); 128 expect(done, isTrue); 129 }, overrides: <Type, Generator>{ 130 Platform: () => FakePlatform(operatingSystem: testOs), 131 Stdio: () => mockStdio, 132 }); 133 134 testUsingContext('AnsiSpinner works for $testOs (2)', () async { 135 bool done = false; 136 mockStopwatch = FakeStopwatch(); 137 FakeAsync().run((FakeAsync time) { 138 final AnsiSpinner ansiSpinner = AnsiSpinner( 139 timeout: const Duration(seconds: 2), 140 )..start(); 141 mockStopwatch.elapsed = const Duration(seconds: 1); 142 doWhileAsync(time, () => ansiSpinner.ticks < 10); // one second 143 expect(ansiSpinner.seemsSlow, isFalse); 144 expect(outputStdout().join('\n'), isNot(contains('This is taking an unexpectedly long time.'))); 145 mockStopwatch.elapsed = const Duration(seconds: 3); 146 doWhileAsync(time, () => ansiSpinner.ticks < 30); // three seconds 147 expect(ansiSpinner.seemsSlow, isTrue); 148 // Check the 2nd line to verify there's a newline before the warning 149 expect(outputStdout()[1], contains('This is taking an unexpectedly long time.')); 150 ansiSpinner.stop(); 151 expect(outputStdout().join('\n'), isNot(contains('(!)'))); 152 done = true; 153 }); 154 expect(done, isTrue); 155 }, overrides: <Type, Generator>{ 156 Platform: () => FakePlatform(operatingSystem: testOs), 157 Stdio: () => mockStdio, 158 Stopwatch: () => mockStopwatch, 159 }); 160 161 testUsingContext('Stdout startProgress on colored terminal for $testOs', () async { 162 bool done = false; 163 FakeAsync().run((FakeAsync time) { 164 final Logger logger = context.get<Logger>(); 165 final Status status = logger.startProgress( 166 'Hello', 167 progressId: null, 168 timeout: timeoutConfiguration.slowOperation, 169 progressIndicatorPadding: 20, // this minus the "Hello" equals the 15 below. 170 ); 171 expect(outputStderr().length, equals(1)); 172 expect(outputStderr().first, isEmpty); 173 // the 5 below is the margin that is always included between the message and the time. 174 expect(outputStdout().join('\n'), matches(platform.isWindows ? r'^Hello {15} {5} {8}[\b]{8} {7}\\$' : 175 r'^Hello {15} {5} {8}[\b]{8} {7}⣽$')); 176 status.stop(); 177 expect(outputStdout().join('\n'), matches(platform.isWindows ? r'^Hello {15} {5} {8}[\b]{8} {7}\\[\b]{8} {8}[\b]{8}[\d, ]{4}[\d]\.[\d]s[\n]$' : 178 r'^Hello {15} {5} {8}[\b]{8} {7}⣽[\b]{8} {8}[\b]{8}[\d, ]{4}[\d]\.[\d]s[\n]$')); 179 done = true; 180 }); 181 expect(done, isTrue); 182 }, overrides: <Type, Generator>{ 183 Logger: () => StdoutLogger(), 184 OutputPreferences: () => OutputPreferences(showColor: true), 185 Platform: () => FakePlatform(operatingSystem: testOs)..stdoutSupportsAnsi = true, 186 Stdio: () => mockStdio, 187 }); 188 189 testUsingContext('Stdout startProgress on colored terminal pauses on $testOs', () async { 190 bool done = false; 191 FakeAsync().run((FakeAsync time) { 192 final Logger logger = context.get<Logger>(); 193 final Status status = logger.startProgress( 194 'Knock Knock, Who\'s There', 195 timeout: const Duration(days: 10), 196 progressIndicatorPadding: 10, 197 ); 198 logger.printStatus('Rude Interrupting Cow'); 199 status.stop(); 200 final String a = platform.isWindows ? '\\' : '⣽'; 201 final String b = platform.isWindows ? '|' : '⣻'; 202 expect( 203 outputStdout().join('\n'), 204 'Knock Knock, Who\'s There ' // initial message 205 ' ' // placeholder so that spinner can backspace on its first tick 206 '\b\b\b\b\b\b\b\b $a' // first tick 207 '\b\b\b\b\b\b\b\b ' // clearing the spinner 208 '\b\b\b\b\b\b\b\b' // clearing the clearing of the spinner 209 '\b\b\b\b\b\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 210 '\b\b\b\b\b\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 211 'Rude Interrupting Cow\n' // message 212 'Knock Knock, Who\'s There ' // message restoration 213 ' ' // placeholder so that spinner can backspace on its second tick 214 '\b\b\b\b\b\b\b\b $b' // second tick 215 '\b\b\b\b\b\b\b\b ' // clearing the spinner to put the time 216 '\b\b\b\b\b\b\b\b' // clearing the clearing of the spinner 217 ' 0.0s\n', // replacing it with the time 218 ); 219 done = true; 220 }); 221 expect(done, isTrue); 222 }, overrides: <Type, Generator>{ 223 Logger: () => StdoutLogger(), 224 OutputPreferences: () => OutputPreferences(showColor: true), 225 Platform: () => FakePlatform(operatingSystem: testOs)..stdoutSupportsAnsi = true, 226 Stdio: () => mockStdio, 227 }); 228 229 testUsingContext('AnsiStatus works for $testOs', () { 230 final AnsiStatus ansiStatus = _createAnsiStatus(); 231 bool done = false; 232 FakeAsync().run((FakeAsync time) { 233 ansiStatus.start(); 234 mockStopwatch.elapsed = const Duration(seconds: 1); 235 doWhileAsync(time, () => ansiStatus.ticks < 10); // one second 236 expect(ansiStatus.seemsSlow, isFalse); 237 expect(outputStdout().join('\n'), isNot(contains('This is taking an unexpectedly long time.'))); 238 expect(outputStdout().join('\n'), isNot(contains('(!)'))); 239 mockStopwatch.elapsed = const Duration(seconds: 3); 240 doWhileAsync(time, () => ansiStatus.ticks < 30); // three seconds 241 expect(ansiStatus.seemsSlow, isTrue); 242 expect(outputStdout().join('\n'), contains('This is taking an unexpectedly long time.')); 243 244 // Test that the number of '\b' is correct. 245 for (String line in outputStdout()) { 246 int currLength = 0; 247 for (int i = 0; i < line.length; i += 1) { 248 currLength += line[i] == '\b' ? -1 : 1; 249 expect(currLength, isNonNegative, reason: 'The following line has overflow backtraces:\n' + jsonEncode(line)); 250 } 251 } 252 253 ansiStatus.stop(); 254 expect(outputStdout().join('\n'), contains('(!)')); 255 done = true; 256 }); 257 expect(done, isTrue); 258 }, overrides: <Type, Generator>{ 259 Platform: () => FakePlatform(operatingSystem: testOs), 260 Stdio: () => mockStdio, 261 Stopwatch: () => mockStopwatch, 262 }); 263 264 testUsingContext('AnsiStatus works when canceled for $testOs', () async { 265 final AnsiStatus ansiStatus = _createAnsiStatus(); 266 bool done = false; 267 FakeAsync().run((FakeAsync time) { 268 ansiStatus.start(); 269 mockStopwatch.elapsed = const Duration(seconds: 1); 270 doWhileAsync(time, () => ansiStatus.ticks < 10); 271 List<String> lines = outputStdout(); 272 expect(lines[0], startsWith(platform.isWindows 273 ? '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 |' 274 : '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 ⣻')); 275 expect(lines.length, equals(1)); 276 expect(lines[0].endsWith('\n'), isFalse); 277 278 // Verify a cancel does _not_ print the time and prints a newline. 279 ansiStatus.cancel(); 280 lines = outputStdout(); 281 final List<Match> matches = secondDigits.allMatches(lines[0]).toList(); 282 expect(matches, isEmpty); 283 final String x = platform.isWindows ? '|' : '⣻'; 284 expect(lines[0], endsWith('$x\b\b\b\b\b\b\b\b \b\b\b\b\b\b\b\b')); 285 expect(called, equals(1)); 286 expect(lines.length, equals(2)); 287 expect(lines[1], equals('')); 288 289 // Verify that stopping or canceling multiple times throws. 290 expect(() { ansiStatus.cancel(); }, throwsA(isInstanceOf<AssertionError>())); 291 expect(() { ansiStatus.stop(); }, throwsA(isInstanceOf<AssertionError>())); 292 done = true; 293 }); 294 expect(done, isTrue); 295 }, overrides: <Type, Generator>{ 296 Platform: () => FakePlatform(operatingSystem: testOs), 297 Stdio: () => mockStdio, 298 Stopwatch: () => mockStopwatch, 299 }); 300 301 testUsingContext('AnsiStatus works when stopped for $testOs', () async { 302 final AnsiStatus ansiStatus = _createAnsiStatus(); 303 bool done = false; 304 FakeAsync().run((FakeAsync time) { 305 ansiStatus.start(); 306 mockStopwatch.elapsed = const Duration(seconds: 1); 307 doWhileAsync(time, () => ansiStatus.ticks < 10); 308 List<String> lines = outputStdout(); 309 expect(lines, hasLength(1)); 310 expect(lines[0], 311 platform.isWindows 312 ? '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 |' 313 : '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 ⣻', 314 ); 315 316 // Verify a stop prints the time. 317 ansiStatus.stop(); 318 lines = outputStdout(); 319 expect(lines, hasLength(2)); 320 expect(lines[0], matches( 321 platform.isWindows 322 ? 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$' 323 : 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$' 324 )); 325 expect(lines[1], isEmpty); 326 final List<Match> times = secondDigits.allMatches(lines[0]).toList(); 327 expect(times, isNotNull); 328 expect(times, hasLength(1)); 329 final Match match = times.single; 330 expect(lines[0], endsWith(match.group(0))); 331 expect(called, equals(1)); 332 expect(lines.length, equals(2)); 333 expect(lines[1], equals('')); 334 335 // Verify that stopping or canceling multiple times throws. 336 expect(() { ansiStatus.stop(); }, throwsA(isInstanceOf<AssertionError>())); 337 expect(() { ansiStatus.cancel(); }, throwsA(isInstanceOf<AssertionError>())); 338 done = true; 339 }); 340 expect(done, isTrue); 341 }, overrides: <Type, Generator>{ 342 Platform: () => FakePlatform(operatingSystem: testOs), 343 Stdio: () => mockStdio, 344 Stopwatch: () => mockStopwatch, 345 }); 346 } 347 }); 348 group('Output format', () { 349 MockStdio mockStdio; 350 SummaryStatus summaryStatus; 351 int called; 352 final RegExp secondDigits = RegExp(r'[^\b]\b\b\b\b\b[0-9]+[.][0-9]+(?:s|ms)'); 353 354 setUp(() { 355 mockStdio = MockStdio(); 356 called = 0; 357 summaryStatus = SummaryStatus( 358 message: 'Hello world', 359 timeout: timeoutConfiguration.slowOperation, 360 padding: 20, 361 onFinish: () => called++, 362 ); 363 }); 364 365 List<String> outputStdout() => mockStdio.writtenToStdout.join('').split('\n'); 366 List<String> outputStderr() => mockStdio.writtenToStderr.join('').split('\n'); 367 368 testUsingContext('Error logs are wrapped', () async { 369 final Logger logger = context.get<Logger>(); 370 logger.printError('0123456789' * 15); 371 final List<String> lines = outputStderr(); 372 expect(outputStdout().length, equals(1)); 373 expect(outputStdout().first, isEmpty); 374 expect(lines[0], equals('0123456789' * 4)); 375 expect(lines[1], equals('0123456789' * 4)); 376 expect(lines[2], equals('0123456789' * 4)); 377 expect(lines[3], equals('0123456789' * 3)); 378 }, overrides: <Type, Generator>{ 379 Logger: () => StdoutLogger(), 380 OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 40, showColor: false), 381 Stdio: () => mockStdio, 382 Platform: _kNoAnsiPlatform, 383 }); 384 385 testUsingContext('Error logs are wrapped and can be indented.', () async { 386 final Logger logger = context.get<Logger>(); 387 logger.printError('0123456789' * 15, indent: 5); 388 final List<String> lines = outputStderr(); 389 expect(outputStdout().length, equals(1)); 390 expect(outputStdout().first, isEmpty); 391 expect(lines.length, equals(6)); 392 expect(lines[0], equals(' 01234567890123456789012345678901234')); 393 expect(lines[1], equals(' 56789012345678901234567890123456789')); 394 expect(lines[2], equals(' 01234567890123456789012345678901234')); 395 expect(lines[3], equals(' 56789012345678901234567890123456789')); 396 expect(lines[4], equals(' 0123456789')); 397 expect(lines[5], isEmpty); 398 }, overrides: <Type, Generator>{ 399 Logger: () => StdoutLogger(), 400 OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 40, showColor: false), 401 Stdio: () => mockStdio, 402 Platform: _kNoAnsiPlatform, 403 }); 404 405 testUsingContext('Error logs are wrapped and can have hanging indent.', () async { 406 final Logger logger = context.get<Logger>(); 407 logger.printError('0123456789' * 15, hangingIndent: 5); 408 final List<String> lines = outputStderr(); 409 expect(outputStdout().length, equals(1)); 410 expect(outputStdout().first, isEmpty); 411 expect(lines.length, equals(6)); 412 expect(lines[0], equals('0123456789012345678901234567890123456789')); 413 expect(lines[1], equals(' 01234567890123456789012345678901234')); 414 expect(lines[2], equals(' 56789012345678901234567890123456789')); 415 expect(lines[3], equals(' 01234567890123456789012345678901234')); 416 expect(lines[4], equals(' 56789')); 417 expect(lines[5], isEmpty); 418 }, overrides: <Type, Generator>{ 419 Logger: () => StdoutLogger(), 420 OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 40, showColor: false), 421 Stdio: () => mockStdio, 422 Platform: _kNoAnsiPlatform, 423 }); 424 425 testUsingContext('Error logs are wrapped, indented, and can have hanging indent.', () async { 426 final Logger logger = context.get<Logger>(); 427 logger.printError('0123456789' * 15, indent: 4, hangingIndent: 5); 428 final List<String> lines = outputStderr(); 429 expect(outputStdout().length, equals(1)); 430 expect(outputStdout().first, isEmpty); 431 expect(lines.length, equals(6)); 432 expect(lines[0], equals(' 012345678901234567890123456789012345')); 433 expect(lines[1], equals(' 6789012345678901234567890123456')); 434 expect(lines[2], equals(' 7890123456789012345678901234567')); 435 expect(lines[3], equals(' 8901234567890123456789012345678')); 436 expect(lines[4], equals(' 901234567890123456789')); 437 expect(lines[5], isEmpty); 438 }, overrides: <Type, Generator>{ 439 Logger: () => StdoutLogger(), 440 OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 40, showColor: false), 441 Stdio: () => mockStdio, 442 Platform: _kNoAnsiPlatform, 443 }); 444 445 testUsingContext('Stdout logs are wrapped', () async { 446 final Logger logger = context.get<Logger>(); 447 logger.printStatus('0123456789' * 15); 448 final List<String> lines = outputStdout(); 449 expect(outputStderr().length, equals(1)); 450 expect(outputStderr().first, isEmpty); 451 expect(lines[0], equals('0123456789' * 4)); 452 expect(lines[1], equals('0123456789' * 4)); 453 expect(lines[2], equals('0123456789' * 4)); 454 expect(lines[3], equals('0123456789' * 3)); 455 }, overrides: <Type, Generator>{ 456 Logger: () => StdoutLogger(), 457 OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 40, showColor: false), 458 Stdio: () => mockStdio, 459 Platform: _kNoAnsiPlatform, 460 }); 461 462 testUsingContext('Stdout logs are wrapped and can be indented.', () async { 463 final Logger logger = context.get<Logger>(); 464 logger.printStatus('0123456789' * 15, indent: 5); 465 final List<String> lines = outputStdout(); 466 expect(outputStderr().length, equals(1)); 467 expect(outputStderr().first, isEmpty); 468 expect(lines.length, equals(6)); 469 expect(lines[0], equals(' 01234567890123456789012345678901234')); 470 expect(lines[1], equals(' 56789012345678901234567890123456789')); 471 expect(lines[2], equals(' 01234567890123456789012345678901234')); 472 expect(lines[3], equals(' 56789012345678901234567890123456789')); 473 expect(lines[4], equals(' 0123456789')); 474 expect(lines[5], isEmpty); 475 }, overrides: <Type, Generator>{ 476 Logger: () => StdoutLogger(), 477 OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 40, showColor: false), 478 Stdio: () => mockStdio, 479 Platform: _kNoAnsiPlatform, 480 }); 481 482 testUsingContext('Stdout logs are wrapped and can have hanging indent.', () async { 483 final Logger logger = context.get<Logger>(); 484 logger.printStatus('0123456789' * 15, hangingIndent: 5); 485 final List<String> lines = outputStdout(); 486 expect(outputStderr().length, equals(1)); 487 expect(outputStderr().first, isEmpty); 488 expect(lines.length, equals(6)); 489 expect(lines[0], equals('0123456789012345678901234567890123456789')); 490 expect(lines[1], equals(' 01234567890123456789012345678901234')); 491 expect(lines[2], equals(' 56789012345678901234567890123456789')); 492 expect(lines[3], equals(' 01234567890123456789012345678901234')); 493 expect(lines[4], equals(' 56789')); 494 expect(lines[5], isEmpty); 495 }, overrides: <Type, Generator>{ 496 Logger: () => StdoutLogger(), 497 OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 40, showColor: false), 498 Stdio: () => mockStdio, 499 Platform: _kNoAnsiPlatform, 500 }); 501 502 testUsingContext('Stdout logs are wrapped, indented, and can have hanging indent.', () async { 503 final Logger logger = context.get<Logger>(); 504 logger.printStatus('0123456789' * 15, indent: 4, hangingIndent: 5); 505 final List<String> lines = outputStdout(); 506 expect(outputStderr().length, equals(1)); 507 expect(outputStderr().first, isEmpty); 508 expect(lines.length, equals(6)); 509 expect(lines[0], equals(' 012345678901234567890123456789012345')); 510 expect(lines[1], equals(' 6789012345678901234567890123456')); 511 expect(lines[2], equals(' 7890123456789012345678901234567')); 512 expect(lines[3], equals(' 8901234567890123456789012345678')); 513 expect(lines[4], equals(' 901234567890123456789')); 514 expect(lines[5], isEmpty); 515 }, overrides: <Type, Generator>{ 516 Logger: () => StdoutLogger(), 517 OutputPreferences: () => OutputPreferences(wrapText: true, wrapColumn: 40, showColor: false), 518 Stdio: () => mockStdio, 519 Platform: _kNoAnsiPlatform, 520 }); 521 522 testUsingContext('Error logs are red', () async { 523 final Logger logger = context.get<Logger>(); 524 logger.printError('Pants on fire!'); 525 final List<String> lines = outputStderr(); 526 expect(outputStdout().length, equals(1)); 527 expect(outputStdout().first, isEmpty); 528 expect(lines[0], equals('${AnsiTerminal.red}Pants on fire!${AnsiTerminal.resetColor}')); 529 }, overrides: <Type, Generator>{ 530 Logger: () => StdoutLogger(), 531 OutputPreferences: () => OutputPreferences(showColor: true), 532 Platform: () => FakePlatform()..stdoutSupportsAnsi = true, 533 Stdio: () => mockStdio, 534 }); 535 536 testUsingContext('Stdout logs are not colored', () async { 537 final Logger logger = context.get<Logger>(); 538 logger.printStatus('All good.'); 539 final List<String> lines = outputStdout(); 540 expect(outputStderr().length, equals(1)); 541 expect(outputStderr().first, isEmpty); 542 expect(lines[0], equals('All good.')); 543 }, overrides: <Type, Generator>{ 544 Logger: () => StdoutLogger(), 545 OutputPreferences: () => OutputPreferences(showColor: true), 546 Stdio: () => mockStdio, 547 }); 548 549 testUsingContext('Stdout printStatus handle null inputs on colored terminal', () async { 550 final Logger logger = context.get<Logger>(); 551 logger.printStatus( 552 null, 553 emphasis: null, 554 color: null, 555 newline: null, 556 indent: null, 557 ); 558 final List<String> lines = outputStdout(); 559 expect(outputStderr().length, equals(1)); 560 expect(outputStderr().first, isEmpty); 561 expect(lines[0], equals('')); 562 }, overrides: <Type, Generator>{ 563 Logger: () => StdoutLogger(), 564 OutputPreferences: () => OutputPreferences(showColor: true), 565 Stdio: () => mockStdio, 566 }); 567 568 testUsingContext('Stdout printStatus handle null inputs on non-color terminal', () async { 569 final Logger logger = context.get<Logger>(); 570 logger.printStatus( 571 null, 572 emphasis: null, 573 color: null, 574 newline: null, 575 indent: null, 576 ); 577 final List<String> lines = outputStdout(); 578 expect(outputStderr().length, equals(1)); 579 expect(outputStderr().first, isEmpty); 580 expect(lines[0], equals('')); 581 }, overrides: <Type, Generator>{ 582 Logger: () => StdoutLogger(), 583 OutputPreferences: () => OutputPreferences(showColor: false), 584 Stdio: () => mockStdio, 585 Platform: _kNoAnsiPlatform, 586 }); 587 588 testUsingContext('Stdout startProgress on non-color terminal', () async { 589 bool done = false; 590 FakeAsync().run((FakeAsync time) { 591 final Logger logger = context.get<Logger>(); 592 final Status status = logger.startProgress( 593 'Hello', 594 progressId: null, 595 timeout: timeoutConfiguration.slowOperation, 596 progressIndicatorPadding: 20, // this minus the "Hello" equals the 15 below. 597 ); 598 expect(outputStderr().length, equals(1)); 599 expect(outputStderr().first, isEmpty); 600 // the 5 below is the margin that is always included between the message and the time. 601 expect(outputStdout().join('\n'), matches(platform.isWindows ? r'^Hello {15} {5}$' : 602 r'^Hello {15} {5}$')); 603 status.stop(); 604 expect(outputStdout().join('\n'), matches(platform.isWindows ? r'^Hello {15} {5}[\d, ]{4}[\d]\.[\d]s[\n]$' : 605 r'^Hello {15} {5}[\d, ]{4}[\d]\.[\d]s[\n]$')); 606 done = true; 607 }); 608 expect(done, isTrue); 609 }, overrides: <Type, Generator>{ 610 Logger: () => StdoutLogger(), 611 OutputPreferences: () => OutputPreferences(showColor: false), 612 Stdio: () => mockStdio, 613 Platform: _kNoAnsiPlatform, 614 }); 615 616 testUsingContext('SummaryStatus works when canceled', () async { 617 summaryStatus.start(); 618 List<String> lines = outputStdout(); 619 expect(lines[0], startsWith('Hello world ')); 620 expect(lines.length, equals(1)); 621 expect(lines[0].endsWith('\n'), isFalse); 622 623 // Verify a cancel does _not_ print the time and prints a newline. 624 summaryStatus.cancel(); 625 lines = outputStdout(); 626 final List<Match> matches = secondDigits.allMatches(lines[0]).toList(); 627 expect(matches, isEmpty); 628 expect(lines[0], endsWith(' ')); 629 expect(called, equals(1)); 630 expect(lines.length, equals(2)); 631 expect(lines[1], equals('')); 632 633 // Verify that stopping or canceling multiple times throws. 634 expect(() { summaryStatus.cancel(); }, throwsA(isInstanceOf<AssertionError>())); 635 expect(() { summaryStatus.stop(); }, throwsA(isInstanceOf<AssertionError>())); 636 }, overrides: <Type, Generator>{Stdio: () => mockStdio, Platform: _kNoAnsiPlatform}); 637 638 testUsingContext('SummaryStatus works when stopped', () async { 639 summaryStatus.start(); 640 List<String> lines = outputStdout(); 641 expect(lines[0], startsWith('Hello world ')); 642 expect(lines.length, equals(1)); 643 644 // Verify a stop prints the time. 645 summaryStatus.stop(); 646 lines = outputStdout(); 647 final List<Match> matches = secondDigits.allMatches(lines[0]).toList(); 648 expect(matches, isNotNull); 649 expect(matches, hasLength(1)); 650 final Match match = matches.first; 651 expect(lines[0], endsWith(match.group(0))); 652 expect(called, equals(1)); 653 expect(lines.length, equals(2)); 654 expect(lines[1], equals('')); 655 656 // Verify that stopping or canceling multiple times throws. 657 expect(() { summaryStatus.stop(); }, throwsA(isInstanceOf<AssertionError>())); 658 expect(() { summaryStatus.cancel(); }, throwsA(isInstanceOf<AssertionError>())); 659 }, overrides: <Type, Generator>{Stdio: () => mockStdio, Platform: _kNoAnsiPlatform}); 660 661 testUsingContext('sequential startProgress calls with StdoutLogger', () async { 662 final Logger logger = context.get<Logger>(); 663 logger.startProgress('AAA', timeout: timeoutConfiguration.fastOperation)..stop(); 664 logger.startProgress('BBB', timeout: timeoutConfiguration.fastOperation)..stop(); 665 final List<String> output = outputStdout(); 666 expect(output.length, equals(3)); 667 // There's 61 spaces at the start: 59 (padding default) - 3 (length of AAA) + 5 (margin). 668 // Then there's a left-padded "0ms" 8 characters wide, so 5 spaces then "0ms" 669 // (except sometimes it's randomly slow so we handle up to "99,999ms"). 670 expect(output[0], matches(RegExp(r'AAA[ ]{61}[\d, ]{5}[\d]ms'))); 671 expect(output[1], matches(RegExp(r'BBB[ ]{61}[\d, ]{5}[\d]ms'))); 672 }, overrides: <Type, Generator>{ 673 Logger: () => StdoutLogger(), 674 OutputPreferences: () => OutputPreferences(showColor: false), 675 Stdio: () => mockStdio, 676 Platform: _kNoAnsiPlatform, 677 }); 678 679 testUsingContext('sequential startProgress calls with VerboseLogger and StdoutLogger', () async { 680 final Logger logger = context.get<Logger>(); 681 logger.startProgress('AAA', timeout: timeoutConfiguration.fastOperation)..stop(); 682 logger.startProgress('BBB', timeout: timeoutConfiguration.fastOperation)..stop(); 683 expect(outputStdout(), <Matcher>[ 684 matches(r'^\[ (?: {0,2}\+[0-9]{1,4} ms| )\] AAA$'), 685 matches(r'^\[ (?: {0,2}\+[0-9]{1,4} ms| )\] AAA \(completed.*\)$'), 686 matches(r'^\[ (?: {0,2}\+[0-9]{1,4} ms| )\] BBB$'), 687 matches(r'^\[ (?: {0,2}\+[0-9]{1,4} ms| )\] BBB \(completed.*\)$'), 688 matches(r'^$'), 689 ]); 690 }, overrides: <Type, Generator>{ 691 Logger: () => VerboseLogger(StdoutLogger()), 692 Stdio: () => mockStdio, 693 Platform: _kNoAnsiPlatform, 694 }); 695 696 testUsingContext('sequential startProgress calls with BufferLogger', () async { 697 final BufferLogger logger = context.get<Logger>(); 698 logger.startProgress('AAA', timeout: timeoutConfiguration.fastOperation)..stop(); 699 logger.startProgress('BBB', timeout: timeoutConfiguration.fastOperation)..stop(); 700 expect(logger.statusText, 'AAA\nBBB\n'); 701 }, overrides: <Type, Generator>{ 702 Logger: () => BufferLogger(), 703 Platform: _kNoAnsiPlatform, 704 }); 705 }); 706} 707 708class FakeStopwatch implements Stopwatch { 709 @override 710 bool get isRunning => _isRunning; 711 bool _isRunning = false; 712 713 @override 714 void start() => _isRunning = true; 715 716 @override 717 void stop() => _isRunning = false; 718 719 @override 720 Duration elapsed = Duration.zero; 721 722 @override 723 int get elapsedMicroseconds => elapsed.inMicroseconds; 724 725 @override 726 int get elapsedMilliseconds => elapsed.inMilliseconds; 727 728 @override 729 int get elapsedTicks => elapsed.inMilliseconds; 730 731 @override 732 int get frequency => 1000; 733 734 @override 735 void reset() { 736 _isRunning = false; 737 elapsed = Duration.zero; 738 } 739 740 @override 741 String toString() => '$runtimeType $elapsed $isRunning'; 742} 743