[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:
Danny Tuppeny 2021-06-13 19:10:42 +00:00 committed by commit-bot@chromium.org
parent a83f35be82
commit d5e92e1c7a
5 changed files with 363 additions and 15 deletions

View file

@ -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);
}

View file

@ -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,

View file

@ -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';
}
}

View 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);
});
}

View file

@ -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;
}
}