blob: f09372abf00a9518ce13dae2a00d1283cba8245f [file] [log] [blame]
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
library test_progress;
import "dart:async";
import "dart:io";
import "dart:io" as io;
import "dart:convert" show JSON;
import "path.dart";
import "status_file_parser.dart";
import "summary_report.dart";
import "test_runner.dart";
import "test_suite.dart";
import "utils.dart";
String _pad(String s, int length) {
StringBuffer buffer = new StringBuffer();
for (int i = s.length; i < length; i++) {
buffer.write(' ');
}
buffer.write(s);
return buffer.toString();
}
String _padTime(int time) {
if (time == 0) {
return '00';
} else if (time < 10) {
return '0$time';
} else {
return '$time';
}
}
String _timeString(Duration d) {
var min = d.inMinutes;
var sec = d.inSeconds % 60;
return '${_padTime(min)}:${_padTime(sec)}';
}
class Formatter {
const Formatter();
String passed(msg) => msg;
String failed(msg) => msg;
}
class ColorFormatter extends Formatter {
static int BOLD = 1;
static int GREEN = 32;
static int RED = 31;
static int NONE = 0;
static String ESCAPE = decodeUtf8([27]);
String passed(String msg) => _color(msg, GREEN);
String failed(String msg) => _color(msg, RED);
static String _color(String msg, int color) {
return "$ESCAPE[${color}m$msg$ESCAPE[0m";
}
}
List<String> _buildFailureOutput(TestCase test,
[Formatter formatter = const Formatter()]) {
List<String> getLinesWithoutCarriageReturn(List<int> output) {
return decodeUtf8(output)
.replaceAll('\r\n', '\n')
.replaceAll('\r', '\n')
.split('\n');
}
List<String> output = new List<String>();
output.add('');
output.add(formatter.failed('FAILED: ${test.configurationString}'
' ${test.displayName}'));
StringBuffer expected = new StringBuffer();
expected.write('Expected: ');
for (var expectation in test.expectedOutcomes) {
expected.write('$expectation ');
}
output.add(expected.toString());
output.add('Actual: ${test.result}');
if (!test.lastCommandOutput.hasTimedOut) {
if (test.commandOutputs.length != test.commands.length &&
!test.expectCompileError) {
output.add('Unexpected compile-time error.');
} else {
if (test.expectCompileError) {
output.add('Compile-time error expected.');
}
if (test.hasRuntimeError) {
output.add('Runtime error expected.');
}
if (test.configuration['checked'] && test.isNegativeIfChecked) {
output.add('Dynamic type error expected.');
}
}
}
for (var i = 0; i < test.commands.length; i++) {
var command = test.commands[i];
var commandOutput = test.commandOutputs[command];
if (commandOutput != null) {
output.add("CommandOutput[${command.displayName}]:");
if (!commandOutput.diagnostics.isEmpty) {
String prefix = 'diagnostics:';
for (var s in commandOutput.diagnostics) {
output.add('$prefix ${s}');
prefix = ' ';
}
}
if (!commandOutput.stdout.isEmpty) {
output.add('');
output.add('stdout:');
output.addAll(getLinesWithoutCarriageReturn(commandOutput.stdout));
}
if (!commandOutput.stderr.isEmpty) {
output.add('');
output.add('stderr:');
output.addAll(getLinesWithoutCarriageReturn(commandOutput.stderr));
}
}
}
if (test is BrowserTestCase) {
// Additional command for rerunning the steps locally after the fact.
var command = test.configuration["_servers_"].httpServerCommandline();
output.add('');
output.add('To retest, run: $command');
}
for (var i = 0; i < test.commands.length; i++) {
var command = test.commands[i];
var commandOutput = test.commandOutputs[command];
output.add('');
output.add('Command[${command.displayName}]: $command');
if (commandOutput != null) {
output.add('Took ${commandOutput.time}');
} else {
output.add('Did not run');
}
}
var arguments = ['python', 'tools/test.py'];
arguments.addAll(test.configuration['_reproducing_arguments_']);
arguments.add(test.displayName);
var testPyCommandline = arguments.map(escapeCommandLineArgument).join(' ');
output.add('');
output.add('Short reproduction command (experimental):');
output.add(" $testPyCommandline");
return output;
}
String _buildSummaryEnd(int failedTests) {
if (failedTests == 0) {
return '\n===\n=== All tests succeeded\n===\n';
} else {
var pluralSuffix = failedTests != 1 ? 's' : '';
return '\n===\n=== ${failedTests} test$pluralSuffix failed\n===\n';
}
}
class EventListener {
void testAdded() {}
void done(TestCase test) {}
void allTestsKnown() {}
void allDone() {}
}
class ExitCodeSetter extends EventListener {
void done(TestCase test) {
if (test.unexpectedOutput) {
io.exitCode = 1;
}
}
}
class IgnoredTestMonitor extends EventListener {
static final int maxIgnored = 5;
int countIgnored = 0;
void done(TestCase test) {
if (test.lastCommandOutput.result(test) == Expectation.IGNORE) {
countIgnored++;
if (countIgnored > maxIgnored) {
print("/nMore than $maxIgnored tests were ignored due to flakes in");
print("the test infrastructure. Notify whesse@google.com.");
print("Output of the last ignored test was:");
print(_buildFailureOutput(test));
exit(1);
}
}
}
void allDone() {
if (countIgnored > 0) {
print("Ignored $countIgnored tests due to flaky infrastructure");
}
}
}
class FlakyLogWriter extends EventListener {
void done(TestCase test) {
if (test.isFlaky && test.result != Expectation.PASS) {
var buf = new StringBuffer();
for (var l in _buildFailureOutput(test)) {
buf.write("$l\n");
}
_appendToFlakyFile(buf.toString());
}
}
void _appendToFlakyFile(String msg) {
var file = new File(TestUtils.flakyFileName());
var fd = file.openSync(mode: FileMode.APPEND);
fd.writeStringSync(msg);
fd.closeSync();
}
}
class TestOutcomeLogWriter extends EventListener {
/*
* The ".test-outcome.log" file contains one line for every executed test.
* Such a line is an encoded JSON data structure of the following form:
* The durations are double values in milliseconds.
*
* {
* name: 'co19/LibTest/math/x',
* configuration: {
* mode : 'release',
* compiler : 'dart2js',
* ....
* },
* test_result: {
* outcome: 'RuntimeError',
* expected_outcomes: ['Pass', 'Fail'],
* duration: 2600.64,
* command_results: [
* {
* name: 'dart2js',
* duration: 2400.44,
* },
* {
* name: 'ff',
* duration: 200.2,
* },
* ],
* }
* },
*/
static final INTERESTED_CONFIGURATION_PARAMETERS = [
'mode',
'arch',
'compiler',
'runtime',
'checked',
'strong',
'host_checked',
'minified',
'csp',
'system',
'vm_options',
'use_sdk',
'use_repository_packages',
'use_public_packages',
'builder_tag'
];
IOSink _sink;
void done(TestCase test) {
var name = test.displayName;
var configuration = {};
for (var key in INTERESTED_CONFIGURATION_PARAMETERS) {
configuration[key] = test.configuration[key];
}
var outcome = '${test.lastCommandOutput.result(test)}';
var expectations =
test.expectedOutcomes.map((expectation) => "$expectation").toList();
var commandResults = [];
double totalDuration = 0.0;
for (var command in test.commands) {
var output = test.commandOutputs[command];
if (output != null) {
double duration = output.time.inMicroseconds / 1000.0;
totalDuration += duration;
commandResults
.add({'name': command.displayName, 'duration': duration,});
}
}
_writeTestOutcomeRecord({
'name': name,
'configuration': configuration,
'test_result': {
'outcome': outcome,
'expected_outcomes': expectations,
'duration': totalDuration,
'command_results': commandResults,
},
});
}
void allDone() {
if (_sink != null) _sink.close();
}
void _writeTestOutcomeRecord(Map record) {
if (_sink == null) {
_sink = new File(TestUtils.testOutcomeFileName())
.openWrite(mode: FileMode.APPEND);
}
_sink.write("${JSON.encode(record)}\n");
}
}
class UnexpectedCrashDumpArchiver extends EventListener {
void done(TestCase test) {
if (test.unexpectedOutput && test.result == Expectation.CRASH) {
var name = "core.dart.${test.lastCommandOutput.pid}";
var file = new File(name);
if (file.existsSync()) {
// Find the binary - we assume this is the first part of the command
var binName = test.lastCommandExecuted.toString().split(' ').first;
var binFile = new File(binName);
var binBaseName = new Path(binName).filename;
if (binFile.existsSync()) {
var tmpPath = new Path(Directory.systemTemp.path);
var dir = new Path(TestUtils
.mkdirRecursive(
tmpPath, new Path('coredump_${test.lastCommandOutput.pid}'))
.path);
TestUtils.copyFile(new Path(name), dir.append(name));
TestUtils.copyFile(new Path(binName), dir.append(binBaseName));
print("\nCopied core dump and binary for unexpected crash to: "
"$dir");
}
}
}
}
}
class SummaryPrinter extends EventListener {
final bool jsonOnly;
SummaryPrinter({bool jsonOnly})
: jsonOnly = (jsonOnly == null) ? false : jsonOnly;
void allTestsKnown() {
if (jsonOnly) {
print("JSON:");
print(JSON.encode(summaryReport.values));
} else {
summaryReport.printReport();
}
}
}
class TimingPrinter extends EventListener {
final _command2testCases = new Map<Command, List<TestCase>>();
final _commandOutputs = new Set<CommandOutput>();
DateTime _startTime;
TimingPrinter(this._startTime);
void done(TestCase testCase) {
for (var commandOutput in testCase.commandOutputs.values) {
var command = commandOutput.command;
_commandOutputs.add(commandOutput);
_command2testCases.putIfAbsent(command, () => <TestCase>[]);
_command2testCases[command].add(testCase);
}
}
void allDone() {
Duration d = (new DateTime.now()).difference(_startTime);
print('\n--- Total time: ${_timeString(d)} ---');
var outputs = _commandOutputs.toList();
outputs.sort((a, b) {
return b.time.inMilliseconds - a.time.inMilliseconds;
});
for (int i = 0; i < 20 && i < outputs.length; i++) {
var commandOutput = outputs[i];
var command = commandOutput.command;
var testCases = _command2testCases[command];
var testCasesDescription = testCases.map((testCase) {
return "${testCase.configurationString}/${testCase.displayName}";
}).join(', ');
print('${commandOutput.time} - '
'${command.displayName} - '
'$testCasesDescription');
}
}
}
class StatusFileUpdatePrinter extends EventListener {
var statusToConfigs = new Map<String, List<String>>();
var _failureSummary = <String>[];
void done(TestCase test) {
if (test.unexpectedOutput) {
_printFailureOutput(test);
}
}
void allDone() {
_printFailureSummary();
}
void _printFailureOutput(TestCase test) {
String status = '${test.displayName}: ${test.result}';
List<String> configs =
statusToConfigs.putIfAbsent(status, () => <String>[]);
configs.add(test.configurationString);
if (test.lastCommandOutput.hasTimedOut) {
print('\n${test.displayName} timed out on ${test.configurationString}');
}
}
String _extractRuntime(String configuration) {
// Extract runtime from a configuration, for example,
// 'none-vm-checked release_ia32'.
List<String> runtime = configuration.split(' ')[0].split('-');
return '${runtime[0]}-${runtime[1]}';
}
void _printFailureSummary() {
var groupedStatuses = new Map<String, List<String>>();
statusToConfigs.forEach((String status, List<String> configs) {
var runtimeToConfiguration = new Map<String, List<String>>();
for (String config in configs) {
String runtime = _extractRuntime(config);
var runtimeConfigs =
runtimeToConfiguration.putIfAbsent(runtime, () => <String>[]);
runtimeConfigs.add(config);
}
runtimeToConfiguration
.forEach((String runtime, List<String> runtimeConfigs) {
runtimeConfigs.sort((a, b) => a.compareTo(b));
List<String> statuses = groupedStatuses.putIfAbsent(
'$runtime: $runtimeConfigs', () => <String>[]);
statuses.add(status);
});
});
print('\n\nNecessary status file updates:');
groupedStatuses.forEach((String config, List<String> statuses) {
print('');
print('$config:');
statuses.sort((a, b) => a.compareTo(b));
for (String status in statuses) {
print(' $status');
}
});
}
}
class SkippedCompilationsPrinter extends EventListener {
int _skippedCompilations = 0;
void done(TestCase test) {
for (var commandOutput in test.commandOutputs.values) {
if (commandOutput.compilationSkipped) _skippedCompilations++;
}
}
void allDone() {
if (_skippedCompilations > 0) {
print('\n$_skippedCompilations compilations were skipped because '
'the previous output was already up to date\n');
}
}
}
class LeftOverTempDirPrinter extends EventListener {
final MIN_NUMBER_OF_TEMP_DIRS = 50;
static RegExp _getTemporaryDirectoryRegexp() {
// These are the patterns of temporary directory names created by
// 'Directory.systemTemp.createTemp()' on linux/macos and windows.
if (['macos', 'linux'].contains(Platform.operatingSystem)) {
return new RegExp(r'^temp_dir1_......$');
} else {
return new RegExp(r'tempdir-........-....-....-....-............$');
}
}
static Stream<FileSystemEntity> getLeftOverTemporaryDirectories() {
var regExp = _getTemporaryDirectoryRegexp();
return Directory.systemTemp.list().where((FileSystemEntity fse) {
if (fse is Directory) {
if (regExp.hasMatch(new Path(fse.path).filename)) {
return true;
}
}
return false;
});
}
void allDone() {
getLeftOverTemporaryDirectories().length.then((int count) {
if (count > MIN_NUMBER_OF_TEMP_DIRS) {
DebugLogger.warning("There are ${count} directories "
"in the system tempdir "
"('${Directory.systemTemp.path}')! "
"Maybe left over directories?\n");
}
}).catchError((error) {
DebugLogger.warning("Could not list temp directories, got: $error");
});
}
}
class LineProgressIndicator extends EventListener {
void done(TestCase test) {
var status = 'pass';
if (test.unexpectedOutput) {
status = 'fail';
}
print('Done ${test.configurationString} ${test.displayName}: $status');
}
}
class TestFailurePrinter extends EventListener {
bool _printSummary;
var _formatter;
var _failureSummary = <String>[];
var _failedTests = 0;
TestFailurePrinter(this._printSummary, [this._formatter = const Formatter()]);
void done(TestCase test) {
if (test.unexpectedOutput) {
_failedTests++;
var lines = _buildFailureOutput(test, _formatter);
for (var line in lines) {
print(line);
}
print('');
if (_printSummary) {
_failureSummary.addAll(lines);
_failureSummary.add('');
}
}
}
void allDone() {
if (_printSummary) {
if (!_failureSummary.isEmpty) {
print('\n=== Failure summary:\n');
for (String line in _failureSummary) {
print(line);
}
print('');
print(_buildSummaryEnd(_failedTests));
}
}
}
}
class ProgressIndicator extends EventListener {
ProgressIndicator(this._startTime);
void testAdded() {
_foundTests++;
}
void done(TestCase test) {
if (test.unexpectedOutput) {
_failedTests++;
} else {
_passedTests++;
}
_printDoneProgress(test);
}
void allTestsKnown() {
_allTestsKnown = true;
}
void _printDoneProgress(TestCase test) {}
int _completedTests() => _passedTests + _failedTests;
int _foundTests = 0;
int _passedTests = 0;
int _failedTests = 0;
bool _allTestsKnown = false;
DateTime _startTime;
}
abstract class CompactIndicator extends ProgressIndicator {
CompactIndicator(DateTime startTime) : super(startTime);
void allDone() {
if (_failedTests > 0) {
// We may have printed many failure logs, so reprint the summary data.
_printProgress();
}
print('');
}
void _printDoneProgress(TestCase test) => _printProgress();
void _printProgress();
}
class CompactProgressIndicator extends CompactIndicator {
Formatter _formatter;
CompactProgressIndicator(DateTime startTime, this._formatter)
: super(startTime);
void _printProgress() {
var percent = ((_completedTests() / _foundTests) * 100).toInt().toString();
var progressPadded = _pad(_allTestsKnown ? percent : '--', 3);
var passedPadded = _pad(_passedTests.toString(), 5);
var failedPadded = _pad(_failedTests.toString(), 5);
Duration d = (new DateTime.now()).difference(_startTime);
var progressLine = '\r[${_timeString(d)} | $progressPadded% | '
'+${_formatter.passed(passedPadded)} | '
'-${_formatter.failed(failedPadded)}]';
stdout.write(progressLine);
}
}
class VerboseProgressIndicator extends ProgressIndicator {
VerboseProgressIndicator(DateTime startTime) : super(startTime);
void _printDoneProgress(TestCase test) {
var status = 'pass';
if (test.unexpectedOutput) {
status = 'fail';
}
print('Done ${test.configurationString} ${test.displayName}: $status');
}
}
class BuildbotProgressIndicator extends ProgressIndicator {
static String stepName;
var _failureSummary = <String>[];
BuildbotProgressIndicator(DateTime startTime) : super(startTime);
void done(TestCase test) {
super.done(test);
if (test.unexpectedOutput) {
_failureSummary.addAll(_buildFailureOutput(test));
}
}
void _printDoneProgress(TestCase test) {
var status = 'pass';
if (test.unexpectedOutput) {
status = 'fail';
}
var percent = ((_completedTests() / _foundTests) * 100).toInt().toString();
print('Done ${test.configurationString} ${test.displayName}: $status');
print('@@@STEP_CLEAR@@@');
print('@@@STEP_TEXT@ $percent% +$_passedTests -$_failedTests @@@');
}
void allDone() {
if (!_failureSummary.isEmpty) {
print('@@@STEP_FAILURE@@@');
if (stepName != null) {
print('@@@BUILD_STEP $stepName failures@@@');
}
for (String line in _failureSummary) {
print(line);
}
print('');
}
print(_buildSummaryEnd(_failedTests));
}
}
EventListener progressIndicatorFromName(
String name, DateTime startTime, Formatter formatter) {
switch (name) {
case 'compact':
return new CompactProgressIndicator(startTime, formatter);
case 'line':
return new LineProgressIndicator();
case 'verbose':
return new VerboseProgressIndicator(startTime);
case 'status':
return new ProgressIndicator(startTime);
case 'buildbot':
return new BuildbotProgressIndicator(startTime);
default:
assert(false);
break;
}
}