// 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); final String title = 'ERROR #${_errorMessages.length + 1}'; print('$red╔═╡$bold$title$reset$red╞═${"═" * (width - 4 - title.length)}'); 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; onError?.call(); } @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); print('You may find the errors by searching for "╡ERROR #" in the logs.'); 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; } }