2b88f4fea8
My previous CL made some LSP tests run with String IDs instead of ints, but those tests don't appear to go through the session logger so did not fail with the casts here. This change adds a base integration test that also uses string IDs, which did fail on the cast, so I've updated the session logger to use `Either2<int, String>` for IDs instead. Fixes https://github.com/dart-lang/sdk/issues/62442 Change-Id: Iee582e9ce2b8b5a1127120c987670a679d2ca76c Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/473260 Commit-Queue: Brian Wilkerson <brianwilkerson@google.com> Reviewed-by: Jake Macdonald <jakemac@google.com> Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
305 lines
9.3 KiB
Dart
305 lines
9.3 KiB
Dart
// Copyright (c) 2019, 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';
|
|
|
|
import 'package:analysis_server/lsp_protocol/protocol.dart';
|
|
import 'package:analysis_server/src/lsp/channel/lsp_byte_stream_channel.dart';
|
|
import 'package:analysis_server/src/services/pub/pub_command.dart';
|
|
import 'package:analysis_server/src/session_logger/session_logger.dart';
|
|
import 'package:analyzer/file_system/physical_file_system.dart';
|
|
import 'package:analyzer/instrumentation/instrumentation.dart';
|
|
import 'package:analyzer_plugin/src/utilities/client_uri_converter.dart';
|
|
import 'package:meta/meta.dart';
|
|
import 'package:path/path.dart' as path;
|
|
|
|
import '../../test/constants.dart';
|
|
import '../../test/lsp/request_helpers_mixin.dart';
|
|
import '../../test/lsp/server_abstract.dart';
|
|
import '../../test/support/sdk_paths.dart';
|
|
|
|
abstract class AbstractLspAnalysisServerIntegrationTest
|
|
with
|
|
ClientCapabilitiesHelperMixin,
|
|
LspRequestHelpersMixin,
|
|
LspReverseRequestHelpersMixin,
|
|
LspEditHelpersMixin,
|
|
LspVerifyEditHelpersMixin,
|
|
LspAnalysisServerTestMixin {
|
|
final List<String> vmArgs = [];
|
|
LspServerClient? client;
|
|
InstrumentationService? instrumentationService;
|
|
final Map<Either2<int, String>, Completer<ResponseMessage>> _completers = {};
|
|
String dartSdkPath = path.dirname(path.dirname(Platform.resolvedExecutable));
|
|
|
|
@override
|
|
late final ClientUriConverter uriConverter = ClientUriConverter.noop(
|
|
pathContext,
|
|
);
|
|
|
|
/// Tracks the current overlay content so that when we apply edits they can
|
|
/// be applied in the same way a real client would apply them.
|
|
final _overlayContent = <Uri, String>{};
|
|
|
|
/// Temporary folders created by the test that should be deleted (recursively)
|
|
/// during [tearDown].
|
|
final List<String> _temporaryFolders = [];
|
|
|
|
LspByteStreamServerChannel get channel => client!.channel!;
|
|
|
|
@override
|
|
path.Context get pathContext => PhysicalResourceProvider.INSTANCE.pathContext;
|
|
|
|
@override
|
|
Stream<Message> get serverToClient => client!.serverToClient;
|
|
|
|
@override
|
|
Future<void> closeFile(Uri uri) {
|
|
_overlayContent.remove(uri);
|
|
return super.closeFile(uri);
|
|
}
|
|
|
|
/// Sends a request to the server and unwraps the result. Throws if the
|
|
/// response was not successful or returned an error.
|
|
@override
|
|
Future<T> expectSuccessfulResponseTo<T, R>(
|
|
RequestMessage request,
|
|
T Function(R) fromJson,
|
|
) async {
|
|
var resp = await sendRequestToServer(request);
|
|
var error = resp.error;
|
|
if (error != null) {
|
|
throw error;
|
|
} else if (T == Null) {
|
|
return resp.result == null
|
|
? null as T
|
|
: throw 'Expected Null response but got ${resp.result}';
|
|
} else {
|
|
return fromJson(resp.result as R);
|
|
}
|
|
}
|
|
|
|
@override
|
|
String? getCurrentFileContent(Uri uri) {
|
|
// First try and overlay the test has set.
|
|
if (_overlayContent.containsKey(uri)) {
|
|
return _overlayContent[uri];
|
|
}
|
|
|
|
// Otherwise fall back to the disk.
|
|
try {
|
|
return File(uri.toFilePath()).readAsStringSync();
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
void newFile(String path, String content) =>
|
|
File(path).writeAsStringSync(content);
|
|
|
|
void newFolder(String path) => Directory(path).createSync(recursive: true);
|
|
|
|
@override
|
|
Future<void> openFile(Uri uri, String content, {int version = 1}) {
|
|
_overlayContent[uri] = content;
|
|
return super.openFile(uri, content, version: version);
|
|
}
|
|
|
|
@override
|
|
Future<void> replaceFile(int newVersion, Uri uri, String content) {
|
|
_overlayContent[uri] = content;
|
|
return super.replaceFile(newVersion, uri, content);
|
|
}
|
|
|
|
@override
|
|
void sendNotificationToServer(NotificationMessage notification) =>
|
|
channel.sendNotification(notification);
|
|
|
|
@override
|
|
Future<ResponseMessage> sendRequestToServer(RequestMessage request) {
|
|
var completer = Completer<ResponseMessage>();
|
|
var id = request.id;
|
|
_completers[id] = completer;
|
|
|
|
channel.sendRequest(request);
|
|
|
|
return completer.future;
|
|
}
|
|
|
|
@override
|
|
void sendResponseToServer(ResponseMessage response) =>
|
|
channel.sendResponse(response);
|
|
|
|
@mustCallSuper
|
|
Future<void> setUp() async {
|
|
// Set up temporary folder for the test.
|
|
projectFolderPath = Directory.systemTemp
|
|
.createTempSync('analysisServer_test_integration_lspProject')
|
|
.resolveSymbolicLinksSync();
|
|
_temporaryFolders.add(projectFolderPath);
|
|
newFolder(projectFolderPath);
|
|
newFolder(path.join(projectFolderPath, 'lib'));
|
|
mainFilePath = path.join(projectFolderPath, 'lib', 'main.dart');
|
|
analysisOptionsPath = path.join(projectFolderPath, 'analysis_options.yaml');
|
|
|
|
var client = LspServerClient(instrumentationService);
|
|
this.client = client;
|
|
await client.start(dartSdkPath: dartSdkPath, vmArgs: vmArgs);
|
|
client.serverToClient.listen((message) {
|
|
if (message is ResponseMessage) {
|
|
var id = message.id;
|
|
var completer = _completers[id];
|
|
if (completer == null) {
|
|
throw 'Response with ID $id was unexpected';
|
|
} else {
|
|
_completers.remove(id);
|
|
completer.complete(message);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void tearDown() {
|
|
// TODO(dantup): Graceful shutdown?
|
|
client?.close();
|
|
for (var temporaryFolder in _temporaryFolders) {
|
|
Directory(temporaryFolder).deleteSync(recursive: true);
|
|
}
|
|
}
|
|
}
|
|
|
|
class LspServerClient {
|
|
final InstrumentationService? instrumentationService;
|
|
Process? _process;
|
|
LspByteStreamServerChannel? channel;
|
|
final StreamController<Message> _serverToClient =
|
|
StreamController<Message>.broadcast();
|
|
|
|
/// Whether the first line of output from the server was already checked.
|
|
bool _firstLineChecked = false;
|
|
|
|
/// If the first line of output turned out to be the DevTools URI line,
|
|
/// these whole line is used for this completer.
|
|
final Completer<String> _devToolsLineCompleter = Completer<String>();
|
|
|
|
LspServerClient(this.instrumentationService);
|
|
|
|
/// Completes with the DevTools URI line, maybe never.
|
|
Future<String> get devToolsLine => _devToolsLineCompleter.future;
|
|
|
|
Future<int> get exitCode => _process!.exitCode;
|
|
|
|
path.Context get pathContext => PhysicalResourceProvider.INSTANCE.pathContext;
|
|
|
|
Stream<Message> get serverToClient => _serverToClient.stream;
|
|
|
|
void close() {
|
|
channel?.close();
|
|
_process?.kill();
|
|
}
|
|
|
|
Future<void> start({
|
|
required String dartSdkPath,
|
|
List<String>? vmArgs,
|
|
}) async {
|
|
if (_process != null) {
|
|
throw Exception('Process already started');
|
|
}
|
|
|
|
var dartBinary = path.join(dartSdkPath, 'bin', 'dart');
|
|
var serverPath = await getAnalysisServerPath(dartSdkPath);
|
|
|
|
var arguments = [...?vmArgs, serverPath, '--lsp', '--suppress-analytics'];
|
|
var process = await Process.start(
|
|
dartBinary,
|
|
arguments,
|
|
environment: {PubCommand.disablePubCommandEnvironmentKey: 'true'},
|
|
);
|
|
_process = process;
|
|
unawaited(
|
|
process.exitCode.then((int code) {
|
|
if (code != 0) {
|
|
// TODO(dantup): Log/fail tests...
|
|
}
|
|
}),
|
|
);
|
|
|
|
// If the server writes to stderr, fail tests with a more useful message
|
|
// (rather than having the test just hang waiting for a response).
|
|
process.stderr.listen((data) {
|
|
var message = String.fromCharCodes(data);
|
|
throw 'Analysis Server wrote to stderr:\n\n$message';
|
|
});
|
|
|
|
var inputStream = _extractDevToolsLine(process.stdout);
|
|
var outputStream = process.stdin;
|
|
|
|
channel = LspByteStreamServerChannel(
|
|
inputStream,
|
|
outputStream,
|
|
instrumentationService ?? InstrumentationLogAdapter(PrintableLogger()),
|
|
sessionLogger: SessionLogger(),
|
|
)..listen(_serverToClient.add);
|
|
}
|
|
|
|
/// Checks the first line in the [input], and if it is the DevTools URI
|
|
/// line, completes [devToolsLine] with it, and excludes it from the sink.
|
|
/// Otherwise, and after the first line, passes data to the sink.
|
|
Stream<List<int>> _extractDevToolsLine(Stream<List<int>> input) {
|
|
var buffer = <int>[];
|
|
return input.transform(
|
|
StreamTransformer.fromHandlers(
|
|
handleData: (bytes, sink) {
|
|
if (_firstLineChecked) {
|
|
sink.add(bytes);
|
|
} else {
|
|
buffer.addAll(bytes);
|
|
var lineFeedIndex = buffer.indexOf(0x0A);
|
|
if (lineFeedIndex != -1) {
|
|
_firstLineChecked = true;
|
|
var lineBytes = buffer.sublist(0, lineFeedIndex);
|
|
var line = utf8.decode(lineBytes);
|
|
if (line.startsWith('The Dart DevTools')) {
|
|
_devToolsLineCompleter.complete(line);
|
|
sink.add(buffer.sublist(lineFeedIndex + 1));
|
|
} else {
|
|
sink.add(buffer);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// An [InstrumentationLogger] that buffers logs until [debugStdio()] is called.
|
|
class PrintableLogger extends InstrumentationLogger {
|
|
bool _printLogs = debugPrintCommunication;
|
|
final _buffer = StringBuffer();
|
|
|
|
void debugStdio() {
|
|
print(_buffer.toString());
|
|
_buffer.clear();
|
|
_printLogs = true;
|
|
}
|
|
|
|
@override
|
|
void log(String message) {
|
|
if (_printLogs) {
|
|
print(message);
|
|
} else {
|
|
_buffer.writeln(message);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> shutdown() async {
|
|
_printLogs = false;
|
|
_buffer.clear();
|
|
}
|
|
}
|