flutter_compact_formatter.dart 8.54 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
Dan Field's avatar
Dan Field committed
2 3 4 5 6 7 8 9 10 11
// 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';

final Stopwatch _stopwatch = Stopwatch();

/// A wrapper around package:test's JSON reporter.
///
Chris Bracken's avatar
Chris Bracken committed
12
/// This class behaves similarly to the compact reporter, but suppresses all
Dan Field's avatar
Dan Field committed
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
/// 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' : '';

51 52
  /// The terminal escape for clearing the line, or a carriage return if
  /// this is Windows or not outputting to a terminal.
Dan Field's avatar
Dan Field committed
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
  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.
76
  TestResult? processRawOutput(String raw) {
Dan Field's avatar
Dan Field committed
77 78 79 80 81 82
    assert(raw != null);
    // We might be getting messages from Flutter Tool about updating/building.
    if (!raw.startsWith('{')) {
      print(raw);
      return null;
    }
83
    final Map<String, dynamic> decoded = json.decode(raw) as Map<String, dynamic>;
84
    final TestResult? originalResult = _tests[decoded['testID']];
85
    switch (decoded['type'] as String) {
Dan Field's avatar
Dan Field committed
86 87 88 89 90 91 92
      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':
93
        final Map<String, dynamic> testData = decoded['test'] as Map<String, dynamic>;
Dan Field's avatar
Dan Field committed
94 95 96 97 98 99 100 101
        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;
        }
102 103 104
        _tests[testData['id'] as int] = TestResult(
          id: testData['id'] as int,
          name: testData['name'] as String,
105 106 107
          line: testData['root_line'] as int? ?? testData['line'] as int,
          column: testData['root_column'] as int? ?? testData['column'] as int,
          path: testData['root_url'] as String? ?? testData['url'] as String,
108
          startTime: decoded['time'] as int,
Dan Field's avatar
Dan Field committed
109 110 111 112 113 114
        );
        break;
      case 'testDone':
        if (originalResult == null) {
          break;
        }
115
        originalResult.endTime = decoded['time'] as int;
Dan Field's avatar
Dan Field committed
116 117 118 119 120 121 122 123 124 125 126 127 128 129
        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':
130 131
        final String error = decoded['error'] as String;
        final String stackTrace = decoded['stackTrace'] as String;
Dan Field's avatar
Dan Field committed
132
        if (originalResult != null) {
133 134 135 136 137 138 139
          originalResult.errorMessage = error;
          originalResult.stackTrace = stackTrace;
        } else {
          if (error != null)
            stderr.writeln(error);
          if (stackTrace != null)
            stderr.writeln(stackTrace);
Dan Field's avatar
Dan Field committed
140 141 142 143
        }
        break;
      case 'print':
        if (originalResult != null) {
144
          originalResult.messages.add(decoded['message'] as String);
Dan Field's avatar
Dan Field committed
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
        }
        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>[];
161
    for (final TestResult result in _tests.values) {
Dan Field's avatar
Dan Field committed
162 163 164 165 166 167 168 169 170 171 172 173
      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}):',
174
            result.errorMessage!,
Dan Field's avatar
Dan Field committed
175
            _noColor + _red,
176
            result.stackTrace!,
Dan Field's avatar
Dan Field committed
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209
          ]);
          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({
210 211 212 213 214 215
    required this.id,
    required this.name,
    required this.line,
    required this.column,
    required this.path,
    required this.startTime,
Dan Field's avatar
Dan Field committed
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254
    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].
255
  String? errorMessage;
Dan Field's avatar
Dan Field committed
256 257

  /// The stacktrace from a test failure.
258
  String? stackTrace;
Dan Field's avatar
Dan Field committed
259 260

  /// The time, in milliseconds relative to suite startup, that the test ended.
261
  int? endTime;
Dan Field's avatar
Dan Field committed
262 263 264 265 266 267 268

  /// 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}}';
}