Files
Danny Tuppeny 336ad52179 [analysis_server] Fix handling of default values in unanswered Interactive Forms
This fixes a bug where an interactive form where all fields have defaults would be considered complete immediately, so we'd never present the fields to the client.

It also includes some minor refactoring extracted from a future CL that implements command/resolve and supports Interactive Forms in refactors in an attempt to reduce the size of that change to aid reviewing.

Change-Id: I176fe25dbb0b610d69617fa04562b0d3ce571642
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/508220
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Samuel Rawlins <srawlins@google.com>
2026-06-01 12:43:19 -07:00

336 lines
10 KiB
Dart

// Copyright (c) 2016, 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' show jsonDecode, jsonEncode;
import 'dart:io';
import 'package:analysis_server/lsp_protocol/protocol.dart' as lsp;
import 'package:analysis_server/src/lsp/client_capabilities.dart';
import 'package:analysis_server/src/protocol_server.dart';
import 'package:analyzer/instrumentation/instrumentation.dart';
import 'package:test/test.dart';
import '../../integration_test/lsp_server/integration_tests.dart';
import '../../integration_test/support/integration_tests.dart';
import '../../test/lsp/server_abstract.dart' show ClientCapabilitiesHelperMixin;
/// A server protocol-agnostic interface to the memory test, allowing the same
/// benchmarks to run for both the original protocol and LSP.
abstract class AbstractBenchmarkTest {
Future<void> get analysisFinished;
Future<void> closeFile(String filePath);
Future<void> complete(String filePath, int offset, {required bool isWarmUp});
void debugStdio();
Future<int> getMemoryUsage();
Future<void> openFile(String filePath, String contents);
Future<void> setUp(String dartSdkPath, List<String> roots);
Future<void> shutdown();
Future<void> updateFile(String filePath, String contents);
}
/// An implementation of [AbstractBenchmarkTest] for an 'original protocol'
/// memory test.
class AnalysisServerBenchmarkTest extends AbstractBenchmarkTest {
final _test = AnalysisServerMemoryUsageTest();
@override
Future<void> get analysisFinished => _test.analysisFinished;
@override
Future<void> closeFile(String filePath) =>
_test.sendAnalysisUpdateContent({filePath: RemoveContentOverlay()});
@override
Future<void> complete(
String filePath,
int offset, {
required bool isWarmUp,
}) async {
await _test.sendCompletionGetSuggestions2(
filePath,
offset,
100,
timeout: isWarmUp ? 60 * 1000 : 0,
);
}
@override
void debugStdio() => _test.debugStdio();
@override
Future<int> getMemoryUsage() => _test.getMemoryUsage();
@override
Future<void> openFile(String filePath, String contents) async {
await _test.sendAnalysisUpdateContent({
filePath: AddContentOverlay(contents),
});
await _test.sendAnalysisSetPriorityFiles([filePath]);
}
@override
Future<void> setUp(String dartSdkPath, List<String> roots) async {
_test.dartSdkPath = dartSdkPath;
await _test.setUp();
await _test.subscribeToStatusNotifications();
await _test.sendAnalysisSetAnalysisRoots(roots, []);
}
@override
Future<void> shutdown() => _test.shutdown();
@override
Future<void> updateFile(String filePath, String contents) =>
_test.sendAnalysisUpdateContent({filePath: AddContentOverlay(contents)});
}
/// Base class for analysis server memory usage tests.
class AnalysisServerMemoryUsageTest
extends AbstractAnalysisServerIntegrationTest
with ServerMemoryUsageMixin {
/// Send the server an 'analysis.setAnalysisRoots' command directing it to
/// analyze [sourceDirectory].
Future<void> setAnalysisRoot() =>
sendAnalysisSetAnalysisRoots([sourceDirectory.path], []);
/// The server is automatically started before every test.
@override
Future<void> setUp() async {
_vmServicePort = await ServiceProtocol._findAvailableSocketPort();
onAnalysisErrors.listen((AnalysisErrorsParams params) {
currentAnalysisErrors[params.file] = params.errors;
});
onServerError.listen((ServerErrorParams params) {
// A server error should never happen during an integration test.
fail('${params.message}\n${params.stackTrace}');
});
var serverConnected = Completer<void>();
onServerConnected.listen((_) {
outOfTestExpect(serverConnected.isCompleted, isFalse);
serverConnected.complete();
});
return startServer(servicePort: _vmServicePort).then((_) {
server.listenToOutput(dispatchNotification);
server.exitCode.then((_) {
skipShutdown = true;
});
return serverConnected.future;
});
}
/// After every test, the server is stopped.
Future<void> shutdown() async => await shutdownIfNeeded();
/// Enable [ServerService.STATUS] notifications so that [analysisFinished]
/// can be used.
Future<void> subscribeToStatusNotifications() async {
await sendServerSetSubscriptions([ServerService.STATUS]);
}
}
/// An implementation of [AbstractBenchmarkTest] for an LSP memory test.
class LspAnalysisServerBenchmarkTest extends AbstractBenchmarkTest
with ClientCapabilitiesHelperMixin {
final _test = LspAnalysisServerMemoryUsageTest();
final PrintableLogger _logger = PrintableLogger();
/// Track the file contents so we can easily convert offsets (used in
/// the interface) to Positions required by LSP without having to keep
/// passing in the contents.
final Map<String, String> _fileContents = {};
int _fileVersion = 1;
@override
Future<void> get analysisFinished => _test.initialAnalysis;
@override
Future<void> closeFile(String filePath) {
_fileContents.remove(filePath);
return _test.closeFile(Uri.file(filePath));
}
@override
Future<void> complete(String filePath, int offset, {required bool isWarmUp}) {
var contents = _fileContents[filePath]!;
var position = _test.positionFromOffset(offset, contents);
return _test.getCompletion(Uri.file(filePath), position);
}
@override
void debugStdio() => _logger.debugStdio();
@override
Future<int> getMemoryUsage() => _test.getMemoryUsage();
@override
Future<void> openFile(String filePath, String contents) {
_fileContents[filePath] = contents;
return _test.openFile(
Uri.file(filePath),
contents,
version: _fileVersion++,
);
}
@override
Future<void> setUp(String dartSdkPath, List<String> roots) async {
// Use some reasonable default client capabilities that will activate
// features that will exercise more code that benchmarks should measure
// (such as applyEdit to allow suggestion sets results to be merged in).
setDocumentChangesSupport();
setApplyEditSupport();
setWorkDoneProgressSupport();
setCompletionItemSnippetSupport();
setCompletionItemKinds(
LspClientCapabilities.defaultSupportedCompletionKinds.toList(),
);
_test.dartSdkPath = dartSdkPath;
_test.instrumentationService = InstrumentationLogAdapter(_logger);
await _test.setUp();
_test.projectFolderPath = roots.single;
await _test.initialize();
}
@override
Future<void> shutdown() async {
_test.tearDown();
await _logger.shutdown();
}
@override
Future<void> updateFile(String filePath, String contents) {
_fileContents[filePath] = contents;
return _test.replaceFile(_fileVersion++, Uri.file(filePath), contents);
}
}
/// Base class for LSP analysis server memory usage tests.
class LspAnalysisServerMemoryUsageTest
extends AbstractLspAnalysisServerIntegrationTest
with ServerMemoryUsageMixin {
Map<String, List<lsp.Diagnostic>> currentAnalysisErrors = {};
@override
void expect(Object? actual, Object? matcher, {String? reason}) {
outOfTestExpect(actual, matcher, reason: reason);
}
/// The server is automatically started before every test.
@override
Future<void> setUp() async {
_vmServicePort = await ServiceProtocol._findAvailableSocketPort();
vmArgs.addAll([
'--enable-vm-service=$_vmServicePort',
'-DSILENT_VM_SERVICE=true',
'--disable-service-auth-codes',
'--disable-dart-dev',
'--no-dds',
]);
await super.setUp();
errorNotificationsFromServer.listen((lsp.NotificationMessage error) {
// A server error should never happen during an integration test.
fail('${error.toJson()}');
});
}
/// After every test, the server is stopped.
Future<void> shutdown() async => this.tearDown();
}
mixin ServerMemoryUsageMixin {
late int _vmServicePort;
Future<int> getMemoryUsage() async {
var uri = Uri.parse('ws://127.0.0.1:$_vmServicePort/ws');
var service = await ServiceProtocol.connect(uri);
var vm = await service.call('getVM');
var total = 0;
var isolateGroupsRefs = vm['isolateGroups'] as List<Object?>;
for (var isolateGroupRef
in isolateGroupsRefs.cast<Map<Object?, Object?>>()) {
var isolateGroupMemoryUsage = await service.call(
'getIsolateGroupMemoryUsage',
{'isolateGroupId': isolateGroupRef['id']},
);
var heapUsage = isolateGroupMemoryUsage['heapUsage'] as int;
var externalUsage = isolateGroupMemoryUsage['externalUsage'] as int;
total += heapUsage + externalUsage;
}
await service.dispose();
return total;
}
}
class ServiceProtocol {
final WebSocket socket;
int _id = 0;
final Map<String, Completer<Map<Object?, Object?>>> _completers = {};
new _(this.socket) {
socket.listen(_handleMessage);
}
Future<Map<Object?, Object?>> call(
String method, [
Map<Object?, Object?> args = const {},
]) {
var id = '${++_id}';
var completer = Completer<Map<Object?, Object?>>();
_completers[id] = completer;
var messageMap = <String, dynamic>{
'jsonrpc': '2.0',
'id': id,
'method': method,
'args': args,
'params': args,
};
var message = jsonEncode(messageMap);
socket.add(message);
return completer.future;
}
Future<void> dispose() => socket.close();
void _handleMessage(Object? message) {
if (message is! String) {
return;
}
try {
var json = jsonDecode(message) as Map<Object?, Object?>;
if (json.containsKey('id')) {
var id = json['id'];
_completers[id]?.complete(json['result'] as Map<Object?, Object?>);
_completers.remove(id);
}
} catch (e) {
// ignore
}
}
static Future<ServiceProtocol> connect(Uri uri) async {
var socket = await WebSocket.connect(uri.toString());
return ServiceProtocol._(socket);
}
static Future<int> _findAvailableSocketPort() async {
var socket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
try {
return socket.port;
} finally {
await socket.close();
}
}
}