// 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 vmArgs = []; LspServerClient? client; InstrumentationService? instrumentationService; final Map> _completers = {}; LspByteStreamServerChannel get channel => client!.channel!; @override Stream 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 expectSuccessfulResponseTo( 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); } } 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 sendRequestToServer(RequestMessage request) { final completer = Completer(); 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 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 _serverToClient = StreamController.broadcast(); LspServerClient(this.instrumentationService); Future get exitCode => _process!.exitCode; Stream 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? 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 shutdown() async { _printLogs = false; _buffer.clear(); } }