blob: 34e20efdbb12f04d3580a5b54762526a16ef8807 [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.driver;
import 'dart:async';
import 'dart:convert';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/file_system/memory_file_system.dart';
import 'package:analyzer/source/package_map_resolver.dart';
import 'package:analyzer/src/dart/analysis/byte_store.dart';
import 'package:analyzer/src/dart/analysis/driver.dart';
import 'package:analyzer/src/error/codes.dart';
import 'package:analyzer/src/generated/engine.dart' show AnalysisOptionsImpl;
import 'package:analyzer/src/generated/source.dart';
import 'package:convert/convert.dart';
import 'package:crypto/crypto.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../../context/mock_sdk.dart';
main() {
defineReflectiveSuite(() {
defineReflectiveTests(DriverTest);
});
}
/**
* Returns a [Future] that completes after pumping the event queue [times]
* times. By default, this should pump the event queue enough times to allow
* any code to run, as long as it's not waiting on some external event.
*/
Future pumpEventQueue([int times = 5000]) {
if (times == 0) return new Future.value();
// We use a delayed future to allow microtask events to finish. The
// Future.value or Future() constructors use scheduleMicrotask themselves and
// would therefore not wait for microtask callbacks that are scheduled after
// invoking this method.
return new Future.delayed(Duration.ZERO, () => pumpEventQueue(times - 1));
}
@reflectiveTest
class DriverTest {
static final MockSdk sdk = new MockSdk();
final MemoryResourceProvider provider = new MemoryResourceProvider();
final ByteStore byteStore = new _TestByteStore();
final ContentCache contentCache = new ContentCache();
final StringBuffer logBuffer = new StringBuffer();
AnalysisDriver driver;
final _Monitor idleStatusMonitor = new _Monitor();
final List<AnalysisStatus> allStatuses = <AnalysisStatus>[];
final List<AnalysisResult> allResults = <AnalysisResult>[];
String testProject;
String testFile;
void setUp() {
new MockSdk();
testProject = _p('/test/lib');
testFile = _p('/test/lib/test.dart');
driver = new AnalysisDriver(
new PerformanceLog(logBuffer),
provider,
byteStore,
contentCache,
new SourceFactory([
new DartUriResolver(sdk),
new PackageMapUriResolver(provider, <String, List<Folder>>{
'test': [provider.getFolder(testProject)]
})
], null, provider),
new AnalysisOptionsImpl()..strongMode = true);
driver.status.lastWhere((status) {
allStatuses.add(status);
if (status.isIdle) {
idleStatusMonitor.notify();
}
});
driver.results.listen(allResults.add);
}
test_addFile_thenRemove() async {
var a = _p('/test/lib/a.dart');
var b = _p('/test/lib/b.dart');
provider.newFile(a, 'class A {}');
provider.newFile(b, 'class B {}');
driver.addFile(a);
driver.addFile(b);
// Now remove 'a'.
driver.removeFile(a);
await _waitForIdle();
// Only 'b' has been analyzed, because 'a' was removed before we started.
expect(allResults, hasLength(1));
expect(allResults[0].path, b);
}
test_changeFile_implicitlyAnalyzed() async {
var a = _p('/test/lib/a.dart');
var b = _p('/test/lib/b.dart');
provider.newFile(
a,
r'''
import 'b.dart';
var A = B;
''');
provider.newFile(b, 'var B = 1;');
driver.priorityFiles = [a];
driver.addFile(a);
// We have a result only for "a".
await _waitForIdle();
expect(allResults, hasLength(1));
{
AnalysisResult ar = allResults.firstWhere((r) => r.path == a);
expect(_getTopLevelVarType(ar.unit, 'A'), 'int');
}
allResults.clear();
// Change "b" and notify.
provider.updateFile(b, 'var B = 1.2;');
driver.changeFile(b);
// While "b" is not analyzed explicitly, it is analyzed implicitly.
// The change causes "a" to be reanalyzed.
await _waitForIdle();
expect(allResults, hasLength(1));
{
AnalysisResult ar = allResults.firstWhere((r) => r.path == a);
expect(_getTopLevelVarType(ar.unit, 'A'), 'double');
}
}
test_changeFile_selfConsistent() async {
var a = _p('/test/lib/a.dart');
var b = _p('/test/lib/b.dart');
provider.newFile(
a,
r'''
import 'b.dart';
var A1 = 1;
var A2 = B1;
''');
provider.newFile(
b,
r'''
import 'a.dart';
var B1 = A1;
''');
driver.priorityFiles = [a, b];
driver.addFile(a);
driver.addFile(b);
await _waitForIdle();
// We have results for both "a" and "b".
expect(allResults, hasLength(2));
{
AnalysisResult ar = allResults.firstWhere((r) => r.path == a);
expect(_getTopLevelVarType(ar.unit, 'A1'), 'int');
expect(_getTopLevelVarType(ar.unit, 'A2'), 'int');
}
{
AnalysisResult br = allResults.firstWhere((r) => r.path == b);
expect(_getTopLevelVarType(br.unit, 'B1'), 'int');
}
// Clear the results and update "a".
allResults.clear();
provider.updateFile(
a,
r'''
import 'b.dart';
var A1 = 1.2;
var A2 = B1;
''');
driver.changeFile(a);
// We again get results for both "a" and "b".
// The results are consistent.
await _waitForIdle();
expect(allResults, hasLength(2));
{
AnalysisResult ar = allResults.firstWhere((r) => r.path == a);
expect(_getTopLevelVarType(ar.unit, 'A1'), 'double');
expect(_getTopLevelVarType(ar.unit, 'A2'), 'double');
}
{
AnalysisResult br = allResults.firstWhere((r) => r.path == b);
expect(_getTopLevelVarType(br.unit, 'B1'), 'double');
}
}
test_changeFile_single() async {
_addTestFile('var V = 1;', priority: true);
// Initial analysis.
{
await _waitForIdle();
expect(allResults, hasLength(1));
AnalysisResult result = allResults[0];
expect(result.path, testFile);
expect(_getTopLevelVarType(result.unit, 'V'), 'int');
}
// Update the file, but don't notify the driver.
allResults.clear();
provider.updateFile(testFile, 'var V = 1.2');
// No new results.
await pumpEventQueue();
expect(allResults, isEmpty);
// Notify the driver about the change.
driver.changeFile(testFile);
// We get a new result.
{
await _waitForIdle();
expect(allResults, hasLength(1));
AnalysisResult result = allResults[0];
expect(result.path, testFile);
expect(_getTopLevelVarType(result.unit, 'V'), 'double');
}
}
test_getResult() async {
String content = 'int f() => 42;';
_addTestFile(content, priority: true);
AnalysisResult result = await driver.getResult(testFile);
expect(result.path, testFile);
expect(result.uri.toString(), 'package:test/test.dart');
expect(result.content, content);
expect(result.contentHash, _md5(content));
expect(result.unit, isNotNull);
expect(result.errors, hasLength(0));
var f = result.unit.declarations[0] as FunctionDeclaration;
expect(f.name.staticType.toString(), '() → int');
expect(f.returnType.type.toString(), 'int');
// The same result is also received through the stream.
await _waitForIdle();
expect(allResults, [result]);
}
test_getResult_errors() async {
String content = 'main() { int vv; }';
_addTestFile(content, priority: true);
AnalysisResult result = await driver.getResult(testFile);
expect(result.path, testFile);
expect(result.errors, hasLength(1));
{
AnalysisError error = result.errors[0];
expect(error.offset, 13);
expect(error.length, 2);
expect(error.errorCode, HintCode.UNUSED_LOCAL_VARIABLE);
expect(error.message, "The value of the local variable 'vv' isn't used.");
expect(error.correction, "Try removing the variable, or using it.");
}
}
test_getResult_selfConsistent() async {
var a = _p('/test/lib/a.dart');
var b = _p('/test/lib/b.dart');
provider.newFile(
a,
r'''
import 'b.dart';
var A1 = 1;
var A2 = B1;
''');
provider.newFile(
b,
r'''
import 'a.dart';
var B1 = A1;
''');
driver.addFile(a);
driver.addFile(b);
await _waitForIdle();
{
AnalysisResult result = await driver.getResult(a);
expect(_getTopLevelVarType(result.unit, 'A1'), 'int');
expect(_getTopLevelVarType(result.unit, 'A2'), 'int');
}
// Update "a" that that "A1" is now "double".
// Get result for "a".
//
// Even though we have not notified the driver about the change,
// we still get "double" for "A1", because getResult() re-read the content.
//
// We also get "double" for "A2", even though "A2" has the type from "b".
// That's because we check for "a" API signature consistency, and because
// it has changed, we invalidated the dependency cache, relinked libraries
// and recomputed types.
provider.updateFile(
a,
r'''
import 'b.dart';
var A1 = 1.2;
var A2 = B1;
''');
{
AnalysisResult result = await driver.getResult(a);
expect(_getTopLevelVarType(result.unit, 'A1'), 'double');
expect(_getTopLevelVarType(result.unit, 'A2'), 'double');
}
}
test_getResult_thenRemove() async {
_addTestFile('main() {}', priority: true);
Future<AnalysisResult> resultFuture = driver.getResult(testFile);
driver.removeFile(testFile);
AnalysisResult result = await resultFuture;
expect(result, isNotNull);
expect(result.path, testFile);
expect(result.unit, isNotNull);
}
test_getResult_twoPendingFutures() async {
String content = 'main() {}';
_addTestFile(content, priority: true);
Future<AnalysisResult> future1 = driver.getResult(testFile);
Future<AnalysisResult> future2 = driver.getResult(testFile);
// Both futures complete, with the same result.
AnalysisResult result1 = await future1;
AnalysisResult result2 = await future2;
expect(result2, same(result1));
expect(result1.path, testFile);
expect(result1.unit, isNotNull);
}
test_removeFile_changeFile_implicitlyAnalyzed() async {
var a = _p('/test/lib/a.dart');
var b = _p('/test/lib/b.dart');
provider.newFile(
a,
r'''
import 'b.dart';
var A = B;
''');
provider.newFile(b, 'var B = 1;');
driver.priorityFiles = [a, b];
driver.addFile(a);
driver.addFile(b);
// We have results for both "a" and "b".
await _waitForIdle();
expect(allResults, hasLength(2));
{
AnalysisResult ar = allResults.firstWhere((r) => r.path == a);
expect(_getTopLevelVarType(ar.unit, 'A'), 'int');
}
{
AnalysisResult br = allResults.firstWhere((r) => r.path == b);
expect(_getTopLevelVarType(br.unit, 'B'), 'int');
}
allResults.clear();
// Remove "b" and send the change notification.
provider.updateFile(b, 'var B = 1.2;');
driver.removeFile(b);
driver.changeFile(b);
// While "b" is not analyzed explicitly, it is analyzed implicitly.
// We don't get a result for "b".
// But the change causes "a" to be reanalyzed.
await _waitForIdle();
expect(allResults, hasLength(1));
{
AnalysisResult ar = allResults.firstWhere((r) => r.path == a);
expect(_getTopLevelVarType(ar.unit, 'A'), 'double');
}
}
test_removeFile_changeFile_notAnalyzed() async {
_addTestFile('main() {}');
// We have a result.
await _waitForIdle();
expect(allResults, hasLength(1));
expect(allResults[0].path, testFile);
allResults.clear();
// Remove the file and send the change notification.
// The change notification does nothing, because the file is explicitly
// or implicitly analyzed.
driver.removeFile(testFile);
driver.changeFile(testFile);
await _waitForIdle();
expect(allResults, isEmpty);
}
test_results_priority() async {
String content = 'int f() => 42;';
_addTestFile(content, priority: true);
await _waitForIdle();
expect(allResults, hasLength(1));
AnalysisResult result = allResults.single;
expect(result.path, testFile);
expect(result.uri.toString(), 'package:test/test.dart');
expect(result.content, content);
expect(result.contentHash, _md5(content));
expect(result.unit, isNotNull);
expect(result.errors, hasLength(0));
var f = result.unit.declarations[0] as FunctionDeclaration;
expect(f.name.staticType.toString(), '() → int');
expect(f.returnType.type.toString(), 'int');
}
test_results_priorityFirst() async {
var a = _p('/test/lib/a.dart');
var b = _p('/test/lib/b.dart');
var c = _p('/test/lib/c.dart');
provider.newFile(a, 'class A {}');
provider.newFile(b, 'class B {}');
provider.newFile(c, 'class C {}');
driver.addFile(a);
driver.addFile(b);
driver.addFile(c);
driver.priorityFiles = [b];
await _waitForIdle();
expect(allResults, hasLength(3));
AnalysisResult result = allResults[0];
expect(result.path, b);
expect(result.unit, isNotNull);
expect(result.errors, hasLength(0));
}
test_results_regular() async {
String content = 'int f() => 42;';
_addTestFile(content);
await _waitForIdle();
expect(allResults, hasLength(1));
AnalysisResult result = allResults.single;
expect(result.path, testFile);
expect(result.uri.toString(), 'package:test/test.dart');
expect(result.content, isNull);
expect(result.contentHash, _md5(content));
expect(result.unit, isNull);
expect(result.errors, hasLength(0));
}
test_results_status() async {
_addTestFile('int f() => 42;');
await _waitForIdle();
expect(allStatuses, hasLength(2));
expect(allStatuses[0].isAnalyzing, isTrue);
expect(allStatuses[0].isIdle, isFalse);
expect(allStatuses[1].isAnalyzing, isFalse);
expect(allStatuses[1].isIdle, isTrue);
}
void _addTestFile(String content, {bool priority: false}) {
provider.newFile(testFile, content);
driver.addFile(testFile);
if (priority) {
driver.priorityFiles = [testFile];
}
}
VariableDeclaration _getTopLevelVar(CompilationUnit unit, String name) {
for (CompilationUnitMember declaration in unit.declarations) {
if (declaration is TopLevelVariableDeclaration) {
for (VariableDeclaration variable in declaration.variables.variables) {
if (variable.name.name == name) {
return variable;
}
}
}
}
fail('Cannot find the top-level variable $name in\n$unit');
return null;
}
String _getTopLevelVarType(CompilationUnit unit, String name) {
return _getTopLevelVar(unit, name).element.type.toString();
}
/**
* Return the [provider] specific path for the given Posix [path].
*/
String _p(String path) => provider.convertPath(path);
Future<Null> _waitForIdle() async {
await idleStatusMonitor.signal;
}
static String _md5(String content) {
return hex.encode(md5.convert(UTF8.encode(content)).bytes);
}
}
class _Monitor {
Completer<Null> _completer = new Completer<Null>();
Future<Null> get signal async {
await _completer.future;
_completer = new Completer<Null>();
}
void notify() {
if (!_completer.isCompleted) {
_completer.complete(null);
}
}
}
class _TestByteStore implements ByteStore {
final map = <String, List<int>>{};
@override
List<int> get(String key) {
return map[key];
}
@override
void put(String key, List<int> bytes) {
map[key] = bytes;
}
}