| // Copyright (c) 2014, 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 'dart:convert'; |
| |
| import 'package:analysis_server/lsp_protocol/protocol_generated.dart' as lsp; |
| import 'package:analysis_server/lsp_protocol/protocol_generated.dart'; |
| import 'package:analysis_server/lsp_protocol/protocol_special.dart' as lsp; |
| import 'package:analysis_server/src/lsp/channel/lsp_channel.dart'; |
| |
| const _jsonEncoder = JsonEncoder.withIndent(' '); |
| |
| /// A mock [LspServerCommunicationChannel] for testing [LspAnalysisServer]. |
| class MockLspServerChannel implements LspServerCommunicationChannel { |
| final StreamController<lsp.Message> _clientToServer = |
| StreamController<lsp.Message>.broadcast(); |
| final StreamController<lsp.Message> _serverToClient = |
| StreamController<lsp.Message>.broadcast(); |
| |
| /// Completer that will be signalled when the input stream is closed. |
| final Completer _closed = Completer(); |
| |
| /// Errors popups sent to the user. |
| final shownErrors = <lsp.ShowMessageParams>[]; |
| |
| /// Warning popups sent to the user. |
| final shownWarnings = <lsp.ShowMessageParams>[]; |
| |
| MockLspServerChannel(bool printMessages) { |
| if (printMessages) { |
| _serverToClient.stream |
| .listen((message) => print('<== ${jsonEncode(message)}')); |
| _clientToServer.stream |
| .listen((message) => print('==> ${jsonEncode(message)}')); |
| } |
| |
| // Keep track of any errors/warnings that are sent to the user with |
| // `window/showMessage`. |
| _serverToClient.stream.listen((message) { |
| if (message is lsp.NotificationMessage && |
| message.method == Method.window_showMessage) { |
| final params = message.params; |
| if (params is lsp.ShowMessageParams) { |
| if (params.type == MessageType.Error) { |
| shownErrors.add(params); |
| } else if (params.type == MessageType.Warning) { |
| shownWarnings.add(params); |
| } |
| } |
| } |
| }); |
| } |
| |
| /// Future that will be completed when the input stream is closed. |
| @override |
| Future get closed { |
| return _closed.future; |
| } |
| |
| Stream<lsp.Message> get serverToClient => _serverToClient.stream; |
| |
| @override |
| void close() { |
| if (!_closed.isCompleted) { |
| _closed.complete(); |
| } |
| if (!_serverToClient.isClosed) { |
| _serverToClient.close(); |
| } |
| if (!_clientToServer.isClosed) { |
| _clientToServer.close(); |
| } |
| } |
| |
| @override |
| StreamSubscription<void> listen(void Function(lsp.Message message) onMessage, |
| {Function? onError, void Function()? onDone}) { |
| return _clientToServer.stream |
| .listen(onMessage, onError: onError, onDone: onDone); |
| } |
| |
| @override |
| void sendNotification(lsp.NotificationMessage notification) { |
| // Don't deliver notifications after the connection is closed. |
| if (_closed.isCompleted) { |
| return; |
| } |
| |
| notification = _convertJson(notification, lsp.NotificationMessage.fromJson); |
| |
| _serverToClient.add(notification); |
| } |
| |
| void sendNotificationToServer(lsp.NotificationMessage notification) { |
| // Don't deliver notifications after the connection is closed. |
| if (_closed.isCompleted) { |
| return; |
| } |
| |
| notification = _convertJson(notification, lsp.NotificationMessage.fromJson); |
| |
| // Wrap send request in future to simulate WebSocket. |
| Future(() => _clientToServer.add(notification)); |
| } |
| |
| @override |
| void sendRequest(lsp.RequestMessage request) { |
| // Don't deliver notifications after the connection is closed. |
| if (_closed.isCompleted) { |
| return; |
| } |
| |
| request = _convertJson(request, lsp.RequestMessage.fromJson); |
| |
| _serverToClient.add(request); |
| } |
| |
| /// Send the given [request] to the server and return a future that will |
| /// complete when a response associated with the [request] has been received. |
| /// The value of the future will be the received response. |
| Future<lsp.ResponseMessage> sendRequestToServer(lsp.RequestMessage request) { |
| // No further requests should be sent after the connection is closed. |
| if (_closed.isCompleted) { |
| throw Exception('${request.method} request sent after connection closed'); |
| } |
| |
| request = _convertJson(request, lsp.RequestMessage.fromJson); |
| |
| // Wrap send request in future to simulate WebSocket. |
| Future(() => _clientToServer.add(request)); |
| return waitForResponse(request); |
| } |
| |
| @override |
| void sendResponse(lsp.ResponseMessage response) { |
| // Don't deliver responses after the connection is closed. |
| if (_closed.isCompleted) { |
| return; |
| } |
| |
| response = _convertJson(response, lsp.ResponseMessage.fromJson); |
| |
| // Wrap send response in future to simulate WebSocket. |
| Future(() => _serverToClient.add(response)); |
| } |
| |
| void sendResponseToServer(lsp.ResponseMessage response) { |
| // Don't deliver notifications after the connection is closed. |
| if (_closed.isCompleted) { |
| return; |
| } |
| |
| response = _convertJson(response, lsp.ResponseMessage.fromJson); |
| |
| _clientToServer.add(response); |
| } |
| |
| /// Return a future that will complete when a response associated with the |
| /// given [request] has been received. The value of the future will be the |
| /// received response. The returned future will throw an exception if a server |
| /// error is reported before the response has been received. |
| /// |
| /// Unlike [sendLspRequest], this method assumes that the [request] has |
| /// already been sent to the server. |
| Future<lsp.ResponseMessage> waitForResponse( |
| lsp.RequestMessage request, { |
| bool throwOnError = true, |
| }) async { |
| final response = await _serverToClient.stream.firstWhere((message) => |
| (message is lsp.ResponseMessage && message.id == request.id) || |
| (throwOnError && |
| message is lsp.NotificationMessage && |
| message.method == Method.window_showMessage && |
| lsp.ShowMessageParams.fromJson( |
| message.params as Map<String, Object?>) |
| .type == |
| MessageType.Error)); |
| |
| if (response is lsp.ResponseMessage) { |
| return response; |
| } else { |
| throw 'An error occurred while waiting for a response to ${request.method}: ' |
| '${_jsonEncoder.convert(response.toJson())}'; |
| } |
| } |
| |
| /// Round trips the object to JSON and back to ensure it behaves the same as |
| /// when running over the real STDIO server. Without this, the object passed |
| /// to the handlers will have concrete types as constructed in tests rather |
| /// than the maps as they would be (the server expects to do the conversion). |
| T _convertJson<T>( |
| lsp.ToJsonable message, T Function(Map<String, dynamic>) constructor) { |
| return constructor( |
| jsonDecode(jsonEncode(message.toJson())) as Map<String, Object?>); |
| } |
| } |