blob: d0288ee79e7f75b62cccb0b93a58d0468bafc962 [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.
// @dart = 2.7
import 'dart:async';
import 'dart:io';
import 'package:_fe_analyzer_shared/src/testing/id_testing.dart';
import 'package:compiler/src/common.dart';
import 'package:compiler/compiler_new.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/kernel/element_map_impl.dart';
import 'package:compiler/src/kernel/kernel_strategy.dart';
import 'package:expect/expect.dart';
import 'package:kernel/ast.dart' as ir;
import '../helpers/compiler_helper.dart';
import '../helpers/memory_compiler.dart';
import '../equivalence/id_equivalence.dart';
export 'package:_fe_analyzer_shared/src/testing/id_testing.dart'
show DataInterpreter, StringDataInterpreter;
export '../helpers/memory_compiler.dart' show CollectedMessage;
const String specMarker = 'spec';
const String prodMarker = 'prod';
const String canaryMarker = 'canary';
const String twoDeferredFragmentMarker = 'two-frag';
const String threeDeferredFragmentMarker = 'three-frag';
const TestConfig specConfig = TestConfig(specMarker, 'compliance mode', []);
const TestConfig prodConfig = TestConfig(prodMarker, 'production mode',
[Flags.omitImplicitChecks, Flags.laxRuntimeTypeToString]);
const TestConfig canaryConfig =
TestConfig(canaryMarker, 'canary mode', [Flags.canary]);
const TestConfig twoDeferredFragmentConfig = TestConfig(
twoDeferredFragmentMarker,
'two deferred fragment mode',
['${Flags.mergeFragmentsThreshold}=2']);
const TestConfig threeDeferredFragmentConfig = TestConfig(
threeDeferredFragmentMarker,
'three deferred fragment mode',
['${Flags.mergeFragmentsThreshold}=3']);
/// Default internal configurations not including experimental features.
const List<TestConfig> defaultInternalConfigs = [specConfig, prodConfig];
/// All internal configurations including experimental features.
const List<TestConfig> allInternalConfigs = [specConfig, prodConfig];
/// Compliance mode configurations (with strong mode checks) including
/// experimental features.
const List<TestConfig> allSpecConfigs = [specConfig];
/// Test configuration used in tests shared with CFE.
const TestConfig sharedConfig = TestConfig(dart2jsMarker, 'dart2js', []);
abstract class DataComputer<T> {
const DataComputer();
/// Called before testing to setup flags needed for data collection.
void setup() {}
/// Function that computes a data mapping for [member].
///
/// Fills [actualMap] with the data.
void computeMemberData(
Compiler compiler, MemberEntity member, Map<Id, ActualData<T>> actualMap,
{bool verbose});
/// Returns `true` if frontend member should be tested.
bool get testFrontend => false;
/// Function that computes a data mapping for [cls].
///
/// Fills [actualMap] with the data.
void computeClassData(
Compiler compiler, ClassEntity cls, Map<Id, ActualData<T>> actualMap,
{bool verbose}) {}
/// Function that computes a data mapping for [library].
///
/// Fills [actualMap] with the data.
void computeLibraryData(Compiler compiler, LibraryEntity library,
Map<Id, ActualData<T>> actualMap,
{bool verbose}) {}
/// Returns `true` if this data computer supports tests with compile-time
/// errors.
///
/// Unsuccessful compilation might leave the compiler in an inconsistent
/// state, so this testing feature is opt-in.
bool get supportsErrors => false;
/// Returns data corresponding to [error].
T computeErrorData(Compiler compiler, Id id, List<CollectedMessage> errors) =>
null;
DataInterpreter<T> get dataValidator;
}
const String stopAfterTypeInference = 'stopAfterTypeInference';
/// Reports [message] as an error using [spannable] as error location.
void reportError(
DiagnosticReporter reporter, Spannable spannable, String message) {
reporter
.reportErrorMessage(spannable, MessageKind.GENERIC, {'text': message});
}
/// Compute actual data for all members defined in the program with the
/// [entryPoint] and [memorySourceFiles].
///
/// Actual data is computed using [computeMemberData].
Future<CompiledData<T>> computeData<T>(String name, Uri entryPoint,
Map<String, String> memorySourceFiles, DataComputer<T> dataComputer,
{List<String> options: const <String>[],
bool verbose: false,
bool testFrontend: false,
bool printCode: false,
bool forUserLibrariesOnly: true,
bool skipUnprocessedMembers: false,
bool skipFailedCompilations: false,
Iterable<Id> globalIds: const <Id>[],
Future<void> verifyCompiler(String test, Compiler compiler)}) async {
OutputCollector outputCollector = new OutputCollector();
DiagnosticCollector diagnosticCollector = new DiagnosticCollector();
Uri packageConfig;
Uri wantedPackageConfig = createUriForFileName(".packages");
for (String key in memorySourceFiles.keys) {
if (key == wantedPackageConfig.path) {
packageConfig = wantedPackageConfig;
break;
}
}
CompilationResult result = await runCompiler(
entryPoint: entryPoint,
memorySourceFiles: memorySourceFiles,
outputProvider: outputCollector,
diagnosticHandler: diagnosticCollector,
options: options,
beforeRun: (compiler) {
compiler.stopAfterTypeInference =
options.contains(stopAfterTypeInference);
},
packageConfig: packageConfig,
unsafeToTouchSourceFiles: true);
if (!result.isSuccess) {
if (skipFailedCompilations) return null;
Expect.isTrue(
dataComputer.supportsErrors,
"Compilation with compile-time errors not supported for this "
"testing setup.");
}
if (printCode) {
print('--code------------------------------------------------------------');
print(outputCollector.getOutput('', OutputType.js));
print('------------------------------------------------------------------');
}
Compiler compiler = result.compiler;
if (verifyCompiler != null) {
await verifyCompiler(name, compiler);
}
Map<Uri, Map<Id, ActualData<T>>> actualMaps = <Uri, Map<Id, ActualData<T>>>{};
Map<Id, ActualData<T>> globalData = <Id, ActualData<T>>{};
Map<Id, ActualData<T>> actualMapForUri(Uri uri) {
return actualMaps.putIfAbsent(uri, () => <Id, ActualData<T>>{});
}
Map<Uri, Map<int, List<CollectedMessage>>> errors = {};
for (CollectedMessage error in diagnosticCollector.errors) {
Map<int, List<CollectedMessage>> map =
errors.putIfAbsent(error.uri, () => {});
List<CollectedMessage> list = map.putIfAbsent(error.begin, () => []);
list.add(error);
}
errors.forEach((Uri uri, Map<int, List<CollectedMessage>> map) {
map.forEach((int offset, List<CollectedMessage> list) {
NodeId id = new NodeId(offset, IdKind.error);
T data = dataComputer.computeErrorData(compiler, id, list);
if (data != null) {
Map<Id, ActualData<T>> actualMap = actualMapForUri(uri);
actualMap[id] = new ActualData<T>(id, data, uri, offset, list);
}
});
});
if (!result.isSuccess) {
return new Dart2jsCompiledData<T>(
compiler, null, entryPoint, actualMaps, globalData);
}
dynamic closedWorld = testFrontend
? compiler.frontendClosedWorldForTesting
: compiler.backendClosedWorldForTesting;
ElementEnvironment elementEnvironment = closedWorld?.elementEnvironment;
CommonElements commonElements = closedWorld.commonElements;
Map<Id, ActualData<T>> actualMapFor(Entity entity) {
SourceSpan span =
compiler.backendStrategy.spanFromSpannable(entity, entity);
return actualMapForUri(span.uri);
}
void processMember(MemberEntity member, Map<Id, ActualData<T>> actualMap) {
if (member.isAbstract) {
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;
}
}
dataComputer.computeMemberData(compiler, member, actualMap,
verbose: verbose);
}
void processClass(ClassEntity cls, Map<Id, ActualData<T>> actualMap) {
if (skipUnprocessedMembers && !closedWorld.isImplemented(cls)) {
return;
}
dataComputer.computeClassData(compiler, cls, actualMap, verbose: verbose);
}
bool excludeLibrary(LibraryEntity library) {
return forUserLibrariesOnly &&
(library.canonicalUri.scheme == 'dart' ||
library.canonicalUri.scheme == 'package');
}
ir.Library getIrLibrary(LibraryEntity library) {
KernelFrontendStrategy frontendStrategy = compiler.frontendStrategy;
KernelToElementMapImpl elementMap = frontendStrategy.elementMap;
LibraryEntity kLibrary =
elementMap.elementEnvironment.lookupLibrary(library.canonicalUri);
return elementMap.getLibraryNode(kLibrary);
}
for (LibraryEntity library in elementEnvironment.libraries) {
if (excludeLibrary(library) &&
!memorySourceFiles.containsKey(getIrLibrary(library).fileUri.path)) {
continue;
}
dataComputer.computeLibraryData(
compiler, library, actualMapForUri(getIrLibrary(library).fileUri),
verbose: verbose);
elementEnvironment.forEachClass(library, (ClassEntity cls) {
processClass(cls, actualMapFor(cls));
});
}
for (MemberEntity member in closedWorld.processedMembers) {
if (excludeLibrary(member.library) &&
!memorySourceFiles
.containsKey(getIrLibrary(member.library).fileUri.path)) continue;
processMember(member, actualMapFor(member));
}
List<LibraryEntity> globalLibraries = <LibraryEntity>[
commonElements.coreLibrary,
elementEnvironment.lookupLibrary(Uri.parse('dart:collection')),
commonElements.interceptorsLibrary,
commonElements.jsHelperLibrary,
commonElements.asyncLibrary,
];
LibraryEntity htmlLibrary =
elementEnvironment.lookupLibrary(Uri.parse('dart:html'), required: false);
if (htmlLibrary != null) {
globalLibraries.add(htmlLibrary);
}
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 '$memberName' not found in the global "
"libraries: ${globalLibraries.map((l) => l.canonicalUri).join(', ')}");
return member;
}
for (Id id in globalIds) {
if (id is MemberId) {
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) {
ClassEntity cls = getGlobalClass(id.className);
processClass(cls, globalData);
} else {
throw new UnsupportedError("Unexpected global id: $id");
}
}
return new Dart2jsCompiledData<T>(
compiler, elementEnvironment, entryPoint, actualMaps, globalData);
}
class Dart2jsCompiledData<T> extends CompiledData<T> {
final Compiler compiler;
final ElementEnvironment elementEnvironment;
Dart2jsCompiledData(
this.compiler,
this.elementEnvironment,
Uri mainUri,
Map<Uri, Map<Id, ActualData<T>>> actualMaps,
Map<Id, ActualData<T>> globalData)
: super(mainUri, actualMaps, globalData);
@override
int getOffsetFromId(Id id, Uri uri) {
return compiler.reporter
.spanFromSpannable(computeSpannable(elementEnvironment, uri, id))
?.begin;
}
@override
void reportError(Uri uri, int offset, String message,
{bool succinct: false}) {
compiler.reporter.reportErrorMessage(
computeSourceSpanFromUriOffset(uri, offset),
MessageKind.GENERIC,
{'text': message});
}
}
typedef void Callback();
class TestConfig {
final String marker;
final String name;
final List<String> options;
const TestConfig(this.marker, this.name, this.options);
}
/// 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<void> checkTests<T>(Directory dataDir, DataComputer<T> dataComputer,
{List<String> skip: const <String>[],
bool filterActualData(IdValue idValue, ActualData<T> actualData),
List<String> options: const <String>[],
List<String> args: const <String>[],
bool forUserLibrariesOnly: true,
Callback setUpFunction,
int shards: 1,
int shardIndex: 0,
void onTest(Uri uri),
List<TestConfig> testedConfigs = const [],
Map<String, List<String>> perTestOptions = const {},
Future<void> verifyCompiler(String test, Compiler compiler)}) async {
if (testedConfigs.isEmpty) testedConfigs = defaultInternalConfigs;
Set<String> testedMarkers =
testedConfigs.map((config) => config.marker).toSet();
Expect.isTrue(
testedConfigs.length == testedMarkers.length,
"Unexpected test markers $testedMarkers. "
"Tested configs: $testedConfigs.");
dataComputer.setup();
Future<Map<String, TestResult<T>>> checkTest(TestData testData,
{bool testAfterFailures,
bool verbose,
bool succinct,
bool printCode,
Map<String, List<String>> skipMap,
Uri nullUri}) async {
for (TestConfig testConfiguration in testedConfigs) {
Expect.isTrue(
testData.expectedMaps.containsKey(testConfiguration.marker),
"Unexpected test marker '${testConfiguration.marker}'. "
"Supported markers: ${testData.expectedMaps.keys}.");
}
String name = testData.name;
List<String> testOptions = options.toList();
if (name.endsWith('_ea.dart')) {
testOptions.add(Flags.enableAsserts);
}
if (perTestOptions.containsKey(name)) {
testOptions.addAll(perTestOptions[name]);
}
if (setUpFunction != null) setUpFunction();
Map<String, TestResult<T>> results = {};
if (skip.contains(name)) {
print('--skipped ------------------------------------------------------');
} else {
for (TestConfig testConfiguration in testedConfigs) {
if (skipForConfig(testData.name, testConfiguration.marker, skipMap)) {
continue;
}
print('--from (${testConfiguration.name})-------------');
results[testConfiguration.marker] = await runTestForConfiguration(
testConfiguration, dataComputer, testData, testOptions,
filterActualData: filterActualData,
verbose: verbose,
succinct: succinct,
testAfterFailures: testAfterFailures,
forUserLibrariesOnly: forUserLibrariesOnly,
printCode: printCode,
verifyCompiler: verifyCompiler);
}
}
return results;
}
await runTests<T>(dataDir,
args: args,
shards: shards,
shardIndex: shardIndex,
onTest: onTest,
createUriForFileName: createUriForFileName,
onFailure: Expect.fail,
runTest: checkTest);
}
Uri createUriForFileName(String fileName) {
// Pretend this is a web/native test to allow use of 'native'
// keyword and import of private libraries.
return Uri.parse('memory:sdk/tests/web/native/$fileName');
}
Future<TestResult<T>> runTestForConfiguration<T>(TestConfig testConfiguration,
DataComputer<T> dataComputer, TestData testData, List<String> options,
{bool filterActualData(IdValue idValue, ActualData<T> actualData),
bool verbose: false,
bool succinct: false,
bool printCode: false,
bool forUserLibrariesOnly: true,
bool testAfterFailures: false,
Future<void> verifyCompiler(String test, Compiler compiler)}) async {
MemberAnnotations<IdValue> annotations =
testData.expectedMaps[testConfiguration.marker];
CompiledData<T> compiledData = await computeData(testData.name,
testData.entryPoint, testData.memorySourceFiles, dataComputer,
options: [...options, ...testConfiguration.options],
verbose: verbose,
printCode: printCode,
testFrontend: dataComputer.testFrontend,
forUserLibrariesOnly: forUserLibrariesOnly,
globalIds: annotations.globalData.keys,
verifyCompiler: verifyCompiler);
return await checkCode(testConfiguration.name, testData.testFileUri,
testData.code, annotations, compiledData, dataComputer.dataValidator,
filterActualData: filterActualData,
fatalErrors: !testAfterFailures,
onFailure: Expect.fail,
succinct: succinct);
}
/// 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 MemberId) {
if (elementEnvironment == null) {
// If compilation resulted in error we might not have an
// element environment.
return NO_LOCATION_SPANNABLE;
}
String memberName = id.memberName;
bool isSetter = false;
if (memberName != '[]=' && 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);
if (cls == null) {
// Constant expression in CFE might remove inlined parts of sources.
print("No class '${id.className}' in $mainUri.");
return NO_LOCATION_SPANNABLE;
}
MemberEntity member = elementEnvironment
.lookupClassMember(cls, memberName, setter: isSetter);
if (member == null) {
ConstructorEntity constructor =
elementEnvironment.lookupConstructor(cls, memberName);
if (constructor == null) {
// Constant expression in CFE might remove inlined parts of sources.
print("No class member '${memberName}' in $cls.");
return NO_LOCATION_SPANNABLE;
}
return constructor;
}
return member;
} else {
MemberEntity member = elementEnvironment
.lookupLibraryMember(library, memberName, setter: isSetter);
if (member == null) {
// Constant expression in CFE might remove inlined parts of sources.
print("No member '${memberName}' in $mainUri.");
return NO_LOCATION_SPANNABLE;
}
return member;
}
} else if (id is ClassId) {
LibraryEntity library = elementEnvironment.lookupLibrary(mainUri);
ClassEntity cls = elementEnvironment.lookupClass(library, id.className);
if (cls == null) {
// Constant expression in CFE might remove inlined parts of sources.
print("No class '${id.className}' in $mainUri.");
return NO_LOCATION_SPANNABLE;
}
return cls;
} else if (id is LibraryId) {
return new SourceSpan(id.uri, null, null);
}
throw new UnsupportedError('Unsupported id $id.');
}