Files
sdk/pkg/analysis_server/test/lsp/document_changes_test.dart
T
Danny Tuppeny 4f0dc3bfd9 [analysis_server] Ensure textDocument/didClose removes overlays synchronously to avoid races when files are closed/opened quickly
Plugin re-registration could cause VS Code to send us a close/open request very close together and this `await` before removing the overlay could result in them being processed in the wrong order. This resulted in subsequent file updates failing with "Unable to edit document because the file was not previously opened"

Fixes https://github.com/Dart-Code/Dart-Code/issues/4862
See also https://github.com/Dart-Code/Dart-Code/issues/4740

Change-Id: If6beeebdd79914e1b26553b81f4fb38f000cbaf7
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/338660
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Samuel Rawlins <srawlins@google.com>
2023-11-28 17:52:57 +00:00

366 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',
)),
]);
final 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.
final fileInsideRootPath = mainFilePath;
final 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
final driverForInside = server.getAnalysisDriver(fileInsideRootPath)!;
final 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 {
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[mainFilePath], 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[mainFilePath], 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 {
const content = 'error';
newFile(mainFilePath, content);
// Expect diagnostics after initial analysis because file has invalid
// content.
await initialize();
await pumpEventQueue(times: 5000);
expect(diagnostics[mainFilePath], isNotEmpty);
// Expect diagnostics after opening the file with the same contents.
await openFile(mainFileUri, content);
await pumpEventQueue(times: 5000);
expect(diagnostics[mainFilePath], isNotEmpty);
// Expect diagnostics after deleting the file because the overlay is still
// active.
deleteFile(mainFilePath);
await pumpEventQueue(times: 5000);
expect(diagnostics[mainFilePath], isNotEmpty);
// Expect diagnostics to be removed after we close the file (which removes
// the overlay).
await closeFile(mainFileUri);
await pumpEventQueue(times: 5000);
expect(diagnostics[mainFilePath], 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 {
const content = 'error';
newFile(mainFilePath, content);
// Expect diagnostics after initial analysis because file has invalid
// content.
await initialize();
await pumpEventQueue(times: 5000);
expect(diagnostics[mainFilePath], isNotEmpty);
// Expect diagnostics after opening the file with the same contents.
await openFile(mainFileUri, content);
await pumpEventQueue(times: 5000);
expect(diagnostics[mainFilePath], isNotEmpty);
// Expect diagnostics after deleting the file because the overlay is still
// active.
deleteFile(mainFilePath);
await pumpEventQueue(times: 5000);
expect(diagnostics[mainFilePath], isNotEmpty);
// Expect diagnostics remain after re-creating the file (the overlay is still
// active).
newFile(mainFilePath, content);
await pumpEventQueue(times: 5000);
expect(diagnostics[mainFilePath], 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[mainFilePath], isNotEmpty);
// Finally, expect deleteing the file clears the diagnostics.
deleteFile(mainFilePath);
await pumpEventQueue(times: 5000);
expect(diagnostics[mainFilePath], 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 {
final binFolder = convertPath(join(projectFolderPath, 'bin'));
final binMainFilePath = convertPath(join(binFolder, 'main.dart'));
final fooFilePath = convertPath(join(binFolder, 'foo.dart'));
final 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[binMainFilePath], 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[binMainFilePath], 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.
final completer = Completer<void>();
// Send the initialize request but do not await it.
final initResponse = initialize();
// When asked for config, delay the response until we have sent the openFile notification.
final 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);
}
}