| // 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); |
| } |
| } |