diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md index 53312a7f3..52022b2b2 100644 --- a/dwds/CHANGELOG.md +++ b/dwds/CHANGELOG.md @@ -1,5 +1,7 @@ ## 19.0.0-dev + - Allow clients to specify the connected app's entrypoint file. - [#2047](https://github.com/dart-lang/webdev/pull/2047) +- Fix `getObject` failure on record class - [2063](https://github.com/dart-lang/webdev/pull/2063) ## 18.0.2 diff --git a/dwds/lib/src/debugging/metadata/class.dart b/dwds/lib/src/debugging/metadata/class.dart index 6fb842a7f..e70c66426 100644 --- a/dwds/lib/src/debugging/metadata/class.dart +++ b/dwds/lib/src/debugging/metadata/class.dart @@ -13,11 +13,14 @@ const _dartCoreLibrary = 'dart:core'; const _dartInterceptorsLibrary = 'dart:_interceptors'; /// A hard-coded ClassRef for the Closure class. -final classRefForClosure = classRefFor(_dartCoreLibrary, 'Closure'); +final classRefForClosure = classRefFor(_dartCoreLibrary, InstanceKind.kClosure); /// A hard-coded ClassRef for the String class. final classRefForString = classRefFor(_dartCoreLibrary, InstanceKind.kString); +/// A hard-coded ClassRef for the Record class. +final classRefForRecord = classRefFor(_dartCoreLibrary, InstanceKind.kRecord); + /// A hard-coded ClassRef for a (non-existent) class called Unknown. final classRefForUnknown = classRefFor(_dartCoreLibrary, 'Unknown'); @@ -62,15 +65,22 @@ LibraryRef libraryRefFor(String libraryId) => LibraryRef( /// Returns a [ClassRef] for the provided library ID and class name. ClassRef classRefFor(String libraryId, String? name) => ClassRef( - id: 'classes|$libraryId|$name', + id: classIdFor(libraryId, name), name: name, library: libraryRefFor(libraryId), ); +String classIdFor(String libraryId, String? name) => 'classes|$libraryId|$name'; + /// Meta data for a remote Dart class in Chrome. class ClassMetaData { static final _logger = Logger('ClassMetadata'); + /// Class id. + /// + /// Takes the form of 'libraryId:name'. + final String id; + /// The name of the JS constructor for the object. /// /// This may be a constructor for a Dart, but it's still a JS name. For @@ -85,8 +95,8 @@ class ClassMetaData { /// For example, 'int', 'List', 'Null' final String? dartName; - /// The library identifier, which is the URI of the library. - final String libraryId; + /// Class ref for the class metadata. + final ClassRef classRef; factory ClassMetaData({ Object? jsName, @@ -97,10 +107,18 @@ class ClassMetaData { bool isRecord = false, bool isNativeError = false, }) { + final jName = jsName as String?; + final dName = dartName as String?; + final library = libraryId as String? ?? _dartCoreLibrary; + final id = '$library:$jName'; + + final classRef = isRecord ? classRefForRecord : classRefFor(library, dName); + return ClassMetaData._( - jsName as String?, - libraryId as String? ?? _dartCoreLibrary, - dartName as String?, + id, + classRef, + jName, + dName, int.tryParse('$length'), isFunction, isRecord, @@ -109,8 +127,9 @@ class ClassMetaData { } ClassMetaData._( + this.id, + this.classRef, this.jsName, - this.libraryId, this.dartName, this.length, this.isFunction, @@ -118,11 +137,6 @@ class ClassMetaData { this.isNativeError, ); - /// Returns the ID of the class. - /// - /// Takes the form of 'libraryId:name'. - String get id => '$libraryId:$jsName'; - /// Returns the [ClassMetaData] for the Chrome [remoteObject]. /// /// Returns null if the [remoteObject] is not a Dart class. @@ -189,9 +203,6 @@ class ClassMetaData { } } - /// Return a [ClassRef] appropriate to this metadata. - ClassRef get classRef => classRefFor(libraryId, dartName); - /// True if this class refers to system maps, which are treated specially. /// /// Classes that implement Map or inherit from MapBase are still treated as diff --git a/dwds/test/instances/instance_inspection_common.dart b/dwds/test/instances/instance_inspection_common.dart index f82edaced..7e97c44c0 100644 --- a/dwds/test/instances/instance_inspection_common.dart +++ b/dwds/test/instances/instance_inspection_common.dart @@ -5,6 +5,7 @@ @TestOn('vm') @Timeout(Duration(minutes: 2)) +import 'package:dwds/src/debugging/metadata/class.dart'; import 'package:test/test.dart'; import 'package:vm_service/vm_service.dart'; @@ -183,23 +184,24 @@ Matcher matchSetInstance({required String type}) => isA() .having((e) => e.kind, 'kind', InstanceKind.kSet) .having((e) => e.classRef!.name, 'classRef.name', type); -Matcher matchRecordInstance({required int length, required String type}) => - isA() - .having((e) => e.kind, 'kind', InstanceKind.kRecord) - .having((e) => e.length, 'length', length) - .having((e) => e.classRef!, 'classRef', matchRecordType(type)); - -/// Currently some dart versions allow for the record type -/// to show as `RecordType(type1, type2...)`, and some as `(type1, type2...)`. -/// Match both versions. -/// -/// TODO(annagrin): Replace by matching `(type1, type2...)` after dart 3.0 -/// is stable. -Matcher matchRecordType(String type) => isA().having( - (e) => e.name, - 'type name', - anyOf([type.replaceAll('RecordType', ''), type]), - ); +Matcher matchRecordInstance({required int length}) => isA() + .having((e) => e.kind, 'kind', InstanceKind.kRecord) + .having((e) => e.length, 'length', length) + .having((e) => e.classRef!, 'classRef', matchRecordType); + +Matcher matchRecordClass = matchClass(libraryId: 'dart:core', type: 'Record'); + +Matcher matchTypeClass = matchClass(libraryId: 'dart:_runtime', type: '_Type'); + +Matcher matchClass({required String libraryId, required String type}) => + isA() + .having((e) => e.name, 'name', type) + .having((e) => e.id, 'id', classIdFor(libraryId, type)); + +Matcher matchRecordType = matchType('Record'); + +Matcher matchType(String type) => + isA().having((e) => e.name, 'type name', type); Object? _getValue(InstanceRef instanceRef) { switch (instanceRef.kind) { diff --git a/dwds/test/instances/record_inspection_test.dart b/dwds/test/instances/record_inspection_test.dart index 0e3abd165..196c17fe0 100644 --- a/dwds/test/instances/record_inspection_test.dart +++ b/dwds/test/instances/record_inspection_test.dart @@ -92,16 +92,55 @@ Future _runTests({ setUp(() => setCurrentLogWriter(debug: debug)); tearDown(() => service.resume(isolateId)); - test('simple records', () async { + test('simple record display', () async { await onBreakPoint('printSimpleLocalRecord', (event) async { final frame = event.topFrame!.index!; + final instanceRef = await getInstanceRef(frame, 'record'); + final instance = await getObject(instanceRef.id!); + final classId = instance.classRef!.id; + + expect(await getObject(classId), matchRecordClass); + + final stringRef = await getInstanceRef(frame, 'record.toString()'); + final stringRefId = stringRef.id!; + + expect( + await getObject(stringRefId), + matchPrimitiveInstance( + kind: InstanceKind.kString, + value: '(true, 3)', + ), + ); + + final typeInstanceRef = + await getInstanceRef(frame, 'record.runtimeType'); + final typeInstance = await getObject(typeInstanceRef.id!); + final typeClassId = typeInstance.classRef!.id; + + expect(await getObject(typeClassId), matchTypeClass); + + final typeStringRef = + await getInstanceRef(frame, 'record.runtimeType.toString()'); + final typeStringId = typeStringRef.id!; - final instanceId = instanceRef.id!; expect( - await getObject(instanceId), - matchRecordInstance(length: 2, type: 'RecordType(bool, int)'), + await getObject(typeStringId), + matchPrimitiveInstance( + kind: InstanceKind.kString, + value: '(bool, int)', + ), ); + }); + }); + + test('simple records', () async { + await onBreakPoint('printSimpleLocalRecord', (event) async { + final frame = event.topFrame!.index!; + final instanceRef = await getInstanceRef(frame, 'record'); + final instanceId = instanceRef.id!; + + expect(await getObject(instanceId), matchRecordInstance(length: 2)); expect(await getFields(instanceRef), {1: true, 2: 3}); expect(await getFields(instanceRef, offset: 0), {1: true, 2: 3}); @@ -136,20 +175,56 @@ Future _runTests({ }); }); - test('simple records with named fields', () async { + test('simple records with named fields display', () async { await onBreakPoint('printSimpleNamedLocalRecord', (event) async { final frame = event.topFrame!.index!; + final instanceRef = await getInstanceRef(frame, 'record'); + final instance = await getObject(instanceRef.id!); + final classId = instance.classRef!.id; - final instanceId = instanceRef.id!; + expect(await getObject(classId), matchRecordClass); + + final stringRef = await getInstanceRef(frame, 'record.toString()'); + final stringId = stringRef.id!; + + expect( + await getObject(stringId), + matchPrimitiveInstance( + kind: InstanceKind.kString, + value: '(true, cat: Vasya)', + ), + ); + + final typeInstanceRef = + await getInstanceRef(frame, 'record.runtimeType'); + final typeInstance = await getObject(typeInstanceRef.id!); + final typeClassId = typeInstance.classRef!.id; + + expect(await getObject(typeClassId), matchTypeClass); + + final typeStringRef = + await getInstanceRef(frame, 'record.runtimeType.toString()'); + final typeStringId = typeStringRef.id!; expect( - await getObject(instanceId), - matchRecordInstance( - length: 2, - type: 'RecordType(bool, {String cat})', + await getObject(typeStringId), + matchPrimitiveInstance( + kind: InstanceKind.kString, + value: '(bool, {String cat})', ), ); + }); + }); + + test('simple records with named fields', () async { + await onBreakPoint('printSimpleNamedLocalRecord', (event) async { + final frame = event.topFrame!.index!; + final instanceRef = await getInstanceRef(frame, 'record'); + + final instanceId = instanceRef.id!; + + expect(await getObject(instanceId), matchRecordInstance(length: 2)); expect(await getFields(instanceRef), {1: true, 'cat': 'Vasya'}); expect( @@ -187,19 +262,55 @@ Future _runTests({ }); }); - test('complex records fields', () async { + test('complex records display', () async { await onBreakPoint('printComplexLocalRecord', (event) async { final frame = event.topFrame!.index!; + final instanceRef = await getInstanceRef(frame, 'record'); + final instance = await getObject(instanceRef.id!); + final classId = instance.classRef!.id; + + expect(await getObject(classId), matchRecordClass); + + final stringRef = await getInstanceRef(frame, 'record.toString()'); + final stringId = stringRef.id!; + + expect( + await getObject(stringId), + matchPrimitiveInstance( + kind: InstanceKind.kString, + value: '(true, 3, {a: 1, b: 5})', + ), + ); + + final typeInstanceRef = + await getInstanceRef(frame, 'record.runtimeType'); + final typeInstance = await getObject(typeInstanceRef.id!); + final typeClassId = typeInstance.classRef!.id; + + expect(await getObject(typeClassId), matchTypeClass); + + final typeStringRef = + await getInstanceRef(frame, 'record.runtimeType.toString()'); + final typeStringId = typeStringRef.id!; - final instanceId = instanceRef.id!; expect( - await getObject(instanceId), - matchRecordInstance( - length: 3, - type: 'RecordType(bool, int, IdentityMap)', + await getObject(typeStringId), + matchPrimitiveInstance( + kind: InstanceKind.kString, + value: '(bool, int, IdentityMap)', ), ); + }); + }); + + test('complex records', () async { + await onBreakPoint('printComplexLocalRecord', (event) async { + final frame = event.topFrame!.index!; + final instanceRef = await getInstanceRef(frame, 'record'); + + final instanceId = instanceRef.id!; + expect(await getObject(instanceId), matchRecordInstance(length: 3)); expect(await getFields(instanceRef), { 1: true, @@ -258,19 +369,55 @@ Future _runTests({ }); }); - test('complex records with named fields', () async { + test('complex records with named fields display', () async { await onBreakPoint('printComplexNamedLocalRecord', (event) async { final frame = event.topFrame!.index!; + final instanceRef = await getInstanceRef(frame, 'record'); + final instance = await getObject(instanceRef.id!); + final classId = instance.classRef!.id; + + expect(await getObject(classId), matchRecordClass); + + final stringRef = await getInstanceRef(frame, 'record.toString()'); + final stringId = stringRef.id!; + + expect( + await getObject(stringId), + matchPrimitiveInstance( + kind: InstanceKind.kString, + value: '(true, 3, array: {a: 1, b: 5})', + ), + ); + + final typeInstanceRef = + await getInstanceRef(frame, 'record.runtimeType'); + final typeInstance = await getObject(typeInstanceRef.id!); + final typeClassId = typeInstance.classRef!.id; + + expect(await getObject(typeClassId), matchTypeClass); + + final typeStringRef = + await getInstanceRef(frame, 'record.runtimeType.toString()'); + final typeStringId = typeStringRef.id!; - final instanceId = instanceRef.id!; expect( - await getObject(instanceId), - matchRecordInstance( - length: 3, - type: 'RecordType(bool, int, {IdentityMap array})', + await getObject(typeStringId), + matchPrimitiveInstance( + kind: InstanceKind.kString, + value: '(bool, int, {IdentityMap array})', ), ); + }); + }); + + test('complex records with named fields', () async { + await onBreakPoint('printComplexNamedLocalRecord', (event) async { + final frame = event.topFrame!.index!; + final instanceRef = await getInstanceRef(frame, 'record'); + + final instanceId = instanceRef.id!; + expect(await getObject(instanceId), matchRecordInstance(length: 3)); expect(await getFields(instanceRef), { 1: true, @@ -329,20 +476,56 @@ Future _runTests({ }); }); - test('nested records', () async { + test('nested records display', () async { await onBreakPoint('printNestedLocalRecord', (event) async { final frame = event.topFrame!.index!; + final instanceRef = await getInstanceRef(frame, 'record'); + final instance = await getObject(instanceRef.id!); + final classId = instance.classRef!.id; + + expect(await getObject(classId), matchRecordClass); + + final stringRef = await getInstanceRef(frame, 'record.toString()'); + final stringId = stringRef.id!; - final instanceId = instanceRef.id!; expect( - await getObject(instanceId), - matchRecordInstance( - length: 2, - type: 'RecordType(bool, RecordType(bool, int))', + await getObject(stringId), + matchPrimitiveInstance( + kind: InstanceKind.kString, + value: '(true, (false, 5))', ), ); + final typeInstanceRef = + await getInstanceRef(frame, 'record.runtimeType'); + final typeInstance = await getObject(typeInstanceRef.id!); + final typeClassId = typeInstance.classRef!.id; + + expect(await getObject(typeClassId), matchTypeClass); + + final typeStringRef = + await getInstanceRef(frame, 'record.runtimeType.toString()'); + final typeStringId = typeStringRef.id!; + + expect( + await getObject(typeStringId), + matchPrimitiveInstance( + kind: InstanceKind.kString, + value: '(bool, (bool, int))', + ), + ); + }); + }); + + test('nested records', () async { + await onBreakPoint('printNestedLocalRecord', (event) async { + final frame = event.topFrame!.index!; + final instanceRef = await getInstanceRef(frame, 'record'); + + final instanceId = instanceRef.id!; + expect(await getObject(instanceId), matchRecordInstance(length: 2)); + expect(await getFields(instanceRef), { 1: true, 2: {1: false, 2: 5} @@ -375,30 +558,63 @@ Future _runTests({ final instanceRef = await getInstanceRef(frame, r'record.$2'); final instanceId = instanceRef.id!; - expect( - await getObject(instanceId), - matchRecordInstance(length: 2, type: 'RecordType(bool, int)'), - ); + expect(await getObject(instanceId), matchRecordInstance(length: 2)); expect(await getFields(instanceRef), {1: false, 2: 5}); expect(await getFields(instanceRef, offset: 0), {1: false, 2: 5}); }); }); - test('nested records with named fields,', () async { + test('nested records with named fields display', () async { await onBreakPoint('printNestedNamedLocalRecord', (event) async { final frame = event.topFrame!.index!; + final instanceRef = await getInstanceRef(frame, 'record'); + final instance = await getObject(instanceRef.id!); + final classId = instance.classRef!.id; + + expect(await getObject(classId), matchRecordClass); + + final stringRef = await getInstanceRef(frame, 'record.toString()'); + final stringId = stringRef.id!; - final instanceId = instanceRef.id!; expect( - await getObject(instanceId), - matchRecordInstance( - length: 2, - type: 'RecordType(bool, {RecordType(bool, int) inner})', + await getObject(stringId), + matchPrimitiveInstance( + kind: InstanceKind.kString, + value: '(true, inner: (false, 5))', ), ); + final typeInstanceRef = + await getInstanceRef(frame, 'record.runtimeType'); + final typeInstance = await getObject(typeInstanceRef.id!); + final typeClassId = typeInstance.classRef!.id; + + expect(await getObject(typeClassId), matchTypeClass); + + final typeStringRef = + await getInstanceRef(frame, 'record.runtimeType.toString()'); + final typeStringId = typeStringRef.id!; + + expect( + await getObject(typeStringId), + matchPrimitiveInstance( + kind: InstanceKind.kString, + value: '(bool, {(bool, int) inner})', + ), + ); + }); + }); + + test('nested records with named fields', () async { + await onBreakPoint('printNestedNamedLocalRecord', (event) async { + final frame = event.topFrame!.index!; + final instanceRef = await getInstanceRef(frame, 'record'); + + final instanceId = instanceRef.id!; + expect(await getObject(instanceId), matchRecordInstance(length: 2)); + expect(await getFields(instanceRef), { 1: true, 'inner': {1: false, 2: 5} @@ -437,10 +653,7 @@ Future _runTests({ final instanceRef = await getInstanceRef(frame, r'record.inner'); final instanceId = instanceRef.id!; - expect( - await getObject(instanceId), - matchRecordInstance(length: 2, type: 'RecordType(bool, int)'), - ); + expect(await getObject(instanceId), matchRecordInstance(length: 2)); expect(await getFields(instanceRef), {1: false, 2: 5}); expect(await getFields(instanceRef, offset: 0), {1: false, 2: 5});