blob: 9c581cd6f2680b6a6ce332457f9c9551aac80ff6 [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.
import 'dart:async';
import 'package:analysis_server/protocol/protocol.dart';
import 'package:analysis_server/src/channel/channel.dart';
import 'package:analysis_server/src/plugin/notification_manager.dart';
import 'package:analysis_server/src/plugin/plugin_manager.dart';
import 'package:analyzer/dart/analysis/context_root.dart' as analyzer;
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/instrumentation/instrumentation.dart';
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:test/test.dart';
import 'package:watcher/watcher.dart';
/// A mock [ServerCommunicationChannel] for testing [AnalysisServer].
class MockServerChannel implements ServerCommunicationChannel {
StreamController<Request> requestController = StreamController<Request>();
StreamController<Response> responseController =
StreamController<Response>.broadcast();
StreamController<Notification> notificationController =
StreamController<Notification>(sync: true);
Completer<Response>? errorCompleter;
List<Response> responsesReceived = [];
List<Notification> notificationsReceived = [];
bool _closed = false;
String? name;
MockServerChannel();
@override
void close() {
_closed = true;
}
void expectMsgCount({responseCount = 0, notificationCount = 0}) {
expect(responsesReceived, hasLength(responseCount));
expect(notificationsReceived, hasLength(notificationCount));
}
@override
void listen(void Function(Request request) onRequest,
{Function? onError, void Function()? onDone}) {
requestController.stream
.listen(onRequest, onError: onError, onDone: onDone);
}
@override
void sendNotification(Notification notification) {
// Don't deliver notifications after the connection is closed.
if (_closed) {
return;
}
notificationsReceived.add(notification);
final 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);
}
/// 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. If [throwOnError] is
/// `true` (the default) then the returned future will throw an exception if a
/// server error is reported before the response has been received.
Future<Response> sendRequest(Request request, {bool throwOnError = true}) {
// TODO(brianwilkerson) Attempt to remove the `throwOnError` parameter and
// have the default behavior be the only behavior.
// No further requests should be sent after the connection is closed.
if (_closed) {
throw Exception('sendRequest after connection closed');
}
// Wrap send request in future to simulate WebSocket.
Future(() => requestController.add(request));
return waitForResponse(request, throwOnError: throwOnError);
}
@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));
}
/// 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. If [throwOnError] is `true` (the default) then the
/// returned future will throw an exception if a server error is reported
/// before the response has been received.
///
/// Unlike [sendRequest], this method assumes that the [request] has already
/// been sent to the server.
Future<Response> waitForResponse(Request request,
{bool throwOnError = true}) {
// TODO(brianwilkerson) Attempt to remove the `throwOnError` parameter and
// have the default behavior be the only behavior.
var id = request.id;
var response =
responseController.stream.firstWhere((response) => response.id == id);
if (throwOnError) {
var completer = errorCompleter = Completer<Response>();
try {
return Future.any([response, completer.future]);
} finally {
errorCompleter = null;
}
}
return response;
}
}
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;
@override
List<PluginInfo> plugins = [];
StreamController<void> pluginsChangedController =
StreamController.broadcast();
@override
String get byteStorePath {
fail('Unexpected invocation of byteStorePath');
}
@override
InstrumentationService get instrumentationService {
fail('Unexpected invocation of instrumentationService');
}
@override
AbstractNotificationManager get notificationManager {
fail('Unexpected invocation of notificationManager');
}
@override
Stream<void> get pluginsChanged => pluginsChangedController.stream;
@override
ResourceProvider get resourceProvider {
fail('Unexpected invocation of resourceProvider');
}
@override
String get sdkPath {
fail('Unexpected invocation of sdkPath');
}
@override
Future<void> addPluginToContextRoot(
analyzer.ContextRoot contextRoot, String path) async {
fail('Unexpected invocation of addPluginToContextRoot');
}
@override
Map<PluginInfo, Future<plugin.Response>> broadcastRequest(
plugin.RequestParams params,
{analyzer.ContextRoot? contextRoot}) {
broadcastedRequest = params;
return handleRequest?.call(params) ??
broadcastResults ??
<PluginInfo, Future<plugin.Response>>{};
}
@override
Future<List<Future<plugin.Response>>> broadcastWatchEvent(
WatchEvent watchEvent) async {
return <Future<plugin.Response>>[];
}
@override
List<String> pathsFor(String pluginPath) {
fail('Unexpected invocation of pathsFor');
}
@override
List<PluginInfo> pluginsForContextRoot(analyzer.ContextRoot? contextRoot) {
fail('Unexpected invocation of pluginsForContextRoot');
}
@override
void recordPluginFailure(String hostPackageName, String message) {
fail('Unexpected invocation of recordPluginFailure');
}
@override
void removedContextRoot(analyzer.ContextRoot contextRoot) {
fail('Unexpected invocation of removedContextRoot');
}
@override
Future<void> restartPlugins() async {
// Nothing to restart.
return null;
}
@override
void setAnalysisSetPriorityFilesParams(
plugin.AnalysisSetPriorityFilesParams params) {
analysisSetPriorityFilesParams = params;
}
@override
void setAnalysisSetSubscriptionsParams(
plugin.AnalysisSetSubscriptionsParams params) {
analysisSetSubscriptionsParams = params;
}
@override
void setAnalysisUpdateContentParams(
plugin.AnalysisUpdateContentParams params) {
analysisUpdateContentParams = params;
}
@override
Future<List<void>> stopAll() async {
fail('Unexpected invocation of stopAll');
}
}