blob: 67180d3042e45a490953b56aeb9ada52812699f0 [file] [log] [blame]
// 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.
/// @docImport 'package:analysis_server/src/analysis_server.dart';
library;
import 'dart:async';
import 'dart:convert';
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:watcher/watcher.dart';
/// A mock [ServerCommunicationChannel] for testing [AnalysisServer].
class MockServerChannel implements ServerCommunicationChannel {
/// A controller for the stream of requests and responses from the client to
/// the server.
///
/// Messages added to this stream should be converted to/from JSON to ensure
/// they are fully serialized/deserialized as they would be in a real server
/// otherwise tests may receive real instances where in reality they would be
/// maps.
StreamController<RequestOrResponse> requestController =
StreamController<RequestOrResponse>();
/// A controller for the stream of requests and responses from the server to
/// the client.
///
/// Messages added to this stream should be converted to/from JSON to ensure
/// they are fully serialized/deserialized as they would be in a real server
/// otherwise tests may receive real instances where in reality they would be
/// maps.
StreamController<RequestOrResponse> responseController =
StreamController<RequestOrResponse>.broadcast();
/// A controller for the stream of notifications from the server to the
/// client.
///
/// Unlike [requestController] and [responseController], notifications added
/// here are not round-tripped through JSON but instead have real class
/// instances as their `params`. This is because they are only used by tests
/// which will cast and/or switch on the types for convenience.
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;
/// True if we are printing out messages exchanged with the server.
final bool printMessages;
MockServerChannel({bool? printMessages})
: printMessages = printMessages ?? false;
/// Return the broadcast stream of notifications.
Stream<Notification> get notifications {
return notificationController.stream;
}
@override
Stream<RequestOrResponse> get requests => requestController.stream;
/// Return the broadcast stream of server-to-client requests.
Stream<Request> get serverToClientRequests {
return responseController.stream.where((r) => r is Request).cast<Request>();
}
@override
void close() {
_closed = true;
}
@override
void sendNotification(Notification notification) {
// Don't deliver notifications after the connection is closed.
if (_closed) {
return;
}
notificationsReceived.add(notification);
notificationController.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),
);
}
}
@override
void sendRequest(Request request) {
var jsonString = jsonEncode(request.toJson());
if (printMessages) {
print('<== $jsonString');
}
// Round-trip via JSON to ensure all types are fully serialized as they
// would be in a real setup.
request = Request.fromJson(jsonDecode(jsonString) as Map<String, Object?>)!;
serverRequestsSent.add(request);
responseController.add(request);
}
@override
void sendResponse(Response response) {
// Don't deliver responses after the connection is closed.
if (_closed) {
return;
}
var jsonString = jsonEncode(response.toJson());
if (printMessages) {
print('<== $jsonString');
}
// Round-trip via JSON to ensure all types are fully serialized as they
// would be in a real setup.
response =
Response.fromJson(jsonDecode(jsonString) as Map<String, Object?>)!;
responsesReceived.add(response);
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) async {
if (_closed) {
throw Exception('simulateRequestFromClient after connection closed');
}
var jsonString = jsonEncode(request.toJson());
if (printMessages) {
print('==> $jsonString');
}
// Round-trip via JSON to ensure all types are fully serialized as they
// would be in a real setup.
request = Request.fromJson(jsonDecode(jsonString) as Map<String, Object?>)!;
requestController.add(request);
var response = await waitForResponse(request);
// Round-trip via JSON to ensure all types are fully serialized as they
// would be in a real setup.
response =
Response.fromJson(
jsonDecode(jsonEncode(response)) as Map<String, Object?>,
)!;
return response;
}
/// Send the given [response] to the server as if it had been sent from the
/// client.
void simulateResponseFromClient(Response response) {
// No further requests should be sent after the connection is closed.
if (_closed) {
throw Exception('simulateRequestFromClient after connection closed');
}
var jsonString = jsonEncode(response.toJson());
if (printMessages) {
print('==> $jsonString');
}
// Round-trip via JSON to ensure all types are fully serialized as they
// would be in a real setup.
response =
Response.fromJson(jsonDecode(jsonString) as Map<String, Object?>)!;
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
.where((r) => r is Response)
.cast<Response>()
.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 = [];
@override
Completer<void> initializedCompleter = Completer();
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) =>
throw Exception('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;
}
}