blob: 4d15a4df883d7bed3e66d6e731b9a47da13b55cb [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.
import 'package:path/path.dart' as p;
import 'editable_status_file.dart';
import 'fork.dart';
import 'io.dart';
import 'log.dart';
import 'test_directories.dart';
/// Migrates the status file entries that match [files].
void migrateStatusEntries(List<Fork> files, Map<String, List<String>> todos) {
var entriesToMove = new EntrySet();
_collectEntries(files, entriesToMove, isOne: true);
_collectEntries(files, entriesToMove, isOne: false);
for (var statusFile in entriesToMove.statusFiles) {
var sections = entriesToMove.sections(statusFile);
_addEntries(statusFile, sections);
}
// If any entries need manual splitting, let the user know.
for (var statusFile in entriesToMove._todoHeaders.keys) {
var headers = entriesToMove._todoHeaders[statusFile];
var splits = headers.map((header) {
var files = filesForHeader(header).map((file) => bold(file)).join(", ");
return "Manually split status file section across $files status files:\n"
" $header";
}).toList();
if (splits.isNotEmpty) todos[statusFile] = splits;
}
}
/// Given the header for a status file section, looks at the condition
/// expression to determine which status files it should go in.
Set<String> filesForHeader(String header) {
// Figure out which status file it goes into.
var result = new Set<String>();
// The various compilers are roughly separate products.
const compilers = const {
r"$compiler == dart2analyzer": "analyzer",
r"$compiler == dart2js": "dart2js",
r"$compiler == dartdevc": "dartdevc",
// This deliberately matches both dartk and dartkp.
r"$compiler == dartk": "kernel",
r"$compiler == precompiler": "precompiled"
};
// TODO(rnystrom): This is obviously very sensitive to the formatting of
// the expression. Hacky, but hopefully good enough for now.
compilers.forEach((compiler, file) {
if (header.contains(compiler)) result.add(file);
});
// If we couldn't figure out where to put it based on the compiler, look at
// the runtime.
if (result.isEmpty) {
const runtimes = const {
r"$runtime == vm": "vm",
r"$runtime == flutter": "flutter",
r"$runtime == dart_precompiled": "precompiled",
};
runtimes.forEach((runtime, file) {
if (header.contains(runtime)) result.add(file);
});
}
return result;
}
/// Tracks a set of entries to add to a set of Dart 2.0 status files.
class EntrySet {
/// Keys are the names of the Dart 2.0 status file that will receive the
/// entries. The value for each key is a map of section headers to the list
/// of entries to add under that section.
final Map<String, Map<String, List<String>>> _files = {};
final _todoHeaders = <String, Set<String>>{};
Iterable<String> get statusFiles => _files.keys;
/// Attempts to add the [entry] under [header] in a status file in [fromDir]
/// to this EntrySet.
///
/// Returns true if successful or false if the header's condition doesn't fit
/// into a single status file and needs to be manually split by the user.
bool add(String fromFile, String fromDir, String header, String entry) {
// Since we're migrating isolate and html directories into lib_2 instead of
// isolate_2 and html_2, the status file entries are moved from
// {isolate,html}.status -> lib_2_*.status. This checks to see if we're
// handling these special directories and sets the 'to directory' to lib_2
// instead of isolate_2 or html_2.
String toDir;
if ((fromDir == "isolate") || (fromDir == "html")) {
toDir = "lib_2";
entry = p.join(fromDir, entry);
} else {
toDir = toTwoDirectory(fromDir);
}
// Figure out which status file it goes into.
var possibleFiles = filesForHeader(header);
var destination = "$toDir.status";
if (possibleFiles.length > 1) {
// The condition matches multiple files, so the user is going to have to
// manually split it up into multiple sections first.
// TODO(rnystrom): Would be good to automate this, though it requires
// being able to work with condition expressions directly.
var statusRelative = p.relative(fromFile, from: testRoot);
_todoHeaders.putIfAbsent(statusRelative, () => new Set()).add(header);
return false;
}
// The main "_strong.status" files skip lots of tests that are or were not
// strong mode clean. We don't want to skip those tests in 2.0 -- we want
// to fix them. If we're in that header, do remove the entry from the old
// file, but don't add it to the new one.
if (header == "[ \$strong ]") return true;
// If the condition places it directly into one file, put it there.
if (possibleFiles.length == 1) {
destination = "${toDir}_${possibleFiles.single}.status";
}
var sections = _files.putIfAbsent(p.join(toDir, destination), () => {});
var entries = sections.putIfAbsent(header, () => []);
entries.add(entry);
return true;
}
Map<String, List<String>> sections(String file) => _files[file];
}
/// Removes entries from the 1.0 and strong status files that correspond to
/// the list of [files] being migrated.
///
/// Adds moved entries to [entriesToMove].
void _collectEntries(List<Fork> files, EntrySet entriesToMove, {bool isOne}) {
// Map the files to the way they will appear in the status file.
var filePaths = files
.map((fork) => p.withoutExtension(isOne ? fork.onePath : fork.strongPath))
.toList();
for (var fromDir in isOne ? oneRootDirs : strongRootDirs) {
for (var path in listFiles(fromDir, extensions: [".status"])) {
// Don't copy entries from the special "_parser" status files because
// they use an unsupported compiler name.
if (path.contains("_parser.status")) continue;
var editable = new EditableStatusFile(path);
var deleteLines = <int>[];
for (var section in editable.statusFile.sections) {
// TODO(rnystrom): For now, we don't support entries in the initial
// implicit section at the top of the file. Do we need to?
if (section.condition == null) continue;
for (var entry in section.entries) {
var entryPath = p.join(fromDir, entry.path);
for (var filePath in filePaths) {
// We only support entries that precisely match the file being
// migrated, or a multitest within that. In both cases, the entry
// path will begin with the full path of the file. We don't migrate
// directory or glob patterns because those may also match other
// files that have not been migrated yet.
// TODO(rnystrom): It would be good to detect when a glob matches
// a migrated file and let the user know that they may need to
// manually handle it.
if (!entryPath.startsWith(filePath)) continue;
// Add it to the 2.0 one.
if (entriesToMove.add(
path,
fromDir,
editable.lineAt(section.lineNumber),
editable.lineAt(entry.lineNumber))) {
// Remove it from the original status file.
deleteLines.add(entry.lineNumber - 1);
}
}
}
}
// TODO(rnystrom): If all of the entries are deleted from a section, it
// would be nice to delete the section header too.
// We don't delete entries from the 1.0 status files so that we can keep
// testing 1.0, but we do from the "_strong" directories since those
// should get migrated fully into "_2".
if (!isOne) editable.delete(deleteLines);
}
}
}
/// Adds all of [entries] to the status file at [path].
///
/// If the status file already has a section that matches a header in [entries],
/// then adds those entries to the end of that section. Otherwise, appends a
/// new section to the end of the file.
void _addEntries(String path, Map<String, List<String>> entries) {
var editable = new EditableStatusFile(p.join(testRoot, path));
for (var header in entries.keys) {
var found = false;
// Look for an existing section with the same header to add it to.
for (var section in editable.statusFile.sections) {
if (header == editable.lineAt(section.lineNumber)) {
var line = section.lineNumber;
// Add after existing entries, if there are any.
if (section.entries.isNotEmpty) {
line = section.entries.last.lineNumber;
}
editable.insert(line, entries[header]);
found = true;
break;
}
}
if (!found) {
// This section doesn't exist in the status file, so add it.
editable.append(header, entries[header]);
}
}
}