99ce88f8bc
Review URL: https://codereview.chromium.org/2426723002 .
301 lines
10 KiB
Dart
301 lines
10 KiB
Dart
// 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 {}
|