blob: 8dacd5aeb7f47eed2f6e93a2687713059463dee7 [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
/// '$dart2js_with_kernel' 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 2 sections:
///
/// [ $compiler == dart2js && $dart2js_with_kernel && $host_checked ]
///
/// and:
///
/// [ $compiler == dart2js && $dart2js_with_kernel && $minified ]
library status_files.update_from_log;
import 'dart:io';
import 'record.dart';
import 'log_parser.dart';
final configurations = {
'checked':
r'[ $compiler == dart2js && $dart2js_with_kernel && $host_checked ]',
'minified': r'[ $compiler == dart2js && $dart2js_with_kernel && $minified ]',
};
final statusFiles = {
'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) {
if (args.length < 2) {
print('usage: update_from_log.dart <mode> log.txt');
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.resolve(args[1]);
var file = new File.fromUri(uri);
if (!file.existsSync()) {
print('file not found: $file');
exit(1);
}
updateLogs(mode, file.readAsStringSync());
}
/// Update all status files based on the [log] records when running the compiler
/// in [mode].
void updateLogs(String mode, String log) {
List<Record> records = parse(log);
records.sort();
var last;
var section;
for (var record in records) {
if (last == record) continue; // ignore duplicates
if (section?.suite != record.suite) {
section?.update();
section = ConfigurationInSuiteSection.create(record.suite, mode);
}
section.add(record);
last = record;
}
section?.update();
}
/// 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() {
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.
// Records are already sorted, but we sort the file contents in case the
// file has been tampered with.
originalEntries.sort();
var newContents = new StringBuffer();
newContents.write(_contents.substring(0, _begin));
addFromRecord(Record record) {
var comment = record.reason != null ? ' # ${record.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) {
var statusFile = statusFiles[suite];
var contents = new File(statusFile).readAsStringSync();
var condition = configurations[mode];
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 end = contents.indexOf('\n[', begin + 1);
end = end == -1 ? contents.length : end + 1;
return new ConfigurationInSuiteSection(
suite, statusFile, contents, begin, end);
}
}