fadf8a0959
There are a few things in this CL that were difficult to separate testing of (because they're all part of the same feature): 1. sets `requiresTrustedCaller=false` on the `executeCommand` handler so that commands can be called over DTD. It adds the same flag to the command handlers themselves, so a command can control whether it can be called over DTD or not (right now we allow everything except `logAction` and `sendWorkspaceEdit` which are both commands used internally and not appropriate for DTD clients to call). 2. Removes the allow-list on DTD methods, allowing all LSP shared methods to be available over DTD 3. Extends the integration test classes to support reverse-requests so we can verify the edits being sent back to the editor when calling the code actions commands over DTD 4. It also fixes a few bugs where we read the callers capabilities instead of the editors capabilities (which until now would always be the same in those places, but with this change are not). Change-Id: I6d271ddad6dc1b00a98b10b735763a368c91af7a Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/428784 Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Reviewed-by: Samuel Rawlins <srawlins@google.com> Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
283 lines
8.9 KiB
Dart
283 lines
8.9 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.
|
|
|
|
/// Code generation for the file "integration_test_methods.dart".
|
|
library;
|
|
|
|
import 'package:analyzer_utilities/tools.dart';
|
|
import 'package:path/path.dart' as path;
|
|
|
|
import 'api.dart';
|
|
import 'codegen_dart.dart';
|
|
import 'from_html.dart';
|
|
import 'to_html.dart';
|
|
|
|
final GeneratedFile target = GeneratedFile(
|
|
'test/integration/support/integration_test_methods.dart',
|
|
(String pkgPath) async {
|
|
var visitor = CodegenInttestMethodsVisitor(
|
|
path.basename(pkgPath),
|
|
readApi(pkgPath),
|
|
);
|
|
return visitor.collectCode(visitor.visitApi);
|
|
},
|
|
);
|
|
|
|
/// Visitor that generates the code for integration_test_methods.dart
|
|
class CodegenInttestMethodsVisitor extends DartCodegenVisitor
|
|
with CodeGenerator {
|
|
/// The name of the package into which code is being generated.
|
|
final String packageName;
|
|
|
|
/// Visitor used to produce doc comments.
|
|
final ToHtmlVisitor toHtmlVisitor;
|
|
|
|
/// Code snippets concatenated to initialize all of the class fields.
|
|
List<String> fieldInitializationCode = <String>[];
|
|
|
|
/// Code snippets concatenated to produce the contents of the switch statement
|
|
/// for dispatching notifications.
|
|
List<String> notificationSwitchContents = <String>[];
|
|
|
|
CodegenInttestMethodsVisitor(this.packageName, Api api)
|
|
: toHtmlVisitor = ToHtmlVisitor(api),
|
|
super(api) {
|
|
codeGeneratorSettings.commentLineLength = 79;
|
|
codeGeneratorSettings.docCommentStartMarker = null;
|
|
codeGeneratorSettings.docCommentLineLeader = '/// ';
|
|
codeGeneratorSettings.docCommentEndMarker = null;
|
|
codeGeneratorSettings.languageName = 'dart';
|
|
}
|
|
|
|
/// Generate a function argument for the given parameter field.
|
|
String formatArgument(TypeObjectField field) =>
|
|
'${fieldDartType(field)} ${field.name}';
|
|
|
|
/// Figures out the appropriate Dart type for data having the given API
|
|
/// protocol [type].
|
|
String jsonType(TypeDecl type) {
|
|
type = resolveTypeReferenceChain(type);
|
|
return switch (type) {
|
|
TypeEnum() => 'String',
|
|
TypeList() => 'List<${jsonType(type.itemType)}>',
|
|
TypeMap() => 'Map<String, ${jsonType(type.valueType)}>',
|
|
TypeObject() => 'Map<String, dynamic>',
|
|
TypeReference() => switch (type.typeName) {
|
|
// These types correspond exactly to Dart types.
|
|
'String' || 'int' || 'bool' => type.typeName,
|
|
'object' => 'Map<String, dynamic>',
|
|
_ => throw Exception(type.typeName),
|
|
},
|
|
TypeUnion() => 'Object',
|
|
};
|
|
}
|
|
|
|
@override
|
|
void visitApi() {
|
|
outputHeader(year: '2017');
|
|
writeln();
|
|
writeln('/// Convenience methods for running integration tests.');
|
|
writeln('library;');
|
|
writeln();
|
|
writeln("import 'dart:async';");
|
|
writeln();
|
|
writeln("import 'package:$packageName/protocol/protocol_generated.dart';");
|
|
writeln(
|
|
"import 'package:$packageName/src/protocol/protocol_internal.dart';",
|
|
);
|
|
writeln(
|
|
"import 'package:analyzer_plugin/src/utilities/client_uri_converter.dart';",
|
|
);
|
|
for (var uri in api.types.importUris) {
|
|
write("import '");
|
|
write(uri);
|
|
writeln("';");
|
|
}
|
|
writeln("import 'package:path/path.dart' as path;");
|
|
writeln("import 'package:test/test.dart';");
|
|
writeln();
|
|
writeln("import 'integration_tests.dart';");
|
|
writeln("import 'protocol_matchers.dart';");
|
|
writeln();
|
|
writeln('/// Base implementation for running integration tests.');
|
|
writeln('abstract class IntegrationTest {');
|
|
indent(() {
|
|
writeln('Server get server;');
|
|
writeln();
|
|
writeln(
|
|
'/// The converter used to convert between URI/Paths in server communication.',
|
|
);
|
|
writeln(
|
|
'final ClientUriConverter uriConverter = ClientUriConverter.noop(path.context);',
|
|
);
|
|
super.visitApi();
|
|
writeln();
|
|
docComment(
|
|
toHtmlVisitor.collectHtml(() {
|
|
toHtmlVisitor.writeln('Dispatch the notification named [event], and');
|
|
toHtmlVisitor.writeln('containing parameters [params], to the');
|
|
toHtmlVisitor.writeln('appropriate stream.');
|
|
}),
|
|
);
|
|
writeln('void dispatchNotification(String event, params) {');
|
|
indent(() {
|
|
writeln('var decoder = ResponseDecoder(null);');
|
|
writeln('switch (event) {');
|
|
indent(() {
|
|
write(notificationSwitchContents.join());
|
|
writeln('default:');
|
|
indent(() {
|
|
writeln("fail('Unexpected notification: \$event');");
|
|
});
|
|
});
|
|
writeln('}');
|
|
});
|
|
writeln('}');
|
|
});
|
|
writeln('}');
|
|
}
|
|
|
|
@override
|
|
void visitNotification(Notification notification) {
|
|
var streamName = camelJoin([
|
|
'on',
|
|
notification.domainName,
|
|
notification.event,
|
|
]);
|
|
var className = camelJoin([
|
|
notification.domainName,
|
|
notification.event,
|
|
'params',
|
|
], doCapitalize: true);
|
|
writeln();
|
|
docComment(
|
|
toHtmlVisitor.collectHtml(() {
|
|
toHtmlVisitor.translateHtml(notification.html);
|
|
toHtmlVisitor.describePayload(notification.params, 'Parameters');
|
|
}),
|
|
);
|
|
writeln(
|
|
'late final Stream<$className> $streamName = '
|
|
'_$streamName.stream.asBroadcastStream();',
|
|
);
|
|
writeln();
|
|
docComment(
|
|
toHtmlVisitor.collectHtml(() {
|
|
toHtmlVisitor.write('Stream controller for [$streamName].');
|
|
}),
|
|
);
|
|
writeln('final _$streamName = StreamController<$className>(sync: true);');
|
|
notificationSwitchContents.add(
|
|
collectCode(() {
|
|
writeln("case '${notification.longEvent}':");
|
|
indent(() {
|
|
var paramsValidator = camelJoin([
|
|
'is',
|
|
notification.domainName,
|
|
notification.event,
|
|
'params',
|
|
]);
|
|
writeln('outOfTestExpect(params, $paramsValidator);');
|
|
String constructorCall;
|
|
if (notification.params == null) {
|
|
constructorCall = '$className()';
|
|
} else {
|
|
constructorCall =
|
|
"$className.fromJson(decoder, 'params', params, clientUriConverter: uriConverter)";
|
|
}
|
|
writeln('_$streamName.add($constructorCall);');
|
|
});
|
|
}),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void visitRequest(Request request) {
|
|
var methodName = camelJoin(['send', request.domainName, request.method]);
|
|
var args = <String>[];
|
|
var optionalArgs = <String>[];
|
|
var params = request.params;
|
|
if (params != null) {
|
|
for (var field in params.fields) {
|
|
if (field.optional) {
|
|
optionalArgs.add(formatArgument(field));
|
|
} else {
|
|
args.add(formatArgument(field));
|
|
}
|
|
}
|
|
}
|
|
if (optionalArgs.isNotEmpty) {
|
|
args.add('{${optionalArgs.join(', ')}}');
|
|
}
|
|
writeln();
|
|
docComment(
|
|
toHtmlVisitor.collectHtml(() {
|
|
toHtmlVisitor.translateHtml(request.html);
|
|
toHtmlVisitor.describePayload(params, 'Parameters');
|
|
toHtmlVisitor.describePayload(request.result, 'Returns');
|
|
}),
|
|
);
|
|
if (request.deprecated) {
|
|
writeln(' // TODO(srawlins): Provide a deprecation message, or remove.');
|
|
writeln(' // ignore: provide_deprecation_message');
|
|
writeln('@deprecated');
|
|
}
|
|
|
|
String? resultClass;
|
|
String futureClass;
|
|
var hasResult = request.result != null;
|
|
if (hasResult) {
|
|
resultClass = camelJoin([
|
|
request.domainName,
|
|
request.method,
|
|
'result',
|
|
], doCapitalize: true);
|
|
futureClass = 'Future<$resultClass>';
|
|
} else {
|
|
futureClass = 'Future<void>';
|
|
}
|
|
|
|
writeln('$futureClass $methodName(${args.join(', ')}) async {');
|
|
indent(() {
|
|
var requestClass = camelJoin([
|
|
request.domainName,
|
|
request.method,
|
|
'params',
|
|
], doCapitalize: true);
|
|
var paramsVar = 'null';
|
|
if (params != null) {
|
|
paramsVar = 'params';
|
|
var args = <String>[];
|
|
var optionalArgs = <String>[];
|
|
for (var field in params.fields) {
|
|
if (field.optional) {
|
|
optionalArgs.add('${field.name}: ${field.name}');
|
|
} else {
|
|
args.add(field.name);
|
|
}
|
|
}
|
|
args.addAll(optionalArgs);
|
|
writeln(
|
|
'var params = $requestClass(${args.join(', ')}).toJson(clientUriConverter: uriConverter);',
|
|
);
|
|
}
|
|
var methodJson = "'${request.longMethod}'";
|
|
writeln('var result = await server.send($methodJson, $paramsVar);');
|
|
if (resultClass != null) {
|
|
var kind = 'null';
|
|
if (requestClass == 'EditGetRefactoringParams') {
|
|
kind = 'kind';
|
|
}
|
|
writeln('var decoder = ResponseDecoder($kind);');
|
|
writeln(
|
|
"return $resultClass.fromJson(decoder, 'result', result, clientUriConverter: uriConverter);",
|
|
);
|
|
} else {
|
|
writeln('outOfTestExpect(result, isNull);');
|
|
}
|
|
});
|
|
writeln('}');
|
|
}
|
|
}
|