| // Copyright (c) 2024, 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/protocol/protocol_generated.dart'; | |
| import 'package:analysis_server/src/lsp/constants.dart'; | |
| import 'package:analysis_server/src/lsp/handlers/commands/perform_refactor.dart'; | |
| import 'package:analysis_server/src/scheduler/message_scheduler.dart'; | |
| import 'package:analyzer/src/test_utilities/test_code_format.dart'; | |
| import 'package:language_server_protocol/protocol_generated.dart'; | |
| import 'package:test/test.dart'; | |
| import 'package:test_reflective_loader/test_reflective_loader.dart'; | |
| import '../../test/analysis_server_base.dart'; | |
| import '../../test/lsp/code_actions_refactor_test.dart'; | |
| import '../../test/utils/message_scheduler_test_view.dart'; | |
| import '../../test/utils/test_code_extensions.dart'; | |
| void main() { | |
| defineReflectiveSuite(() { | |
| defineReflectiveTests(LspServerMessageSchedulerTest); | |
| defineReflectiveTests(LegacyServerMessageSchedulerTest); | |
| }); | |
| } | |
| void _assertLogContents(MessageSchedulerTestView testView, String expected) { | |
| var actual = _getLogContents(testView.messageLog); | |
| if (actual != expected) { | |
| print('-------- Actual --------'); | |
| print('$actual------------------------'); | |
| } | |
| expect(actual, expected); | |
| } | |
| String _getLogContents(List<String> log) { | |
| var buffer = StringBuffer(); | |
| for (var event in log) { | |
| buffer.writeln(event); | |
| } | |
| return buffer.toString(); | |
| } | |
| @reflectiveTest | |
| class LegacyServerMessageSchedulerTest extends PubPackageAnalysisServerTest { | |
| late MessageScheduler messageScheduler; | |
| @override | |
| bool get retainDataForTesting => true; | |
| @override | |
| Future<void> setUp() async { | |
| super.setUp(); | |
| messageScheduler = server.messageScheduler; | |
| } | |
| Future<void> test_initialize() async { | |
| await setRoots(included: [workspaceRootPath], excluded: []); | |
| await waitForTasksFinished(); | |
| _assertLogContents(testView!, r''' | |
| Incoming LegacyMessage: legacy:analysis.setAnalysisRoots | |
| Entering process messages loop | |
| Start LegacyMessage: legacy:analysis.setAnalysisRoots | |
| Exit process messages loop | |
| Complete LegacyMessage: legacy:analysis.setAnalysisRoots | |
| '''); | |
| } | |
| Future<void> test_multipleRequests() async { | |
| if (MessageScheduler.allowOverlappingHandlers) return; | |
| var futures = <Future<void>>[]; | |
| futures.add(setRoots(included: [workspaceRootPath], excluded: [])); | |
| var request = ExecutionCreateContextParams('/a/b.dart') | |
| .toRequest('0', clientUriConverter: server.uriConverter); | |
| futures.add(handleSuccessfulRequest(request)); | |
| await Future.wait(futures); | |
| await waitForTasksFinished(); | |
| _assertLogContents(testView!, r''' | |
| Incoming LegacyMessage: legacy:analysis.setAnalysisRoots | |
| Entering process messages loop | |
| Start LegacyMessage: legacy:analysis.setAnalysisRoots | |
| Incoming LegacyMessage: legacy:execution.createContext | |
| Complete LegacyMessage: legacy:analysis.setAnalysisRoots | |
| Start LegacyMessage: legacy:execution.createContext | |
| Complete LegacyMessage: legacy:execution.createContext | |
| Exit process messages loop | |
| '''); | |
| } | |
| } | |
| @reflectiveTest | |
| class LspServerMessageSchedulerTest extends RefactorCodeActionsTest { | |
| late MessageScheduler messageScheduler; | |
| final extractMethodTitle = 'Extract Method'; | |
| @override | |
| bool get retainDataForTesting => true; | |
| @override | |
| void setUp() { | |
| super.setUp(); | |
| messageScheduler = server.messageScheduler; | |
| } | |
| Future<void> test_documentChange() async { | |
| if (MessageScheduler.allowOverlappingHandlers) return; | |
| const content = ''' | |
| void f() { | |
| print('Test!'); | |
| [!print('Test!');!] | |
| } | |
| '''; | |
| newFile(mainFilePath, content); | |
| var codeAction = await expectCodeActionLiteral( | |
| content, | |
| command: Commands.performRefactor, | |
| title: extractMethodTitle, | |
| openTargetFile: true, | |
| ); | |
| // Use a Completer to control when the refactor handler starts computing. | |
| var completer = Completer<void>(); | |
| PerformRefactorCommandHandler.delayAfterResolveForTests = completer.future; | |
| try { | |
| // Send an edit request immediately after the refactor request. | |
| var futures = <Future<void>>[]; | |
| var request = makeRequest( | |
| Method.workspace_executeCommand, | |
| ExecuteCommandParams( | |
| command: codeAction.command!.command, | |
| arguments: codeAction.command!.arguments, | |
| ), | |
| ); | |
| futures.add(sendRequestToServer(request)); | |
| futures.add(replaceFile(100, mainFileUri, 'new test content')); | |
| completer.complete(); | |
| await Future.wait(futures); | |
| } finally { | |
| // Ensure we never leave an incomplete future if anything above throws. | |
| PerformRefactorCommandHandler.delayAfterResolveForTests = null; | |
| } | |
| _assertLogContents(testView!, r''' | |
| Incoming RequestMessage: lsp:initialize | |
| Entering process messages loop | |
| Start LspMessage: lsp:initialize | |
| Complete LspMessage: lsp:initialize | |
| Exit process messages loop | |
| Incoming NotificationMessage: lsp:initialized | |
| Entering process messages loop | |
| Start LspMessage: lsp:initialized | |
| Complete LspMessage: lsp:initialized | |
| Exit process messages loop | |
| Incoming NotificationMessage: lsp:textDocument/didOpen | |
| Entering process messages loop | |
| Start LspMessage: lsp:textDocument/didOpen | |
| Complete LspMessage: lsp:textDocument/didOpen | |
| Exit process messages loop | |
| Incoming RequestMessage: lsp:textDocument/codeAction | |
| Entering process messages loop | |
| Start LspMessage: lsp:textDocument/codeAction | |
| Complete LspMessage: lsp:textDocument/codeAction | |
| Exit process messages loop | |
| Incoming RequestMessage: lsp:workspace/executeCommand | |
| Entering process messages loop | |
| Start LspMessage: lsp:workspace/executeCommand | |
| Incoming NotificationMessage: lsp:textDocument/didChange | |
| Canceled in progress request lsp:workspace/executeCommand | |
| Complete LspMessage: lsp:workspace/executeCommand | |
| Start LspMessage: lsp:textDocument/didChange | |
| Complete LspMessage: lsp:textDocument/didChange | |
| Exit process messages loop | |
| '''); | |
| } | |
| Future<void> test_duplicateRequests() async { | |
| if (MessageScheduler.allowOverlappingHandlers) return; | |
| const content = ''' | |
| class B { | |
| @^ | |
| } | |
| '''; | |
| newFile(mainFilePath, content); | |
| await initialize(); | |
| var code = TestCode.parse(content); | |
| await openFile(mainFileUri, code.code); | |
| var request = makeRequest( | |
| Method.textDocument_completion, | |
| CompletionParams( | |
| textDocument: TextDocumentIdentifier(uri: mainFileUri), | |
| position: code.position.position, | |
| ), | |
| ); | |
| var futures = <Future<void>>[]; | |
| futures.add(sendRequestToServer(request)); | |
| futures.add(sendRequestToServer(request)); | |
| futures.add(sendRequestToServer(request)); | |
| await Future.wait(futures); | |
| await pumpEventQueue(times: 5000); | |
| _assertLogContents(testView!, r''' | |
| Incoming RequestMessage: lsp:initialize | |
| Entering process messages loop | |
| Start LspMessage: lsp:initialize | |
| Complete LspMessage: lsp:initialize | |
| Exit process messages loop | |
| Incoming NotificationMessage: lsp:initialized | |
| Entering process messages loop | |
| Start LspMessage: lsp:initialized | |
| Complete LspMessage: lsp:initialized | |
| Exit process messages loop | |
| Incoming NotificationMessage: lsp:textDocument/didOpen | |
| Entering process messages loop | |
| Start LspMessage: lsp:textDocument/didOpen | |
| Complete LspMessage: lsp:textDocument/didOpen | |
| Exit process messages loop | |
| Incoming RequestMessage: lsp:textDocument/completion | |
| Entering process messages loop | |
| Start LspMessage: lsp:textDocument/completion | |
| Incoming RequestMessage: lsp:textDocument/completion | |
| Canceled in progress request lsp:textDocument/completion | |
| Incoming RequestMessage: lsp:textDocument/completion | |
| Canceled in progress request lsp:textDocument/completion | |
| Canceled request on queue lsp:textDocument/completion | |
| Complete LspMessage: lsp:textDocument/completion | |
| Start LspMessage: lsp:textDocument/completion | |
| Complete LspMessage: lsp:textDocument/completion | |
| Start LspMessage: lsp:textDocument/completion | |
| Complete LspMessage: lsp:textDocument/completion | |
| Exit process messages loop | |
| '''); | |
| } | |
| Future<void> test_initialize() async { | |
| await initialize(); | |
| await workspaceAnalysisComplete(); | |
| _assertLogContents(testView!, r''' | |
| Incoming RequestMessage: initialize:0 | |
| Entering process messages loop | |
| Start LspMessage: initialize:0 | |
| Exit process messages loop | |
| Complete LspMessage: initialize:0 | |
| Incoming NotificationMessage: initialized | |
| Entering process messages loop | |
| Start LspMessage: initialized | |
| Exit process messages loop | |
| Complete LspMessage: initialized | |
| Incoming RequestMessage: dart/workspace/analysis/complete:1 | |
| Entering process messages loop | |
| Start LspMessage: dart/workspace/analysis/complete:1 | |
| Exit process messages loop | |
| '''); | |
| } | |
| Future<void> test_multipleRequests() async { | |
| if (MessageScheduler.allowOverlappingHandlers) return; | |
| const content = ''' | |
| void main() { | |
| print('Hello world!!'); | |
| } | |
| '''; | |
| newFile(mainFilePath, content); | |
| await initialize(); | |
| var futures = <Future<void>>[]; | |
| futures.add(getDocumentSymbols(mainFileUri)); | |
| futures.add(getDocumentLinks(mainFileUri)); | |
| await Future.wait(futures); | |
| await pumpEventQueue(times: 5000); | |
| _assertLogContents(testView!, r''' | |
| Incoming RequestMessage: lsp:initialize | |
| Entering process messages loop | |
| Start LspMessage: lsp:initialize | |
| Complete LspMessage: lsp:initialize | |
| Exit process messages loop | |
| Incoming NotificationMessage: lsp:initialized | |
| Entering process messages loop | |
| Start LspMessage: lsp:initialized | |
| Complete LspMessage: lsp:initialized | |
| Exit process messages loop | |
| Incoming RequestMessage: lsp:textDocument/documentSymbol | |
| Entering process messages loop | |
| Start LspMessage: lsp:textDocument/documentSymbol | |
| Incoming RequestMessage: lsp:textDocument/documentLink | |
| Complete LspMessage: lsp:textDocument/documentSymbol | |
| Start LspMessage: lsp:textDocument/documentLink | |
| Complete LspMessage: lsp:textDocument/documentLink | |
| Exit process messages loop | |
| '''); | |
| } | |
| Future<void> test_pauseResume() async { | |
| // Content isn't important, we just need a valid file to send requests for. | |
| const content = ''; | |
| newFile(mainFilePath, content); | |
| await initialize(); | |
| var futures = <Future<void>>[]; | |
| /// Helper to send two hover requests and pump the event queue, but not wait | |
| /// for the requests to complete. | |
| Future<void> sendHovers() async { | |
| futures.add(getHover(mainFileUri, Position(line: 0, character: 0))); | |
| futures.add(getHover(mainFileUri, Position(line: 0, character: 0))); | |
| await pumpEventQueue(times: 5000); | |
| } | |
| /// Helper to resume the scheduler and pump the event queue to allow time | |
| /// for processing to ensure the logs are in a consistent order. | |
| Future<void> resume() async { | |
| messageScheduler.resume(); | |
| await pumpEventQueue(times: 5000); | |
| } | |
| await sendHovers(); | |
| messageScheduler.pause(); // Pause 1 | |
| await sendHovers(); | |
| messageScheduler.pause(); // Pause 2 | |
| await sendHovers(); | |
| await resume(); // Resume 1 | |
| await sendHovers(); | |
| await resume(); // Resume 2 | |
| await Future.wait(futures); | |
| _assertLogContents(testView!, r''' | |
| Incoming RequestMessage: initialize:0 | |
| Entering process messages loop | |
| Start LspMessage: initialize:0 | |
| Exit process messages loop | |
| Complete LspMessage: initialize:0 | |
| Incoming NotificationMessage: initialized | |
| Entering process messages loop | |
| Start LspMessage: initialized | |
| Exit process messages loop | |
| Complete LspMessage: initialized | |
| Incoming RequestMessage: textDocument/hover:1 | |
| Incoming RequestMessage: textDocument/hover:2 | |
| Entering process messages loop | |
| Start LspMessage: textDocument/hover:1 | |
| Start LspMessage: textDocument/hover:2 | |
| Exit process messages loop | |
| Complete LspMessage: textDocument/hover:1 | |
| Complete LspMessage: textDocument/hover:2 | |
| Pause requested - there are now 1 pauses | |
| Incoming RequestMessage: textDocument/hover:3 | |
| Incoming RequestMessage: textDocument/hover:4 | |
| Entering process messages loop | |
| Exit process messages loop | |
| Pause requested - there are now 2 pauses | |
| Incoming RequestMessage: textDocument/hover:5 | |
| Incoming RequestMessage: textDocument/hover:6 | |
| Entering process messages loop | |
| Exit process messages loop | |
| Resume requested - there are now 1 pauses | |
| Incoming RequestMessage: textDocument/hover:7 | |
| Incoming RequestMessage: textDocument/hover:8 | |
| Entering process messages loop | |
| Exit process messages loop | |
| Resume requested - there are now 0 pauses | |
| Entering process messages loop | |
| Start LspMessage: textDocument/hover:3 | |
| Start LspMessage: textDocument/hover:4 | |
| Start LspMessage: textDocument/hover:5 | |
| Start LspMessage: textDocument/hover:6 | |
| Start LspMessage: textDocument/hover:7 | |
| Start LspMessage: textDocument/hover:8 | |
| Exit process messages loop | |
| Complete LspMessage: textDocument/hover:3 | |
| Complete LspMessage: textDocument/hover:4 | |
| Complete LspMessage: textDocument/hover:5 | |
| Complete LspMessage: textDocument/hover:6 | |
| Complete LspMessage: textDocument/hover:7 | |
| Complete LspMessage: textDocument/hover:8 | |
| '''); | |
| } | |
| Future<void> test_response() async { | |
| if (MessageScheduler.allowOverlappingHandlers) return; | |
| const content = ''' | |
| void f() { | |
| print('Test!'); | |
| [!print('Test!');!] | |
| } | |
| '''; | |
| var codeAction = await expectCodeActionLiteral( | |
| content, | |
| command: Commands.performRefactor, | |
| title: extractMethodTitle, | |
| ); | |
| // Respond to any applyEdit requests from the server with successful responses | |
| // and capturing the last edit. | |
| requestsFromServer.listen((request) { | |
| if (request.method == Method.workspace_applyEdit) { | |
| ApplyWorkspaceEditParams.fromJson( | |
| request.params as Map<String, Object?>, | |
| ); | |
| respondTo(request, ApplyWorkspaceEditResult(applied: true)); | |
| } | |
| }); | |
| await executeCommand(codeAction.command!); | |
| await pumpEventQueue(times: 5000); | |
| _assertLogContents(testView!, r''' | |
| Incoming RequestMessage: lsp:initialize | |
| Entering process messages loop | |
| Start LspMessage: lsp:initialize | |
| Complete LspMessage: lsp:initialize | |
| Exit process messages loop | |
| Incoming NotificationMessage: lsp:initialized | |
| Entering process messages loop | |
| Start LspMessage: lsp:initialized | |
| Complete LspMessage: lsp:initialized | |
| Exit process messages loop | |
| Incoming RequestMessage: lsp:textDocument/codeAction | |
| Entering process messages loop | |
| Start LspMessage: lsp:textDocument/codeAction | |
| Complete LspMessage: lsp:textDocument/codeAction | |
| Exit process messages loop | |
| Incoming RequestMessage: lsp:workspace/executeCommand | |
| Entering process messages loop | |
| Start LspMessage: lsp:workspace/executeCommand | |
| Incoming ResponseMessage: ResponseMessage | |
| Complete LspMessage: lsp:workspace/executeCommand | |
| Exit process messages loop | |
| '''); | |
| } | |
| } |