• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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