Chromium Code Reviews| Index: pkg/analyzer/test/stress/for_git_repository.dart |
| diff --git a/pkg/analyzer/test/stress/for_git_repository.dart b/pkg/analyzer/test/stress/for_git_repository.dart |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..cda2c0faa047c62b772321b814bfad18859b92f2 |
| --- /dev/null |
| +++ b/pkg/analyzer/test/stress/for_git_repository.dart |
| @@ -0,0 +1,635 @@ |
| +// Copyright (c) 2016, 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. |
| + |
| +library analyzer.test.stress.limited_invalidation; |
| + |
| +import 'dart:async'; |
| +import 'dart:io'; |
| + |
| +import 'package:analyzer/dart/ast/ast.dart'; |
| +import 'package:analyzer/dart/element/element.dart'; |
| +import 'package:analyzer/dart/element/type.dart'; |
| +import 'package:analyzer/file_system/file_system.dart' as fs; |
| +import 'package:analyzer/file_system/physical_file_system.dart'; |
| +import 'package:analyzer/src/context/builder.dart'; |
| +import 'package:analyzer/src/context/cache.dart'; |
| +import 'package:analyzer/src/context/context.dart'; |
| +import 'package:analyzer/src/dart/ast/utilities.dart'; |
| +import 'package:analyzer/src/dart/element/member.dart'; |
| +import 'package:analyzer/src/generated/engine.dart'; |
| +import 'package:analyzer/src/generated/error.dart'; |
| +import 'package:analyzer/src/generated/sdk.dart'; |
| +import 'package:analyzer/src/generated/sdk_io.dart'; |
| +import 'package:analyzer/src/generated/source.dart'; |
| +import 'package:analyzer/src/generated/utilities_collection.dart'; |
| +import 'package:analyzer/src/task/dart.dart'; |
| +import 'package:analyzer/task/general.dart'; |
| +import 'package:analyzer/task/model.dart'; |
| +import 'package:path/path.dart' as path; |
| +import 'package:unittest/unittest.dart'; |
| + |
| +main() { |
| + new StressTest().run(); |
| +} |
| + |
| +void _failTypeMismatch(Object actual, Object expected, {String reason}) { |
| + String message = 'Actual $actual is ${actual.runtimeType}, ' |
| + 'but expected $expected is ${expected.runtimeType}'; |
| + if (reason != null) { |
| + message += ' $reason'; |
| + } |
| + fail(message); |
| +} |
| + |
| +void _logPrint(String message) { |
| + DateTime time = new DateTime.now(); |
| + print('$time: $message'); |
| +} |
| + |
| +class FileInfo { |
| + final String path; |
| + final int modification; |
| + |
| + FileInfo(this.path, this.modification); |
| +} |
| + |
| +class FolderDiff { |
| + final List<String> added; |
| + final List<String> changed; |
| + final List<String> removed; |
| + |
| + FolderDiff(this.added, this.changed, this.removed); |
| + |
| + bool get isEmpty => added.isEmpty && changed.isEmpty && removed.isEmpty; |
| + bool get isNotEmpty => !isEmpty; |
| + |
| + @override |
| + String toString() { |
| + return '[added=$added, changed=$changed, removed=$removed]'; |
| + } |
| +} |
| + |
| +class FolderInfo { |
| + final String path; |
| + final List<FileInfo> files = <FileInfo>[]; |
| + |
| + FolderInfo(this.path) { |
| + List<FileSystemEntity> entities = |
| + new Directory(path).listSync(recursive: true); |
| + for (FileSystemEntity entity in entities) { |
| + if (entity is File) { |
| + String path = entity.path; |
| + if (path.contains('packages') || path.contains('.pub')) { |
| + continue; |
| + } |
| + if (path.endsWith('.dart')) { |
| + files.add(new FileInfo( |
| + path, entity.lastModifiedSync().millisecondsSinceEpoch)); |
| + } |
| + } |
| + } |
| + } |
| + |
| + FolderDiff diff(FolderInfo oldFolder) { |
| + Map<String, FileInfo> toMap(FolderInfo folder) { |
| + Map<String, FileInfo> map = <String, FileInfo>{}; |
| + folder.files.forEach((file) { |
| + map[file.path] = file; |
| + }); |
| + return map; |
| + } |
| + Map<String, FileInfo> newFiles = toMap(this); |
| + Map<String, FileInfo> oldFiles = toMap(oldFolder); |
| + Set<String> addedPaths = newFiles.keys.toSet()..removeAll(oldFiles.keys); |
| + Set<String> removedPaths = oldFiles.keys.toSet()..removeAll(newFiles.keys); |
| + List<String> changedPaths = <String>[]; |
| + newFiles.forEach((path, newFile) { |
| + FileInfo oldFile = oldFiles[path]; |
| + if (oldFile != null && oldFile.modification != newFile.modification) { |
| + changedPaths.add(path); |
| + } |
| + }); |
| + return new FolderDiff( |
| + addedPaths.toList(), changedPaths, removedPaths.toList()); |
| + } |
| +} |
| + |
| +class GitException { |
| + final String message; |
| + final String stdout; |
| + final String stderr; |
| + |
| + GitException(this.message) |
| + : stdout = null, |
| + stderr = null; |
| + |
| + GitException.forProcessResult(this.message, ProcessResult processResult) |
| + : stdout = processResult.stdout, |
| + stderr = processResult.stderr; |
| + |
| + @override |
| + String toString() => '$message\n$stdout\n$stderr\n'; |
| +} |
| + |
| +class GitRepository { |
| + final String path; |
| + |
| + GitRepository(this.path); |
| + |
| + Future checkout(String hash) async { |
| + // TODO(scheglov) use for updating only some files |
| + if (hash.endsWith('hash')) { |
| + List<String> filePaths = <String>[ |
| + '/Users/user/full/path/one.dart', |
| + '/Users/user/full/path/two.dart', |
| + ]; |
| + for (var filePath in filePaths) { |
| + await Process.run('git', <String>['checkout', '-f', hash, filePath], |
| + workingDirectory: path); |
| + } |
| + return; |
| + } |
| + ProcessResult processResult = await Process |
| + .run('git', <String>['checkout', '-f', hash], workingDirectory: path); |
| + _throwIfNotSuccess(processResult); |
| + } |
| + |
| + Future<List<GitRevision>> getRevisions({String after}) async { |
| + List<String> args = <String>['log', '--format=%ct %H %s']; |
| + if (after != null) { |
| + args.add('--after=$after'); |
| + } |
| + ProcessResult processResult = |
| + await Process.run('git', args, workingDirectory: path); |
| + _throwIfNotSuccess(processResult); |
| + String output = processResult.stdout; |
| + List<String> logLines = output.split('\n'); |
| + List<GitRevision> revisions = <GitRevision>[]; |
| + for (String logLine in logLines) { |
| + int index1 = logLine.indexOf(' '); |
| + if (index1 != -1) { |
| + int index2 = logLine.indexOf(' ', index1 + 1); |
| + if (index2 != -1) { |
| + int timestamp = int.parse(logLine.substring(0, index1)); |
| + String hash = logLine.substring(index1 + 1, index2); |
| + String message = logLine.substring(index2).trim(); |
| + revisions.add(new GitRevision(timestamp, hash, message)); |
| + } |
| + } |
| + } |
| + return revisions; |
| + } |
| + |
| + void removeIndexLock() { |
| + File file = new File('$path/.git/index.lock'); |
| + if (file.existsSync()) { |
| + file.deleteSync(); |
| + } |
| + } |
| + |
| + Future resetHard() async { |
| + ProcessResult processResult = await Process |
| + .run('git', <String>['reset', '--hard'], workingDirectory: path); |
| + _throwIfNotSuccess(processResult); |
| + } |
| + |
| + void _throwIfNotSuccess(ProcessResult processResult) { |
| + if (processResult.exitCode != 0) { |
| + throw new GitException.forProcessResult( |
| + 'Unable to run "git log".', processResult); |
| + } |
| + } |
| +} |
| + |
| +class GitRevision { |
| + final int timestamp; |
| + final String hash; |
| + final String message; |
| + |
| + GitRevision(this.timestamp, this.hash, this.message); |
| + |
| + @override |
| + String toString() { |
| + DateTime dateTime = |
| + new DateTime.fromMillisecondsSinceEpoch(timestamp * 1000, isUtc: true) |
| + .toLocal(); |
| + return '$dateTime|$hash|$message|'; |
| + } |
| +} |
| + |
| +class StressTest { |
| +// String repoPath = '/Users/scheglov/tmp/limited-invalidation/path'; |
| +// String folderPath = '/Users/scheglov/tmp/limited-invalidation/path'; |
| +// String repoPath = '/Users/scheglov/tmp/limited-invalidation/async'; |
| +// String folderPath = '/Users/scheglov/tmp/limited-invalidation/async'; |
| + String repoPath = '/Users/scheglov/tmp/limited-invalidation/sdk'; |
| + String folderPath = '/Users/scheglov/tmp/limited-invalidation/sdk/pkg/analyzer'; |
|
Brian Wilkerson
2016/08/03 13:49:11
Needs to not have machine specific paths. Can we m
|
| + |
| + fs.ResourceProvider resourceProvider; |
| + path.Context pathContext; |
| + DartSdkManager sdkManager; |
| + ContentCache contentCache; |
| + |
| + AnalysisContextImpl expectedContext; |
| + AnalysisContextImpl actualContext; |
| + |
| + Set<Element> currentRevisionValidatedElements = new Set<Element>(); |
| + |
| + void createContexts() { |
| + assert(expectedContext == null); |
| + assert(actualContext == null); |
| + resourceProvider = PhysicalResourceProvider.INSTANCE; |
| + pathContext = resourceProvider.pathContext; |
| + sdkManager = new DartSdkManager( |
| + DirectoryBasedDartSdk.defaultSdkDirectory.getAbsolutePath(), |
| + false, |
| + (_) => DirectoryBasedDartSdk.defaultSdk); |
| + contentCache = new ContentCache(); |
| + ContextBuilder builder = |
| + new ContextBuilder(resourceProvider, sdkManager, contentCache); |
| + builder.defaultOptions = new AnalysisOptionsImpl(); |
| + expectedContext = builder.buildContext(folderPath); |
| + actualContext = builder.buildContext(folderPath); |
| + expectedContext.analysisOptions = |
| + new AnalysisOptionsImpl.from(expectedContext.analysisOptions) |
| + ..incremental = true; |
| + actualContext.analysisOptions = |
| + new AnalysisOptionsImpl.from(actualContext.analysisOptions) |
| + ..incremental = true |
| + ..finerGrainedInvalidation = true; |
| + print(actualContext == expectedContext); |
| + print('Created contexts'); |
| + } |
| + |
| + run() async { |
| + GitRepository repository = new GitRepository(repoPath); |
| + |
| + // Recover. |
| + repository.removeIndexLock(); |
| + await repository.resetHard(); |
| + |
| + await repository.checkout('master'); |
| + List<GitRevision> revisions = |
| + await repository.getRevisions(after: '2016-03-01'); |
| + revisions = revisions.reversed.toList(); |
| + // TODO(scheglov) Use to compare two revisions. |
| +// List<GitRevision> revisions = [ |
| +// new GitRevision(0, '99517a162cbabf3d3afbdb566df3fe2b18cd4877', 'aaa'), |
| +// new GitRevision(0, '2ef00b0c3d0182b5e4ea5ca55fd00b9d038ae40d', 'bbb'), |
| +// ]; |
| + FolderInfo oldFolder = null; |
| + for (GitRevision revision in revisions) { |
| + print(revision); |
| + await repository.checkout(revision.hash); |
| + |
| + // Run "pub get". |
| + if (!new File('$folderPath/pubspec.yaml').existsSync()) { |
| + continue; |
| + } |
| + { |
| + ProcessResult processResult = await Process.run( |
| + '/Users/scheglov/Applications/dart-sdk/bin/pub', <String>['get'], |
| + workingDirectory: folderPath); |
| + if (processResult.exitCode != 0) { |
| + _logPrint('Pub get failed.'); |
| + _logPrint(processResult.stdout); |
| + _logPrint(processResult.stderr); |
| + continue; |
| + } |
| + _logPrint('\tpub get OK'); |
| + } |
| + FolderInfo newFolder = new FolderInfo(folderPath); |
| + |
| + if (expectedContext == null) { |
| + createContexts(); |
| + _applyChanges( |
| + newFolder.files.map((file) => file.path).toList(), [], []); |
| + _analyzeContexts(); |
| + } |
| + |
| + if (oldFolder != null) { |
| + FolderDiff diff = newFolder.diff(oldFolder); |
| + print(' $diff'); |
| + if (diff.isNotEmpty) { |
| + _applyChanges(diff.added, diff.changed, diff.removed); |
| + _analyzeContexts(); |
| + } |
| + } |
| + oldFolder = newFolder; |
| + print('\n'); |
| + print('\n'); |
| + } |
| + } |
| + |
| + /** |
| + * Perform analysis tasks up to 512 times and assert that it was enough. |
| + */ |
| + void _analyzeAll_assertFinished(AnalysisContext context, |
| + [int maxIterations = 1000000]) { |
| + for (int i = 0; i < maxIterations; i++) { |
| + List<ChangeNotice> notice = context.performAnalysisTask().changeNotices; |
| + if (notice == null) { |
| + return; |
| + } |
| + } |
| + throw new StateError( |
| + "performAnalysisTask failed to terminate after analyzing all sources"); |
| + } |
| + |
| + void _analyzeContexts() { |
| + { |
| + Stopwatch sw = new Stopwatch()..start(); |
| + _analyzeAll_assertFinished(expectedContext); |
| + print(' analyze(expected): ${sw.elapsedMilliseconds}'); |
| + } |
| + { |
| + Stopwatch sw = new Stopwatch()..start(); |
| + _analyzeAll_assertFinished(actualContext); |
| + print(' analyze(actual): ${sw.elapsedMilliseconds}'); |
| + } |
| + _validateContexts(); |
| + } |
| + |
| + void _applyChanges( |
| + List<String> added, List<String> changed, List<String> removed) { |
| + ChangeSet changeSet = new ChangeSet(); |
| + added.map(_pathToSource).forEach(changeSet.addedSource); |
| + removed.map(_pathToSource).forEach(changeSet.removedSource); |
| + changed.map(_pathToSource).forEach(changeSet.changedSource); |
| + changed.forEach((path) => new File(path).readAsStringSync()); |
| + { |
| + Stopwatch sw = new Stopwatch()..start(); |
| + expectedContext.applyChanges(changeSet); |
| + print(' apply(expected): ${sw.elapsedMilliseconds}'); |
| + } |
| + { |
| + Stopwatch sw = new Stopwatch()..start(); |
| + actualContext.applyChanges(changeSet); |
| + print(' apply(actual): ${sw.elapsedMilliseconds}'); |
| + } |
| + } |
| + |
| + Source _pathToSource(String path) { |
| + fs.File file = resourceProvider.getFile(path); |
| + return _createSourceInContext(expectedContext, file); |
| + } |
| + |
| + void _validateContexts() { |
| + currentRevisionValidatedElements.clear(); |
| + MapIterator<AnalysisTarget, CacheEntry> iterator = |
| + expectedContext.privateAnalysisCachePartition.iterator(); |
| + while (iterator.moveNext()) { |
| + AnalysisTarget target = iterator.key; |
| + CacheEntry entry = iterator.value; |
| + if (target is NonExistingSource) { |
| + continue; |
| + } |
| + _validateEntry(target, entry); |
| + } |
| + } |
| + |
| + void _validateElements( |
| + Element actualValue, Element expectedValue, Set visited) { |
| + if (actualValue == null && expectedValue == null) { |
| + return; |
| + } |
| + if (!currentRevisionValidatedElements.add(expectedValue)) { |
| + return; |
| + } |
| + if (!visited.add(expectedValue)) { |
| + return; |
| + } |
| + List<Element> sortElements(List<Element> elements) { |
| + elements = elements.toList(); |
| + elements.sort((a, b) { |
| + if (a.nameOffset != b.nameOffset) { |
| + return a.nameOffset - b.nameOffset; |
| + } |
| + return a.name.compareTo(b.name); |
| + }); |
| + return elements; |
| + } |
| + void validateSortedElements( |
| + List<Element> actualElements, List<Element> expectedElements) { |
| + expect(actualElements, hasLength(expectedElements.length)); |
| + actualElements = sortElements(actualElements); |
| + expectedElements = sortElements(expectedElements); |
| + for (int i = 0; i < expectedElements.length; i++) { |
| + _validateElements(actualElements[i], expectedElements[i], visited); |
| + } |
| + } |
| + expect(actualValue?.runtimeType, expectedValue?.runtimeType); |
| + expect(actualValue.nameOffset, expectedValue.nameOffset); |
| + expect(actualValue.name, expectedValue.name); |
| + if (expectedValue is ClassElement) { |
| + var actualElement = actualValue as ClassElement; |
| + validateSortedElements(actualElement.accessors, expectedValue.accessors); |
| + validateSortedElements( |
| + actualElement.constructors, expectedValue.constructors); |
| + validateSortedElements(actualElement.fields, expectedValue.fields); |
| + validateSortedElements(actualElement.methods, expectedValue.methods); |
| + } |
| + if (expectedValue is CompilationUnitElement) { |
| + var actualElement = actualValue as CompilationUnitElement; |
| + validateSortedElements(actualElement.accessors, expectedValue.accessors); |
| + validateSortedElements(actualElement.functions, expectedValue.functions); |
| + validateSortedElements(actualElement.types, expectedValue.types); |
| + validateSortedElements( |
| + actualElement.functionTypeAliases, expectedValue.functionTypeAliases); |
| + validateSortedElements( |
| + actualElement.topLevelVariables, expectedValue.topLevelVariables); |
| + } |
| + if (expectedValue is ExecutableElement) { |
| + var actualElement = actualValue as ExecutableElement; |
| + validateSortedElements( |
| + actualElement.parameters, expectedValue.parameters); |
| + _validateTypes( |
| + actualElement.returnType, expectedValue.returnType, visited); |
| + } |
| + } |
| + |
| + void _validateEntry(AnalysisTarget target, CacheEntry expectedEntry) { |
| + CacheEntry actualEntry = |
| + actualContext.privateAnalysisCachePartition.get(target); |
| + if (actualEntry == null) { |
| + return; |
| + } |
| + print(' (${target.runtimeType}) $target'); |
| + for (ResultDescriptor result in expectedEntry.nonInvalidResults) { |
| + var expectedData = expectedEntry.getResultDataOrNull(result); |
| + var actualData = actualEntry.getResultDataOrNull(result); |
| + if (expectedData?.state == CacheState.INVALID) { |
| + expectedData = null; |
| + } |
| + if (actualData?.state == CacheState.INVALID) { |
| + actualData = null; |
| + } |
| + if (actualData == null) { |
| + if (result != CONTENT && |
| + result != LIBRARY_ELEMENT4 && |
| + result != LIBRARY_ELEMENT5 && |
| + result != READY_LIBRARY_ELEMENT6 && |
| + result != READY_LIBRARY_ELEMENT7) { |
| + Source targetSource = target.source; |
| + if (targetSource != null && |
| + targetSource.fullName.startsWith(folderPath)) { |
| + fail('No ResultData $result for $target'); |
| + } |
| + } |
| + continue; |
| + } |
| + Object expectedValue = expectedData.value; |
| + Object actualValue = actualData.value; |
| + print(' $result ${expectedValue?.runtimeType}'); |
| + _validateResult(target, result, actualValue, expectedValue); |
| + } |
| + } |
| + |
| + void _validatePairs(AnalysisTarget target, ResultDescriptor result, |
| + List actualList, List expectedList) { |
| + if (expectedList == null) { |
| + expect(actualList, isNull); |
| + return; |
| + } |
| + expect(actualList, isNotNull); |
| + expect(actualList, hasLength(expectedList.length)); |
| + for (int i = 0; i < expectedList.length; i++) { |
| + Object expected = expectedList[i]; |
| + Object actual = actualList[i]; |
| + _validateResult(target, result, actual, expected); |
| + } |
| + } |
| + |
| + void _validateResult(AnalysisTarget target, ResultDescriptor result, |
| + Object actualValue, Object expectedValue) { |
| + if (expectedValue is bool) { |
| + expect(actualValue, expectedValue, reason: '$result of $target'); |
| + } |
| + if (expectedValue is CompilationUnit) { |
| + expect(actualValue, new isInstanceOf<CompilationUnit>()); |
| + new _AstValidator().isEqualNodes(expectedValue, actualValue); |
| + } |
| + if (expectedValue is Element) { |
| + expect(actualValue, new isInstanceOf<Element>()); |
| + _validateElements(actualValue, expectedValue, new Set.identity()); |
| + } |
| + if (expectedValue is List) { |
| + if (actualValue is List) { |
| + _validatePairs(target, result, actualValue, expectedValue); |
| + } else { |
| + _failTypeMismatch(actualValue, expectedValue); |
| + } |
| + } |
| + if (expectedValue is AnalysisError) { |
| + if (actualValue is AnalysisError) { |
| + expect(actualValue.source, expectedValue.source); |
| + expect(actualValue.offset, expectedValue.offset); |
| + expect(actualValue.message, expectedValue.message); |
| + } else { |
| + _failTypeMismatch(actualValue, expectedValue); |
| + } |
| + } |
| + } |
| + |
| + void _validateTypes(DartType actualType, DartType expectedType, Set visited) { |
| + if (!visited.add(expectedType)) { |
| + return; |
| + } |
| + expect(actualType?.runtimeType, expectedType?.runtimeType); |
| + _validateElements(actualType.element, expectedType.element, visited); |
| + } |
| + |
| + /** |
| + * Create and return a source representing the given [file] within the given |
| + * [context]. |
| + */ |
| + static Source _createSourceInContext(AnalysisContext context, fs.File file) { |
| + Source source = file.createSource(); |
| + if (context == null) { |
| + return source; |
| + } |
| + Uri uri = context.sourceFactory.restoreUri(source); |
| + return file.createSource(uri); |
| + } |
| +} |
| + |
| +/** |
| + * Compares tokens and ASTs, and built elements of declared identifiers. |
| + */ |
| +class _AstValidator extends AstComparator { |
| + @override |
| + bool isEqualNodes(AstNode expected, AstNode actual) { |
| + // TODO(scheglov) skip comments for now |
| + // [ElementBuilder.visitFunctionExpression] in resolver_test.dart |
| + // Going from c4493869ca19ef9ba6bd35d3d42e1209eb3b7e63 |
| + // to 3977c9f2274df35df6332a65af9973fd6517bc12 |
| + // With files: |
| + // '/Users/scheglov/tmp/limited-invalidation/sdk/pkg/analyzer/lib/src/generated/resolver.dart', |
| + // '/Users/scheglov/tmp/limited-invalidation/sdk/pkg/analyzer/lib/src/dart/element/builder.dart', |
| + // '/Users/scheglov/tmp/limited-invalidation/sdk/pkg/analyzer/test/generated/resolver_test.dart', |
| + if (expected is CommentReference) { |
| + return true; |
| + } |
| + // Compare nodes. |
| + bool result = super.isEqualNodes(expected, actual); |
| + if (!result) { |
| + fail('|$actual| != expected |$expected|'); |
| + } |
| + // Verify that identifiers have equal elements and types. |
| + if (expected is SimpleIdentifier && actual is SimpleIdentifier) { |
| + _verifyElements(actual.staticElement, expected.staticElement, |
| + '$expected staticElement'); |
| + _verifyElements(actual.propagatedElement, expected.propagatedElement, |
| + '$expected staticElement'); |
| + _verifyTypes( |
| + actual.staticType, expected.staticType, '$expected staticType'); |
| + _verifyTypes(actual.propagatedType, expected.propagatedType, |
| + '$expected propagatedType'); |
| + _verifyElements(actual.staticParameterElement, |
| + expected.staticParameterElement, '$expected staticParameterElement'); |
| + _verifyElements( |
| + actual.propagatedParameterElement, |
| + expected.propagatedParameterElement, |
| + '$expected propagatedParameterElement'); |
| + } |
| + return true; |
| + } |
| + |
| + void _verifyElements(Element actual, Element expected, String desc) { |
| + if (expected == null && actual == null) { |
| + return; |
| + } |
| + if (expected is MultiplyDefinedElement && |
| + actual is MultiplyDefinedElement) { |
| + return; |
| + } |
| + while (expected is Member) { |
| + if (actual is Member) { |
| + actual = (actual as Member).baseElement; |
| + expected = (expected as Member).baseElement; |
| + } else { |
| + _failTypeMismatch(actual, expected, reason: desc); |
| + } |
| + } |
| + expect(actual, equals(expected), reason: desc); |
| + } |
| + |
| + void _verifyTypes(DartType actual, DartType expected, String desc) { |
| + _verifyElements(actual?.element, expected?.element, '$desc element'); |
| + if (expected is InterfaceType) { |
| + if (actual is InterfaceType) { |
| + List<DartType> actualArguments = actual.typeArguments; |
| + List<DartType> expectedArguments = expected.typeArguments; |
| + expect( |
| + actualArguments, |
| + pairwiseCompare(expectedArguments, (a, b) { |
| + _verifyTypes(a, b, '$desc typeArguments'); |
| + return true; |
| + }, 'elements')); |
| + } else { |
| + _failTypeMismatch(actual, expected); |
| + } |
| + } |
| + } |
| +} |