blob: 8f816d5af354b4220f7fd40cb1a628fd59001633 [file] [log] [blame]
// Copyright (c) 2017, 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.
/// Script that updates dart2js status lines automatically for tests under the
/// '$fasta' configuration.
///
/// This script is hardcoded to only support this configuration and relies on
/// a convention for how the status files are structured, In particular,
/// every status file for dart2js should have 3 sections:
///
/// [ $compiler == dart2js && $fasta && $host_checked ]
///
/// and:
///
/// [ $compiler == dart2js && $fasta && $minified ]
///
/// and:
///
/// [ $compiler == dart2js && $fasta && $fast_startup ]
///
/// and:
///
/// [ $compiler == dart2js && $checked && $fasta ]
library compiler.status_files.update_from_log;
import 'dart:io';
import 'record.dart';
import 'log_parser.dart';
final dart2jsConfigurations = {
'host-checked': r'[ $compiler == dart2js && $fasta && $host_checked ]',
'minified': r'[ $compiler == dart2js && $fasta && $minified ]',
'host-checked-strong':
r'[ $compiler == dart2js && $fasta && $host_checked && $strong ]',
'minified-strong':
r'[ $compiler == dart2js && $fasta && $minified && $strong ]',
'fast-startup': r'[ $compiler == dart2js && $fast_startup && $fasta ]',
'fast-startup-strong':
r'[ $compiler == dart2js && $fast_startup && $fasta && $strong ]',
'checked-mode': r'[ $compiler == dart2js && $checked && $fasta ]',
'checked-mode-strong':
r'[ $compiler == dart2js && $checked && $fasta && $strong ]',
};
final dart2jsStatusFiles = {
'language': 'tests/language/language_dart2js.status',
'corelib': 'tests/corelib/corelib.status',
'language_2': 'tests/language_2/language_2_dart2js.status',
// TODO(sigmund,rnystrom): update when corelib_2 gets split into multiple
// status files.
'corelib_2': 'tests/corelib_2/corelib_2.status',
'dart2js_extra': 'tests/compiler/dart2js_extra/dart2js_extra.status',
'dart2js_native': 'tests/compiler/dart2js_native/dart2js_native.status',
'html': 'tests/html/html.status',
};
main(args) {
mainInternal(args, dart2jsConfigurations, dart2jsStatusFiles);
}
/// Note: this is called above and also from
/// pkg/front_end/tool/status_files/update_from_log.dart
mainInternal(List<String> args, Map<String, String> configurations,
Map<String, String> statusFiles) {
if (args.length < 2) {
print('usage: update_from_log.dart <mode> log.txt [message-in-quotes]');
print(' where mode is one of these values: ${configurations.keys}');
exit(1);
}
var mode = args[0];
if (!configurations.containsKey(mode)) {
print('invalid mode: $mode, expected one in ${configurations.keys}');
exit(1);
}
var uri =
Uri.base.resolveUri(new Uri.file(args[1], windows: Platform.isWindows));
var file = new File.fromUri(uri);
if (!file.existsSync()) {
print('file not found: $file');
exit(1);
}
var globalReason = args.length > 2 ? args[2] : null;
updateLogs(
mode, file.readAsStringSync(), configurations, statusFiles, globalReason);
}
/// Update all status files based on the [log] records when running the compiler
/// in [mode]. If provided [globalReason] is added as a comment to new test
/// failures. If not, an automated reason might be extracted from the test
/// failure message.
void updateLogs(String mode, String log, Map<String, String> configurations,
Map<String, String> statusFiles, String globalReason) {
List<Record> records = parse(log);
records.sort();
var last;
ConfigurationInSuiteSection section;
for (var record in records) {
if (last == record) continue; // ignore duplicates
if (section?.suite != record.suite) {
section?.update(globalReason);
var statusFile = statusFiles[record.suite];
if (statusFile == null) {
print("No status file for suite '${record.suite}'.");
continue;
}
var condition = configurations[mode];
section = ConfigurationInSuiteSection.create(
record.suite, mode, statusFile, condition);
}
section.add(record);
last = record;
}
section?.update(globalReason);
}
/// Represents an existing entry in the logs.
class ExistingEntry {
final String test;
final String status;
final bool hasComment;
ExistingEntry(this.test, this.status, this.hasComment);
static parse(String line) {
var colonIndex = line.indexOf(':');
var test = line.substring(0, colonIndex);
var status = line.substring(colonIndex + 1).trim();
var commentIndex = status.indexOf("#");
if (commentIndex != -1) {
status = status.substring(0, commentIndex);
}
return new ExistingEntry(test, status, commentIndex != -1);
}
}
/// Represents a section in a .status file that corresponds to a specific suite
/// and configuration.
class ConfigurationInSuiteSection {
final String suite;
final String _statusFile;
final String _contents;
final int _begin;
final int _end;
final List<Record> _records = [];
ConfigurationInSuiteSection(
this.suite, this._statusFile, this._contents, this._begin, this._end);
/// Add a new test record, indicating that the test status should be updated.
void add(Record record) => _records.add(record);
/// Update the section in the file.
///
/// This will reflect the new status lines as recorded in [_records].
void update(String providedReason) {
int changes = 0;
int ignored = 0;
var originalEntries = _contents.substring(_begin, _end).split('\n');
// The algorithm below walks entries in the file and from the log in the
// same order: preserving entries that didn't change, and updating entries
// where the logs show that the test status changed.
// Sort the file contents in case the file has been tampered with.
originalEntries.sort();
/// Re-sort records by name (they came sorted by suite and status first, so
/// it may be wrong for the merging below).
_records.sort((a, b) => a.test.compareTo(b.test));
var newContents = new StringBuffer();
newContents.write(_contents.substring(0, _begin));
addFromRecord(Record record) {
var reason = providedReason ?? record.reason;
var comment = reason != null && reason.isNotEmpty ? ' # ${reason}' : '';
newContents.writeln('${record.test}: ${record.actual}$comment');
}
int i = 0, j = 0;
while (i < originalEntries.length && j < _records.length) {
var existingLine = originalEntries[i];
if (existingLine.trim().isEmpty) {
i++;
continue;
}
var existing = ExistingEntry.parse(existingLine);
var record = _records[j];
var compare = existing.test.compareTo(record.test);
if (compare < 0) {
// Existing test was unaffected, copy the status line.
newContents.writeln(existingLine);
i++;
} else if (compare > 0) {
// New entry, if it's a failure, we haven't seen this before and must
// add it. If the status says it is passing, we ignore it. We do this
// to support making this script idempotent if the patching has already
// been done.
if (!record.isPassing) {
// New failure never seen before
addFromRecord(record);
changes++;
}
j++;
} else if (existing.status == record.actual) {
if (!existing.hasComment && record.reason != null) {
addFromRecord(record);
changes++;
} else {
// This also should only happen if the patching has already been done.
// We don't complain to make this script idempotent.
newContents.writeln(existingLine);
}
ignored++;
i++;
j++;
} else {
changes++;
// The status changed, if it is now passing, we omit the entry entirely,
// otherwise we use the status from the logs.
if (!record.isPassing) {
addFromRecord(record);
}
i++;
j++;
}
}
for (; i < originalEntries.length; i++) {
newContents.writeln(originalEntries[i]);
}
for (; j < _records.length; j++) {
changes++;
addFromRecord(_records[j]);
}
newContents.write('\n');
newContents.write(_contents.substring(_end));
new File(_statusFile).writeAsStringSync('$newContents');
print("updated '$_statusFile' with $changes changes");
if (ignored > 0) {
print(' $ignored changes were already applied in the status file.');
}
}
static ConfigurationInSuiteSection create(
String suite, String mode, String statusFile, String condition) {
var contents = new File(statusFile).readAsStringSync();
int sectionDeclaration = contents.indexOf(condition);
if (sectionDeclaration == -1) {
print('error: unable to find condition $condition in $statusFile');
exit(1);
}
int begin = contents.indexOf('\n', sectionDeclaration) + 1;
assert(begin != 0);
int newlinePos = contents.indexOf('\n', begin + 1);
int end = newlinePos;
while (true) {
if (newlinePos == -1) break;
if (newlinePos + 1 < contents.length) {
if (contents[newlinePos + 1] == '[') {
// We've found the end of the section
break;
} else if (contents[newlinePos + 1] == '#') {
// We've found a commented out line. This line might belong to the
// next section.
newlinePos = contents.indexOf('\n', newlinePos + 1);
continue;
}
}
// We've found an ordinary line. It's part of this section, so update
// end.
newlinePos = contents.indexOf('\n', newlinePos + 1);
end = newlinePos;
}
end = end == -1 ? contents.length : end + 1;
return new ConfigurationInSuiteSection(
suite, statusFile, contents, begin, end);
}
}