blob: 788088f0d59be11fd4df7c9f89bc7914761d94e8 [file] [log] [blame]
// Copyright (c) 2015, 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 test.analysis.updateContent;
import 'package:analysis_server/plugin/protocol/protocol.dart';
import 'package:analysis_server/src/analysis_server.dart';
import 'package:analysis_server/src/constants.dart';
import 'package:analysis_server/src/services/index/index.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import 'package:typed_mock/typed_mock.dart';
import '../analysis_abstract.dart';
main() {
defineReflectiveSuite(() {
defineReflectiveTests(UpdateContentTest);
});
}
compilationUnitMatcher(String file) {
return new _ArgumentMatcher_CompilationUnit(file);
}
@reflectiveTest
class UpdateContentTest extends AbstractAnalysisTest {
Map<String, List<String>> filesErrors = {};
int serverErrorCount = 0;
int navigationCount = 0;
Index createIndex() {
return new _MockIndex();
}
@override
void processNotification(Notification notification) {
if (notification.event == ANALYSIS_ERRORS) {
var decoded = new AnalysisErrorsParams.fromNotification(notification);
String _format(AnalysisError e) =>
"${e.location.startLine}: ${e.message}";
filesErrors[decoded.file] = decoded.errors.map(_format).toList();
}
if (notification.event == ANALYSIS_NAVIGATION) {
navigationCount++;
}
if (notification.event == SERVER_ERROR) {
serverErrorCount++;
}
}
test_discardNotifications_onSourceChange() async {
createProject();
addTestFile('');
await server.onAnalysisComplete;
server.setAnalysisSubscriptions({
AnalysisService.NAVIGATION: [testFile].toSet()
});
// update file, analyze, but don't sent notifications
navigationCount = 0;
server.updateContent('1', {testFile: new AddContentOverlay('foo() {}')});
server.test_performAllAnalysisOperations();
expect(serverErrorCount, 0);
expect(navigationCount, 0);
// replace the file contents,
// should discard any pending notification operations
server.updateContent('2', {testFile: new AddContentOverlay('bar() {}')});
await server.onAnalysisComplete;
expect(serverErrorCount, 0);
expect(navigationCount, 1);
}
test_illegal_ChangeContentOverlay() {
// It should be illegal to send a ChangeContentOverlay for a file that
// doesn't have an overlay yet.
createProject();
addTestFile('library foo;');
String id = 'myId';
try {
server.updateContent(id, {
testFile: new ChangeContentOverlay([new SourceEdit(8, 3, 'bar')])
});
fail('Expected an exception to be thrown');
} on RequestFailure catch (e) {
expect(e.response.id, id);
expect(e.response.error.code, RequestErrorCode.INVALID_OVERLAY_CHANGE);
}
}
test_indexUnitAfterNopChange() async {
var testUnitMatcher = compilationUnitMatcher(testFile) as dynamic;
createProject();
addTestFile('main() { print(1); }');
await server.onAnalysisComplete;
verify(server.index.indexUnit(testUnitMatcher)).times(1);
// add an overlay
server.updateContent(
'1', {testFile: new AddContentOverlay('main() { print(2); }')});
// Perform the next single operation: analysis.
// It will schedule an indexing operation.
await server.test_onOperationPerformed;
// Update the file and remove an overlay.
resourceProvider.updateFile(testFile, 'main() { print(2); }');
server.updateContent('2', {testFile: new RemoveContentOverlay()});
// Validate that at the end the unit was indexed.
await server.onAnalysisComplete;
verify(server.index.indexUnit(testUnitMatcher)).times(3);
}
test_multiple_contexts() async {
String fooPath = '/project1/foo.dart';
resourceProvider.newFile(
fooPath,
'''
library foo;
import '../project2/baz.dart';
main() { f(); }''');
String barPath = '/project2/bar.dart';
resourceProvider.newFile(
barPath,
'''
library bar;
import 'baz.dart';
main() { f(); }''');
String bazPath = '/project2/baz.dart';
resourceProvider.newFile(
bazPath,
'''
library baz;
f(int i) {}
''');
Request request =
new AnalysisSetAnalysisRootsParams(['/project1', '/project2'], [])
.toRequest('0');
handleSuccessfulRequest(request);
{
await server.onAnalysisComplete;
// Files foo.dart and bar.dart should both have errors, since they both
// call f() with the wrong number of arguments.
expect(filesErrors[fooPath], hasLength(1));
expect(filesErrors[barPath], hasLength(1));
// Overlay the content of baz.dart to eliminate the errors.
server.updateContent('1', {
bazPath: new AddContentOverlay('''
library baz;
f() {}
''')
});
}
{
await server.onAnalysisComplete;
// The overlay should have been propagated to both contexts, causing both
// foo.dart and bar.dart to be reanalyzed and found to be free of errors.
expect(filesErrors[fooPath], isEmpty);
expect(filesErrors[barPath], isEmpty);
}
}
test_overlay_addPreviouslyImported() async {
Folder project = resourceProvider.newFolder('/project');
handleSuccessfulRequest(
new AnalysisSetAnalysisRootsParams([project.path], []).toRequest('0'));
server.updateContent('1',
{'/project/main.dart': new AddContentOverlay('import "target.dart";')});
await server.onAnalysisComplete;
expect(filesErrors, {
'/project/main.dart': ["1: Target of URI doesn't exist: 'target.dart'."],
'/project/target.dart': []
});
server.updateContent('1',
{'/project/target.dart': new AddContentOverlay('import "none.dart";')});
await server.onAnalysisComplete;
expect(filesErrors, {
'/project/main.dart': ["1: Unused import."],
'/project/target.dart': ["1: Target of URI doesn't exist: 'none.dart'."],
'/project/none.dart': []
});
}
test_overlayOnly() async {
String filePath = '/User/project1/test.dart';
Folder folder1 = resourceProvider.newFolder('/User/project1');
Folder folder2 = resourceProvider.newFolder('/User/project2');
Request request =
new AnalysisSetAnalysisRootsParams([folder1.path, folder2.path], [])
.toRequest('0');
handleSuccessfulRequest(request);
// exactly 2 contexts
expect(server.folderMap, hasLength(2));
AnalysisContext context1 = server.folderMap[folder1];
AnalysisContext context2 = server.folderMap[folder2];
// no sources
expect(_getUserSources(context1), isEmpty);
expect(_getUserSources(context2), isEmpty);
// add an overlay - new Source in context1
server.updateContent('1', {filePath: new AddContentOverlay('')});
{
List<Source> sources = _getUserSources(context1);
expect(sources, hasLength(1));
expect(sources[0].fullName, filePath);
}
expect(_getUserSources(context2), isEmpty);
// remove the overlay - no sources
server.updateContent('2', {filePath: new RemoveContentOverlay()});
expect(_getUserSources(context1), isEmpty);
expect(_getUserSources(context2), isEmpty);
}
test_removeOverlay_incrementalChange() async {
createProject();
addTestFile('main() { print(1); }');
await server.onAnalysisComplete;
CompilationUnit unit = _getTestUnit();
// add an overlay
server.updateContent(
'1', {testFile: new AddContentOverlay('main() { print(2); }')});
// it was an incremental change
await server.onAnalysisComplete;
expect(_getTestUnit(), same(unit));
// remove overlay
server.updateContent('2', {testFile: new RemoveContentOverlay()});
// it was an incremental change
await server.onAnalysisComplete;
expect(_getTestUnit(), same(unit));
}
test_sendNoticesAfterNopChange() async {
createProject();
addTestFile('');
await server.onAnalysisComplete;
// add an overlay
server.updateContent(
'1', {testFile: new AddContentOverlay('main() {} main() {}')});
await server.onAnalysisComplete;
// clear errors and make a no-op change
filesErrors.clear();
server.updateContent('2', {
testFile: new ChangeContentOverlay([new SourceEdit(0, 4, 'main')])
});
await server.onAnalysisComplete;
// errors should have been resent
expect(filesErrors, isNotEmpty);
}
test_sendNoticesAfterNopChange_flushedUnit() async {
createProject();
addTestFile('');
await server.onAnalysisComplete;
// add an overlay
server.updateContent(
'1', {testFile: new AddContentOverlay('main() {} main() {}')});
await server.onAnalysisComplete;
// clear errors and make a no-op change
filesErrors.clear();
server.test_flushAstStructures(testFile);
server.updateContent('2', {
testFile: new ChangeContentOverlay([new SourceEdit(0, 4, 'main')])
});
await server.onAnalysisComplete;
// errors should have been resent
expect(filesErrors, isNotEmpty);
}
CompilationUnit _getTestUnit() {
ContextSourcePair pair = server.getContextSourcePair(testFile);
AnalysisContext context = pair.context;
Source source = pair.source;
return context.getResolvedCompilationUnit2(source, source);
}
List<Source> _getUserSources(AnalysisContext context) {
List<Source> sources = <Source>[];
context.sources.forEach((source) {
if (source.fullName.startsWith('/User/')) {
sources.add(source);
}
});
return sources;
}
}
class _ArgumentMatcher_CompilationUnit extends ArgumentMatcher {
final String file;
_ArgumentMatcher_CompilationUnit(this.file);
@override
bool matches(arg) {
return arg is CompilationUnit && arg.element.source.fullName == file;
}
}
class _MockIndex extends TypedMock implements Index {}