| // Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'package:test/test.dart'; |
| import 'package:test_common/utilities.dart'; |
| import 'package:vm_service/vm_service.dart'; |
| |
| import '../../fixtures/context.dart'; |
| |
| class TestInspector { |
| TestInspector(this.context); |
| TestContext context; |
| |
| VmService get service => context.debugConnection.vmService; |
| |
| Future<void> onBreakPoint( |
| Stream<Event> stream, |
| String isolateId, |
| ScriptRef script, |
| String breakPointId, |
| Future<void> Function(Event event) body, |
| ) async { |
| Breakpoint? bp; |
| try { |
| final line = |
| await context.findBreakpointLine(breakPointId, isolateId, script); |
| bp = await service.addBreakpointWithScriptUri( |
| isolateId, |
| script.uri!, |
| line, |
| ); |
| |
| final event = |
| await stream.firstWhere((e) => e.kind == EventKind.kPauseBreakpoint); |
| |
| await body(event); |
| } finally { |
| // Remove breakpoint so it doesn't impact other tests or retries. |
| if (bp != null) { |
| await service.removeBreakpoint(isolateId, bp.id!); |
| } |
| } |
| } |
| |
| Future<Map<dynamic, Object?>> getFields( |
| String isolateId, |
| InstanceRef instanceRef, { |
| int? offset, |
| int? count, |
| int depth = -1, |
| }) async { |
| final instanceId = instanceRef.id!; |
| |
| final result = await service.getObject( |
| isolateId, |
| instanceId, |
| offset: offset, |
| count: count, |
| ); |
| |
| expect(result, isA<Instance>()); |
| final instance = result as Instance; |
| |
| expect( |
| instance.kind, |
| instanceRef.kind, |
| reason: 'object $instanceId with ref kind ${instanceRef.kind} ' |
| 'has an instance kind ${instance.kind}', |
| ); |
| |
| final fields = instance.fields; |
| final associations = instance.associations; |
| final elements = instance.elements; |
| |
| Map<dynamic, InstanceRef>? fieldRefs; |
| if (fields != null) { |
| fieldRefs = _boundFieldsToMap(fields); |
| } else if (associations != null) { |
| fieldRefs = _associationsToMap(associations); |
| } else if (elements != null) { |
| fieldRefs = _elementsToMap(elements); |
| } else { |
| fieldRefs = {}; |
| } |
| |
| if (depth > 0) { |
| depth--; |
| } |
| if (depth == 0) { |
| return fieldRefs; |
| } |
| |
| final fieldValues = <dynamic, Object?>{}; |
| for (var p in fieldRefs.entries) { |
| fieldValues[p.key] = _getValue(p.value) ?? |
| await getFields( |
| isolateId, |
| p.value, |
| depth: depth, |
| ); |
| } |
| return fieldValues; |
| } |
| |
| Future<Map<String, InstanceRef>> getGetters( |
| String isolateId, |
| InstanceRef instanceRef, |
| ) async { |
| final cls = |
| await service.getObject(isolateId, instanceRef.classRef!.id!) as Class; |
| final getters = |
| cls.functions?.where((f) => f.isGetter ?? false).toList() ?? []; |
| |
| final results = await Future.wait([ |
| for (var getter in getters) |
| service.evaluate(isolateId, instanceRef.id!, getter.name!), |
| ]); |
| |
| return Map<String, InstanceRef>.fromIterables( |
| getters.map((e) => e.name!), |
| results.map((e) => e as InstanceRef), |
| ); |
| } |
| |
| Future<InstanceRef> getInstanceRef( |
| String isolateId, |
| int frame, |
| String expression, |
| ) async { |
| final result = await service.evaluateInFrame( |
| isolateId, |
| frame, |
| expression, |
| ); |
| expect(result, isA<InstanceRef>()); |
| return result as InstanceRef; |
| } |
| |
| Future<Instance> getInstance( |
| String isolateId, |
| int frame, |
| String expression, |
| ) async { |
| final instanceRef = await getInstanceRef( |
| isolateId, |
| frame, |
| expression, |
| ); |
| |
| expect(instanceRef.id, isNotNull); |
| final result = await service.getObject( |
| isolateId, |
| instanceRef.id!, |
| ); |
| |
| 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; |
| } |
| |
| Future<InstanceRef> getDisplayedRef( |
| String isolateId, |
| String instanceId, |
| ) async => |
| await service.invoke(isolateId, instanceId, 'toString', []) |
| as InstanceRef; |
| |
| Future<Map<dynamic, String?>> getDisplayedFields( |
| String isolateId, |
| InstanceRef ref, |
| ) async { |
| final fieldRefs = |
| await getFields(isolateId, ref, depth: 1) as Map<dynamic, InstanceRef>; |
| |
| Future<String?> toStringValue(InstanceRef ref) async => |
| ref.valueAsString ?? |
| (await getDisplayedRef(isolateId, ref.id!)).valueAsString; |
| |
| final fields = await Future.wait(fieldRefs.values.map(toStringValue)); |
| return Map<dynamic, String?>.fromIterables(fieldRefs.keys, fields); |
| } |
| |
| Future<Map<dynamic, String?>> getDisplayedGetters( |
| String isolateId, |
| InstanceRef ref, |
| ) async { |
| final fieldRefs = |
| await getGetters(isolateId, ref) as Map<dynamic, InstanceRef>; |
| |
| Future<String?> toStringValue(InstanceRef ref) async => |
| ref.valueAsString ?? |
| (await getDisplayedRef(isolateId, ref.id!)).valueAsString; |
| |
| final fields = await Future.wait(fieldRefs.values.map(toStringValue)); |
| return Map<dynamic, String?>.fromIterables(fieldRefs.keys, fields); |
| } |
| |
| Future<List<Instance>> getElements( |
| String isolateId, |
| String instanceId, |
| ) async { |
| final instance = await service.getObject(isolateId, instanceId) as Instance; |
| return Future.wait( |
| instance.fields!.map( |
| (e) async => await service.getObject( |
| isolateId, |
| (e.value as InstanceRef).id!, |
| ) as Instance, |
| ), |
| ); |
| } |
| } |
| |
| Map<String, InstanceRef> _associationsToMap( |
| Iterable<MapAssociation> associations, |
| ) => |
| Map.fromEntries( |
| associations.map((e) => MapEntry(e.key.valueAsString, e.value)), |
| ); |
| |
| Map<dynamic, InstanceRef> _boundFieldsToMap(Iterable<BoundField> fields) => |
| Map.fromEntries( |
| fields.where((e) => e.name != null).map((e) => MapEntry(e.name, e.value)), |
| ); |
| |
| Map<dynamic, InstanceRef> _elementsToMap(List<dynamic> fields) => |
| Map.fromEntries( |
| fields |
| .where((e) => e != null) |
| .map((e) => MapEntry(fields.indexOf(e), e!)), |
| ); |
| |
| Matcher matchRecordInstanceRef({required int length}) => isA<InstanceRef>() |
| .having((e) => e.kind, 'kind', InstanceKind.kRecord) |
| .having((e) => e.length, 'length', length) |
| .having((e) => e.classRef!, 'classRef', matchRecordClassRef); |
| |
| Matcher matchRecordTypeInstanceRef({required int length}) => isA<InstanceRef>() |
| .having( |
| (e) => e.kind, |
| 'kind', |
| // See https://github.com/dart-lang/sdk/commit/67e052d7e996be8ad9d02970117ffef07eab1c77. |
| // TODO() Can't compare edge verisons, wait for this to get to a dev release. |
| dartSdkIsAtLeast('3.4.0-edge.eeec4d36e3ea9b166da277a46f62d7d3b9ce645a') |
| ? InstanceKind.kType |
| : InstanceKind.kRecordType, |
| ) |
| .having((e) => e.length, 'length', length) |
| .having((e) => e.classRef!, 'classRef', matchRecordTypeClassRef); |
| |
| Matcher matchTypeInstanceRef(dynamic name) => isA<InstanceRef>() |
| .having((e) => e.kind, 'kind', InstanceKind.kType) |
| .having((e) => e.name, 'type ref name', name) |
| .having((e) => e.classRef, 'classRef', matchTypeClassRef); |
| |
| Matcher matchPrimitiveInstanceRef({ |
| required String kind, |
| }) => |
| isA<InstanceRef>().having((e) => e.kind, 'kind', kind); |
| |
| Matcher matchPrimitiveInstance({ |
| required String kind, |
| required dynamic value, |
| }) => |
| isA<Instance>() |
| .having((e) => e.kind, 'kind', kind) |
| .having(_getValue, 'value', value); |
| |
| Matcher matchPlainInstance({required libraryId, required String type}) => |
| isA<Instance>() |
| .having((e) => e.kind, 'kind', InstanceKind.kPlainInstance) |
| .having( |
| (e) => e.classRef, |
| 'classRef', |
| matchClassRef(name: type, libraryId: libraryId), |
| ); |
| |
| Matcher matchListInstance({required dynamic type}) => isA<Instance>() |
| .having((e) => e.kind, 'kind', InstanceKind.kList) |
| .having((e) => e.classRef, 'classRef', matchListClassRef(type)); |
| |
| Matcher matchMapInstance({required String type}) => isA<Instance>() |
| .having((e) => e.kind, 'kind', InstanceKind.kMap) |
| .having((e) => e.classRef, 'classRef', matchMapClassRef(type)); |
| |
| Matcher matchSetInstance({required String type}) => isA<Instance>() |
| .having((e) => e.kind, 'kind', InstanceKind.kSet) |
| .having((e) => e.classRef, 'classRef', matchSetClassRef(type)); |
| |
| Matcher matchRecordInstance({required int length}) => isA<Instance>() |
| .having((e) => e.kind, 'kind', InstanceKind.kRecord) |
| .having((e) => e.length, 'length', length) |
| .having((e) => e.classRef, 'classRef', matchRecordClassRef); |
| |
| Matcher matchRecordTypeInstance({required int length}) => isA<Instance>() |
| .having((e) => e.kind, 'kind', InstanceKind.kRecordType) |
| .having((e) => e.length, 'length', length) |
| .having((e) => e.classRef, 'classRef', matchRecordTypeClassRef); |
| |
| Matcher matchTypeStringInstance(dynamic name) => |
| matchPrimitiveInstance(kind: InstanceKind.kString, value: name); |
| |
| Matcher matchTypeInstance(dynamic name) => isA<Instance>() |
| .having((e) => e.kind, 'kind', InstanceKind.kType) |
| .having((e) => e.name, 'type name', name) |
| .having((e) => e.classRef, 'classRef', matchTypeClassRef); |
| |
| Matcher matchRecordClass = |
| matchClass(name: matchRecordClassName, libraryId: _dartCoreLibrary); |
| Matcher matchTypeClass = |
| matchClass(name: matchTypeClassName, libraryId: _dartCoreLibrary); |
| |
| /// TODO(annagrin): record type class is reported incorrectly |
| /// in ddc https://github.com/dart-lang/sdk/issues/54609, |
| /// remove when fixed. |
| Matcher matchRecordTypeClass = anyOf( |
| matchTypeClass, |
| matchClass(name: matchRecordTypeClassName, libraryId: _dartRuntimeLibrary), |
| ); |
| |
| Matcher matchClass({dynamic name, String? libraryId}) => isA<Class>() |
| .having((e) => e.name, 'class name', name) |
| .having((e) => e.library, 'library', matchLibraryRef(libraryId)); |
| |
| Matcher matchRecordClassRef = |
| matchClassRef(name: matchRecordClassName, libraryId: _dartCoreLibrary); |
| |
| /// TODO(annagrin): record type class is reported incorrectly |
| /// in ddc https://github.com/dart-lang/sdk/issues/54609, |
| /// remove when fixed. |
| Matcher matchRecordTypeClassRef = anyOf( |
| matchTypeClassRef, |
| matchClassRef(name: matchRecordTypeClassName, libraryId: _dartRuntimeLibrary), |
| ); |
| Matcher matchTypeClassRef = matchClassRef( |
| name: matchTypeClassName, |
| libraryId: _dartCoreLibrary, |
| ); |
| Matcher matchListClassRef(String type) => matchClassRef( |
| name: matchListClassName(type), |
| libraryId: _matchListLibraryName, |
| ); |
| Matcher matchMapClassRef(String type) => |
| matchClassRef(name: type, libraryId: _dartJsHelperLibrary); |
| Matcher matchSetClassRef(String type) => |
| matchClassRef(name: type, libraryId: _dartCollectionLibrary); |
| |
| Matcher matchClassRef({dynamic name, dynamic libraryId}) => isA<ClassRef>() |
| .having((e) => e.name, 'class ref name', name) |
| .having((e) => e.library, 'library', matchLibraryRef(libraryId)); |
| |
| Matcher matchLibraryRef(dynamic libraryId) => isA<LibraryRef>() |
| .having((e) => e.name, 'library name', libraryId) |
| .having((e) => e.id, 'id', libraryId) |
| .having((e) => e.uri, 'uri', libraryId); |
| |
| Object? _getValue(InstanceRef instanceRef) { |
| switch (instanceRef.kind) { |
| case InstanceKind.kBool: |
| return instanceRef.valueAsString == 'true'; |
| case InstanceKind.kDouble: |
| case InstanceKind.kInt: |
| return double.parse(instanceRef.valueAsString!); |
| case InstanceKind.kString: |
| return instanceRef.valueAsString; |
| default: |
| return null; |
| } |
| } |
| |
| final _dartCoreLibrary = 'dart:core'; |
| final _dartInterceptorsLibrary = 'dart:_interceptors'; |
| final _dartJsHelperLibrary = 'dart:_js_helper'; |
| final _dartCollectionLibrary = 'dart:collection'; |
| final _dartRuntimeLibrary = 'dart:_runtime'; |
| |
| final matchRecordClassName = 'Record'; |
| |
| /// Match types for old and new type systems. |
| /// - Old type system has |
| /// - for arrays: `dart:_interceptors|List` |
| /// - for type: `dart:_runtime|_Type`. |
| /// - New type system has |
| /// - for arrays: dart:_interceptors|JSArray`, and |
| /// - for type: `dart:core|Type`. |
| /// TODO(annagrin): remove old matchers when DDC enables new type system. |
| /// TODO(annagrin): `matchTypeClassName` is reported incorrectly |
| /// in ddc https://github.com/dart-lang/sdk/issues/54609, |
| final matchTypeClassName = anyOf(['Type', '_Type']); |
| final matchRecordTypeClassName = 'RecordType'; |
| |
| Matcher matchListClassName(String elementType) => |
| anyOf(['JSArray<$elementType>', 'List<$elementType>']); |
| |
| final _matchListLibraryName = |
| anyOf([_dartInterceptorsLibrary, _dartCoreLibrary]); |