5c07c89a35
Change-Id: I71ee934b915d4aff924cf5925f87bcc9adc6c015 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/362901 Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
375 lines
13 KiB
Dart
375 lines
13 KiB
Dart
// Copyright (c) 2019, 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.
|
|
|
|
import 'dart:async';
|
|
|
|
import 'package:analysis_server/lsp_protocol/protocol.dart';
|
|
import 'package:analysis_server/src/analysis_server.dart';
|
|
import 'package:analysis_server/src/protocol/protocol_internal.dart';
|
|
import 'package:analyzer_plugin/protocol/protocol_common.dart' hide Position;
|
|
import 'package:test/test.dart';
|
|
import 'package:test_reflective_loader/test_reflective_loader.dart';
|
|
|
|
import 'server_abstract.dart';
|
|
|
|
void main() {
|
|
defineReflectiveSuite(() {
|
|
defineReflectiveTests(DocumentChangesTest);
|
|
});
|
|
}
|
|
|
|
@reflectiveTest
|
|
class DocumentChangesTest extends AbstractLspAnalysisServerTest {
|
|
String get content => '''
|
|
class Foo {
|
|
String get bar => 'baz';
|
|
}
|
|
''';
|
|
|
|
String get contentAfterUpdate => '''
|
|
class Bar {
|
|
String get bar => 'updated';
|
|
}
|
|
''';
|
|
|
|
Future<void> test_documentChange_notifiesPlugins() async {
|
|
if (!AnalysisServer.supportsPlugins) return;
|
|
await _initializeAndOpen();
|
|
await changeFile(2, mainFileUri, [
|
|
TextDocumentContentChangeEvent.t1(TextDocumentContentChangeEvent1(
|
|
range: Range(
|
|
start: Position(line: 0, character: 6),
|
|
end: Position(line: 0, character: 9)),
|
|
text: 'Bar',
|
|
)),
|
|
TextDocumentContentChangeEvent.t1(TextDocumentContentChangeEvent1(
|
|
range: Range(
|
|
start: Position(line: 1, character: 21),
|
|
end: Position(line: 1, character: 24)),
|
|
text: 'updated',
|
|
)),
|
|
]);
|
|
|
|
var notifiedChanges = pluginManager.analysisUpdateContentParams!
|
|
.files[mainFilePath] as ChangeContentOverlay;
|
|
|
|
expect(
|
|
applySequenceOfEdits(content, notifiedChanges.edits),
|
|
contentAfterUpdate,
|
|
);
|
|
}
|
|
|
|
Future<void> test_documentChange_updatesOverlay() async {
|
|
await _initializeAndOpen();
|
|
await changeFile(2, mainFileUri, [
|
|
TextDocumentContentChangeEvent.t1(TextDocumentContentChangeEvent1(
|
|
range: Range(
|
|
start: Position(line: 0, character: 6),
|
|
end: Position(line: 0, character: 9)),
|
|
text: 'Bar',
|
|
)),
|
|
TextDocumentContentChangeEvent.t1(TextDocumentContentChangeEvent1(
|
|
range: Range(
|
|
start: Position(line: 1, character: 21),
|
|
end: Position(line: 1, character: 24)),
|
|
text: 'updated',
|
|
)),
|
|
]);
|
|
|
|
expect(server.resourceProvider.hasOverlay(mainFilePath), isTrue);
|
|
expect(server.resourceProvider.getFile(mainFilePath).readAsStringSync(),
|
|
equals(contentAfterUpdate));
|
|
}
|
|
|
|
Future<void> test_documentClose_deletesOverlay() async {
|
|
await _initializeAndOpen();
|
|
await closeFile(mainFileUri);
|
|
|
|
expect(server.resourceProvider.hasOverlay(mainFilePath), isFalse);
|
|
}
|
|
|
|
Future<void> test_documentClose_notifiesPlugins() async {
|
|
if (!AnalysisServer.supportsPlugins) return;
|
|
await _initializeAndOpen();
|
|
await closeFile(mainFileUri);
|
|
|
|
expect(pluginManager.analysisUpdateContentParams!.files,
|
|
equals({mainFilePath: RemoveContentOverlay()}));
|
|
}
|
|
|
|
Future<void>
|
|
test_documentOpen_addsOverlayOnlyToDriver_onlyIfInsideRoots() async {
|
|
// Ensures that opening a file doesn't add it to the driver if it's outside
|
|
// of the drivers root.
|
|
var fileInsideRootPath = mainFilePath;
|
|
var fileOutsideRootPath = convertPath('/home/unrelated/main.dart');
|
|
await initialize();
|
|
await openFile(pathContext.toUri(fileInsideRootPath), content);
|
|
await openFile(pathContext.toUri(fileOutsideRootPath), content);
|
|
|
|
// Expect both files return the same driver
|
|
var driverForInside = server.getAnalysisDriver(fileInsideRootPath)!;
|
|
var driverForOutside = server.getAnalysisDriver(fileOutsideRootPath)!;
|
|
expect(driverForInside, equals(driverForOutside));
|
|
// But that only the file inside the root was added.
|
|
expect(driverForInside.addedFiles, contains(fileInsideRootPath));
|
|
expect(driverForInside.addedFiles, isNot(contains(fileOutsideRootPath)));
|
|
}
|
|
|
|
Future<void> test_documentOpen_contentChanged_analysis() async {
|
|
failTestOnErrorDiagnostic = false;
|
|
|
|
const content = '// original content';
|
|
const newContent = 'new content'; // triggers diagnostic
|
|
newFile(mainFilePath, content);
|
|
|
|
await initialize();
|
|
await initialAnalysis;
|
|
|
|
await openFile(mainFileUri, newContent);
|
|
await pumpEventQueue(times: 50000);
|
|
|
|
// Expect diagnostics, because changing the content will have triggered
|
|
// analysis.
|
|
expect(diagnostics[mainFileUri], isNotEmpty);
|
|
}
|
|
|
|
Future<void> test_documentOpen_contentUnchanged_noAnalysis() async {
|
|
const content = '// original content';
|
|
newFile(mainFilePath, content);
|
|
|
|
await initialize();
|
|
await initialAnalysis;
|
|
|
|
await openFile(mainFileUri, content);
|
|
await pumpEventQueue(times: 5000);
|
|
|
|
// Expect no diagnostics because the file didn't actually change content
|
|
// when the overlay was created, so it should not have triggered analysis.
|
|
expect(diagnostics[mainFileUri], isNull);
|
|
}
|
|
|
|
Future<void> test_documentOpen_createsOverlay() async {
|
|
await _initializeAndOpen();
|
|
|
|
expect(server.resourceProvider.hasOverlay(mainFilePath), isTrue);
|
|
expect(server.resourceProvider.getFile(mainFilePath).readAsStringSync(),
|
|
equals(content));
|
|
}
|
|
|
|
/// Verify that calling open/close repeatedly produces no errors and ends
|
|
/// in the closed state.
|
|
Future<void> test_documentOpen_documentClose_repeatedly_endClosed() async {
|
|
await initialize(
|
|
// This forces roots to be rebuilt when the files open/close which
|
|
// increases the workload and makes any `await` in processing open/close
|
|
// files more likely to trigger the failure.
|
|
initializationOptions: {'onlyAnalyzeProjectsWithOpenFiles': true},
|
|
);
|
|
|
|
await Future.wait([
|
|
for (var i = 0; i < 100; i++) ...[
|
|
openFile(mainFileUri, content),
|
|
closeFile(mainFileUri)
|
|
]
|
|
]);
|
|
|
|
expect(server.resourceProvider.hasOverlay(mainFilePath), isFalse);
|
|
}
|
|
|
|
/// Verify that calling open/close repeatedly produces no errors and ends
|
|
/// in the open state if the last entry was open.
|
|
Future<void> test_documentOpen_documentClose_repeatedly_endOpen() async {
|
|
await initialize(
|
|
// This forces roots to be rebuilt when the files open/close which
|
|
// increases the workload and makes any `await` in processing open/close
|
|
// files more likely to trigger the failure.
|
|
initializationOptions: {'onlyAnalyzeProjectsWithOpenFiles': true},
|
|
);
|
|
|
|
await Future.wait([
|
|
for (var i = 0; i < 100; i++) ...[
|
|
openFile(mainFileUri, content),
|
|
closeFile(mainFileUri)
|
|
],
|
|
openFile(mainFileUri, content),
|
|
]);
|
|
|
|
expect(server.resourceProvider.hasOverlay(mainFilePath), isTrue);
|
|
}
|
|
|
|
/// Tests that deleting a file does not clear diagnostics while there's an
|
|
/// overlay, and that removing the overlay later clears the diagnostics.
|
|
///
|
|
/// https://github.com/dart-lang/sdk/issues/53475
|
|
Future<void> test_documentOpen_fileDeleted_documentClosed() async {
|
|
failTestOnErrorDiagnostic = false;
|
|
|
|
const content = 'error';
|
|
newFile(mainFilePath, content);
|
|
|
|
// Expect diagnostics after initial analysis because file has invalid
|
|
// content.
|
|
await initialize();
|
|
await pumpEventQueue(times: 5000);
|
|
expect(diagnostics[mainFileUri], isNotEmpty);
|
|
|
|
// Expect diagnostics after opening the file with the same contents.
|
|
await openFile(mainFileUri, content);
|
|
await pumpEventQueue(times: 5000);
|
|
expect(diagnostics[mainFileUri], isNotEmpty);
|
|
|
|
// Expect diagnostics after deleting the file because the overlay is still
|
|
// active.
|
|
deleteFile(mainFilePath);
|
|
await pumpEventQueue(times: 5000);
|
|
expect(diagnostics[mainFileUri], isNotEmpty);
|
|
|
|
// Expect diagnostics to be removed after we close the file (which removes
|
|
// the overlay).
|
|
await closeFile(mainFileUri);
|
|
await pumpEventQueue(times: 5000);
|
|
expect(diagnostics[mainFileUri], isEmpty);
|
|
}
|
|
|
|
/// Tests that deleting and re-creating a file while an overlay is active
|
|
/// keeps the diagnotics when the overlay is then removed, then removes them
|
|
/// when the file is deleted.
|
|
///
|
|
/// https://github.com/dart-lang/sdk/issues/53475
|
|
Future<void>
|
|
test_documentOpen_fileDeleted_fileCreated_documentClosed_fileDeleted() async {
|
|
failTestOnErrorDiagnostic = false;
|
|
|
|
const content = 'error';
|
|
newFile(mainFilePath, content);
|
|
|
|
// Expect diagnostics after initial analysis because file has invalid
|
|
// content.
|
|
await initialize();
|
|
await pumpEventQueue(times: 5000);
|
|
expect(diagnostics[mainFileUri], isNotEmpty);
|
|
|
|
// Expect diagnostics after opening the file with the same contents.
|
|
await openFile(mainFileUri, content);
|
|
await pumpEventQueue(times: 5000);
|
|
expect(diagnostics[mainFileUri], isNotEmpty);
|
|
|
|
// Expect diagnostics after deleting the file because the overlay is still
|
|
// active.
|
|
deleteFile(mainFilePath);
|
|
await pumpEventQueue(times: 5000);
|
|
expect(diagnostics[mainFileUri], isNotEmpty);
|
|
|
|
// Expect diagnostics remain after re-creating the file (the overlay is still
|
|
// active).
|
|
newFile(mainFilePath, content);
|
|
await pumpEventQueue(times: 5000);
|
|
expect(diagnostics[mainFileUri], isNotEmpty);
|
|
|
|
// Expect diagnostics remain after we close the file because the file still
|
|
//exists on disk.
|
|
await closeFile(mainFileUri);
|
|
await pumpEventQueue(times: 5000);
|
|
expect(diagnostics[mainFileUri], isNotEmpty);
|
|
|
|
// Finally, expect deleting the file clears the diagnostics.
|
|
deleteFile(mainFilePath);
|
|
await pumpEventQueue(times: 5000);
|
|
expect(diagnostics[mainFileUri], isEmpty);
|
|
}
|
|
|
|
Future<void> test_documentOpen_notifiesPlugins() async {
|
|
if (!AnalysisServer.supportsPlugins) return;
|
|
await _initializeAndOpen();
|
|
|
|
expect(pluginManager.analysisUpdateContentParams!.files,
|
|
equals({mainFilePath: AddContentOverlay(content)}));
|
|
}
|
|
|
|
/// Verifies the fix for a race condition where an overlay would not be
|
|
/// processed if the file was created on disk before the overlay was processed
|
|
/// (but the watch event had also not yet been processed).
|
|
///
|
|
/// https://github.com/dart-lang/sdk/issues/51159
|
|
Future<void> test_documentOpen_processesOverlay_dartSdk_issue51159() async {
|
|
failTestOnErrorDiagnostic = false;
|
|
|
|
var binFolder = convertPath(join(projectFolderPath, 'bin'));
|
|
var binMainFilePath = convertPath(join(binFolder, 'main.dart'));
|
|
var binMainFileUri = pathContext.toUri(binMainFilePath);
|
|
var fooFilePath = convertPath(join(binFolder, 'foo.dart'));
|
|
var fooUri = pathContext.toUri(fooFilePath);
|
|
|
|
const binMainContent = '''
|
|
import 'foo.dart';
|
|
|
|
Foo? f;
|
|
''';
|
|
const fooContent = '''
|
|
class Foo {}
|
|
''';
|
|
|
|
newFolder(binFolder);
|
|
newFile(binMainFilePath, binMainContent);
|
|
|
|
await initialize();
|
|
await initialAnalysis;
|
|
|
|
// Expect diagnostics because 'foo.dart' doesn't exist.
|
|
expect(diagnostics[binMainFileUri], isNotEmpty);
|
|
|
|
// Create the file and _immediately_ open it, so the file exists when the
|
|
// overlay is created, even though the watcher event has not been processed.
|
|
newFile(fooFilePath, fooContent);
|
|
await Future.wait([
|
|
openFile(fooUri, fooContent),
|
|
waitForAnalysisComplete(),
|
|
]);
|
|
|
|
// Expect the diagnostics have gone.
|
|
expect(diagnostics[binMainFileUri], isEmpty);
|
|
}
|
|
|
|
Future<void> test_documentOpen_setsPriorityFileIfEarly() async {
|
|
setConfigurationSupport();
|
|
|
|
// When initializing with config support, the server will call back to the client
|
|
// which can delay analysis roots being configured. This can result in files
|
|
// being opened before analysis roots are set which has previously caused the
|
|
// files not to be marked as priority on the created drivers.
|
|
// https://github.com/Dart-Code/Dart-Code/issues/2438
|
|
// https://github.com/dart-lang/sdk/issues/42994
|
|
|
|
// Initialize the server, but delay providing the configuration until after
|
|
// we've opened the file.
|
|
var completer = Completer<void>();
|
|
|
|
// Send the initialize request but do not await it.
|
|
var initResponse = initialize();
|
|
|
|
// When asked for config, delay the response until we have sent the openFile notification.
|
|
var config = provideConfig(
|
|
() => initResponse,
|
|
completer.future.then((_) => {'dart.foo': false}),
|
|
);
|
|
|
|
// Wait for initialization to finish, open the file, then allow config to complete.
|
|
await initResponse;
|
|
await openFile(mainFileUri, content);
|
|
completer.complete();
|
|
await config;
|
|
await pumpEventQueue(times: 5000);
|
|
|
|
// Ensure the opened file is in the priority list.
|
|
expect(server.getAnalysisDriver(mainFilePath)!.priorityFiles,
|
|
equals([mainFilePath]));
|
|
}
|
|
|
|
Future<void> _initializeAndOpen() async {
|
|
await initialize();
|
|
await openFile(mainFileUri, content);
|
|
}
|
|
}
|