Version 2.15.0-30.0.dev
Merge commit '30a3b9c716273b4200eb2687d975fa50c1388d9e' into 'dev'
diff --git a/pkg/dds/lib/src/dap/adapters/dart.dart b/pkg/dds/lib/src/dap/adapters/dart.dart
index fc56f09..ed80060 100644
--- a/pkg/dds/lib/src/dap/adapters/dart.dart
+++ b/pkg/dds/lib/src/dap/adapters/dart.dart
@@ -48,6 +48,15 @@
/// will work.
const threadExceptionExpression = r'$_threadException';
+/// Pattern for extracting useful error messages from an evaluation exception.
+final _evalErrorMessagePattern = RegExp('Error: (.*)');
+
+/// Pattern for extracting useful error messages from an unhandled exception.
+final _exceptionMessagePattern = RegExp('Unhandled exception:\n(.*)');
+
+/// Pattern for a trailing semicolon.
+final _trailingSemicolonPattern = RegExp(r';$');
+
/// A base DAP Debug Adapter implementation for running and debugging Dart-based
/// applications (including Flutter and Tests).
///
@@ -170,6 +179,8 @@
/// VM Service closing).
bool _hasSentTerminatedEvent = false;
+ late final sendLogsToClient = args.sendLogsToClient ?? false;
+
DartDebugAdapter(
ByteStreamServerChannel channel, {
this.ipv6 = false,
@@ -299,8 +310,7 @@
logger?.call('Connecting to debugger at $uri');
sendOutput('console', 'Connecting to VM Service at $uri\n');
- final vmService =
- await _vmServiceConnectUri(uri.toString(), logger: logger);
+ final vmService = await _vmServiceConnectUri(uri.toString());
logger?.call('Connected to debugger at $uri!');
// TODO(dantup): VS Code currently depends on a custom dart.debuggerUris
@@ -492,25 +502,49 @@
// 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 expression = args.expression
+ .trim()
+ // Remove any trailing semicolon as the VM only evaluates expressions
+ // but a user may have highlighted a whole line/statement to send for
+ // evaluation.
+ .replaceFirst(_trailingSemicolonPattern, '');
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,
- );
+ try {
+ if (exceptionReference != null && isExceptionExpression) {
+ result = await _evaluateExceptionExpression(
+ exceptionReference,
+ expression,
+ thread,
+ );
+ } else {
+ result = await vmService?.evaluateInFrame(
+ thread.isolate.id!,
+ frameIndex,
+ expression,
+ disableBreakpoints: true,
+ );
+ }
+ } catch (e) {
+ final rawMessage = '$e';
+
+ // Error messages can be quite verbose and don't fit well into a
+ // single-line watch window. For example:
+ //
+ // evaluateInFrame: (113) Expression compilation error
+ // org-dartlang-debug:synthetic_debug_expression:1:5: Error: A value of type 'String' can't be assigned to a variable of type 'num'.
+ // 1 + "a"
+ // ^
+ //
+ // So in the case of a Watch context, try to extract the useful message.
+ if (args.context == 'watch') {
+ throw DebugAdapterException(extractEvaluationErrorMessage(rawMessage));
+ }
+
+ throw DebugAdapterException(rawMessage);
}
if (result is vm.ErrorRef) {
@@ -542,6 +576,24 @@
}
}
+ /// Tries to extract the useful part from an evaluation exception message.
+ ///
+ /// If no message could be extracted, returns the whole original error.
+ String extractEvaluationErrorMessage(String rawError) {
+ final match = _evalErrorMessagePattern.firstMatch(rawError);
+ final shortError = match != null ? match.group(1)! : null;
+ return shortError ?? rawError;
+ }
+
+ /// Tries to extract the useful part from an unhandled exception message.
+ ///
+ /// If no message could be extracted, returns the whole original error.
+ String extractUnhandledExceptionMessage(String rawError) {
+ final match = _exceptionMessagePattern.firstMatch(rawError);
+ final shortError = match != null ? match.group(1)! : null;
+ return shortError ?? rawError;
+ }
+
/// Sends a [TerminatedEvent] if one has not already been sent.
void handleSessionTerminate() {
if (_hasSentTerminatedEvent) {
@@ -1280,6 +1332,13 @@
}
}
+ void _logTraffic(String data) {
+ logger?.call(data);
+ if (sendLogsToClient) {
+ sendEvent(RawEventBody(data), eventType: 'dart.log');
+ }
+ }
+
/// Performs some setup that is common to both [launchRequest] and
/// [attachRequest].
Future<void> _prepareForLaunchOrAttach() async {
@@ -1319,17 +1378,15 @@
/// A wrapper around the same name function from package:vm_service that
/// allows logging all traffic over the VM Service.
- Future<vm.VmService> _vmServiceConnectUri(
- String wsUri, {
- Logger? logger,
- }) async {
+ Future<vm.VmService> _vmServiceConnectUri(String wsUri) async {
final socket = await WebSocket.connect(wsUri);
final controller = StreamController();
final streamClosedCompleter = Completer();
+ final logger = this.logger;
socket.listen(
(data) {
- logger?.call('<== [VM] $data');
+ _logTraffic('<== [VM] $data');
controller.add(data);
},
onDone: () => streamClosedCompleter.complete(),
@@ -1339,6 +1396,7 @@
controller.stream,
(String message) {
logger?.call('==> [VM] $message');
+ _logTraffic('==> [VM] $message');
socket.add(message);
},
log: logger != null ? VmServiceLogger(logger) : null,
diff --git a/pkg/dds/lib/src/dap/base_debug_adapter.dart b/pkg/dds/lib/src/dap/base_debug_adapter.dart
index 6b15fac..1071fee 100644
--- a/pkg/dds/lib/src/dap/base_debug_adapter.dart
+++ b/pkg/dds/lib/src/dap/base_debug_adapter.dart
@@ -167,10 +167,10 @@
/// Sends an event, lookup up the event type based on the runtimeType of
/// [body].
- void sendEvent(EventBody body) {
+ void sendEvent(EventBody body, {String? eventType}) {
final event = Event(
seq: _sequence++,
- event: eventTypes[body.runtimeType]!,
+ event: eventType ?? eventTypes[body.runtimeType]!,
body: body,
);
_channel.sendEvent(event);
diff --git a/pkg/dds/lib/src/dap/protocol_common.dart b/pkg/dds/lib/src/dap/protocol_common.dart
index f9c09f4..9a20311 100644
--- a/pkg/dds/lib/src/dap/protocol_common.dart
+++ b/pkg/dds/lib/src/dap/protocol_common.dart
@@ -2,12 +2,35 @@
// 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:convert';
+
/// A base class for (spec-generated) classes that represent the `body` of a an
/// event.
abstract class EventBody {
static bool canParse(Object? obj) => obj is Map<String, Object?>?;
}
+/// A generic event body class that just supplies an object directly.
+///
+/// Used to support custom events sent by the debug adapter such as 'dart.log'.
+///
+/// The supplied [body] must be convertable to JSON.
+class RawEventBody extends EventBody {
+ final Object body;
+
+ RawEventBody(this.body)
+ : assert(() {
+ try {
+ jsonEncode(body);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }(), 'body should be JSON encodable');
+
+ Object toJson() => body;
+}
+
/// A generic arguments class that just supplies the arguments map directly.
///
/// Used to support custom requests that may be provided by other implementing
diff --git a/pkg/dds/lib/src/dap/protocol_converter.dart b/pkg/dds/lib/src/dap/protocol_converter.dart
index 2d2f7240..a3e836e 100644
--- a/pkg/dds/lib/src/dap/protocol_converter.dart
+++ b/pkg/dds/lib/src/dap/protocol_converter.dart
@@ -99,7 +99,11 @@
ref,
includeQuotesAroundString: false,
);
- stringValue += ' ($toStringValue)';
+ // Include the toString() result only if it's not the default (which
+ // duplicates the type name we're already showing).
+ if (toStringValue != "Instance of '${ref.classRef?.name}'") {
+ stringValue += ' ($toStringValue)';
+ }
}
return stringValue;
} else if (ref.kind == 'List') {
@@ -220,21 +224,29 @@
/// Helper to evaluate each getter and convert the response to a
/// variable.
Future<dap.Variable> evaluate(int index, String getterName) async {
- final response = await service.evaluate(
- thread.isolate.id!,
- instance.id!,
- getterName,
- );
- // Convert results to variables.
- return convertVmResponseToVariable(
- thread,
- response,
- name: getterName,
- evaluateName:
- _adapter.combineEvaluateName(evaluateName, '.$getterName'),
- allowCallingToString:
- allowCallingToString && index <= maxToStringsPerEvaluation,
- );
+ try {
+ final response = await service.evaluate(
+ thread.isolate.id!,
+ instance.id!,
+ getterName,
+ );
+ // Convert results to variables.
+ return convertVmResponseToVariable(
+ thread,
+ response,
+ name: getterName,
+ evaluateName:
+ _adapter.combineEvaluateName(evaluateName, '.$getterName'),
+ allowCallingToString:
+ allowCallingToString && index <= maxToStringsPerEvaluation,
+ );
+ } catch (e) {
+ return dap.Variable(
+ name: getterName,
+ value: _adapter.extractEvaluationErrorMessage('$e'),
+ variablesReference: 0,
+ );
+ }
}
variables.addAll(await Future.wait(getterNames.mapIndexed(evaluate)));
@@ -309,13 +321,21 @@
);
} else if (response is vm.Sentinel) {
return dap.Variable(
- name: '<sentinel>',
+ name: name ?? '<sentinel>',
value: response.valueAsString.toString(),
variablesReference: 0,
);
+ } else if (response is vm.ErrorRef) {
+ final errorMessage = _adapter
+ .extractUnhandledExceptionMessage(response.message ?? '<error>');
+ return dap.Variable(
+ name: name ?? '<error>',
+ value: '<$errorMessage>',
+ variablesReference: 0,
+ );
} else {
return dap.Variable(
- name: '<error>',
+ name: name ?? '<error>',
value: response.runtimeType.toString(),
variablesReference: 0,
);
diff --git a/pkg/dds/test/dap/integration/debug_breakpoints_test.dart b/pkg/dds/test/dap/integration/debug_breakpoints_test.dart
index a9e8b85..c67890b 100644
--- a/pkg/dds/test/dap/integration/debug_breakpoints_test.dart
+++ b/pkg/dds/test/dap/integration/debug_breakpoints_test.dart
@@ -19,7 +19,7 @@
test('stops at a line breakpoint', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
await client.hitBreakpoint(testFile, breakpointLine);
});
@@ -27,7 +27,7 @@
test('stops at a line breakpoint and can be resumed', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
// Hit the initial breakpoint.
final stop = await client.hitBreakpoint(testFile, breakpointLine);
@@ -40,14 +40,14 @@
});
test('stops at a line breakpoint and can step over (next)', () async {
- final testFile = dap.createTestFile(r'''
+ final testFile = dap.createTestFile('''
void main(List<String> args) async {
- print('Hello!'); // BREAKPOINT
- print('Hello!'); // STEP
+ print('Hello!'); $breakpointMarker
+ print('Hello!'); $stepMarker
}
''');
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
- final stepLine = lineWith(testFile, '// STEP');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
+ final stepLine = lineWith(testFile, stepMarker);
// Hit the initial breakpoint.
final stop = await dap.client.hitBreakpoint(testFile, breakpointLine);
@@ -63,18 +63,18 @@
'stops at a line breakpoint and can step over (next) an async boundary',
() async {
final client = dap.client;
- final testFile = dap.createTestFile(r'''
+ final testFile = dap.createTestFile('''
Future<void> main(List<String> args) async {
- await asyncPrint('Hello!'); // BREAKPOINT
- await asyncPrint('Hello!'); // STEP
+ await asyncPrint('Hello!'); $breakpointMarker
+ await asyncPrint('Hello!'); $stepMarker
}
Future<void> asyncPrint(String message) async {
await Future.delayed(const Duration(milliseconds: 1));
}
''');
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
- final stepLine = lineWith(testFile, '// STEP');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
+ final stepLine = lineWith(testFile, stepMarker);
// Hit the initial breakpoint.
final stop = await dap.client.hitBreakpoint(testFile, breakpointLine);
@@ -96,17 +96,17 @@
test('stops at a line breakpoint and can step in', () async {
final client = dap.client;
- final testFile = dap.createTestFile(r'''
+ final testFile = dap.createTestFile('''
void main(List<String> args) async {
- log('Hello!'); // BREAKPOINT
+ log('Hello!'); $breakpointMarker
}
-void log(String message) { // STEP
+void log(String message) { $stepMarker
print(message);
}
''');
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
- final stepLine = lineWith(testFile, '// STEP');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
+ final stepLine = lineWith(testFile, stepMarker);
// Hit the initial breakpoint.
final stop = await client.hitBreakpoint(testFile, breakpointLine);
@@ -120,18 +120,18 @@
test('stops at a line breakpoint and can step out', () async {
final client = dap.client;
- final testFile = dap.createTestFile(r'''
+ final testFile = dap.createTestFile('''
void main(List<String> args) async {
log('Hello!');
- log('Hello!'); // STEP
+ log('Hello!'); $stepMarker
}
void log(String message) {
- print(message); // BREAKPOINT
+ print(message); $breakpointMarker
}
''');
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
- final stepLine = lineWith(testFile, '// STEP');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
+ final stepLine = lineWith(testFile, stepMarker);
// Hit the initial breakpoint.
final stop = await client.hitBreakpoint(testFile, breakpointLine);
@@ -145,14 +145,14 @@
test('does not step into SDK code with debugSdkLibraries=false', () async {
final client = dap.client;
- final testFile = dap.createTestFile(r'''
+ final testFile = dap.createTestFile('''
void main(List<String> args) async {
- print('Hello!'); // BREAKPOINT
- print('Hello!'); // STEP
+ print('Hello!'); $breakpointMarker
+ print('Hello!'); $stepMarker
}
''');
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
- final stepLine = lineWith(testFile, '// STEP');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
+ final stepLine = lineWith(testFile, stepMarker);
// Hit the initial breakpoint.
final stop = await client.hitBreakpoint(
@@ -173,13 +173,13 @@
test('steps into SDK code with debugSdkLibraries=true', () async {
final client = dap.client;
- final testFile = dap.createTestFile(r'''
+ final testFile = dap.createTestFile('''
void main(List<String> args) async {
- print('Hello!'); // BREAKPOINT
+ print('Hello!'); $breakpointMarker
print('Hello!');
}
''');
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
// Hit the initial breakpoint.
final stop = await dap.client.hitBreakpoint(
@@ -207,12 +207,12 @@
import '$otherPackageUri';
void main(List<String> args) async {
- foo(); // BREAKPOINT
- foo(); // STEP
+ foo(); $breakpointMarker
+ foo(); $stepMarker
}
''');
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
- final stepLine = lineWith(testFile, '// STEP');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
+ final stepLine = lineWith(testFile, stepMarker);
// Hit the initial breakpoint.
final stop = await client.hitBreakpoint(
@@ -240,11 +240,11 @@
import '$otherPackageUri';
void main(List<String> args) async {
- foo(); // BREAKPOINT
+ foo(); $breakpointMarker
foo();
}
''');
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
// Hit the initial breakpoint.
final stop = await dap.client.hitBreakpoint(
@@ -272,11 +272,11 @@
import '$otherPackageUri';
void main(List<String> args) async {
- foo(); // BREAKPOINT
+ foo(); $breakpointMarker
foo();
}
''');
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
// Hit the initial breakpoint.
final stop = await client.hitBreakpoint(
@@ -300,14 +300,14 @@
test('allows changing debug settings during session', () async {
final client = dap.client;
- final testFile = dap.createTestFile(r'''
+ final testFile = dap.createTestFile('''
void main(List<String> args) async {
- print('Hello!'); // BREAKPOINT
- print('Hello!'); // STEP
+ print('Hello!'); $breakpointMarker
+ print('Hello!'); $stepMarker
}
''');
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
- final stepLine = lineWith(testFile, '// STEP');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
+ final stepLine = lineWith(testFile, stepMarker);
// Start with debugSdkLibraryes _enabled_ and hit the breakpoint.
final stop = await client.hitBreakpoint(
@@ -337,7 +337,7 @@
test('stops with condition evaluating to true', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
await client.hitBreakpoint(
testFile,
@@ -349,7 +349,7 @@
test('does not stop with condition evaluating to false', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
await client.doNotHitBreakpoint(
testFile,
@@ -361,7 +361,7 @@
test('stops with condition evaluating to non-zero', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
await client.hitBreakpoint(
testFile,
@@ -373,7 +373,7 @@
test('does not stop with condition evaluating to zero', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
await client.doNotHitBreakpoint(
testFile,
@@ -385,7 +385,7 @@
test('reports evaluation errors for conditions', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
final outputEventsFuture = client.outputEvents.toList();
@@ -422,7 +422,7 @@
) async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
final outputEventsFuture = client.outputEvents.toList();
diff --git a/pkg/dds/test/dap/integration/debug_eval_test.dart b/pkg/dds/test/dap/integration/debug_eval_test.dart
index dc5b2a0..9d6ff1b 100644
--- a/pkg/dds/test/dap/integration/debug_eval_test.dart
+++ b/pkg/dds/test/dap/integration/debug_eval_test.dart
@@ -19,29 +19,31 @@
group('debug mode evaluation', () {
test('evaluates expressions with simple results', () async {
final client = dap.client;
- final testFile = await dap.createTestFile(r'''
+ final testFile = await dap.createTestFile('''
void main(List<String> args) {
var a = 1;
var b = 2;
var c = 'test';
- print('Hello!'); // BREAKPOINT
+ print('Hello!'); $breakpointMarker
}''');
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
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"');
+ final topFrameId = await client.getTopFrameId(stop.threadId!);
+ await client.expectEvalResult(topFrameId, 'a', '1');
+ await client.expectEvalResult(topFrameId, 'a * b', '2');
+ await client.expectEvalResult(topFrameId, 'c', '"test"');
});
test('evaluates expressions with complex results', () async {
final client = dap.client;
final testFile = await dap.createTestFile(simpleBreakpointProgram);
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
final stop = await client.hitBreakpoint(testFile, breakpointLine);
- final result = await client.expectTopFrameEvalResult(
- stop.threadId!,
+ final topFrameId = await client.getTopFrameId(stop.threadId!);
+ final result = await client.expectEvalResult(
+ topFrameId,
'DateTime(2000, 1, 1)',
'DateTime',
);
@@ -56,12 +58,27 @@
);
});
+ test('evaluates expressions ending with semicolons', () async {
+ final client = dap.client;
+ final testFile = await dap.createTestFile('''
+void main(List<String> args) {
+ var a = 1;
+ var b = 2;
+ print('Hello!'); $breakpointMarker
+}''');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
+
+ final stop = await client.hitBreakpoint(testFile, breakpointLine);
+ final topFrameId = await client.getTopFrameId(stop.threadId!);
+ await client.expectEvalResult(topFrameId, 'a + b;', '3');
+ });
+
test(
'evaluates complex expressions expressions with evaluateToStringInDebugViews=true',
() async {
final client = dap.client;
final testFile = await dap.createTestFile(simpleBreakpointProgram);
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
final stop = await client.hitBreakpoint(
testFile,
@@ -70,8 +87,9 @@
client.launch(testFile.path, evaluateToStringInDebugViews: true),
);
- await client.expectTopFrameEvalResult(
- stop.threadId!,
+ final topFrameId = await client.getTopFrameId(stop.threadId!);
+ await client.expectEvalResult(
+ topFrameId,
'DateTime(2000, 1, 1)',
'DateTime (2000-01-01 00:00:00.000)',
);
@@ -87,9 +105,9 @@
}''');
final stop = await client.hitException(testFile);
-
- final result = await client.expectTopFrameEvalResult(
- stop.threadId!,
+ final topFrameId = await client.getTopFrameId(stop.threadId!);
+ final result = await client.expectEvalResult(
+ topFrameId,
threadExceptionExpression,
'"my error"',
);
@@ -106,8 +124,9 @@
}''');
final stop = await client.hitException(testFile);
- final result = await client.expectTopFrameEvalResult(
- stop.threadId!,
+ final topFrameId = await client.getTopFrameId(stop.threadId!);
+ final result = await client.expectEvalResult(
+ topFrameId,
threadExceptionExpression,
'_Exception',
);
@@ -125,8 +144,9 @@
''');
final stop = await client.hitException(testFile);
- await client.expectTopFrameEvalResult(
- stop.threadId!,
+ final topFrameId = await client.getTopFrameId(stop.threadId!);
+ await client.expectEvalResult(
+ topFrameId,
'$threadExceptionExpression.message.length',
'5',
);
@@ -134,16 +154,16 @@
test('can evaluate expressions in non-top frames', () async {
final client = dap.client;
- final testFile = await dap.createTestFile(r'''
+ final testFile = await dap.createTestFile('''
void main(List<String> args) {
var a = 999;
foo();
}
void foo() {
- var a = 111; // BREAKPOINT
+ var a = 111; $breakpointMarker
}''');
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
final stop = await client.hitBreakpoint(testFile, breakpointLine);
final stack = await client.getValidStack(stop.threadId!,
@@ -153,6 +173,48 @@
await client.expectEvalResult(secondFrameId, 'a', '999');
});
+ test('returns the full message for evaluation errors', () async {
+ final client = dap.client;
+ final testFile = await dap.createTestFile(simpleBreakpointProgram);
+ final breakpointLine = lineWith(testFile, breakpointMarker);
+
+ final stop = await client.hitBreakpoint(testFile, breakpointLine);
+ final topFrameId = await client.getTopFrameId(stop.threadId!);
+ expectResponseError(
+ client.evaluate(
+ '1 + "a"',
+ frameId: topFrameId,
+ ),
+ allOf([
+ contains('evaluateInFrame: (113) Expression compilation error'),
+ contains("'String' can't be assigned to a variable of type 'num'."),
+ contains(
+ '1 + "a"\n'
+ ' ^',
+ )
+ ]),
+ );
+ });
+
+ test('returns short errors for evaluation in "watch" context', () async {
+ final client = dap.client;
+ final testFile = await dap.createTestFile(simpleBreakpointProgram);
+ final breakpointLine = lineWith(testFile, breakpointMarker);
+
+ final stop = await client.hitBreakpoint(testFile, breakpointLine);
+ final topFrameId = await client.getTopFrameId(stop.threadId!);
+ expectResponseError(
+ client.evaluate(
+ '1 + "a"',
+ frameId: topFrameId,
+ context: 'watch',
+ ),
+ equals(
+ "A value of type 'String' can't be assigned "
+ "to a variable of type 'num'.",
+ ),
+ );
+ });
// These tests can be slow due to starting up the external server process.
}, timeout: Timeout.none);
}
diff --git a/pkg/dds/test/dap/integration/debug_stack_test.dart b/pkg/dds/test/dap/integration/debug_stack_test.dart
index 2a348e8..72f2cb5 100644
--- a/pkg/dds/test/dap/integration/debug_stack_test.dart
+++ b/pkg/dds/test/dap/integration/debug_stack_test.dart
@@ -19,7 +19,7 @@
test('includes expected names and async boundaries', () async {
final client = dap.client;
final testFile = await dap.createTestFile(simpleAsyncProgram);
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
final stop = await client.hitBreakpoint(testFile, breakpointLine);
final stack = await client.getValidStack(
@@ -61,7 +61,7 @@
test('only sets canRestart where VM can rewind', () async {
final client = dap.client;
final testFile = await dap.createTestFile(simpleAsyncProgram);
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
final stop = await client.hitBreakpoint(testFile, breakpointLine);
final stack = await client.getValidStack(
@@ -92,7 +92,7 @@
test('deemphasizes SDK frames when debugSdk=false', () async {
final client = dap.client;
final testFile = await dap.createTestFile(sdkStackFrameProgram);
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
final stop = await client.hitBreakpoint(
testFile,
@@ -124,7 +124,7 @@
test('does not deemphasize SDK frames when debugSdk=true', () async {
final client = dap.client;
final testFile = await dap.createTestFile(sdkStackFrameProgram);
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
final stop = await client.hitBreakpoint(
testFile,
diff --git a/pkg/dds/test/dap/integration/debug_test.dart b/pkg/dds/test/dap/integration/debug_test.dart
index 3a58c37..8c6a42a 100644
--- a/pkg/dds/test/dap/integration/debug_test.dart
+++ b/pkg/dds/test/dap/integration/debug_test.dart
@@ -101,7 +101,7 @@
test('provides a list of threads', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
await client.hitBreakpoint(testFile, breakpointLine);
final response = await client.getValidThreads();
@@ -113,7 +113,7 @@
test('runs with DDS by default', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
await client.hitBreakpoint(testFile, breakpointLine);
expect(await client.ddsAvailable, isTrue);
@@ -130,7 +130,7 @@
test('can download source code from the VM', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
// Hit the initial breakpoint.
final stop = await dap.client.hitBreakpoint(
@@ -175,7 +175,7 @@
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
await client.hitBreakpoint(testFile, breakpointLine);
diff --git a/pkg/dds/test/dap/integration/debug_variables_test.dart b/pkg/dds/test/dap/integration/debug_variables_test.dart
index 8acecb6..86f4412 100644
--- a/pkg/dds/test/dap/integration/debug_variables_test.dart
+++ b/pkg/dds/test/dap/integration/debug_variables_test.dart
@@ -5,6 +5,7 @@
import 'package:test/test.dart';
import 'test_client.dart';
+import 'test_scripts.dart';
import 'test_support.dart';
main() {
@@ -17,7 +18,7 @@
group('debug mode variables', () {
test('provides variable list for frames', () async {
final client = dap.client;
- final testFile = await dap.createTestFile(r'''
+ final testFile = await dap.createTestFile('''
void main(List<String> args) {
final myVariable = 1;
foo();
@@ -25,10 +26,10 @@
void foo() {
final b = 2;
- print('Hello!'); // BREAKPOINT
+ print('Hello!'); $breakpointMarker
}
''');
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
final stop = await client.hitBreakpoint(testFile, breakpointLine);
final stack = await client.getValidStack(
@@ -64,12 +65,7 @@
''');
final stop = await client.hitException(testFile);
- final stack = await client.getValidStack(
- stop.threadId!,
- startFrame: 0,
- numFrames: 1,
- );
- final topFrameId = stack.stackFrames.first.id;
+ final topFrameId = await client.getTopFrameId(stop.threadId!);
// Check for an additional Scope named "Exceptions" that includes the
// exception.
@@ -82,7 +78,7 @@
);
});
- test('provides complex exception types frames', () async {
+ test('provides complex exception types for frames', () async {
final client = dap.client;
final testFile = await dap.createTestFile(r'''
void main(List<String> args) {
@@ -91,12 +87,7 @@
''');
final stop = await client.hitException(testFile);
- final stack = await client.getValidStack(
- stop.threadId!,
- startFrame: 0,
- numFrames: 1,
- );
- final topFrameId = stack.stackFrames.first.id;
+ final topFrameId = await client.getTopFrameId(stop.threadId!);
// Check for an additional Scope named "Exceptions" that includes the
// exception.
@@ -113,13 +104,13 @@
test('includes simple variable fields', () async {
final client = dap.client;
- final testFile = await dap.createTestFile(r'''
+ final testFile = await dap.createTestFile('''
void main(List<String> args) {
final myVariable = DateTime(2000, 1, 1);
- print('Hello!'); // BREAKPOINT
+ print('Hello!'); $breakpointMarker
}
''');
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
final stop = await client.hitBreakpoint(testFile, breakpointLine);
await client.expectLocalVariable(
@@ -135,13 +126,13 @@
test('includes variable getters when evaluateGettersInDebugViews=true',
() async {
final client = dap.client;
- final testFile = await dap.createTestFile(r'''
+ final testFile = await dap.createTestFile('''
void main(List<String> args) {
final myVariable = DateTime(2000, 1, 1);
- print('Hello!'); // BREAKPOINT
+ print('Hello!'); $breakpointMarker
}
''');
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
final stop = await client.hitBreakpoint(
testFile,
@@ -181,13 +172,13 @@
test('renders a simple list', () async {
final client = dap.client;
- final testFile = await dap.createTestFile(r'''
+ final testFile = await dap.createTestFile('''
void main(List<String> args) {
final myVariable = ["first", "second", "third"];
- print('Hello!'); // BREAKPOINT
+ print('Hello!'); $breakpointMarker
}
''');
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
final stop = await client.hitBreakpoint(testFile, breakpointLine);
await client.expectLocalVariable(
@@ -204,13 +195,13 @@
test('renders a simple list subset', () async {
final client = dap.client;
- final testFile = await dap.createTestFile(r'''
+ final testFile = await dap.createTestFile('''
void main(List<String> args) {
final myVariable = ["first", "second", "third"];
- print('Hello!'); // BREAKPOINT
+ print('Hello!'); $breakpointMarker
}
''');
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
final stop = await client.hitBreakpoint(testFile, breakpointLine);
await client.expectLocalVariable(
@@ -227,17 +218,17 @@
test('renders a simple map with keys/values', () async {
final client = dap.client;
- final testFile = await dap.createTestFile(r'''
+ final testFile = await dap.createTestFile('''
void main(List<String> args) {
final myVariable = {
'zero': 0,
'one': 1,
'two': 2
};
- print('Hello!'); // BREAKPOINT
+ print('Hello!'); $breakpointMarker
}
''');
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
final stop = await client.hitBreakpoint(testFile, breakpointLine);
final variables = await client.expectLocalVariable(
@@ -270,17 +261,17 @@
test('renders a simple map subset', () async {
final client = dap.client;
- final testFile = await dap.createTestFile(r'''
+ final testFile = await dap.createTestFile('''
void main(List<String> args) {
final myVariable = {
'zero': 0,
'one': 1,
'two': 2
};
- print('Hello!'); // BREAKPOINT
+ print('Hello!'); $breakpointMarker
}
''');
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
final stop = await client.hitBreakpoint(testFile, breakpointLine);
await client.expectLocalVariable(
@@ -300,15 +291,15 @@
test('renders a complex map with keys/values', () async {
final client = dap.client;
- final testFile = await dap.createTestFile(r'''
+ final testFile = await dap.createTestFile('''
void main(List<String> args) {
final myVariable = {
DateTime(2000, 1, 1): Exception("my error")
};
- print('Hello!'); // BREAKPOINT
+ print('Hello!'); $breakpointMarker
}
''');
- final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
final stop = await client.hitBreakpoint(testFile, breakpointLine);
final mapVariables = await client.expectLocalVariable(
@@ -355,6 +346,109 @@
''',
);
});
+
+ test('calls toString() on custom classes', () async {
+ final client = dap.client;
+ final testFile = await dap.createTestFile('''
+class Foo {
+ toString() => 'Bar!';
+}
+
+void main() {
+ final myVariable = Foo();
+ print('Hello!'); $breakpointMarker
+}
+ ''');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
+
+ final stop = await client.hitBreakpoint(
+ testFile,
+ breakpointLine,
+ launch: () => client.launch(
+ testFile.path,
+ evaluateToStringInDebugViews: true,
+ ),
+ );
+
+ await client.expectScopeVariables(
+ await client.getTopFrameId(stop.threadId!),
+ 'Locals',
+ r'''
+ myVariable: Foo (Bar!), eval: myVariable
+ ''',
+ );
+ });
+
+ test('does not use toString() result if "Instance of Foo"', () async {
+ // When evaluateToStringInDebugViews=true, we should discard the result of
+ // caling toString() when it's just 'Instance of Foo' because we're already
+ // showing the type, and otherwise we show:
+ //
+ // myVariable: Foo (Instance of Foo)
+ final client = dap.client;
+ final testFile = await dap.createTestFile('''
+class Foo {}
+
+void main() {
+ final myVariable = Foo();
+ print('Hello!'); $breakpointMarker
+}
+ ''');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
+
+ final stop = await client.hitBreakpoint(
+ testFile,
+ breakpointLine,
+ launch: () => client.launch(
+ testFile.path,
+ evaluateToStringInDebugViews: true,
+ ),
+ );
+
+ await client.expectScopeVariables(
+ await client.getTopFrameId(stop.threadId!),
+ 'Locals',
+ r'''
+ myVariable: Foo, eval: myVariable
+ ''',
+ );
+ });
+
+ test('handles errors in getters', () async {
+ final client = dap.client;
+ final testFile = await dap.createTestFile('''
+class Foo {
+ String get doesNotThrow => "success";
+ String get throws => throw Exception('err');
+}
+
+void main() {
+ final myVariable = Foo();
+ print('Hello!'); $breakpointMarker
+}
+ ''');
+ final breakpointLine = lineWith(testFile, breakpointMarker);
+
+ final stop = await client.hitBreakpoint(
+ testFile,
+ breakpointLine,
+ launch: () => client.launch(
+ testFile.path,
+ evaluateGettersInDebugViews: true,
+ ),
+ );
+
+ await client.expectLocalVariable(
+ stop.threadId!,
+ expectedName: 'myVariable',
+ expectedDisplayString: 'Foo',
+ expectedVariables: '''
+ doesNotThrow: "success", eval: myVariable.doesNotThrow
+ throws: <Exception: err>
+ ''',
+ ignore: {'runtimeType'},
+ );
+ });
// These tests can be slow due to starting up the external server process.
}, timeout: Timeout.none);
}
diff --git a/pkg/dds/test/dap/integration/test_client.dart b/pkg/dds/test/dap/integration/test_client.dart
index 0099e3f..150050e 100644
--- a/pkg/dds/test/dap/integration/test_client.dart
+++ b/pkg/dds/test/dap/integration/test_client.dart
@@ -346,14 +346,13 @@
/// Returns [future].
Future<T> _logIfSlow<T>(String name, Future<T> future) {
var didComplete = false;
- future.then((_) => didComplete = true);
Future.delayed(_requestWarningDuration).then((_) {
if (!didComplete) {
print(
'$name has taken longer than ${_requestWarningDuration.inSeconds}s');
}
});
- return future;
+ return future.whenComplete(() => didComplete = true);
}
/// Creates a [DapTestClient] that connects the server listening on
@@ -745,17 +744,11 @@
return variables;
}
- /// Evalutes [expression] in the top frame of thread [threadId] and expects a
- /// specific [expectedResult].
- Future<EvaluateResponseBody> expectTopFrameEvalResult(
+ Future<int> getTopFrameId(
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);
+ return stack.stackFrames.first.id;
}
/// Evalutes [expression] in frame [frameId] and expects a specific
diff --git a/pkg/dds/test/dap/integration/test_scripts.dart b/pkg/dds/test/dap/integration/test_scripts.dart
index 5582f34..df3ed5d 100644
--- a/pkg/dds/test/dap/integration/test_scripts.dart
+++ b/pkg/dds/test/dap/integration/test_scripts.dart
@@ -2,6 +2,9 @@
// 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.
+/// A marker used in some test scripts/tests for where to set breakpoints.
+const breakpointMarker = '// BREAKPOINT';
+
/// A simple empty Dart script that should run with no output and no errors.
const emptyProgram = '''
void main(List<String> args) {}
@@ -9,17 +12,17 @@
/// A simple async Dart script that when stopped at the line of '// BREAKPOINT'
/// will contain SDK frames in the call stack.
-const sdkStackFrameProgram = r'''
+const sdkStackFrameProgram = '''
void main() {
[0].where((i) {
- return i == 0; // BREAKPOINT
+ return i == 0; $breakpointMarker
}).toList();
}
''';
/// A simple async Dart script that when stopped at the line of '// BREAKPOINT'
/// will contain multiple stack frames across some async boundaries.
-const simpleAsyncProgram = r'''
+const simpleAsyncProgram = '''
import 'dart:async';
Future<void> main() async {
@@ -40,16 +43,16 @@
}
void four() {
- print('!'); // BREAKPOINT
+ print('!'); $breakpointMarker
}
''';
/// A simple Dart script that should run with no errors and contains a comment
/// marker '// BREAKPOINT' for use in tests that require stopping at a breakpoint
/// but require no other context.
-const simpleBreakpointProgram = r'''
+const simpleBreakpointProgram = '''
void main(List<String> args) async {
- print('Hello!'); // BREAKPOINT
+ print('Hello!'); $breakpointMarker
}
''';
@@ -70,3 +73,6 @@
throw 'error';
}
''';
+
+/// A marker used in some test scripts/tests for where to expected steps.
+const stepMarker = '// STEP';
diff --git a/pkg/dds/test/dap/integration/test_server.dart b/pkg/dds/test/dap/integration/test_server.dart
index ffaabf2..3e4cb72 100644
--- a/pkg/dds/test/dap/integration/test_server.dart
+++ b/pkg/dds/test/dap/integration/test_server.dart
@@ -53,7 +53,6 @@
}) async {
return InProcessDapTestServer._([
...?additionalArgs,
- if (logger != null) '--verbose',
]);
}
}
@@ -107,12 +106,7 @@
final _process = await Process.start(
Platform.resolvedExecutable,
- [
- dapServerScript,
- 'dap',
- ...?additionalArgs,
- if (logger != null) '--verbose'
- ],
+ [dapServerScript, 'dap', ...?additionalArgs],
);
return OutOfProcessDapTestServer._(_process, logger);
diff --git a/pkg/dds/test/dap/integration/test_support.dart b/pkg/dds/test/dap/integration/test_support.dart
index d5d8372..9fd5a88 100644
--- a/pkg/dds/test/dap/integration/test_support.dart
+++ b/pkg/dds/test/dap/integration/test_support.dart
@@ -6,6 +6,7 @@
import 'dart:io';
import 'package:dds/src/dap/logging.dart';
+import 'package:dds/src/dap/protocol_generated.dart';
import 'package:package_config/package_config.dart';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';
@@ -22,6 +23,13 @@
/// simplified in VS Code by using a launch config with custom CodeLens links).
final useInProcessDap = Platform.environment['DAP_TEST_INTERNAL'] == 'true';
+/// Whether to print all protocol traffic to stdout while running tests.
+///
+/// This is useful for debugging locally or on the bots and will include both
+/// DAP traffic (between the test DAP client and the DAP server) and the VM
+/// Service traffic (wrapped in a custom 'dart.log' event).
+final verboseLogging = Platform.environment['DAP_TEST_VERBOSE'] == 'true';
+
/// A [RegExp] that matches the `path` part of a VM Service URI that contains
/// an authentication token.
final vmServiceAuthCodePathPattern = RegExp(r'^/[\w_\-=]{5,15}/ws$');
@@ -48,6 +56,18 @@
);
}
+/// Expects [response] to fail with a `message` matching [messageMatcher].
+expectResponseError<T>(Future<T> response, Matcher messageMatcher) {
+ expect(
+ response,
+ throwsA(
+ const TypeMatcher<Response>()
+ .having((r) => r.success, 'success', isFalse)
+ .having((r) => r.message, 'message', messageMatcher),
+ ),
+ );
+}
+
/// Returns the 1-base line in [file] that contains [searchText].
int lineWith(File file, String searchText) =>
file.readAsLinesSync().indexWhere((line) => line.contains(searchText)) + 1;
@@ -128,8 +148,11 @@
static Future<DapTestSession> setUp({List<String>? additionalArgs}) async {
final server = await _startServer(additionalArgs: additionalArgs);
- final client =
- await DapTestClient.connect(server, captureVmServiceTraffic: true);
+ final client = await DapTestClient.connect(
+ server,
+ captureVmServiceTraffic: verboseLogging,
+ logger: verboseLogging ? print : null,
+ );
return DapTestSession._(server, client);
}
diff --git a/pkg/dds/tool/dap/run_server.dart b/pkg/dds/tool/dap/run_server.dart
index ba22d72..0178e85 100644
--- a/pkg/dds/tool/dap/run_server.dart
+++ b/pkg/dds/tool/dap/run_server.dart
@@ -26,7 +26,6 @@
static const argIpv6 = 'ipv6';
static const argDds = 'dds';
static const argAuthCodes = 'auth-codes';
- static const argVerbose = 'verbose';
final Stream<List<int>> _inputStream;
final StreamSink<List<int>> _outputSink;
diff --git a/tools/VERSION b/tools/VERSION
index 2fc2c05..088b16a 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -27,5 +27,5 @@
MAJOR 2
MINOR 15
PATCH 0
-PRERELEASE 29
+PRERELEASE 30
PRERELEASE_PATCH 0
\ No newline at end of file