blob: 09b47fa78ea3c5d7b85de633546dd345ad46a0e9 [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 'dart:async';
import 'dart:io';
import 'package:compiler/src/colors.dart' as colors;
import 'package:compiler/src/common.dart';
import 'package:compiler/src/common_elements.dart';
import 'package:compiler/src/commandline_options.dart';
import 'package:compiler/src/compiler.dart';
import 'package:compiler/src/elements/entities.dart';
import 'package:compiler/src/elements/elements.dart';
import 'package:compiler/src/io/source_file.dart';
import 'package:compiler/src/source_file_provider.dart';
import 'package:compiler/src/world.dart';
import 'package:expect/expect.dart';
import 'package:sourcemap_testing/src/annotated_code_helper.dart';
import '../memory_compiler.dart';
import '../equivalence/id_equivalence.dart';
import '../kernel/test_helpers.dart';
/// `true` if ANSI colors are supported by stdout.
bool useColors = stdout.supportsAnsiEscapes;
/// Colorize a matching annotation [text], if ANSI colors are supported.
String colorizeMatch(String text) {
if (useColors) {
return '${colors.blue(text)}';
} else {
return text;
}
}
/// Colorize a single annotation [text], if ANSI colors are supported.
String colorizeSingle(String text) {
if (useColors) {
return '${colors.green(text)}';
} else {
return text;
}
}
/// Colorize diffs [left] and [right] and [delimiter], if ANSI colors are
/// supported.
String colorizeDiff(String left, String delimiter, String right) {
if (useColors) {
return '${colors.green(left)}'
'${colors.yellow(delimiter)}${colors.red(right)}';
} else {
return '$left$delimiter$right';
}
}
/// Colorize annotation delimiters [start] and [end] surrounding [text], if
/// ANSI colors are supported.
String colorizeAnnotation(String start, String text, String end) {
if (useColors) {
return '${colors.yellow(start)}$text${colors.yellow(end)}';
} else {
return '$start$text$end';
}
}
/// Function that computes a data mapping for [member].
///
/// Fills [actualMap] with the data and [sourceSpanMap] with the source spans
/// for the data origin.
typedef void ComputeMemberDataFunction(
Compiler compiler, MemberEntity member, Map<Id, ActualData> actualMap,
{bool verbose});
/// Function that computes a data mapping for [cls].
///
/// Fills [actualMap] with the data and [sourceSpanMap] with the source spans
/// for the data origin.
typedef void ComputeClassDataFunction(
Compiler compiler, ClassEntity cls, Map<Id, ActualData> actualMap,
{bool verbose});
const String stopAfterTypeInference = 'stopAfterTypeInference';
/// Compute actual data for all members defined in the program with the
/// [entryPoint] and [memorySourceFiles].
///
/// Actual data is computed using [computeMemberData].
Future<CompiledData> computeData(
Uri entryPoint,
Map<String, String> memorySourceFiles,
ComputeMemberDataFunction computeMemberData,
{List<String> options: const <String>[],
bool verbose: false,
bool forUserLibrariesOnly: true,
bool skipUnprocessedMembers: false,
bool skipFailedCompilations: false,
ComputeClassDataFunction computeClassData,
Iterable<Id> globalIds: const <Id>[]}) async {
CompilationResult result = await runCompiler(
entryPoint: entryPoint,
memorySourceFiles: memorySourceFiles,
options: options,
beforeRun: (compiler) {
compiler.stopAfterTypeInference =
options.contains(stopAfterTypeInference);
});
if (!result.isSuccess) {
if (skipFailedCompilations) return null;
Expect.isTrue(result.isSuccess, "Unexpected compilation error.");
}
Compiler compiler = result.compiler;
ClosedWorld closedWorld = compiler.backendClosedWorldForTesting;
ElementEnvironment elementEnvironment = closedWorld.elementEnvironment;
CommonElements commonElements = closedWorld.commonElements;
Map<Uri, Map<Id, ActualData>> actualMaps = <Uri, Map<Id, ActualData>>{};
Map<Id, ActualData> globalData = <Id, ActualData>{};
Map<Id, ActualData> actualMapFor(Entity entity) {
if (entity is Element) {
// TODO(johnniwinther): Remove this when patched members from kernel are
// no longer ascribed to the patch file.
Element element = entity;
entity = element.implementation;
}
SourceSpan span =
compiler.backendStrategy.spanFromSpannable(entity, entity);
Uri uri = resolveFastaUri(span.uri);
return actualMaps.putIfAbsent(uri, () => <Id, ActualData>{});
}
void processMember(MemberEntity member, Map<Id, ActualData> actualMap) {
if (member.isAbstract) {
return;
}
if (member is ConstructorElement && member.isRedirectingFactory) {
return;
}
if (skipUnprocessedMembers &&
!closedWorld.processedMembers.contains(member)) {
return;
}
if (member.enclosingClass != null) {
if (elementEnvironment.isEnumClass(member.enclosingClass)) {
if (member.isConstructor ||
member.isInstanceMember ||
member.name == 'values') {
return;
}
}
if (member.isConstructor &&
elementEnvironment.isMixinApplication(member.enclosingClass)) {
return;
}
}
computeMemberData(compiler, member, actualMap, verbose: verbose);
}
void processClass(ClassEntity cls, Map<Id, ActualData> actualMap) {
if (skipUnprocessedMembers && !closedWorld.isImplemented(cls)) {
return;
}
computeClassData(compiler, cls, actualMap, verbose: verbose);
}
bool excludeLibrary(LibraryEntity library) {
return forUserLibrariesOnly &&
(library.canonicalUri.scheme == 'dart' ||
library.canonicalUri.scheme == 'package');
}
if (computeClassData != null) {
for (LibraryEntity library in elementEnvironment.libraries) {
if (excludeLibrary(library)) continue;
elementEnvironment.forEachClass(library, (ClassEntity cls) {
processClass(cls, actualMapFor(cls));
});
}
}
for (MemberEntity member in closedWorld.processedMembers) {
if (excludeLibrary(member.library)) continue;
processMember(member, actualMapFor(member));
}
List<LibraryEntity> globalLibraries = <LibraryEntity>[
commonElements.coreLibrary,
elementEnvironment.lookupLibrary(Uri.parse('dart:collection')),
commonElements.interceptorsLibrary,
commonElements.jsHelperLibrary,
];
ClassEntity getGlobalClass(String className) {
ClassEntity cls;
for (LibraryEntity library in globalLibraries) {
cls ??= elementEnvironment.lookupClass(library, className);
}
Expect.isNotNull(
cls,
"Global class '$className' not found in the global "
"libraries: ${globalLibraries.map((l) => l.canonicalUri).join(', ')}");
return cls;
}
MemberEntity getGlobalMember(String memberName) {
MemberEntity member;
for (LibraryEntity library in globalLibraries) {
member ??= elementEnvironment.lookupLibraryMember(library, memberName);
}
Expect.isNotNull(
member,
"Global member '$member' not found in the global "
"libraries: ${globalLibraries.map((l) => l.canonicalUri).join(', ')}");
return member;
}
for (Id id in globalIds) {
if (id is ElementId) {
MemberEntity member;
if (id.className != null) {
ClassEntity cls = getGlobalClass(id.className);
member = elementEnvironment.lookupClassMember(cls, id.memberName);
member ??= elementEnvironment.lookupConstructor(cls, id.memberName);
Expect.isNotNull(
member, "Global member '$member' not found in class $cls.");
} else {
member = getGlobalMember(id.memberName);
}
processMember(member, globalData);
} else if (id is ClassId) {
if (computeClassData != null) {
ClassEntity cls = getGlobalClass(id.className);
processClass(cls, globalData);
}
} else {
throw new UnsupportedError("Unexpected global id: $id");
}
}
return new CompiledData(
compiler, elementEnvironment, entryPoint, actualMaps, globalData);
}
class CompiledData {
final Compiler compiler;
final ElementEnvironment elementEnvironment;
final Uri mainUri;
final Map<Uri, Map<Id, ActualData>> actualMaps;
final Map<Id, ActualData> globalData;
CompiledData(this.compiler, this.elementEnvironment, this.mainUri,
this.actualMaps, this.globalData);
Map<int, List<String>> computeAnnotations(Uri uri) {
Map<Id, ActualData> thisMap = actualMaps[uri];
Map<int, List<String>> annotations = <int, List<String>>{};
thisMap.forEach((Id id, ActualData data1) {
String value1 = '${data1.value}';
annotations
.putIfAbsent(data1.offset, () => [])
.add(colorizeSingle(value1));
});
return annotations;
}
Map<int, List<String>> computeDiffAnnotationsAgainst(
Map<Id, ActualData> thisMap, Map<Id, ActualData> otherMap, Uri uri,
{bool includeMatches: false}) {
Map<int, List<String>> annotations = <int, List<String>>{};
thisMap.forEach((Id id, ActualData data1) {
ActualData data2 = otherMap[id];
String value1 = '${data1.value}';
if (data1.value != data2?.value) {
String value2 = '${data2?.value ?? '---'}';
annotations
.putIfAbsent(data1.offset, () => [])
.add(colorizeDiff(value1, ' | ', value2));
} else if (includeMatches) {
annotations
.putIfAbsent(data1.offset, () => [])
.add(colorizeMatch(value1));
}
});
otherMap.forEach((Id id, ActualData data2) {
if (!thisMap.containsKey(id)) {
String value1 = '---';
String value2 = '${data2.value}';
annotations
.putIfAbsent(data2.offset, () => [])
.add(colorizeDiff(value1, ' | ', value2));
}
});
return annotations;
}
}
String withAnnotations(String sourceCode, Map<int, List<String>> annotations) {
StringBuffer sb = new StringBuffer();
int end = 0;
for (int offset in annotations.keys.toList()..sort()) {
if (offset >= sourceCode.length) {
sb.write('...');
return sb.toString();
}
if (offset > end) {
sb.write(sourceCode.substring(end, offset));
}
for (String annotation in annotations[offset]) {
sb.write(colorizeAnnotation('/*', annotation, '*/'));
}
end = offset;
}
if (end < sourceCode.length) {
sb.write(sourceCode.substring(end));
}
return sb.toString();
}
/// Data collected by [computeData].
class IdData {
final Map<Uri, AnnotatedCode> code;
final MemberAnnotations<IdValue> expectedMaps;
final CompiledData _compiledData;
final MemberAnnotations<ActualData> _actualMaps = new MemberAnnotations();
IdData(this.code, this.expectedMaps, this._compiledData) {
for (Uri uri in code.keys) {
_actualMaps[uri] = _compiledData.actualMaps[uri] ?? <Id, ActualData>{};
}
_actualMaps.globalData.addAll(_compiledData.globalData);
}
Compiler get compiler => _compiledData.compiler;
ElementEnvironment get elementEnvironment => _compiledData.elementEnvironment;
Uri get mainUri => _compiledData.mainUri;
MemberAnnotations<ActualData> get actualMaps => _actualMaps;
String actualCode(Uri uri) {
Map<int, List<String>> annotations = <int, List<String>>{};
actualMaps[uri].forEach((Id id, ActualData data) {
annotations
.putIfAbsent(data.sourceSpan.begin, () => [])
.add('${data.value}');
});
return withAnnotations(code[uri].sourceCode, annotations);
}
String diffCode(Uri uri) {
Map<int, List<String>> annotations = <int, List<String>>{};
actualMaps[uri].forEach((Id id, ActualData data) {
IdValue value = expectedMaps[uri][id];
if (data.value != value || value == null && data.value.value != '') {
String expected = value?.toString() ?? '';
int offset = getOffsetFromId(id, uri);
String value1 = '${expected}';
String value2 = '${data.value}';
annotations
.putIfAbsent(offset, () => [])
.add(colorizeDiff(value1, ' | ', value2));
}
});
expectedMaps[uri].forEach((Id id, IdValue expected) {
if (!actualMaps[uri].containsKey(id)) {
int offset = getOffsetFromId(id, uri);
String value1 = '${expected}';
String value2 = '---';
annotations
.putIfAbsent(offset, () => [])
.add(colorizeDiff(value1, ' | ', value2));
}
});
return withAnnotations(code[uri].sourceCode, annotations);
}
int getOffsetFromId(Id id, Uri uri) {
return compiler.reporter
.spanFromSpannable(computeSpannable(elementEnvironment, uri, id))
.begin;
}
}
/// Encapsulates the member data computed for each source file of interest.
/// It's a glorified wrapper around a map of maps, but written this way to
/// provide a little more information about what it's doing. [DataType] refers
/// to the type this map is holding -- it is either [IdValue] or [ActualData].
class MemberAnnotations<DataType> {
/// For each Uri, we create a map associating an element id with its
/// corresponding annotations.
final Map<Uri, Map<Id, DataType>> _computedDataForEachFile =
new Map<Uri, Map<Id, DataType>>();
/// Member or class annotations that don't refer to any of the user files.
final Map<Id, DataType> globalData = <Id, DataType>{};
void operator []=(Uri file, Map<Id, DataType> computedData) {
_computedDataForEachFile[file] = computedData;
}
void forEach(void f(Uri file, Map<Id, DataType> computedData)) {
_computedDataForEachFile.forEach(f);
}
Map<Id, DataType> operator [](Uri file) {
if (!_computedDataForEachFile.containsKey(file)) {
_computedDataForEachFile[file] = <Id, DataType>{};
}
return _computedDataForEachFile[file];
}
}
typedef void Callback();
/// Check code for all test files int [data] using [computeFromAst] and
/// [computeFromKernel] from the respective front ends. If [skipForKernel]
/// contains the name of the test file it isn't tested for kernel.
///
/// [libDirectory] contains the directory for any supporting libraries that need
/// to be loaded. We expect supporting libraries to have the same prefix as the
/// original test in [dataDir]. So, for example, if testing `foo.dart` in
/// [dataDir], then this function will consider any files named `foo.*\.dart`,
/// such as `foo2.dart`, `foo_2.dart`, and `foo_blah_blah_blah.dart` in
/// [libDirectory] to be supporting library files for `foo.dart`.
/// [setUpFunction] is called once for every test that is executed.
/// If [forUserSourceFilesOnly] is true, we examine the elements in the main
/// file and any supporting libraries.
Future checkTests(Directory dataDir, ComputeMemberDataFunction computeFromAst,
ComputeMemberDataFunction computeFromKernel,
{List<String> skipForAst: const <String>[],
List<String> skipForKernel: const <String>[],
bool filterActualData(IdValue idValue, ActualData actualData),
List<String> options: const <String>[],
List<String> args: const <String>[],
Directory libDirectory: null,
bool forUserLibrariesOnly: true,
Callback setUpFunction,
ComputeClassDataFunction computeClassDataFromAst,
ComputeClassDataFunction computeClassDataFromKernel,
int shards: 1,
int shardIndex: 0}) async {
args = args.toList();
bool verbose = args.remove('-v');
bool shouldContinue = args.remove('-c');
bool testAfterFailures = args.remove('-a');
bool continued = false;
bool hasFailures = false;
var relativeDir = dataDir.uri.path.replaceAll(Uri.base.path, '');
print('Data dir: ${relativeDir}');
List<FileSystemEntity> entities = dataDir.listSync();
if (shards > 1) {
int start = entities.length * shardIndex ~/ shards;
int end = entities.length * (shardIndex + 1) ~/ shards;
entities = entities.sublist(start, end);
}
for (FileSystemEntity entity in entities) {
String name = entity.uri.pathSegments.last;
if (args.isNotEmpty && !args.contains(name) && !continued) continue;
if (shouldContinue) continued = true;
List<String> testOptions = options.toList();
bool strongModeOnlyTest = false;
if (name.endsWith('_ea.dart')) {
testOptions.add(Flags.enableAsserts);
} else if (name.endsWith('_strong.dart')) {
strongModeOnlyTest = true;
testOptions.add(Flags.strongMode);
} else if (name.endsWith('_checked.dart')) {
testOptions.add(Flags.enableCheckedMode);
}
print('----------------------------------------------------------------');
print('Test: $name');
// Pretend this is a dart2js_native test to allow use of 'native' keyword
// and import of private libraries.
String commonTestPath = 'sdk/tests/compiler';
Uri entryPoint =
Uri.parse('memory:$commonTestPath/dart2js_native/main.dart');
String annotatedCode = await new File.fromUri(entity.uri).readAsString();
userFiles.add('main.dart');
Map<Uri, AnnotatedCode> code = {
entryPoint:
new AnnotatedCode.fromText(annotatedCode, commentStart, commentEnd)
};
Map<String, MemberAnnotations<IdValue>> expectedMaps = {
astMarker: new MemberAnnotations<IdValue>(),
kernelMarker: new MemberAnnotations<IdValue>()
};
computeExpectedMap(entryPoint, code[entryPoint], expectedMaps);
Map<String, String> memorySourceFiles = {
entryPoint.path: code[entryPoint].sourceCode
};
if (libDirectory != null) {
print('Supporting libraries:');
String filePrefix = name.substring(0, name.lastIndexOf('.'));
await for (FileSystemEntity libEntity in libDirectory.list()) {
String libFileName = libEntity.uri.pathSegments.last;
if (libFileName.startsWith(filePrefix)) {
print(' - libs/$libFileName');
Uri libFileUri =
Uri.parse('memory:$commonTestPath/libs/$libFileName');
userFiles.add(libEntity.uri.pathSegments.last);
String libCode = await new File.fromUri(libEntity.uri).readAsString();
AnnotatedCode annotatedLibCode =
new AnnotatedCode.fromText(libCode, commentStart, commentEnd);
memorySourceFiles[libFileUri.path] = annotatedLibCode.sourceCode;
code[libFileUri] = annotatedLibCode;
computeExpectedMap(libFileUri, annotatedLibCode, expectedMaps);
}
}
}
if (setUpFunction != null) setUpFunction();
if (skipForAst.contains(name) || strongModeOnlyTest) {
print('--skipped for ast-----------------------------------------------');
} else {
print('--from ast------------------------------------------------------');
MemberAnnotations<IdValue> annotations = expectedMaps[astMarker];
CompiledData compiledData1 = await computeData(
entryPoint, memorySourceFiles, computeFromAst,
computeClassData: computeClassDataFromAst,
options: [Flags.useOldFrontend]..addAll(testOptions),
verbose: verbose,
forUserLibrariesOnly: forUserLibrariesOnly,
globalIds: annotations.globalData.keys);
if (await checkCode(code, annotations, compiledData1,
fatalErrors: !testAfterFailures)) {
hasFailures = true;
}
}
if (skipForKernel.contains(name)) {
print('--skipped for kernel--------------------------------------------');
} else {
print('--from kernel---------------------------------------------------');
MemberAnnotations<IdValue> annotations = expectedMaps[kernelMarker];
CompiledData compiledData2 = await computeData(
entryPoint, memorySourceFiles, computeFromKernel,
computeClassData: computeClassDataFromKernel,
options: testOptions,
verbose: verbose,
forUserLibrariesOnly: forUserLibrariesOnly,
globalIds: annotations.globalData.keys);
if (await checkCode(code, annotations, compiledData2,
filterActualData: filterActualData,
fatalErrors: !testAfterFailures)) {
hasFailures = true;
}
}
}
Expect.isFalse(hasFailures, 'Errors found.');
}
final Set<String> userFiles = new Set<String>();
/// Checks [compiledData] against the expected data in [expectedMap] derived
/// from [code].
Future<bool> checkCode(Map<Uri, AnnotatedCode> code,
MemberAnnotations<IdValue> expectedMaps, CompiledData compiledData,
{bool filterActualData(IdValue expected, ActualData actualData),
bool fatalErrors: true}) async {
IdData data = new IdData(code, expectedMaps, compiledData);
bool hasFailure = false;
void checkActualMap(
Map<Id, ActualData> actualMap, Map<Id, IdValue> expectedMap,
[Uri uri]) {
bool hasLocalFailure = false;
actualMap.forEach((Id id, ActualData actualData) {
IdValue actual = actualData.value;
if (!expectedMap.containsKey(id)) {
if (actual.value != '') {
reportHere(
data.compiler.reporter,
actualData.sourceSpan,
'Id $id = ${colors.red('$actual')} for ${actualData.object} '
'(${actualData.object.runtimeType}) '
'not expected in ${expectedMap.keys}');
if (filterActualData == null || filterActualData(null, actualData)) {
hasLocalFailure = true;
}
}
} else {
IdValue expected = expectedMap[id];
if (actual != expected) {
reportHere(
data.compiler.reporter,
actualData.sourceSpan,
'Object: ${actualData.object} '
'(${actualData.object.runtimeType})\n '
'expected: ${colors.green('$expected')}\n '
'actual: ${colors.red('$actual')}');
if (filterActualData == null ||
filterActualData(expected, actualData)) {
hasLocalFailure = true;
}
}
}
});
if (hasLocalFailure) {
hasFailure = true;
if (uri != null) {
print('--annotations diff [${uri.pathSegments.last}]-------------');
print(data.diffCode(uri));
print('----------------------------------------------------------');
}
}
}
data.actualMaps.forEach((Uri uri, Map<Id, ActualData> actualMap) {
checkActualMap(actualMap, data.expectedMaps[uri], uri);
});
checkActualMap(data.actualMaps.globalData, data.expectedMaps.globalData);
Set<Id> missingIds = new Set<Id>();
StringBuffer combinedAnnotationsDiff = new StringBuffer();
void checkMissing(Map<Id, IdValue> expectedMap, Map<Id, ActualData> actualMap,
[Uri uri]) {
expectedMap.forEach((Id id, IdValue expected) {
if (!actualMap.containsKey(id)) {
missingIds.add(id);
StringBuffer sb = new StringBuffer();
for (Id id in actualMap.keys.where((d) => d.kind == id.kind)) {
sb.write('\n $id');
}
if (uri != null) {
reportHere(
data.compiler.reporter,
computeSpannable(data.elementEnvironment, uri, id),
'Expected $expected for id $id missing in${sb}');
} else {
print('Expected $expected for id $id missing in${sb}');
}
}
});
if (missingIds.isNotEmpty && uri != null) {
combinedAnnotationsDiff.write('Missing in $uri:\n');
combinedAnnotationsDiff.write('${data.diffCode(uri)}\n');
}
}
data.expectedMaps.forEach((Uri uri, Map<Id, IdValue> expectedMap) {
checkMissing(expectedMap, data.actualMaps[uri], uri);
});
checkMissing(data.expectedMaps.globalData, data.actualMaps.globalData);
if (combinedAnnotationsDiff.isNotEmpty) {
print('--annotations diff--------------------------------------------');
print(combinedAnnotationsDiff.toString());
print('--------------------------------------------------------------');
}
if (missingIds.isNotEmpty) {
print("Ids not found: ${missingIds}.");
hasFailure = true;
}
if (hasFailure && fatalErrors) {
Expect.fail('Errors found.');
}
return hasFailure;
}
/// Compute a [Spannable] from an [id] in the library [mainUri].
Spannable computeSpannable(
ElementEnvironment elementEnvironment, Uri mainUri, Id id) {
if (id is NodeId) {
return new SourceSpan(mainUri, id.value, id.value + 1);
} else if (id is ElementId) {
String memberName = id.memberName;
bool isSetter = false;
if (memberName != '[]=' && memberName.endsWith('=')) {
isSetter = true;
memberName = memberName.substring(0, memberName.length - 1);
}
LibraryEntity library = elementEnvironment.lookupLibrary(mainUri);
if (id.className != null) {
ClassEntity cls =
elementEnvironment.lookupClass(library, id.className, required: true);
if (cls == null) {
throw new ArgumentError("No class '${id.className}' in $mainUri.");
}
MemberEntity member = elementEnvironment
.lookupClassMember(cls, memberName, setter: isSetter);
if (member == null) {
ConstructorEntity constructor =
elementEnvironment.lookupConstructor(cls, memberName);
if (constructor == null) {
throw new ArgumentError("No class member '${memberName}' in $cls.");
}
return constructor;
}
return member;
} else {
MemberEntity member = elementEnvironment
.lookupLibraryMember(library, memberName, setter: isSetter);
if (member == null) {
throw new ArgumentError("No member '${memberName}' in $mainUri.");
}
return member;
}
} else if (id is ClassId) {
LibraryEntity library = elementEnvironment.lookupLibrary(mainUri);
ClassEntity cls =
elementEnvironment.lookupClass(library, id.className, required: true);
if (cls == null) {
throw new ArgumentError("No class '${id.className}' in $mainUri.");
}
return cls;
}
throw new UnsupportedError('Unsupported id $id.');
}
const String astMarker = 'ast.';
const String kernelMarker = 'kernel.';
/// Compute two [MemberAnnotations] objects from [code] specifying the expected
/// annotations we anticipate encountering; one corresponding to the old
/// implementation, one for the new implementation.
///
/// If an annotation starts with 'ast.' it is only expected for the old
/// implementation and if it starts with 'kernel.' it is only expected for the
/// new implementation. Otherwise it is expected for both implementations.
///
/// Most nodes have the same and expectations should match this by using
/// annotations without prefixes.
void computeExpectedMap(Uri sourceUri, AnnotatedCode code,
Map<String, MemberAnnotations<IdValue>> maps) {
List<String> mapKeys = [astMarker, kernelMarker];
Map<String, AnnotatedCode> split = splitByPrefixes(code, mapKeys);
split.forEach((String marker, AnnotatedCode code) {
MemberAnnotations<IdValue> fileAnnotations = maps[marker];
Map<Id, IdValue> expectedValues = fileAnnotations[sourceUri];
for (Annotation annotation in code.annotations) {
String text = annotation.text;
IdValue idValue = IdValue.decode(annotation.offset, text);
if (idValue.id.isGlobal) {
Expect.isFalse(
fileAnnotations.globalData.containsKey(idValue.id),
"Duplicate annotations for ${idValue.id}: ${idValue} and "
"${fileAnnotations.globalData[idValue.id]}.");
fileAnnotations.globalData[idValue.id] = idValue;
} else {
Expect.isFalse(
expectedValues.containsKey(idValue.id),
"Duplicate annotations for ${idValue.id}: ${idValue} and "
"${expectedValues[idValue.id]}.");
expectedValues[idValue.id] = idValue;
}
}
});
}
Future<bool> compareData(
Uri entryPoint,
Map<String, String> memorySourceFiles,
ComputeMemberDataFunction computeAstData,
ComputeMemberDataFunction computeIrData,
{List<String> options: const <String>[],
bool forUserLibrariesOnly: true,
bool skipUnprocessedMembers: false,
bool skipFailedCompilations: false,
bool verbose: false,
bool whiteList(Uri uri, Id id)}) async {
print('--from ast----------------------------------------------------------');
CompiledData data1 = await computeData(
entryPoint, memorySourceFiles, computeAstData,
options: [Flags.useOldFrontend]..addAll(options),
forUserLibrariesOnly: forUserLibrariesOnly,
skipUnprocessedMembers: skipUnprocessedMembers,
skipFailedCompilations: skipFailedCompilations);
if (data1 == null) return false;
print('--from kernel-------------------------------------------------------');
CompiledData data2 = await computeData(
entryPoint, memorySourceFiles, computeIrData,
options: options,
forUserLibrariesOnly: forUserLibrariesOnly,
skipUnprocessedMembers: skipUnprocessedMembers,
skipFailedCompilations: skipFailedCompilations);
if (data2 == null) return false;
await compareCompiledData(data1, data2,
whiteList: whiteList,
skipMissingUris: !forUserLibrariesOnly,
verbose: verbose);
return true;
}
Future compareCompiledData(CompiledData data1, CompiledData data2,
{bool skipMissingUris: false,
bool verbose: false,
bool whiteList(Uri uri, Id id)}) async {
if (whiteList == null) {
whiteList = (uri, id) => false;
}
bool hasErrors = false;
String libraryRoot1;
SourceFileProvider provider1 = data1.compiler.provider;
SourceFileProvider provider2 = data2.compiler.provider;
for (Uri uri1 in data1.actualMaps.keys) {
Uri uri2 = uri1;
bool hasErrorsInUri = false;
Map<Id, ActualData> actualMap1 = data1.actualMaps[uri1];
Map<Id, ActualData> actualMap2 = data2.actualMaps[uri2];
if (actualMap2 == null && skipMissingUris) {
libraryRoot1 ??= '${data1.compiler.options.libraryRoot}';
String uriText = '$uri1';
if (uriText.startsWith(libraryRoot1)) {
String relativePath = uriText.substring(libraryRoot1.length);
uri2 =
resolveFastaUri(Uri.parse('patched_dart2js_sdk/${relativePath}'));
actualMap2 = data2.actualMaps[uri2];
}
if (actualMap2 == null) {
continue;
}
}
Expect.isNotNull(actualMap2,
"No data for $uri1 in:\n ${data2.actualMaps.keys.join('\n ')}");
SourceFile sourceFile1 = await provider1.getUtf8SourceFile(uri1) ??
await provider1.autoReadFromFile(uri1);
Expect.isNotNull(sourceFile1, 'No source file for $uri1');
String sourceCode1 = sourceFile1.slowText();
if (uri1 != uri2) {
SourceFile sourceFile2 = await provider2.getUtf8SourceFile(uri2) ??
await provider2.autoReadFromFile(uri2);
Expect.isNotNull(sourceFile2, 'No source file for $uri2');
String sourceCode2 = sourceFile2.slowText();
if (sourceCode1.length != sourceCode2.length) {
continue;
}
}
actualMap1.forEach((Id id, ActualData actualData1) {
IdValue value1 = actualData1.value;
IdValue value2 = actualMap2[id]?.value;
if (value1 != value2) {
reportHere(data1.compiler.reporter, actualData1.sourceSpan,
'$id: from source:${value1},from dill:${value2}');
if (!whiteList(uri1, id)) {
hasErrors = hasErrorsInUri = true;
}
}
});
actualMap2.forEach((Id id, ActualData actualData2) {
IdValue value2 = actualData2.value;
IdValue value1 = actualMap1[id]?.value;
if (value1 != value2) {
reportHere(data2.compiler.reporter, actualData2.sourceSpan,
'$id: from source:${value1},from dill:${value2}');
if (!whiteList(uri1, id)) {
hasErrors = hasErrorsInUri = true;
}
}
});
if (hasErrorsInUri) {
print('--annotations diff $uri1---------------------------------------');
print(withAnnotations(
sourceCode1,
data1.computeDiffAnnotationsAgainst(actualMap1, actualMap2, uri1,
includeMatches: verbose)));
print('----------------------------------------------------------');
}
}
if (hasErrors) {
Expect.fail('Annotations mismatch');
}
}
/// Set of features used in annotations.
class Features {
Map<String, Object> _features = <String, Object>{};
void add(String key, {var value: ''}) {
_features[key] = value.toString();
}
void addElement(String key, [var value]) {
List<String> list = _features.putIfAbsent(key, () => <String>[]);
if (value != null) {
list.add(value.toString());
}
}
bool containsKey(String key) {
return _features.containsKey(key);
}
void operator []=(String key, String value) {
_features[key] = value;
}
String operator [](String key) => _features[key];
String remove(String key) => _features.remove(key);
/// Returns a string containing all features in a comma-separated list sorted
/// by feature names.
String getText() {
StringBuffer sb = new StringBuffer();
bool needsComma = false;
for (String name in _features.keys.toList()..sort()) {
dynamic value = _features[name];
if (value != null) {
if (needsComma) {
sb.write(',');
}
sb.write(name);
if (value is List<String>) {
value = '[${(value..sort()).join(',')}]';
}
if (value != '') {
sb.write('=');
sb.write(value);
}
needsComma = true;
}
}
return sb.toString();
}
}