blob: 6b05d1a58a885633247706d935364a22dfa41e95 [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:io';
import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
import 'package:analysis_server/src/lsp/channel/lsp_byte_stream_channel.dart';
import 'package:analyzer/instrumentation/instrumentation.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart';
import '../../lsp/server_abstract.dart';
abstract class AbstractLspAnalysisServerIntegrationTest
with ClientCapabilitiesHelperMixin, LspAnalysisServerTestMixin {
final List<String> vmArgs = [];
LspServerClient? client;
InstrumentationService? instrumentationService;
final Map<num, Completer<ResponseMessage>> _completers = {};
LspByteStreamServerChannel get channel => client!.channel!;
@override
Stream<Message> get serverToClient => client!.serverToClient;
/// 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 {
final resp = await sendRequestToServer(request);
final 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);
}
}
void newFile(String path, {String? content}) =>
File(path).writeAsStringSync(content ?? '');
void newFolder(String path) => Directory(path).createSync(recursive: true);
@override
void sendNotificationToServer(NotificationMessage notification) =>
channel.sendNotification(notification);
@override
Future<ResponseMessage> sendRequestToServer(RequestMessage request) {
final completer = Completer<ResponseMessage>();
final 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')
.resolveSymbolicLinksSync();
newFolder(projectFolderPath);
newFolder(join(projectFolderPath, 'lib'));
projectFolderUri = Uri.file(projectFolderPath);
mainFilePath = join(projectFolderPath, 'lib', 'main.dart');
mainFileUri = Uri.file(mainFilePath);
analysisOptionsPath = join(projectFolderPath, 'analysis_options.yaml');
analysisOptionsUri = Uri.file(analysisOptionsPath);
final client = LspServerClient(instrumentationService);
this.client = client;
await client.start(vmArgs: vmArgs);
client.serverToClient.listen((message) {
if (message is ResponseMessage) {
final id = message.id!.map((number) => number,
(string) => throw 'String IDs not supported in tests');
final 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();
}
}
class LspServerClient {
final InstrumentationService? instrumentationService;
Process? _process;
LspByteStreamServerChannel? channel;
final StreamController<Message> _serverToClient =
StreamController<Message>.broadcast();
LspServerClient(this.instrumentationService);
Future<int> get exitCode => _process!.exitCode;
Stream<Message> get serverToClient => _serverToClient.stream;
void close() {
channel?.close();
_process?.kill();
}
/// Find the root directory of the analysis_server package by proceeding
/// upward to the 'test' dir, and then going up one more directory.
String findRoot(String pathname) {
while (!['benchmark', 'test'].contains(basename(pathname))) {
var parent = dirname(pathname);
if (parent.length >= pathname.length) {
throw Exception("Can't find root directory");
}
pathname = parent;
}
return dirname(pathname);
}
Future start({List<String>? vmArgs}) async {
if (_process != null) {
throw Exception('Process already started');
}
var dartBinary = Platform.executable;
var useSnapshot = true;
String serverPath;
if (useSnapshot) {
// Look for snapshots/analysis_server.dart.snapshot.
serverPath = normalize(join(dirname(Platform.resolvedExecutable),
'snapshots', 'analysis_server.dart.snapshot'));
if (!FileSystemEntity.isFileSync(serverPath)) {
// Look for dart-sdk/bin/snapshots/analysis_server.dart.snapshot.
serverPath = normalize(join(dirname(Platform.resolvedExecutable),
'dart-sdk', 'bin', 'snapshots', 'analysis_server.dart.snapshot'));
}
} else {
final rootDir =
findRoot(Platform.script.toFilePath(windows: Platform.isWindows));
serverPath = normalize(join(rootDir, 'bin', 'server.dart'));
}
final arguments = [...?vmArgs, serverPath, '--lsp', '--suppress-analytics'];
final process = await Process.start(dartBinary, arguments);
_process = process;
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) {
final message = String.fromCharCodes(data);
throw 'Analysis Server wrote to stderr:\n\n$message';
});
channel = LspByteStreamServerChannel(process.stdout, process.stdin,
instrumentationService ?? InstrumentationService.NULL_SERVICE)
..listen(_serverToClient.add);
}
}
/// An [InstrumentationLogger] that buffers logs until [debugStdio()] is called.
class PrintableLogger extends InstrumentationLogger {
bool _printLogs = false;
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();
}
}