• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2019 The Flutter 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';
6import 'dart:io';
7
8import 'package:meta/meta.dart';
9
10final Stopwatch _stopwatch = Stopwatch();
11
12/// A wrapper around package:test's JSON reporter.
13///
14/// This class behaves similarly to the compact reporter, but suppresses all
15/// output except for progress until the end of testing. In other words, errors,
16/// [print] calls, and skipped test messages will not be printed during the run
17/// of the suite.
18///
19/// It also processes the JSON data into a collection of [TestResult]s for any
20/// other post processing needs, e.g. sending data to analytics.
21class FlutterCompactFormatter {
22  FlutterCompactFormatter() {
23    _stopwatch.start();
24  }
25
26  /// Whether to use color escape codes in writing to stdout.
27  final bool useColor = stdout.supportsAnsiEscapes;
28
29  /// The terminal escape for green text, or the empty string if this is Windows
30  /// or not outputting to a terminal.
31  String get _green => useColor ? '\u001b[32m' : '';
32
33  /// The terminal escape for red text, or the empty string if this is Windows
34  /// or not outputting to a terminal.
35  String get _red => useColor ? '\u001b[31m' : '';
36
37  /// The terminal escape for yellow text, or the empty string if this is
38  /// Windows or not outputting to a terminal.
39  String get _yellow => useColor ? '\u001b[33m' : '';
40
41  /// The terminal escape for gray text, or the empty string if this is
42  /// Windows or not outputting to a terminal.
43  String get _gray => useColor ? '\u001b[1;30m' : '';
44
45  /// The terminal escape for bold text, or the empty string if this is
46  /// Windows or not outputting to a terminal.
47  String get _bold => useColor ? '\u001b[1m' : '';
48
49  /// The terminal escape for removing test coloring, or the empty string if
50  /// this is Windows or not outputting to a terminal.
51  String get _noColor => useColor ? '\u001b[0m' : '';
52
53  /// The termianl escape for clearing the line, or a carriage return if
54  /// this is Windows or not outputting to a termianl.
55  String get _clearLine => useColor ? '\x1b[2K\r' : '\r';
56
57  final Map<int, TestResult> _tests = <int, TestResult>{};
58
59  /// The test results from this run.
60  Iterable<TestResult> get tests => _tests.values;
61
62  /// The number of tests that were started.
63  int started = 0;
64
65  /// The number of test failures.
66  int failures = 0;
67
68  /// The number of skipped tests.
69  int skips = 0;
70
71  /// The number of successful tests.
72  int successes = 0;
73
74  /// Process a single line of JSON output from the JSON test reporter.
75  ///
76  /// Callers are responsible for splitting multiple lines before calling this
77  /// method.
78  TestResult processRawOutput(String raw) {
79    assert(raw != null);
80    // We might be getting messages from Flutter Tool about updating/building.
81    if (!raw.startsWith('{')) {
82      print(raw);
83      return null;
84    }
85    final Map<String, dynamic> decoded = json.decode(raw);
86    final TestResult originalResult = _tests[decoded['testID']];
87    switch (decoded['type']) {
88      case 'done':
89        stdout.write(_clearLine);
90        stdout.write('$_bold${_stopwatch.elapsed}$_noColor ');
91        stdout.writeln(
92            '$_green+$successes $_yellow~$skips $_red-$failures:$_bold$_gray Done.$_noColor');
93        break;
94      case 'testStart':
95        final Map<String, dynamic> testData = decoded['test'];
96        if (testData['url'] == null) {
97          started += 1;
98          stdout.write(_clearLine);
99          stdout.write('$_bold${_stopwatch.elapsed}$_noColor ');
100          stdout.write(
101              '$_green+$successes $_yellow~$skips $_red-$failures: $_gray${testData['name']}$_noColor');
102          break;
103        }
104        _tests[testData['id']] = TestResult(
105          id: testData['id'],
106          name: testData['name'],
107          line: testData['root_line'] ?? testData['line'],
108          column: testData['root_column'] ?? testData['column'],
109          path: testData['root_url'] ?? testData['url'],
110          startTime: decoded['time'],
111        );
112        break;
113      case 'testDone':
114        if (originalResult == null) {
115          break;
116        }
117        originalResult.endTime = decoded['time'];
118        if (decoded['skipped'] == true) {
119          skips += 1;
120          originalResult.status = TestStatus.skipped;
121        } else {
122          if (decoded['result'] == 'success') {
123            originalResult.status =TestStatus.succeeded;
124            successes += 1;
125          } else {
126            originalResult.status = TestStatus.failed;
127            failures += 1;
128          }
129        }
130        break;
131      case 'error':
132        final String error = decoded['error'];
133        final String stackTrace = decoded['stackTrace'];
134        if (originalResult != null) {
135          originalResult.errorMessage = error;
136          originalResult.stackTrace = stackTrace;
137        } else {
138          if (error != null)
139            stderr.writeln(error);
140          if (stackTrace != null)
141            stderr.writeln(stackTrace);
142        }
143        break;
144      case 'print':
145        if (originalResult != null) {
146          originalResult.messages.add(decoded['message']);
147        }
148        break;
149      case 'group':
150      case 'allSuites':
151      case 'start':
152      case 'suite':
153      default:
154        break;
155    }
156    return originalResult;
157  }
158
159  /// Print summary of test results.
160  void finish() {
161    final List<String> skipped = <String>[];
162    final List<String> failed = <String>[];
163    for (TestResult result in _tests.values) {
164      switch (result.status) {
165        case TestStatus.started:
166          failed.add('${_red}Unexpectedly failed to complete a test!');
167          failed.add(result.toString() + _noColor);
168          break;
169        case TestStatus.skipped:
170          skipped.add(
171              '${_yellow}Skipped ${result.name} (${result.pathLineColumn}).$_noColor');
172          break;
173        case TestStatus.failed:
174          failed.addAll(<String>[
175            '$_bold${_red}Failed ${result.name} (${result.pathLineColumn}):',
176            result.errorMessage,
177            _noColor + _red,
178            result.stackTrace,
179          ]);
180          failed.addAll(result.messages);
181          failed.add(_noColor);
182          break;
183        case TestStatus.succeeded:
184          break;
185      }
186    }
187    skipped.forEach(print);
188    failed.forEach(print);
189    if (failed.isEmpty) {
190      print('${_green}Completed, $successes test(s) passing ($skips skipped).$_noColor');
191    } else {
192      print('$_gray$failures test(s) failed.$_noColor');
193    }
194  }
195}
196
197/// The state of a test received from the JSON reporter.
198enum TestStatus {
199  /// Test execution has started.
200  started,
201  /// Test completed successfully.
202  succeeded,
203  /// Test failed.
204  failed,
205  /// Test was skipped.
206  skipped,
207}
208
209/// The detailed status of a test run.
210class TestResult {
211  TestResult({
212    @required this.id,
213    @required this.name,
214    @required this.line,
215    @required this.column,
216    @required this.path,
217    @required this.startTime,
218    this.status = TestStatus.started,
219  })  : assert(id != null),
220        assert(name != null),
221        assert(line != null),
222        assert(column != null),
223        assert(path != null),
224        assert(startTime != null),
225        assert(status != null),
226        messages = <String>[];
227
228  /// The state of the test.
229  TestStatus status;
230
231  /// The internal ID of the test used by the JSON reporter.
232  final int id;
233
234  /// The name of the test, specified via the `test` method.
235  final String name;
236
237  /// The line number from the original file.
238  final int line;
239
240  /// The column from the original file.
241  final int column;
242
243  /// The path of the original test file.
244  final String path;
245
246  /// A friendly print out of the [path], [line], and [column] of the test.
247  String get pathLineColumn => '$path:$line:$column';
248
249  /// The start time of the test, in milliseconds relative to suite startup.
250  final int startTime;
251
252  /// The stdout of the test.
253  final List<String> messages;
254
255  /// The error message from the test, from an `expect`, an [Exception] or
256  /// [Error].
257  String errorMessage;
258
259  /// The stacktrace from a test failure.
260  String stackTrace;
261
262  /// The time, in milliseconds relative to suite startup, that the test ended.
263  int endTime;
264
265  /// The total time, in milliseconds, that the test took.
266  int get totalTime => (endTime ?? _stopwatch.elapsedMilliseconds) - startTime;
267
268  @override
269  String toString() => '{$runtimeType: {$id, $name, ${totalTime}ms, $pathLineColumn}}';
270}
271