Unverified Commit 97901da1 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Cleaner test.dart output. (#109206)

parent d50c5b1b
This diff is collapsed.
// 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:convert';
import 'dart:io';
final Stopwatch _stopwatch = Stopwatch();
/// A wrapper around package:test's JSON reporter.
///
/// This class behaves similarly to the compact reporter, but suppresses all
/// 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' : '';
/// The terminal escape for clearing the line, or a carriage return if
/// this is Windows or not outputting to a terminal.
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;
}
final Map<String, dynamic> decoded = json.decode(raw) as Map<String, dynamic>;
final TestResult? originalResult = _tests[decoded['testID']];
switch (decoded['type'] as String) {
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':
final Map<String, dynamic> testData = decoded['test'] as Map<String, dynamic>;
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;
}
_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,
);
break;
case 'testDone':
if (originalResult == null) {
break;
}
originalResult.endTime = decoded['time'] as int;
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':
final String error = decoded['error'] as String;
final String stackTrace = decoded['stackTrace'] as String;
if (originalResult != null) {
originalResult.errorMessage = error;
originalResult.stackTrace = stackTrace;
} else {
if (error != null) {
stderr.writeln(error);
}
if (stackTrace != null) {
stderr.writeln(stackTrace);
}
}
break;
case 'print':
if (originalResult != null) {
originalResult.messages.add(decoded['message'] as String);
}
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>[];
for (final TestResult result in _tests.values) {
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}}';
}
...@@ -50,23 +50,9 @@ class Command { ...@@ -50,23 +50,9 @@ class Command {
/// The raw process that was launched for this command. /// The raw process that was launched for this command.
final io.Process process; final io.Process process;
final Stopwatch _time; final Stopwatch _time;
final Future<List<List<int>>>? _savedStdout; final Future<String> _savedStdout;
final Future<List<List<int>>>? _savedStderr; final Future<String> _savedStderr;
/// Evaluates when the [process] exits.
///
/// Returns the result of running the command.
Future<CommandResult> get onExit async {
final int exitCode = await process.exitCode;
_time.stop();
// Saved output is null when OutputMode.print is used.
final String? flattenedStdout = _savedStdout != null ? _flattenToString((await _savedStdout)!) : null;
final String? flattenedStderr = _savedStderr != null ? _flattenToString((await _savedStderr)!) : null;
return CommandResult._(exitCode, _time.elapsed, flattenedStdout, flattenedStderr);
}
} }
/// The result of running a command using [startCommand] and [runCommand]; /// The result of running a command using [startCommand] and [runCommand];
...@@ -105,46 +91,50 @@ Future<Command> startCommand(String executable, List<String> arguments, { ...@@ -105,46 +91,50 @@ Future<Command> startCommand(String executable, List<String> arguments, {
}) async { }) async {
final String commandDescription = '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}'; final String commandDescription = '${path.relative(executable, from: workingDirectory)} ${arguments.join(' ')}';
final String relativeWorkingDir = path.relative(workingDirectory ?? io.Directory.current.path); final String relativeWorkingDir = path.relative(workingDirectory ?? io.Directory.current.path);
printProgress('RUNNING', relativeWorkingDir, commandDescription); print('RUNNING: cd $cyan$relativeWorkingDir$reset; $green$commandDescription$reset');
final Stopwatch time = Stopwatch()..start(); final Stopwatch time = Stopwatch()..start();
final io.Process process = await io.Process.start(executable, arguments, final io.Process process = await io.Process.start(executable, arguments,
workingDirectory: workingDirectory, workingDirectory: workingDirectory,
environment: environment, environment: environment,
); );
return Command._(
Future<List<List<int>>> savedStdout = Future<List<List<int>>>.value(<List<int>>[]); process,
Future<List<List<int>>> savedStderr = Future<List<List<int>>>.value(<List<int>>[]); time,
final Stream<List<int>> stdoutSource = process.stdout process.stdout
.transform<String>(const Utf8Decoder()) .transform<String>(const Utf8Decoder())
.transform(const LineSplitter()) .transform(const LineSplitter())
.where((String line) => removeLine == null || !removeLine(line)) .where((String line) => removeLine == null || !removeLine(line))
.map((String line) { .map<String>((String line) {
final String formattedLine = '$line\n'; final String formattedLine = '$line\n';
if (outputListener != null) { if (outputListener != null) {
outputListener(formattedLine, process); outputListener(formattedLine, process);
} }
return formattedLine; switch (outputMode) {
}) case OutputMode.print:
.transform(const Utf8Encoder()); print(line);
switch (outputMode) { break;
case OutputMode.print: case OutputMode.capture:
stdoutSource.listen((List<int> output) { break;
io.stdout.add(output); }
savedStdout.then((List<List<int>> list) => list.add(output)); return line;
}); })
process.stderr.listen((List<int> output) { .join('\n'),
io.stdout.add(output); process.stderr
savedStdout.then((List<List<int>> list) => list.add(output)); .transform<String>(const Utf8Decoder())
}); .transform(const LineSplitter())
break; .map<String>((String line) {
case OutputMode.capture: switch (outputMode) {
savedStdout = stdoutSource.toList(); case OutputMode.print:
savedStderr = process.stderr.toList(); print(line);
break; break;
} case OutputMode.capture:
break;
return Command._(process, time, savedStdout, savedStderr); }
return line;
})
.join('\n'),
);
} }
/// Runs the `executable` and waits until the process exits. /// Runs the `executable` and waits until the process exits.
...@@ -182,7 +172,12 @@ Future<CommandResult> runCommand(String executable, List<String> arguments, { ...@@ -182,7 +172,12 @@ Future<CommandResult> runCommand(String executable, List<String> arguments, {
outputListener: outputListener, outputListener: outputListener,
); );
final CommandResult result = await command.onExit; final CommandResult result = CommandResult._(
await command.process.exitCode,
command._time.elapsed,
await command._savedStdout,
await command._savedStderr,
);
if ((result.exitCode == 0) == expectNonZeroExit || (expectedExitCode != null && result.exitCode != expectedExitCode)) { if ((result.exitCode == 0) == expectNonZeroExit || (expectedExitCode != null && result.exitCode != expectedExitCode)) {
// Print the output when we get unexpected results (unless output was // Print the output when we get unexpected results (unless output was
...@@ -191,28 +186,24 @@ Future<CommandResult> runCommand(String executable, List<String> arguments, { ...@@ -191,28 +186,24 @@ Future<CommandResult> runCommand(String executable, List<String> arguments, {
case OutputMode.print: case OutputMode.print:
break; break;
case OutputMode.capture: case OutputMode.capture:
io.stdout.writeln(result.flattenedStdout); print(result.flattenedStdout);
io.stdout.writeln(result.flattenedStderr); print(result.flattenedStderr);
break; break;
} }
foundError(<String>[ foundError(<String>[
if (failureMessage != null) if (failureMessage != null)
failureMessage failureMessage
else else
'${bold}ERROR: ${red}Last command exited with ${result.exitCode} (expected: ${expectNonZeroExit ? (expectedExitCode ?? 'non-zero') : 'zero'}).$reset', '$bold${red}Command exited with exit code ${result.exitCode} but expected: ${expectNonZeroExit ? (expectedExitCode ?? 'non-zero') : 'zero'} exit code.$reset',
'${bold}Command: $green$commandDescription$reset', '${bold}Command: $green$commandDescription$reset',
'${bold}Relative working directory: $cyan$relativeWorkingDir$reset', '${bold}Relative working directory: $cyan$relativeWorkingDir$reset',
]); ]);
} else { } else {
print('$clock ELAPSED TIME: ${prettyPrintDuration(result.elapsedTime)} for $green$commandDescription$reset in $cyan$relativeWorkingDir$reset'); print('ELAPSED TIME: ${prettyPrintDuration(result.elapsedTime)} for $green$commandDescription$reset in $cyan$relativeWorkingDir$reset');
} }
return result; return result;
} }
/// Flattens a nested list of UTF-8 code units into a single string.
String _flattenToString(List<List<int>> chunks) =>
utf8.decode(chunks.expand<int>((List<int> ints) => ints).toList());
/// Specifies what to do with the command output from [runCommand] and [startCommand]. /// Specifies what to do with the command output from [runCommand] and [startCommand].
enum OutputMode { enum OutputMode {
/// Forwards standard output and standard error streams to the test process' /// Forwards standard output and standard error streams to the test process'
......
...@@ -163,8 +163,8 @@ Future<void> runWebServiceWorkerTest({ ...@@ -163,8 +163,8 @@ Future<void> runWebServiceWorkerTest({
Future<void> startAppServer({ Future<void> startAppServer({
required String cacheControl, required String cacheControl,
}) async { }) async {
final int serverPort = await findAvailablePort(); final int serverPort = await findAvailablePortAndPossiblyCauseFlakyTests();
final int browserDebugPort = await findAvailablePort(); final int browserDebugPort = await findAvailablePortAndPossiblyCauseFlakyTests();
server = await AppServer.start( server = await AppServer.start(
headless: headless, headless: headless,
cacheControl: cacheControl, cacheControl: cacheControl,
...@@ -201,7 +201,7 @@ Future<void> runWebServiceWorkerTest({ ...@@ -201,7 +201,7 @@ Future<void> runWebServiceWorkerTest({
final bool shouldExpectFlutterJs = testType != ServiceWorkerTestType.withoutFlutterJs; final bool shouldExpectFlutterJs = testType != ServiceWorkerTestType.withoutFlutterJs;
print('BEGIN runWebServiceWorkerTest(headless: $headless, testType: $testType)\n'); print('BEGIN runWebServiceWorkerTest(headless: $headless, testType: $testType)');
try { try {
///// /////
...@@ -417,7 +417,7 @@ Future<void> runWebServiceWorkerTest({ ...@@ -417,7 +417,7 @@ Future<void> runWebServiceWorkerTest({
await server?.stop(); await server?.stop();
} }
print('END runWebServiceWorkerTest(headless: $headless, testType: $testType)\n'); print('END runWebServiceWorkerTest(headless: $headless, testType: $testType)');
} }
Future<void> runWebServiceWorkerTestWithCachingResources({ Future<void> runWebServiceWorkerTestWithCachingResources({
...@@ -435,8 +435,8 @@ Future<void> runWebServiceWorkerTestWithCachingResources({ ...@@ -435,8 +435,8 @@ Future<void> runWebServiceWorkerTestWithCachingResources({
Future<void> startAppServer({ Future<void> startAppServer({
required String cacheControl, required String cacheControl,
}) async { }) async {
final int serverPort = await findAvailablePort(); final int serverPort = await findAvailablePortAndPossiblyCauseFlakyTests();
final int browserDebugPort = await findAvailablePort(); final int browserDebugPort = await findAvailablePortAndPossiblyCauseFlakyTests();
server = await AppServer.start( server = await AppServer.start(
headless: headless, headless: headless,
cacheControl: cacheControl, cacheControl: cacheControl,
...@@ -472,7 +472,7 @@ Future<void> runWebServiceWorkerTestWithCachingResources({ ...@@ -472,7 +472,7 @@ Future<void> runWebServiceWorkerTestWithCachingResources({
final bool shouldExpectFlutterJs = testType != ServiceWorkerTestType.withoutFlutterJs; final bool shouldExpectFlutterJs = testType != ServiceWorkerTestType.withoutFlutterJs;
print('BEGIN runWebServiceWorkerTestWithCachingResources(headless: $headless, testType: $testType)\n'); print('BEGIN runWebServiceWorkerTestWithCachingResources(headless: $headless, testType: $testType)');
try { try {
////////////////////////////////////////////////////// //////////////////////////////////////////////////////
...@@ -576,7 +576,7 @@ Future<void> runWebServiceWorkerTestWithCachingResources({ ...@@ -576,7 +576,7 @@ Future<void> runWebServiceWorkerTestWithCachingResources({
await server?.stop(); await server?.stop();
} }
print('END runWebServiceWorkerTestWithCachingResources(headless: $headless, testType: $testType)\n'); print('END runWebServiceWorkerTestWithCachingResources(headless: $headless, testType: $testType)');
} }
Future<void> runWebServiceWorkerTestWithBlockedServiceWorkers({ Future<void> runWebServiceWorkerTestWithBlockedServiceWorkers({
...@@ -593,8 +593,8 @@ Future<void> runWebServiceWorkerTestWithBlockedServiceWorkers({ ...@@ -593,8 +593,8 @@ Future<void> runWebServiceWorkerTestWithBlockedServiceWorkers({
Future<void> startAppServer({ Future<void> startAppServer({
required String cacheControl, required String cacheControl,
}) async { }) async {
final int serverPort = await findAvailablePort(); final int serverPort = await findAvailablePortAndPossiblyCauseFlakyTests();
final int browserDebugPort = await findAvailablePort(); final int browserDebugPort = await findAvailablePortAndPossiblyCauseFlakyTests();
server = await AppServer.start( server = await AppServer.start(
headless: headless, headless: headless,
cacheControl: cacheControl, cacheControl: cacheControl,
...@@ -628,7 +628,7 @@ Future<void> runWebServiceWorkerTestWithBlockedServiceWorkers({ ...@@ -628,7 +628,7 @@ Future<void> runWebServiceWorkerTestWithBlockedServiceWorkers({
workingDirectory: _testAppWebDirectory, workingDirectory: _testAppWebDirectory,
); );
print('BEGIN runWebServiceWorkerTestWithBlockedServiceWorkers(headless: $headless)\n'); print('BEGIN runWebServiceWorkerTestWithBlockedServiceWorkers(headless: $headless)');
try { try {
await _rebuildApp(version: 1, testType: ServiceWorkerTestType.blockedServiceWorkers, target: _targetWithBlockedServiceWorkers); await _rebuildApp(version: 1, testType: ServiceWorkerTestType.blockedServiceWorkers, target: _targetWithBlockedServiceWorkers);
...@@ -662,5 +662,5 @@ Future<void> runWebServiceWorkerTestWithBlockedServiceWorkers({ ...@@ -662,5 +662,5 @@ Future<void> runWebServiceWorkerTestWithBlockedServiceWorkers({
); );
await server?.stop(); await server?.stop();
} }
print('END runWebServiceWorkerTestWithBlockedServiceWorkers(headless: $headless)\n'); print('END runWebServiceWorkerTestWithBlockedServiceWorkers(headless: $headless)');
} }
This diff is collapsed.
This diff is collapsed.
...@@ -17,7 +17,17 @@ import 'common.dart'; ...@@ -17,7 +17,17 @@ import 'common.dart';
/// will include the process result's stdio in the failure message. /// will include the process result's stdio in the failure message.
void expectExitCode(ProcessResult result, int expectedExitCode) { void expectExitCode(ProcessResult result, int expectedExitCode) {
if (result.exitCode != expectedExitCode) { if (result.exitCode != expectedExitCode) {
fail('Failure due to exit code ${result.exitCode}\nSTDOUT:\n${result.stdout}\nSTDERR:\n${result.stderr}'); fail(
'Process ${result.pid} exitted with the wrong exit code.\n'
'\n'
'EXPECTED: exit code $expectedExitCode\n'
'ACTUAL: exit code ${result.exitCode}\n'
'\n'
'STDOUT:\n'
'${result.stdout}\n'
'STDERR:\n'
'${result.stderr}'
);
} }
} }
...@@ -96,10 +106,13 @@ void main() { ...@@ -96,10 +106,13 @@ void main() {
group('test.dart script', () { group('test.dart script', () {
const ProcessManager processManager = LocalProcessManager(); const ProcessManager processManager = LocalProcessManager();
Future<ProcessResult> runScript( Future<ProcessResult> runScript([
[Map<String, String>? environment, List<String> otherArgs = const <String>[]]) async { Map<String, String>? environment,
List<String> otherArgs = const <String>[],
]) async {
final String dart = path.absolute( final String dart = path.absolute(
path.join('..', '..', 'bin', 'cache', 'dart-sdk', 'bin', 'dart')); path.join('..', '..', 'bin', 'cache', 'dart-sdk', 'bin', 'dart'),
);
final ProcessResult scriptProcess = processManager.runSync(<String>[ final ProcessResult scriptProcess = processManager.runSync(<String>[
dart, dart,
'test.dart', 'test.dart',
...@@ -112,21 +125,21 @@ void main() { ...@@ -112,21 +125,21 @@ void main() {
// When updating this test, try to pick shard numbers that ensure we're checking // When updating this test, try to pick shard numbers that ensure we're checking
// that unequal test distributions don't miss tests. // that unequal test distributions don't miss tests.
ProcessResult result = await runScript( ProcessResult result = await runScript(
<String, String>{'SHARD': 'test_harness_tests', 'SUBSHARD': '1_3'}, <String, String>{'SHARD': kTestHarnessShardName, 'SUBSHARD': '1_3'},
); );
expectExitCode(result, 0); expectExitCode(result, 0);
expect(result.stdout, contains('Selecting subshard 1 of 3 (range 1-3 of 8)')); expect(result.stdout, contains('Selecting subshard 1 of 3 (tests 1-3 of 8)'));
result = await runScript( result = await runScript(
<String, String>{'SHARD': 'test_harness_tests', 'SUBSHARD': '3_3'}, <String, String>{'SHARD': kTestHarnessShardName, 'SUBSHARD': '3_3'},
); );
expectExitCode(result, 0); expectExitCode(result, 0);
expect(result.stdout, contains('Selecting subshard 3 of 3 (range 7-8 of 8)')); expect(result.stdout, contains('Selecting subshard 3 of 3 (tests 7-8 of 8)'));
}); });
test('exits with code 1 when SUBSHARD index greater than total', () async { test('exits with code 1 when SUBSHARD index greater than total', () async {
final ProcessResult result = await runScript( final ProcessResult result = await runScript(
<String, String>{'SHARD': 'test_harness_tests', 'SUBSHARD': '100_99'}, <String, String>{'SHARD': kTestHarnessShardName, 'SUBSHARD': '100_99'},
); );
expectExitCode(result, 1); expectExitCode(result, 1);
expect(result.stdout, contains('Invalid subshard name')); expect(result.stdout, contains('Invalid subshard name'));
......
...@@ -2,28 +2,82 @@ ...@@ -2,28 +2,82 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:core' as core_internals show print; import 'dart:async';
import 'dart:core' hide print; import 'dart:core' hide print;
import 'dart:io' as system show exit; import 'dart:io' as system show exit;
import 'dart:io' hide exit; import 'dart:io' hide exit;
import 'dart:math' as math;
import 'package:meta/meta.dart'; 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
final bool hasColor = stdout.supportsAnsiEscapes; final bool hasColor = stdout.supportsAnsiEscapes;
final String bold = hasColor ? '\x1B[1m' : ''; // used for shard titles final String bold = hasColor ? '\x1B[1m' : ''; // shard titles
final String red = hasColor ? '\x1B[31m' : ''; // used for errors final String red = hasColor ? '\x1B[31m' : ''; // errors
final String green = hasColor ? '\x1B[32m' : ''; // used for section titles, commands final String green = hasColor ? '\x1B[32m' : ''; // section titles, commands
final String yellow = hasColor ? '\x1B[33m' : ''; // used for skips final String yellow = hasColor ? '\x1B[33m' : ''; // indications that a test was skipped (usually renders orange or brown)
final String cyan = hasColor ? '\x1B[36m' : ''; // used for paths final String cyan = hasColor ? '\x1B[36m' : ''; // paths
final String reverse = hasColor ? '\x1B[7m' : ''; // used for clocks 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' : ''; final String reset = hasColor ? '\x1B[0m' : '';
final String redLine = '$red━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━$reset';
typedef PrintCallback = void Function(Object line); 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. // Allow print() to be overridden, for tests.
PrintCallback print = core_internals.print; //
// 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 get hasError => _hasError;
bool _hasError = false; bool _hasError = false;
...@@ -31,22 +85,44 @@ bool _hasError = false; ...@@ -31,22 +85,44 @@ bool _hasError = false;
Iterable<String> get errorMessages => _errorMessages; Iterable<String> get errorMessages => _errorMessages;
List<String> _errorMessages = <String>[]; List<String> _errorMessages = <String>[];
final List<String> _pendingLogs = <String>[];
Timer? _hideTimer; // When this is null, the output is verbose.
void foundError(List<String> messages) { void foundError(List<String> messages) {
assert(messages.isNotEmpty); assert(messages.isNotEmpty);
print(redLine); // Make the error message easy to notice in the logs by
messages.forEach(print); // wrapping it in a red box.
print(redLine); 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.addAll(messages); _errorMessages.addAll(messages);
_hasError = true; _hasError = true;
if (onError != null) {
onError!();
}
} }
@visibleForTesting @visibleForTesting
void resetErrorStatus() { void resetErrorStatus() {
_hasError = false; _hasError = false;
_errorMessages.clear(); _errorMessages.clear();
_pendingLogs.clear();
_hideTimer?.cancel();
_hideTimer = null;
} }
Never reportErrorsAndExit() { Never reportErrorsAndExit() {
_hideTimer?.cancel();
_hideTimer = null;
print(redLine); print(redLine);
print('For your convenience, the error messages reported above are repeated here:'); print('For your convenience, the error messages reported above are repeated here:');
_errorMessages.forEach(print); _errorMessages.forEach(print);
...@@ -54,35 +130,93 @@ Never reportErrorsAndExit() { ...@@ -54,35 +130,93 @@ Never reportErrorsAndExit() {
system.exit(1); system.exit(1);
} }
String get clock { void printProgress(String message) {
final DateTime now = DateTime.now(); _pendingLogs.clear();
return '$reverse▌' _hideTimer?.cancel();
'${now.hour.toString().padLeft(2, "0")}:' _hideTimer = null;
'${now.minute.toString().padLeft(2, "0")}:' print('$clock $message $reset');
'${now.second.toString().padLeft(2, "0")}' if (hasColor) {
'▐$reset'; // 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();
});
}
} }
String prettyPrintDuration(Duration duration) { final Pattern _lineBreak = RegExp(r'[\r\n]');
String result = '';
final int minutes = duration.inMinutes; void _printQuietly(Object? message) {
if (minutes > 0) { // The point of this function is to avoid printing its output unless the timer
result += '${minutes}min '; // 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');
} }
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;
} }
void printProgress(String action, String workingDir, String command) { void _printLoudly(String message) {
print('$clock $action: cd $cyan$workingDir$reset; $green$command$reset'); 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; int _portCounter = 8080;
/// Finds the next available local port. /// Finds the next available local port.
Future<int> findAvailablePort() async { Future<int> findAvailablePortAndPossiblyCauseFlakyTests() async {
while (!await _isPortAvailable(_portCounter)) { while (!await _isPortAvailable(_portCounter)) {
_portCounter += 1; _portCounter += 1;
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment