blob: 77dea52e8eb3528a13d812813225313d1aa0a048 [file] [log] [blame]
// 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: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,
LspNotificationHelpersMixin,
LspEditHelpersMixin,
LspVerifyEditHelpersMixin,
LspAnalysisServerTestMixin {
final List<String> vmArgs = [];
LspServerClient? client;
InstrumentationService? instrumentationService;
final Map<num, 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.map(
(number) => number,
(string) => throw 'String IDs not supported in tests',
);
_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!.map(
(number) => number,
(string) => throw 'String IDs not supported in tests',
);
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()),
)..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();
}
}