blob: 23105816ef255109355f5c26325e402123a3077f [file] [log] [blame]
// 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/error/error.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/dart/sdk/sdk.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/generated/sdk.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:test/test.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';
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;
fs.Folder sdkDirectory =
FolderBasedDartSdk.defaultSdkDirectory(resourceProvider);
sdkManager = new DartSdkManager(sdkDirectory.path, false);
contentCache = new ContentCache();
ContextBuilderOptions builderOptions = new ContextBuilderOptions();
builderOptions.defaultOptions = new AnalysisOptionsImpl();
ContextBuilder builder = new ContextBuilder(
resourceProvider, sdkManager, contentCache,
options: builderOptions);
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('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-01-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);
}
}
}
}