c5fb640747
This updates the `showUserPrompt` method to accept a cancellation token so that if requests are cancelled (by the client, or something like a second refactor cancelling the first), the prompt is also cancelled (and when the response arrives, it can be ignored). Note: Unfortunately the protocol (and VS Code) don't actually allow for the prompt to be cancelled/hidden from the user, this is mainly to avoid us keeping the request "alive" on the server if the user ignores a prompt and we know the parent request was cancelled anyway. Fixes https://github.com/dart-lang/sdk/issues/63285 Change-Id: Ia77880a749b284a2ff31c8cf560307ad908d2175 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/499360 Reviewed-by: Konstantin Shcheglov <scheglov@google.com> Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
441 lines
14 KiB
Dart
441 lines
14 KiB
Dart
// 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 'package:analysis_server/lsp_protocol/protocol.dart' as lsp;
|
|
import 'package:analysis_server/protocol/protocol.dart';
|
|
import 'package:analysis_server/protocol/protocol_constants.dart';
|
|
import 'package:analysis_server/protocol/protocol_generated.dart'
|
|
hide MessageType;
|
|
import 'package:analysis_server/src/analysis_server.dart' show MessageType;
|
|
import 'package:analysis_server/src/services/user_prompts/dart_fix_prompt_manager.dart';
|
|
import 'package:analyzer/src/utilities/cancellation.dart';
|
|
import 'package:test/test.dart';
|
|
import 'package:test_reflective_loader/test_reflective_loader.dart';
|
|
|
|
import 'analysis_server_base.dart';
|
|
import 'constants.dart';
|
|
import 'mocks.dart';
|
|
|
|
void main() {
|
|
defineReflectiveSuite(() {
|
|
defineReflectiveTests(ServerDomainTest);
|
|
defineReflectiveTests(ServerDartFixPromptTest);
|
|
});
|
|
}
|
|
|
|
/// Checks server interacts with [DartFixPromptManager] correctly.
|
|
///
|
|
/// Tests for [DartFixPromptManager]'s behaviour are in
|
|
/// test/services/user_prompts/dart_fix_prompt_manager_test.dart.
|
|
@reflectiveTest
|
|
class ServerDartFixPromptTest extends PubPackageAnalysisServerTest {
|
|
late TestDartFixPromptManager promptManager;
|
|
|
|
@override
|
|
DartFixPromptManager? get dartFixPromptManager => promptManager;
|
|
|
|
@override
|
|
void setUp() {
|
|
promptManager = TestDartFixPromptManager();
|
|
super.setUp();
|
|
}
|
|
|
|
Future<void> test_trigger_afterInitialAnalysis() async {
|
|
await setRoots(included: [workspaceRootPath], excluded: []);
|
|
await pumpEventQueue(times: 5000);
|
|
expect(promptManager.checksTriggered, 1);
|
|
}
|
|
|
|
Future<void> test_trigger_afterPackageConfigChange() async {
|
|
// Ensure there's a file to analyze otherwise writing the package_config
|
|
// won't trigger any additional analysis.
|
|
newFile('$testPackageLibPath/test.dart', 'void f() {}');
|
|
|
|
// Set up and let initial analysis complete.
|
|
await setRoots(included: [workspaceRootPath], excluded: []);
|
|
await pumpEventQueue(times: 5000);
|
|
expect(promptManager.checksTriggered, 1);
|
|
|
|
// Expect that writing package config attempts to trigger another check.
|
|
writeTestPackageConfig();
|
|
await pumpEventQueue(times: 5000);
|
|
expect(promptManager.checksTriggered, 2);
|
|
}
|
|
}
|
|
|
|
@reflectiveTest
|
|
class ServerDomainTest extends PubPackageAnalysisServerTest {
|
|
Future<void> test_getVersion() async {
|
|
var request = ServerGetVersionParams().toRequest(
|
|
'0',
|
|
clientUriConverter: server.uriConverter,
|
|
);
|
|
var response = await handleSuccessfulRequest(request);
|
|
expect(
|
|
response.toJson(),
|
|
equals({
|
|
Response.idAttributeName: '0',
|
|
Response.resultAttributeName: {versionKey: PROTOCOL_VERSION},
|
|
}),
|
|
);
|
|
}
|
|
|
|
Future<void> test_openUrl() async {
|
|
server.clientCapabilities.requests = ['openUrlRequest'];
|
|
|
|
// Send the request.
|
|
var uri = toUri('https://dart.dev');
|
|
var responseFuture = server.openUriNotificationSender!.call(uri);
|
|
expect(serverChannel.serverRequestsSent, hasLength(1));
|
|
|
|
// Simulate the response.
|
|
var request = serverChannel.serverRequestsSent[0];
|
|
serverChannel.simulateResponseFromClient(
|
|
ServerOpenUrlRequestResult().toResponse(
|
|
request.id,
|
|
clientUriConverter: server.uriConverter,
|
|
),
|
|
);
|
|
await responseFuture;
|
|
}
|
|
|
|
Future<void> test_setClientCapabilities_lspCapabilities() async {
|
|
// Test an arbitrary set of capabilities.
|
|
var capabilities = lsp.ClientCapabilities(
|
|
textDocument: lsp.TextDocumentClientCapabilities(
|
|
hover: lsp.HoverClientCapabilities(
|
|
contentFormat: [lsp.MarkupKind.PlainText],
|
|
),
|
|
),
|
|
workspace: lsp.WorkspaceClientCapabilities(
|
|
applyEdit: true,
|
|
workspaceEdit: lsp.WorkspaceEditClientCapabilities(
|
|
documentChanges: true,
|
|
resourceOperations: [lsp.ResourceOperationKind.Create],
|
|
),
|
|
),
|
|
);
|
|
|
|
var request = ServerSetClientCapabilitiesParams(
|
|
[],
|
|
lspCapabilities: capabilities.toJson(),
|
|
).toRequest('1', clientUriConverter: server.uriConverter);
|
|
|
|
await handleSuccessfulRequest(request);
|
|
var effectiveCapabilities = server.editorClientCapabilities;
|
|
expect(
|
|
effectiveCapabilities.hoverContentFormats,
|
|
equals([lsp.MarkupKind.PlainText]),
|
|
);
|
|
expect(effectiveCapabilities.applyEdit, isTrue);
|
|
expect(effectiveCapabilities.documentChanges, isTrue);
|
|
expect(effectiveCapabilities.createResourceOperations, isTrue);
|
|
}
|
|
|
|
Future<void> test_setClientCapabilities_lspCapabilities_invalid() async {
|
|
var request = ServerSetClientCapabilitiesParams(
|
|
[],
|
|
lspCapabilities: {
|
|
'textDocument': 1, // Not valid
|
|
},
|
|
).toRequest('1', clientUriConverter: server.uriConverter);
|
|
|
|
var response = await handleRequest(request);
|
|
expect(
|
|
response,
|
|
isResponseFailure('1', RequestErrorCode.INVALID_PARAMETER),
|
|
);
|
|
expect(
|
|
response.error!.message,
|
|
"The 'lspCapabilities' parameter was invalid:"
|
|
' textDocument must be of type TextDocumentClientCapabilities',
|
|
);
|
|
}
|
|
|
|
Future<void> test_setClientCapabilities_requests() async {
|
|
var requestId = -1;
|
|
|
|
Future<void> setCapabilities({
|
|
required bool openUrlRequest,
|
|
required bool showMessageRequest,
|
|
}) async {
|
|
var requests = [
|
|
if (openUrlRequest) 'openUrlRequest',
|
|
if (showMessageRequest) 'showMessageRequest',
|
|
];
|
|
if (requestId >= 0) {
|
|
// This is a bit of a kludge, but the first time this function is called
|
|
// we won't set the request, we'll just test the default state.
|
|
var request = ServerSetClientCapabilitiesParams(requests).toRequest(
|
|
requestId.toString(),
|
|
clientUriConverter: server.uriConverter,
|
|
);
|
|
await handleSuccessfulRequest(request);
|
|
}
|
|
requestId++;
|
|
|
|
expect(server.clientCapabilities.requests, requests);
|
|
expect(
|
|
server.openUriNotificationSender,
|
|
openUrlRequest ? isNotNull : isNull,
|
|
);
|
|
expect(server.userPromptSender, showMessageRequest ? isNotNull : isNull);
|
|
}
|
|
|
|
await setCapabilities(openUrlRequest: false, showMessageRequest: false);
|
|
await setCapabilities(openUrlRequest: true, showMessageRequest: false);
|
|
await setCapabilities(openUrlRequest: true, showMessageRequest: true);
|
|
await setCapabilities(openUrlRequest: false, showMessageRequest: true);
|
|
await setCapabilities(openUrlRequest: false, showMessageRequest: false);
|
|
}
|
|
|
|
/// Verify that the server handles URIs once we've enabled the supportsUris
|
|
/// client capability.
|
|
Future<void>
|
|
test_setClientCapabilities_supportsUris_clientToServer_request() async {
|
|
// Tell the server we support URIs.
|
|
await handleSuccessfulRequest(
|
|
ServerSetClientCapabilitiesParams(
|
|
[],
|
|
supportsUris: true,
|
|
).toRequest('-1', clientUriConverter: server.uriConverter),
|
|
);
|
|
|
|
// Set the roots using a URI. Since the helper methods will to through
|
|
// toJson() (which will convert paths to URIs) we need to pass the JSON
|
|
// manually here.
|
|
await handleSuccessfulRequest(
|
|
Request('1', 'analysis.setAnalysisRoots', {
|
|
'included': [toUri(workspaceRootPath).toString()],
|
|
'excluded': [],
|
|
}),
|
|
);
|
|
await pumpEventQueue(times: 5000);
|
|
|
|
// Ensure the roots were recorded correctly.
|
|
expect(server.contextManager.includedPaths, [
|
|
convertPath(workspaceRootPath),
|
|
]);
|
|
}
|
|
|
|
Future<void> test_setClientCapabilities_supportsUris_defaults() async {
|
|
// Before request.
|
|
expect(server.clientCapabilities.supportsUris, isNull);
|
|
expect(server.uriConverter.supportedNonFileSchemes, isEmpty);
|
|
|
|
// If not supplied.
|
|
await handleSuccessfulRequest(
|
|
ServerSetClientCapabilitiesParams(
|
|
[],
|
|
).toRequest('-1', clientUriConverter: server.uriConverter),
|
|
);
|
|
expect(server.clientCapabilities.supportsUris, isNull);
|
|
expect(server.uriConverter.supportedNonFileSchemes, isEmpty);
|
|
|
|
// If set explicitly to false.
|
|
await handleSuccessfulRequest(
|
|
ServerSetClientCapabilitiesParams(
|
|
[],
|
|
supportsUris: false,
|
|
).toRequest('-1', clientUriConverter: server.uriConverter),
|
|
);
|
|
expect(server.clientCapabilities.supportsUris, isFalse);
|
|
expect(server.uriConverter.supportedNonFileSchemes, isEmpty);
|
|
}
|
|
|
|
Future<void>
|
|
test_setClientCapabilities_supportsUris_false_rejectsUris() async {
|
|
// Explicitly tell the server we do not support URIs.
|
|
await handleSuccessfulRequest(
|
|
ServerSetClientCapabilitiesParams(
|
|
[],
|
|
supportsUris: false,
|
|
).toRequest('-1', clientUriConverter: server.uriConverter),
|
|
);
|
|
|
|
// Try to send a URI anyway.
|
|
var request = Request('1', 'analysis.setAnalysisRoots', {
|
|
'included': [toUri(workspaceRootPath).toString()],
|
|
'excluded': [],
|
|
});
|
|
var response = await handleRequest(request);
|
|
expect(
|
|
response,
|
|
isResponseFailure(request.id, RequestErrorCode.INVALID_FILE_PATH_FORMAT),
|
|
);
|
|
}
|
|
|
|
/// Verify that the server uses URIs in notifications once we've enabled the
|
|
/// supportsUris client capability.
|
|
Future<void>
|
|
test_setClientCapabilities_supportsUris_serverToClient_notification() async {
|
|
// Add a file with an error for testing.
|
|
var testFilePath = convertPath('$testPackageLibPath/test.dart');
|
|
var testFileUriString = toUri(testFilePath).toString();
|
|
newFile(testFilePath, 'broken');
|
|
|
|
// Tell the server we support URIs before analysis starts since we will
|
|
// verify the analysis.errors notification.
|
|
await handleSuccessfulRequest(
|
|
ServerSetClientCapabilitiesParams(
|
|
[],
|
|
supportsUris: true,
|
|
).toRequest('10', clientUriConverter: server.uriConverter),
|
|
);
|
|
await pumpEventQueue(times: 5000);
|
|
|
|
// Trigger analysis.
|
|
await setRoots(
|
|
// We can use paths here because toJson() will handle the conversion.
|
|
included: [workspaceRootPath],
|
|
excluded: [],
|
|
);
|
|
await pumpEventQueue(times: 5000);
|
|
|
|
// Verify the last error for this file was using a URI.
|
|
var lastErrorFile = serverChannel.notificationsReceived
|
|
.where((notification) => notification.event == 'analysis.errors')
|
|
.map((notification) => notification.params!['file'] as String)
|
|
.lastWhere((filePath) => filePath.endsWith('test.dart'));
|
|
expect(lastErrorFile, testFileUriString);
|
|
}
|
|
|
|
/// Verify that the server returns URIs once we've enabled the supportsUris
|
|
/// client capability.
|
|
Future<void>
|
|
test_setClientCapabilities_supportsUris_serverToClient_response() async {
|
|
// Add a file with an error for testing.
|
|
var testFilePath = convertPath('$testPackageLibPath/test.dart');
|
|
var testFileUriString = toUri(testFilePath).toString();
|
|
newFile(testFilePath, 'broken');
|
|
|
|
await setRoots(included: [workspaceRootPath], excluded: []);
|
|
await pumpEventQueue(times: 5000);
|
|
|
|
// Tell the server we support URIs.
|
|
await handleSuccessfulRequest(
|
|
ServerSetClientCapabilitiesParams(
|
|
[],
|
|
supportsUris: true,
|
|
).toRequest('10', clientUriConverter: server.uriConverter),
|
|
);
|
|
await pumpEventQueue(times: 5000);
|
|
|
|
// Send a GetErrors request. The response has nested FilePaths inside the
|
|
// AnalysisErrors so will confirm the server mapped correctly.
|
|
var response = await handleSuccessfulRequest(
|
|
Request('1', 'analysis.getErrors', {'file': testFileUriString}),
|
|
);
|
|
// Verify the error location was the expected URI.
|
|
expect(
|
|
(response.result as dynamic)['errors'][0]!['location']['file'],
|
|
testFileUriString,
|
|
);
|
|
}
|
|
|
|
Future<void>
|
|
test_setClientCapabilities_supportsUris_unspecified_rejectsUris() async {
|
|
// Do not tell the server we support URIs.
|
|
|
|
// Try to send a URI anyway.
|
|
var request = Request('1', 'analysis.setAnalysisRoots', {
|
|
'included': [toUri(workspaceRootPath).toString()],
|
|
'excluded': [],
|
|
});
|
|
var response = await handleRequest(request);
|
|
expect(
|
|
response,
|
|
isResponseFailure(request.id, RequestErrorCode.INVALID_FILE_PATH_FORMAT),
|
|
);
|
|
}
|
|
|
|
Future<void> test_setSubscriptions_invalidServiceName() async {
|
|
var request = Request('0', serverRequestSetSubscriptions, {
|
|
subscriptionsKey: ['noSuchService'],
|
|
});
|
|
var response = await handleRequest(request);
|
|
expect(response, isResponseFailure('0'));
|
|
}
|
|
|
|
Future<void> test_setSubscriptions_success() async {
|
|
expect(server.serverServices, isEmpty);
|
|
// send request
|
|
var request = ServerSetSubscriptionsParams([
|
|
ServerService.STATUS,
|
|
]).toRequest('0', clientUriConverter: server.uriConverter);
|
|
await handleSuccessfulRequest(request);
|
|
// set of services has been changed
|
|
expect(server.serverServices, contains(ServerService.STATUS));
|
|
}
|
|
|
|
Future<void> test_showMessage() async {
|
|
server.clientCapabilities.requests = ['showMessageRequest'];
|
|
|
|
// Send the request.
|
|
var responseFuture = server.userPromptSender!(
|
|
MessageType.warning,
|
|
'message',
|
|
['a', 'b'],
|
|
NotCancelableToken(),
|
|
);
|
|
expect(serverChannel.serverRequestsSent, hasLength(1));
|
|
|
|
// Simulate the response.
|
|
var request = serverChannel.serverRequestsSent[0];
|
|
serverChannel.simulateResponseFromClient(
|
|
ServerShowMessageRequestResult(
|
|
action: 'a',
|
|
).toResponse(request.id, clientUriConverter: server.uriConverter),
|
|
);
|
|
var response = await responseFuture;
|
|
expect(response, 'a');
|
|
}
|
|
|
|
Future<void> test_showMessage_nullResponse() async {
|
|
server.clientCapabilities.requests = ['showMessageRequest'];
|
|
|
|
// Send the request.
|
|
var responseFuture = server.userPromptSender!(
|
|
MessageType.warning,
|
|
'message',
|
|
['a', 'b'],
|
|
NotCancelableToken(),
|
|
);
|
|
expect(serverChannel.serverRequestsSent, hasLength(1));
|
|
|
|
// Simulate the response.
|
|
var request = serverChannel.serverRequestsSent[0];
|
|
serverChannel.simulateResponseFromClient(
|
|
ServerShowMessageRequestResult().toResponse(
|
|
request.id,
|
|
clientUriConverter: server.uriConverter,
|
|
),
|
|
);
|
|
var response = await responseFuture;
|
|
expect(response, isNull);
|
|
}
|
|
|
|
Future<void> test_shutdown() async {
|
|
var request = ServerShutdownParams().toRequest(
|
|
'0',
|
|
clientUriConverter: server.uriConverter,
|
|
);
|
|
await handleSuccessfulRequest(request);
|
|
}
|
|
}
|
|
|
|
class TestDartFixPromptManager implements DartFixPromptManager {
|
|
var checksTriggered = 0;
|
|
|
|
@override
|
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
|
|
|
@override
|
|
void triggerCheck() {
|
|
checksTriggered++;
|
|
}
|
|
}
|