mirror of
https://github.com/dart-lang/sdk
synced 2024-09-16 00:39:49 +00:00
[dds] Add expression eval support to DAP
Change-Id: I0c55b4dde12d40467f8243e4b0c0ccc882eb045d Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/203243 Reviewed-by: Ben Konyi <bkonyi@google.com> Commit-Queue: Ben Konyi <bkonyi@google.com>
This commit is contained in:
parent
a83f35be82
commit
d5e92e1c7a
|
@ -23,6 +23,18 @@ import '../protocol_stream.dart';
|
|||
/// client requests 500 items in a variablesRequest for a list.
|
||||
const maxToStringsPerEvaluation = 10;
|
||||
|
||||
/// An expression that evaluates to the exception for the current thread.
|
||||
///
|
||||
/// In order to support some functionality like "Copy Value" in VS Code's
|
||||
/// Scopes/Variables window, each variable must have a valid "evaluateName" (an
|
||||
/// expression that evaluates to it). Since we show exceptions in there we use
|
||||
/// this magic value as an expression that maps to it.
|
||||
///
|
||||
/// This is not intended to be used by the user directly, although if they
|
||||
/// evaluate it as an expression and the current thread has an exception, it
|
||||
/// will work.
|
||||
const threadExceptionExpression = r'$_threadException';
|
||||
|
||||
/// A base DAP Debug Adapter implementation for running and debugging Dart-based
|
||||
/// applications (including Flutter and Tests).
|
||||
///
|
||||
|
@ -268,6 +280,94 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
|
|||
sendResponse();
|
||||
}
|
||||
|
||||
/// evaluateRequest is called by the client to evaluate a string expression.
|
||||
///
|
||||
/// This could come from the user typing into an input (for example VS Code's
|
||||
/// Debug Console), automatic refresh of a Watch window, or called as part of
|
||||
/// an operation like "Copy Value" for an item in the watch/variables window.
|
||||
///
|
||||
/// If execution is not paused, the `frameId` will not be provided.
|
||||
@override
|
||||
Future<void> evaluateRequest(
|
||||
Request request,
|
||||
EvaluateArguments args,
|
||||
void Function(EvaluateResponseBody) sendResponse,
|
||||
) async {
|
||||
final frameId = args.frameId;
|
||||
// TODO(dantup): Special handling for clipboard/watch (see Dart-Code DAP) to
|
||||
// avoid wrapping strings in quotes, etc.
|
||||
|
||||
// If the frameId was supplied, it maps to an ID we provided from stored
|
||||
// data so we need to look up the isolate + frame index for it.
|
||||
ThreadInfo? thread;
|
||||
int? frameIndex;
|
||||
if (frameId != null) {
|
||||
final data = _isolateManager.getStoredData(frameId);
|
||||
if (data != null) {
|
||||
thread = data.thread;
|
||||
frameIndex = (data.data as vm.Frame).index;
|
||||
}
|
||||
}
|
||||
|
||||
if (thread == null || frameIndex == null) {
|
||||
// TODO(dantup): Dart-Code evaluates these in the context of the rootLib
|
||||
// rather than just not supporting it. Consider something similar (or
|
||||
// better here).
|
||||
throw UnimplementedError('Global evaluation not currently supported');
|
||||
}
|
||||
|
||||
// The value in the constant `frameExceptionExpression` is used as a special
|
||||
// expression that evaluates to the exception on the current thread. This
|
||||
// allows us to construct evaluateNames that evaluate to the fields down the
|
||||
// tree to support some of the debugger functionality (for example
|
||||
// "Copy Value", which re-evaluates).
|
||||
final expression = args.expression.trim();
|
||||
final exceptionReference = thread.exceptionReference;
|
||||
final isExceptionExpression = expression == threadExceptionExpression ||
|
||||
expression.startsWith('$threadExceptionExpression.');
|
||||
|
||||
vm.Response? result;
|
||||
if (exceptionReference != null && isExceptionExpression) {
|
||||
result = await _evaluateExceptionExpression(
|
||||
exceptionReference,
|
||||
expression,
|
||||
thread,
|
||||
);
|
||||
} else {
|
||||
result = await vmService?.evaluateInFrame(
|
||||
thread.isolate.id!,
|
||||
frameIndex,
|
||||
expression,
|
||||
disableBreakpoints: true,
|
||||
);
|
||||
}
|
||||
|
||||
if (result is vm.ErrorRef) {
|
||||
throw DebugAdapterException(result.message ?? '<error ref>');
|
||||
} else if (result is vm.Sentinel) {
|
||||
throw DebugAdapterException(result.valueAsString ?? '<collected>');
|
||||
} else if (result is vm.InstanceRef) {
|
||||
final resultString = await _converter.convertVmInstanceRefToDisplayString(
|
||||
thread,
|
||||
result,
|
||||
allowCallingToString: true,
|
||||
);
|
||||
// TODO(dantup): We may need to store `expression` with this data
|
||||
// to allow building nested evaluateNames.
|
||||
final variablesReference =
|
||||
_converter.isSimpleKind(result.kind) ? 0 : thread.storeData(result);
|
||||
|
||||
sendResponse(EvaluateResponseBody(
|
||||
result: resultString,
|
||||
variablesReference: variablesReference,
|
||||
));
|
||||
} else {
|
||||
throw DebugAdapterException(
|
||||
'Unknown evaluation response type: ${result?.runtimeType}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// [initializeRequest] is the first call from the client during
|
||||
/// initialization and allows exchanging capabilities and configuration
|
||||
/// between client and server.
|
||||
|
@ -662,7 +762,7 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
|
|||
// TODO(dantup): evaluateName
|
||||
// should be built taking the parent into account, for ex. if
|
||||
// args.variablesReference == thread.exceptionReference then we need to
|
||||
// use some sythensized variable name like $e.
|
||||
// use some sythensized variable name like `frameExceptionExpression`.
|
||||
variables.addAll(await _converter.convertVmInstanceToVariablesList(
|
||||
thread,
|
||||
object,
|
||||
|
@ -683,6 +783,38 @@ abstract class DartDebugAdapter<T extends DartLaunchRequestArguments>
|
|||
sendResponse(VariablesResponseBody(variables: variables));
|
||||
}
|
||||
|
||||
/// Handles evaluation of an expression that is (or begins with)
|
||||
/// `threadExceptionExpression` which corresponds to the exception at the top
|
||||
/// of [thread].
|
||||
Future<vm.Response?> _evaluateExceptionExpression(
|
||||
int exceptionReference,
|
||||
String expression,
|
||||
ThreadInfo thread,
|
||||
) async {
|
||||
final exception = _isolateManager.getStoredData(exceptionReference)?.data
|
||||
as vm.InstanceRef?;
|
||||
|
||||
if (exception == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (expression == threadExceptionExpression) {
|
||||
return exception;
|
||||
}
|
||||
|
||||
// Strip the prefix off since we'll evaluate against the exception
|
||||
// by its ID.
|
||||
final expressionWithoutExceptionExpression =
|
||||
expression.substring(threadExceptionExpression.length + 1);
|
||||
|
||||
return vmService?.evaluate(
|
||||
thread.isolate.id!,
|
||||
exception.id!,
|
||||
expressionWithoutExceptionExpression,
|
||||
disableBreakpoints: true,
|
||||
);
|
||||
}
|
||||
|
||||
void _handleDebugEvent(vm.Event event) {
|
||||
_isolateManager.handleEvent(event);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
import 'dart:async';
|
||||
|
||||
import 'exceptions.dart';
|
||||
import 'logging.dart';
|
||||
import 'protocol_common.dart';
|
||||
import 'protocol_generated.dart';
|
||||
|
@ -64,6 +65,12 @@ abstract class BaseDebugAdapter<TLaunchArgs extends LaunchRequestArguments> {
|
|||
void Function() sendResponse,
|
||||
);
|
||||
|
||||
Future<void> evaluateRequest(
|
||||
Request request,
|
||||
EvaluateArguments args,
|
||||
void Function(EvaluateResponseBody) sendResponse,
|
||||
);
|
||||
|
||||
/// Calls [handler] for an incoming request, using [fromJson] to parse its
|
||||
/// arguments from the request.
|
||||
///
|
||||
|
@ -114,7 +121,7 @@ abstract class BaseDebugAdapter<TLaunchArgs extends LaunchRequestArguments> {
|
|||
requestSeq: request.seq,
|
||||
seq: _sequence++,
|
||||
command: request.command,
|
||||
message: '$e',
|
||||
message: e is DebugAdapterException ? e.message : '$e',
|
||||
body: '$s',
|
||||
);
|
||||
_channel.sendResponse(response);
|
||||
|
@ -279,6 +286,8 @@ abstract class BaseDebugAdapter<TLaunchArgs extends LaunchRequestArguments> {
|
|||
handle(request, scopesRequest, ScopesArguments.fromJson);
|
||||
} else if (request.command == 'variables') {
|
||||
handle(request, variablesRequest, VariablesArguments.fromJson);
|
||||
} else if (request.command == 'evaluate') {
|
||||
handle(request, evaluateRequest, EvaluateArguments.fromJson);
|
||||
} else {
|
||||
final response = Response(
|
||||
success: false,
|
||||
|
|
|
@ -106,7 +106,7 @@ class ProtocolConverter {
|
|||
final associations = instance.associations;
|
||||
final fields = instance.fields;
|
||||
|
||||
if (_isSimpleKind(instance.kind)) {
|
||||
if (isSimpleKind(instance.kind)) {
|
||||
// For simple kinds, just return a single variable with their value.
|
||||
return [
|
||||
await convertVmResponseToVariable(
|
||||
|
@ -235,7 +235,7 @@ class ProtocolConverter {
|
|||
// For non-simple variables, store them and produce a new reference that
|
||||
// can be used to access their fields/items/associations.
|
||||
final variablesReference =
|
||||
_isSimpleKind(response.kind) ? 0 : thread.storeData(response);
|
||||
isSimpleKind(response.kind) ? 0 : thread.storeData(response);
|
||||
|
||||
return dap.Variable(
|
||||
name: name ?? response.kind.toString(),
|
||||
|
@ -371,6 +371,17 @@ class ProtocolConverter {
|
|||
}
|
||||
}
|
||||
|
||||
/// Whether [kind] is a simple kind, and does not need to be mapped to a variable.
|
||||
bool isSimpleKind(String? kind) {
|
||||
return kind == 'String' ||
|
||||
kind == 'Bool' ||
|
||||
kind == 'Int' ||
|
||||
kind == 'Num' ||
|
||||
kind == 'Double' ||
|
||||
kind == 'Null' ||
|
||||
kind == 'Closure';
|
||||
}
|
||||
|
||||
/// Invokes the toString() method on a [vm.InstanceRef] and converts the
|
||||
/// response to a user-friendly display string.
|
||||
///
|
||||
|
@ -434,15 +445,4 @@ class ProtocolConverter {
|
|||
|
||||
return getterNames;
|
||||
}
|
||||
|
||||
/// Whether [kind] is a simple kind, and does not need to be mapped to a variable.
|
||||
bool _isSimpleKind(String? kind) {
|
||||
return kind == 'String' ||
|
||||
kind == 'Bool' ||
|
||||
kind == 'Int' ||
|
||||
kind == 'Num' ||
|
||||
kind == 'Double' ||
|
||||
kind == 'Null' ||
|
||||
kind == 'Closure';
|
||||
}
|
||||
}
|
||||
|
|
159
pkg/dds/test/dap/integration/debug_eval_test.dart
Normal file
159
pkg/dds/test/dap/integration/debug_eval_test.dart
Normal file
|
@ -0,0 +1,159 @@
|
|||
// Copyright (c) 2021, 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 'package:dds/src/dap/adapters/dart.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'test_client.dart';
|
||||
import 'test_support.dart';
|
||||
|
||||
main() {
|
||||
testDap((dap) async {
|
||||
group('debug mode evaluation', () {
|
||||
test('evaluates expressions with simple results', () async {
|
||||
final client = dap.client;
|
||||
final testFile = await dap.createTestFile(r'''
|
||||
void main(List<String> args) {
|
||||
var a = 1;
|
||||
var b = 2;
|
||||
var c = 'test';
|
||||
print('Hello!'); // BREAKPOINT
|
||||
}''');
|
||||
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
|
||||
|
||||
final stop = await client.hitBreakpoint(testFile, breakpointLine);
|
||||
await client.expectTopFrameEvalResult(stop.threadId!, 'a', '1');
|
||||
await client.expectTopFrameEvalResult(stop.threadId!, 'a * b', '2');
|
||||
await client.expectTopFrameEvalResult(stop.threadId!, 'c', '"test"');
|
||||
});
|
||||
|
||||
test('evaluates expressions with complex results', () async {
|
||||
final client = dap.client;
|
||||
final testFile = await dap.createTestFile(r'''
|
||||
void main(List<String> args) {
|
||||
print('Hello!'); // BREAKPOINT
|
||||
}''');
|
||||
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
|
||||
|
||||
final stop = await client.hitBreakpoint(testFile, breakpointLine);
|
||||
final result = await client.expectTopFrameEvalResult(
|
||||
stop.threadId!,
|
||||
'DateTime(2000, 1, 1)',
|
||||
'DateTime',
|
||||
);
|
||||
|
||||
// Check we got a variablesReference that maps on to the fields.
|
||||
expect(result.variablesReference, greaterThan(0));
|
||||
await client.expectVariables(
|
||||
result.variablesReference,
|
||||
'''
|
||||
isUtc: false
|
||||
''',
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'evaluates complex expressions expressions with evaluateToStringInDebugViews=true',
|
||||
() async {
|
||||
final client = dap.client;
|
||||
final testFile = await dap.createTestFile(r'''
|
||||
void main(List<String> args) {
|
||||
print('Hello!'); // BREAKPOINT
|
||||
}''');
|
||||
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
|
||||
|
||||
final stop = await client.hitBreakpoint(
|
||||
testFile,
|
||||
breakpointLine,
|
||||
launch: () =>
|
||||
client.launch(testFile.path, evaluateToStringInDebugViews: true),
|
||||
);
|
||||
|
||||
await client.expectTopFrameEvalResult(
|
||||
stop.threadId!,
|
||||
'DateTime(2000, 1, 1)',
|
||||
'DateTime (2000-01-01 00:00:00.000)',
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'evaluates $threadExceptionExpression to the threads exception (simple type)',
|
||||
() async {
|
||||
final client = dap.client;
|
||||
final testFile = await dap.createTestFile(r'''
|
||||
void main(List<String> args) {
|
||||
throw 'my error';
|
||||
}''');
|
||||
|
||||
final stop = await client.hitException(testFile);
|
||||
|
||||
final result = await client.expectTopFrameEvalResult(
|
||||
stop.threadId!,
|
||||
threadExceptionExpression,
|
||||
'"my error"',
|
||||
);
|
||||
expect(result.variablesReference, equals(0));
|
||||
});
|
||||
|
||||
test(
|
||||
'evaluates $threadExceptionExpression to the threads exception (complex type)',
|
||||
() async {
|
||||
final client = dap.client;
|
||||
final testFile = await dap.createTestFile(r'''
|
||||
void main(List<String> args) {
|
||||
throw Exception('my error');
|
||||
}''');
|
||||
|
||||
final stop = await client.hitException(testFile);
|
||||
final result = await client.expectTopFrameEvalResult(
|
||||
stop.threadId!,
|
||||
threadExceptionExpression,
|
||||
'_Exception',
|
||||
);
|
||||
expect(result.variablesReference, greaterThan(0));
|
||||
});
|
||||
|
||||
test(
|
||||
'evaluates $threadExceptionExpression.x.y to x.y on the threads exception',
|
||||
() async {
|
||||
final client = dap.client;
|
||||
final testFile = await dap.createTestFile(r'''
|
||||
void main(List<String> args) {
|
||||
throw Exception('12345');
|
||||
}
|
||||
''');
|
||||
|
||||
final stop = await client.hitException(testFile);
|
||||
await client.expectTopFrameEvalResult(
|
||||
stop.threadId!,
|
||||
'$threadExceptionExpression.message.length',
|
||||
'5',
|
||||
);
|
||||
});
|
||||
|
||||
test('can evaluate expressions in non-top frames', () async {
|
||||
final client = dap.client;
|
||||
final testFile = await dap.createTestFile(r'''
|
||||
void main(List<String> args) {
|
||||
var a = 999;
|
||||
foo();
|
||||
}
|
||||
|
||||
void foo() {
|
||||
var a = 111; // BREAKPOINT
|
||||
}''');
|
||||
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
|
||||
|
||||
final stop = await client.hitBreakpoint(testFile, breakpointLine);
|
||||
final stack = await client.getValidStack(stop.threadId!,
|
||||
startFrame: 0, numFrames: 2);
|
||||
final secondFrameId = stack.stackFrames[1].id;
|
||||
|
||||
await client.expectEvalResult(secondFrameId, 'a', '999');
|
||||
});
|
||||
|
||||
// These tests can be slow due to starting up the external server process.
|
||||
}, timeout: Timeout.none);
|
||||
});
|
||||
}
|
|
@ -79,6 +79,23 @@ class DapTestClient {
|
|||
|
||||
Future<Response> disconnect() => sendRequest(DisconnectArguments());
|
||||
|
||||
/// Sends an evaluate request for the given [expression], optionally for a
|
||||
/// specific [frameId].
|
||||
///
|
||||
/// Returns a Future that completes when the server returns a corresponding
|
||||
/// response.
|
||||
Future<Response> evaluate(
|
||||
String expression, {
|
||||
int? frameId,
|
||||
String? context,
|
||||
}) {
|
||||
return sendRequest(EvaluateArguments(
|
||||
expression: expression,
|
||||
frameId: frameId,
|
||||
context: context,
|
||||
));
|
||||
}
|
||||
|
||||
/// Returns a Future that completes with the next [event] event.
|
||||
Future<Event> event(String event) => _logIfSlow(
|
||||
'Event "$event"',
|
||||
|
@ -549,4 +566,35 @@ extension DapTestClientExtension on DapTestClient {
|
|||
|
||||
return variables;
|
||||
}
|
||||
|
||||
/// Evalutes [expression] in the top frame of thread [threadId] and expects a
|
||||
/// specific [expectedResult].
|
||||
Future<EvaluateResponseBody> expectTopFrameEvalResult(
|
||||
int threadId,
|
||||
String expression,
|
||||
String expectedResult,
|
||||
) async {
|
||||
final stack = await getValidStack(threadId, startFrame: 0, numFrames: 1);
|
||||
final topFrameId = stack.stackFrames.first.id;
|
||||
|
||||
return expectEvalResult(topFrameId, expression, expectedResult);
|
||||
}
|
||||
|
||||
/// Evalutes [expression] in frame [frameId] and expects a specific
|
||||
/// [expectedResult].
|
||||
Future<EvaluateResponseBody> expectEvalResult(
|
||||
int frameId,
|
||||
String expression,
|
||||
String expectedResult,
|
||||
) async {
|
||||
final response = await evaluate(expression, frameId: frameId);
|
||||
expect(response.success, isTrue);
|
||||
expect(response.command, equals('evaluate'));
|
||||
final body =
|
||||
EvaluateResponseBody.fromJson(response.body as Map<String, Object?>);
|
||||
|
||||
expect(body.result, equals(expectedResult));
|
||||
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue