[dds] Improve error handling of calling getters + toString() display in DAP variables

Change-Id: I5b93667925a7f4a6da9edd8f5c94e56b1dbff6d9
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/209963
Reviewed-by: Ben Konyi <bkonyi@google.com>
Commit-Queue: Ben Konyi <bkonyi@google.com>
diff --git a/pkg/dds/lib/src/dap/adapters/dart.dart b/pkg/dds/lib/src/dap/adapters/dart.dart
index 27ad157..ed80060 100644
--- a/pkg/dds/lib/src/dap/adapters/dart.dart
+++ b/pkg/dds/lib/src/dap/adapters/dart.dart
@@ -51,6 +51,9 @@
 /// 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';$');
 
@@ -538,10 +541,7 @@
       //
       // So in the case of a Watch context, try to extract the useful message.
       if (args.context == 'watch') {
-        final match = _evalErrorMessagePattern.firstMatch(rawMessage);
-        final shortError = match != null ? match.group(1)! : null;
-
-        throw DebugAdapterException(shortError ?? rawMessage);
+        throw DebugAdapterException(extractEvaluationErrorMessage(rawMessage));
       }
 
       throw DebugAdapterException(rawMessage);
@@ -576,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) {
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 301e180..9d6ff1b 100644
--- a/pkg/dds/test/dap/integration/debug_eval_test.dart
+++ b/pkg/dds/test/dap/integration/debug_eval_test.dart
@@ -19,14 +19,14 @@
   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);
       final topFrameId = await client.getTopFrameId(stop.threadId!);
@@ -38,7 +38,7 @@
     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 topFrameId = await client.getTopFrameId(stop.threadId!);
@@ -60,13 +60,13 @@
 
     test('evaluates expressions ending with semicolons', () 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;
-  print('Hello!'); // BREAKPOINT
+  print('Hello!'); $breakpointMarker
 }''');
-      final breakpointLine = lineWith(testFile, '// BREAKPOINT');
+      final breakpointLine = lineWith(testFile, breakpointMarker);
 
       final stop = await client.hitBreakpoint(testFile, breakpointLine);
       final topFrameId = await client.getTopFrameId(stop.threadId!);
@@ -78,7 +78,7 @@
         () 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,
@@ -154,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!,
@@ -176,7 +176,7 @@
     test('returns the full message for evaluation errors', () 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 topFrameId = await client.getTopFrameId(stop.threadId!);
@@ -199,7 +199,7 @@
     test('returns short errors for evaluation in "watch" context', () 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 topFrameId = await client.getTopFrameId(stop.threadId!);
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_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';