| // Copyright 2019 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:convert'; |
| import 'dart:io'; |
| |
| import 'package:meta/meta.dart'; |
| |
| final Stopwatch _stopwatch = Stopwatch(); |
| |
| /// A wrapper around package:test's JSON reporter. |
| /// |
| /// This class behaves similarly to the compact reporter, but suppresses all |
| /// output except for progress until the end of testing. In other words, errors, |
| /// [print] calls, and skipped test messages will not be printed during the run |
| /// of the suite. |
| /// |
| /// It also processes the JSON data into a collection of [TestResult]s for any |
| /// other post processing needs, e.g. sending data to analytics. |
| class FlutterCompactFormatter { |
| FlutterCompactFormatter() { |
| _stopwatch.start(); |
| } |
| |
| /// Whether to use color escape codes in writing to stdout. |
| final bool useColor = stdout.supportsAnsiEscapes; |
| |
| /// The terminal escape for green text, or the empty string if this is Windows |
| /// or not outputting to a terminal. |
| String get _green => useColor ? '\u001b[32m' : ''; |
| |
| /// The terminal escape for red text, or the empty string if this is Windows |
| /// or not outputting to a terminal. |
| String get _red => useColor ? '\u001b[31m' : ''; |
| |
| /// The terminal escape for yellow text, or the empty string if this is |
| /// Windows or not outputting to a terminal. |
| String get _yellow => useColor ? '\u001b[33m' : ''; |
| |
| /// The terminal escape for gray text, or the empty string if this is |
| /// Windows or not outputting to a terminal. |
| String get _gray => useColor ? '\u001b[1;30m' : ''; |
| |
| /// The terminal escape for bold text, or the empty string if this is |
| /// Windows or not outputting to a terminal. |
| String get _bold => useColor ? '\u001b[1m' : ''; |
| |
| /// The terminal escape for removing test coloring, or the empty string if |
| /// this is Windows or not outputting to a terminal. |
| String get _noColor => useColor ? '\u001b[0m' : ''; |
| |
| /// The termianl escape for clearing the line, or a carriage return if |
| /// this is Windows or not outputting to a termianl. |
| String get _clearLine => useColor ? '\x1b[2K\r' : '\r'; |
| |
| final Map<int, TestResult> _tests = <int, TestResult>{}; |
| |
| /// The test results from this run. |
| Iterable<TestResult> get tests => _tests.values; |
| |
| /// The number of tests that were started. |
| int started = 0; |
| |
| /// The number of test failures. |
| int failures = 0; |
| |
| /// The number of skipped tests. |
| int skips = 0; |
| |
| /// The number of successful tests. |
| int successes = 0; |
| |
| /// Process a single line of JSON output from the JSON test reporter. |
| /// |
| /// Callers are responsible for splitting multiple lines before calling this |
| /// method. |
| TestResult processRawOutput(String raw) { |
| assert(raw != null); |
| // We might be getting messages from Flutter Tool about updating/building. |
| if (!raw.startsWith('{')) { |
| print(raw); |
| return null; |
| } |
| final Map<String, dynamic> decoded = json.decode(raw); |
| final TestResult originalResult = _tests[decoded['testID']]; |
| switch (decoded['type']) { |
| case 'done': |
| stdout.write(_clearLine); |
| stdout.write('$_bold${_stopwatch.elapsed}$_noColor '); |
| stdout.writeln( |
| '$_green+$successes $_yellow~$skips $_red-$failures:$_bold$_gray Done.$_noColor'); |
| break; |
| case 'testStart': |
| final Map<String, dynamic> testData = decoded['test']; |
| if (testData['url'] == null) { |
| started += 1; |
| stdout.write(_clearLine); |
| stdout.write('$_bold${_stopwatch.elapsed}$_noColor '); |
| stdout.write( |
| '$_green+$successes $_yellow~$skips $_red-$failures: $_gray${testData['name']}$_noColor'); |
| break; |
| } |
| _tests[testData['id']] = TestResult( |
| id: testData['id'], |
| name: testData['name'], |
| line: testData['root_line'] ?? testData['line'], |
| column: testData['root_column'] ?? testData['column'], |
| path: testData['root_url'] ?? testData['url'], |
| startTime: decoded['time'], |
| ); |
| break; |
| case 'testDone': |
| if (originalResult == null) { |
| break; |
| } |
| originalResult.endTime = decoded['time']; |
| if (decoded['skipped'] == true) { |
| skips += 1; |
| originalResult.status = TestStatus.skipped; |
| } else { |
| if (decoded['result'] == 'success') { |
| originalResult.status =TestStatus.succeeded; |
| successes += 1; |
| } else { |
| originalResult.status = TestStatus.failed; |
| failures += 1; |
| } |
| } |
| break; |
| case 'error': |
| final String error = decoded['error']; |
| final String stackTrace = decoded['stackTrace']; |
| if (originalResult != null) { |
| originalResult.errorMessage = error; |
| originalResult.stackTrace = stackTrace; |
| } else { |
| if (error != null) |
| stderr.writeln(error); |
| if (stackTrace != null) |
| stderr.writeln(stackTrace); |
| } |
| break; |
| case 'print': |
| if (originalResult != null) { |
| originalResult.messages.add(decoded['message']); |
| } |
| break; |
| case 'group': |
| case 'allSuites': |
| case 'start': |
| case 'suite': |
| default: |
| break; |
| } |
| return originalResult; |
| } |
| |
| /// Print summary of test results. |
| void finish() { |
| final List<String> skipped = <String>[]; |
| final List<String> failed = <String>[]; |
| for (TestResult result in _tests.values) { |
| switch (result.status) { |
| case TestStatus.started: |
| failed.add('${_red}Unexpectedly failed to complete a test!'); |
| failed.add(result.toString() + _noColor); |
| break; |
| case TestStatus.skipped: |
| skipped.add( |
| '${_yellow}Skipped ${result.name} (${result.pathLineColumn}).$_noColor'); |
| break; |
| case TestStatus.failed: |
| failed.addAll(<String>[ |
| '$_bold${_red}Failed ${result.name} (${result.pathLineColumn}):', |
| result.errorMessage, |
| _noColor + _red, |
| result.stackTrace, |
| ]); |
| failed.addAll(result.messages); |
| failed.add(_noColor); |
| break; |
| case TestStatus.succeeded: |
| break; |
| } |
| } |
| skipped.forEach(print); |
| failed.forEach(print); |
| if (failed.isEmpty) { |
| print('${_green}Completed, $successes test(s) passing ($skips skipped).$_noColor'); |
| } else { |
| print('$_gray$failures test(s) failed.$_noColor'); |
| } |
| } |
| } |
| |
| /// The state of a test received from the JSON reporter. |
| enum TestStatus { |
| /// Test execution has started. |
| started, |
| /// Test completed successfully. |
| succeeded, |
| /// Test failed. |
| failed, |
| /// Test was skipped. |
| skipped, |
| } |
| |
| /// The detailed status of a test run. |
| class TestResult { |
| TestResult({ |
| @required this.id, |
| @required this.name, |
| @required this.line, |
| @required this.column, |
| @required this.path, |
| @required this.startTime, |
| this.status = TestStatus.started, |
| }) : assert(id != null), |
| assert(name != null), |
| assert(line != null), |
| assert(column != null), |
| assert(path != null), |
| assert(startTime != null), |
| assert(status != null), |
| messages = <String>[]; |
| |
| /// The state of the test. |
| TestStatus status; |
| |
| /// The internal ID of the test used by the JSON reporter. |
| final int id; |
| |
| /// The name of the test, specified via the `test` method. |
| final String name; |
| |
| /// The line number from the original file. |
| final int line; |
| |
| /// The column from the original file. |
| final int column; |
| |
| /// The path of the original test file. |
| final String path; |
| |
| /// A friendly print out of the [path], [line], and [column] of the test. |
| String get pathLineColumn => '$path:$line:$column'; |
| |
| /// The start time of the test, in milliseconds relative to suite startup. |
| final int startTime; |
| |
| /// The stdout of the test. |
| final List<String> messages; |
| |
| /// The error message from the test, from an `expect`, an [Exception] or |
| /// [Error]. |
| String errorMessage; |
| |
| /// The stacktrace from a test failure. |
| String stackTrace; |
| |
| /// The time, in milliseconds relative to suite startup, that the test ended. |
| int endTime; |
| |
| /// The total time, in milliseconds, that the test took. |
| int get totalTime => (endTime ?? _stopwatch.elapsedMilliseconds) - startTime; |
| |
| @override |
| String toString() => '{$runtimeType: {$id, $name, ${totalTime}ms, $pathLineColumn}}'; |
| } |