blob: 4e235242fa7009e64e3b159a3329b7d37357158e [file] [log] [blame]
// Copyright (c) 2022, 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 'dart:convert';
import 'dart:io';
Future<void> main(List<String> args) async {
print('''
================================================================================
Stress test tool for language-server protocol.
Example run:
out/ReleaseX64/dart-sdk/bin/dart \\
pkg/analysis_server/tool/lspTestWithParameters.dart \\
--root=pkg/analysis_server \\
--sdk=out/ReleaseX64/dart-sdk/ \\
--click=pkg/analyzer/lib/src/dart/analysis/driver.dart \\
--line=506 \\
--column=20
Additional options:
-v / --verbose Be more verbose. Specify several times for more verbosity.
--verbosity=<int> Set verbosity directly. Defaults to 0. A higher number is
more verbose.
--every=<int> Set how often - in ms - to fire an event. Defaults to 100.
================================================================================
''');
{
Uri exe = Uri.base.resolve(Platform.resolvedExecutable);
Uri librariesDart =
exe.resolve('../lib/_internal/sdk_library_metadata/lib/libraries.dart');
if (!File.fromUri(librariesDart).existsSync()) {
throw 'Execute with a dart that has '
"'../lib/_internal/sdk_library_metadata/lib/libraries.dart' "
'available (e.g. out/ReleaseX64/dart-sdk/bin/dart)';
}
}
Uri? rootUri;
Uri? sdkUri;
Uri? clickOnUri;
int? clickLine;
int? clickColumn;
int everyMs = 100;
for (String arg in args) {
if (arg.startsWith('--root=')) {
rootUri = Uri.base.resolve(arg.substring('--root='.length).trim());
} else if (arg.startsWith('--sdk=')) {
sdkUri = Uri.base.resolve(arg.substring('--sdk='.length).trim());
} else if (arg.startsWith('--click=')) {
clickOnUri = Uri.base.resolve(arg.substring('--click='.length).trim());
} else if (arg.startsWith('--line=')) {
clickLine = int.parse(arg.substring('--line='.length).trim());
} else if (arg.startsWith('--column=')) {
clickColumn = int.parse(arg.substring('--column='.length).trim());
} else if (arg.startsWith('--every=')) {
everyMs = int.parse(arg.substring('--every='.length).trim());
} else if (arg == '--verbose' || arg == '-v') {
verbosity++;
} else if (arg.startsWith('--verbosity=')) {
verbosity = int.parse(arg.substring('--verbosity='.length).trim());
} else {
throw 'Unknown argument: $arg';
}
}
if (rootUri == null) {
throw "Didn't get a root uri. Specify with --root=";
}
if (!Directory.fromUri(rootUri).existsSync()) {
throw "Directory $rootUri doesn't exist. "
'Specify existing directory with --root=';
}
if (sdkUri == null) {
throw "Didn't get a sdk path. Specify with --sdk=";
}
if (!Directory.fromUri(sdkUri).existsSync()) {
throw "Directory $sdkUri doesn't exist. "
'Specify existing directory with --sdk=';
}
if (clickOnUri == null) {
throw "Didn't get a sdk path. Specify with --click=";
}
if (!File.fromUri(clickOnUri).existsSync()) {
throw "File $clickOnUri doesn't exist. "
'Specify existing file with --click=';
}
if (clickLine == null) {
throw "Didn't get a line to click on. Specify with --line=";
}
if (clickColumn == null) {
throw "Didn't get a column to click on. Specify with --column=";
}
Process p = await Process.start(Platform.resolvedExecutable, [
'language-server',
]);
p.stdout.listen(listenToStdout);
Timer.periodic(const Duration(seconds: 1), (timer) {
bool reportedSomething = false;
for (MapEntry<int, Stopwatch> waitingFor
in outstandingRequestsWithId.entries) {
if (waitingFor.value.elapsed > const Duration(seconds: 1)) {
if (!reportedSomething) {
print('----');
reportedSomething = true;
}
print('==> Has been waiting for ${waitingFor.key} for '
'${waitingFor.value.elapsed}');
}
}
if (reportedSomething) {
print('----');
} else {
print(' -- not waiting for anything -- ');
}
});
await send(p, initMessage(pid, rootUri));
await receivedCompleter.future;
await send(p, initNotification);
await receivedCompleter.future;
await send(p, initMore(sdkUri));
await receivedCompleter.future;
// Try to let it get done...
await Future.delayed(const Duration(seconds: 2));
Duration everyDuration = Duration(milliseconds: everyMs);
while (true) {
await send(
p,
gotoDef(
largestIdSeen + 1,
clickOnUri,
clickLine,
clickColumn,
));
await Future.delayed(everyDuration);
}
}
final buffer = <int>[];
int? headerContentLength;
Map<String, dynamic> initNotification = {
'jsonrpc': '2.0',
'method': 'initialized',
'params': {}
};
/// There's something weird about getting (several) id 3's that wasn't
/// requested...
int largestIdSeen = 3;
Map<int, Stopwatch> outstandingRequestsWithId = {};
Completer<Map<String, dynamic>> receivedCompleter = Completer();
int verbosity = 0;
Map<String, dynamic> gotoDef(int id, Uri uri, int line, int char) {
return {
'jsonrpc': '2.0',
'id': id,
'method': 'textDocument/definition',
'params': {
'textDocument': {'uri': '$uri'},
'position': {'line': line, 'character': char}
}
};
}
// Messages taken from what VSCode sent.
Map<String, dynamic> initMessage(int processId, Uri rootUri) {
String rootPath = rootUri.toFilePath();
String name = rootUri.pathSegments.last;
if (name.isEmpty) {
name = rootUri.pathSegments[rootUri.pathSegments.length - 2];
}
return {
'id': 0,
'jsonrpc': '2.0',
'method': 'initialize',
'params': {
'processId': processId,
'clientInfo': {'name': 'lspTestScript', 'version': '0.0.1'},
'locale': 'en',
'rootPath': rootPath,
'rootUri': '$rootUri',
'capabilities': {},
'initializationOptions': {},
'workspaceFolders': [
{'uri': '$rootUri', 'name': rootUri.pathSegments.last}
]
}
};
}
Map<String, dynamic> initMore(Uri sdkUri) {
String sdkPath = sdkUri.toFilePath();
return {
'id': 1,
'jsonrpc': '2.0',
'result': [
{
'useLsp': true,
'sdkPath': sdkPath,
'allowAnalytics': false,
}
]
};
}
void listenToStdout(List<int> event) {
// General idea taken from
// pkg/analysis_server/lib/src/lsp/lsp_packet_transformer.dart
for (int element in event) {
buffer.add(element);
if (verbosity > 3 && buffer.length % 1000 == 999) {
print('DEBUG MESSAGE: Stdout buffer with length ${buffer.length} so far: '
'${utf8.decode(buffer)}');
}
if (headerContentLength == null && _endsWithCrLfCrLf()) {
String headerRaw = utf8.decode(buffer);
buffer.clear();
List<String> headers = headerRaw.split('\r\n');
for (String header in headers) {
if (header.startsWith('Content-Length:')) {
String contentLength =
header.substring('Content-Length:'.length).trim();
headerContentLength = int.parse(contentLength);
break;
}
}
} else if (headerContentLength != null &&
buffer.length == headerContentLength!) {
String messageString = utf8.decode(buffer);
buffer.clear();
headerContentLength = null;
Map<String, dynamic> message =
json.decode(messageString) as Map<String, dynamic>;
dynamic possibleId = message['id'];
if (possibleId is int) {
if (possibleId > largestIdSeen) {
largestIdSeen = possibleId;
}
if (verbosity > 0) {
if (messageString.length > 100) {
print('Got message ${messageString.substring(0, 100)}...');
} else {
print('Got message $messageString');
}
}
Stopwatch? stopwatch = outstandingRequestsWithId.remove(possibleId);
if (stopwatch != null) {
stopwatch.stop();
if (verbosity > 2) {
print(' => Got response for $possibleId in ${stopwatch.elapsed}');
}
}
} else if (verbosity > 1) {
if (messageString.length > 100) {
print('Got message ${messageString.substring(0, 100)}...');
} else {
print('Got message $messageString');
}
}
receivedCompleter.complete(message);
receivedCompleter = Completer();
}
}
}
Future<void> send(Process p, Map<String, dynamic> json) async {
// Mostly copied from
// pkg/analysis_server/lib/src/lsp/channel/lsp_byte_stream_channel.dart
var jsonEncodedBody = jsonEncode(json);
var utf8EncodedBody = utf8.encode(jsonEncodedBody);
var header = 'Content-Length: ${utf8EncodedBody.length}\r\n'
'Content-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n';
var asciiEncodedHeader = ascii.encode(header);
dynamic possibleId = json['id'];
if (possibleId is int && possibleId > largestIdSeen) {
largestIdSeen = possibleId;
outstandingRequestsWithId[possibleId] = Stopwatch()..start();
if (verbosity > 2) {
print('Sending message with id $possibleId');
}
}
// Header is always ascii, body is always utf8!
p.stdin.add(asciiEncodedHeader);
p.stdin.add(utf8EncodedBody);
await p.stdin.flush();
if (verbosity > 2) {
print('\n\nMessage sent...\n\n');
}
}
/// Copied from pkg/analysis_server/lib/src/lsp/lsp_packet_transformer.dart.
bool _endsWithCrLfCrLf() {
var l = buffer.length;
return l > 4 &&
buffer[l - 1] == 10 &&
buffer[l - 2] == 13 &&
buffer[l - 3] == 10 &&
buffer[l - 4] == 13;
}