| // Copyright (c) 2017, 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/protocol/protocol.dart'; |
| import 'package:analysis_server/src/channel/channel.dart'; |
| import 'package:analysis_server/src/plugin/plugin_manager.dart'; |
| import 'package:analyzer/dart/analysis/context_root.dart' as analyzer; |
| import 'package:analyzer_plugin/protocol/protocol.dart' as plugin; |
| import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin; |
| import 'package:analyzer_plugin/src/protocol/protocol_internal.dart' as plugin; |
| import 'package:matcher/expect.dart'; |
| import 'package:watcher/watcher.dart'; |
| |
| /// A mock [ServerCommunicationChannel] for testing [AnalysisServer]. |
| class MockServerChannel implements ServerCommunicationChannel { |
| StreamController<RequestOrResponse> requestController = |
| StreamController<RequestOrResponse>(); |
| StreamController<Response> responseController = |
| StreamController<Response>.broadcast(); |
| StreamController<Notification> notificationController = |
| StreamController<Notification>.broadcast(sync: true); |
| Completer<Response>? errorCompleter; |
| |
| List<Response> responsesReceived = []; |
| List<Notification> notificationsReceived = []; |
| List<Request> serverRequestsSent = []; |
| |
| bool _closed = false; |
| |
| String? name; |
| |
| MockServerChannel(); |
| |
| /// Return the broadcast stream of notifications. |
| Stream<Notification> get notifications { |
| return notificationController.stream; |
| } |
| |
| @override |
| Stream<RequestOrResponse> get requests => requestController.stream; |
| |
| @override |
| void close() { |
| _closed = true; |
| } |
| |
| void expectMsgCount({responseCount = 0, notificationCount = 0}) { |
| expect(responsesReceived, hasLength(responseCount)); |
| expect(notificationsReceived, hasLength(notificationCount)); |
| } |
| |
| @override |
| void sendNotification(Notification notification) { |
| // Don't deliver notifications after the connection is closed. |
| if (_closed) { |
| return; |
| } |
| notificationsReceived.add(notification); |
| var errorCompleter = this.errorCompleter; |
| if (errorCompleter != null && notification.event == 'server.error') { |
| var params = notification.params!; |
| print('[server.error] test: $name message: ${params['message']}'); |
| errorCompleter.completeError( |
| ServerError(params['message'] as String), |
| StackTrace.fromString(params['stackTrace'] as String), |
| ); |
| } |
| // Wrap send notification in future to simulate websocket |
| // TODO(scheglov): ask Dan why and decide what to do |
| // new Future(() => notificationController.add(notification)); |
| notificationController.add(notification); |
| } |
| |
| @override |
| void sendRequest(Request request) { |
| serverRequestsSent.add(request); |
| } |
| |
| @override |
| void sendResponse(Response response) { |
| // Don't deliver responses after the connection is closed. |
| if (_closed) { |
| return; |
| } |
| responsesReceived.add(response); |
| // Wrap send response in future to simulate WebSocket. |
| Future(() => responseController.add(response)); |
| } |
| |
| /// Send the given [request] to the server as if it had been sent from the |
| /// client, 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<Response> simulateRequestFromClient(Request request) { |
| if (_closed) { |
| throw Exception('simulateRequestFromClient after connection closed'); |
| } |
| // Wrap send request in future to simulate WebSocket. |
| Future(() => requestController.add(request)); |
| return waitForResponse(request); |
| } |
| |
| /// Send the given [response] to the server as if it had been sent from the |
| /// client. |
| Future<void> simulateResponseFromClient(Response response) { |
| // No further requests should be sent after the connection is closed. |
| if (_closed) { |
| throw Exception('simulateRequestFromClient after connection closed'); |
| } |
| // Wrap send request in future to simulate WebSocket. |
| return Future(() => requestController.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. |
| /// |
| /// Unlike [simulateRequestFromClient], this method assumes that the [request] |
| /// has already been sent to the server. |
| Future<Response> waitForResponse(Request request) { |
| var id = request.id; |
| return responseController.stream.firstWhere( |
| (response) => response.id == id, |
| ); |
| } |
| } |
| |
| class ServerError implements Exception { |
| final String message; |
| |
| ServerError(this.message); |
| |
| @override |
| String toString() { |
| return 'Server Error: $message'; |
| } |
| } |
| |
| /// A plugin manager that simulates broadcasting requests to plugins by |
| /// hard-coding the responses. |
| class TestPluginManager implements PluginManager { |
| plugin.AnalysisSetPriorityFilesParams? analysisSetPriorityFilesParams; |
| plugin.AnalysisSetSubscriptionsParams? analysisSetSubscriptionsParams; |
| plugin.AnalysisUpdateContentParams? analysisUpdateContentParams; |
| plugin.RequestParams? broadcastedRequest; |
| Map<PluginInfo, Future<plugin.Response>>? broadcastResults; |
| Map<PluginInfo, Future<plugin.Response>>? Function(plugin.RequestParams)? |
| handleRequest; |
| Map<analyzer.ContextRoot, List<String>> contextRootPlugins = {}; |
| |
| @override |
| List<PluginInfo> plugins = []; |
| |
| StreamController<void> pluginsChangedController = |
| StreamController.broadcast(); |
| |
| @override |
| Stream<void> get pluginsChanged => pluginsChangedController.stream; |
| |
| @override |
| Future<void> addPluginToContextRoot( |
| analyzer.ContextRoot contextRoot, |
| String path, { |
| required bool isLegacyPlugin, |
| }) async { |
| contextRootPlugins.putIfAbsent(contextRoot, () => []).add(path); |
| } |
| |
| @override |
| Map<PluginInfo, Future<plugin.Response>> broadcastRequest( |
| plugin.RequestParams params, { |
| analyzer.ContextRoot? contextRoot, |
| }) { |
| broadcastedRequest = params; |
| return handleRequest?.call(params) ?? broadcastResults ?? {}; |
| } |
| |
| @override |
| Future<List<Future<plugin.Response>>> broadcastWatchEvent( |
| WatchEvent watchEvent, |
| ) async { |
| return []; |
| } |
| |
| @override |
| dynamic noSuchMethod(Invocation invocation) => |
| fail('Unexpected invocation of ${invocation.memberName}'); |
| |
| @override |
| void removedContextRoot(analyzer.ContextRoot contextRoot) { |
| contextRootPlugins.remove(contextRoot); |
| } |
| |
| @override |
| Future<void> restartPlugins() async { |
| // Nothing to restart. |
| } |
| |
| @override |
| void setAnalysisSetPriorityFilesParams( |
| plugin.AnalysisSetPriorityFilesParams params, |
| ) { |
| analysisSetPriorityFilesParams = params; |
| } |
| |
| @override |
| void setAnalysisSetSubscriptionsParams( |
| plugin.AnalysisSetSubscriptionsParams params, |
| ) { |
| analysisSetSubscriptionsParams = params; |
| } |
| |
| @override |
| void setAnalysisUpdateContentParams( |
| plugin.AnalysisUpdateContentParams params, |
| ) { |
| analysisUpdateContentParams = params; |
| } |
| } |