blob: 43e84d46b40bc71f79018dd4d40be60269b5f267 [file] [log] [blame]
// 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.
@TestOn('vm')
@Tags(['daily'])
@Timeout(Duration(minutes: 2))
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:dwds/src/config/tool_configuration.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 'package:webkit_inspection_protocol/webkit_inspection_protocol.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 provider = TestSdkConfigurationProvider(verbose: debug);
tearDownAll(provider.dispose);
final context = TestContext(TestProject.testWithSoundNullSafety, provider);
group('shared context', () {
setUpAll(() async {
setCurrentLogWriter(debug: debug);
await context.setUp(
testSettings: TestSettings(
enableExpressionEvaluation: true,
verboseCompiler: false,
),
);
});
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(<String>[]),
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<InstanceRef>().having(
(instance) => instance.valueAsString,
'value',
'world',
),
);
});
test('can return bools', () async {
expect(
await service.evaluate(
isolate.id!,
bootstrap!.id!,
'helloBool(true)',
),
const TypeMatcher<InstanceRef>().having(
(instance) => instance.valueAsString,
'valueAsString',
'true',
),
);
expect(
await service.evaluate(
isolate.id!,
bootstrap!.id!,
'helloBool(false)',
),
const TypeMatcher<InstanceRef>().having(
(instance) => instance.valueAsString,
'valueAsString',
'false',
),
);
});
test('can return nums', () async {
expect(
await service.evaluate(
isolate.id!,
bootstrap!.id!,
'helloNum(42.0)',
),
const TypeMatcher<InstanceRef>().having(
(instance) => instance.valueAsString,
'valueAsString',
'42',
),
);
expect(
await service.evaluate(
isolate.id!,
bootstrap!.id!,
'helloNum(42.2)',
),
const TypeMatcher<InstanceRef>().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<InstanceRef>()
.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<InstanceRef> 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<InstanceRef>().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<InstanceRef>().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<FlagList>());
});
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<Isolate>());
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);
});
/// Helper to create a list of 1001 elements, doing a direct JS eval.
Future<RemoteObject> createList() {
final expr = '''
(function () {
const sdk = ${globalToolConfiguration.loadStrategy.loadModuleSnippet}("dart_sdk");
const list = sdk.dart.dsend(sdk.core.List,"filled", [1001, 5]);
list[4] = 100;
return list;
})()''';
return service.inspector.jsEvaluate(expr);
}
/// Helper to create a LinkedHashMap with 1001 entries, doing a direct JS eval.
Future<RemoteObject> createMap() {
final expr = '''
(function () {
const sdk = ${globalToolConfiguration.loadStrategy.loadModuleSnippet}("dart_sdk");
const iterable = sdk.dart.dsend(sdk.core.Iterable, "generate", [1001]);
const list1 = sdk.dart.dsend(iterable, "toList", []);
const reversed = sdk.dart.dload(list1, "reversed");
const list2 = sdk.dart.dsend(reversed, "toList", []);
const map = sdk.dart.dsend(list2, "asMap", []);
const linkedMap = sdk.dart.dsend(sdk.collection.LinkedHashMap, "from", [map]);
return linkedMap;
})()''';
return service.inspector.jsEvaluate(expr);
}
test('Lists', () async {
final list = await createList();
final inst =
await service.getObject(isolate.id!, list.objectId!) 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 createMap();
final inst =
await service.getObject(isolate.id!, map.objectId!) 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 (var 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 createList();
final inst = await service.getObject(
isolate.id!,
list.objectId!,
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 createList();
final inst = await service.getObject(
isolate.id!,
list.objectId!,
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 createList();
final inst = await service.getObject(
isolate.id!,
list.objectId!,
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 createList();
final inst = await service.getObject(
isolate.id!,
list.objectId!,
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 createList();
final inst = await service.getObject(
isolate.id!,
list.objectId!,
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 createList();
final inst = await service.getObject(
isolate.id!,
list.objectId!,
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 createList();
final inst = await service.getObject(
isolate.id!,
list.objectId!,
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 createMap();
final inst = await service.getObject(
isolate.id!,
map.objectId!,
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 list = await createMap();
final inst = await service.getObject(
isolate.id!,
list.objectId!,
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 createMap();
final inst = await service.getObject(
isolate.id!,
map.objectId!,
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 createMap();
final inst = await service.getObject(
isolate.id!,
map.objectId!,
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 createMap();
final inst = await service.getObject(
isolate.id!,
map.objectId!,
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 list = await createMap();
final inst = await service.getObject(
isolate.id!,
list.objectId!,
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 list = await createMap();
final inst = await service.getObject(
isolate.id!,
list.objectId!,
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 list = await createMap();
final inst = await service.getObject(
isolate.id!,
list.objectId!,
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<Event> 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<Event> 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, '<closure>');
});
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, '<closure>');
});
});
group('getStack', () {
late VmServiceInterface service;
String? isolateId;
late Stream<Event> 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<Stack> 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, '<closure>');
// 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<LibraryRef>().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<InstanceRef>().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<InstanceRef>().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<InstanceRef>().having(
(instance) => instance.valueAsString,
'helloString',
'abc',
),
);
expect(
remote,
const TypeMatcher<InstanceRef>()
.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<InstanceRef>().having(
(instance) => instance.valueAsString,
'helloString',
'null',
),
);
expect(
remote,
const TypeMatcher<InstanceRef>()
.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<InstanceRef>().having(
(instance) => instance.valueAsString,
'helloBool',
'true',
),
);
expect(
remote,
const TypeMatcher<InstanceRef>()
.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<InstanceRef>().having(
(instance) => instance.valueAsString,
'helloNum',
'123',
),
);
expect(
remote,
const TypeMatcher<InstanceRef>()
.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<InstanceRef>().having(
(instance) => instance.valueAsString,
'messagesCombined',
'worldworld',
),
);
expect(
remote,
const TypeMatcher<InstanceRef>()
.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<Success>());
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<Success>());
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<String>.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<String>.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<String>.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('streamListen/onEvent', () {
late ChromeProxyService service;
group('Debug', () {
late Stream<Event> eventStream;
setUp(() async {
setCurrentLogWriter(debug: debug);
service = context.service;
expect(
await service.streamListen('Debug'),
const TypeMatcher<Success>(),
);
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<Event>()
.having((e) => e.kind, 'kind', EventKind.kPauseInterrupted),
),
);
safeUnawaited(context.tabConnection.debugger.resume());
expect(
eventStream,
emitsThrough(
const TypeMatcher<Event>()
.having((e) => e.kind, 'kind', EventKind.kResume),
),
);
});
test('Inspect', () async {
expect(
eventStream,
emitsThrough(
const TypeMatcher<Event>()
.having((e) => e.kind, 'kind', EventKind.kInspect)
.having(
(e) => e.inspectee,
'inspectee',
const TypeMatcher<InstanceRef>()
.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<Event> eventStream;
setUp(() async {
setCurrentLogWriter(debug: debug);
service = context.service;
expect(
await service.streamListen('Extension'),
const TypeMatcher<Success>(),
);
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<Event> eventMatcher(
String data,
) =>
const TypeMatcher<Event>()
.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 (var data in batch1) {
await context.tabConnection.runtime.evaluate(emitDebugEvent(data));
}
await Future.delayed(delay);
for (var data in batch2) {
await context.tabConnection.runtime.evaluate(emitDebugEvent(data));
}
});
});
test('GC', () async {
expect(service.streamListen('GC'), completion(_isSuccess));
});
group('Isolate', () {
late Stream<Event> 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<Event> eventMatcher(String extension) =>
const TypeMatcher<Event>()
.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 (var 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,
),
),
);
});
});
test('Logging', () async {
final service = context.service;
expect(
service.streamListen(EventStreams.kLogging),
completion(_isSuccess),
);
final stream = service.onEvent(EventStreams.kLogging);
final message = 'myMessage';
safeUnawaited(
context.tabConnection.runtime.evaluate("sendLog('$message');"),
);
final event = await stream.first;
expect(event.kind, EventKind.kLogging);
final logRecord = event.logRecord!;
expect(logRecord.message!.valueAsString, message);
expect(logRecord.loggerName!.valueAsString, 'testLogCategory');
});
});
}
final _isSuccess = isA<Success>();
TypeMatcher _libRef(uriMatcher) =>
isA<LibraryRef>().having((l) => l.uri, 'uri', uriMatcher);