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