utils.dart 8.95 KB
Newer Older
1 2 3 4
// Copyright 2014 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.

5
import 'dart:async';
6
import 'dart:core' hide print;
7
import 'dart:io' as system show exit;
8
import 'dart:io' hide exit;
9
import 'dart:math' as math;
10

11 12
import 'package:meta/meta.dart';

13 14
const Duration _quietTimeout = Duration(minutes: 10); // how long the output should be hidden between calls to printProgress before just being verbose

15 16 17 18
// If running from LUCI set to False.
final bool isLuci =  Platform.environment['LUCI_CI'] == 'True';
final bool hasColor = stdout.supportsAnsiEscapes && !isLuci;

19

20 21 22 23 24 25 26 27
final String bold = hasColor ? '\x1B[1m' : ''; // shard titles
final String red = hasColor ? '\x1B[31m' : ''; // errors
final String green = hasColor ? '\x1B[32m' : ''; // section titles, commands
final String yellow = hasColor ? '\x1B[33m' : ''; // indications that a test was skipped (usually renders orange or brown)
final String cyan = hasColor ? '\x1B[36m' : ''; // paths
final String reverse = hasColor ? '\x1B[7m' : ''; // clocks
final String gray = hasColor ? '\x1B[30m' : ''; // subtle decorative items (usually renders as dark gray)
final String white = hasColor ? '\x1B[37m' : ''; // last log line (usually renders as light gray)
28 29
final String reset = hasColor ? '\x1B[0m' : '';

30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
const int kESC = 0x1B;
const int kOpenSquareBracket = 0x5B;
const int kCSIParameterRangeStart = 0x30;
const int kCSIParameterRangeEnd = 0x3F;
const int kCSIIntermediateRangeStart = 0x20;
const int kCSIIntermediateRangeEnd = 0x2F;
const int kCSIFinalRangeStart = 0x40;
const int kCSIFinalRangeEnd = 0x7E;


String get redLine {
  if (hasColor) {
    return '$red${'━' * stdout.terminalColumns}$reset';
  }
  return '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━';
}

String get clock {
  final DateTime now = DateTime.now();
  return '$reverse▌'
         '${now.hour.toString().padLeft(2, "0")}:'
         '${now.minute.toString().padLeft(2, "0")}:'
         '${now.second.toString().padLeft(2, "0")}'
         '▐$reset';
}

String prettyPrintDuration(Duration duration) {
  String result = '';
  final int minutes = duration.inMinutes;
  if (minutes > 0) {
    result += '${minutes}min ';
  }
  final int seconds = duration.inSeconds - minutes * 60;
  final int milliseconds = duration.inMilliseconds - (seconds * 1000 + minutes * 60 * 1000);
  result += '$seconds.${milliseconds.toString().padLeft(3, "0")}s';
  return result;
}

typedef PrintCallback = void Function(Object? line);
typedef VoidCallback = void Function();
70

71
// Allow print() to be overridden, for tests.
72 73 74 75 76 77 78 79 80 81 82 83
//
// Files that import this library should not import `print` from dart:core
// and should not use dart:io's `stdout` or `stderr`.
//
// By default this hides log lines between `printProgress` calls unless a
// timeout expires or anything calls `foundError`.
//
// Also used to implement `--verbose` in test.dart.
PrintCallback print = _printQuietly;

// Called by foundError and used to implement `--abort-on-error` in test.dart.
VoidCallback? onError;
84

85 86
bool get hasError => _hasError;
bool _hasError = false;
87

88
List<List<String>> _errorMessages = <List<String>>[];
89

90 91 92
final List<String> _pendingLogs = <String>[];
Timer? _hideTimer; // When this is null, the output is verbose.

93 94
void foundError(List<String> messages) {
  assert(messages.isNotEmpty);
95 96 97 98 99 100 101 102 103 104 105 106 107 108
  // Make the error message easy to notice in the logs by
  // wrapping it in a red box.
  final int width = math.max(15, (hasColor ? stdout.terminalColumns : 80) - 1);
  print('$red╔═╡${bold}ERROR$reset$red╞═${"═" * (width - 9)}');
  for (final String message in messages.expand((String line) => line.split('\n'))) {
    print('$red$reset $message');
  }
  print('$red${"═" * width}');
  // Normally, "print" actually prints to the log. To make the errors visible,
  // and to include useful context, print the entire log up to this point, and
  // clear it. Subsequent messages will continue to not be logged until there is
  // another error.
  _pendingLogs.forEach(_printLoudly);
  _pendingLogs.clear();
109
  _errorMessages.add(messages);
110
  _hasError = true;
111 112 113
  if (onError != null) {
    onError!();
  }
114 115
}

116 117 118 119
@visibleForTesting
void resetErrorStatus() {
  _hasError = false;
  _errorMessages.clear();
120 121 122
  _pendingLogs.clear();
  _hideTimer?.cancel();
  _hideTimer = null;
123
}
124

