| // 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. | |
| import 'dart:async'; | |
| import 'dart:core' hide print; | |
| import 'dart:io' as system show exit; | |
| import 'dart:io' hide exit; | |
| import 'dart:math' as math; | |
| import 'package:meta/meta.dart'; | |
| const Duration _quietTimeout = Duration(minutes: 10); // how long the output should be hidden between calls to printProgress before just being verbose | |
| // If running from LUCI set to False. | |
| final bool isLuci = Platform.environment['LUCI_CI'] == 'True'; | |
| final bool hasColor = stdout.supportsAnsiEscapes && !isLuci; | |
| 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) | |
| final String reset = hasColor ? '\x1B[0m' : ''; | |
| 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(); | |
| // Allow print() to be overridden, for tests. | |
| // | |
| // 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; | |
| bool get hasError => _hasError; | |
| bool _hasError = false; | |
| List<List<String>> _errorMessages = <List<String>>[]; | |
| final List<String> _pendingLogs = <String>[]; | |
| Timer? _hideTimer; // When this is null, the output is verbose. | |
| void foundError(List<String> messages) { | |
| assert(messages.isNotEmpty); | |
| // 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(); | |
| _errorMessages.add(messages); | |
| _hasError = true; | |
| if (onError != null) { | |
| onError!(); | |
| } | |
| } | |
| @visibleForTesting | |
| void resetErrorStatus() { | |
| _hasError = false; | |
| _errorMessages.clear(); | |
| _pendingLogs.clear(); | |
| _hideTimer?.cancel(); | |
| _hideTimer = null; | |
| } | |
| Never reportSuccessAndExit(String message) { | |
| _hideTimer?.cancel(); | |
| _hideTimer = null; | |
| print('$clock $message$reset'); | |
| system.exit(0); | |
| } | |
| Never reportErrorsAndExit(String message) { | |
| _hideTimer?.cancel(); | |
| _hideTimer = null; | |
| print('$clock $message$reset'); | |
| print(redLine); | |
| 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(' 🙙 🙛 '); | |
| } | |
| } | |
| print(redLine); | |
| system.exit(1); | |
| } | |
| void printProgress(String message) { | |
| _pendingLogs.clear(); | |
| _hideTimer?.cancel(); | |
| _hideTimer = null; | |
| print('$clock $message$reset'); | |
| 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(); | |
| }); | |
| } | |
| } | |
| 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'); | |
| } | |
| } | |
| 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); | |
| } | |
| } | |
| // 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 | |
| int _portCounter = 8080; | |
| /// Finds the next available local port. | |
| Future<int> findAvailablePortAndPossiblyCauseFlakyTests() async { | |
| 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; | |
| } | |
| } |