Skip to content

Commit fb3255d

Browse files
DanTupCommit Queue
authored andcommitted
[analysis_server] Add support/tests for reverse-requests for LSP-over-Legacy
This adds shared tests for sending workspace/applyEdit reverse-requests that run for both the LSP and Legacy servers. It involved moving some code out of the base LSP test onto mixins to be used by LSP-over-Legacy tests and I extracted some mixins that can be used in tests to provide a common interface to both servers for writing shared tests. Change-Id: I8c6d09f220b2593680547311a4fce66013f86e2e Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/404420 Reviewed-by: Konstantin Shcheglov <[email protected]> Reviewed-by: Brian Wilkerson <[email protected]> Commit-Queue: Brian Wilkerson <[email protected]>
1 parent 722e4f2 commit fb3255d

13 files changed

+563
-171
lines changed

pkg/analysis_server/lib/src/legacy_analysis_server.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,8 @@ class LegacyAnalysisServer extends AnalysisServer {
462462
}
463463

464464
@override
465-
get editorClientCapabilities => _editorClientCapabilities;
465+
lsp.LspClientCapabilities get editorClientCapabilities =>
466+
_editorClientCapabilities;
466467

467468
/// The [Future] that completes when analysis is complete.
468469
Future<void> get onAnalysisComplete {

pkg/analysis_server/lib/src/utilities/mocks.dart

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import 'dart:async';
6+
import 'dart:convert';
67

78
import 'package:analysis_server/protocol/protocol.dart';
89
import 'package:analysis_server/src/channel/channel.dart';
@@ -15,12 +16,21 @@ import 'package:watcher/watcher.dart';
1516

1617
/// A mock [ServerCommunicationChannel] for testing [AnalysisServer].
1718
class MockServerChannel implements ServerCommunicationChannel {
19+
/// A controller for the stream of requests and responses from the client to
20+
/// the server.
1821
StreamController<RequestOrResponse> requestController =
1922
StreamController<RequestOrResponse>();
20-
StreamController<Response> responseController =
21-
StreamController<Response>.broadcast();
23+
24+
/// A controller for the stream of requests and responses from the server to
25+
/// the client.
26+
StreamController<RequestOrResponse> responseController =
27+
StreamController<RequestOrResponse>.broadcast();
28+
29+
/// A controller for the stream of notifications from the server to the
30+
/// client.
2231
StreamController<Notification> notificationController =
2332
StreamController<Notification>.broadcast(sync: true);
33+
2434
Completer<Response>? errorCompleter;
2535

2636
List<Response> responsesReceived = [];
@@ -41,6 +51,11 @@ class MockServerChannel implements ServerCommunicationChannel {
4151
@override
4252
Stream<RequestOrResponse> get requests => requestController.stream;
4353

54+
/// Return the broadcast stream of server-to-client requests.
55+
Stream<Request> get serverToClientRequests {
56+
return responseController.stream.where((r) => r is Request).cast<Request>();
57+
}
58+
4459
@override
4560
void close() {
4661
_closed = true;
@@ -71,6 +86,7 @@ class MockServerChannel implements ServerCommunicationChannel {
7186
@override
7287
void sendRequest(Request request) {
7388
serverRequestsSent.add(request);
89+
responseController.add(request);
7490
}
7591

7692
@override
@@ -89,13 +105,30 @@ class MockServerChannel implements ServerCommunicationChannel {
89105
/// with the [request] has been received.
90106
///
91107
/// The value of the future will be the received response.
92-
Future<Response> simulateRequestFromClient(Request request) {
108+
Future<Response> simulateRequestFromClient(Request request) async {
93109
if (_closed) {
94110
throw Exception('simulateRequestFromClient after connection closed');
95111
}
112+
113+
// Round-trip via JSON to ensure all types are fully serialized as they
114+
// would be in a real setup.
115+
request =
116+
Request.fromJson(
117+
jsonDecode(jsonEncode(request)) as Map<String, Object?>,
118+
)!;
119+
96120
// Wrap send request in future to simulate WebSocket.
97-
Future(() => requestController.add(request));
98-
return waitForResponse(request);
121+
unawaited(Future(() => requestController.add(request)));
122+
var response = await waitForResponse(request);
123+
124+
// Round-trip via JSON to ensure all types are fully serialized as they
125+
// would be in a real setup.
126+
response =
127+
Response.fromJson(
128+
jsonDecode(jsonEncode(response)) as Map<String, Object?>,
129+
)!;
130+
131+
return response;
99132
}
100133

101134
/// Send the given [response] to the server as if it had been sent from the
@@ -117,9 +150,10 @@ class MockServerChannel implements ServerCommunicationChannel {
117150
/// has already been sent to the server.
118151
Future<Response> waitForResponse(Request request) {
119152
var id = request.id;
120-
return responseController.stream.firstWhere(
121-
(response) => response.id == id,
122-
);
153+
return responseController.stream
154+
.where((r) => r is Response)
155+
.cast<Response>()
156+
.firstWhere((response) => response.id == id);
123157
}
124158
}
125159

pkg/analysis_server/test/analysis_server_base.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,9 @@ abstract class ContextResolutionTest with ResourceProviderMixin {
224224
class PubPackageAnalysisServerTest extends ContextResolutionTest
225225
with MockPackagesMixin, ConfigurationFilesMixin, TestMacros {
226226
// TODO(scheglov): Consider turning it back into a getter.
227-
late String testFilePath = '$testPackageLibPath/test.dart';
227+
late String testFilePath = resourceProvider.convertPath(
228+
'$testPackageLibPath/test.dart',
229+
);
228230

229231
/// Return a list of the experiments that are to be enabled for tests in this
230232
/// class, an empty list if there are no experiments that should be enabled.

pkg/analysis_server/test/domain_server_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ class ServerDomainTest extends PubPackageAnalysisServerTest {
124124
).toRequest('1', clientUriConverter: server.uriConverter);
125125

126126
await handleSuccessfulRequest(request);
127-
var effectiveCapabilities = server.editorClientCapabilities!;
127+
var effectiveCapabilities = server.editorClientCapabilities;
128128
expect(
129129
effectiveCapabilities.hoverContentFormats,
130130
equals([lsp.MarkupKind.PlainText]),

pkg/analysis_server/test/integration/lsp/abstract_lsp_over_legacy.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ abstract class AbstractLspOverLegacyTest
2525
),
2626
);
2727

28+
@override
29+
Stream<RequestMessage> get requestsFromServer =>
30+
throw 'Reverse-requests not currently supported for LSP-over-Legacy integration tests';
31+
2832
/// The URI for the macro-generated content for [testFileUri].
2933
Uri get testFileMacroUri =>
3034
Uri.file(testFile).replace(scheme: macroClientUriScheme);
@@ -65,4 +69,9 @@ abstract class AbstractLspOverLegacyTest
6569
return fromJson(lspResponse.result as R);
6670
}
6771
}
72+
73+
@override
74+
void sendResponseToServer(ResponseMessage response) {
75+
throw 'Reverse-requests not currently supported for LSP-over-Legacy integration tests';
76+
}
6877
}

pkg/analysis_server/test/lsp/request_helpers_mixin.dart

Lines changed: 157 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'dart:async';
66

77
import 'package:analysis_server/lsp_protocol/protocol.dart';
8+
import 'package:analysis_server/src/lsp/client_capabilities.dart';
89
import 'package:analysis_server/src/lsp/constants.dart';
910
import 'package:analysis_server/src/lsp/mapping.dart';
1011
import 'package:analysis_server/src/services/completion/dart/feature_computer.dart';
@@ -14,7 +15,7 @@ import 'package:collection/collection.dart';
1415
import 'package:language_server_protocol/json_parsing.dart';
1516
import 'package:path/path.dart' as path;
1617
import 'package:test/test.dart' as test show expect;
17-
import 'package:test/test.dart' hide expect;
18+
import 'package:test/test.dart';
1819

1920
import 'change_verifier.dart';
2021

@@ -169,6 +170,8 @@ mixin LspRequestHelpersMixin {
169170

170171
Stream<NotificationMessage> get notificationsFromServer;
171172

173+
Stream<RequestMessage> get requestsFromServer;
174+
172175
Future<List<CallHierarchyIncomingCall>?> callHierarchyIncoming(
173176
CallHierarchyItem item,
174177
) {
@@ -225,6 +228,29 @@ mixin LspRequestHelpersMixin {
225228
void expect(Object? actual, Matcher matcher, {String? reason}) =>
226229
test.expect(actual, matcher, reason: reason);
227230

231+
/// Expects a [method] request from the server after executing [f].
232+
Future<RequestMessage> expectRequest(
233+
Method method,
234+
FutureOr<void> Function() f, {
235+
Duration timeout = const Duration(seconds: 5),
236+
}) async {
237+
var firstRequest = requestsFromServer.firstWhere((n) => n.method == method);
238+
await f();
239+
240+
var requestFromServer = await firstRequest.timeout(
241+
timeout,
242+
onTimeout:
243+
() =>
244+
throw TimeoutException(
245+
'Did not receive the expected $method request from the server in the timeout period',
246+
timeout,
247+
),
248+
);
249+
250+
expect(requestFromServer, isNotNull);
251+
return requestFromServer;
252+
}
253+
228254
Future<T> expectSuccessfulResponseTo<T, R>(
229255
RequestMessage request,
230256
T Function(R) fromJson,
@@ -777,6 +803,71 @@ mixin LspRequestHelpersMixin {
777803
);
778804
}
779805

806+
/// Executes [f] then waits for a request of type [method] from the server which
807+
/// is passed to [handler] to process, then waits for (and returns) the
808+
/// response to the original request.
809+
///
810+
/// This is used for testing things like code actions, where the client initiates
811+
/// a request but the server does not respond to it until it's sent its own
812+
/// request to the client and it received a response.
813+
///
814+
/// Client Server
815+
/// 1. |- Req: textDocument/codeAction ->
816+
/// 1. <- Resp: textDocument/codeAction -|
817+
///
818+
/// 2. |- Req: workspace/executeCommand ->
819+
/// 3. <- Req: textDocument/applyEdits -|
820+
/// 3. |- Resp: textDocument/applyEdits ->
821+
/// 2. <- Resp: workspace/executeCommand -|
822+
///
823+
/// Request 2 from the client is not responded to until the server has its own
824+
/// response to the request it sends (3).
825+
Future<T> handleExpectedRequest<T, R, RR>(
826+
Method method,
827+
R Function(Map<String, dynamic>) fromJson,
828+
Future<T> Function() f, {
829+
required FutureOr<RR> Function(R) handler,
830+
Duration timeout = const Duration(seconds: 5),
831+
}) async {
832+
late Future<T> outboundRequest;
833+
Object? outboundRequestError;
834+
835+
// Run [f] and wait for the incoming request from the server.
836+
var incomingRequest = await expectRequest(method, () {
837+
// Don't return/await the response yet, as this may not complete until
838+
// after we have handled the request that comes from the server.
839+
outboundRequest = f();
840+
841+
// Because we don't await this future until "later", if it throws the
842+
// error is treated as unhandled and will fail the test even if expected.
843+
// Instead, capture the error and suppress it. But if we time out (in
844+
// which case we will never return outboundRequest), then we'll raise this
845+
// error.
846+
outboundRequest.then(
847+
(_) {},
848+
onError: (e) {
849+
outboundRequestError = e;
850+
return null;
851+
},
852+
);
853+
}, timeout: timeout).catchError((Object timeoutException) {
854+
// We timed out waiting for the request from the server. Probably this is
855+
// because our outbound request for some reason, so if we have an error
856+
// for that, then throw it. Otherwise, propogate the timeout.
857+
throw outboundRequestError ?? timeoutException;
858+
}, test: (e) => e is TimeoutException);
859+
860+
// Handle the request from the server and send the response back.
861+
var clientsResponse = await handler(
862+
fromJson(incomingRequest.params as Map<String, Object?>),
863+
);
864+
respondTo(incomingRequest, clientsResponse);
865+
866+
// Return a future that completes when the response to the original request
867+
// (from [f]) returns.
868+
return outboundRequest;
869+
}
870+
780871
RequestMessage makeRequest(Method method, ToJsonable? params) {
781872
var id = Either2<int, String>.t1(_id++);
782873
return RequestMessage(
@@ -851,6 +942,22 @@ mixin LspRequestHelpersMixin {
851942
return expectSuccessfulResponseTo(request, CompletionItem.fromJson);
852943
}
853944

945+
/// Sends [responseParams] to the server as a successful response to
946+
/// a server-initiated [request].
947+
void respondTo<T>(RequestMessage request, T responseParams) {
948+
sendResponseToServer(
949+
ResponseMessage(
950+
id: request.id,
951+
result: responseParams,
952+
jsonrpc: jsonRpcVersion,
953+
),
954+
);
955+
}
956+
957+
/// Sends a ResponseMessage to the server, completing a reverse
958+
/// (server-to-client) request.
959+
void sendResponseToServer(ResponseMessage response);
960+
854961
Future<List<TypeHierarchyItem>?> typeHierarchySubtypes(
855962
TypeHierarchyItem item,
856963
) {
@@ -1079,15 +1186,64 @@ mixin LspRequestHelpersMixin {
10791186
/// Extends [LspEditHelpersMixin] with methods for accessing file state and
10801187
/// information about the project to build paths.
10811188
mixin LspVerifyEditHelpersMixin on LspEditHelpersMixin {
1189+
LspClientCapabilities get editorClientCapabilities;
1190+
10821191
path.Context get pathContext;
10831192

10841193
String get projectFolderPath;
10851194

10861195
ClientUriConverter get uriConverter;
10871196

1197+
/// Executes a function which is expected to call back to the client to apply
1198+
/// a [WorkspaceEdit].
1199+
///
1200+
/// Returns a [LspChangeVerifier] that can be used to verify changes.
1201+
Future<LspChangeVerifier> executeForEdits(
1202+
Future<Object?> Function() function, {
1203+
ApplyWorkspaceEditResult? applyEditResult,
1204+
}) async {
1205+
ApplyWorkspaceEditParams? editParams;
1206+
1207+
var commandResponse = await handleExpectedRequest<
1208+
Object?,
1209+
ApplyWorkspaceEditParams,
1210+
ApplyWorkspaceEditResult
1211+
>(
1212+
Method.workspace_applyEdit,
1213+
ApplyWorkspaceEditParams.fromJson,
1214+
function,
1215+
handler: (edit) {
1216+
// When the server sends the edit back, just keep a copy and say we
1217+
// applied successfully (it'll be verified by the caller).
1218+
editParams = edit;
1219+
return applyEditResult ?? ApplyWorkspaceEditResult(applied: true);
1220+
},
1221+
);
1222+
// Successful edits return an empty success() response.
1223+
expect(commandResponse, isNull);
1224+
1225+
// Ensure the edit came back, and using the expected change type.
1226+
expect(editParams, isNotNull);
1227+
var edit = editParams!.edit;
1228+
1229+
var expectDocumentChanges = editorClientCapabilities.documentChanges;
1230+
expect(edit.documentChanges, expectDocumentChanges ? isNotNull : isNull);
1231+
expect(edit.changes, expectDocumentChanges ? isNull : isNotNull);
1232+
1233+
return LspChangeVerifier(this, edit);
1234+
}
1235+
10881236
/// A function to get the current contents of a file to apply edits.
10891237
String? getCurrentFileContent(Uri uri);
10901238

1239+
Future<T> handleExpectedRequest<T, R, RR>(
1240+
Method method,
1241+
R Function(Map<String, dynamic>) fromJson,
1242+
Future<T> Function() f, {
1243+
required FutureOr<RR> Function(R) handler,
1244+
Duration timeout = const Duration(seconds: 5),
1245+
});
1246+
10911247
/// Formats a path relative to the project root always using forward slashes.
10921248
///
10931249
/// This is used in the text format for comparing edits.

0 commit comments

Comments
 (0)