125
Never reportSuccessAndExit(String message) {
126 127
  _hideTimer?.cancel();
  _hideTimer = null;
128 129 130 131 132 133 134 135
  print('$clock $message$reset');
  system.exit(0);
}

Never reportErrorsAndExit(String message) {
  _hideTimer?.cancel();
  _hideTimer = null;
  print('$clock $message$reset');
136
  print(redLine);
137 138 139 140 141 142 143 144 145 146 147 148
  print('${red}For your convenience, the error messages reported above are repeated here:$reset');
  final bool printSeparators = _errorMessages.any((List<String> messages) => messages.length > 1);
  if (printSeparators) {
    print('  🙙  🙛  ');
  }
  for (int index = 0; index < _errorMessages.length * 2 - 1; index += 1) {
    if (index.isEven) {
      _errorMessages[index ~/ 2].forEach(print);
    } else if (printSeparators) {
      print('  🙙  🙛  ');
    }
  }
149 150 151
  print(redLine);
  system.exit(1);
}
152

153 154 155 156
void printProgress(String message) {
  _pendingLogs.clear();
  _hideTimer?.cancel();
  _hideTimer = null;
157
  print('$clock $message$reset');
158 159 160 161 162 163 164 165 166 167 168 169
  if (hasColor) {
    // This sets up a timer to switch to verbose mode when the tests take too long,
    // so that if a test hangs we can see the logs.
    // (This is only supported with a color terminal. When the terminal doesn't
    // support colors, the scripts just print everything verbosely, that way in
    // CI there's nothing hidden.)
    _hideTimer = Timer(_quietTimeout, () {
      _hideTimer = null;
      _pendingLogs.forEach(_printLoudly);
      _pendingLogs.clear();
    });
  }
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
final Pattern _lineBreak = RegExp(r'[\r\n]');

void _printQuietly(Object? message) {
  // The point of this function is to avoid printing its output unless the timer
  // has gone off in which case the function assumes verbose mode is active and
  // prints everything. To show that progress is still happening though, rather
  // than showing nothing at all, it instead shows the last line of output and
  // keeps overwriting it. To do this in color mode, carefully measures the line
  // of text ignoring color codes, which is what the parser below does.
  if (_hideTimer != null) {
    _pendingLogs.add(message.toString());
    String line = '$message'.trimRight();
    final int start = line.lastIndexOf(_lineBreak) + 1;
    int index = start;
    int length = 0;
    while (index < line.length && length < stdout.terminalColumns) {
      if (line.codeUnitAt(index) == kESC) { // 0x1B
        index += 1;
        if (index < line.length && line.codeUnitAt(index) == kOpenSquareBracket) { // 0x5B, [
          // That was the start of a CSI sequence.
          index += 1;
          while (index < line.length && line.codeUnitAt(index) >= kCSIParameterRangeStart
                                     && line.codeUnitAt(index) <= kCSIParameterRangeEnd) { // 0x30..0x3F
            index += 1; // ...parameter bytes...
          }
          while (index < line.length && line.codeUnitAt(index) >= kCSIIntermediateRangeStart
                                     && line.codeUnitAt(index) <= kCSIIntermediateRangeEnd) { // 0x20..0x2F
            index += 1; // ...intermediate bytes...
          }
          if (index < line.length && line.codeUnitAt(index) >= kCSIFinalRangeStart
                                  && line.codeUnitAt(index) <= kCSIFinalRangeEnd) { // 0x40..0x7E
            index += 1; // ...final byte.
          }
        }
      } else {
        index += 1;
        length += 1;
      }
    }
    line = line.substring(start, index);
    if (line.isNotEmpty) {
      stdout.write('\r\x1B[2K$white$line$reset');
    }
  } else {
    _printLoudly('$message');
217
  }
218 219
}

220 221 222 223 224 225 226
void _printLoudly(String message) {
  if (hasColor) {
    // Overwrite the last line written by _printQuietly.
    stdout.writeln('\r\x1B[2K$reset${message.trimRight()}');
  } else {
    stdout.writeln(message);
  }
227
}
228

229 230 231 232 233 234 235
// THE FOLLOWING CODE IS A VIOLATION OF OUR STYLE GUIDE
// BECAUSE IT INTRODUCES A VERY FLAKY RACE CONDITION
// https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#never-check-if-a-port-is-available-before-using-it-never-add-timeouts-and-other-race-conditions
// DO NOT USE THE FOLLOWING FUNCTIONS
// DO NOT WRITE CODE LIKE THE FOLLOWING FUNCTIONS
// https://github.com/flutter/flutter/issues/109474

236 237 238
int _portCounter = 8080;

/// Finds the next available local port.
239
Future<int> findAvailablePortAndPossiblyCauseFlakyTests() async {
240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
  while (!await _isPortAvailable(_portCounter)) {
    _portCounter += 1;
  }
  return _portCounter++;
}

Future<bool> _isPortAvailable(int port) async {
  try {
    final RawSocket socket = await RawSocket.connect('localhost', port);
    socket.shutdown(SocketDirection.both);
    await socket.close();
    return false;
  } on SocketException {
    return true;
  }
}