From 7caa65225b2100422247e56ef41b20100339022b Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Thu, 5 Dec 2024 12:13:34 -0500 Subject: [PATCH 01/20] Added support for getClassMetadata with the DDc library bundle format --- dwds/CHANGELOG.md | 2 +- dwds/lib/src/debugging/classes.dart | 10 +- .../src/debugging/dart_runtime_debugger.dart | 26 +++- .../instances/class_inspection_amd_test.dart | 35 +++++ ...ss_inspection_ddc_library_bundle_test.dart | 37 +++++ .../test/instances/class_inspection_test.dart | 127 ------------------ .../common/class_inspection_common.dart | 121 +++++++++++++++++ 7 files changed, 221 insertions(+), 137 deletions(-) create mode 100644 dwds/test/instances/class_inspection_amd_test.dart create mode 100644 dwds/test/instances/class_inspection_ddc_library_bundle_test.dart delete mode 100644 dwds/test/instances/class_inspection_test.dart create mode 100644 dwds/test/instances/common/class_inspection_common.dart diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md index 35ab4ea1e..3ba030af7 100644 --- a/dwds/CHANGELOG.md +++ b/dwds/CHANGELOG.md @@ -11,7 +11,7 @@ to use the provided `name` in a `ModuleMetadata`. Metadata provided by DDC when using the library bundle format does not provide a useful bundle name. - Migrate to `package:web` v1.1.0. -- Added support for some debugging APIs with the DDC library bundle format. - [#2488](https://github.com/dart-lang/webdev/issues/2488) +- Added support for some debugging APIs with the DDC library bundle format. - [#2488](https://github.com/dart-lang/webdev/issues/2488), [#2534](https://github.com/dart-lang/webdev/issues/2534) ## 24.1.0 diff --git a/dwds/lib/src/debugging/classes.dart b/dwds/lib/src/debugging/classes.dart index 3cfe3eed7..72d2f467d 100644 --- a/dwds/lib/src/debugging/classes.dart +++ b/dwds/lib/src/debugging/classes.dart @@ -77,14 +77,8 @@ class ClassHelper extends Domain { if (libraryUri == null || classId == null || className == null) return null; - final expression = ''' - (function() { - const sdk = ${globalToolConfiguration.loadStrategy.loadModuleSnippet}('dart_sdk'); - const dart = sdk.dart; - return dart.getClassMetadata('$libraryUri', '$className'); - })() - '''; - + final expression = globalToolConfiguration.loadStrategy.dartRuntimeDebugger + .getClassMetadataJsExpression(libraryUri, className); RemoteObject result; try { result = await inspector.remoteDebugger.evaluate( diff --git a/dwds/lib/src/debugging/dart_runtime_debugger.dart b/dwds/lib/src/debugging/dart_runtime_debugger.dart index 76f2cc06a..beb2f6520 100644 --- a/dwds/lib/src/debugging/dart_runtime_debugger.dart +++ b/dwds/lib/src/debugging/dart_runtime_debugger.dart @@ -14,6 +14,7 @@ class DartRuntimeDebugger { }) : _loadStrategy = loadStrategy, _useLibraryBundleExpression = useLibraryBundleExpression; + /// Generates a JS expression based on DDC module format. String _generateJsExpression( String ddcExpression, String libraryBundleExpression, @@ -23,16 +24,18 @@ class DartRuntimeDebugger { : ddcExpression; } + /// Wraps a JS function call with SDK loader logic. String _wrapWithSdkLoader(String args, String functionCall) { return ''' function($args) { - const sdk = ${_loadStrategy.loadModuleSnippet}("dart_sdk"); + const sdk = ${_loadStrategy.loadModuleSnippet}('dart_sdk'); const dart = sdk.dart; return dart.$functionCall; } '''; } + /// Wraps a JS function call with DDC library bundle loader logic. String _wrapWithBundleLoader(String args, String functionCall) { return ''' function($args) { @@ -41,6 +44,12 @@ class DartRuntimeDebugger { '''; } + /// Wraps an expression in an Immediately Invoked Function Expression (IIFE). + String _wrapInIIFE(String expression) { + return '($expression)()'; + } + + /// Builds a JS expression based on the loading strategy. String _buildExpression( String args, String ddcFunction, @@ -52,6 +61,7 @@ class DartRuntimeDebugger { ); } + /// Generates a JS expression for retrieving object metadata. String getObjectMetadataJsExpression() { return _buildExpression( 'arg', @@ -60,6 +70,7 @@ class DartRuntimeDebugger { ); } + /// Generates a JS expression for retrieving object field names. String getObjectFieldNamesJsExpression() { return _buildExpression( '', @@ -68,6 +79,7 @@ class DartRuntimeDebugger { ); } + /// Generates a JS expression for retrieving function metadata. String getFunctionMetadataJsExpression() { return _buildExpression( '', @@ -76,6 +88,7 @@ class DartRuntimeDebugger { ); } + /// Generates a JS expression for retrieving a subrange of elements. String getSubRangeJsExpression() { return _buildExpression( 'offset, count', @@ -83,4 +96,15 @@ class DartRuntimeDebugger { 'getSubRange(this, offset, count)', ); } + + /// Generates a JS expression for retrieving class metadata. + String getClassMetadataJsExpression(String libraryUri, String className) { + final expression = _buildExpression( + '', + "getClassMetadata('$libraryUri', '$className')", + "getClassMetadata('$libraryUri', '$className')", + ); + // Use the helper method to wrap this in an IIFE + return _wrapInIIFE(expression); + } } diff --git a/dwds/test/instances/class_inspection_amd_test.dart b/dwds/test/instances/class_inspection_amd_test.dart new file mode 100644 index 000000000..1815be2e3 --- /dev/null +++ b/dwds/test/instances/class_inspection_amd_test.dart @@ -0,0 +1,35 @@ +// 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. + +@Tags(['daily']) +@TestOn('vm') +@Timeout(Duration(minutes: 2)) +library; + +import 'package:test/test.dart'; +import 'package:test_common/test_sdk_configuration.dart'; + +import '../fixtures/context.dart'; +import 'common/class_inspection_common.dart'; + +void main() { + // Enable verbose logging for debugging. + final debug = false; + final canaryFeatures = false; + + group('Class |', () { + final provider = TestSdkConfigurationProvider( + verbose: debug, + ); + tearDownAll(provider.dispose); + for (final compilationMode in CompilationMode.values) { + runTests( + provider: provider, + compilationMode: compilationMode, + canaryFeatures: canaryFeatures, + debug: debug, + ); + } + }); +} diff --git a/dwds/test/instances/class_inspection_ddc_library_bundle_test.dart b/dwds/test/instances/class_inspection_ddc_library_bundle_test.dart new file mode 100644 index 000000000..2f54f5fbc --- /dev/null +++ b/dwds/test/instances/class_inspection_ddc_library_bundle_test.dart @@ -0,0 +1,37 @@ +// Copyright (c) 2024, 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. + +@Tags(['daily']) +@TestOn('vm') +@Timeout(Duration(minutes: 2)) +library; + +import 'package:dwds/expression_compiler.dart'; +import 'package:test/test.dart'; +import 'package:test_common/test_sdk_configuration.dart'; + +import '../fixtures/context.dart'; +import 'common/class_inspection_common.dart'; + +void main() { + // Enable verbose logging for debugging. + final debug = false; + final canaryFeatures = true; + final compilationMode = CompilationMode.frontendServer; + final provider = TestSdkConfigurationProvider( + verbose: debug, + canaryFeatures: canaryFeatures, + ddcModuleFormat: ModuleFormat.ddc, + ); + + group('Class |', () { + tearDownAll(provider.dispose); + runTests( + provider: provider, + compilationMode: compilationMode, + canaryFeatures: canaryFeatures, + debug: debug, + ); + }); +} diff --git a/dwds/test/instances/class_inspection_test.dart b/dwds/test/instances/class_inspection_test.dart deleted file mode 100644 index a54afc7d9..000000000 --- a/dwds/test/instances/class_inspection_test.dart +++ /dev/null @@ -1,127 +0,0 @@ -// 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. - -@Tags(['daily']) -@TestOn('vm') -@Timeout(Duration(minutes: 2)) -library; - -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 '../fixtures/utilities.dart'; -import 'common/test_inspector.dart'; - -void main() { - // Enable verbose logging for debugging. - final debug = false; - - final provider = TestSdkConfigurationProvider( - verbose: debug, - ); - - final context = TestContext(TestProject.testExperiment, provider); - final testInspector = TestInspector(context); - - late VmService service; - late Stream stream; - late String isolateId; - late ScriptRef mainScript; - - Future onBreakPoint(breakPointId, body) => testInspector.onBreakPoint( - stream, - isolateId, - mainScript, - breakPointId, - body, - ); - - Future getObject(instanceId) => service.getObject(isolateId, instanceId); - - group('Class |', () { - tearDownAll(provider.dispose); - - for (final compilationMode in CompilationMode.values) { - group('$compilationMode |', () { - setUpAll(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings( - compilationMode: compilationMode, - enableExpressionEvaluation: true, - verboseCompiler: debug, - ), - ); - 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)); - - group('calling getObject for an existent class', () { - test('returns the correct class representation', () async { - await onBreakPoint('testClass1Case1', (event) async { - // classes|dart:core|Object_Diagnosticable - final result = await getObject( - 'classes|org-dartlang-app:///web/main.dart|GreeterClass', - ); - final clazz = result as Class?; - expect(clazz!.name, equals('GreeterClass')); - expect( - clazz.fields!.map((field) => field.name), - unorderedEquals([ - 'greeteeName', - 'useFrench', - ]), - ); - expect( - clazz.functions!.map((fn) => fn.name), - containsAll([ - 'sayHello', - 'greetInEnglish', - 'greetInFrench', - ]), - ); - }); - }); - }); - - group('calling getObject for a non-existent class', () { - // TODO(https://github.com/dart-lang/webdev/issues/2297): Ideally we - // should throw an error in this case for the client to catch instead - // of returning an empty class. - test('returns an empty class representation', () async { - await onBreakPoint('testClass1Case1', (event) async { - final result = await getObject( - 'classes|dart:core|Object_Diagnosticable', - ); - final clazz = result as Class?; - expect(clazz!.name, equals('Object_Diagnosticable')); - expect(clazz.fields, isEmpty); - expect(clazz.functions, isEmpty); - }); - }); - }); - }); - } - }); -} diff --git a/dwds/test/instances/common/class_inspection_common.dart b/dwds/test/instances/common/class_inspection_common.dart new file mode 100644 index 000000000..f3c7f2b7d --- /dev/null +++ b/dwds/test/instances/common/class_inspection_common.dart @@ -0,0 +1,121 @@ +// 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. + +@Tags(['daily']) +@TestOn('vm') +@Timeout(Duration(minutes: 2)) +library; + +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 '../../fixtures/utilities.dart'; +import '../common/test_inspector.dart'; + +void runTests({ + required TestSdkConfigurationProvider provider, + required CompilationMode compilationMode, + required bool canaryFeatures, + required bool debug, +}) { + final context = TestContext(TestProject.testExperiment, provider); + final testInspector = TestInspector(context); + + late VmService service; + late Stream stream; + late String isolateId; + late ScriptRef mainScript; + + Future onBreakPoint(breakPointId, body) => testInspector.onBreakPoint( + stream, + isolateId, + mainScript, + breakPointId, + body, + ); + + Future getObject(instanceId) => service.getObject(isolateId, instanceId); + + group('$compilationMode |', () { + setUpAll(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + testSettings: TestSettings( + compilationMode: compilationMode, + enableExpressionEvaluation: true, + verboseCompiler: debug, + canaryFeatures: canaryFeatures, + moduleFormat: provider.ddcModuleFormat, + ), + ); + 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)); + + group('calling getObject for an existent class', () { + test('returns the correct class representation', () async { + await onBreakPoint('testClass1Case1', (event) async { + // classes|dart:core|Object_Diagnosticable + final result = await getObject( + 'classes|org-dartlang-app:///web/main.dart|GreeterClass', + ); + final clazz = result as Class?; + expect(clazz!.name, equals('GreeterClass')); + expect( + clazz.fields!.map((field) => field.name), + unorderedEquals([ + 'greeteeName', + 'useFrench', + ]), + ); + expect( + clazz.functions!.map((fn) => fn.name), + containsAll([ + 'sayHello', + 'greetInEnglish', + 'greetInFrench', + ]), + ); + }); + }); + }); + + group('calling getObject for a non-existent class', () { + // TODO(https://github.com/dart-lang/webdev/issues/2297): Ideally we + // should throw an error in this case for the client to catch instead + // of returning an empty class. + test('returns an empty class representation', () async { + await onBreakPoint('testClass1Case1', (event) async { + final result = await getObject( + 'classes|dart:core|Object_Diagnosticable', + ); + final clazz = result as Class?; + expect(clazz!.name, equals('Object_Diagnosticable')); + expect(clazz.fields, isEmpty); + expect(clazz.functions, isEmpty); + }); + }); + }); + }); +} From f3b2bd7b4305108525502a4208348376d5bd9449 Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Thu, 12 Dec 2024 12:17:43 -0500 Subject: [PATCH 02/20] Added support for some debugging APIs with the DDC library bundle format. --- dwds/CHANGELOG.md | 1 + .../src/debugging/dart_runtime_debugger.dart | 49 +++++++++++++++++++ dwds/lib/src/debugging/inspector.dart | 11 ++--- dwds/lib/src/debugging/instance.dart | 7 +-- dwds/lib/src/debugging/libraries.dart | 9 +--- .../common/instance_inspection_common.dart | 3 +- ... instance_inspection_amd_canary_test.dart} | 4 +- ...dart => instance_inspection_amd_test.dart} | 4 +- ...ce_inspection_ddc_library_bundle_test.dart | 37 ++++++++++++++ 9 files changed, 105 insertions(+), 20 deletions(-) rename dwds/test/instances/{instance_inspection_canary_test.dart => instance_inspection_amd_canary_test.dart} (83%) rename dwds/test/instances/{instance_inspection_test.dart => instance_inspection_amd_test.dart} (83%) create mode 100644 dwds/test/instances/instance_inspection_ddc_library_bundle_test.dart diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md index 3ad574c28..12955ec7a 100644 --- a/dwds/CHANGELOG.md +++ b/dwds/CHANGELOG.md @@ -1,6 +1,7 @@ ## 24.2.1-wip - Update to be forward compatible with changes to `package:shelf_web_socket`. +- Added support for some debugging APIs with the DDC library bundle format. - [#2537](https://github.com/dart-lang/webdev/issues/2537) ## 24.2.0 diff --git a/dwds/lib/src/debugging/dart_runtime_debugger.dart b/dwds/lib/src/debugging/dart_runtime_debugger.dart index beb2f6520..2363621bc 100644 --- a/dwds/lib/src/debugging/dart_runtime_debugger.dart +++ b/dwds/lib/src/debugging/dart_runtime_debugger.dart @@ -107,4 +107,53 @@ class DartRuntimeDebugger { // Use the helper method to wrap this in an IIFE return _wrapInIIFE(expression); } + + /// Generates a JS expression for retrieving extension names. + String getExtensionNamesJsExpression() { + return _generateJsExpression( + "${_loadStrategy.loadModuleSnippet}('dart_sdk').developer._extensions.keys.toList();", + 'dartDevEmbedder.debugger.extensionNames', + ); + } + + /// Generates a JS expression for retrieving library metadata. + String getLibraryMetadataJsExpression(String libraryUri) { + final expression = _buildExpression( + '', + "getLibraryMetadata('$libraryUri')", + "getClassesInLibrary('$libraryUri')", + ); + // Use the helper method to wrap this in an IIFE + return _wrapInIIFE(expression); + } + + /// Generates a JS expression for retrieving map elements. + String getMapElementsJsExpression() { + return _buildExpression( + '', + 'getMapElements(this)', + 'getMapElements(this)', + ); + } + + /// Generates a JS expression for dynamically loading an object's field. + String dloadReplJsExpression(String fieldName) { + return _generateJsExpression( + _wrapWithSdkLoader('', 'dloadRepl(this, "$fieldName")'), + ''' + function() { + return this["$fieldName"]; + } + ''', + ); + } + + /// Generates a JS expression for retrieving set elements. + String getSetElementsJsExpression() { + return _buildExpression( + '', + 'getSetElements(this)', + 'getSetElements(this)', + ); + } } diff --git a/dwds/lib/src/debugging/inspector.dart b/dwds/lib/src/debugging/inspector.dart index 49a3b443a..1bfc4beec 100644 --- a/dwds/lib/src/debugging/inspector.dart +++ b/dwds/lib/src/debugging/inspector.dart @@ -192,11 +192,8 @@ class AppInspector implements AppInspectorInterface { /// Get the value of the field named [fieldName] from [receiver]. @override Future loadField(RemoteObject receiver, String fieldName) { - final load = ''' - function() { - return ${globalToolConfiguration.loadStrategy.loadModuleSnippet}("dart_sdk").dart.dloadRepl(this, "$fieldName"); - } - '''; + final load = globalToolConfiguration.loadStrategy.dartRuntimeDebugger + .dloadReplJsExpression(fieldName); return jsCallFunctionOn(receiver, load, []); } @@ -748,8 +745,8 @@ class AppInspector implements AppInspectorInterface { /// Runs an eval on the page to compute all existing registered extensions. Future> _getExtensionRpcs() async { - final expression = - "${globalToolConfiguration.loadStrategy.loadModuleSnippet}('dart_sdk').developer._extensions.keys.toList();"; + final expression = globalToolConfiguration.loadStrategy.dartRuntimeDebugger + .getExtensionNamesJsExpression(); final extensionRpcs = []; final params = { 'expression': expression, diff --git a/dwds/lib/src/debugging/instance.dart b/dwds/lib/src/debugging/instance.dart index e11e9dda5..35c8698f9 100644 --- a/dwds/lib/src/debugging/instance.dart +++ b/dwds/lib/src/debugging/instance.dart @@ -306,7 +306,8 @@ class InstanceHelper extends Domain { // We do this in in awkward way because we want the keys and values, but we // can't return things by value or some Dart objects will come back as // values that we need to be RemoteObject, e.g. a List of int. - final expression = _jsRuntimeFunctionCall('getMapElements(this)'); + final expression = globalToolConfiguration.loadStrategy.dartRuntimeDebugger + .getMapElementsJsExpression(); final keysAndValues = await inspector.jsCallFunctionOn(map, expression, []); final keys = await inspector.loadField(keysAndValues, 'keys'); @@ -674,8 +675,8 @@ class InstanceHelper extends Domain { final length = metaData.length; final objectId = remoteObject.objectId; if (objectId == null) return null; - - final expression = _jsRuntimeFunctionCall('getSetElements(this)'); + final expression = globalToolConfiguration.loadStrategy.dartRuntimeDebugger + .getSetElementsJsExpression(); final result = await inspector.jsCallFunctionOn(remoteObject, expression, []); diff --git a/dwds/lib/src/debugging/libraries.dart b/dwds/lib/src/debugging/libraries.dart index f36e11e6a..841194cdf 100644 --- a/dwds/lib/src/debugging/libraries.dart +++ b/dwds/lib/src/debugging/libraries.dart @@ -82,13 +82,8 @@ class LibraryHelper extends Domain { final libraryUri = libraryRef.uri; if (libraryId == null || libraryUri == null) return null; // Fetch information about all the classes in this library. - final expression = ''' - (function() { - const sdk = ${globalToolConfiguration.loadStrategy.loadModuleSnippet}('dart_sdk'); - const dart = sdk.dart; - return dart.getLibraryMetadata('$libraryUri'); - })() - '''; + final expression = globalToolConfiguration.loadStrategy.dartRuntimeDebugger + .getLibraryMetadataJsExpression(libraryUri); RemoteObject? result; try { diff --git a/dwds/test/instances/common/instance_inspection_common.dart b/dwds/test/instances/common/instance_inspection_common.dart index 099d326cf..170c9292b 100644 --- a/dwds/test/instances/common/instance_inspection_common.dart +++ b/dwds/test/instances/common/instance_inspection_common.dart @@ -1,4 +1,4 @@ -// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// Copyright (c) 2023-2024, 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. @@ -62,6 +62,7 @@ void runTests({ verboseCompiler: debug, canaryFeatures: canaryFeatures, experiments: ['records'], + moduleFormat: provider.ddcModuleFormat, ), ); service = context.debugConnection.vmService; diff --git a/dwds/test/instances/instance_inspection_canary_test.dart b/dwds/test/instances/instance_inspection_amd_canary_test.dart similarity index 83% rename from dwds/test/instances/instance_inspection_canary_test.dart rename to dwds/test/instances/instance_inspection_amd_canary_test.dart index 68e36b829..60bb04635 100644 --- a/dwds/test/instances/instance_inspection_canary_test.dart +++ b/dwds/test/instances/instance_inspection_amd_canary_test.dart @@ -1,4 +1,4 @@ -// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// Copyright (c) 2020-2024, 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. @@ -7,6 +7,7 @@ @Timeout(Duration(minutes: 2)) library; +import 'package:dwds/src/services/expression_compiler.dart'; import 'package:test/test.dart'; import 'package:test_common/test_sdk_configuration.dart'; @@ -22,6 +23,7 @@ void main() { final provider = TestSdkConfigurationProvider( verbose: debug, canaryFeatures: canaryFeatures, + ddcModuleFormat: ModuleFormat.amd, ); tearDownAll(provider.dispose); diff --git a/dwds/test/instances/instance_inspection_test.dart b/dwds/test/instances/instance_inspection_amd_test.dart similarity index 83% rename from dwds/test/instances/instance_inspection_test.dart rename to dwds/test/instances/instance_inspection_amd_test.dart index 8765583e8..3c3d2e128 100644 --- a/dwds/test/instances/instance_inspection_test.dart +++ b/dwds/test/instances/instance_inspection_amd_test.dart @@ -1,4 +1,4 @@ -// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// Copyright (c) 2020-2024, 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. @@ -7,6 +7,7 @@ @Timeout(Duration(minutes: 2)) library; +import 'package:dwds/src/services/expression_compiler.dart'; import 'package:test/test.dart'; import 'package:test_common/test_sdk_configuration.dart'; @@ -22,6 +23,7 @@ void main() { final provider = TestSdkConfigurationProvider( verbose: debug, canaryFeatures: canaryFeatures, + ddcModuleFormat: ModuleFormat.amd, ); tearDownAll(provider.dispose); diff --git a/dwds/test/instances/instance_inspection_ddc_library_bundle_test.dart b/dwds/test/instances/instance_inspection_ddc_library_bundle_test.dart new file mode 100644 index 000000000..bfca2b3c7 --- /dev/null +++ b/dwds/test/instances/instance_inspection_ddc_library_bundle_test.dart @@ -0,0 +1,37 @@ +// Copyright (c) 2024, 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. + +@Tags(['daily']) +@TestOn('vm') +@Timeout(Duration(minutes: 2)) +library; + +import 'package:dwds/expression_compiler.dart'; +import 'package:test/test.dart'; +import 'package:test_common/test_sdk_configuration.dart'; + +import '../fixtures/context.dart'; +import 'common/instance_inspection_common.dart'; + +void main() { + // Enable verbose logging for debugging. + final debug = false; + final canaryFeatures = true; + final compilationMode = CompilationMode.frontendServer; + + group('canary: $canaryFeatures |', () { + final provider = TestSdkConfigurationProvider( + verbose: debug, + canaryFeatures: canaryFeatures, + ddcModuleFormat: ModuleFormat.ddc, + ); + tearDownAll(provider.dispose); + runTests( + provider: provider, + compilationMode: compilationMode, + canaryFeatures: canaryFeatures, + debug: debug, + ); + }); +} From a1f017c3bea5b4ce02b5fa506c0b452a15c86a64 Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Fri, 13 Dec 2024 10:58:32 -0500 Subject: [PATCH 03/20] Update pattern test to account for new DDC JS variable naming --- dwds/test/instances/common/patterns_inspection_common.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dwds/test/instances/common/patterns_inspection_common.dart b/dwds/test/instances/common/patterns_inspection_common.dart index 935ab91d8..d2bf17ec2 100644 --- a/dwds/test/instances/common/patterns_inspection_common.dart +++ b/dwds/test/instances/common/patterns_inspection_common.dart @@ -98,8 +98,10 @@ void runTests({ expect(await getFrameVariables(frame), { 'obj': matchListInstance(type: 'Object'), - 'a': matchPrimitiveInstance(kind: InstanceKind.kString, value: 'b'), - 'n': matchPrimitiveInstance(kind: InstanceKind.kDouble, value: 3.14), + // Renamed to avoid shadowing variables from previous case. + 'a\$': matchPrimitiveInstance(kind: InstanceKind.kString, value: 'b'), + 'n\$': + matchPrimitiveInstance(kind: InstanceKind.kDouble, value: 3.14), }); }); }); From 58b8762a54f79fe1e7c30cccae63332b33f6c8a8 Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Fri, 13 Dec 2024 12:19:59 -0500 Subject: [PATCH 04/20] reverting change to pattern test --- dwds/test/instances/common/patterns_inspection_common.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/dwds/test/instances/common/patterns_inspection_common.dart b/dwds/test/instances/common/patterns_inspection_common.dart index d2bf17ec2..935ab91d8 100644 --- a/dwds/test/instances/common/patterns_inspection_common.dart +++ b/dwds/test/instances/common/patterns_inspection_common.dart @@ -98,10 +98,8 @@ void runTests({ expect(await getFrameVariables(frame), { 'obj': matchListInstance(type: 'Object'), - // Renamed to avoid shadowing variables from previous case. - 'a\$': matchPrimitiveInstance(kind: InstanceKind.kString, value: 'b'), - 'n\$': - matchPrimitiveInstance(kind: InstanceKind.kDouble, value: 3.14), + 'a': matchPrimitiveInstance(kind: InstanceKind.kString, value: 'b'), + 'n': matchPrimitiveInstance(kind: InstanceKind.kDouble, value: 3.14), }); }); }); From c781e5ffded553ac4864cd463020059d85f726d4 Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Fri, 13 Dec 2024 14:30:57 -0500 Subject: [PATCH 05/20] Added support for debugging API with the DDC library bundle format. --- dwds/CHANGELOG.md | 2 +- .../src/debugging/dart_runtime_debugger.dart | 8 ++++ dwds/lib/src/debugging/instance.dart | 3 +- .../common/record_inspection_common.dart | 1 + ...=> record_inspection_amd_canary_test.dart} | 4 +- ...t.dart => record_inspection_amd_test.dart} | 4 +- ...rd_inspection_ddc_library_bundle_test.dart | 37 +++++++++++++++++++ 7 files changed, 55 insertions(+), 4 deletions(-) rename dwds/test/instances/{record_inspection_canary_test.dart => record_inspection_amd_canary_test.dart} (83%) rename dwds/test/instances/{record_inspection_test.dart => record_inspection_amd_test.dart} (83%) create mode 100644 dwds/test/instances/record_inspection_ddc_library_bundle_test.dart diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md index 12955ec7a..55eb577d2 100644 --- a/dwds/CHANGELOG.md +++ b/dwds/CHANGELOG.md @@ -1,7 +1,7 @@ ## 24.2.1-wip - Update to be forward compatible with changes to `package:shelf_web_socket`. -- Added support for some debugging APIs with the DDC library bundle format. - [#2537](https://github.com/dart-lang/webdev/issues/2537) +- Added support for some debugging APIs with the DDC library bundle format. - [#2537](https://github.com/dart-lang/webdev/issues/2537),[#2544](https://github.com/dart-lang/webdev/issues/2544) ## 24.2.0 diff --git a/dwds/lib/src/debugging/dart_runtime_debugger.dart b/dwds/lib/src/debugging/dart_runtime_debugger.dart index 2363621bc..951fd531b 100644 --- a/dwds/lib/src/debugging/dart_runtime_debugger.dart +++ b/dwds/lib/src/debugging/dart_runtime_debugger.dart @@ -156,4 +156,12 @@ class DartRuntimeDebugger { 'getSetElements(this)', ); } + + String getRecordFieldsJsExpression() { + return _buildExpression( + '', + 'getRecordFields(this)', + 'getRecordFields(this)', + ); + } } diff --git a/dwds/lib/src/debugging/instance.dart b/dwds/lib/src/debugging/instance.dart index 35c8698f9..2815b255a 100644 --- a/dwds/lib/src/debugging/instance.dart +++ b/dwds/lib/src/debugging/instance.dart @@ -524,7 +524,8 @@ class InstanceHelper extends Domain { // We do this in in awkward way because we want the keys and values, but we // can't return things by value or some Dart objects will come back as // values that we need to be RemoteObject, e.g. a List of int. - final expression = _jsRuntimeFunctionCall('getRecordFields(this)'); + final expression = globalToolConfiguration.loadStrategy.dartRuntimeDebugger + .getRecordFieldsJsExpression(); final result = await inspector.jsCallFunctionOn(record, expression, []); final fieldNameElements = diff --git a/dwds/test/instances/common/record_inspection_common.dart b/dwds/test/instances/common/record_inspection_common.dart index a5e6e837d..a1bbdd161 100644 --- a/dwds/test/instances/common/record_inspection_common.dart +++ b/dwds/test/instances/common/record_inspection_common.dart @@ -66,6 +66,7 @@ void runTests({ verboseCompiler: debug, experiments: ['records', 'patterns'], canaryFeatures: canaryFeatures, + moduleFormat: provider.ddcModuleFormat, ), ); service = context.debugConnection.vmService; diff --git a/dwds/test/instances/record_inspection_canary_test.dart b/dwds/test/instances/record_inspection_amd_canary_test.dart similarity index 83% rename from dwds/test/instances/record_inspection_canary_test.dart rename to dwds/test/instances/record_inspection_amd_canary_test.dart index c1b79e7cb..37a649b70 100644 --- a/dwds/test/instances/record_inspection_canary_test.dart +++ b/dwds/test/instances/record_inspection_amd_canary_test.dart @@ -1,4 +1,4 @@ -// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// Copyright (c) 2023-2024, 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. @@ -7,6 +7,7 @@ @Timeout(Duration(minutes: 2)) library; +import 'package:dwds/src/services/expression_compiler.dart'; import 'package:test/test.dart'; import 'package:test_common/test_sdk_configuration.dart'; @@ -22,6 +23,7 @@ void main() { final provider = TestSdkConfigurationProvider( verbose: debug, canaryFeatures: canaryFeatures, + ddcModuleFormat: ModuleFormat.amd, ); tearDownAll(provider.dispose); diff --git a/dwds/test/instances/record_inspection_test.dart b/dwds/test/instances/record_inspection_amd_test.dart similarity index 83% rename from dwds/test/instances/record_inspection_test.dart rename to dwds/test/instances/record_inspection_amd_test.dart index a8af90b88..726a1d988 100644 --- a/dwds/test/instances/record_inspection_test.dart +++ b/dwds/test/instances/record_inspection_amd_test.dart @@ -1,4 +1,4 @@ -// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// Copyright (c) 2023-2024, 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. @@ -7,6 +7,7 @@ @Timeout(Duration(minutes: 2)) library; +import 'package:dwds/src/services/expression_compiler.dart'; import 'package:test/test.dart'; import 'package:test_common/test_sdk_configuration.dart'; @@ -22,6 +23,7 @@ void main() { final provider = TestSdkConfigurationProvider( verbose: debug, canaryFeatures: canaryFeatures, + ddcModuleFormat: ModuleFormat.amd, ); tearDownAll(provider.dispose); diff --git a/dwds/test/instances/record_inspection_ddc_library_bundle_test.dart b/dwds/test/instances/record_inspection_ddc_library_bundle_test.dart new file mode 100644 index 000000000..deb18060a --- /dev/null +++ b/dwds/test/instances/record_inspection_ddc_library_bundle_test.dart @@ -0,0 +1,37 @@ +// Copyright (c) 2024, 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. + +@Tags(['daily']) +@TestOn('vm') +@Timeout(Duration(minutes: 2)) +library; + +import 'package:dwds/src/services/expression_compiler.dart'; +import 'package:test/test.dart'; +import 'package:test_common/test_sdk_configuration.dart'; + +import '../fixtures/context.dart'; +import 'common/record_inspection_common.dart'; + +void main() { + // Enable verbose logging for debugging. + final debug = false; + final canaryFeatures = true; + final compilationMode = CompilationMode.frontendServer; + + group('canary: $canaryFeatures |', () { + final provider = TestSdkConfigurationProvider( + verbose: debug, + canaryFeatures: canaryFeatures, + ddcModuleFormat: ModuleFormat.ddc, + ); + tearDownAll(provider.dispose); + runTests( + provider: provider, + compilationMode: compilationMode, + canaryFeatures: canaryFeatures, + debug: debug, + ); + }); +} From 0b235e6fda66614b05768bf989301e9b105ce52c Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Tue, 17 Dec 2024 13:40:44 -0500 Subject: [PATCH 06/20] updated licenses --- dwds/test/instances/record_inspection_amd_canary_test.dart | 2 +- dwds/test/instances/record_inspection_amd_test.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dwds/test/instances/record_inspection_amd_canary_test.dart b/dwds/test/instances/record_inspection_amd_canary_test.dart index 37a649b70..7c2c5c0a8 100644 --- a/dwds/test/instances/record_inspection_amd_canary_test.dart +++ b/dwds/test/instances/record_inspection_amd_canary_test.dart @@ -1,4 +1,4 @@ -// Copyright (c) 2023-2024, the Dart project authors. Please see the AUTHORS file +// Copyright (c) 2024, 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. diff --git a/dwds/test/instances/record_inspection_amd_test.dart b/dwds/test/instances/record_inspection_amd_test.dart index 726a1d988..afee36510 100644 --- a/dwds/test/instances/record_inspection_amd_test.dart +++ b/dwds/test/instances/record_inspection_amd_test.dart @@ -1,4 +1,4 @@ -// Copyright (c) 2023-2024, the Dart project authors. Please see the AUTHORS file +// Copyright (c) 2024, 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. From 607c7e41619c0000d7901a3a38d54995da73a193 Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Tue, 17 Dec 2024 13:43:08 -0500 Subject: [PATCH 07/20] updated licenses and remove new line from changelog --- dwds/CHANGELOG.md | 1 - dwds/test/instances/common/instance_inspection_common.dart | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md index 16c076010..55eb577d2 100644 --- a/dwds/CHANGELOG.md +++ b/dwds/CHANGELOG.md @@ -3,7 +3,6 @@ - Update to be forward compatible with changes to `package:shelf_web_socket`. - Added support for some debugging APIs with the DDC library bundle format. - [#2537](https://github.com/dart-lang/webdev/issues/2537),[#2544](https://github.com/dart-lang/webdev/issues/2544) - ## 24.2.0 - Consolidate `FrontendServerDdcStrategyProvider` and `FrontendServerRequireStrategyProvider` under a shared parent class. - [#2517](https://github.com/dart-lang/webdev/issues/2517) diff --git a/dwds/test/instances/common/instance_inspection_common.dart b/dwds/test/instances/common/instance_inspection_common.dart index 170c9292b..c0d57ed76 100644 --- a/dwds/test/instances/common/instance_inspection_common.dart +++ b/dwds/test/instances/common/instance_inspection_common.dart @@ -1,4 +1,4 @@ -// Copyright (c) 2023-2024, the Dart project authors. Please see the AUTHORS file +// 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. From b9a8c2fe683f10f3233446905af76b756cd88f63 Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Wed, 18 Dec 2024 13:12:17 -0500 Subject: [PATCH 08/20] Added support for some debugging APIs with the DDC library bundle format - getRecordTypeFieldsJsExpression, callInstanceMethodJsExpression --- dwds/CHANGELOG.md | 2 +- .../src/debugging/dart_runtime_debugger.dart | 30 +++++++++++++++ dwds/lib/src/debugging/inspector.dart | 8 +--- dwds/lib/src/debugging/instance.dart | 11 +----- .../common/record_type_inspection_common.dart | 1 + ...cord_type_inspection_amd_canary_test.dart} | 4 +- ...t => record_type_inspection_amd_test.dart} | 4 +- ...pe_inspection_ddc_library_bundle_test.dart | 37 +++++++++++++++++++ 8 files changed, 79 insertions(+), 18 deletions(-) rename dwds/test/instances/{record_type_inspection_canary_test.dart => record_type_inspection_amd_canary_test.dart} (86%) rename dwds/test/instances/{record_type_inspection_test.dart => record_type_inspection_amd_test.dart} (86%) create mode 100644 dwds/test/instances/record_type_inspection_ddc_library_bundle_test.dart diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md index 55eb577d2..3aa5919d9 100644 --- a/dwds/CHANGELOG.md +++ b/dwds/CHANGELOG.md @@ -1,7 +1,7 @@ ## 24.2.1-wip - Update to be forward compatible with changes to `package:shelf_web_socket`. -- Added support for some debugging APIs with the DDC library bundle format. - [#2537](https://github.com/dart-lang/webdev/issues/2537),[#2544](https://github.com/dart-lang/webdev/issues/2544) +- Added support for some debugging APIs with the DDC library bundle format. - [#2537](https://github.com/dart-lang/webdev/issues/2537),[#2544](https://github.com/dart-lang/webdev/issues/2544),[#2548](https://github.com/dart-lang/webdev/issues/2548) ## 24.2.0 diff --git a/dwds/lib/src/debugging/dart_runtime_debugger.dart b/dwds/lib/src/debugging/dart_runtime_debugger.dart index 90b47d581..e09c86e5d 100644 --- a/dwds/lib/src/debugging/dart_runtime_debugger.dart +++ b/dwds/lib/src/debugging/dart_runtime_debugger.dart @@ -169,4 +169,34 @@ class DartRuntimeDebugger { 'getRecordFields(this)', ); } + + /// Generates a JS expression for retrieving the fields of a record type. + String getRecordTypeFieldsJsExpression() { + return _buildExpression( + '', + 'getRecordTypeFields(this)', + 'getRecordTypeFields(this)', + ); + } + + /// Generates a JS expression for calling an instance method on an object. + String callInstanceMethodJsExpression(String methodName) { + String generateInstanceMethodJsExpression(String functionCall) { + return ''' + function () { + if (!Object.getPrototypeOf(this)) { return 'Instance of PlainJavaScriptObject'; } + return $functionCall; + } + '''; + } + + return _generateJsExpression( + generateInstanceMethodJsExpression( + '${_loadStrategy.loadModuleSnippet}("dart_sdk").dart.dsendRepl(this, "$methodName", arguments)', + ), + generateInstanceMethodJsExpression( + 'dartDevEmbedder.debugger.callInstanceMethod(this, "$methodName", arguments)', + ), + ); + } } diff --git a/dwds/lib/src/debugging/inspector.dart b/dwds/lib/src/debugging/inspector.dart index 23f47504d..4ce408b3d 100644 --- a/dwds/lib/src/debugging/inspector.dart +++ b/dwds/lib/src/debugging/inspector.dart @@ -210,12 +210,8 @@ class AppInspector implements AppInspectorInterface { throw UnsupportedError('Named arguments are not yet supported'); } // We use the JS pseudo-variable 'arguments' to get the list of all arguments. - final send = ''' - function () { - if (!Object.getPrototypeOf(this)) { return 'Instance of PlainJavaScriptObject';} - return ${globalToolConfiguration.loadStrategy.loadModuleSnippet}("dart_sdk").dart.dsendRepl(this, "$methodName", arguments); - } - '''; + final send = globalToolConfiguration.loadStrategy.dartRuntimeDebugger + .callInstanceMethodJsExpression(methodName); final remote = await jsCallFunctionOn(receiver, send, positionalArgs); return remote; } diff --git a/dwds/lib/src/debugging/instance.dart b/dwds/lib/src/debugging/instance.dart index 2815b255a..0db74d5ae 100644 --- a/dwds/lib/src/debugging/instance.dart +++ b/dwds/lib/src/debugging/instance.dart @@ -653,7 +653,8 @@ class InstanceHelper extends Domain { // We do this in in awkward way because we want the names and types, but we // can't return things by value or some Dart objects will come back as // values that we need to be RemoteObject, e.g. a List of int. - final expression = _jsRuntimeFunctionCall('getRecordTypeFields(this)'); + final expression = globalToolConfiguration.loadStrategy.dartRuntimeDebugger + .getRecordTypeFieldsJsExpression(); final result = await inspector.jsCallFunctionOn(record, expression, []); final fieldNameElements = @@ -887,11 +888,3 @@ class InstanceHelper extends Domain { } } } - -String _jsRuntimeFunctionCall(String expression) => ''' - function() { - const sdk = ${globalToolConfiguration.loadStrategy.loadModuleSnippet}('dart_sdk'); - const dart = sdk.dart; - return dart.$expression; - } -'''; diff --git a/dwds/test/instances/common/record_type_inspection_common.dart b/dwds/test/instances/common/record_type_inspection_common.dart index 2d51bee26..241195da5 100644 --- a/dwds/test/instances/common/record_type_inspection_common.dart +++ b/dwds/test/instances/common/record_type_inspection_common.dart @@ -64,6 +64,7 @@ void runTests({ verboseCompiler: debug, experiments: ['records', 'patterns'], canaryFeatures: canaryFeatures, + moduleFormat: provider.ddcModuleFormat, ), ); service = context.debugConnection.vmService; diff --git a/dwds/test/instances/record_type_inspection_canary_test.dart b/dwds/test/instances/record_type_inspection_amd_canary_test.dart similarity index 86% rename from dwds/test/instances/record_type_inspection_canary_test.dart rename to dwds/test/instances/record_type_inspection_amd_canary_test.dart index 2bc861f58..02d5f826b 100644 --- a/dwds/test/instances/record_type_inspection_canary_test.dart +++ b/dwds/test/instances/record_type_inspection_amd_canary_test.dart @@ -1,4 +1,4 @@ -// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// Copyright (c) 2024, 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. @@ -7,6 +7,7 @@ @Timeout(Duration(minutes: 2)) library; +import 'package:dwds/expression_compiler.dart'; import 'package:test/test.dart'; import 'package:test_common/test_sdk_configuration.dart'; @@ -22,6 +23,7 @@ void main() { final provider = TestSdkConfigurationProvider( verbose: debug, canaryFeatures: canaryFeatures, + ddcModuleFormat: ModuleFormat.amd, ); tearDownAll(provider.dispose); diff --git a/dwds/test/instances/record_type_inspection_test.dart b/dwds/test/instances/record_type_inspection_amd_test.dart similarity index 86% rename from dwds/test/instances/record_type_inspection_test.dart rename to dwds/test/instances/record_type_inspection_amd_test.dart index 342d75b4e..d471b0769 100644 --- a/dwds/test/instances/record_type_inspection_test.dart +++ b/dwds/test/instances/record_type_inspection_amd_test.dart @@ -1,4 +1,4 @@ -// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// Copyright (c) 2024, 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. @@ -7,6 +7,7 @@ @Timeout(Duration(minutes: 2)) library; +import 'package:dwds/expression_compiler.dart'; import 'package:test/test.dart'; import 'package:test_common/test_sdk_configuration.dart'; @@ -22,6 +23,7 @@ void main() { final provider = TestSdkConfigurationProvider( verbose: debug, canaryFeatures: canaryFeatures, + ddcModuleFormat: ModuleFormat.amd, ); tearDownAll(provider.dispose); diff --git a/dwds/test/instances/record_type_inspection_ddc_library_bundle_test.dart b/dwds/test/instances/record_type_inspection_ddc_library_bundle_test.dart new file mode 100644 index 000000000..9e5cb3c11 --- /dev/null +++ b/dwds/test/instances/record_type_inspection_ddc_library_bundle_test.dart @@ -0,0 +1,37 @@ +// Copyright (c) 2024, 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. + +@Tags(['daily']) +@TestOn('vm') +@Timeout(Duration(minutes: 2)) +library; + +import 'package:dwds/expression_compiler.dart'; +import 'package:test/test.dart'; +import 'package:test_common/test_sdk_configuration.dart'; + +import '../fixtures/context.dart'; +import 'common/record_type_inspection_common.dart'; + +void main() { + // Enable verbose logging for debugging. + final debug = false; + final canaryFeatures = true; + final compilationMode = CompilationMode.frontendServer; + + group('canary: $canaryFeatures |', () { + final provider = TestSdkConfigurationProvider( + verbose: debug, + canaryFeatures: canaryFeatures, + ddcModuleFormat: ModuleFormat.ddc, + ); + tearDownAll(provider.dispose); + runTests( + provider: provider, + compilationMode: compilationMode, + canaryFeatures: canaryFeatures, + debug: debug, + ); + }); +} From 1e44766002f0e4f49ef2a14fd4e6e7d03450b5e6 Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Thu, 9 Jan 2025 14:40:42 -0500 Subject: [PATCH 09/20] Added support for some debugging APIs with the DDC library bundle format --- .../src/debugging/dart_runtime_debugger.dart | 14 +- .../src/services/chrome_proxy_service.dart | 6 +- ...art => chrome_proxy_service_amd_test.dart} | 13 +- ...proxy_service_ddc_library_bundle_test.dart | 135 ++++++++++++++++++ dwds/test/fixtures/project.dart | 8 ++ .../hello_world/main_ddc_library_bundle.dart | 27 ++++ 6 files changed, 192 insertions(+), 11 deletions(-) rename dwds/test/{chrome_proxy_service_test.dart => chrome_proxy_service_amd_test.dart} (99%) create mode 100644 dwds/test/chrome_proxy_service_ddc_library_bundle_test.dart create mode 100644 fixtures/_testSound/example/hello_world/main_ddc_library_bundle.dart diff --git a/dwds/lib/src/debugging/dart_runtime_debugger.dart b/dwds/lib/src/debugging/dart_runtime_debugger.dart index e09c86e5d..ed633ee48 100644 --- a/dwds/lib/src/debugging/dart_runtime_debugger.dart +++ b/dwds/lib/src/debugging/dart_runtime_debugger.dart @@ -18,11 +18,8 @@ class DartRuntimeDebugger { String _generateJsExpression( String ddcExpression, String libraryBundleExpression, - ) { - return _useLibraryBundleExpression - ? libraryBundleExpression - : ddcExpression; - } + ) => + _useLibraryBundleExpression ? libraryBundleExpression : ddcExpression; /// Wraps a JS function call with SDK loader logic. String _wrapWithSdkLoader(String args, String functionCall) { @@ -199,4 +196,11 @@ class DartRuntimeDebugger { ), ); } + + String invokeExtensionJsExpression(String methodName, String encodedJson) { + return _generateJsExpression( + "${_loadStrategy.loadModuleSnippet}('dart_sdk').developer.invokeExtension('$methodName', JSON.stringify($encodedJson));", + "dartDevEmbedder.debugger.invokeExtension('$methodName', JSON.stringify($encodedJson));", + ); + } } diff --git a/dwds/lib/src/services/chrome_proxy_service.dart b/dwds/lib/src/services/chrome_proxy_service.dart index 8690b39dc..92aedf9a7 100644 --- a/dwds/lib/src/services/chrome_proxy_service.dart +++ b/dwds/lib/src/services/chrome_proxy_service.dart @@ -518,10 +518,8 @@ class ChromeProxyService implements VmServiceInterface { v is String ? v : jsonEncode(v), ), ); - final expression = ''' -${globalToolConfiguration.loadStrategy.loadModuleSnippet}("dart_sdk").developer.invokeExtension( - "$method", JSON.stringify(${jsonEncode(stringArgs)})); -'''; + final expression = globalToolConfiguration.loadStrategy.dartRuntimeDebugger + .invokeExtensionJsExpression(method, jsonEncode(stringArgs)); final result = await inspector.jsEvaluate(expression, awaitPromise: true); final decodedResponse = jsonDecode(result.value as String) as Map; diff --git a/dwds/test/chrome_proxy_service_test.dart b/dwds/test/chrome_proxy_service_amd_test.dart similarity index 99% rename from dwds/test/chrome_proxy_service_test.dart rename to dwds/test/chrome_proxy_service_amd_test.dart index 1cddd4cfc..81458782e 100644 --- a/dwds/test/chrome_proxy_service_test.dart +++ b/dwds/test/chrome_proxy_service_amd_test.dart @@ -11,6 +11,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:dwds/expression_compiler.dart'; import 'package:dwds/src/services/chrome_proxy_service.dart'; import 'package:dwds/src/utilities/dart_uri.dart'; import 'package:dwds/src/utilities/shared.dart'; @@ -29,8 +30,14 @@ import 'fixtures/utilities.dart'; void main() { // Change to true to see verbose output from the tests. final debug = false; - - final provider = TestSdkConfigurationProvider(verbose: debug); + final moduleFormat = ModuleFormat.amd; + final canaryFeatures = false; + + final provider = TestSdkConfigurationProvider( + verbose: debug, + ddcModuleFormat: moduleFormat, + canaryFeatures: canaryFeatures, + ); tearDownAll(provider.dispose); final context = TestContext(TestProject.test, provider); @@ -42,6 +49,8 @@ void main() { testSettings: TestSettings( enableExpressionEvaluation: true, verboseCompiler: false, + moduleFormat: provider.ddcModuleFormat, + canaryFeatures: provider.canaryFeatures, ), ); }); diff --git a/dwds/test/chrome_proxy_service_ddc_library_bundle_test.dart b/dwds/test/chrome_proxy_service_ddc_library_bundle_test.dart new file mode 100644 index 000000000..4add74f8a --- /dev/null +++ b/dwds/test/chrome_proxy_service_ddc_library_bundle_test.dart @@ -0,0 +1,135 @@ +// Copyright (c) 2025, 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') +@Tags(['daily']) +@Timeout(Duration(minutes: 2)) +library; + +import 'dart:convert'; + +import 'package:dwds/expression_compiler.dart'; +import 'package:dwds/src/services/chrome_proxy_service.dart'; +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 'fixtures/utilities.dart'; + +void main() { + // Change to true to see verbose output from the tests. + final debug = false; + final moduleFormat = ModuleFormat.ddc; + final canaryFeatures = true; + final compilationMode = CompilationMode.frontendServer; + + final provider = TestSdkConfigurationProvider( + verbose: debug, + ddcModuleFormat: moduleFormat, + canaryFeatures: canaryFeatures, + ); + tearDownAll(provider.dispose); + + final context = TestContext(TestProject.testDdcLibraryBundle, provider); + + group('shared context', () { + setUpAll(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + testSettings: TestSettings( + enableExpressionEvaluation: true, + verboseCompiler: false, + moduleFormat: provider.ddcModuleFormat, + canaryFeatures: provider.canaryFeatures, + compilationMode: compilationMode, + ), + ); + }); + + tearDownAll(() async { + await context.tearDown(); + }); + + group('callServiceExtension', () { + late ChromeProxyService service; + + setUp(() { + setCurrentLogWriter(debug: debug); + service = context.service; + }); + + test( + 'success', + () async { + final serviceMethod = 'ext.test.callServiceExtension'; + await context.tabConnection.runtime + .evaluate('registerExtension("$serviceMethod");'); + + // The non-string keys/values get auto json-encoded to match the vm + // behavior. + final args = { + 'bool': true, + 'list': [1, '2', 3], + 'map': {'foo': 'bar'}, + 'num': 1.0, + 'string': 'hello', + 1: 2, + false: true, + }; + + final result = + await service.callServiceExtension(serviceMethod, args: args); + expect( + result.json, + args.map( + (k, v) => MapEntry( + k is String ? k : jsonEncode(k), + v is String ? v : jsonEncode(v), + ), + ), + ); + }, + onPlatform: { + 'windows': + const Skip('https://github.com/dart-lang/webdev/issues/711'), + }, + ); + + test( + 'failure', + () async { + final serviceMethod = 'ext.test.callServiceExtensionWithError'; + await context.tabConnection.runtime + .evaluate('registerExtensionWithError("$serviceMethod");'); + + final errorDetails = {'intentional': 'error'}; + expect( + service.callServiceExtension( + serviceMethod, + args: { + 'code': '-32001', + 'details': jsonEncode(errorDetails), + }, + ), + throwsA( + predicate( + (dynamic error) => + error is RPCError && + error.code == -32001 && + error.details == jsonEncode(errorDetails), + ), + ), + ); + }, + onPlatform: { + 'windows': + const Skip('https://github.com/dart-lang/webdev/issues/711'), + }, + ); + }); + }); +} diff --git a/dwds/test/fixtures/project.dart b/dwds/test/fixtures/project.dart index b06b1d5c8..3c2f42ab6 100644 --- a/dwds/test/fixtures/project.dart +++ b/dwds/test/fixtures/project.dart @@ -102,6 +102,14 @@ class TestProject { htmlEntryFileName: 'index.html', ); + static const testDdcLibraryBundle = TestProject._( + packageName: '_test_sound', + packageDirectory: '_testSound', + webAssetsPath: 'example/hello_world', + dartEntryFileName: 'main_ddc_library_bundle.dart', + htmlEntryFileName: 'index.html', + ); + static final testScopes = TestProject._( packageName: '_test_sound', packageDirectory: '_testSound', diff --git a/fixtures/_testSound/example/hello_world/main_ddc_library_bundle.dart b/fixtures/_testSound/example/hello_world/main_ddc_library_bundle.dart new file mode 100644 index 000000000..0ab7d09e5 --- /dev/null +++ b/fixtures/_testSound/example/hello_world/main_ddc_library_bundle.dart @@ -0,0 +1,27 @@ +// Copyright (c) 2025, 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 'dart:convert'; +import 'dart:developer'; +import 'dart:js'; + +// Create a series of top level objects for tests in +// dwds/test/chrome_proxy_service_ddc_library_bundle_test.dart + +void main() async { + context['registerExtension'] = (String method) { + registerExtension(method, + (String method, Map parameters) async { + return ServiceExtensionResponse.result(jsonEncode(parameters)); + }); + }; + + context['registerExtensionWithError'] = (String method) { + registerExtension(method, + (String method, Map parameters) async { + return ServiceExtensionResponse.error( + int.parse(parameters['code']!), parameters['details']!); + }); + }; +} From c537b7b0b7fbd6866aa160fe76ec2b6a5b4b2a9d Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Thu, 9 Jan 2025 14:44:49 -0500 Subject: [PATCH 10/20] updated CHANGELOG --- dwds/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md index ad6dc0373..1941b4c3b 100644 --- a/dwds/CHANGELOG.md +++ b/dwds/CHANGELOG.md @@ -1,4 +1,5 @@ ## 24.4.0-wip +- Added support for some debugging APIs with the DDC library bundle format. - [#2563](https://github.com/dart-lang/webdev/issues/2563) ## 24.3.0 From 0e35c5e7419e277c97a8ff6586a21a005da7ada4 Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Fri, 10 Jan 2025 13:17:30 -0500 Subject: [PATCH 11/20] refactored test to use common file --- dwds/test/chrome_proxy_service_amd_test.dart | 2527 +--------------- ...proxy_service_ddc_library_bundle_test.dart | 131 +- .../common/chrome_proxy_service_common.dart | 2559 +++++++++++++++++ dwds/test/fixtures/project.dart | 8 - 4 files changed, 2594 insertions(+), 2631 deletions(-) create mode 100644 dwds/test/common/chrome_proxy_service_common.dart diff --git a/dwds/test/chrome_proxy_service_amd_test.dart b/dwds/test/chrome_proxy_service_amd_test.dart index 81458782e..dbba5bab1 100644 --- a/dwds/test/chrome_proxy_service_amd_test.dart +++ b/dwds/test/chrome_proxy_service_amd_test.dart @@ -7,2527 +7,34 @@ @Timeout(Duration(minutes: 2)) library; -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - import 'package:dwds/expression_compiler.dart'; -import 'package:dwds/src/services/chrome_proxy_service.dart'; -import 'package:dwds/src/utilities/dart_uri.dart'; -import 'package:dwds/src/utilities/shared.dart'; -import 'package:http/http.dart' as http; -import 'package:path/path.dart' as path; 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 'package:vm_service_interface/vm_service_interface.dart'; +import 'common/chrome_proxy_service_common.dart'; import 'fixtures/context.dart'; -import 'fixtures/project.dart'; -import 'fixtures/utilities.dart'; void main() { - // Change to true to see verbose output from the tests. + // Enable verbose logging for debugging. final debug = false; - final moduleFormat = ModuleFormat.amd; final canaryFeatures = false; + final moduleFormat = ModuleFormat.amd; + final compilationMode = CompilationMode.buildDaemon; - final provider = TestSdkConfigurationProvider( - verbose: debug, - ddcModuleFormat: moduleFormat, - canaryFeatures: canaryFeatures, - ); - tearDownAll(provider.dispose); - - final context = TestContext(TestProject.test, provider); - - group('shared context', () { - setUpAll(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings( - enableExpressionEvaluation: true, - verboseCompiler: false, - moduleFormat: provider.ddcModuleFormat, - canaryFeatures: provider.canaryFeatures, - ), - ); - }); - - tearDownAll(() async { - await context.tearDown(); - }); - - group('breakpoints', () { - late VmServiceInterface service; - VM vm; - late Isolate isolate; - - late ScriptList scripts; - late ScriptRef mainScript; - - setUp(() async { - setCurrentLogWriter(debug: debug); - service = context.service; - vm = await service.getVM(); - isolate = await service.getIsolate(vm.isolates!.first.id!); - scripts = await service.getScripts(isolate.id!); - mainScript = scripts.scripts! - .firstWhere((each) => each.uri!.contains('main.dart')); - }); - - test('addBreakpoint', () async { - final line = await context.findBreakpointLine( - 'printHelloWorld', - isolate.id!, - mainScript, - ); - final firstBp = - await service.addBreakpoint(isolate.id!, mainScript.id!, line); - expect(firstBp, isNotNull); - expect(firstBp.id, isNotNull); - - final secondBp = - await service.addBreakpoint(isolate.id!, mainScript.id!, line); - expect(secondBp, isNotNull); - expect(secondBp.id, isNotNull); - - expect(firstBp.id, equals(secondBp.id)); - - // Remove breakpoint so it doesn't impact other tests. - await service.removeBreakpoint(isolate.id!, firstBp.id!); - }); - - test('addBreakpoint succeeds when sending the same breakpoint twice', - () async { - final line = await context.findBreakpointLine( - 'printHelloWorld', - isolate.id!, - mainScript, - ); - final firstBp = - service.addBreakpoint(isolate.id!, mainScript.id!, line); - final secondBp = - service.addBreakpoint(isolate.id!, mainScript.id!, line); - - // Remove breakpoint so it doesn't impact other tests. - await service.removeBreakpoint(isolate.id!, (await firstBp).id!); - expect((await firstBp).id, equals((await secondBp).id)); - }); - - test('addBreakpoint in nonsense location throws', () async { - expect( - service.addBreakpoint(isolate.id!, mainScript.id!, 200000), - throwsA(predicate((dynamic e) => e is RPCError && e.code == 102)), - ); - }); - - test('addBreakpoint on a part file', () async { - final partScript = scripts.scripts! - .firstWhere((script) => script.uri!.contains('part.dart')); - final bp = await service.addBreakpoint(isolate.id!, partScript.id!, 10); - // Remove breakpoint so it doesn't impact other tests. - await service.removeBreakpoint(isolate.id!, bp.id!); - expect(bp.id, isNotNull); - }); - - test('addBreakpointAtEntry', () async { - await expectLater(service.addBreakpointAtEntry('', ''), throwsRPCError); - }); - - test('addBreakpointWithScriptUri', () async { - final line = await context.findBreakpointLine( - 'printHelloWorld', - isolate.id!, - mainScript, - ); - final bp = await service.addBreakpointWithScriptUri( - isolate.id!, - mainScript.uri!, - line, - ); - // Remove breakpoint so it doesn't impact other tests. - await service.removeBreakpoint(isolate.id!, bp.id!); - expect(bp.id, isNotNull); - }); - - test('addBreakpointWithScriptUri absolute file URI', () async { - final test = context.project.absolutePackageDirectory; - final scriptPath = Uri.parse(mainScript.uri!).path.substring(1); - final fullPath = path.join(test, scriptPath); - final fileUri = Uri.file(fullPath); - final line = await context.findBreakpointLine( - 'printHelloWorld', - isolate.id!, - mainScript, - ); - final bp = await service.addBreakpointWithScriptUri( - isolate.id!, - '$fileUri', - line, - ); - // Remove breakpoint so it doesn't impact other tests. - await service.removeBreakpoint(isolate.id!, bp.id!); - expect(bp.id, isNotNull); - }); - - test('removeBreakpoint null arguments', () async { - await expectLater( - service.removeBreakpoint('', ''), - throwsSentinelException, - ); - await expectLater( - service.removeBreakpoint(isolate.id!, ''), - throwsRPCError, - ); - }); - - test("removeBreakpoint that doesn't exist fails", () async { - await expectLater( - service.removeBreakpoint(isolate.id!, '1234'), - throwsRPCError, - ); - }); - - test('add and remove breakpoint', () async { - final line = await context.findBreakpointLine( - 'printHelloWorld', - isolate.id!, - mainScript, - ); - final bp = - await service.addBreakpoint(isolate.id!, mainScript.id!, line); - expect(isolate.breakpoints, [bp]); - await service.removeBreakpoint(isolate.id!, bp.id!); - expect(isolate.breakpoints, isEmpty); - }); - }); - - group('callServiceExtension', () { - late ChromeProxyService service; - - setUp(() { - setCurrentLogWriter(debug: debug); - service = context.service; - }); - - test( - 'success', - () async { - final serviceMethod = 'ext.test.callServiceExtension'; - await context.tabConnection.runtime - .evaluate('registerExtension("$serviceMethod");'); - - // The non-string keys/values get auto json-encoded to match the vm - // behavior. - final args = { - 'bool': true, - 'list': [1, '2', 3], - 'map': {'foo': 'bar'}, - 'num': 1.0, - 'string': 'hello', - 1: 2, - false: true, - }; - - final result = - await service.callServiceExtension(serviceMethod, args: args); - expect( - result.json, - args.map( - (k, v) => MapEntry( - k is String ? k : jsonEncode(k), - v is String ? v : jsonEncode(v), - ), - ), - ); - }, - onPlatform: { - 'windows': - const Skip('https://github.com/dart-lang/webdev/issues/711'), - }, - ); - - test( - 'failure', - () async { - final serviceMethod = 'ext.test.callServiceExtensionWithError'; - await context.tabConnection.runtime - .evaluate('registerExtensionWithError("$serviceMethod");'); - - final errorDetails = {'intentional': 'error'}; - expect( - service.callServiceExtension( - serviceMethod, - args: { - 'code': '-32001', - 'details': jsonEncode(errorDetails), - }, - ), - throwsA( - predicate( - (dynamic error) => - error is RPCError && - error.code == -32001 && - error.details == jsonEncode(errorDetails), - ), - ), - ); - }, - onPlatform: { - 'windows': - const Skip('https://github.com/dart-lang/webdev/issues/711'), - }, - ); - }); - - group('VMTimeline', () { - late VmServiceInterface service; - - setUp(() { - setCurrentLogWriter(debug: debug); - service = context.service; - }); - - test('clearVMTimeline', () async { - await expectLater(service.clearVMTimeline(), throwsRPCError); - }); - - test('getVMTimelineMicros', () async { - await expectLater(service.getVMTimelineMicros(), throwsRPCError); - }); - - test('getVMTimeline', () async { - await expectLater(service.getVMTimeline(), throwsRPCError); - }); - - test('getVMTimelineFlags', () async { - await expectLater(service.getVMTimelineFlags(), throwsRPCError); - }); - - test('setVMTimelineFlags', () async { - await expectLater( - service.setVMTimelineFlags([]), - throwsRPCError, - ); - }); - }); - - test('getMemoryUsage', () async { - final service = context.service; - final vm = await service.getVM(); - final isolate = await service.getIsolate(vm.isolates!.first.id!); - - final memoryUsage = await service.getMemoryUsage(isolate.id!); - - expect(memoryUsage.heapUsage, isNotNull); - expect(memoryUsage.heapUsage, greaterThan(0)); - expect(memoryUsage.heapCapacity, greaterThan(0)); - expect(memoryUsage.externalUsage, equals(0)); - }); - - group('evaluate', () { - late VmServiceInterface service; - late Isolate isolate; - LibraryRef? bootstrap; - - setUpAll(() async { - setCurrentLogWriter(debug: debug); - service = context.service; - final vm = await service.getVM(); - isolate = await service.getIsolate(vm.isolates!.first.id!); - bootstrap = isolate.rootLib; - }); - - group('top level methods', () { - setUp(() { - setCurrentLogWriter(debug: debug); - }); - - test('can return strings', () async { - expect( - await service.evaluate( - isolate.id!, - bootstrap!.id!, - "helloString('world')", - ), - const TypeMatcher().having( - (instance) => instance.valueAsString, - 'value', - 'world', - ), - ); - }); - - test('can return bools', () async { - expect( - await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'helloBool(true)', - ), - const TypeMatcher().having( - (instance) => instance.valueAsString, - 'valueAsString', - 'true', - ), - ); - expect( - await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'helloBool(false)', - ), - const TypeMatcher().having( - (instance) => instance.valueAsString, - 'valueAsString', - 'false', - ), - ); - }); - - test('can return nums', () async { - expect( - await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'helloNum(42.0)', - ), - const TypeMatcher().having( - (instance) => instance.valueAsString, - 'valueAsString', - '42', - ), - ); - expect( - await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'helloNum(42.2)', - ), - const TypeMatcher().having( - (instance) => instance.valueAsString, - 'valueAsString', - '42.2', - ), - ); - }); - - test('can return objects with ids', () async { - final object = await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'createObject("cool")', - ); - expect( - object, - const TypeMatcher() - .having((instance) => instance.id, 'id', isNotNull), - ); - // TODO(jakemac): Add tests for the ClassRef once we create one, - // https://github.com/dart-lang/sdk/issues/36771. - }); - - group('with provided scope', () { - setUp(() { - setCurrentLogWriter(debug: debug); - }); - - Future createRemoteObject(String message) async { - return await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'createObject("$message")', - ) as InstanceRef; - } - - test('single scope object', () async { - final instance = await createRemoteObject('A'); - final result = await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'messageFor(arg1)', - scope: {'arg1': instance.id!}, - ); - expect( - result, - const TypeMatcher().having( - (instance) => instance.valueAsString, - 'valueAsString', - 'A', - ), - ); - }); - - test('multiple scope objects', () async { - final instance1 = await createRemoteObject('A'); - final instance2 = await createRemoteObject('B'); - final result = await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'messagesCombined(arg1, arg2)', - scope: {'arg1': instance1.id!, 'arg2': instance2.id!}, - ); - expect( - result, - const TypeMatcher().having( - (instance) => instance.valueAsString, - 'valueAsString', - 'AB', - ), - ); - }); - }); - }); - }); - - test('evaluateInFrame', () async { - final service = context.service; - await expectLater( - service.evaluateInFrame('', 0, ''), - throwsSentinelException, - ); - }); - - test('getAllocationProfile', () async { - final service = context.service; - await expectLater(service.getAllocationProfile(''), throwsRPCError); - }); - - test('getClassList', () async { - final service = context.service; - await expectLater(service.getClassList(''), throwsRPCError); - }); - - test('getFlagList', () async { - final service = context.service; - expect(await service.getFlagList(), isA()); - }); - - test('getInstances', () async { - final service = context.service; - await expectLater(service.getInstances('', '', 0), throwsRPCError); - }); - - group('getIsolate', () { - late VmServiceInterface service; - setUp(() { - setCurrentLogWriter(debug: debug); - service = context.service; - }); - - test('works for existing isolates', () async { - final vm = await service.getVM(); - final result = await service.getIsolate(vm.isolates!.first.id!); - expect(result, const TypeMatcher()); - final isolate = result; - expect(isolate.name, contains('main')); - // TODO: library names change with kernel dart-lang/sdk#36736 - expect(isolate.rootLib!.uri, endsWith('.dart')); - - expect( - isolate.libraries, - containsAll([ - _libRef('package:path/path.dart'), - // TODO: library names change with kernel dart-lang/sdk#36736 - _libRef(endsWith('main.dart')), - ]), - ); - expect(isolate.extensionRPCs, contains('ext.hello_world.existing')); - }); - - test('throws for invalid ids', () async { - expect(service.getIsolate('bad'), throwsSentinelException); - }); - }); - - group('getObject', () { - late ChromeProxyService service; - late Isolate isolate; - LibraryRef? bootstrap; - - Library? rootLibrary; - - setUpAll(() async { - setCurrentLogWriter(debug: debug); - service = context.service; - final vm = await service.getVM(); - isolate = await service.getIsolate(vm.isolates!.first.id!); - bootstrap = isolate.rootLib; - rootLibrary = - await service.getObject(isolate.id!, bootstrap!.id!) as Library; - }); - - setUp(() { - setCurrentLogWriter(debug: debug); - }); - - test('root Library', () async { - expect(rootLibrary, isNotNull); - // TODO: library names change with kernel dart-lang/sdk#36736 - expect(rootLibrary!.uri, endsWith('main.dart')); - expect(rootLibrary!.classes, hasLength(1)); - final testClass = rootLibrary!.classes!.first; - expect(testClass.name, 'MyTestClass'); - }); - - test('Library only contains included scripts', () async { - final library = - await service.getObject(isolate.id!, rootLibrary!.id!) as Library; - expect(library.scripts, hasLength(2)); - expect( - library.scripts, - unorderedEquals([ - predicate( - (ScriptRef s) => - s.uri == 'org-dartlang-app:///example/hello_world/main.dart', - ), - predicate( - (ScriptRef s) => - s.uri == 'org-dartlang-app:///example/hello_world/part.dart', - ), - ]), - ); - }); - - test('Can get the same library in parallel', () async { - final futures = [ - service.getObject(isolate.id!, rootLibrary!.id!), - service.getObject(isolate.id!, rootLibrary!.id!), - ]; - final results = await Future.wait(futures); - final library1 = results[0] as Library; - final library2 = results[1] as Library; - expect(library1, equals(library2)); - }); - - test('Classes', () async { - final testClass = await service.getObject( - isolate.id!, - rootLibrary!.classes!.first.id!, - ) as Class; - expect( - testClass.functions, - unorderedEquals([ - predicate((FuncRef f) => f.name == 'staticHello' && f.isStatic!), - predicate((FuncRef f) => f.name == 'hello' && !f.isStatic!), - predicate((FuncRef f) => f.name == 'hashCode' && !f.isStatic!), - predicate((FuncRef f) => f.name == 'runtimeType' && !f.isStatic!), - ]), - ); - expect( - testClass.fields, - unorderedEquals([ - predicate( - (FieldRef f) => - f.name == 'message' && - f.declaredType != null && - !f.isStatic! && - !f.isConst! && - f.isFinal!, - ), - predicate( - (FieldRef f) => - f.name == 'notFinal' && - f.declaredType != null && - !f.isStatic! && - !f.isConst! && - !f.isFinal!, - ), - predicate( - (FieldRef f) => - f.name == 'staticMessage' && - f.declaredType != null && - f.isStatic! && - !f.isConst! && - !f.isFinal!, - ), - ]), - ); - }); - - test('Runtime classes', () async { - final testClass = await service.getObject( - isolate.id!, - 'classes|dart:_runtime|_Type', - ) as Class; - expect(testClass.name, '_Type'); - }); - - test('String', () async { - final worldRef = await service.evaluate( - isolate.id!, - bootstrap!.id!, - "helloString('world')", - ) as InstanceRef; - final world = - await service.getObject(isolate.id!, worldRef.id!) as Instance; - expect(world.valueAsString, 'world'); - }); - - test('Large strings not truncated', () async { - final largeString = await service.evaluate( - isolate.id!, - bootstrap!.id!, - "helloString('${'abcde' * 250}')", - ) as InstanceRef; - expect(largeString.valueAsStringIsTruncated, isNot(isTrue)); - expect(largeString.valueAsString!.length, largeString.length); - expect(largeString.length, 5 * 250); - }); - - test('Lists', () async { - final list = await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'topLevelList', - ) as InstanceRef; - final inst = await service.getObject(isolate.id!, list.id!) as Instance; - expect(inst.length, 1001); - expect(inst.offset, null); - expect(inst.count, null); - expect(inst.elements!.length, 1001); - final fifth = inst.elements![4] as InstanceRef; - expect(fifth.valueAsString, '100'); - final sixth = inst.elements![5] as InstanceRef; - expect(sixth.valueAsString, '5'); - }); - - test('Maps', () async { - final map = await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'topLevelMap', - ) as InstanceRef; - final inst = await service.getObject(isolate.id!, map.id!) as Instance; - expect(inst.length, 1001); - expect(inst.offset, null); - expect(inst.count, null); - expect(inst.associations!.length, 1001); - final fifth = inst.associations![4]; - expect(fifth.key.valueAsString, '4'); - expect(fifth.value.valueAsString, '996'); - final sixth = inst.associations![5]; - expect(sixth.key.valueAsString, '5'); - expect(sixth.value.valueAsString, '995'); - }); - - test('bool', () async { - final ref = await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'helloBool(true)', - ) as InstanceRef; - final obj = await service.getObject(isolate.id!, ref.id!) as Instance; - expect(obj.kind, InstanceKind.kBool); - expect(obj.classRef!.name, 'Bool'); - expect(obj.valueAsString, 'true'); - }); - - test('num', () async { - final ref = await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'helloNum(42)', - ) as InstanceRef; - final obj = await service.getObject(isolate.id!, ref.id!) as Instance; - expect(obj.kind, InstanceKind.kDouble); - expect(obj.classRef!.name, 'Double'); - expect(obj.valueAsString, '42'); - }); - - test('Scripts', () async { - final scripts = await service.getScripts(isolate.id!); - assert(scripts.scripts!.isNotEmpty); - for (final scriptRef in scripts.scripts!) { - final script = - await service.getObject(isolate.id!, scriptRef.id!) as Script; - final serverPath = DartUri(script.uri!, 'hello_world/').serverPath; - final result = await http - .get(Uri.parse('http://localhost:${context.port}/$serverPath')); - expect(script.source, result.body); - expect(scriptRef.uri, endsWith('.dart')); - expect(script.tokenPosTable, isNotEmpty); - } - }); - - group('getObject called with offset/count parameters', () { - test('Lists with null offset and count are not truncated', () async { - final list = await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'topLevelList', - ) as InstanceRef; - final inst = await service.getObject( - isolate.id!, - list.id!, - count: null, - offset: null, - ) as Instance; - expect(inst.length, 1001); - expect(inst.offset, null); - expect(inst.count, null); - expect(inst.elements!.length, 1001); - final fifth = inst.elements![4] as InstanceRef; - expect(fifth.valueAsString, '100'); - final sixth = inst.elements![5] as InstanceRef; - expect(sixth.valueAsString, '5'); - }); - - test('Lists with null count are not truncated', () async { - final list = await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'topLevelList', - ) as InstanceRef; - final inst = await service.getObject( - isolate.id!, - list.id!, - count: null, - offset: 0, - ) as Instance; - expect(inst.length, 1001); - expect(inst.offset, 0); - expect(inst.count, null); - expect(inst.elements!.length, 1001); - final fifth = inst.elements![4] as InstanceRef; - expect(fifth.valueAsString, '100'); - final sixth = inst.elements![5] as InstanceRef; - expect(sixth.valueAsString, '5'); - }); - - test( - 'Lists with null count and offset greater than 0 are ' - 'truncated from offset to end of list', () async { - final list = await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'topLevelList', - ) as InstanceRef; - final inst = await service.getObject( - isolate.id!, - list.id!, - count: null, - offset: 1000, - ) as Instance; - expect(inst.length, 1001); - expect(inst.offset, 1000); - expect(inst.count, null); - expect(inst.elements!.length, 1); - final only = inst.elements![0] as InstanceRef; - expect(only.valueAsString, '5'); - }); - - test('Lists with offset/count are truncated', () async { - final list = await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'topLevelList', - ) as InstanceRef; - final inst = await service.getObject( - isolate.id!, - list.id!, - count: 7, - offset: 4, - ) as Instance; - expect(inst.length, 1001); - expect(inst.offset, 4); - expect(inst.count, 7); - expect(inst.elements!.length, 7); - final fifth = inst.elements![0] as InstanceRef; - expect(fifth.valueAsString, '100'); - final sixth = inst.elements![1] as InstanceRef; - expect(sixth.valueAsString, '5'); - }); - - test('Lists are truncated to the end if offset/count runs off the end', - () async { - final list = await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'topLevelList', - ) as InstanceRef; - final inst = await service.getObject( - isolate.id!, - list.id!, - count: 5, - offset: 1000, - ) as Instance; - expect(inst.length, 1001); - expect(inst.offset, 1000); - expect(inst.count, 1); - expect(inst.elements!.length, 1); - final only = inst.elements![0] as InstanceRef; - expect(only.valueAsString, '5'); - }); - - test('Lists are truncated to empty if offset runs off the end', - () async { - final list = await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'topLevelList', - ) as InstanceRef; - final inst = await service.getObject( - isolate.id!, - list.id!, - count: 5, - offset: 1002, - ) as Instance; - expect(inst.elements!.length, 0); - expect(inst.length, 1001); - expect(inst.offset, 1002); - expect(inst.count, 0); - expect(inst.elements!.length, 0); - }); - - test('Lists are truncated to empty with 0 count and null offset', - () async { - final list = await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'topLevelList', - ) as InstanceRef; - final inst = await service.getObject( - isolate.id!, - list.id!, - count: 0, - offset: null, - ) as Instance; - expect(inst.elements!.length, 0); - expect(inst.length, 1001); - expect(inst.offset, null); - expect(inst.count, 0); - expect(inst.elements!.length, 0); - }); - - test('Maps with null offset/count are not truncated', () async { - final map = await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'topLevelMap', - ) as InstanceRef; - final inst = await service.getObject( - isolate.id!, - map.id!, - count: null, - offset: null, - ) as Instance; - expect(inst.length, 1001); - expect(inst.offset, null); - expect(inst.count, null); - expect(inst.associations!.length, 1001); - final fifth = inst.associations![4]; - expect(fifth.key.valueAsString, '4'); - expect(fifth.value.valueAsString, '996'); - final sixth = inst.associations![5]; - expect(sixth.key.valueAsString, '5'); - expect(sixth.value.valueAsString, '995'); - }); - - test( - 'Maps with null count and offset greater than 0 are ' - 'truncated from offset to end of map', () async { - final map = await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'topLevelMap', - ) as InstanceRef; - final inst = await service.getObject( - isolate.id!, - map.id!, - count: null, - offset: 1000, - ) as Instance; - expect(inst.length, 1001); - expect(inst.offset, 1000); - expect(inst.count, null); - expect(inst.associations!.length, 1); - final only = inst.associations![0]; - expect(only.key.valueAsString, '1000'); - expect(only.value.valueAsString, '0'); - }); - - test('Maps with null count are not truncated', () async { - final map = await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'topLevelMap', - ) as InstanceRef; - final inst = await service.getObject( - isolate.id!, - map.id!, - count: null, - offset: 0, - ) as Instance; - expect(inst.length, 1001); - expect(inst.offset, 0); - expect(inst.count, null); - expect(inst.associations!.length, 1001); - final fifth = inst.associations![4]; - expect(fifth.key.valueAsString, '4'); - expect(fifth.value.valueAsString, '996'); - final sixth = inst.associations![5]; - expect(sixth.key.valueAsString, '5'); - expect(sixth.value.valueAsString, '995'); - }); - - test('Maps with offset/count are truncated', () async { - final map = await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'topLevelMap', - ) as InstanceRef; - final inst = await service.getObject( - isolate.id!, - map.id!, - count: 7, - offset: 4, - ) as Instance; - expect(inst.length, 1001); - expect(inst.offset, 4); - expect(inst.count, 7); - expect(inst.associations!.length, 7); - final fifth = inst.associations![0]; - expect(fifth.key.valueAsString, '4'); - expect(fifth.value.valueAsString, '996'); - final sixth = inst.associations![1]; - expect(sixth.key.valueAsString, '5'); - expect(sixth.value.valueAsString, '995'); - }); - - test('Maps are truncated to the end if offset/count runs off the end', - () async { - final map = await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'topLevelMap', - ) as InstanceRef; - final inst = await service.getObject( - isolate.id!, - map.id!, - count: 5, - offset: 1000, - ) as Instance; - expect(inst.length, 1001); - expect(inst.offset, 1000); - expect(inst.count, 1); - expect(inst.associations!.length, 1); - final only = inst.associations![0]; - expect(only.key.valueAsString, '1000'); - expect(only.value.valueAsString, '0'); - }); - - test('Maps are truncated to empty if offset runs off the end', - () async { - final map = await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'topLevelMap', - ) as InstanceRef; - final inst = await service.getObject( - isolate.id!, - map.id!, - count: 5, - offset: 1002, - ) as Instance; - expect(inst.associations!.length, 0); - expect(inst.length, 1001); - expect(inst.offset, 1002); - expect(inst.count, 0); - expect(inst.associations!.length, 0); - }); - - test('Strings with offset/count are truncated', () async { - final worldRef = await service.evaluate( - isolate.id!, - bootstrap!.id!, - "helloString('world')", - ) as InstanceRef; - final world = await service.getObject( - isolate.id!, - worldRef.id!, - count: 2, - offset: 1, - ) as Instance; - expect(world.valueAsString, 'or'); - expect(world.count, 2); - expect(world.length, 5); - expect(world.offset, 1); - }); - - test('Maps are truncated to empty if offset runs off the end', - () async { - final map = await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'topLevelMap', - ) as InstanceRef; - final inst = await service.getObject( - isolate.id!, - map.id!, - count: 5, - offset: 1002, - ) as Instance; - expect(inst.associations!.length, 0); - expect(inst.length, 1001); - expect(inst.offset, 1002); - expect(inst.count, 0); - expect(inst.associations!.length, 0); - }); - - test('Maps are truncated to empty with 0 count and null offset', - () async { - final map = await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'topLevelMap', - ) as InstanceRef; - final inst = await service.getObject( - isolate.id!, - map.id!, - count: 0, - offset: null, - ) as Instance; - expect(inst.associations!.length, 0); - expect(inst.length, 1001); - expect(inst.offset, null); - expect(inst.count, 0); - expect(inst.associations!.length, 0); - }); - - test( - 'Strings are truncated to the end if offset/count runs off the end', - () async { - final worldRef = await service.evaluate( - isolate.id!, - bootstrap!.id!, - "helloString('world')", - ) as InstanceRef; - final world = await service.getObject( - isolate.id!, - worldRef.id!, - count: 5, - offset: 3, - ) as Instance; - expect(world.valueAsString, 'ld'); - expect(world.count, 2); - expect(world.length, 5); - expect(world.offset, 3); - }); - - test( - 'offset/count parameters greater than zero are ignored for Classes', - () async { - final testClass = await service.getObject( - isolate.id!, - rootLibrary!.classes!.first.id!, - offset: 100, - count: 100, - ) as Class; - expect( - testClass.functions, - unorderedEquals([ - predicate((FuncRef f) => f.name == 'staticHello' && f.isStatic!), - predicate((FuncRef f) => f.name == 'hello' && !f.isStatic!), - predicate((FuncRef f) => f.name == 'hashCode' && !f.isStatic!), - predicate((FuncRef f) => f.name == 'runtimeType' && !f.isStatic!), - ]), - ); - expect( - testClass.fields, - unorderedEquals([ - predicate( - (FieldRef f) => - f.name == 'message' && - f.declaredType != null && - !f.isStatic! && - !f.isConst! && - f.isFinal!, - ), - predicate( - (FieldRef f) => - f.name == 'notFinal' && - f.declaredType != null && - !f.isStatic! && - !f.isConst! && - !f.isFinal!, - ), - predicate( - (FieldRef f) => - f.name == 'staticMessage' && - f.declaredType != null && - f.isStatic! && - !f.isConst! && - !f.isFinal!, - ), - ]), - ); - }); - - test('offset/count parameters equal to zero are ignored for Classes', - () async { - final testClass = await service.getObject( - isolate.id!, - rootLibrary!.classes!.first.id!, - offset: 0, - count: 0, - ) as Class; - expect( - testClass.functions, - unorderedEquals([ - predicate((FuncRef f) => f.name == 'staticHello' && f.isStatic!), - predicate((FuncRef f) => f.name == 'hello' && !f.isStatic!), - predicate((FuncRef f) => f.name == 'hashCode' && !f.isStatic!), - predicate((FuncRef f) => f.name == 'runtimeType' && !f.isStatic!), - ]), - ); - expect( - testClass.fields, - unorderedEquals([ - predicate( - (FieldRef f) => - f.name == 'message' && - f.declaredType != null && - !f.isStatic! && - !f.isConst! && - f.isFinal!, - ), - predicate( - (FieldRef f) => - f.name == 'notFinal' && - f.declaredType != null && - !f.isStatic! && - !f.isConst! && - !f.isFinal!, - ), - predicate( - (FieldRef f) => - f.name == 'staticMessage' && - f.declaredType != null && - f.isStatic! && - !f.isConst! && - !f.isFinal!, - ), - ]), - ); - }); - - test('offset/count parameters are ignored for bools', () async { - final ref = await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'helloBool(true)', - ) as InstanceRef; - final obj = await service.getObject( - isolate.id!, - ref.id!, - offset: 100, - count: 100, - ) as Instance; - expect(obj.kind, InstanceKind.kBool); - expect(obj.classRef!.name, 'Bool'); - expect(obj.valueAsString, 'true'); - }); - - test('offset/count parameters are ignored for nums', () async { - final ref = await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'helloNum(42)', - ) as InstanceRef; - final obj = await service.getObject( - isolate.id!, - ref.id!, - offset: 100, - count: 100, - ) as Instance; - expect(obj.kind, InstanceKind.kDouble); - expect(obj.classRef!.name, 'Double'); - expect(obj.valueAsString, '42'); - }); - - test('offset/count parameters are ignored for null', () async { - final ref = await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'helloNum(null)', - ) as InstanceRef; - final obj = await service.getObject( - isolate.id!, - ref.id!, - offset: 100, - count: 100, - ) as Instance; - expect(obj.kind, InstanceKind.kNull); - expect(obj.classRef!.name, 'Null'); - expect(obj.valueAsString, 'null'); - }); - }); - }); - - test('getScripts', () async { - final service = context.service; - final vm = await service.getVM(); - final isolateId = vm.isolates!.first.id!; - final scripts = await service.getScripts(isolateId); - expect(scripts, isNotNull); - expect(scripts.scripts, isNotEmpty); - - final scriptUris = scripts.scripts!.map((s) => s.uri); - - // Contains main script only once. - expect( - scriptUris.where((uri) => uri!.contains('hello_world/main.dart')), - hasLength(1), - ); - - // Contains a known script. - expect(scriptUris, contains('package:path/path.dart')); - - // Contains part files as well. - expect(scriptUris, contains(endsWith('part.dart'))); - expect( - scriptUris, - contains('package:intl/src/date_format_internal.dart'), - ); - }); - - group('getSourceReport', () { - late VmServiceInterface service; - - setUp(() { - setCurrentLogWriter(debug: debug); - service = context.service; - }); - - test('Coverage report', () async { - final vm = await service.getVM(); - final isolateId = vm.isolates!.first.id!; - - await expectLater( - service.getSourceReport(isolateId, ['Coverage']), - throwsRPCError, - ); - }); - - test('Coverage report', () async { - final vm = await service.getVM(); - final isolateId = vm.isolates!.first.id!; - - await expectLater( - service.getSourceReport( - isolateId, - ['Coverage'], - libraryFilters: ['foo'], - ), - throwsRPCError, - ); - }); - - test('report type not understood', () async { - final vm = await service.getVM(); - final isolateId = vm.isolates!.first.id!; - - await expectLater( - service.getSourceReport(isolateId, ['FooBar']), - throwsRPCError, - ); - }); - - test('PossibleBreakpoints report', () async { - final vm = await service.getVM(); - final isolateId = vm.isolates!.first.id!; - final scripts = await service.getScripts(isolateId); - final mainScript = scripts.scripts! - .firstWhere((script) => script.uri!.contains('main.dart')); - - final sourceReport = await service.getSourceReport( - isolateId, - ['PossibleBreakpoints'], - scriptId: mainScript.id, - ); - - expect(sourceReport.scripts, isNotEmpty); - expect(sourceReport.ranges, isNotEmpty); - - final sourceReportRange = sourceReport.ranges!.first; - expect(sourceReportRange.possibleBreakpoints, isNotEmpty); - }); - }); - - group('Pausing', () { - late VmServiceInterface service; - String? isolateId; - late Stream stream; - ScriptList scripts; - late ScriptRef mainScript; - - setUp(() async { - setCurrentLogWriter(debug: debug); - service = context.service; - final vm = await service.getVM(); - isolateId = vm.isolates!.first.id; - scripts = await service.getScripts(isolateId!); - await service.streamListen('Debug'); - stream = service.onEvent('Debug'); - mainScript = scripts.scripts! - .firstWhere((script) => script.uri!.contains('main.dart')); - }); - - test('at breakpoints sets pauseBreakPoints', () async { - final line = await context.findBreakpointLine( - 'callPrintCount', - isolateId!, - mainScript, - ); - final bp = - await service.addBreakpoint(isolateId!, mainScript.id!, line); - final event = await stream - .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint); - final pauseBreakpoints = event.pauseBreakpoints!; - expect(pauseBreakpoints, hasLength(1)); - expect(pauseBreakpoints.first.id, bp.id); - await service.removeBreakpoint(isolateId!, bp.id!); - // Resume execution to not impact other tests. - await service.resume(isolateId!); - }); - - test('resuming throws kIsolateMustBePaused error if not paused', - () async { - await expectLater( - service.resume(isolateId!), - throwsRPCErrorWithCode(RPCErrorKind.kIsolateMustBePaused.code), - ); - }); - }); - - group('Step', () { - late VmServiceInterface service; - String? isolateId; - late Stream stream; - ScriptList scripts; - ScriptRef mainScript; - - setUp(() async { - setCurrentLogWriter(debug: debug); - service = context.service; - final vm = await service.getVM(); - isolateId = vm.isolates!.first.id; - scripts = await service.getScripts(isolateId!); - await service.streamListen('Debug'); - stream = service.onEvent('Debug'); - mainScript = scripts.scripts! - .firstWhere((script) => script.uri!.contains('main.dart')); - final line = await context.findBreakpointLine( - 'callPrintCount', - isolateId!, - mainScript, - ); - final bp = - await service.addBreakpoint(isolateId!, mainScript.id!, line); - // Wait for breakpoint to trigger. - await stream - .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint); - await service.removeBreakpoint(isolateId!, bp.id!); - }); - - tearDown(() async { - // Resume execution to not impact other tests. - await service.resume(isolateId!); - }); - - test('Into goes to the next Dart location', () async { - await service.resume(isolateId!, step: 'Into'); - // Wait for the step to actually occur. - await stream - .firstWhere((event) => event.kind == EventKind.kPauseInterrupted); - final stack = await service.getStack(isolateId!); - expect(stack, isNotNull); - final first = stack.frames!.first; - expect(first.kind, 'Regular'); - expect(first.code!.kind, 'Dart'); - expect(first.code!.name, 'printCount'); - }); - - test('Over goes to the next Dart location', () async { - await service.resume(isolateId!, step: 'Over'); - // Wait for the step to actually occur. - await stream - .firstWhere((event) => event.kind == EventKind.kPauseInterrupted); - final stack = await service.getStack(isolateId!); - expect(stack, isNotNull); - final first = stack.frames!.first; - expect(first.kind, 'Regular'); - expect(first.code!.kind, 'Dart'); - expect(first.code!.name, ''); - }); - - test('Out goes to the next Dart location', () async { - await service.resume(isolateId!, step: 'Out'); - // Wait for the step to actually occur. - await stream - .firstWhere((event) => event.kind == EventKind.kPauseInterrupted); - final stack = await service.getStack(isolateId!); - expect(stack, isNotNull); - final first = stack.frames!.first; - expect(first.kind, 'Regular'); - expect(first.code!.kind, 'Dart'); - expect(first.code!.name, ''); - }); - }); - - group('getStack', () { - late VmServiceInterface service; - String? isolateId; - late Stream stream; - ScriptList scripts; - late ScriptRef mainScript; - - setUp(() async { - setCurrentLogWriter(debug: debug); - service = context.service; - final vm = await service.getVM(); - isolateId = vm.isolates!.first.id; - scripts = await service.getScripts(isolateId!); - await service.streamListen('Debug'); - stream = service.onEvent('Debug'); - mainScript = scripts.scripts! - .firstWhere((each) => each.uri!.contains('main.dart')); - }); - - test( - 'throws if not paused', - () async { - await expectLater(service.getStack(isolateId!), throwsRPCError); - }, - skip: Platform.isWindows, - ); // Issue: https://github.com/dart-lang/webdev/issues/1749 - - /// Support function for pausing and returning the stack at a line. - Future breakAt(String breakpointId, {int? limit}) async { - final line = await context.findBreakpointLine( - breakpointId, - isolateId!, - mainScript, - ); - Breakpoint? bp; - try { - bp = await service.addBreakpoint(isolateId!, mainScript.id!, line); - // Wait for breakpoint to trigger. - await stream - .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint); - return await service.getStack(isolateId!, limit: limit); - } finally { - // Remove breakpoint and resume so it doesn't impact other tests. - if (bp != null) { - await service.removeBreakpoint(isolateId!, bp.id!); - } - await service.resume(isolateId!); - } - } - - test('returns stack when broken', () async { - final stack = await breakAt('inPrintCount'); - expect(stack, isNotNull); - expect(stack.frames, hasLength(2)); - final first = stack.frames!.first; - expect(first.kind, 'Regular'); - expect(first.code!.kind, 'Dart'); - expect(first.code!.name, 'printCount'); - }); - - test('stack has a variable', () async { - final stack = await breakAt('callPrintCount'); - expect(stack, isNotNull); - expect(stack.frames, hasLength(1)); - final first = stack.frames!.first; - expect(first.kind, 'Regular'); - expect(first.code!.kind, 'Dart'); - expect(first.code!.name, ''); - // TODO: Make this more precise once this case doesn't - // also include all the libraries. - expect(first.vars, hasLength(greaterThanOrEqualTo(1))); - final underscore = first.vars!.firstWhere((v) => v.name == '_'); - expect(underscore, isNotNull); - }); - - test('collects async frames', () async { - final stack = await breakAt('asyncCall'); - expect(stack, isNotNull); - expect(stack.frames, hasLength(greaterThan(1))); - - final first = stack.frames!.first; - expect(first.kind, 'Regular'); - expect(first.code!.kind, 'Dart'); - - // We should have an async marker. - final suspensionFrames = stack.frames! - .where((frame) => frame.kind == FrameKind.kAsyncSuspensionMarker); - expect(suspensionFrames, isNotEmpty); - - // We should have async frames. - final asyncFrames = stack.frames! - .where((frame) => frame.kind == FrameKind.kAsyncCausal); - expect(asyncFrames, isNotEmpty); - }); - - test('returns the correct number of frames when a limit is provided', - () async { - var stack = await breakAt('asyncCall', limit: 4); - expect(stack, isNotNull); - expect(stack.frames, hasLength(equals(4))); - stack = await breakAt('asyncCall', limit: 2); - expect(stack, isNotNull); - expect(stack.frames, hasLength(equals(2))); - stack = await breakAt('asyncCall'); - expect(stack, isNotNull); - expect(stack.frames, hasLength(equals(5))); - }); - - test('truncated stacks are properly indicated', () async { - var stack = await breakAt('asyncCall', limit: 3); - expect(stack, isNotNull); - expect(stack.truncated, isTrue); - stack = await breakAt('asyncCall'); - expect(stack, isNotNull); - expect(stack.truncated, isFalse); - stack = await breakAt('asyncCall', limit: 20000); - expect(stack, isNotNull); - expect(stack.truncated, isFalse); - }); - - test('break on exceptions with setIsolatePauseMode', () async { - final oldPauseMode = - (await service.getIsolate(isolateId!)).exceptionPauseMode; - await service.setIsolatePauseMode( - isolateId!, - exceptionPauseMode: ExceptionPauseMode.kAll, - ); - // Wait for pausing to actually propagate. - final event = await stream - .firstWhere((event) => event.kind == EventKind.kPauseException); - expect(event.exception, isNotNull); - // Check that the exception stack trace has been mapped to Dart source files. - expect(event.exception!.valueAsString, contains('main.dart')); - - final stack = await service.getStack(isolateId!); - expect(stack, isNotNull); - - await service.setIsolatePauseMode( - isolateId!, - exceptionPauseMode: oldPauseMode, - ); - await service.resume(isolateId!); - }); - - test('returns non-null stack when paused', () async { - await service.pause(isolateId!); - // Wait for pausing to actually propagate. - await stream - .firstWhere((event) => event.kind == EventKind.kPauseInterrupted); - expect(await service.getStack(isolateId!), isNotNull); - // Resume the isolate to not impact other tests. - await service.resume(isolateId!); - }); - }); - - test('getVM', () async { - final service = context.service; - final vm = await service.getVM(); - expect(vm.name, isNotNull); - expect(vm.version, Platform.version); - expect(vm.isolates, hasLength(1)); - final isolate = vm.isolates!.first; - expect(isolate.id, isNotNull); - expect(isolate.name, isNotNull); - expect(isolate.number, isNotNull); - }); - - test('getVersion', () async { - final service = context.service; - final version = await service.getVersion(); - expect(version, isNotNull); - expect(version.major, greaterThan(0)); - }); - - group('invoke', () { - late ChromeProxyService service; - VM vm; - late Isolate isolate; - LibraryRef? bootstrap; - late InstanceRef testInstance; - - setUp(() async { - setCurrentLogWriter(debug: debug); - service = context.service; - vm = await service.getVM(); - isolate = await service.getIsolate(vm.isolates!.first.id!); - bootstrap = isolate.rootLib; - testInstance = await service.evaluate( - isolate.id!, - bootstrap!.id!, - 'myInstance', - ) as InstanceRef; - }); - - test('rootLib', () async { - expect( - bootstrap, - const TypeMatcher().having( - (library) => library.name, - 'name', - 'org-dartlang-app:///example/hello_world/main.dart', - ), - ); - }); - - test('toString()', () async { - final remote = - await service.invoke(isolate.id!, testInstance.id!, 'toString', []); - expect( - remote, - const TypeMatcher().having( - (instance) => instance.valueAsString, - 'toString()', - "Instance of 'MyTestClass'", - ), - ); - }); - - test('hello()', () async { - final remote = - await service.invoke(isolate.id!, testInstance.id!, 'hello', []); - expect( - remote, - const TypeMatcher().having( - (instance) => instance.valueAsString, - 'hello()', - 'world', - ), - ); - }); - - test('helloString', () async { - final remote = await service.invoke( - isolate.id!, - bootstrap!.id!, - 'helloString', - ['#StringInstanceRef#abc'], - ); - expect( - remote, - const TypeMatcher().having( - (instance) => instance.valueAsString, - 'helloString', - 'abc', - ), - ); - expect( - remote, - const TypeMatcher() - .having((instance) => instance.kind, 'kind', 'String'), - ); - }); - - test('null argument', () async { - final remote = await service.invoke( - isolate.id!, - bootstrap!.id!, - 'helloString', - ['objects/null'], - ); - expect( - remote, - const TypeMatcher().having( - (instance) => instance.valueAsString, - 'helloString', - 'null', - ), - ); - expect( - remote, - const TypeMatcher() - .having((instance) => instance.kind, 'kind', 'Null'), - ); - }); - - test('helloBool', () async { - final remote = await service.invoke( - isolate.id!, - bootstrap!.id!, - 'helloBool', - ['objects/bool-true'], - ); - expect( - remote, - const TypeMatcher().having( - (instance) => instance.valueAsString, - 'helloBool', - 'true', - ), - ); - expect( - remote, - const TypeMatcher() - .having((instance) => instance.kind, 'kind', 'Bool'), - ); - }); - - test('helloNum', () async { - final remote = await service.invoke( - isolate.id!, - bootstrap!.id!, - 'helloNum', - ['objects/int-123'], - ); - expect( - remote, - const TypeMatcher().having( - (instance) => instance.valueAsString, - 'helloNum', - '123', - ), - ); - expect( - remote, - const TypeMatcher() - .having((instance) => instance.kind, 'kind', 'Double'), - ); - }); - - test('two object arguments', () async { - final remote = await service.invoke( - isolate.id!, - bootstrap!.id!, - 'messagesCombined', - [testInstance.id, testInstance.id], - ); - expect( - remote, - const TypeMatcher().having( - (instance) => instance.valueAsString, - 'messagesCombined', - 'worldworld', - ), - ); - expect( - remote, - const TypeMatcher() - .having((instance) => instance.kind, 'kind', 'String'), - ); - }); - }); - - test('kill', () async { - final service = context.service; - await expectLater(service.kill(''), throwsRPCError); - }); - - test('onEvent', () async { - final service = context.service; - expect(() => service.onEvent(''), throwsRPCError); - }); - - test('pause / resume', () async { - final service = context.service; - await service.streamListen('Debug'); - final stream = service.onEvent('Debug'); - final vm = await service.getVM(); - final isolateId = vm.isolates!.first.id!; - final pauseCompleter = Completer(); - final pauseSub = context.tabConnection.debugger.onPaused.listen((_) { - pauseCompleter.complete(); - }); - final resumeCompleter = Completer(); - final resumeSub = context.tabConnection.debugger.onResumed.listen((_) { - resumeCompleter.complete(); - }); - expect(await service.pause(isolateId), const TypeMatcher()); - await stream - .firstWhere((event) => event.kind == EventKind.kPauseInterrupted); - expect( - (await service.getIsolate(isolateId)).pauseEvent!.kind, - EventKind.kPauseInterrupted, - ); - await pauseCompleter.future; - expect(await service.resume(isolateId), const TypeMatcher()); - await stream.firstWhere((event) => event.kind == EventKind.kResume); - expect( - (await service.getIsolate(isolateId)).pauseEvent!.kind, - EventKind.kResume, - ); - await resumeCompleter.future; - await pauseSub.cancel(); - await resumeSub.cancel(); - }); - - test('getInboundReferences', () async { - final service = context.service; - await expectLater( - service.getInboundReferences('', '', 0), - throwsRPCError, - ); - }); - - test('getRetainingPath', () async { - final service = context.service; - await expectLater(service.getRetainingPath('', '', 0), throwsRPCError); - }); - - test('lookupResolvedPackageUris converts package and org-dartlang-app uris', - () async { - final service = context.service; - final vm = await service.getVM(); - final isolateId = vm.isolates!.first.id!; - final scriptList = await service.getScripts(isolateId); - - final uris = scriptList.scripts!.map((e) => e.uri!).toList(); - final resolvedUris = - await service.lookupResolvedPackageUris(isolateId, uris); - - expect( - resolvedUris.uris, - containsAll([ - contains('/_testSound/example/hello_world/main.dart'), - contains('/lib/path.dart'), - contains('/lib/src/path_set.dart'), - ]), - ); - }); - - test('lookupResolvedPackageUris does not translate non-existent paths', - () async { - final service = context.service; - final vm = await service.getVM(); - final isolateId = vm.isolates!.first.id!; - - final resolvedUris = await service.lookupResolvedPackageUris(isolateId, [ - 'package:does/not/exist.dart', - 'dart:does_not_exist', - 'file:///does_not_exist.dart', - ]); - expect(resolvedUris.uris, [null, null, null]); - }); - - test( - 'lookupResolvedPackageUris translates dart uris', - () async { - final service = context.service; - final vm = await service.getVM(); - final isolateId = vm.isolates!.first.id!; - - final resolvedUris = - await service.lookupResolvedPackageUris(isolateId, [ - 'dart:html', - 'dart:async', - ]); - - expect(resolvedUris.uris, [ - 'org-dartlang-sdk:///sdk/lib/html/dart2js/html_dart2js.dart', - 'org-dartlang-sdk:///sdk/lib/async/async.dart', - ]); - }, - skip: 'https://github.com/dart-lang/webdev/issues/1584', + group('canary: $canaryFeatures |', () { + final provider = TestSdkConfigurationProvider( + verbose: debug, + canaryFeatures: canaryFeatures, + ddcModuleFormat: moduleFormat, ); - - test('lookupPackageUris finds package and org-dartlang-app paths', - () async { - final service = context.service; - final vm = await service.getVM(); - final isolateId = vm.isolates!.first.id!; - final scriptList = await service.getScripts(isolateId); - - final uris = scriptList.scripts!.map((e) => e.uri!).toList(); - final resolvedUris = - await service.lookupResolvedPackageUris(isolateId, uris); - - final packageUris = await service.lookupPackageUris( - isolateId, - List.from(resolvedUris.uris!), - ); - expect( - packageUris.uris, - containsAll([ - 'org-dartlang-app:///example/hello_world/main.dart', - 'package:path/path.dart', - 'package:path/src/path_set.dart', - ]), - ); - }); - - test('lookupPackageUris ignores local parameter', () async { - final service = context.service; - final vm = await service.getVM(); - final isolateId = vm.isolates!.first.id!; - final scriptList = await service.getScripts(isolateId); - - final uris = scriptList.scripts!.map((e) => e.uri!).toList(); - final resolvedUrisWithLocal = - await service.lookupResolvedPackageUris(isolateId, uris, local: true); - - final packageUrisWithLocal = await service.lookupPackageUris( - isolateId, - List.from(resolvedUrisWithLocal.uris!), - ); - expect( - packageUrisWithLocal.uris, - containsAll([ - 'org-dartlang-app:///example/hello_world/main.dart', - 'package:path/path.dart', - 'package:path/src/path_set.dart', - ]), - ); - - final resolvedUrisWithoutLocal = - await service.lookupResolvedPackageUris(isolateId, uris, local: true); - - final packageUrisWithoutLocal = await service.lookupPackageUris( - isolateId, - List.from(resolvedUrisWithoutLocal.uris!), - ); - expect( - packageUrisWithoutLocal.uris, - containsAll([ - 'org-dartlang-app:///example/hello_world/main.dart', - 'package:path/path.dart', - 'package:path/src/path_set.dart', - ]), - ); - }); - - test('lookupPackageUris does not translate non-existent paths', () async { - final service = context.service; - final vm = await service.getVM(); - final isolateId = vm.isolates!.first.id!; - - final resolvedUris = await service.lookupPackageUris(isolateId, [ - 'org-dartlang-sdk:///sdk/does/not/exist.dart', - 'does_not_exist.dart', - 'file:///does_not_exist.dart', - ]); - expect(resolvedUris.uris, [null, null, null]); - }); - - test( - 'lookupPackageUris translates dart uris', - () async { - final service = context.service; - final vm = await service.getVM(); - final isolateId = vm.isolates!.first.id!; - - final resolvedUris = await service.lookupPackageUris(isolateId, [ - 'org-dartlang-sdk:///sdk/lib/html/dart2js/html_dart2js.dart', - 'org-dartlang-sdk:///sdk/lib/async/async.dart', - ]); - - expect(resolvedUris.uris, [ - 'dart:html', - 'dart:async', - ]); - }, - skip: 'https://github.com/dart-lang/webdev/issues/1584', + tearDownAll(provider.dispose); + + runTests( + provider: provider, + moduleFormat: moduleFormat, + compilationMode: compilationMode, + canaryFeatures: canaryFeatures, + debug: debug, ); - - test('registerService', () async { - final service = context.service; - await expectLater( - service.registerService('ext.foo.bar', ''), - throwsRPCError, - ); - }); - - test('reloadSources', () async { - final service = context.service; - await expectLater(service.reloadSources(''), throwsRPCError); - }); - - test('setIsolatePauseMode', () async { - final service = context.service; - final vm = await service.getVM(); - final isolateId = vm.isolates!.first.id!; - expect(await service.setIsolatePauseMode(isolateId), _isSuccess); - expect( - await service.setIsolatePauseMode( - isolateId, - exceptionPauseMode: ExceptionPauseMode.kAll, - ), - _isSuccess, - ); - expect( - await service.setIsolatePauseMode( - isolateId, - exceptionPauseMode: ExceptionPauseMode.kUnhandled, - ), - _isSuccess, - ); - // Make sure this is the last one - or future tests might hang. - expect( - await service.setIsolatePauseMode( - isolateId, - exceptionPauseMode: ExceptionPauseMode.kNone, - ), - _isSuccess, - ); - await expectLater( - service.setIsolatePauseMode(isolateId, exceptionPauseMode: 'invalid'), - throwsRPCError, - ); - }); - - test('setFlag', () async { - final service = context.service; - await expectLater(service.setFlag('', ''), throwsRPCError); - }); - - test('setLibraryDebuggable', () async { - final service = context.service; - await expectLater( - service.setLibraryDebuggable('', '', false), - throwsRPCError, - ); - }); - - test('setName', () async { - final service = context.service; - final vm = await service.getVM(); - final isolateId = vm.isolates!.first.id!; - expect(service.setName(isolateId, 'test'), completion(_isSuccess)); - final isolate = await service.getIsolate(isolateId); - expect(isolate.name, 'test'); - }); - - test('setVMName', () async { - final service = context.service; - expect(service.setVMName('foo'), completion(_isSuccess)); - final vm = await service.getVM(); - expect(vm.name, 'foo'); - }); - - test('streamCancel', () async { - final service = context.service; - await expectLater(service.streamCancel(''), throwsRPCError); - }); - - group('setFlag', () { - test('pause_isolates_on_start set to true', () { - final service = context.service; - expect( - service.setFlag('pause_isolates_on_start', 'true'), - completion(_isSuccess), - ); - expect( - context.service.pauseIsolatesOnStart, - equals(true), - ); - }); - - test('pause_isolates_on_start set to false', () { - final service = context.service; - expect( - service.setFlag('pause_isolates_on_start', 'false'), - completion(_isSuccess), - ); - expect( - context.service.pauseIsolatesOnStart, - equals(false), - ); - }); - - test('pause_isolates_on_start set to invalid value', () { - final service = context.service; - expect( - service.setFlag('pause_isolates_on_start', 'pizza'), - throwsRPCError, - ); - }); - }); - - group('getFlagList', () { - List stringifyFlags(FlagList flagList) { - return flagList.flags - ?.map((flag) => '${flag.name} -> ${flag.valueAsString}') - .toList() ?? - []; - } - - test('returns expected default values', () async { - final service = context.service; - final flagList = await service.getFlagList(); - expect( - stringifyFlags(flagList), - containsAll([ - 'pause_isolates_on_start -> false', - ]), - ); - }); - - test('returns any modified flag values', () async { - final service = context.service; - await service.setFlag('pause_isolates_on_start', 'true'); - final flagList = await service.getFlagList(); - expect( - stringifyFlags(flagList), - containsAll([ - 'pause_isolates_on_start -> true', - ]), - ); - }); - }); - - group('streamListen/onEvent', () { - late ChromeProxyService service; - - group('Debug', () { - late Stream eventStream; - - setUp(() async { - setCurrentLogWriter(debug: debug); - service = context.service; - expect( - await service.streamListen('Debug'), - const TypeMatcher(), - ); - eventStream = service.onEvent('Debug'); - }); - - test('basic Pause/Resume', () async { - expect(service.streamListen('Debug'), completion(_isSuccess)); - final stream = service.onEvent('Debug'); - safeUnawaited(context.tabConnection.debugger.pause()); - await expectLater( - stream, - emitsThrough( - const TypeMatcher() - .having((e) => e.kind, 'kind', EventKind.kPauseInterrupted), - ), - ); - safeUnawaited(context.tabConnection.debugger.resume()); - expect( - eventStream, - emitsThrough( - const TypeMatcher() - .having((e) => e.kind, 'kind', EventKind.kResume), - ), - ); - }); - - test('Inspect', () async { - expect( - eventStream, - emitsThrough( - const TypeMatcher() - .having((e) => e.kind, 'kind', EventKind.kInspect) - .having( - (e) => e.inspectee, - 'inspectee', - const TypeMatcher() - .having((instance) => instance.id, 'id', isNotNull) - .having( - (instance) => instance.kind, - 'inspectee.kind', - InstanceKind.kPlainInstance, - ), - ), - ), - ); - await context.tabConnection.runtime.evaluate('inspectInstance()'); - }); - }); - - group('Extension', () { - late VmServiceInterface service; - late Stream eventStream; - - setUp(() async { - setCurrentLogWriter(debug: debug); - service = context.service; - expect( - await service.streamListen('Extension'), - const TypeMatcher(), - ); - eventStream = service.onEvent('Extension'); - }); - - test('Custom debug event', () async { - final eventKind = 'my.custom.event'; - expect( - eventStream, - emitsThrough( - predicate( - (Event event) => - event.kind == EventKind.kExtension && - event.extensionKind == eventKind && - event.extensionData!.data['example'] == 'data', - ), - ), - ); - await context.tabConnection.runtime - .evaluate("postEvent('$eventKind');"); - }); - - test('Batched debug events from injected client', () async { - final eventKind = EventKind.kExtension; - final extensionKind = 'MyEvent'; - final eventData = 'eventData'; - final delay = const Duration(milliseconds: 2000); - - TypeMatcher eventMatcher( - String data, - ) => - const TypeMatcher() - .having((event) => event.kind, 'kind', eventKind) - .having( - (event) => event.extensionKind, - 'extensionKind', - extensionKind, - ) - .having( - (event) => event.extensionData!.data['eventData'], - 'eventData', - data, - ); - - String emitDebugEvent(String data) => - "\$emitDebugEvent('$extensionKind', '{ \"$eventData\": \"$data\" }');"; - - final size = 2; - final batch1 = List.generate(size, (int i) => 'data$i'); - final batch2 = List.generate(size, (int i) => 'data${size + i}'); - - expect( - eventStream, - emitsInOrder([ - ...batch1.map(eventMatcher), - ...batch2.map(eventMatcher), - ]), - ); - - for (final data in batch1) { - await context.tabConnection.runtime.evaluate(emitDebugEvent(data)); - } - await Future.delayed(delay); - for (final data in batch2) { - await context.tabConnection.runtime.evaluate(emitDebugEvent(data)); - } - }); - }); - - test('GC', () async { - expect(service.streamListen('GC'), completion(_isSuccess)); - }); - - group('Isolate', () { - late Stream isolateEventStream; - - setUp(() async { - expect(await service.streamListen(EventStreams.kIsolate), _isSuccess); - isolateEventStream = service.onEvent(EventStreams.kIsolate); - }); - - test('serviceExtensionAdded', () async { - final extensionMethod = 'ext.foo.bar'; - expect( - isolateEventStream, - emitsThrough( - predicate( - (Event event) => - event.kind == EventKind.kServiceExtensionAdded && - event.extensionRPC == extensionMethod, - ), - ), - ); - await context.tabConnection.runtime - .evaluate("registerExtension('$extensionMethod');"); - }); - - test('lifecycle events', () async { - final vm = await service.getVM(); - final initialIsolateId = vm.isolates!.first.id; - final eventsDone = expectLater( - isolateEventStream, - emitsThrough( - emitsInOrder([ - predicate( - (Event event) => - event.kind == EventKind.kIsolateExit && - event.isolate!.id == initialIsolateId, - ), - predicate( - (Event event) => - event.kind == EventKind.kIsolateStart && - event.isolate!.id != initialIsolateId, - ), - predicate( - (Event event) => - event.kind == EventKind.kIsolateRunnable && - event.isolate!.id != initialIsolateId, - ), - ]), - ), - ); - service.destroyIsolate(); - await service.createIsolate(context.appConnection); - await eventsDone; - expect( - (await service.getVM()).isolates!.first.id, - isNot(initialIsolateId), - ); - }); - - test('RegisterExtension events from injected client', () async { - final eventKind = EventKind.kServiceExtensionAdded; - final extensions = List.generate(10, (index) => 'extension$index'); - - TypeMatcher eventMatcher(String extension) => - const TypeMatcher() - .having((event) => event.kind, 'kind', eventKind) - .having((event) => event.extensionRPC, 'RPC', extension); - - String emitRegisterEvent(String extension) => - "\$emitRegisterEvent('$extension')"; - - expect( - isolateEventStream, - emitsInOrder(extensions.map(eventMatcher)), - ); - for (final extension in extensions) { - await context.tabConnection.runtime - .evaluate(emitRegisterEvent(extension)); - } - }); - }); - - test('Timeline', () async { - expect(service.streamListen('Timeline'), completion(_isSuccess)); - }); - - test('Stdout', () async { - expect(service.streamListen('Stdout'), completion(_isSuccess)); - expect( - service.onEvent('Stdout'), - emitsThrough( - predicate( - (Event event) => - event.kind == EventKind.kWriteEvent && - String.fromCharCodes(base64.decode(event.bytes!)) - .contains('hello'), - ), - ), - ); - await context.tabConnection.runtime.evaluate('console.log("hello");'); - }); - - test('Stderr', () async { - expect(service.streamListen('Stderr'), completion(_isSuccess)); - final stderrStream = service.onEvent('Stderr'); - expect( - stderrStream, - emitsThrough( - predicate( - (Event event) => - event.kind == EventKind.kWriteEvent && - String.fromCharCodes(base64.decode(event.bytes!)) - .contains('Error'), - ), - ), - ); - await context.tabConnection.runtime.evaluate('console.error("Error");'); - }); - - test('exception stack trace mapper', () async { - expect(service.streamListen('Stderr'), completion(_isSuccess)); - final stderrStream = service.onEvent('Stderr'); - expect( - stderrStream, - emitsThrough( - predicate( - (Event event) => - event.kind == EventKind.kWriteEvent && - String.fromCharCodes(base64.decode(event.bytes!)) - .contains('main.dart'), - ), - ), - ); - await context.tabConnection.runtime - .evaluate('throwUncaughtException();'); - }); - - test('VM', () async { - final status = await service.streamListen('VM'); - expect(status, _isSuccess); - final stream = service.onEvent('VM'); - expect( - stream, - emitsThrough( - predicate( - (Event e) => - e.kind == EventKind.kVMUpdate && e.vm!.name == 'test', - ), - ), - ); - await service.setVMName('test'); - }); - - test('custom stream', () { - expect( - () => service.streamListen('aCustomStreamId'), - throwsA( - predicate( - (e) => - (e is RPCError) && e.code == RPCErrorKind.kInvalidParams.code, - ), - ), - ); - }); - }); - - group('Logging |', () { - test('logging stream is registered', () { - final service = context.service; - expect( - service.streamListen(EventStreams.kLogging), - completion(_isSuccess), - ); - }); - - test('dart:developer logs are correctly converted to log records', - () async { - final logStream = context.service.onEvent(EventStreams.kLogging); - final message = 'myMessage'; - - safeUnawaited( - context.tabConnection.runtime.evaluate("sendLog('$message');"), - ); - - final event = await logStream.first; - expect(event.kind, EventKind.kLogging); - - final logRecord = event.logRecord!; - expect(logRecord.message!.valueAsString, message); - expect(logRecord.loggerName!.valueAsString, 'testLogCategory'); - }); - - test('long dart:developer log messages are not truncated', () async { - final logStream = context.service.onEvent(EventStreams.kLogging); - final longMessage = - 'A very long log message that Chrome truncates by default and ' - 'requires users to expand in order to see the entire message.'; - safeUnawaited( - context.tabConnection.runtime.evaluate("sendLog('$longMessage');"), - ); - - final event = await logStream.first; - expect(event.logRecord!.message!.valueAsString, longMessage); - }); - }); }); } - -final _isSuccess = isA(); - -TypeMatcher _libRef(uriMatcher) => - isA().having((l) => l.uri, 'uri', uriMatcher); - -void expectEventually(Matcher expectation) {} diff --git a/dwds/test/chrome_proxy_service_ddc_library_bundle_test.dart b/dwds/test/chrome_proxy_service_ddc_library_bundle_test.dart index 4add74f8a..a9f31437a 100644 --- a/dwds/test/chrome_proxy_service_ddc_library_bundle_test.dart +++ b/dwds/test/chrome_proxy_service_ddc_library_bundle_test.dart @@ -7,129 +7,34 @@ @Timeout(Duration(minutes: 2)) library; -import 'dart:convert'; - import 'package:dwds/expression_compiler.dart'; -import 'package:dwds/src/services/chrome_proxy_service.dart'; 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 'common/chrome_proxy_service_common.dart'; import 'fixtures/context.dart'; -import 'fixtures/project.dart'; -import 'fixtures/utilities.dart'; void main() { - // Change to true to see verbose output from the tests. + // Enable verbose logging for debugging. final debug = false; - final moduleFormat = ModuleFormat.ddc; final canaryFeatures = true; + final moduleFormat = ModuleFormat.ddc; final compilationMode = CompilationMode.frontendServer; - final provider = TestSdkConfigurationProvider( - verbose: debug, - ddcModuleFormat: moduleFormat, - canaryFeatures: canaryFeatures, - ); - tearDownAll(provider.dispose); - - final context = TestContext(TestProject.testDdcLibraryBundle, provider); - - group('shared context', () { - setUpAll(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings( - enableExpressionEvaluation: true, - verboseCompiler: false, - moduleFormat: provider.ddcModuleFormat, - canaryFeatures: provider.canaryFeatures, - compilationMode: compilationMode, - ), - ); - }); - - tearDownAll(() async { - await context.tearDown(); - }); - - group('callServiceExtension', () { - late ChromeProxyService service; - - setUp(() { - setCurrentLogWriter(debug: debug); - service = context.service; - }); - - test( - 'success', - () async { - final serviceMethod = 'ext.test.callServiceExtension'; - await context.tabConnection.runtime - .evaluate('registerExtension("$serviceMethod");'); - - // The non-string keys/values get auto json-encoded to match the vm - // behavior. - final args = { - 'bool': true, - 'list': [1, '2', 3], - 'map': {'foo': 'bar'}, - 'num': 1.0, - 'string': 'hello', - 1: 2, - false: true, - }; - - final result = - await service.callServiceExtension(serviceMethod, args: args); - expect( - result.json, - args.map( - (k, v) => MapEntry( - k is String ? k : jsonEncode(k), - v is String ? v : jsonEncode(v), - ), - ), - ); - }, - onPlatform: { - 'windows': - const Skip('https://github.com/dart-lang/webdev/issues/711'), - }, - ); - - test( - 'failure', - () async { - final serviceMethod = 'ext.test.callServiceExtensionWithError'; - await context.tabConnection.runtime - .evaluate('registerExtensionWithError("$serviceMethod");'); - - final errorDetails = {'intentional': 'error'}; - expect( - service.callServiceExtension( - serviceMethod, - args: { - 'code': '-32001', - 'details': jsonEncode(errorDetails), - }, - ), - throwsA( - predicate( - (dynamic error) => - error is RPCError && - error.code == -32001 && - error.details == jsonEncode(errorDetails), - ), - ), - ); - }, - onPlatform: { - 'windows': - const Skip('https://github.com/dart-lang/webdev/issues/711'), - }, - ); - }); + group('canary: $canaryFeatures |', () { + final provider = TestSdkConfigurationProvider( + verbose: debug, + canaryFeatures: canaryFeatures, + ddcModuleFormat: moduleFormat, + ); + tearDownAll(provider.dispose); + + runTests( + provider: provider, + moduleFormat: moduleFormat, + compilationMode: compilationMode, + canaryFeatures: canaryFeatures, + debug: debug, + ); }); } diff --git a/dwds/test/common/chrome_proxy_service_common.dart b/dwds/test/common/chrome_proxy_service_common.dart new file mode 100644 index 000000000..4e849b279 --- /dev/null +++ b/dwds/test/common/chrome_proxy_service_common.dart @@ -0,0 +1,2559 @@ +// Copyright (c) 2025, 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') +@Tags(['daily']) +@Timeout(Duration(minutes: 2)) +library; + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:dwds/expression_compiler.dart'; +import 'package:dwds/src/services/chrome_proxy_service.dart'; +import 'package:dwds/src/utilities/dart_uri.dart'; +import 'package:dwds/src/utilities/shared.dart'; +import 'package:http/http.dart' as http; +import 'package:path/path.dart' as path; +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 'package:vm_service_interface/vm_service_interface.dart'; + +import '../fixtures/context.dart'; +import '../fixtures/project.dart'; +import '../fixtures/utilities.dart'; + +void runTests({ + required TestSdkConfigurationProvider provider, + required ModuleFormat moduleFormat, + required CompilationMode compilationMode, + required bool canaryFeatures, + required bool debug, +}) { + final project = TestProject.test; + final context = TestContext(project, provider); + + group('shared context', () { + setUpAll(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + testSettings: TestSettings( + enableExpressionEvaluation: true, + verboseCompiler: false, + moduleFormat: provider.ddcModuleFormat, + canaryFeatures: canaryFeatures, + compilationMode: compilationMode, + ), + ); + }); + + tearDownAll(() async { + await context.tearDown(); + }); + + group('breakpoints', () { + late VmServiceInterface service; + VM vm; + late Isolate isolate; + + late ScriptList scripts; + late ScriptRef mainScript; + + setUp(() async { + setCurrentLogWriter(debug: debug); + service = context.service; + vm = await service.getVM(); + isolate = await service.getIsolate(vm.isolates!.first.id!); + scripts = await service.getScripts(isolate.id!); + mainScript = scripts.scripts! + .firstWhere((each) => each.uri!.contains('main.dart')); + }); + + test('addBreakpoint', () async { + final line = await context.findBreakpointLine( + 'printHelloWorld', + isolate.id!, + mainScript, + ); + final firstBp = + await service.addBreakpoint(isolate.id!, mainScript.id!, line); + expect(firstBp, isNotNull); + expect(firstBp.id, isNotNull); + + final secondBp = + await service.addBreakpoint(isolate.id!, mainScript.id!, line); + expect(secondBp, isNotNull); + expect(secondBp.id, isNotNull); + + expect(firstBp.id, equals(secondBp.id)); + + // Remove breakpoint so it doesn't impact other tests. + await service.removeBreakpoint(isolate.id!, firstBp.id!); + }); + + test('addBreakpoint succeeds when sending the same breakpoint twice', + () async { + final line = await context.findBreakpointLine( + 'printHelloWorld', + isolate.id!, + mainScript, + ); + final firstBp = + service.addBreakpoint(isolate.id!, mainScript.id!, line); + final secondBp = + service.addBreakpoint(isolate.id!, mainScript.id!, line); + + // Remove breakpoint so it doesn't impact other tests. + await service.removeBreakpoint(isolate.id!, (await firstBp).id!); + expect((await firstBp).id, equals((await secondBp).id)); + }); + + test('addBreakpoint in nonsense location throws', () async { + expect( + service.addBreakpoint(isolate.id!, mainScript.id!, 200000), + throwsA(predicate((dynamic e) => e is RPCError && e.code == 102)), + ); + }); + + test('addBreakpoint on a part file', () async { + final partScript = scripts.scripts! + .firstWhere((script) => script.uri!.contains('part.dart')); + final bp = await service.addBreakpoint(isolate.id!, partScript.id!, 10); + // Remove breakpoint so it doesn't impact other tests. + await service.removeBreakpoint(isolate.id!, bp.id!); + expect(bp.id, isNotNull); + }); + + test('addBreakpointAtEntry', () async { + await expectLater(service.addBreakpointAtEntry('', ''), throwsRPCError); + }); + + test('addBreakpointWithScriptUri', () async { + final line = await context.findBreakpointLine( + 'printHelloWorld', + isolate.id!, + mainScript, + ); + final bp = await service.addBreakpointWithScriptUri( + isolate.id!, + mainScript.uri!, + line, + ); + // Remove breakpoint so it doesn't impact other tests. + await service.removeBreakpoint(isolate.id!, bp.id!); + expect(bp.id, isNotNull); + }); + + test('addBreakpointWithScriptUri absolute file URI', () async { + final test = context.project.absolutePackageDirectory; + final scriptPath = Uri.parse(mainScript.uri!).path.substring(1); + final fullPath = path.join(test, scriptPath); + final fileUri = Uri.file(fullPath); + final line = await context.findBreakpointLine( + 'printHelloWorld', + isolate.id!, + mainScript, + ); + final bp = await service.addBreakpointWithScriptUri( + isolate.id!, + '$fileUri', + line, + ); + // Remove breakpoint so it doesn't impact other tests. + await service.removeBreakpoint(isolate.id!, bp.id!); + expect(bp.id, isNotNull); + }); + + test('removeBreakpoint null arguments', () async { + await expectLater( + service.removeBreakpoint('', ''), + throwsSentinelException, + ); + await expectLater( + service.removeBreakpoint(isolate.id!, ''), + throwsRPCError, + ); + }); + + test("removeBreakpoint that doesn't exist fails", () async { + await expectLater( + service.removeBreakpoint(isolate.id!, '1234'), + throwsRPCError, + ); + }); + + test('add and remove breakpoint', () async { + final line = await context.findBreakpointLine( + 'printHelloWorld', + isolate.id!, + mainScript, + ); + final bp = + await service.addBreakpoint(isolate.id!, mainScript.id!, line); + expect(isolate.breakpoints, [bp]); + await service.removeBreakpoint(isolate.id!, bp.id!); + expect(isolate.breakpoints, isEmpty); + }); + }); + + group('callServiceExtension', () { + late ChromeProxyService service; + + setUp(() { + setCurrentLogWriter(debug: debug); + service = context.service; + }); + + test( + 'success', + () async { + final serviceMethod = 'ext.test.callServiceExtension'; + await context.tabConnection.runtime + .evaluate('registerExtension("$serviceMethod");'); + + // The non-string keys/values get auto json-encoded to match the vm + // behavior. + final args = { + 'bool': true, + 'list': [1, '2', 3], + 'map': {'foo': 'bar'}, + 'num': 1.0, + 'string': 'hello', + 1: 2, + false: true, + }; + + final result = + await service.callServiceExtension(serviceMethod, args: args); + expect( + result.json, + args.map( + (k, v) => MapEntry( + k is String ? k : jsonEncode(k), + v is String ? v : jsonEncode(v), + ), + ), + ); + }, + onPlatform: { + 'windows': + const Skip('https://github.com/dart-lang/webdev/issues/711'), + }, + ); + + test( + 'failure', + () async { + final serviceMethod = 'ext.test.callServiceExtensionWithError'; + await context.tabConnection.runtime + .evaluate('registerExtensionWithError("$serviceMethod");'); + + final errorDetails = {'intentional': 'error'}; + expect( + service.callServiceExtension( + serviceMethod, + args: { + 'code': '-32001', + 'details': jsonEncode(errorDetails), + }, + ), + throwsA( + predicate( + (dynamic error) => + error is RPCError && + error.code == -32001 && + error.details == jsonEncode(errorDetails), + ), + ), + ); + }, + onPlatform: { + 'windows': + const Skip('https://github.com/dart-lang/webdev/issues/711'), + }, + ); + }); + + group('VMTimeline', () { + late VmServiceInterface service; + + setUp(() { + setCurrentLogWriter(debug: debug); + service = context.service; + }); + + test('clearVMTimeline', () async { + await expectLater(service.clearVMTimeline(), throwsRPCError); + }); + + test('getVMTimelineMicros', () async { + await expectLater(service.getVMTimelineMicros(), throwsRPCError); + }); + + test('getVMTimeline', () async { + await expectLater(service.getVMTimeline(), throwsRPCError); + }); + + test('getVMTimelineFlags', () async { + await expectLater(service.getVMTimelineFlags(), throwsRPCError); + }); + + test('setVMTimelineFlags', () async { + await expectLater( + service.setVMTimelineFlags([]), + throwsRPCError, + ); + }); + }); + + test('getMemoryUsage', () async { + final service = context.service; + final vm = await service.getVM(); + final isolate = await service.getIsolate(vm.isolates!.first.id!); + + final memoryUsage = await service.getMemoryUsage(isolate.id!); + + expect(memoryUsage.heapUsage, isNotNull); + expect(memoryUsage.heapUsage, greaterThan(0)); + expect(memoryUsage.heapCapacity, greaterThan(0)); + expect(memoryUsage.externalUsage, equals(0)); + }); + + group('evaluate', () { + late VmServiceInterface service; + late Isolate isolate; + LibraryRef? bootstrap; + + setUpAll(() async { + setCurrentLogWriter(debug: debug); + service = context.service; + final vm = await service.getVM(); + isolate = await service.getIsolate(vm.isolates!.first.id!); + bootstrap = isolate.rootLib; + }); + + group('top level methods', () { + setUp(() { + setCurrentLogWriter(debug: debug); + }); + + test('can return strings', () async { + expect( + await service.evaluate( + isolate.id!, + bootstrap!.id!, + "helloString('world')", + ), + const TypeMatcher().having( + (instance) => instance.valueAsString, + 'value', + 'world', + ), + ); + }); + + test('can return bools', () async { + expect( + await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'helloBool(true)', + ), + const TypeMatcher().having( + (instance) => instance.valueAsString, + 'valueAsString', + 'true', + ), + ); + expect( + await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'helloBool(false)', + ), + const TypeMatcher().having( + (instance) => instance.valueAsString, + 'valueAsString', + 'false', + ), + ); + }); + + test('can return nums', () async { + expect( + await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'helloNum(42.0)', + ), + const TypeMatcher().having( + (instance) => instance.valueAsString, + 'valueAsString', + '42', + ), + ); + expect( + await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'helloNum(42.2)', + ), + const TypeMatcher().having( + (instance) => instance.valueAsString, + 'valueAsString', + '42.2', + ), + ); + }); + + test('can return objects with ids', () async { + final object = await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'createObject("cool")', + ); + expect( + object, + const TypeMatcher() + .having((instance) => instance.id, 'id', isNotNull), + ); + // TODO(jakemac): Add tests for the ClassRef once we create one, + // https://github.com/dart-lang/sdk/issues/36771. + }); + + group('with provided scope', () { + setUp(() { + setCurrentLogWriter(debug: debug); + }); + + Future createRemoteObject(String message) async { + return await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'createObject("$message")', + ) as InstanceRef; + } + + test('single scope object', () async { + final instance = await createRemoteObject('A'); + final result = await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'messageFor(arg1)', + scope: {'arg1': instance.id!}, + ); + expect( + result, + const TypeMatcher().having( + (instance) => instance.valueAsString, + 'valueAsString', + 'A', + ), + ); + }); + + test('multiple scope objects', () async { + final instance1 = await createRemoteObject('A'); + final instance2 = await createRemoteObject('B'); + final result = await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'messagesCombined(arg1, arg2)', + scope: {'arg1': instance1.id!, 'arg2': instance2.id!}, + ); + expect( + result, + const TypeMatcher().having( + (instance) => instance.valueAsString, + 'valueAsString', + 'AB', + ), + ); + }); + }); + }); + }); + + test('evaluateInFrame', () async { + final service = context.service; + await expectLater( + service.evaluateInFrame('', 0, ''), + throwsSentinelException, + ); + }); + + test('getAllocationProfile', () async { + final service = context.service; + await expectLater(service.getAllocationProfile(''), throwsRPCError); + }); + + test('getClassList', () async { + final service = context.service; + await expectLater(service.getClassList(''), throwsRPCError); + }); + + test('getFlagList', () async { + final service = context.service; + expect(await service.getFlagList(), isA()); + }); + + test('getInstances', () async { + final service = context.service; + await expectLater(service.getInstances('', '', 0), throwsRPCError); + }); + + group('getIsolate', () { + late VmServiceInterface service; + setUp(() { + setCurrentLogWriter(debug: debug); + service = context.service; + }); + + test('works for existing isolates', () async { + final vm = await service.getVM(); + final result = await service.getIsolate(vm.isolates!.first.id!); + expect(result, const TypeMatcher()); + final isolate = result; + expect(isolate.name, contains('main')); + // TODO: library names change with kernel dart-lang/sdk#36736 + expect(isolate.rootLib!.uri, endsWith('.dart')); + + expect( + isolate.libraries, + containsAll([ + _libRef('package:path/path.dart'), + // TODO: library names change with kernel dart-lang/sdk#36736 + _libRef(endsWith('main.dart')), + ]), + ); + expect(isolate.extensionRPCs, contains('ext.hello_world.existing')); + }); + + test('throws for invalid ids', () async { + expect(service.getIsolate('bad'), throwsSentinelException); + }); + }); + + group('getObject', () { + late ChromeProxyService service; + late Isolate isolate; + LibraryRef? bootstrap; + + Library? rootLibrary; + + setUpAll(() async { + setCurrentLogWriter(debug: debug); + service = context.service; + final vm = await service.getVM(); + isolate = await service.getIsolate(vm.isolates!.first.id!); + bootstrap = isolate.rootLib; + rootLibrary = + await service.getObject(isolate.id!, bootstrap!.id!) as Library; + }); + + setUp(() { + setCurrentLogWriter(debug: debug); + }); + + test('root Library', () async { + expect(rootLibrary, isNotNull); + // TODO: library names change with kernel dart-lang/sdk#36736 + expect(rootLibrary!.uri, endsWith('main.dart')); + expect(rootLibrary!.classes, hasLength(1)); + final testClass = rootLibrary!.classes!.first; + expect(testClass.name, 'MyTestClass'); + }); + + test('Library only contains included scripts', () async { + final library = + await service.getObject(isolate.id!, rootLibrary!.id!) as Library; + expect(library.scripts, hasLength(2)); + expect( + library.scripts, + unorderedEquals([ + predicate( + (ScriptRef s) => + s.uri == 'org-dartlang-app:///example/hello_world/main.dart', + ), + predicate( + (ScriptRef s) => + s.uri == 'org-dartlang-app:///example/hello_world/part.dart', + ), + ]), + ); + }); + + test('Can get the same library in parallel', () async { + final futures = [ + service.getObject(isolate.id!, rootLibrary!.id!), + service.getObject(isolate.id!, rootLibrary!.id!), + ]; + final results = await Future.wait(futures); + final library1 = results[0] as Library; + final library2 = results[1] as Library; + expect(library1, equals(library2)); + }); + + test('Classes', () async { + final testClass = await service.getObject( + isolate.id!, + rootLibrary!.classes!.first.id!, + ) as Class; + expect( + testClass.functions, + unorderedEquals([ + predicate((FuncRef f) => f.name == 'staticHello' && f.isStatic!), + predicate((FuncRef f) => f.name == 'hello' && !f.isStatic!), + predicate((FuncRef f) => f.name == 'hashCode' && !f.isStatic!), + predicate((FuncRef f) => f.name == 'runtimeType' && !f.isStatic!), + ]), + ); + expect( + testClass.fields, + unorderedEquals([ + predicate( + (FieldRef f) => + f.name == 'message' && + f.declaredType != null && + !f.isStatic! && + !f.isConst! && + f.isFinal!, + ), + predicate( + (FieldRef f) => + f.name == 'notFinal' && + f.declaredType != null && + !f.isStatic! && + !f.isConst! && + !f.isFinal!, + ), + predicate( + (FieldRef f) => + f.name == 'staticMessage' && + f.declaredType != null && + f.isStatic! && + !f.isConst! && + !f.isFinal!, + ), + ]), + ); + }); + + test('Runtime classes', () async { + final testClass = await service.getObject( + isolate.id!, + 'classes|dart:_runtime|_Type', + ) as Class; + expect(testClass.name, '_Type'); + }); + + test('String', () async { + final worldRef = await service.evaluate( + isolate.id!, + bootstrap!.id!, + "helloString('world')", + ) as InstanceRef; + final world = + await service.getObject(isolate.id!, worldRef.id!) as Instance; + expect(world.valueAsString, 'world'); + }); + + test('Large strings not truncated', () async { + final largeString = await service.evaluate( + isolate.id!, + bootstrap!.id!, + "helloString('${'abcde' * 250}')", + ) as InstanceRef; + expect(largeString.valueAsStringIsTruncated, isNot(isTrue)); + expect(largeString.valueAsString!.length, largeString.length); + expect(largeString.length, 5 * 250); + }); + + test('Lists', () async { + final list = await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'topLevelList', + ) as InstanceRef; + final inst = await service.getObject(isolate.id!, list.id!) as Instance; + expect(inst.length, 1001); + expect(inst.offset, null); + expect(inst.count, null); + expect(inst.elements!.length, 1001); + final fifth = inst.elements![4] as InstanceRef; + expect(fifth.valueAsString, '100'); + final sixth = inst.elements![5] as InstanceRef; + expect(sixth.valueAsString, '5'); + }); + + test('Maps', () async { + final map = await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'topLevelMap', + ) as InstanceRef; + final inst = await service.getObject(isolate.id!, map.id!) as Instance; + expect(inst.length, 1001); + expect(inst.offset, null); + expect(inst.count, null); + expect(inst.associations!.length, 1001); + final fifth = inst.associations![4]; + expect(fifth.key.valueAsString, '4'); + expect(fifth.value.valueAsString, '996'); + final sixth = inst.associations![5]; + expect(sixth.key.valueAsString, '5'); + expect(sixth.value.valueAsString, '995'); + }); + + test('bool', () async { + final ref = await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'helloBool(true)', + ) as InstanceRef; + final obj = await service.getObject(isolate.id!, ref.id!) as Instance; + expect(obj.kind, InstanceKind.kBool); + expect(obj.classRef!.name, 'Bool'); + expect(obj.valueAsString, 'true'); + }); + + test('num', () async { + final ref = await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'helloNum(42)', + ) as InstanceRef; + final obj = await service.getObject(isolate.id!, ref.id!) as Instance; + expect(obj.kind, InstanceKind.kDouble); + expect(obj.classRef!.name, 'Double'); + expect(obj.valueAsString, '42'); + }); + + test('Scripts', () async { + final scripts = await service.getScripts(isolate.id!); + assert(scripts.scripts!.isNotEmpty); + for (final scriptRef in scripts.scripts!) { + final script = + await service.getObject(isolate.id!, scriptRef.id!) as Script; + final serverPath = DartUri(script.uri!, 'hello_world/').serverPath; + final result = await http + .get(Uri.parse('http://localhost:${context.port}/$serverPath')); + expect(script.source, result.body); + expect(scriptRef.uri, endsWith('.dart')); + expect(script.tokenPosTable, isNotEmpty); + } + }); + + group('getObject called with offset/count parameters', () { + test('Lists with null offset and count are not truncated', () async { + final list = await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'topLevelList', + ) as InstanceRef; + final inst = await service.getObject( + isolate.id!, + list.id!, + count: null, + offset: null, + ) as Instance; + expect(inst.length, 1001); + expect(inst.offset, null); + expect(inst.count, null); + expect(inst.elements!.length, 1001); + final fifth = inst.elements![4] as InstanceRef; + expect(fifth.valueAsString, '100'); + final sixth = inst.elements![5] as InstanceRef; + expect(sixth.valueAsString, '5'); + }); + + test('Lists with null count are not truncated', () async { + final list = await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'topLevelList', + ) as InstanceRef; + final inst = await service.getObject( + isolate.id!, + list.id!, + count: null, + offset: 0, + ) as Instance; + expect(inst.length, 1001); + expect(inst.offset, 0); + expect(inst.count, null); + expect(inst.elements!.length, 1001); + final fifth = inst.elements![4] as InstanceRef; + expect(fifth.valueAsString, '100'); + final sixth = inst.elements![5] as InstanceRef; + expect(sixth.valueAsString, '5'); + }); + + test( + 'Lists with null count and offset greater than 0 are ' + 'truncated from offset to end of list', () async { + final list = await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'topLevelList', + ) as InstanceRef; + final inst = await service.getObject( + isolate.id!, + list.id!, + count: null, + offset: 1000, + ) as Instance; + expect(inst.length, 1001); + expect(inst.offset, 1000); + expect(inst.count, null); + expect(inst.elements!.length, 1); + final only = inst.elements![0] as InstanceRef; + expect(only.valueAsString, '5'); + }); + + test('Lists with offset/count are truncated', () async { + final list = await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'topLevelList', + ) as InstanceRef; + final inst = await service.getObject( + isolate.id!, + list.id!, + count: 7, + offset: 4, + ) as Instance; + expect(inst.length, 1001); + expect(inst.offset, 4); + expect(inst.count, 7); + expect(inst.elements!.length, 7); + final fifth = inst.elements![0] as InstanceRef; + expect(fifth.valueAsString, '100'); + final sixth = inst.elements![1] as InstanceRef; + expect(sixth.valueAsString, '5'); + }); + + test('Lists are truncated to the end if offset/count runs off the end', + () async { + final list = await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'topLevelList', + ) as InstanceRef; + final inst = await service.getObject( + isolate.id!, + list.id!, + count: 5, + offset: 1000, + ) as Instance; + expect(inst.length, 1001); + expect(inst.offset, 1000); + expect(inst.count, 1); + expect(inst.elements!.length, 1); + final only = inst.elements![0] as InstanceRef; + expect(only.valueAsString, '5'); + }); + + test('Lists are truncated to empty if offset runs off the end', + () async { + final list = await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'topLevelList', + ) as InstanceRef; + final inst = await service.getObject( + isolate.id!, + list.id!, + count: 5, + offset: 1002, + ) as Instance; + expect(inst.elements!.length, 0); + expect(inst.length, 1001); + expect(inst.offset, 1002); + expect(inst.count, 0); + expect(inst.elements!.length, 0); + }); + + test('Lists are truncated to empty with 0 count and null offset', + () async { + final list = await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'topLevelList', + ) as InstanceRef; + final inst = await service.getObject( + isolate.id!, + list.id!, + count: 0, + offset: null, + ) as Instance; + expect(inst.elements!.length, 0); + expect(inst.length, 1001); + expect(inst.offset, null); + expect(inst.count, 0); + expect(inst.elements!.length, 0); + }); + + test('Maps with null offset/count are not truncated', () async { + final map = await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'topLevelMap', + ) as InstanceRef; + final inst = await service.getObject( + isolate.id!, + map.id!, + count: null, + offset: null, + ) as Instance; + expect(inst.length, 1001); + expect(inst.offset, null); + expect(inst.count, null); + expect(inst.associations!.length, 1001); + final fifth = inst.associations![4]; + expect(fifth.key.valueAsString, '4'); + expect(fifth.value.valueAsString, '996'); + final sixth = inst.associations![5]; + expect(sixth.key.valueAsString, '5'); + expect(sixth.value.valueAsString, '995'); + }); + + test( + 'Maps with null count and offset greater than 0 are ' + 'truncated from offset to end of map', () async { + final map = await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'topLevelMap', + ) as InstanceRef; + final inst = await service.getObject( + isolate.id!, + map.id!, + count: null, + offset: 1000, + ) as Instance; + expect(inst.length, 1001); + expect(inst.offset, 1000); + expect(inst.count, null); + expect(inst.associations!.length, 1); + final only = inst.associations![0]; + expect(only.key.valueAsString, '1000'); + expect(only.value.valueAsString, '0'); + }); + + test('Maps with null count are not truncated', () async { + final map = await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'topLevelMap', + ) as InstanceRef; + final inst = await service.getObject( + isolate.id!, + map.id!, + count: null, + offset: 0, + ) as Instance; + expect(inst.length, 1001); + expect(inst.offset, 0); + expect(inst.count, null); + expect(inst.associations!.length, 1001); + final fifth = inst.associations![4]; + expect(fifth.key.valueAsString, '4'); + expect(fifth.value.valueAsString, '996'); + final sixth = inst.associations![5]; + expect(sixth.key.valueAsString, '5'); + expect(sixth.value.valueAsString, '995'); + }); + + test('Maps with offset/count are truncated', () async { + final map = await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'topLevelMap', + ) as InstanceRef; + final inst = await service.getObject( + isolate.id!, + map.id!, + count: 7, + offset: 4, + ) as Instance; + expect(inst.length, 1001); + expect(inst.offset, 4); + expect(inst.count, 7); + expect(inst.associations!.length, 7); + final fifth = inst.associations![0]; + expect(fifth.key.valueAsString, '4'); + expect(fifth.value.valueAsString, '996'); + final sixth = inst.associations![1]; + expect(sixth.key.valueAsString, '5'); + expect(sixth.value.valueAsString, '995'); + }); + + test('Maps are truncated to the end if offset/count runs off the end', + () async { + final map = await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'topLevelMap', + ) as InstanceRef; + final inst = await service.getObject( + isolate.id!, + map.id!, + count: 5, + offset: 1000, + ) as Instance; + expect(inst.length, 1001); + expect(inst.offset, 1000); + expect(inst.count, 1); + expect(inst.associations!.length, 1); + final only = inst.associations![0]; + expect(only.key.valueAsString, '1000'); + expect(only.value.valueAsString, '0'); + }); + + test('Maps are truncated to empty if offset runs off the end', + () async { + final map = await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'topLevelMap', + ) as InstanceRef; + final inst = await service.getObject( + isolate.id!, + map.id!, + count: 5, + offset: 1002, + ) as Instance; + expect(inst.associations!.length, 0); + expect(inst.length, 1001); + expect(inst.offset, 1002); + expect(inst.count, 0); + expect(inst.associations!.length, 0); + }); + + test('Strings with offset/count are truncated', () async { + final worldRef = await service.evaluate( + isolate.id!, + bootstrap!.id!, + "helloString('world')", + ) as InstanceRef; + final world = await service.getObject( + isolate.id!, + worldRef.id!, + count: 2, + offset: 1, + ) as Instance; + expect(world.valueAsString, 'or'); + expect(world.count, 2); + expect(world.length, 5); + expect(world.offset, 1); + }); + + test('Maps are truncated to empty if offset runs off the end', + () async { + final map = await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'topLevelMap', + ) as InstanceRef; + final inst = await service.getObject( + isolate.id!, + map.id!, + count: 5, + offset: 1002, + ) as Instance; + expect(inst.associations!.length, 0); + expect(inst.length, 1001); + expect(inst.offset, 1002); + expect(inst.count, 0); + expect(inst.associations!.length, 0); + }); + + test('Maps are truncated to empty with 0 count and null offset', + () async { + final map = await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'topLevelMap', + ) as InstanceRef; + final inst = await service.getObject( + isolate.id!, + map.id!, + count: 0, + offset: null, + ) as Instance; + expect(inst.associations!.length, 0); + expect(inst.length, 1001); + expect(inst.offset, null); + expect(inst.count, 0); + expect(inst.associations!.length, 0); + }); + + test( + 'Strings are truncated to the end if offset/count runs off the end', + () async { + final worldRef = await service.evaluate( + isolate.id!, + bootstrap!.id!, + "helloString('world')", + ) as InstanceRef; + final world = await service.getObject( + isolate.id!, + worldRef.id!, + count: 5, + offset: 3, + ) as Instance; + expect(world.valueAsString, 'ld'); + expect(world.count, 2); + expect(world.length, 5); + expect(world.offset, 3); + }); + + test( + 'offset/count parameters greater than zero are ignored for Classes', + () async { + final testClass = await service.getObject( + isolate.id!, + rootLibrary!.classes!.first.id!, + offset: 100, + count: 100, + ) as Class; + expect( + testClass.functions, + unorderedEquals([ + predicate((FuncRef f) => f.name == 'staticHello' && f.isStatic!), + predicate((FuncRef f) => f.name == 'hello' && !f.isStatic!), + predicate((FuncRef f) => f.name == 'hashCode' && !f.isStatic!), + predicate((FuncRef f) => f.name == 'runtimeType' && !f.isStatic!), + ]), + ); + expect( + testClass.fields, + unorderedEquals([ + predicate( + (FieldRef f) => + f.name == 'message' && + f.declaredType != null && + !f.isStatic! && + !f.isConst! && + f.isFinal!, + ), + predicate( + (FieldRef f) => + f.name == 'notFinal' && + f.declaredType != null && + !f.isStatic! && + !f.isConst! && + !f.isFinal!, + ), + predicate( + (FieldRef f) => + f.name == 'staticMessage' && + f.declaredType != null && + f.isStatic! && + !f.isConst! && + !f.isFinal!, + ), + ]), + ); + }); + + test('offset/count parameters equal to zero are ignored for Classes', + () async { + final testClass = await service.getObject( + isolate.id!, + rootLibrary!.classes!.first.id!, + offset: 0, + count: 0, + ) as Class; + expect( + testClass.functions, + unorderedEquals([ + predicate((FuncRef f) => f.name == 'staticHello' && f.isStatic!), + predicate((FuncRef f) => f.name == 'hello' && !f.isStatic!), + predicate((FuncRef f) => f.name == 'hashCode' && !f.isStatic!), + predicate((FuncRef f) => f.name == 'runtimeType' && !f.isStatic!), + ]), + ); + expect( + testClass.fields, + unorderedEquals([ + predicate( + (FieldRef f) => + f.name == 'message' && + f.declaredType != null && + !f.isStatic! && + !f.isConst! && + f.isFinal!, + ), + predicate( + (FieldRef f) => + f.name == 'notFinal' && + f.declaredType != null && + !f.isStatic! && + !f.isConst! && + !f.isFinal!, + ), + predicate( + (FieldRef f) => + f.name == 'staticMessage' && + f.declaredType != null && + f.isStatic! && + !f.isConst! && + !f.isFinal!, + ), + ]), + ); + }); + + test('offset/count parameters are ignored for bools', () async { + final ref = await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'helloBool(true)', + ) as InstanceRef; + final obj = await service.getObject( + isolate.id!, + ref.id!, + offset: 100, + count: 100, + ) as Instance; + expect(obj.kind, InstanceKind.kBool); + expect(obj.classRef!.name, 'Bool'); + expect(obj.valueAsString, 'true'); + }); + + test('offset/count parameters are ignored for nums', () async { + final ref = await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'helloNum(42)', + ) as InstanceRef; + final obj = await service.getObject( + isolate.id!, + ref.id!, + offset: 100, + count: 100, + ) as Instance; + expect(obj.kind, InstanceKind.kDouble); + expect(obj.classRef!.name, 'Double'); + expect(obj.valueAsString, '42'); + }); + + test('offset/count parameters are ignored for null', () async { + final ref = await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'helloNum(null)', + ) as InstanceRef; + final obj = await service.getObject( + isolate.id!, + ref.id!, + offset: 100, + count: 100, + ) as Instance; + expect(obj.kind, InstanceKind.kNull); + expect(obj.classRef!.name, 'Null'); + expect(obj.valueAsString, 'null'); + }); + }); + }); + + test('getScripts', () async { + final service = context.service; + final vm = await service.getVM(); + final isolateId = vm.isolates!.first.id!; + final scripts = await service.getScripts(isolateId); + expect(scripts, isNotNull); + expect(scripts.scripts, isNotEmpty); + + final scriptUris = scripts.scripts!.map((s) => s.uri); + + // Contains main script only once. + expect( + scriptUris.where((uri) => uri!.contains('hello_world/main.dart')), + hasLength(1), + ); + + // Contains a known script. + expect(scriptUris, contains('package:path/path.dart')); + + // Contains part files as well. + expect(scriptUris, contains(endsWith('part.dart'))); + expect( + scriptUris, + contains('package:intl/src/date_format_internal.dart'), + ); + }); + + group('getSourceReport', () { + late VmServiceInterface service; + + setUp(() { + setCurrentLogWriter(debug: debug); + service = context.service; + }); + + test('Coverage report', () async { + final vm = await service.getVM(); + final isolateId = vm.isolates!.first.id!; + + await expectLater( + service.getSourceReport(isolateId, ['Coverage']), + throwsRPCError, + ); + }); + + test('Coverage report', () async { + final vm = await service.getVM(); + final isolateId = vm.isolates!.first.id!; + + await expectLater( + service.getSourceReport( + isolateId, + ['Coverage'], + libraryFilters: ['foo'], + ), + throwsRPCError, + ); + }); + + test('report type not understood', () async { + final vm = await service.getVM(); + final isolateId = vm.isolates!.first.id!; + + await expectLater( + service.getSourceReport(isolateId, ['FooBar']), + throwsRPCError, + ); + }); + + test('PossibleBreakpoints report', () async { + final vm = await service.getVM(); + final isolateId = vm.isolates!.first.id!; + final scripts = await service.getScripts(isolateId); + final mainScript = scripts.scripts! + .firstWhere((script) => script.uri!.contains('main.dart')); + + final sourceReport = await service.getSourceReport( + isolateId, + ['PossibleBreakpoints'], + scriptId: mainScript.id, + ); + + expect(sourceReport.scripts, isNotEmpty); + expect(sourceReport.ranges, isNotEmpty); + + final sourceReportRange = sourceReport.ranges!.first; + expect(sourceReportRange.possibleBreakpoints, isNotEmpty); + }); + }); + + group('Pausing', () { + late VmServiceInterface service; + String? isolateId; + late Stream stream; + ScriptList scripts; + late ScriptRef mainScript; + + setUp(() async { + setCurrentLogWriter(debug: debug); + service = context.service; + final vm = await service.getVM(); + isolateId = vm.isolates!.first.id; + scripts = await service.getScripts(isolateId!); + await service.streamListen('Debug'); + stream = service.onEvent('Debug'); + mainScript = scripts.scripts! + .firstWhere((script) => script.uri!.contains('main.dart')); + }); + + test('at breakpoints sets pauseBreakPoints', () async { + final line = await context.findBreakpointLine( + 'callPrintCount', + isolateId!, + mainScript, + ); + final bp = + await service.addBreakpoint(isolateId!, mainScript.id!, line); + final event = await stream + .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint); + final pauseBreakpoints = event.pauseBreakpoints!; + expect(pauseBreakpoints, hasLength(1)); + expect(pauseBreakpoints.first.id, bp.id); + await service.removeBreakpoint(isolateId!, bp.id!); + // Resume execution to not impact other tests. + await service.resume(isolateId!); + }); + + test('resuming throws kIsolateMustBePaused error if not paused', + () async { + await expectLater( + service.resume(isolateId!), + throwsRPCErrorWithCode(RPCErrorKind.kIsolateMustBePaused.code), + ); + }); + }); + + group('Step', () { + late VmServiceInterface service; + String? isolateId; + late Stream stream; + ScriptList scripts; + ScriptRef mainScript; + + setUp(() async { + setCurrentLogWriter(debug: debug); + service = context.service; + final vm = await service.getVM(); + isolateId = vm.isolates!.first.id; + scripts = await service.getScripts(isolateId!); + await service.streamListen('Debug'); + stream = service.onEvent('Debug'); + mainScript = scripts.scripts! + .firstWhere((script) => script.uri!.contains('main.dart')); + final line = await context.findBreakpointLine( + 'callPrintCount', + isolateId!, + mainScript, + ); + final bp = + await service.addBreakpoint(isolateId!, mainScript.id!, line); + // Wait for breakpoint to trigger. + await stream + .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint); + await service.removeBreakpoint(isolateId!, bp.id!); + }); + + tearDown(() async { + // Resume execution to not impact other tests. + await service.resume(isolateId!); + }); + + test('Into goes to the next Dart location', () async { + await service.resume(isolateId!, step: 'Into'); + // Wait for the step to actually occur. + await stream + .firstWhere((event) => event.kind == EventKind.kPauseInterrupted); + final stack = await service.getStack(isolateId!); + expect(stack, isNotNull); + final first = stack.frames!.first; + expect(first.kind, 'Regular'); + expect(first.code!.kind, 'Dart'); + expect(first.code!.name, 'printCount'); + }); + + test('Over goes to the next Dart location', () async { + await service.resume(isolateId!, step: 'Over'); + // Wait for the step to actually occur. + await stream + .firstWhere((event) => event.kind == EventKind.kPauseInterrupted); + final stack = await service.getStack(isolateId!); + expect(stack, isNotNull); + final first = stack.frames!.first; + expect(first.kind, 'Regular'); + expect(first.code!.kind, 'Dart'); + expect(first.code!.name, ''); + }); + + test('Out goes to the next Dart location', () async { + await service.resume(isolateId!, step: 'Out'); + // Wait for the step to actually occur. + await stream + .firstWhere((event) => event.kind == EventKind.kPauseInterrupted); + final stack = await service.getStack(isolateId!); + expect(stack, isNotNull); + final first = stack.frames!.first; + expect(first.kind, 'Regular'); + expect(first.code!.kind, 'Dart'); + expect(first.code!.name, ''); + }); + }); + + group('getStack', () { + late VmServiceInterface service; + String? isolateId; + late Stream stream; + ScriptList scripts; + late ScriptRef mainScript; + + setUp(() async { + setCurrentLogWriter(debug: debug); + service = context.service; + final vm = await service.getVM(); + isolateId = vm.isolates!.first.id; + scripts = await service.getScripts(isolateId!); + await service.streamListen('Debug'); + stream = service.onEvent('Debug'); + mainScript = scripts.scripts! + .firstWhere((each) => each.uri!.contains('main.dart')); + }); + + test( + 'throws if not paused', + () async { + await expectLater(service.getStack(isolateId!), throwsRPCError); + }, + skip: Platform.isWindows, + ); // Issue: https://github.com/dart-lang/webdev/issues/1749 + + /// Support function for pausing and returning the stack at a line. + Future breakAt(String breakpointId, {int? limit}) async { + final line = await context.findBreakpointLine( + breakpointId, + isolateId!, + mainScript, + ); + Breakpoint? bp; + try { + bp = await service.addBreakpoint(isolateId!, mainScript.id!, line); + // Wait for breakpoint to trigger. + await stream + .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint); + return await service.getStack(isolateId!, limit: limit); + } finally { + // Remove breakpoint and resume so it doesn't impact other tests. + if (bp != null) { + await service.removeBreakpoint(isolateId!, bp.id!); + } + await service.resume(isolateId!); + } + } + + test('returns stack when broken', () async { + final stack = await breakAt('inPrintCount'); + expect(stack, isNotNull); + expect(stack.frames, hasLength(2)); + final first = stack.frames!.first; + expect(first.kind, 'Regular'); + expect(first.code!.kind, 'Dart'); + expect(first.code!.name, 'printCount'); + }); + + test('stack has a variable', () async { + final stack = await breakAt('callPrintCount'); + expect(stack, isNotNull); + expect(stack.frames, hasLength(1)); + final first = stack.frames!.first; + expect(first.kind, 'Regular'); + expect(first.code!.kind, 'Dart'); + expect(first.code!.name, ''); + // TODO: Make this more precise once this case doesn't + // also include all the libraries. + expect(first.vars, hasLength(greaterThanOrEqualTo(1))); + final underscore = first.vars!.firstWhere((v) => v.name == '_'); + expect(underscore, isNotNull); + }); + + test('collects async frames', () async { + final stack = await breakAt('asyncCall'); + expect(stack, isNotNull); + expect(stack.frames, hasLength(greaterThan(1))); + + final first = stack.frames!.first; + expect(first.kind, 'Regular'); + expect(first.code!.kind, 'Dart'); + + // We should have an async marker. + final suspensionFrames = stack.frames! + .where((frame) => frame.kind == FrameKind.kAsyncSuspensionMarker); + expect(suspensionFrames, isNotEmpty); + + // We should have async frames. + final asyncFrames = stack.frames! + .where((frame) => frame.kind == FrameKind.kAsyncCausal); + expect(asyncFrames, isNotEmpty); + }); + + test('returns the correct number of frames when a limit is provided', + () async { + var stack = await breakAt('asyncCall', limit: 4); + expect(stack, isNotNull); + expect(stack.frames, hasLength(equals(4))); + stack = await breakAt('asyncCall', limit: 2); + expect(stack, isNotNull); + expect(stack.frames, hasLength(equals(2))); + stack = await breakAt('asyncCall'); + expect(stack, isNotNull); + expect(stack.frames, hasLength(equals(5))); + }); + + test('truncated stacks are properly indicated', () async { + var stack = await breakAt('asyncCall', limit: 3); + expect(stack, isNotNull); + expect(stack.truncated, isTrue); + stack = await breakAt('asyncCall'); + expect(stack, isNotNull); + expect(stack.truncated, isFalse); + stack = await breakAt('asyncCall', limit: 20000); + expect(stack, isNotNull); + expect(stack.truncated, isFalse); + }); + + test('break on exceptions with setIsolatePauseMode', () async { + final oldPauseMode = + (await service.getIsolate(isolateId!)).exceptionPauseMode; + await service.setIsolatePauseMode( + isolateId!, + exceptionPauseMode: ExceptionPauseMode.kAll, + ); + // Wait for pausing to actually propagate. + final event = await stream + .firstWhere((event) => event.kind == EventKind.kPauseException); + expect(event.exception, isNotNull); + // Check that the exception stack trace has been mapped to Dart source files. + expect(event.exception!.valueAsString, contains('main.dart')); + + final stack = await service.getStack(isolateId!); + expect(stack, isNotNull); + + await service.setIsolatePauseMode( + isolateId!, + exceptionPauseMode: oldPauseMode, + ); + await service.resume(isolateId!); + }); + + test('returns non-null stack when paused', () async { + await service.pause(isolateId!); + // Wait for pausing to actually propagate. + await stream + .firstWhere((event) => event.kind == EventKind.kPauseInterrupted); + expect(await service.getStack(isolateId!), isNotNull); + // Resume the isolate to not impact other tests. + await service.resume(isolateId!); + }); + }); + + test('getVM', () async { + final service = context.service; + final vm = await service.getVM(); + expect(vm.name, isNotNull); + expect(vm.version, Platform.version); + expect(vm.isolates, hasLength(1)); + final isolate = vm.isolates!.first; + expect(isolate.id, isNotNull); + expect(isolate.name, isNotNull); + expect(isolate.number, isNotNull); + }); + + test('getVersion', () async { + final service = context.service; + final version = await service.getVersion(); + expect(version, isNotNull); + expect(version.major, greaterThan(0)); + }); + + group('invoke', () { + late ChromeProxyService service; + VM vm; + late Isolate isolate; + LibraryRef? bootstrap; + late InstanceRef testInstance; + + setUp(() async { + setCurrentLogWriter(debug: debug); + service = context.service; + vm = await service.getVM(); + isolate = await service.getIsolate(vm.isolates!.first.id!); + bootstrap = isolate.rootLib; + testInstance = await service.evaluate( + isolate.id!, + bootstrap!.id!, + 'myInstance', + ) as InstanceRef; + }); + + test('rootLib', () async { + expect( + bootstrap, + const TypeMatcher().having( + (library) => library.name, + 'name', + 'org-dartlang-app:///example/hello_world/main.dart', + ), + ); + }); + + test('toString()', () async { + final remote = + await service.invoke(isolate.id!, testInstance.id!, 'toString', []); + expect( + remote, + const TypeMatcher().having( + (instance) => instance.valueAsString, + 'toString()', + "Instance of 'MyTestClass'", + ), + ); + }); + + test('hello()', () async { + final remote = + await service.invoke(isolate.id!, testInstance.id!, 'hello', []); + expect( + remote, + const TypeMatcher().having( + (instance) => instance.valueAsString, + 'hello()', + 'world', + ), + ); + }); + + test( + 'helloString', + () async { + final remote = await service.invoke( + isolate.id!, + bootstrap!.id!, + 'helloString', + ['#StringInstanceRef#abc'], + ); + expect( + remote, + const TypeMatcher().having( + (instance) => instance.valueAsString, + 'helloString', + 'abc', + ), + ); + expect( + remote, + const TypeMatcher() + .having((instance) => instance.kind, 'kind', 'String'), + ); + }, + skip: moduleFormat == ModuleFormat.ddc && canaryFeatures == true + ? 'https://github.com/dart-lang/webdev/issues/2566' + : null, + ); + + test( + 'null argument', + () async { + final remote = await service.invoke( + isolate.id!, + bootstrap!.id!, + 'helloString', + ['objects/null'], + ); + expect( + remote, + const TypeMatcher().having( + (instance) => instance.valueAsString, + 'helloString', + 'null', + ), + ); + expect( + remote, + const TypeMatcher() + .having((instance) => instance.kind, 'kind', 'Null'), + ); + }, + skip: moduleFormat == ModuleFormat.ddc && canaryFeatures == true + ? 'https://github.com/dart-lang/webdev/issues/2566' + : null, + ); + + test( + 'helloBool', + () async { + final remote = await service.invoke( + isolate.id!, + bootstrap!.id!, + 'helloBool', + ['objects/bool-true'], + ); + expect( + remote, + const TypeMatcher().having( + (instance) => instance.valueAsString, + 'helloBool', + 'true', + ), + ); + expect( + remote, + const TypeMatcher() + .having((instance) => instance.kind, 'kind', 'Bool'), + ); + }, + skip: moduleFormat == ModuleFormat.ddc && canaryFeatures == true + ? 'https://github.com/dart-lang/webdev/issues/2566' + : null, + ); + + test( + 'helloNum', + () async { + final remote = await service.invoke( + isolate.id!, + bootstrap!.id!, + 'helloNum', + ['objects/int-123'], + ); + expect( + remote, + const TypeMatcher().having( + (instance) => instance.valueAsString, + 'helloNum', + '123', + ), + ); + expect( + remote, + const TypeMatcher() + .having((instance) => instance.kind, 'kind', 'Double'), + ); + }, + skip: moduleFormat == ModuleFormat.ddc && canaryFeatures == true + ? 'https://github.com/dart-lang/webdev/issues/2566' + : null, + ); + + test( + 'two object arguments', + () async { + final remote = await service.invoke( + isolate.id!, + bootstrap!.id!, + 'messagesCombined', + [testInstance.id, testInstance.id], + ); + expect( + remote, + const TypeMatcher().having( + (instance) => instance.valueAsString, + 'messagesCombined', + 'worldworld', + ), + ); + expect( + remote, + const TypeMatcher() + .having((instance) => instance.kind, 'kind', 'String'), + ); + }, + skip: moduleFormat == ModuleFormat.ddc && canaryFeatures == true + ? 'https://github.com/dart-lang/webdev/issues/2566' + : null, + ); + }); + + test('kill', () async { + final service = context.service; + await expectLater(service.kill(''), throwsRPCError); + }); + + test('onEvent', () async { + final service = context.service; + expect(() => service.onEvent(''), throwsRPCError); + }); + + test('pause / resume', () async { + final service = context.service; + await service.streamListen('Debug'); + final stream = service.onEvent('Debug'); + final vm = await service.getVM(); + final isolateId = vm.isolates!.first.id!; + final pauseCompleter = Completer(); + final pauseSub = context.tabConnection.debugger.onPaused.listen((_) { + pauseCompleter.complete(); + }); + final resumeCompleter = Completer(); + final resumeSub = context.tabConnection.debugger.onResumed.listen((_) { + resumeCompleter.complete(); + }); + expect(await service.pause(isolateId), const TypeMatcher()); + await stream + .firstWhere((event) => event.kind == EventKind.kPauseInterrupted); + expect( + (await service.getIsolate(isolateId)).pauseEvent!.kind, + EventKind.kPauseInterrupted, + ); + await pauseCompleter.future; + expect(await service.resume(isolateId), const TypeMatcher()); + await stream.firstWhere((event) => event.kind == EventKind.kResume); + expect( + (await service.getIsolate(isolateId)).pauseEvent!.kind, + EventKind.kResume, + ); + await resumeCompleter.future; + await pauseSub.cancel(); + await resumeSub.cancel(); + }); + + test('getInboundReferences', () async { + final service = context.service; + await expectLater( + service.getInboundReferences('', '', 0), + throwsRPCError, + ); + }); + + test('getRetainingPath', () async { + final service = context.service; + await expectLater(service.getRetainingPath('', '', 0), throwsRPCError); + }); + + test('lookupResolvedPackageUris converts package and org-dartlang-app uris', + () async { + final service = context.service; + final vm = await service.getVM(); + final isolateId = vm.isolates!.first.id!; + final scriptList = await service.getScripts(isolateId); + + final uris = scriptList.scripts!.map((e) => e.uri!).toList(); + final resolvedUris = + await service.lookupResolvedPackageUris(isolateId, uris); + + expect( + resolvedUris.uris, + containsAll([ + contains('/_testSound/example/hello_world/main.dart'), + contains('/lib/path.dart'), + contains('/lib/src/path_set.dart'), + ]), + ); + }); + + test('lookupResolvedPackageUris does not translate non-existent paths', + () async { + final service = context.service; + final vm = await service.getVM(); + final isolateId = vm.isolates!.first.id!; + + final resolvedUris = await service.lookupResolvedPackageUris(isolateId, [ + 'package:does/not/exist.dart', + 'dart:does_not_exist', + 'file:///does_not_exist.dart', + ]); + expect(resolvedUris.uris, [null, null, null]); + }); + + test( + 'lookupResolvedPackageUris translates dart uris', + () async { + final service = context.service; + final vm = await service.getVM(); + final isolateId = vm.isolates!.first.id!; + + final resolvedUris = + await service.lookupResolvedPackageUris(isolateId, [ + 'dart:html', + 'dart:async', + ]); + + expect(resolvedUris.uris, [ + 'org-dartlang-sdk:///sdk/lib/html/dart2js/html_dart2js.dart', + 'org-dartlang-sdk:///sdk/lib/async/async.dart', + ]); + }, + skip: 'https://github.com/dart-lang/webdev/issues/1584', + ); + + test('lookupPackageUris finds package and org-dartlang-app paths', + () async { + final service = context.service; + final vm = await service.getVM(); + final isolateId = vm.isolates!.first.id!; + final scriptList = await service.getScripts(isolateId); + + final uris = scriptList.scripts!.map((e) => e.uri!).toList(); + final resolvedUris = + await service.lookupResolvedPackageUris(isolateId, uris); + + final packageUris = await service.lookupPackageUris( + isolateId, + List.from(resolvedUris.uris!), + ); + expect( + packageUris.uris, + containsAll([ + 'org-dartlang-app:///example/hello_world/main.dart', + 'package:path/path.dart', + 'package:path/src/path_set.dart', + ]), + ); + }); + + test('lookupPackageUris ignores local parameter', () async { + final service = context.service; + final vm = await service.getVM(); + final isolateId = vm.isolates!.first.id!; + final scriptList = await service.getScripts(isolateId); + + final uris = scriptList.scripts!.map((e) => e.uri!).toList(); + final resolvedUrisWithLocal = + await service.lookupResolvedPackageUris(isolateId, uris, local: true); + + final packageUrisWithLocal = await service.lookupPackageUris( + isolateId, + List.from(resolvedUrisWithLocal.uris!), + ); + expect( + packageUrisWithLocal.uris, + containsAll([ + 'org-dartlang-app:///example/hello_world/main.dart', + 'package:path/path.dart', + 'package:path/src/path_set.dart', + ]), + ); + + final resolvedUrisWithoutLocal = + await service.lookupResolvedPackageUris(isolateId, uris, local: true); + + final packageUrisWithoutLocal = await service.lookupPackageUris( + isolateId, + List.from(resolvedUrisWithoutLocal.uris!), + ); + expect( + packageUrisWithoutLocal.uris, + containsAll([ + 'org-dartlang-app:///example/hello_world/main.dart', + 'package:path/path.dart', + 'package:path/src/path_set.dart', + ]), + ); + }); + + test('lookupPackageUris does not translate non-existent paths', () async { + final service = context.service; + final vm = await service.getVM(); + final isolateId = vm.isolates!.first.id!; + + final resolvedUris = await service.lookupPackageUris(isolateId, [ + 'org-dartlang-sdk:///sdk/does/not/exist.dart', + 'does_not_exist.dart', + 'file:///does_not_exist.dart', + ]); + expect(resolvedUris.uris, [null, null, null]); + }); + + test( + 'lookupPackageUris translates dart uris', + () async { + final service = context.service; + final vm = await service.getVM(); + final isolateId = vm.isolates!.first.id!; + + final resolvedUris = await service.lookupPackageUris(isolateId, [ + 'org-dartlang-sdk:///sdk/lib/html/dart2js/html_dart2js.dart', + 'org-dartlang-sdk:///sdk/lib/async/async.dart', + ]); + + expect(resolvedUris.uris, [ + 'dart:html', + 'dart:async', + ]); + }, + skip: 'https://github.com/dart-lang/webdev/issues/1584', + ); + + test('registerService', () async { + final service = context.service; + await expectLater( + service.registerService('ext.foo.bar', ''), + throwsRPCError, + ); + }); + + test('reloadSources', () async { + final service = context.service; + await expectLater(service.reloadSources(''), throwsRPCError); + }); + + test('setIsolatePauseMode', () async { + final service = context.service; + final vm = await service.getVM(); + final isolateId = vm.isolates!.first.id!; + expect(await service.setIsolatePauseMode(isolateId), _isSuccess); + expect( + await service.setIsolatePauseMode( + isolateId, + exceptionPauseMode: ExceptionPauseMode.kAll, + ), + _isSuccess, + ); + expect( + await service.setIsolatePauseMode( + isolateId, + exceptionPauseMode: ExceptionPauseMode.kUnhandled, + ), + _isSuccess, + ); + // Make sure this is the last one - or future tests might hang. + expect( + await service.setIsolatePauseMode( + isolateId, + exceptionPauseMode: ExceptionPauseMode.kNone, + ), + _isSuccess, + ); + await expectLater( + service.setIsolatePauseMode(isolateId, exceptionPauseMode: 'invalid'), + throwsRPCError, + ); + }); + + test('setFlag', () async { + final service = context.service; + await expectLater(service.setFlag('', ''), throwsRPCError); + }); + + test('setLibraryDebuggable', () async { + final service = context.service; + await expectLater( + service.setLibraryDebuggable('', '', false), + throwsRPCError, + ); + }); + + test('setName', () async { + final service = context.service; + final vm = await service.getVM(); + final isolateId = vm.isolates!.first.id!; + expect(service.setName(isolateId, 'test'), completion(_isSuccess)); + final isolate = await service.getIsolate(isolateId); + expect(isolate.name, 'test'); + }); + + test('setVMName', () async { + final service = context.service; + expect(service.setVMName('foo'), completion(_isSuccess)); + final vm = await service.getVM(); + expect(vm.name, 'foo'); + }); + + test('streamCancel', () async { + final service = context.service; + await expectLater(service.streamCancel(''), throwsRPCError); + }); + + group('setFlag', () { + test('pause_isolates_on_start set to true', () { + final service = context.service; + expect( + service.setFlag('pause_isolates_on_start', 'true'), + completion(_isSuccess), + ); + expect( + context.service.pauseIsolatesOnStart, + equals(true), + ); + }); + + test('pause_isolates_on_start set to false', () { + final service = context.service; + expect( + service.setFlag('pause_isolates_on_start', 'false'), + completion(_isSuccess), + ); + expect( + context.service.pauseIsolatesOnStart, + equals(false), + ); + }); + + test('pause_isolates_on_start set to invalid value', () { + final service = context.service; + expect( + service.setFlag('pause_isolates_on_start', 'pizza'), + throwsRPCError, + ); + }); + }); + + group('getFlagList', () { + List stringifyFlags(FlagList flagList) { + return flagList.flags + ?.map((flag) => '${flag.name} -> ${flag.valueAsString}') + .toList() ?? + []; + } + + test('returns expected default values', () async { + final service = context.service; + final flagList = await service.getFlagList(); + expect( + stringifyFlags(flagList), + containsAll([ + 'pause_isolates_on_start -> false', + ]), + ); + }); + + test('returns any modified flag values', () async { + final service = context.service; + await service.setFlag('pause_isolates_on_start', 'true'); + final flagList = await service.getFlagList(); + expect( + stringifyFlags(flagList), + containsAll([ + 'pause_isolates_on_start -> true', + ]), + ); + }); + }); + + group('streamListen/onEvent', () { + late ChromeProxyService service; + + group('Debug', () { + late Stream eventStream; + + setUp(() async { + setCurrentLogWriter(debug: debug); + service = context.service; + expect( + await service.streamListen('Debug'), + const TypeMatcher(), + ); + eventStream = service.onEvent('Debug'); + }); + + test('basic Pause/Resume', () async { + expect(service.streamListen('Debug'), completion(_isSuccess)); + final stream = service.onEvent('Debug'); + safeUnawaited(context.tabConnection.debugger.pause()); + await expectLater( + stream, + emitsThrough( + const TypeMatcher() + .having((e) => e.kind, 'kind', EventKind.kPauseInterrupted), + ), + ); + safeUnawaited(context.tabConnection.debugger.resume()); + expect( + eventStream, + emitsThrough( + const TypeMatcher() + .having((e) => e.kind, 'kind', EventKind.kResume), + ), + ); + }); + + test('Inspect', () async { + expect( + eventStream, + emitsThrough( + const TypeMatcher() + .having((e) => e.kind, 'kind', EventKind.kInspect) + .having( + (e) => e.inspectee, + 'inspectee', + const TypeMatcher() + .having((instance) => instance.id, 'id', isNotNull) + .having( + (instance) => instance.kind, + 'inspectee.kind', + InstanceKind.kPlainInstance, + ), + ), + ), + ); + await context.tabConnection.runtime.evaluate('inspectInstance()'); + }); + }); + + group('Extension', () { + late VmServiceInterface service; + late Stream eventStream; + + setUp(() async { + setCurrentLogWriter(debug: debug); + service = context.service; + expect( + await service.streamListen('Extension'), + const TypeMatcher(), + ); + eventStream = service.onEvent('Extension'); + }); + + test('Custom debug event', () async { + final eventKind = 'my.custom.event'; + expect( + eventStream, + emitsThrough( + predicate( + (Event event) => + event.kind == EventKind.kExtension && + event.extensionKind == eventKind && + event.extensionData!.data['example'] == 'data', + ), + ), + ); + await context.tabConnection.runtime + .evaluate("postEvent('$eventKind');"); + }); + + test('Batched debug events from injected client', () async { + final eventKind = EventKind.kExtension; + final extensionKind = 'MyEvent'; + final eventData = 'eventData'; + final delay = const Duration(milliseconds: 2000); + + TypeMatcher eventMatcher( + String data, + ) => + const TypeMatcher() + .having((event) => event.kind, 'kind', eventKind) + .having( + (event) => event.extensionKind, + 'extensionKind', + extensionKind, + ) + .having( + (event) => event.extensionData!.data['eventData'], + 'eventData', + data, + ); + + String emitDebugEvent(String data) => + "\$emitDebugEvent('$extensionKind', '{ \"$eventData\": \"$data\" }');"; + + final size = 2; + final batch1 = List.generate(size, (int i) => 'data$i'); + final batch2 = List.generate(size, (int i) => 'data${size + i}'); + + expect( + eventStream, + emitsInOrder([ + ...batch1.map(eventMatcher), + ...batch2.map(eventMatcher), + ]), + ); + + for (final data in batch1) { + await context.tabConnection.runtime.evaluate(emitDebugEvent(data)); + } + await Future.delayed(delay); + for (final data in batch2) { + await context.tabConnection.runtime.evaluate(emitDebugEvent(data)); + } + }); + }); + + test('GC', () async { + expect(service.streamListen('GC'), completion(_isSuccess)); + }); + + group('Isolate', () { + late Stream isolateEventStream; + + setUp(() async { + expect(await service.streamListen(EventStreams.kIsolate), _isSuccess); + isolateEventStream = service.onEvent(EventStreams.kIsolate); + }); + + test('serviceExtensionAdded', () async { + final extensionMethod = 'ext.foo.bar'; + expect( + isolateEventStream, + emitsThrough( + predicate( + (Event event) => + event.kind == EventKind.kServiceExtensionAdded && + event.extensionRPC == extensionMethod, + ), + ), + ); + await context.tabConnection.runtime + .evaluate("registerExtension('$extensionMethod');"); + }); + + test('lifecycle events', () async { + final vm = await service.getVM(); + final initialIsolateId = vm.isolates!.first.id; + final eventsDone = expectLater( + isolateEventStream, + emitsThrough( + emitsInOrder([ + predicate( + (Event event) => + event.kind == EventKind.kIsolateExit && + event.isolate!.id == initialIsolateId, + ), + predicate( + (Event event) => + event.kind == EventKind.kIsolateStart && + event.isolate!.id != initialIsolateId, + ), + predicate( + (Event event) => + event.kind == EventKind.kIsolateRunnable && + event.isolate!.id != initialIsolateId, + ), + ]), + ), + ); + service.destroyIsolate(); + await service.createIsolate(context.appConnection); + await eventsDone; + expect( + (await service.getVM()).isolates!.first.id, + isNot(initialIsolateId), + ); + }); + + test('RegisterExtension events from injected client', () async { + final eventKind = EventKind.kServiceExtensionAdded; + final extensions = List.generate(10, (index) => 'extension$index'); + + TypeMatcher eventMatcher(String extension) => + const TypeMatcher() + .having((event) => event.kind, 'kind', eventKind) + .having((event) => event.extensionRPC, 'RPC', extension); + + String emitRegisterEvent(String extension) => + "\$emitRegisterEvent('$extension')"; + + expect( + isolateEventStream, + emitsInOrder(extensions.map(eventMatcher)), + ); + for (final extension in extensions) { + await context.tabConnection.runtime + .evaluate(emitRegisterEvent(extension)); + } + }); + }); + + test('Timeline', () async { + expect(service.streamListen('Timeline'), completion(_isSuccess)); + }); + + test('Stdout', () async { + expect(service.streamListen('Stdout'), completion(_isSuccess)); + expect( + service.onEvent('Stdout'), + emitsThrough( + predicate( + (Event event) => + event.kind == EventKind.kWriteEvent && + String.fromCharCodes(base64.decode(event.bytes!)) + .contains('hello'), + ), + ), + ); + await context.tabConnection.runtime.evaluate('console.log("hello");'); + }); + + test('Stderr', () async { + expect(service.streamListen('Stderr'), completion(_isSuccess)); + final stderrStream = service.onEvent('Stderr'); + expect( + stderrStream, + emitsThrough( + predicate( + (Event event) => + event.kind == EventKind.kWriteEvent && + String.fromCharCodes(base64.decode(event.bytes!)) + .contains('Error'), + ), + ), + ); + await context.tabConnection.runtime.evaluate('console.error("Error");'); + }); + + test('exception stack trace mapper', () async { + expect(service.streamListen('Stderr'), completion(_isSuccess)); + final stderrStream = service.onEvent('Stderr'); + expect( + stderrStream, + emitsThrough( + predicate( + (Event event) => + event.kind == EventKind.kWriteEvent && + String.fromCharCodes(base64.decode(event.bytes!)) + .contains('main.dart'), + ), + ), + ); + await context.tabConnection.runtime + .evaluate('throwUncaughtException();'); + }); + + test('VM', () async { + final status = await service.streamListen('VM'); + expect(status, _isSuccess); + final stream = service.onEvent('VM'); + expect( + stream, + emitsThrough( + predicate( + (Event e) => + e.kind == EventKind.kVMUpdate && e.vm!.name == 'test', + ), + ), + ); + await service.setVMName('test'); + }); + + test('custom stream', () { + expect( + () => service.streamListen('aCustomStreamId'), + throwsA( + predicate( + (e) => + (e is RPCError) && e.code == RPCErrorKind.kInvalidParams.code, + ), + ), + ); + }); + }); + + group('Logging |', () { + test('logging stream is registered', () { + final service = context.service; + expect( + service.streamListen(EventStreams.kLogging), + completion(_isSuccess), + ); + }); + + test('dart:developer logs are correctly converted to log records', + () async { + final logStream = context.service.onEvent(EventStreams.kLogging); + final message = 'myMessage'; + + safeUnawaited( + context.tabConnection.runtime.evaluate("sendLog('$message');"), + ); + + final event = await logStream.first; + expect(event.kind, EventKind.kLogging); + + final logRecord = event.logRecord!; + expect(logRecord.message!.valueAsString, message); + expect(logRecord.loggerName!.valueAsString, 'testLogCategory'); + }); + + test('long dart:developer log messages are not truncated', () async { + final logStream = context.service.onEvent(EventStreams.kLogging); + final longMessage = + 'A very long log message that Chrome truncates by default and ' + 'requires users to expand in order to see the entire message.'; + safeUnawaited( + context.tabConnection.runtime.evaluate("sendLog('$longMessage');"), + ); + + final event = await logStream.first; + expect(event.logRecord!.message!.valueAsString, longMessage); + }); + }); + }); +} + +final _isSuccess = isA(); + +TypeMatcher _libRef(uriMatcher) => + isA().having((l) => l.uri, 'uri', uriMatcher); + +void expectEventually(Matcher expectation) {} diff --git a/dwds/test/fixtures/project.dart b/dwds/test/fixtures/project.dart index 3c2f42ab6..b06b1d5c8 100644 --- a/dwds/test/fixtures/project.dart +++ b/dwds/test/fixtures/project.dart @@ -102,14 +102,6 @@ class TestProject { htmlEntryFileName: 'index.html', ); - static const testDdcLibraryBundle = TestProject._( - packageName: '_test_sound', - packageDirectory: '_testSound', - webAssetsPath: 'example/hello_world', - dartEntryFileName: 'main_ddc_library_bundle.dart', - htmlEntryFileName: 'index.html', - ); - static final testScopes = TestProject._( packageName: '_test_sound', packageDirectory: '_testSound', From 344867dded73fd1b61b9c81393e6795ad699e90a Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Fri, 10 Jan 2025 14:30:25 -0500 Subject: [PATCH 12/20] delete main_ddc_library_bundle.dart --- .../common/chrome_proxy_service_common.dart | 2 +- .../hello_world/main_ddc_library_bundle.dart | 27 ------------------- 2 files changed, 1 insertion(+), 28 deletions(-) delete mode 100644 fixtures/_testSound/example/hello_world/main_ddc_library_bundle.dart diff --git a/dwds/test/common/chrome_proxy_service_common.dart b/dwds/test/common/chrome_proxy_service_common.dart index 4e849b279..bb4d57884 100644 --- a/dwds/test/common/chrome_proxy_service_common.dart +++ b/dwds/test/common/chrome_proxy_service_common.dart @@ -1,4 +1,4 @@ -// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// Copyright (c) 2019, 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. diff --git a/fixtures/_testSound/example/hello_world/main_ddc_library_bundle.dart b/fixtures/_testSound/example/hello_world/main_ddc_library_bundle.dart deleted file mode 100644 index 0ab7d09e5..000000000 --- a/fixtures/_testSound/example/hello_world/main_ddc_library_bundle.dart +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2025, 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 'dart:convert'; -import 'dart:developer'; -import 'dart:js'; - -// Create a series of top level objects for tests in -// dwds/test/chrome_proxy_service_ddc_library_bundle_test.dart - -void main() async { - context['registerExtension'] = (String method) { - registerExtension(method, - (String method, Map parameters) async { - return ServiceExtensionResponse.result(jsonEncode(parameters)); - }); - }; - - context['registerExtensionWithError'] = (String method) { - registerExtension(method, - (String method, Map parameters) async { - return ServiceExtensionResponse.error( - int.parse(parameters['code']!), parameters['details']!); - }); - }; -} From db9abfb86e25b8dbf0b249ed8e46dac454726ae8 Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Wed, 15 Jan 2025 16:14:46 -0500 Subject: [PATCH 13/20] fixed broken test - decoded response body to match source and renamed stack property --- dwds/lib/src/debugging/dart_scope.dart | 10 ++++++++++ dwds/test/chrome_proxy_service_amd_test.dart | 2 +- dwds/test/common/chrome_proxy_service_common.dart | 12 ++++++++++-- fixtures/_testSound/example/hello_world/index.html | 1 + 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/dwds/lib/src/debugging/dart_scope.dart b/dwds/lib/src/debugging/dart_scope.dart index 8183be19d..bfe0796f4 100644 --- a/dwds/lib/src/debugging/dart_scope.dart +++ b/dwds/lib/src/debugging/dart_scope.dart @@ -60,6 +60,16 @@ Future> visibleVariables({ final objectId = scope.object.objectId; if (objectId != null) { final properties = await inspector.getProperties(objectId); + for (final property in properties) { + if (property.name == '_\$35wc2\$35formal') { + final newProperty = Property({ + 'name': '_', + 'value': property.value, + }); + properties.remove(property); + properties.add(newProperty); + } + } allProperties.addAll(properties); } } diff --git a/dwds/test/chrome_proxy_service_amd_test.dart b/dwds/test/chrome_proxy_service_amd_test.dart index dbba5bab1..296914d5d 100644 --- a/dwds/test/chrome_proxy_service_amd_test.dart +++ b/dwds/test/chrome_proxy_service_amd_test.dart @@ -1,4 +1,4 @@ -// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file +// Copyright (c) 2025, 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. diff --git a/dwds/test/common/chrome_proxy_service_common.dart b/dwds/test/common/chrome_proxy_service_common.dart index bb4d57884..1145a4b8c 100644 --- a/dwds/test/common/chrome_proxy_service_common.dart +++ b/dwds/test/common/chrome_proxy_service_common.dart @@ -739,10 +739,18 @@ void runTests({ for (final scriptRef in scripts.scripts!) { final script = await service.getObject(isolate.id!, scriptRef.id!) as Script; - final serverPath = DartUri(script.uri!, 'hello_world/').serverPath; + var serverPath = DartUri(script.uri!, 'hello_world/').serverPath; + if (serverPath.startsWith('hello_world/packages/')) { + serverPath = serverPath.replaceFirst('hello_world/', ''); + } final result = await http .get(Uri.parse('http://localhost:${context.port}/$serverPath')); - expect(script.source, result.body); + // TODO: Figure out if we can encode the sript as utf8 and avoid this + final body = + (moduleFormat == ModuleFormat.ddc && canaryFeatures == true) + ? utf8.decode(result.body.codeUnits) + : result.body; + expect(script.source, body); expect(scriptRef.uri, endsWith('.dart')); expect(script.tokenPosTable, isNotEmpty); } diff --git a/fixtures/_testSound/example/hello_world/index.html b/fixtures/_testSound/example/hello_world/index.html index d93440a94..9bd15c874 100644 --- a/fixtures/_testSound/example/hello_world/index.html +++ b/fixtures/_testSound/example/hello_world/index.html @@ -1,6 +1,7 @@ + From 1bc9d5ca5763ec649d0d7388220655fad63af834 Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Wed, 15 Jan 2025 16:37:11 -0500 Subject: [PATCH 14/20] updated changelog --- dwds/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md index 1c39092bf..eff4116f7 100644 --- a/dwds/CHANGELOG.md +++ b/dwds/CHANGELOG.md @@ -1,4 +1,5 @@ ## 24.3.3-wip +- Added support for some debugging APIs with the DDC library bundle format. - [#2563](https://github.com/dart-lang/webdev/issues/2563) ## 24.3.2 @@ -7,7 +8,6 @@ ## 24.3.1 - Add support for binding DDS to a custom port. -- Added support for some debugging APIs with the DDC library bundle format. - [#2563](https://github.com/dart-lang/webdev/issues/2563) ## 24.3.0 From 32c12a50478f8085c63dc2935cab4da9c1a78793 Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Thu, 16 Jan 2025 10:47:32 -0500 Subject: [PATCH 15/20] updated dart_scope to not renamed wildcard and skipped related test case --- dwds/lib/src/debugging/dart_scope.dart | 10 ---------- dwds/test/common/chrome_proxy_service_common.dart | 4 +++- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/dwds/lib/src/debugging/dart_scope.dart b/dwds/lib/src/debugging/dart_scope.dart index bfe0796f4..8183be19d 100644 --- a/dwds/lib/src/debugging/dart_scope.dart +++ b/dwds/lib/src/debugging/dart_scope.dart @@ -60,16 +60,6 @@ Future> visibleVariables({ final objectId = scope.object.objectId; if (objectId != null) { final properties = await inspector.getProperties(objectId); - for (final property in properties) { - if (property.name == '_\$35wc2\$35formal') { - final newProperty = Property({ - 'name': '_', - 'value': property.value, - }); - properties.remove(property); - properties.add(newProperty); - } - } allProperties.addAll(properties); } } diff --git a/dwds/test/common/chrome_proxy_service_common.dart b/dwds/test/common/chrome_proxy_service_common.dart index 1145a4b8c..d61c47613 100644 --- a/dwds/test/common/chrome_proxy_service_common.dart +++ b/dwds/test/common/chrome_proxy_service_common.dart @@ -1555,7 +1555,9 @@ void runTests({ expect(first.vars, hasLength(greaterThanOrEqualTo(1))); final underscore = first.vars!.firstWhere((v) => v.name == '_'); expect(underscore, isNotNull); - }); + }, + skip: 'https://github.com/dart-lang/webdev/issues/2570', + ); test('collects async frames', () async { final stack = await breakAt('asyncCall'); From aa38d3aa8614ee098afb3d5cb076feb84faadbcc Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Thu, 16 Jan 2025 10:50:29 -0500 Subject: [PATCH 16/20] formatted test/common/chrome_proxy_service_common.dart --- .../common/chrome_proxy_service_common.dart | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/dwds/test/common/chrome_proxy_service_common.dart b/dwds/test/common/chrome_proxy_service_common.dart index d61c47613..e53f805bb 100644 --- a/dwds/test/common/chrome_proxy_service_common.dart +++ b/dwds/test/common/chrome_proxy_service_common.dart @@ -1542,19 +1542,21 @@ void runTests({ expect(first.code!.name, 'printCount'); }); - test('stack has a variable', () async { - final stack = await breakAt('callPrintCount'); - expect(stack, isNotNull); - expect(stack.frames, hasLength(1)); - final first = stack.frames!.first; - expect(first.kind, 'Regular'); - expect(first.code!.kind, 'Dart'); - expect(first.code!.name, ''); - // TODO: Make this more precise once this case doesn't - // also include all the libraries. - expect(first.vars, hasLength(greaterThanOrEqualTo(1))); - final underscore = first.vars!.firstWhere((v) => v.name == '_'); - expect(underscore, isNotNull); + test( + 'stack has a variable', + () async { + final stack = await breakAt('callPrintCount'); + expect(stack, isNotNull); + expect(stack.frames, hasLength(1)); + final first = stack.frames!.first; + expect(first.kind, 'Regular'); + expect(first.code!.kind, 'Dart'); + expect(first.code!.name, ''); + // TODO: Make this more precise once this case doesn't + // also include all the libraries. + expect(first.vars, hasLength(greaterThanOrEqualTo(1))); + final underscore = first.vars!.firstWhere((v) => v.name == '_'); + expect(underscore, isNotNull); }, skip: 'https://github.com/dart-lang/webdev/issues/2570', ); From d4ec13fe4ea48ea7ed4d59410dbb3579f76bfbb2 Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Thu, 16 Jan 2025 11:52:32 -0500 Subject: [PATCH 17/20] replaced wildcard with timer --- .../common/chrome_proxy_service_common.dart | 32 ++++++++----------- .../_testSound/example/hello_world/main.dart | 2 +- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/dwds/test/common/chrome_proxy_service_common.dart b/dwds/test/common/chrome_proxy_service_common.dart index e53f805bb..2a54c9e1b 100644 --- a/dwds/test/common/chrome_proxy_service_common.dart +++ b/dwds/test/common/chrome_proxy_service_common.dart @@ -1542,24 +1542,20 @@ void runTests({ expect(first.code!.name, 'printCount'); }); - test( - 'stack has a variable', - () async { - final stack = await breakAt('callPrintCount'); - expect(stack, isNotNull); - expect(stack.frames, hasLength(1)); - final first = stack.frames!.first; - expect(first.kind, 'Regular'); - expect(first.code!.kind, 'Dart'); - expect(first.code!.name, ''); - // TODO: Make this more precise once this case doesn't - // also include all the libraries. - expect(first.vars, hasLength(greaterThanOrEqualTo(1))); - final underscore = first.vars!.firstWhere((v) => v.name == '_'); - expect(underscore, isNotNull); - }, - skip: 'https://github.com/dart-lang/webdev/issues/2570', - ); + test('stack has a variable', () async { + final stack = await breakAt('callPrintCount'); + expect(stack, isNotNull); + expect(stack.frames, hasLength(1)); + final first = stack.frames!.first; + expect(first.kind, 'Regular'); + expect(first.code!.kind, 'Dart'); + expect(first.code!.name, ''); + // TODO: Make this more precise once this case doesn't + // also include all the libraries. + expect(first.vars, hasLength(greaterThanOrEqualTo(1))); + final underscore = first.vars!.firstWhere((v) => v.name == 'timer'); + expect(underscore, isNotNull); + }); test('collects async frames', () async { final stack = await breakAt('asyncCall'); diff --git a/fixtures/_testSound/example/hello_world/main.dart b/fixtures/_testSound/example/hello_world/main.dart index cca39c9ce..b817d5b70 100644 --- a/fixtures/_testSound/example/hello_world/main.dart +++ b/fixtures/_testSound/example/hello_world/main.dart @@ -76,7 +76,7 @@ void main() async { scheduleMicrotask(() => throw Exception('UncaughtException')); }; - Timer.periodic(const Duration(seconds: 1), (_) { + Timer.periodic(const Duration(seconds: 1), (timer) { printCount(); // Breakpoint: callPrintCount }); From f5758a2cf5782da1382886121f2c1060fe0dc3e3 Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Fri, 17 Jan 2025 12:27:12 -0500 Subject: [PATCH 18/20] updated call to DartUri and removed unecessary check --- dwds/test/common/chrome_proxy_service_common.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/dwds/test/common/chrome_proxy_service_common.dart b/dwds/test/common/chrome_proxy_service_common.dart index 2a54c9e1b..f640ae0d1 100644 --- a/dwds/test/common/chrome_proxy_service_common.dart +++ b/dwds/test/common/chrome_proxy_service_common.dart @@ -739,10 +739,7 @@ void runTests({ for (final scriptRef in scripts.scripts!) { final script = await service.getObject(isolate.id!, scriptRef.id!) as Script; - var serverPath = DartUri(script.uri!, 'hello_world/').serverPath; - if (serverPath.startsWith('hello_world/packages/')) { - serverPath = serverPath.replaceFirst('hello_world/', ''); - } + final serverPath = DartUri(script.uri!, '').serverPath; final result = await http .get(Uri.parse('http://localhost:${context.port}/$serverPath')); // TODO: Figure out if we can encode the sript as utf8 and avoid this From 7a601a11a0adbcce45970384dc88a99c44aa77cd Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Wed, 22 Jan 2025 13:09:28 -0500 Subject: [PATCH 19/20] updated DCM version --- .github/workflows/dcm.yml | 2 +- dwds/CHANGELOG.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dcm.yml b/.github/workflows/dcm.yml index adc6be4c5..573048c23 100644 --- a/.github/workflows/dcm.yml +++ b/.github/workflows/dcm.yml @@ -19,7 +19,7 @@ jobs: wget -qO- https://dcm.dev/pgp-key.public | sudo gpg --dearmor -o /usr/share/keyrings/dcm.gpg echo 'deb [signed-by=/usr/share/keyrings/dcm.gpg arch=amd64] https://dcm.dev/debian stable main' | sudo tee /etc/apt/sources.list.d/dart_stable.list sudo apt-get update - sudo apt-get install dcm=1.16.2-1 # To avoid errors add `-1` (build number) to the version + sudo apt-get install dcm=1.26.0 # To avoid errors add `-1` (build number) to the version sudo chmod +x /usr/bin/dcm - name: Setup Dart SDK uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md index eff4116f7..062448a80 100644 --- a/dwds/CHANGELOG.md +++ b/dwds/CHANGELOG.md @@ -1,5 +1,6 @@ ## 24.3.3-wip - Added support for some debugging APIs with the DDC library bundle format. - [#2563](https://github.com/dart-lang/webdev/issues/2563) +- Update `DCM` version to `1.26.0` ## 24.3.2 From a76ffcc50e0023b8d270089f3a46c65d03a77d50 Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Wed, 22 Jan 2025 13:17:32 -0500 Subject: [PATCH 20/20] updated DCM version to 1.26.0-1 --- .github/workflows/dcm.yml | 2 +- dwds/CHANGELOG.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dcm.yml b/.github/workflows/dcm.yml index 573048c23..c5725d874 100644 --- a/.github/workflows/dcm.yml +++ b/.github/workflows/dcm.yml @@ -19,7 +19,7 @@ jobs: wget -qO- https://dcm.dev/pgp-key.public | sudo gpg --dearmor -o /usr/share/keyrings/dcm.gpg echo 'deb [signed-by=/usr/share/keyrings/dcm.gpg arch=amd64] https://dcm.dev/debian stable main' | sudo tee /etc/apt/sources.list.d/dart_stable.list sudo apt-get update - sudo apt-get install dcm=1.26.0 # To avoid errors add `-1` (build number) to the version + sudo apt-get install dcm=1.26.0-1 # To avoid errors add `-1` (build number) to the version sudo chmod +x /usr/bin/dcm - name: Setup Dart SDK uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md index 062448a80..b7d2c8fd1 100644 --- a/dwds/CHANGELOG.md +++ b/dwds/CHANGELOG.md @@ -1,6 +1,6 @@ ## 24.3.3-wip - Added support for some debugging APIs with the DDC library bundle format. - [#2563](https://github.com/dart-lang/webdev/issues/2563) -- Update `DCM` version to `1.26.0` +- Update `DCM` version to `1.26.0-1` ## 24.3.2