f780b6ea84
Removes the flaky log, result.log result format, test outcome log, and --append-logs option from test.py. They are replaced by the new --write-results and --write-logs options. The debug log is now always appended to, if it is written to a file. Change-Id: I20dbbc1db86ef23a19ac4ea02067f18ae4906b3d Reviewed-on: https://dart-review.googlesource.com/c/85440 Reviewed-by: Jonas Termansen <sortie@google.com>
563 lines
18 KiB
Dart
563 lines
18 KiB
Dart
// 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.
|
|
|
|
import 'dart:async';
|
|
import 'dart:io';
|
|
import 'dart:convert';
|
|
|
|
import 'configuration.dart';
|
|
import 'path.dart';
|
|
|
|
// This is the maximum time we expect stdout/stderr of subprocesses to deliver
|
|
// data after we've got the exitCode.
|
|
const Duration MAX_STDIO_DELAY = const Duration(seconds: 30);
|
|
|
|
String MAX_STDIO_DELAY_PASSED_MESSAGE =
|
|
"""Not waiting for stdout/stderr from subprocess anymore
|
|
($MAX_STDIO_DELAY passed). Please note that this could be an indicator
|
|
that there is a hanging process which we were unable to kill.""";
|
|
|
|
/// The names of the packages that are available for use in tests.
|
|
const testPackages = const [
|
|
"async_helper",
|
|
"collection",
|
|
"expect",
|
|
"js",
|
|
"matcher",
|
|
"meta",
|
|
"path",
|
|
"stack_trace",
|
|
"unittest"
|
|
];
|
|
|
|
class DebugLogger {
|
|
static IOSink _sink;
|
|
|
|
/**
|
|
* If [path] was null, the DebugLogger will write messages to stdout.
|
|
*/
|
|
static void init(Path path) {
|
|
if (path != null) {
|
|
_sink = new File(path.toNativePath()).openWrite(mode: FileMode.append);
|
|
}
|
|
}
|
|
|
|
static void close() {
|
|
if (_sink != null) {
|
|
_sink.close();
|
|
_sink = null;
|
|
}
|
|
}
|
|
|
|
static String _formatErrorMessage(String msg, error) {
|
|
if (error == null) return msg;
|
|
msg += ": $error";
|
|
// TODO(floitsch): once the dart-executable that is bundled
|
|
// with the Dart sources is updated, pass a trace parameter too and do:
|
|
// if (trace != null) msg += "\nStackTrace: $trace";
|
|
return msg;
|
|
}
|
|
|
|
static void info(String msg, [error]) {
|
|
msg = _formatErrorMessage(msg, error);
|
|
_print("$_datetime Info: $msg");
|
|
}
|
|
|
|
static void warning(String msg, [error]) {
|
|
msg = _formatErrorMessage(msg, error);
|
|
_print("$_datetime Warning: $msg");
|
|
}
|
|
|
|
static void error(String msg, [error]) {
|
|
msg = _formatErrorMessage(msg, error);
|
|
_print("$_datetime Error: $msg");
|
|
}
|
|
|
|
static void _print(String msg) {
|
|
if (_sink != null) {
|
|
_sink.writeln(msg);
|
|
} else {
|
|
print(msg);
|
|
}
|
|
}
|
|
|
|
static String get _datetime => "${new DateTime.now()}";
|
|
}
|
|
|
|
String prettifyJson(Object json, {int startIndentation: 0, int shiftWidth: 6}) {
|
|
int currentIndentation = startIndentation;
|
|
var buffer = new StringBuffer();
|
|
|
|
String indentationString() {
|
|
return new List.filled(currentIndentation, ' ').join('');
|
|
}
|
|
|
|
addString(String s, {bool indentation: true, bool newLine: true}) {
|
|
if (indentation) {
|
|
buffer.write(indentationString());
|
|
}
|
|
buffer.write(s.replaceAll("\n", "\n${indentationString()}"));
|
|
if (newLine) buffer.write("\n");
|
|
}
|
|
|
|
prettifyJsonInternal(Object obj,
|
|
{bool indentation: true, bool newLine: true}) {
|
|
if (obj is List) {
|
|
addString("[", indentation: indentation);
|
|
currentIndentation += shiftWidth;
|
|
for (var item in obj) {
|
|
prettifyJsonInternal(item, indentation: indentation, newLine: false);
|
|
addString(",", indentation: false);
|
|
}
|
|
currentIndentation -= shiftWidth;
|
|
addString("]", indentation: indentation);
|
|
} else if (obj is Map) {
|
|
addString("{", indentation: indentation);
|
|
currentIndentation += shiftWidth;
|
|
for (var key in obj.keys) {
|
|
addString("$key: ", indentation: indentation, newLine: false);
|
|
currentIndentation += shiftWidth;
|
|
prettifyJsonInternal(obj[key], indentation: false);
|
|
currentIndentation -= shiftWidth;
|
|
}
|
|
currentIndentation -= shiftWidth;
|
|
addString("}", indentation: indentation, newLine: newLine);
|
|
} else {
|
|
addString("$obj", indentation: indentation, newLine: newLine);
|
|
}
|
|
}
|
|
|
|
prettifyJsonInternal(json);
|
|
return buffer.toString();
|
|
}
|
|
|
|
/**
|
|
* [areByteArraysEqual] compares a range of bytes from [buffer1] with a
|
|
* range of bytes from [buffer2].
|
|
*
|
|
* Returns [true] if the [count] bytes in [buffer1] (starting at
|
|
* [offset1]) match the [count] bytes in [buffer2] (starting at
|
|
* [offset2]).
|
|
* Otherwise [false] is returned.
|
|
*/
|
|
bool areByteArraysEqual(
|
|
List<int> buffer1, int offset1, List<int> buffer2, int offset2, int count) {
|
|
if ((offset1 + count) > buffer1.length ||
|
|
(offset2 + count) > buffer2.length) {
|
|
return false;
|
|
}
|
|
|
|
for (var i = 0; i < count; i++) {
|
|
if (buffer1[offset1 + i] != buffer2[offset2 + i]) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* [findBytes] searches for [pattern] in [data] beginning at [startPos].
|
|
*
|
|
* Returns [true] if [pattern] was found in [data].
|
|
* Otherwise [false] is returned.
|
|
*/
|
|
int findBytes(List<int> data, List<int> pattern, [int startPos = 0]) {
|
|
// TODO(kustermann): Use one of the fast string-matching algorithms!
|
|
for (int i = startPos; i < (data.length - pattern.length); i++) {
|
|
bool found = true;
|
|
for (int j = 0; j < pattern.length; j++) {
|
|
if (data[i + j] != pattern[j]) {
|
|
found = false;
|
|
break;
|
|
}
|
|
}
|
|
if (found) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
List<int> encodeUtf8(String string) {
|
|
return utf8.encode(string);
|
|
}
|
|
|
|
// TODO(kustermann,ricow): As soon we have a debug log we should log
|
|
// invalid utf8-encoded input to the log.
|
|
// Currently invalid bytes will be replaced by a replacement character.
|
|
String decodeUtf8(List<int> bytes) {
|
|
return utf8.decode(bytes, allowMalformed: true);
|
|
}
|
|
|
|
/// Given a chunk of UTF-8 output, splits it into lines, normalizes carriage
|
|
/// returns, and deletes and trailing and leading whitespace.
|
|
List<String> decodeLines(List<int> output) {
|
|
return decodeUtf8(output)
|
|
.replaceAll('\r\n', '\n')
|
|
.replaceAll('\r', '\n')
|
|
.trim()
|
|
.split('\n');
|
|
}
|
|
|
|
String indent(String string, int numSpaces) {
|
|
var spaces = new List.filled(numSpaces, ' ').join('');
|
|
return string
|
|
.replaceAll('\r\n', '\n')
|
|
.split('\n')
|
|
.map((line) => "$spaces$line")
|
|
.join('\n');
|
|
}
|
|
|
|
/// Convert [duration] to a short but precise human-friendly string.
|
|
String niceTime(Duration duration) {
|
|
String digits(int count, int n, int period) {
|
|
n = n.remainder(period).toInt();
|
|
return n.toString().padLeft(count, "0");
|
|
}
|
|
|
|
var minutes = digits(2, duration.inMinutes, Duration.minutesPerHour);
|
|
var seconds = digits(2, duration.inSeconds, Duration.secondsPerMinute);
|
|
var millis =
|
|
digits(6, duration.inMilliseconds, Duration.millisecondsPerSecond);
|
|
|
|
if (duration.inHours >= 1) {
|
|
return "${duration.inHours}:${minutes}:${seconds}s";
|
|
} else if (duration.inMinutes >= 1) {
|
|
return "${minutes}:${seconds}.${millis}s";
|
|
} else if (duration.inSeconds >= 1) {
|
|
return "${seconds}.${millis}s";
|
|
} else {
|
|
return "${duration.inMilliseconds}ms";
|
|
}
|
|
}
|
|
|
|
// This function is pretty stupid and only puts quotes around an argument if
|
|
// it the argument contains a space.
|
|
String escapeCommandLineArgument(String argument) {
|
|
if (argument.contains(' ')) {
|
|
return '"$argument"';
|
|
}
|
|
return argument;
|
|
}
|
|
|
|
class HashCodeBuilder {
|
|
int _value = 0;
|
|
|
|
void add(Object object) {
|
|
_value = ((_value * 31) ^ object.hashCode) & 0x3FFFFFFF;
|
|
}
|
|
|
|
void addJson(Object object) {
|
|
if (object == null ||
|
|
object is num ||
|
|
object is String ||
|
|
object is Uri ||
|
|
object is bool) {
|
|
add(object);
|
|
} else if (object is List) {
|
|
object.forEach(addJson);
|
|
} else if (object is Map) {
|
|
for (var key in object.keys.toList()..sort()) {
|
|
addJson(key);
|
|
addJson(object[key]);
|
|
}
|
|
} else {
|
|
throw new Exception("Can't build hashcode for non json-like object "
|
|
"(${object.runtimeType})");
|
|
}
|
|
}
|
|
|
|
int get value => _value;
|
|
}
|
|
|
|
bool deepJsonCompare(Object a, Object b) {
|
|
if (a == null || a is num || a is String || a is Uri) {
|
|
return a == b;
|
|
} else if (a is List) {
|
|
if (b is List) {
|
|
if (a.length != b.length) return false;
|
|
|
|
for (int i = 0; i < a.length; i++) {
|
|
if (!deepJsonCompare(a[i], b[i])) return false;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
} else if (a is Map) {
|
|
if (b is Map) {
|
|
if (a.length != b.length) return false;
|
|
|
|
for (var key in a.keys) {
|
|
if (!b.containsKey(key)) return false;
|
|
if (!deepJsonCompare(a[key], b[key])) return false;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
} else {
|
|
throw new Exception("Can't compare two non json-like objects "
|
|
"(a: ${a.runtimeType}, b: ${b.runtimeType})");
|
|
}
|
|
}
|
|
|
|
class UniqueObject {
|
|
static int _nextId = 1;
|
|
final int _hashCode;
|
|
|
|
int get hashCode => _hashCode;
|
|
operator ==(Object other) =>
|
|
other is UniqueObject && _hashCode == other._hashCode;
|
|
|
|
UniqueObject() : _hashCode = ++_nextId;
|
|
}
|
|
|
|
class LastModifiedCache {
|
|
Map<String, DateTime> _cache = <String, DateTime>{};
|
|
|
|
/**
|
|
* Returns the last modified date of the given [uri].
|
|
*
|
|
* The return value will be cached for future queries. If [uri] is a local
|
|
* file, it's last modified [Date] will be returned. If the file does not
|
|
* exist, null will be returned instead.
|
|
* In case [uri] is not a local file, this method will always return
|
|
* the current date.
|
|
*/
|
|
DateTime getLastModified(Uri uri) {
|
|
if (uri.scheme == "file") {
|
|
if (_cache.containsKey(uri.path)) {
|
|
return _cache[uri.path];
|
|
}
|
|
var file = new File(new Path(uri.path).toNativePath());
|
|
_cache[uri.path] = file.existsSync() ? file.lastModifiedSync() : null;
|
|
return _cache[uri.path];
|
|
}
|
|
return new DateTime.now();
|
|
}
|
|
}
|
|
|
|
class ExistsCache {
|
|
Map<String, bool> _cache = <String, bool>{};
|
|
|
|
/**
|
|
* Returns true if the file in [path] exists, false otherwise.
|
|
*
|
|
* The information will be cached.
|
|
*/
|
|
bool doesFileExist(String path) {
|
|
if (!_cache.containsKey(path)) {
|
|
_cache[path] = new File(path).existsSync();
|
|
}
|
|
return _cache[path];
|
|
}
|
|
}
|
|
|
|
class TestUtils {
|
|
static LastModifiedCache lastModifiedCache = new LastModifiedCache();
|
|
static ExistsCache existsCache = new ExistsCache();
|
|
|
|
/**
|
|
* Creates a directory using a [relativePath] to an existing
|
|
* [base] directory if that [relativePath] does not already exist.
|
|
*/
|
|
static Directory mkdirRecursive(Path base, Path relativePath) {
|
|
if (relativePath.isAbsolute) {
|
|
base = new Path('/');
|
|
}
|
|
Directory dir = new Directory(base.toNativePath());
|
|
assert(dir.existsSync());
|
|
var segments = relativePath.segments();
|
|
for (String segment in segments) {
|
|
base = base.append(segment);
|
|
if (base.toString() == "/$segment" &&
|
|
segment.length == 2 &&
|
|
segment.endsWith(':')) {
|
|
// Skip the directory creation for a path like "/E:".
|
|
continue;
|
|
}
|
|
dir = new Directory(base.toNativePath());
|
|
if (!dir.existsSync()) {
|
|
dir.createSync();
|
|
}
|
|
assert(dir.existsSync());
|
|
}
|
|
return dir;
|
|
}
|
|
|
|
/**
|
|
* Keep a map of files copied to avoid race conditions.
|
|
*/
|
|
static Map<String, Future> _copyFilesMap = {};
|
|
|
|
/**
|
|
* Copy a [source] file to a new place.
|
|
* Assumes that the directory for [dest] already exists.
|
|
*/
|
|
static Future copyFile(Path source, Path dest) {
|
|
return _copyFilesMap.putIfAbsent(dest.toNativePath(),
|
|
() => new File(source.toNativePath()).copy(dest.toNativePath()));
|
|
}
|
|
|
|
static Future copyDirectory(String source, String dest) {
|
|
source = new Path(source).toNativePath();
|
|
dest = new Path(dest).toNativePath();
|
|
|
|
var executable = 'cp';
|
|
var args = ['-Rp', source, dest];
|
|
if (Platform.operatingSystem == 'windows') {
|
|
executable = 'xcopy';
|
|
args = [source, dest, '/e', '/i'];
|
|
}
|
|
return Process.run(executable, args).then((ProcessResult result) {
|
|
if (result.exitCode != 0) {
|
|
throw new Exception("Failed to execute '$executable "
|
|
"${args.join(' ')}'.");
|
|
}
|
|
});
|
|
}
|
|
|
|
static Future deleteDirectory(String path) {
|
|
// We are seeing issues with long path names on windows when
|
|
// deleting them. Use the system tools to delete our long paths.
|
|
// See issue 16264.
|
|
if (Platform.operatingSystem == 'windows') {
|
|
var native_path = new Path(path).toNativePath();
|
|
// Running this in a shell sucks, but rmdir is not part of the standard
|
|
// path.
|
|
return Process.run('rmdir', ['/s', '/q', native_path], runInShell: true)
|
|
.then((ProcessResult result) {
|
|
if (result.exitCode != 0) {
|
|
throw new Exception('Can\'t delete path $native_path. '
|
|
'This path might be too long');
|
|
}
|
|
});
|
|
} else {
|
|
var dir = new Directory(path);
|
|
return dir.delete(recursive: true);
|
|
}
|
|
}
|
|
|
|
static void deleteTempSnapshotDirectory(TestConfiguration configuration) {
|
|
if (configuration.compiler == Compiler.appJit ||
|
|
configuration.compiler == Compiler.precompiler ||
|
|
configuration.compiler == Compiler.dartk ||
|
|
configuration.compiler == Compiler.dartkb ||
|
|
configuration.compiler == Compiler.dartkp) {
|
|
var checked = configuration.isChecked ? '-checked' : '';
|
|
var legacy = configuration.noPreviewDart2 ? '-legacy' : '';
|
|
var minified = configuration.isMinified ? '-minified' : '';
|
|
var csp = configuration.isCsp ? '-csp' : '';
|
|
var sdk = configuration.useSdk ? '-sdk' : '';
|
|
var dirName = "${configuration.compiler.name}"
|
|
"$checked$legacy$minified$csp$sdk";
|
|
var generatedPath =
|
|
configuration.buildDirectory + "/generated_compilations/$dirName";
|
|
if (FileSystemEntity.isDirectorySync(generatedPath)) {
|
|
TestUtils.deleteDirectory(generatedPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
static final debugLogFilePath = new Path(".debug.log");
|
|
|
|
/// If test.py was invoked with '--write-results' it will write
|
|
/// test outcomes to this file in the '--output-directory'.
|
|
static const resultsFileName = "results.json";
|
|
|
|
/// If test.py was invoked with '--write-results' it will write
|
|
/// data about this run of test.py to this file in the '--output-directory'.
|
|
static const resultsInstanceFileName = "run.json";
|
|
|
|
/// If test.py was invoked with '--write-results' and '--write-logs", save
|
|
/// the stdout and stderr to this file in the '--output-directory'.
|
|
static const logsFileName = "logs.json";
|
|
|
|
static void ensureExists(String filename, TestConfiguration configuration) {
|
|
if (!configuration.listTests && !existsCache.doesFileExist(filename)) {
|
|
throw "'$filename' does not exist";
|
|
}
|
|
}
|
|
|
|
static int shortNameCounter = 0; // Make unique short file names on Windows.
|
|
|
|
static String getShortName(String path) {
|
|
const pathReplacements = const {
|
|
"tests_co19_src_Language_12_Expressions_14_Function_Invocation_":
|
|
"co19_fn_invoke_",
|
|
"tests_co19_src_LayoutTests_fast_css_getComputedStyle_getComputedStyle-":
|
|
"co19_css_getComputedStyle_",
|
|
"tests_co19_src_LayoutTests_fast_dom_Document_CaretRangeFromPoint_"
|
|
"caretRangeFromPoint-": "co19_caretrangefrompoint_",
|
|
"tests_co19_src_LayoutTests_fast_dom_Document_CaretRangeFromPoint_"
|
|
"hittest-relative-to-viewport_": "co19_caretrange_hittest_",
|
|
"tests_co19_src_LayoutTests_fast_dom_HTMLLinkElement_link-onerror-"
|
|
"stylesheet-with-": "co19_dom_link-",
|
|
"tests_co19_src_LayoutTests_fast_dom_": "co19_dom",
|
|
"tests_co19_src_LayoutTests_fast_canvas_webgl": "co19_canvas_webgl",
|
|
"tests_co19_src_LibTest_core_AbstractClassInstantiationError_"
|
|
"AbstractClassInstantiationError_": "co19_abstract_class_",
|
|
"tests_co19_src_LibTest_core_IntegerDivisionByZeroException_"
|
|
"IntegerDivisionByZeroException_": "co19_division_by_zero",
|
|
"tests_co19_src_WebPlatformTest_html_dom_documents_dom-tree-accessors_":
|
|
"co19_dom_accessors_",
|
|
"tests_co19_src_WebPlatformTest_html_semantics_embedded-content_"
|
|
"media-elements_": "co19_media_elements",
|
|
"tests_co19_src_WebPlatformTest_html_semantics_": "co19_semantics_",
|
|
"tests_co19_src_WebPlatformTest_html-templates_additions-to-"
|
|
"the-steps-to-clone-a-node_": "co19_htmltemplates_clone_",
|
|
"tests_co19_src_WebPlatformTest_html-templates_definitions_"
|
|
"template-contents-owner": "co19_htmltemplates_contents",
|
|
"tests_co19_src_WebPlatformTest_html-templates_parsing-html-"
|
|
"templates_additions-to-": "co19_htmltemplates_add_",
|
|
"tests_co19_src_WebPlatformTest_html-templates_parsing-html-"
|
|
"templates_appending-to-a-template_": "co19_htmltemplates_append_",
|
|
"tests_co19_src_WebPlatformTest_html-templates_parsing-html-"
|
|
"templates_clearing-the-stack-back-to-a-given-context_":
|
|
"co19_htmltemplates_clearstack_",
|
|
"tests_co19_src_WebPlatformTest_html-templates_parsing-html-"
|
|
"templates_creating-an-element-for-the-token_":
|
|
"co19_htmltemplates_create_",
|
|
"tests_co19_src_WebPlatformTest_html-templates_template-element"
|
|
"_template-": "co19_htmltemplates_element-",
|
|
"tests_co19_src_WebPlatformTest_html-templates_": "co19_htmltemplate_",
|
|
"tests_co19_src_WebPlatformTest_shadow-dom_shadow-trees_":
|
|
"co19_shadow-trees_",
|
|
"tests_co19_src_WebPlatformTest_shadow-dom_elements-and-dom-objects_":
|
|
"co19_shadowdom_",
|
|
"tests_co19_src_WebPlatformTest_shadow-dom_html-elements-in-"
|
|
"shadow-trees_": "co19_shadow_html_",
|
|
"tests_co19_src_WebPlatformTest_html_webappapis_system-state-and-"
|
|
"capabilities_the-navigator-object": "co19_webappapis_navigator_",
|
|
"tests_co19_src_WebPlatformTest_DOMEvents_approved_": "co19_dom_approved_"
|
|
};
|
|
|
|
// Some tests are already in [build_dir]/generated_tests.
|
|
var generated = 'generated_tests/';
|
|
if (path.contains(generated)) {
|
|
var index = path.indexOf(generated) + generated.length;
|
|
path = 'multitest/${path.substring(index)}';
|
|
}
|
|
|
|
path = path.replaceAll('/', '_');
|
|
var windowsShortenPathLimit = 58;
|
|
var windowsPathEndLength = 30;
|
|
if (Platform.operatingSystem == 'windows' &&
|
|
path.length > windowsShortenPathLimit) {
|
|
for (var key in pathReplacements.keys) {
|
|
if (path.startsWith(key)) {
|
|
path = path.replaceFirst(key, pathReplacements[key]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (path.length > windowsShortenPathLimit) {
|
|
shortNameCounter++;
|
|
var pathEnd = path.substring(path.length - windowsPathEndLength);
|
|
path = "short${shortNameCounter}_$pathEnd";
|
|
}
|
|
}
|
|
return path;
|
|
}
|
|
}
|