| // 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: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 { | |
| await _initializeAndOpen(); | |
| await changeFile(2, mainFileUri, [ | |
| TextDocumentContentChangeEvent.t1( | |
| TextDocumentContentChangePartial( | |
| range: Range( | |
| start: Position(line: 0, character: 6), | |
| end: Position(line: 0, character: 9), | |
| ), | |
| text: 'Bar', | |
| ), | |
| ), | |
| TextDocumentContentChangeEvent.t1( | |
| TextDocumentContentChangePartial( | |
| 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( | |
| SourceEdit.applySequence(content, notifiedChanges.edits), | |
| contentAfterUpdate, | |
| ); | |
| } | |
| Future<void> test_documentChange_updatesOverlay() async { | |
| await _initializeAndOpen(); | |
| await changeFile(2, mainFileUri, [ | |
| TextDocumentContentChangeEvent.t1( | |
| TextDocumentContentChangePartial( | |
| range: Range( | |
| start: Position(line: 0, character: 6), | |
| end: Position(line: 0, character: 9), | |
| ), | |
| text: 'Bar', | |
| ), | |
| ), | |
| TextDocumentContentChangeEvent.t1( | |
| TextDocumentContentChangePartial( | |
| 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 { | |
| 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 workspaceAnalysisComplete(); | |
| await openFile(mainFileUri, newContent); | |
| await workspaceAnalysisComplete(); | |
| // 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 workspaceAnalysisComplete(); | |
| await openFile(mainFileUri, content); | |
| await workspaceAnalysisComplete(); | |
| // 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), | |
| ]); | |
| await workspaceAnalysisComplete(); | |
| 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 workspaceAnalysisComplete(); | |
| expect(diagnostics[mainFileUri], isNotEmpty); | |
| // Expect diagnostics after deleting the file because the overlay is still | |
| // active. | |
| deleteFile(mainFilePath); | |
| await workspaceAnalysisComplete(); | |
| expect(diagnostics[mainFileUri], isNotEmpty); | |
| // Expect diagnostics to be removed after we close the file (which removes | |
| // the overlay). | |
| await closeFile(mainFileUri); | |
| await workspaceAnalysisComplete(); | |
| 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 workspaceAnalysisComplete(); | |
| expect(diagnostics[mainFileUri], isNotEmpty); | |
| // Expect diagnostics after deleting the file because the overlay is still | |
| // active. | |
| deleteFile(mainFilePath); | |
| await workspaceAnalysisComplete(); | |
| expect(diagnostics[mainFileUri], isNotEmpty); | |
| // Expect diagnostics remain after re-creating the file (the overlay is still | |
| // active). | |
| newFile(mainFilePath, content); | |
| await workspaceAnalysisComplete(); | |
| 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 { | |
| 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 workspaceAnalysisComplete(); | |
| // 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 openFile(fooUri, fooContent); | |
| await workspaceAnalysisComplete(); | |
| // 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); | |
| } | |
| } |