Make debugger skip same locations in dart during stepping. (#2043)
* Temp
* Add tests and cleanup
* Update changelog and disable new tests for all versions before current main
* Update changelog with pull request reference
* Fix test failures
* Update patterns tests to skip correct sdk versions
diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md
index 848870a..d6aac6c 100644
--- a/dwds/CHANGELOG.md
+++ b/dwds/CHANGELOG.md
@@ -1,6 +1,7 @@
## 18.0.2-dev
- Support new DDC temp names for patterns. - [#2042](https://github.com/dart-lang/webdev/pull/2042)
+- Make debugger find next dart location when stepping. -[#2043](https://github.com/dart-lang/webdev/pull/2043)
## 18.0.1
diff --git a/dwds/lib/src/debugging/debugger.dart b/dwds/lib/src/debugging/debugger.dart
index c8e313d..88914ab 100644
--- a/dwds/lib/src/debugging/debugger.dart
+++ b/dwds/lib/src/debugging/debugger.dart
@@ -83,6 +83,7 @@
FrameComputer? stackComputer;
bool _isStepping = false;
+ DartLocation? _previousSteppingLocation;
void updateInspector(AppInspectorInterface appInspector) {
inspector = appInspector;
@@ -143,6 +144,7 @@
}
} else {
_isStepping = false;
+ _previousSteppingLocation = null;
result = await _remoteDebugger.resume();
}
handleErrorIfPresent(result);
@@ -354,7 +356,12 @@
final url = urlForScriptId(scriptId);
if (url == null) return null;
- return _locations.locationForJs(url, line, column);
+ final loc = await _locations.locationForJs(url, line, column);
+ if (loc == null || loc.dartLocation == _previousSteppingLocation) {
+ return null;
+ }
+ _previousSteppingLocation = loc.dartLocation;
+ return loc;
}
/// Returns script ID for the paused event.
diff --git a/dwds/lib/src/debugging/location.dart b/dwds/lib/src/debugging/location.dart
index e0e0fc6..2958884 100644
--- a/dwds/lib/src/debugging/location.dart
+++ b/dwds/lib/src/debugging/location.dart
@@ -75,6 +75,19 @@
}
@override
+ int get hashCode => Object.hashAll([uri, line, column]);
+
+ @override
+ bool operator ==(Object? other) {
+ if (other is! DartLocation) {
+ return false;
+ }
+ return uri.serverPath == other.uri.serverPath &&
+ line == other.line &&
+ column == other.column;
+ }
+
+ @override
String toString() => '[${uri.serverPath}:$line:$column]';
factory DartLocation.fromZeroBased(DartUri uri, int line, int column) =>
diff --git a/dwds/test/instances/instance_inspection_common.dart b/dwds/test/instances/instance_inspection_common.dart
index 22a8eb7..f4be789 100644
--- a/dwds/test/instances/instance_inspection_common.dart
+++ b/dwds/test/instances/instance_inspection_common.dart
@@ -119,6 +119,20 @@
expect(result, isA<Instance>());
return result as Instance;
}
+
+ Future<Map<String?, Instance?>> getFrameVariables(
+ String isolateId, Frame frame) async {
+ final refs = <String, InstanceRef>{
+ for (var variable in frame.vars!)
+ variable.name!: variable.value as InstanceRef
+ };
+ final instances = <String, Instance>{};
+ for (final p in refs.entries) {
+ instances[p.key] =
+ await service.getObject(isolateId, p.value.id!) as Instance;
+ }
+ return instances;
+ }
}
Map<String, InstanceRef> _associationsToMap(
@@ -179,7 +193,7 @@
return instanceRef.valueAsString == 'true';
case InstanceKind.kDouble:
case InstanceKind.kInt:
- return int.parse(instanceRef.valueAsString!);
+ return double.parse(instanceRef.valueAsString!);
case InstanceKind.kString:
return instanceRef.valueAsString;
default:
diff --git a/dwds/test/instances/patterns_inspection_test.dart b/dwds/test/instances/patterns_inspection_test.dart
new file mode 100644
index 0000000..03bf7dc
--- /dev/null
+++ b/dwds/test/instances/patterns_inspection_test.dart
@@ -0,0 +1,155 @@
+// Copyright (c) 2023, 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.
+
+@TestOn('vm')
+@Timeout(Duration(minutes: 2))
+
+import 'dart:io';
+
+import 'package:pub_semver/pub_semver.dart' as semver;
+import 'package:test/test.dart';
+import 'package:test_common/logging.dart';
+import 'package:test_common/test_sdk_configuration.dart';
+import 'package:vm_service/vm_service.dart';
+
+import '../fixtures/context.dart';
+import '../fixtures/project.dart';
+import 'instance_inspection_common.dart';
+
+void main() async {
+ // Enable verbose logging for debugging.
+ final debug = false;
+
+ final provider = TestSdkConfigurationProvider(verbose: debug);
+ tearDownAll(provider.dispose);
+
+ for (var compilationMode in CompilationMode.values) {
+ await _runTests(
+ provider: provider,
+ compilationMode: compilationMode,
+ debug: debug,
+ );
+ }
+}
+
+Future<void> _runTests({
+ required TestSdkConfigurationProvider provider,
+ required CompilationMode compilationMode,
+ required bool debug,
+}) async {
+ final context =
+ TestContext(TestProject.testExperimentWithSoundNullSafety, provider);
+ final testInspector = TestInspector(context);
+
+ late VmServiceInterface service;
+ late Stream<Event> stream;
+ late String isolateId;
+ late ScriptRef mainScript;
+
+ onBreakPoint(breakPointId, body) => testInspector.onBreakPoint(
+ stream, isolateId, mainScript, breakPointId, body);
+
+ getInstanceRef(frame, expression) =>
+ testInspector.getInstanceRef(isolateId, frame, expression);
+
+ getFields(instanceRef, {offset, count}) => testInspector
+ .getFields(isolateId, instanceRef, offset: offset, count: count);
+
+ getFrameVariables(Frame frame) =>
+ testInspector.getFrameVariables(isolateId, frame);
+
+ group('$compilationMode |', () {
+ setUpAll(() async {
+ setCurrentLogWriter(debug: debug);
+ await context.setUp(
+ compilationMode: compilationMode,
+ enableExpressionEvaluation: true,
+ verboseCompiler: debug,
+ experiments: ['records', 'patterns'],
+ );
+ service = context.debugConnection.vmService;
+
+ final vm = await service.getVM();
+ isolateId = vm.isolates!.first.id!;
+ final scripts = await service.getScripts(isolateId);
+
+ await service.streamListen('Debug');
+ stream = service.onEvent('Debug');
+
+ mainScript = scripts.scripts!
+ .firstWhere((each) => each.uri!.contains('main.dart'));
+ });
+
+ tearDownAll(() async {
+ await context.tearDown();
+ });
+
+ setUp(() => setCurrentLogWriter(debug: debug));
+ tearDown(() => service.resume(isolateId));
+
+ test('pattern match case 1', () async {
+ await onBreakPoint('testPatternCase1', (event) async {
+ final frame = event.topFrame!;
+
+ expect(await getFrameVariables(frame), {
+ 'obj': matchListInstance(type: 'List<Object>'),
+ 'a': matchPrimitiveInstance(kind: InstanceKind.kString, value: 'a'),
+ 'n': matchPrimitiveInstance(kind: InstanceKind.kDouble, value: 1),
+ });
+ });
+ });
+
+ test('pattern match case 2', () async {
+ await onBreakPoint('testPatternCase2', (event) async {
+ final frame = event.topFrame!;
+
+ expect(await getFrameVariables(frame), {
+ 'obj': matchListInstance(type: 'List<Object>'),
+ 'a': matchPrimitiveInstance(kind: InstanceKind.kString, value: 'b'),
+ 'n': matchPrimitiveInstance(kind: InstanceKind.kDouble, value: 3.14),
+ });
+ });
+ });
+
+ test('pattern match default case', () async {
+ await onBreakPoint('testPatternDefault', (event) async {
+ final frame = event.topFrame!;
+ final frameIndex = frame.index!;
+ final instanceRef = await getInstanceRef(frameIndex, 'obj');
+ expect(await getFields(instanceRef), [0, 1]);
+
+ expect(await getFrameVariables(frame), {
+ 'obj': matchListInstance(type: 'List<int>'),
+ });
+ });
+ });
+
+ test('stepping through pattern match', () async {
+ await onBreakPoint('callTestPattern1', (Event event) async {
+ var previousLocation = event.topFrame!.location;
+ for (var step in [
+ // Make sure we step into the callee.
+ for (var i = 0; i < 4; i++) 'Into',
+ // Make a few steps inside the callee.
+ for (var i = 0; i < 4; i++) 'Over',
+ ]) {
+ await service.resume(isolateId, step: step);
+
+ event = await stream
+ .firstWhere((e) => e.kind == EventKind.kPauseInterrupted);
+
+ if (step == 'Over') {
+ expect(event.topFrame!.code!.name, 'testPattern');
+ }
+
+ final location = event.topFrame!.location;
+ expect(location, isNot(equals(previousLocation)));
+ previousLocation = location;
+ }
+ });
+ });
+ }, // TODO(annagrin): Remove when dart 3.0 is stable.
+ skip: semver.Version.parse(Platform.version.split(' ')[0]) <
+ semver.Version.parse('3.0.0-351.0.dev'));
+}
diff --git a/dwds/test/instances/record_inspection_test.dart b/dwds/test/instances/record_inspection_test.dart
index 7cf0c82..91e558f 100644
--- a/dwds/test/instances/record_inspection_test.dart
+++ b/dwds/test/instances/record_inspection_test.dart
@@ -65,7 +65,7 @@
compilationMode: compilationMode,
enableExpressionEvaluation: true,
verboseCompiler: debug,
- experiments: ['records'],
+ experiments: ['records', 'patterns'],
);
service = context.debugConnection.vmService;
@@ -88,7 +88,7 @@
tearDown(() => service.resume(isolateId));
test('simple records', () async {
- await onBreakPoint('printSimpleLocal', (event) async {
+ await onBreakPoint('printSimpleLocalRecord', (event) async {
final frame = event.topFrame!.index!;
final instanceRef = await getInstanceRef(frame, 'record');
@@ -111,7 +111,7 @@
});
test('simple records, field access', () async {
- await onBreakPoint('printSimpleLocal', (event) async {
+ await onBreakPoint('printSimpleLocalRecord', (event) async {
final frame = event.topFrame!.index!;
expect(await getInstance(frame, r'record.$1'),
matchPrimitiveInstance(kind: InstanceKind.kBool, value: true));
@@ -122,7 +122,7 @@
});
test('simple records with named fields', () async {
- await onBreakPoint('printSimpleNamedLocal', (event) async {
+ await onBreakPoint('printSimpleNamedLocalRecord', (event) async {
final frame = event.topFrame!.index!;
final instanceRef = await getInstanceRef(frame, 'record');
@@ -149,7 +149,7 @@
});
test('simple records with named fields, field access', () async {
- await onBreakPoint('printSimpleNamedLocal', (event) async {
+ await onBreakPoint('printSimpleNamedLocalRecord', (event) async {
final frame = event.topFrame!.index!;
expect(await getInstance(frame, r'record.$1'),
matchPrimitiveInstance(kind: InstanceKind.kBool, value: true));
@@ -160,7 +160,7 @@
});
test('complex records fields', () async {
- await onBreakPoint('printComplexLocal', (event) async {
+ await onBreakPoint('printComplexLocalRecord', (event) async {
final frame = event.topFrame!.index!;
final instanceRef = await getInstanceRef(frame, 'record');
@@ -208,7 +208,7 @@
});
test('complex records, field access', () async {
- await onBreakPoint('printComplexLocal', (event) async {
+ await onBreakPoint('printComplexLocalRecord', (event) async {
final frame = event.topFrame!.index!;
expect(await getInstance(frame, r'record.$1'),
matchPrimitiveInstance(kind: InstanceKind.kBool, value: true));
@@ -223,7 +223,7 @@
});
test('complex records with named fields', () async {
- await onBreakPoint('printComplexNamedLocal', (event) async {
+ await onBreakPoint('printComplexNamedLocalRecord', (event) async {
final frame = event.topFrame!.index!;
final instanceRef = await getInstanceRef(frame, 'record');
@@ -272,7 +272,7 @@
});
test('complex records with named fields, field access', () async {
- await onBreakPoint('printComplexNamedLocal', (event) async {
+ await onBreakPoint('printComplexNamedLocalRecord', (event) async {
final frame = event.topFrame!.index!;
expect(await getInstance(frame, r'record.$1'),
matchPrimitiveInstance(kind: InstanceKind.kBool, value: true));
@@ -287,7 +287,7 @@
});
test('nested records', () async {
- await onBreakPoint('printNestedLocal', (event) async {
+ await onBreakPoint('printNestedLocalRecord', (event) async {
final frame = event.topFrame!.index!;
final instanceRef = await getInstanceRef(frame, 'record');
@@ -324,7 +324,7 @@
});
test('nested records, field access', () async {
- await onBreakPoint('printNestedLocal', (event) async {
+ await onBreakPoint('printNestedLocalRecord', (event) async {
final frame = event.topFrame!.index!;
final instanceRef = await getInstanceRef(frame, r'record.$2');
@@ -338,7 +338,7 @@
});
test('nested records with named fields,', () async {
- await onBreakPoint('printNestedNamedLocal', (event) async {
+ await onBreakPoint('printNestedNamedLocalRecord', (event) async {
final frame = event.topFrame!.index!;
final instanceRef = await getInstanceRef(frame, 'record');
@@ -382,7 +382,7 @@
});
test('nested records with named fields, field access', () async {
- await onBreakPoint('printNestedNamedLocal', (event) async {
+ await onBreakPoint('printNestedNamedLocalRecord', (event) async {
final frame = event.topFrame!.index!;
final instanceRef = await getInstanceRef(frame, r'record.inner');
diff --git a/fixtures/_experimentSound/web/main.dart b/fixtures/_experimentSound/web/main.dart
index f3200a1..1c34887 100644
--- a/fixtures/_experimentSound/web/main.dart
+++ b/fixtures/_experimentSound/web/main.dart
@@ -9,44 +9,58 @@
void main() {
// for evaluation
Timer.periodic(const Duration(seconds: 1), (_) {
- printSimpleLocal();
- printComplexLocal();
- printNestedLocal();
- printSimpleNamedLocal();
- printComplexNamedLocal();
- printNestedNamedLocal();
+ printSimpleLocalRecord();
+ printComplexLocalRecord();
+ printNestedLocalRecord();
+ printSimpleNamedLocalRecord();
+ printComplexNamedLocalRecord();
+ printNestedNamedLocalRecord();
+ print('Patterns'); // Breakpoint: callTestPattern1
+ testPattern(['a', 1]);
+ testPattern([3.14, 'b']);
+ testPattern([0, 1]);
});
document.body!.appendText('Program is running!');
}
-void printSimpleLocal() {
+void printSimpleLocalRecord() {
final record = (true, 3);
- print(record); // Breakpoint: printSimpleLocal
+ print(record); // Breakpoint: printSimpleLocalRecord
}
-void printSimpleNamedLocal() {
+void printSimpleNamedLocalRecord() {
final record = (true, cat: 'Vasya');
- print(record); // Breakpoint: printSimpleNamedLocal
+ print(record); // Breakpoint: printSimpleNamedLocalRecord
}
-void printComplexLocal() {
+void printComplexLocalRecord() {
final record = (true, 3, {'a': 1, 'b': 5});
- print(record); // Breakpoint: printComplexLocal
+ print(record); // Breakpoint: printComplexLocalRecord
}
-void printComplexNamedLocal() {
+void printComplexNamedLocalRecord() {
final record = (true, 3, array: {'a': 1, 'b': 5});
- print(record); // Breakpoint: printComplexNamedLocal
+ print(record); // Breakpoint: printComplexNamedLocalRecord
}
-void printNestedLocal() {
+void printNestedLocalRecord() {
final record = (true, (false, 5));
- print(record); // Breakpoint: printNestedLocal
+ print(record); // Breakpoint: printNestedLocalRecord
}
-void printNestedNamedLocal() {
+void printNestedNamedLocalRecord() {
final record = (true, inner: (false, 5));
- print(record); // Breakpoint: printNestedNamedLocal
+ print(record); // Breakpoint: printNestedNamedLocalRecord
}
+String testPattern(Object obj) {
+ switch (obj) {
+ case [var a, int n] || [int n, var a] when n == 1 && a is String:
+ return a.toString(); // Breakpoint: testPatternCase1
+ case [double n, var a] || [var a, double n] when (n - 3.14).abs() < 0.001:
+ return a.toString(); // Breakpoint: testPatternCase2
+ default:
+ return 'default'; // Breakpoint: testPatternDefault
+ }
+}