flutter_compact_formatter.dart 8.57 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 12 13
// 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.
///
Chris Bracken's avatar
Chris Bracken committed
14
/// This class behaves similarly to the compact reporter, but suppresses all
Dan Field's avatar
Dan Field committed
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 51 52
/// 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' : '';

53 54
  /// 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
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
  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;
    }
85
    final Map<String, dynamic> decoded = json.decode(raw) as Map<String, dynamic>;
Dan Field's avatar
Dan Field committed
86
    final TestResult originalResult = _tests[decoded['testID']];
87
    switch (decoded['type'] as String) {
Dan Field's avatar
Dan Field committed
88 89 90 91 92 93 94
      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':
95
        final Map<String, dynamic> testData = decoded['test'] as Map<String, dynamic>;
Dan Field's avatar
Dan Field committed
96 97 98 99 100 101 102 103
        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;
        }
104 105 106 107 108 109 110
        _tests[testData['id'] as int] = TestResult(
          id: testData['id'] as int,
          name: testData['name'] as String,
          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,
          startTime: decoded['time'] as int,
Dan Field's avatar
Dan Field committed
111 112 113 114 115 116
        );
        break;
      case 'testDone':
        if (originalResult == null) {
          break;
        }
117
        originalResult.endTime = decoded['time'] as int;
Dan Field's avatar
Dan Field committed
118 119 120 121 122 123 124 125 126 127 128 129 130 131
        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':
132 133
        final String error = decoded['error'] as String;
        final String stackTrace = decoded['stackTrace'] as String;
Dan Field's avatar
Dan Field committed
134
        if (originalResult != null) {
135 136 137 138 139 140 141
          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
142 143 144 145
        }
        break;
      case 'print':
        if (originalResult != null) {
146
          originalResult.messages.add(decoded['message'] as String);
Dan Field's avatar
Dan Field committed
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
        }
        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>[];
163
    for (final TestResult result in _tests.values) {
Dan Field's avatar
Dan Field committed
164 165 166 167 168 169 170 171 172 173 174 175 176 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 210 211 212 213 214 215 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 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270
      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}}';
}