| // 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.'); |
| } |