[ DDS ] Migrate DDS specific tests from service/ and update package:dds_service_extensions to 1.7.0

TEST=migrated tests

Change-Id: Id77f46c7e348614f707fc4d50d8a3101120b48ff
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/347941
Reviewed-by: Derek Xu <derekx@google.com>
Commit-Queue: Ben Konyi <bkonyi@google.com>
This commit is contained in:
Ben Konyi
2024-01-26 22:45:14 +00:00
committed by Commit Queue
parent 1d1531c8d9
commit d48a76e988
37 changed files with 2248 additions and 1036 deletions
+37
View File
@@ -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.
import 'package:dds_service_extensions/dds_service_extensions.dart';
import 'package:test/test.dart';
import 'package:vm_service/vm_service.dart';
import 'common/test_helper.dart';
final tests = <VMTest>[
(VmService service) async {
final defaultClientName = 'client1';
final clientName = 'agent-007';
var result = await service.getClientName();
expect(result.name, defaultClientName);
// Set the name for this client.
await service.setClientName(clientName);
// Check it was set properly.
result = await service.getClientName();
expect(result.name, clientName);
// Check clearing works properly.
await service.setClientName();
result = await service.getClientName();
expect(result.name, defaultClientName);
},
];
void main([args = const <String>[]]) => runVMTests(
args,
tests,
'client_name_rpc_test.dart',
);
@@ -0,0 +1,62 @@
// 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.
import 'package:vm_service/vm_service.dart';
import 'client_resume_approvals_common.dart';
import 'common/service_test_common.dart';
import 'common/test_helper.dart';
const String clientName = 'TestClient';
const String otherClientName = 'OtherTestClient';
void fooBar() {
int i = 0;
print(i);
}
final test = <IsolateTest>[
hasPausedAtStart,
(VmService service, IsolateRef isolate) async {
final isolateId = isolate.id!;
final client1 = await createClient(
service: service,
clientName: clientName,
onPauseStart: true,
);
final client2 = await createClient(
service: service,
clientName: otherClientName,
onPauseStart: true,
);
// Give resume approval for client1 to ensure approval state is cleaned up
// properly when both client1 and client2 have disconnected.
await client1.resume(isolateId);
await hasPausedAtStart(service, isolate);
// Once client1 is disconnected, we should still be paused.
client1.dispose();
await hasPausedAtStart(service, isolate);
// Once client2 disconnects, there are no clients which require resume
// approval. Since there were no resume requests made by clients which are
// still connected, the isolate remains paused.
client2.dispose();
await hasPausedAtStart(service, isolate);
await service.resume(isolateId);
},
hasStoppedAtExit,
];
void main([args = const <String>[]]) => runIsolateTests(
args,
test,
'client_resume_approvals_approve_then_disconnect_test.dart',
testeeConcurrent: fooBar,
pauseOnStart: true,
pauseOnExit: true,
);
@@ -0,0 +1,24 @@
// 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.
import 'package:dds_service_extensions/dds_service_extensions.dart';
import 'package:vm_service/vm_service.dart';
import 'package:vm_service/vm_service_io.dart';
Future<VmService> createClient({
required VmService service,
required String clientName,
bool onPauseStart = false,
bool onPauseExit = false,
bool onPauseReload = false,
}) async {
final client = await vmServiceConnectUri(service.wsUri!);
await client.setClientName(clientName);
await client.requirePermissionToResume(
onPauseStart: onPauseStart,
onPauseExit: onPauseExit,
onPauseReload: onPauseReload,
);
return client;
}
@@ -0,0 +1,59 @@
// 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.
import 'package:vm_service/vm_service.dart';
import 'client_resume_approvals_common.dart';
import 'common/service_test_common.dart';
import 'common/test_helper.dart';
const String clientName = 'TestClient';
const String otherClientName = 'OtherTestClient';
void fooBar() {
int i = 0;
print(i);
}
final test = <IsolateTest>[
// Multiple clients, disconnect client awaiting approval.
hasPausedAtStart,
(VmService service, IsolateRef isolate) async {
final isolateId = isolate.id!;
final client1 = await createClient(
service: service,
clientName: clientName,
onPauseStart: true,
);
final client2 = await createClient(
service: service,
clientName: otherClientName,
onPauseStart: true,
);
// Send a resume request on the test client so we'll resume once the other
// clients which require approval disconnect.
await service.resume(isolateId);
await hasPausedAtStart(service, isolate);
// Once client1 is disconnected, we should still be paused.
await client1.dispose();
await hasPausedAtStart(service, isolate);
// Once client2 disconnects, there are no clients which require resume
// approval. Ensure we resume immediately so we don't deadlock waiting for
// approvals from disconnected clients.
await client2.dispose();
},
hasStoppedAtExit,
];
void main([args = const <String>[]]) => runIsolateTests(
args,
test,
'client_resume_approvals_disconnect_test.dart',
testeeConcurrent: fooBar,
pauseOnStart: true,
pauseOnExit: true,
);
@@ -0,0 +1,44 @@
// 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.
import 'package:vm_service/vm_service.dart';
import 'client_resume_approvals_common.dart';
import 'common/service_test_common.dart';
import 'common/test_helper.dart';
const String clientName = 'TestClient';
void fooBar() {
int i = 0;
print(i);
}
final test = <IsolateTest>[
// Multiple clients, same client names.
(VmService service, IsolateRef isolateRef) async {
// ignore: unused_local_variable
final client1 = await createClient(
service: service,
clientName: clientName,
onPauseStart: true,
);
final client2 = await createClient(
service: service,
clientName: clientName,
);
await hasPausedAtStart(service, isolateRef);
await resumeIsolate(client2, isolateRef);
},
hasStoppedAtExit,
];
void main([args = const <String>[]]) => runIsolateTests(
args,
test,
'client_resume_approvals_identical_names_test.dart',
testeeConcurrent: fooBar,
pauseOnStart: true,
pauseOnExit: true,
);
@@ -0,0 +1,62 @@
// 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.
import 'package:dds_service_extensions/dds_service_extensions.dart';
import 'package:vm_service/vm_service.dart';
import 'client_resume_approvals_common.dart';
import 'common/service_test_common.dart';
import 'common/test_helper.dart';
const String clientName = 'TestClient';
const String otherClientName = 'OtherTestClient';
const String dummyClientName = 'DummyClient';
void fooBar() {
int i = 0;
print(i);
}
final test = <IsolateTest>[
// Multiple clients, different client names.
(VmService service, IsolateRef isolateRef) async {
final isolateId = isolateRef.id!;
final client1 = await createClient(
service: service,
clientName: clientName,
onPauseStart: true,
onPauseExit: true,
);
final client2 = await createClient(
service: service,
clientName: otherClientName,
);
// ignore: unused_local_variable
final client3 = await createClient(
service: service,
clientName: 'DummyClient',
);
await hasPausedAtStart(service, isolateRef);
await client2.resume(isolateId);
await hasPausedAtStart(service, isolateRef);
await client1.resume(isolateId);
await hasStoppedAtExit(service, isolateRef);
await client2.requirePermissionToResume(
onPauseExit: true,
);
await client1.resume(isolateId);
await hasStoppedAtExit(service, isolateRef);
await client2.resume(isolateId);
},
];
void main([args = const <String>[]]) => runIsolateTests(
args,
test,
'client_resume_approvals_multiple_names_test.dart',
testeeConcurrent: fooBar,
pauseOnStart: true,
pauseOnExit: true,
);
@@ -0,0 +1,64 @@
// 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.
import 'package:dds_service_extensions/dds_service_extensions.dart';
import 'package:vm_service/vm_service.dart';
import 'package:vm_service/vm_service_io.dart';
import 'client_resume_approvals_common.dart';
import 'common/service_test_common.dart';
import 'common/test_helper.dart';
const String clientName = 'TestClient';
const String otherClientName = 'OtherTestClient';
const String dummyClientName = 'DummyClient';
void fooBar() {
int i = 0;
print(i);
}
final test = <IsolateTest>[
// Remove required approvals via name change.
(VmService service, IsolateRef isolateRef) async {
final isolateId = isolateRef.id!;
// Create two clients with the same name.
final client1 = await createClient(
service: service,
clientName: clientName,
onPauseStart: true,
);
// Don't use the helper so we don't call `requirePermissionToResume`
final client2 = await vmServiceConnectUri(service.wsUri!);
await client2.setClientName(clientName);
final client3 = await createClient(
service: service,
clientName: otherClientName,
);
// Check that client3 can't resume the isolate on its own.
await hasPausedAtStart(service, isolateRef);
await client3.resume(isolateId);
await hasPausedAtStart(service, isolateRef);
// Change the name of client1. Since client2 has the same name that client1
// originally had, the service still requires approval to resume the
// isolate.
await client1.setClientName('foobar');
await hasPausedAtStart(service, isolateRef);
await client2.setClientName('baz');
},
hasStoppedAtExit,
];
void main([args = const <String>[]]) => runIsolateTests(
args,
test,
'client_resume_approvals_name_change_test.dart',
testeeConcurrent: fooBar,
pauseOnStart: true,
pauseOnExit: true,
);
@@ -0,0 +1,60 @@
// 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.
import 'package:vm_service/vm_service.dart';
import 'client_resume_approvals_common.dart';
import 'common/service_test_common.dart';
import 'common/test_helper.dart';
const String clientName = 'TestClient';
const String otherClientName = 'OtherTestClient';
void fooBar() {
// ignore: unused_local_variable
int i = 0;
while (true) {
i++;
}
}
late VmService client1;
late VmService client2;
final test = <IsolateTest>[
// Multiple clients, hot reload approval.
(VmService service, IsolateRef isolateRef) async {
client1 = await createClient(
service: service,
clientName: clientName,
onPauseReload: true,
);
client2 = await createClient(
service: service,
clientName: otherClientName,
onPauseReload: true,
);
},
hasPausedAtStart,
// Paused on start, resume.
resumeIsolate,
// Reload and then pause.
reloadSources(pause: true),
hasStoppedPostRequest,
(VmService service, IsolateRef isolateRef) async {
final isolateId = isolateRef.id!;
// Check that client2 can't resume the isolate on its own.
await client2.resume(isolateId);
await hasStoppedPostRequest(service, isolateRef);
await resumeIsolate(client1, isolateRef);
},
];
void main([args = const <String>[]]) => runIsolateTests(
args,
test,
'client_resume_approvals_reload_test.dart',
testeeConcurrent: fooBar,
pauseOnStart: true,
);
@@ -0,0 +1,717 @@
// 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.
// NOTE: this file was originally copied from package:vm_service.
library service_test_common;
import 'dart:async';
import 'dart:developer';
import 'dart:typed_data';
import 'package:path/path.dart';
import 'package:test/test.dart';
import 'package:vm_service/vm_service.dart';
typedef IsolateTest = Future<void> Function(
VmService service,
IsolateRef isolate,
);
typedef VMTest = Future<void> Function(VmService service);
Future<void> smartNext(VmService service, IsolateRef isolateRef) async {
print('smartNext');
final isolate = await service.getIsolate(isolateRef.id!);
final Event event = isolate.pauseEvent!;
if (event.kind == EventKind.kPauseBreakpoint) {
// TODO(bkonyi): remove needless refetching of isolate object.
if (event.atAsyncSuspension ?? false) {
return asyncNext(service, isolateRef);
} else {
return syncNext(service, isolateRef);
}
} else {
throw 'The program is already running';
}
}
Future<void> asyncNext(VmService service, IsolateRef isolateRef) async {
print('asyncNext');
final id = isolateRef.id!;
final isolate = await service.getIsolate(id);
final event = isolate.pauseEvent!;
if (event.kind == EventKind.kPauseBreakpoint) {
final dynamic event = isolate.pauseEvent;
if (!event.atAsyncSuspension) {
throw 'No async continuation at this location';
} else {
await service.resume(id, step: 'OverAsyncSuspension');
}
} else {
throw 'The program is already running';
}
}
Future<void> syncNext(VmService service, IsolateRef isolateRef) async {
print('syncNext');
final id = isolateRef.id!;
final isolate = await service.getIsolate(id);
final event = isolate.pauseEvent!;
if (event.kind == EventKind.kPauseBreakpoint) {
await service.resume(id, step: 'Over');
} else {
throw 'The program is already running';
}
}
// WARNING: interleaving calls based on hasPausedFor using Future.wait() may
// cause the debug stream to be cancelled after one of the checks completes.
// If another check is waiting on an event, it will no longer be notified of
// the event, causing the test to hang.
Future<void> hasPausedFor(
VmService service,
IsolateRef isolateRef,
String kind,
) async {
Completer<dynamic>? completer = Completer();
late StreamSubscription<Event> subscription;
subscription = service.onDebugEvent.listen((event) async {
print('subscription event: $event');
if ((isolateRef.id == event.isolate!.id) && (event.kind == kind)) {
if (completer != null) {
try {
await service.streamCancel(EventStreams.kDebug);
} catch (_) {/* swallow exception */} finally {
await subscription.cancel();
completer?.complete();
print('complete');
completer = null;
}
}
}
});
await _subscribeDebugStream(service);
// Pause may have happened before we subscribed.
final id = isolateRef.id!;
final isolate = await service.getIsolate(id);
final event = isolate.pauseEvent!;
print(event);
if (event.kind == kind) {
if (completer != null) {
try {
await service.streamCancel(EventStreams.kDebug);
} catch (_) {/* swallow exception */} finally {
await subscription.cancel();
completer?.complete();
}
}
}
return completer?.future; // Will complete when breakpoint hit.
}
// WARNING: interleaving calls based on hasPausedFor using Future.wait() may
// cause the debug stream to be cancelled after one of the checks completes.
// If another check is waiting on an event, it will no longer be notified of
// the event, causing the test to hang.
Future<void> hasStoppedAtBreakpoint(VmService service, IsolateRef isolate) {
return hasPausedFor(service, isolate, EventKind.kPauseBreakpoint);
}
// WARNING: interleaving calls based on hasPausedFor using Future.wait() may
// cause the debug stream to be cancelled after one of the checks completes.
// If another check is waiting on an event, it will no longer be notified of
// the event, causing the test to hang.
Future<void> hasStoppedPostRequest(VmService service, IsolateRef isolate) {
return hasPausedFor(service, isolate, EventKind.kPausePostRequest);
}
// WARNING: interleaving calls based on hasPausedFor using Future.wait() may
// cause the debug stream to be cancelled after one of the checks completes.
// If another check is waiting on an event, it will no longer be notified of
// the event, causing the test to hang.
Future<void> hasStoppedWithUnhandledException(
VmService service,
IsolateRef isolate,
) {
return hasPausedFor(service, isolate, EventKind.kPauseException);
}
// WARNING: interleaving calls based on hasPausedFor using Future.wait() may
// cause the debug stream to be cancelled after one of the checks completes.
// If another check is waiting on an event, it will no longer be notified of
// the event, causing the test to hang.
Future<void> hasStoppedAtExit(VmService service, IsolateRef isolate) {
return hasPausedFor(service, isolate, EventKind.kPauseExit);
}
// WARNING: interleaving calls based on hasPausedFor using Future.wait() may
// cause the debug stream to be cancelled after one of the checks completes.
// If another check is waiting on an event, it will no longer be notified of
// the event, causing the test to hang.
Future<void> hasPausedAtStart(VmService service, IsolateRef isolate) {
return hasPausedFor(service, isolate, EventKind.kPauseStart);
}
Future<void> markDartColonLibrariesDebuggable(
VmService service,
IsolateRef isolateRef,
) async {
final isolateId = isolateRef.id!;
final isolate = await service.getIsolate(isolateId);
final requests = <Future>[];
for (final libRef in isolate.libraries!) {
final lib = await service.getObject(isolateId, libRef.id!) as Library;
if (lib.uri!.startsWith('dart:') && !lib.uri!.startsWith('dart:_')) {
requests.add(service.setLibraryDebuggable(isolateId, lib.id!, true));
}
}
await Future.wait(requests);
}
// Currying is your friend.
IsolateTest setBreakpointAtLine(int line) {
return (VmService service, IsolateRef isolateRef) async {
print('Setting breakpoint for line $line');
final isolateId = isolateRef.id!;
final isolate = await service.getIsolate(isolateId);
final Library lib =
(await service.getObject(isolateId, isolate.rootLib!.id!)) as Library;
final script = lib.scripts!.first;
final Breakpoint bpt =
await service.addBreakpoint(isolateId, script.id!, line);
print('Breakpoint is $bpt');
};
}
IsolateTest setBreakpointAtUriAndLine(String uri, int line) {
return (VmService service, IsolateRef isolateRef) async {
print('Setting breakpoint for line $line in $uri');
final Breakpoint bpt =
await service.addBreakpointWithScriptUri(isolateRef.id!, uri, line);
print('Breakpoint is $bpt');
expect(bpt, isNotNull);
};
}
IsolateTest setBreakpointAtLineColumn(int line, int column) {
return (VmService service, IsolateRef isolateRef) async {
print('Setting breakpoint for line $line column $column');
final isolateId = isolateRef.id!;
final isolate = await service.getIsolate(isolateId);
final lib =
await service.getObject(isolateId, isolate.rootLib!.id!) as Library;
final ScriptRef script = lib.scripts!.firstWhere((s) => s.uri == lib.uri);
final Breakpoint bpt = await service.addBreakpoint(
isolateId,
script.id!,
line,
column: column,
);
print('Breakpoint is $bpt');
expect(bpt, isNotNull);
};
}
IsolateTest stoppedAtLine(int line) {
return (VmService service, IsolateRef isolateRef) async {
print('Checking we are at line $line');
// Make sure that the isolate has stopped.
final id = isolateRef.id!;
final isolate = await service.getIsolate(id);
final event = isolate.pauseEvent!;
expect(event.kind != EventKind.kResume, isTrue);
final stack = await service.getStack(id);
final frames = stack.frames!;
expect(frames.length, greaterThanOrEqualTo(1));
final top = frames[0];
final Script script =
(await service.getObject(id, top.location!.script!.id!)) as Script;
final int actualLine =
script.getLineNumberFromTokenPos(top.location!.tokenPos!)!;
if (actualLine != line) {
print('Actual: $actualLine Line: $line');
final sb = StringBuffer();
sb.write('Expected to be at line $line but actually at line $actualLine');
sb.write('\nFull stack trace:\n');
for (Frame f in frames) {
sb.write(
' $f [${script.getLineNumberFromTokenPos(f.location!.tokenPos!)}]\n',
);
}
throw sb.toString();
} else {
print('Program is stopped at line: $line');
}
};
}
Future<void> resumeIsolate(VmService service, IsolateRef isolate) async {
final Completer completer = Completer();
late StreamSubscription<Event> subscription;
bool cancelStreamAfterResume = false;
subscription = service.onDebugEvent.listen((event) async {
if (event.kind == EventKind.kResume) {
try {
if (cancelStreamAfterResume) {
await service.streamCancel(EventStreams.kDebug);
}
} catch (_) {/* swallow exception */} finally {
await subscription.cancel();
completer.complete();
}
}
});
cancelStreamAfterResume = await _subscribeDebugStream(service);
await service.resume(isolate.id!);
return completer.future;
}
Future<bool> _subscribeDebugStream(VmService service) async {
try {
await service.streamListen(EventStreams.kDebug);
return true;
} catch (_) {
/* swallow exception */
return false;
}
}
Future<void> _unsubscribeDebugStream(VmService service) async {
try {
await service.streamCancel(EventStreams.kDebug);
} catch (_) {
/* swallow exception */
}
}
Future<void> resumeAndAwaitEvent(
VmService service,
IsolateRef isolateRef,
String streamId,
Function(Event) onEvent,
) async {
final completer = Completer<void>();
late final StreamSubscription sub;
sub = service.onEvent(streamId).listen((event) async {
await onEvent(event);
await sub.cancel();
await service.streamCancel(streamId);
completer.complete();
});
await service.streamListen(streamId);
await service.resume(isolateRef.id!);
return completer.future;
}
IsolateTest resumeIsolateAndAwaitEvent(
String streamId,
Function(Event) onEvent,
) {
return (VmService service, IsolateRef isolate) async =>
resumeAndAwaitEvent(service, isolate, streamId, onEvent);
}
Future<void> stepOver(VmService service, IsolateRef isolateRef) async {
await _subscribeDebugStream(service);
await service.resume(isolateRef.id!, step: 'Over');
await hasStoppedAtBreakpoint(service, isolateRef);
await _unsubscribeDebugStream(service);
}
Future<void> stepInto(VmService service, IsolateRef isolateRef) async {
await _subscribeDebugStream(service);
await service.resume(isolateRef.id!, step: 'Into');
await hasStoppedAtBreakpoint(service, isolateRef);
await _unsubscribeDebugStream(service);
}
Future<void> stepOut(VmService service, IsolateRef isolateRef) async {
await _subscribeDebugStream(service);
await service.resume(isolateRef.id!, step: 'Out');
await hasStoppedAtBreakpoint(service, isolateRef);
await _unsubscribeDebugStream(service);
}
IsolateTest resumeProgramRecordingStops(
List<String> recordStops,
bool includeCaller,
) {
return (VmService service, IsolateRef isolateRef) async {
final completer = Completer<void>();
late StreamSubscription subscription;
subscription = service.onDebugEvent.listen((event) async {
if (event.kind == EventKind.kPauseBreakpoint) {
final stack = await service.getStack(isolateRef.id!);
expect(stack.frames!.length, greaterThanOrEqualTo(2));
String brokeAt =
await _locationToString(service, isolateRef, stack.frames![0]);
if (includeCaller) {
brokeAt =
'$brokeAt (${await _locationToString(service, isolateRef, stack.frames![1])})';
}
recordStops.add(brokeAt);
await service.resume(isolateRef.id!);
} else if (event.kind == EventKind.kPauseExit) {
await subscription.cancel();
await service.streamCancel(EventStreams.kDebug);
completer.complete();
}
});
await service.streamListen(EventStreams.kDebug);
await service.resume(isolateRef.id!);
return completer.future;
};
}
Future<String> _locationToString(
VmService service,
IsolateRef isolateRef,
Frame frame,
) async {
final location = frame.location!;
final Script script =
await service.getObject(isolateRef.id!, location.script!.id!) as Script;
final scriptName = basename(script.uri!);
final tokenPos = location.tokenPos!;
final line = script.getLineNumberFromTokenPos(tokenPos);
final column = script.getColumnNumberFromTokenPos(tokenPos);
return '$scriptName:$line:$column';
}
IsolateTest runStepThroughProgramRecordingStops(List<String> recordStops) {
return (VmService service, IsolateRef isolateRef) async {
final completer = Completer<void>();
late StreamSubscription subscription;
subscription = service.onDebugEvent.listen((event) async {
if (event.kind == EventKind.kPauseBreakpoint) {
final isolate = await service.getIsolate(isolateRef.id!);
final frame = isolate.pauseEvent!.topFrame!;
recordStops.add(await _locationToString(service, isolateRef, frame));
if (event.atAsyncSuspension ?? false) {
await service.resume(
isolateRef.id!,
step: StepOption.kOverAsyncSuspension,
);
} else {
await service.resume(isolateRef.id!, step: StepOption.kOver);
}
} else if (event.kind == EventKind.kPauseExit) {
await subscription.cancel();
await service.streamCancel(EventStreams.kDebug);
completer.complete();
}
});
await service.streamListen(EventStreams.kDebug);
await service.resume(isolateRef.id!);
return completer.future;
};
}
IsolateTest runStepIntoThroughProgramRecordingStops(List<String> recordStops) {
return (VmService service, IsolateRef isolateRef) async {
final completer = Completer<void>();
late StreamSubscription subscription;
subscription = service.onDebugEvent.listen((event) async {
if (event.kind == EventKind.kPauseBreakpoint) {
final isolate = await service.getIsolate(isolateRef.id!);
final frame = isolate.pauseEvent!.topFrame!;
recordStops.add(await _locationToString(service, isolateRef, frame));
await service.resume(isolateRef.id!, step: StepOption.kInto);
} else if (event.kind == EventKind.kPauseExit) {
await subscription.cancel();
await service.streamCancel(EventStreams.kDebug);
completer.complete();
}
});
await service.streamListen(EventStreams.kDebug);
await service.resume(isolateRef.id!);
return completer.future;
};
}
IsolateTest checkRecordedStops(
List<String> recordStops,
List<String> expectedStops, {
bool removeDuplicates = false,
bool debugPrint = false,
String? debugPrintFile,
int? debugPrintLine,
}) {
return (VmService service, IsolateRef isolate) async {
if (debugPrint) {
for (int i = 0; i < recordStops.length; i++) {
final String line = recordStops[i];
String output = line;
final int firstColon = line.indexOf(':');
final int lastColon = line.lastIndexOf(':');
if (debugPrintFile != null &&
debugPrintLine != null &&
firstColon > 0 &&
lastColon > 0) {
final int lineNumber =
int.parse(line.substring(firstColon + 1, lastColon));
final int relativeLineNumber = lineNumber - debugPrintLine;
final columnNumber = line.substring(lastColon + 1);
final file = line.substring(0, firstColon);
if (file == debugPrintFile) {
output = '\$file:\${LINE+$relativeLineNumber}:$columnNumber';
}
}
final String comma = i == recordStops.length - 1 ? '' : ',';
print("'$output'$comma");
}
}
if (removeDuplicates) {
recordStops = removeAdjacentDuplicates(recordStops);
expectedStops = removeAdjacentDuplicates(expectedStops);
}
// Single stepping may record extra stops.
// Allow the extra ones as long as the expected ones are recorded.
int i = 0;
int j = 0;
while (i < recordStops.length && j < expectedStops.length) {
if (recordStops[i] != expectedStops[j]) {
// Check if recordStops[i] is an extra stop.
int k = i + 1;
while (k < recordStops.length && recordStops[k] != expectedStops[j]) {
k++;
}
if (k < recordStops.length) {
// Allow and ignore extra recorded stops from i to k-1.
i = k;
} else {
// This will report an error.
expect(recordStops[i], expectedStops[j]);
}
}
i++;
j++;
}
expect(
recordStops.length >= expectedStops.length,
true,
reason: 'Expects at least ${expectedStops.length} breaks, '
'got ${recordStops.length}.',
);
};
}
List<String> removeAdjacentDuplicates(List<String> fromList) {
final List<String> result = <String>[];
String? latestLine;
for (String s in fromList) {
if (s == latestLine) continue;
latestLine = s;
result.add(s);
}
return result;
}
/// Waits for ServiceProtocolInfo.serverUri to be populated.
Future<ServiceProtocolInfo> waitForServiceInfo() async {
print('Waiting for the VM service URI to become available...');
var info = await Service.getInfo();
while (info.serverUri == null) {
await Future.delayed(const Duration(milliseconds: 100));
info = await Service.getInfo();
}
print('VM service URI has become available: ${info.serverUri}');
return info;
}
typedef ServiceExtensionHandler = Future<Map<String, dynamic>> Function(
Map<String, dynamic> cb,
);
/// Registers a service extension and returns the actual service name used to
/// invoke the service.
Future<String> registerServiceHelper(
VmService primaryClient,
VmService serviceRegisterClient,
String serviceName,
ServiceExtensionHandler callback,
) async {
final serviceNameCompleter = Completer<String>();
late final StreamSubscription sub;
sub = primaryClient.onServiceEvent.listen((event) {
if (event.kind == EventKind.kServiceRegistered &&
event.method!.endsWith(serviceName)) {
serviceNameCompleter.complete(event.method!);
sub.cancel();
}
});
// TODO(bkonyi): if we end up in a situation where this call throws due to a
// prior subscription to the Service stream, we should do something similar
// to _subscribeDebugStream in this method.
await primaryClient.streamListen(EventStreams.kService);
// Register the service.
serviceRegisterClient.registerServiceCallback(serviceName, callback);
await serviceRegisterClient.registerService(serviceName, serviceName);
// Wait for the service registered event on the non-registering client to get
// the actual service name.
final actualServiceName = await serviceNameCompleter.future;
print("Service '$serviceName' registered as '$actualServiceName'");
await primaryClient.streamCancel(EventStreams.kService);
return actualServiceName;
}
Future<void> evaluateInFrameAndExpect(
VmService service,
String isolateId,
String expression,
String expected, {
Map<String, String>? scope,
String? kind,
int topFrame = 0,
}) async {
final result = await service.evaluateInFrame(
isolateId,
topFrame,
expression,
scope: scope,
) as InstanceRef;
expect(result.valueAsString, expected);
if (kind != null) {
expect(result.kind!, kind);
}
}
Future<void> evaluateAndExpect(
VmService service,
String isolateId,
String targetId,
String expression,
String expected, {
Map<String, String>? scope,
String? kind,
}) async {
final result = await service.evaluate(
isolateId,
targetId,
expression,
scope: scope,
) as InstanceRef;
expect(result.valueAsString, expected);
if (kind != null) {
expect(result.kind!, kind);
}
}
Future<HeapSnapshotGraph> fetchHeapSnapshot(
VmService service,
IsolateRef isolateRef,
) async {
final isolateId = isolateRef.id!;
final completer = Completer<void>();
late final StreamSubscription sub;
final data = <ByteData>[];
sub = service.onHeapSnapshotEvent.listen((event) async {
data.add(event.data!);
if (event.last == true) {
await sub.cancel();
await service.streamCancel(EventStreams.kHeapSnapshot);
completer.complete();
}
});
await service.streamListen(EventStreams.kHeapSnapshot);
await service.requestHeapSnapshot(isolateId);
await completer.future;
return HeapSnapshotGraph.fromChunks(data);
}
IsolateTest reloadSources({bool pause = false}) {
return (VmService service, IsolateRef isolateRef) async {
await service.reloadSources(isolateRef.id!, pause: pause);
};
}
IsolateTest hasLocalVarInTopStackFrame(String varName) {
return (VmService service, IsolateRef isolateRef) async {
print("Checking we have variable '$varName' in the top frame");
final isolateId = isolateRef.id!;
// Make sure that the isolate has stopped.
final isolate = await service.getIsolate(isolateId);
expect(isolate.pauseEvent, isNotNull);
expect(isolate.pauseEvent!.kind, isNot(EventKind.kResume));
final stack = await service.getStack(isolateId);
final frames = stack.frames!;
expect(frames.length, greaterThanOrEqualTo(1));
final top = frames[0];
final vars = top.vars!;
for (final variable in vars) {
if (variable.name == varName) {
return;
}
}
final sb = StringBuffer();
sb.write('Expected to find $varName in top awaiter stack frame, found ');
if (vars.isEmpty) {
sb.writeln('no variables');
} else {
sb.writeln('these instead:');
for (var variable in vars) {
sb.writeln('\t${variable.name}');
}
}
throw sb.toString();
};
}
IsolateTest stoppedInFunction(String functionName) {
return (VmService service, IsolateRef isolateRef) async {
print('Checking we are in function: $functionName');
final isolateId = isolateRef.id!;
final stack = await service.getStack(isolateId);
final frames = stack.frames!;
expect(frames, isNotEmpty);
final topFrame = frames[0];
final function = await service.getObject(
isolateId,
topFrame.function!.id!,
) as Func;
final name = function.name!;
if (name != functionName) {
final sb = StringBuffer();
sb.writeln(
'Expected to be in function $functionName but '
'actually in function $name',
);
sb.writeln('Full stack trace:');
for (final frame in frames) {
final func = await service.getObject(
isolateId,
frame.function!.id!,
) as Func;
final ownerName = func.owner.name!;
sb.write(' $frame [${func.name}] [$ownerName]\n');
}
throw sb.toString();
} else {
print('Program is stopped in function: $functionName');
}
};
}
+520 -10
View File
@@ -1,25 +1,48 @@
// Copyright (c) 2020, 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.
// NOTE: this file was originally copied from package:vm_service.
// ignore_for_file: constant_identifier_names
library test_helper;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:io' as io;
import 'package:test/test.dart';
import 'package:vm_service/vm_service.dart';
import 'package:vm_service/vm_service_io.dart';
import 'service_test_common.dart';
export 'service_test_common.dart' show IsolateTest, VMTest;
/// The extra arguments to use
const List<String> extraDebuggingArgs = [];
/// Will be set to the http address of the VM's service protocol before
/// any tests are invoked.
late String serviceHttpAddress;
late String serviceWebsocketAddress;
const String _TESTEE_ENV_KEY = 'SERVICE_TEST_TESTEE';
const Map<String, String> _TESTEE_SPAWN_ENV = {_TESTEE_ENV_KEY: 'true'};
late Uri remoteVmServiceUri;
Future<Process> spawnDartProcess(
Future<io.Process> spawnDartProcess(
String script, {
bool serveObservatory = true,
bool pauseOnStart = true,
bool disableServiceAuthCodes = false,
}) async {
final executable = Platform.executable;
final tmpDir = await Directory.systemTemp.createTemp('dart_service');
final executable = io.Platform.executable;
final tmpDir = await io.Directory.systemTemp.createTemp('dart_service');
final serviceInfoUri = tmpDir.uri.resolve('service_info.json');
final serviceInfoFile = await File.fromUri(serviceInfoUri).create();
final serviceInfoFile = await io.File.fromUri(serviceInfoUri).create();
final arguments = [
'--disable-dart-dev',
@@ -28,10 +51,10 @@ Future<Process> spawnDartProcess(
if (pauseOnStart) '--pause-isolates-on-start',
if (disableServiceAuthCodes) '--disable-service-auth-codes',
'--write-service-info=$serviceInfoUri',
...Platform.executableArguments,
Platform.script.resolve(script).toString(),
...io.Platform.executableArguments,
io.Platform.script.resolve(script).toString(),
];
final process = await Process.start(executable, arguments);
final process = await io.Process.start(executable, arguments);
process.stdout
.transform(utf8.decoder)
.listen((line) => print('TESTEE OUT: $line'));
@@ -70,5 +93,492 @@ Future<void> executeUntilNextPause(VmService service) async {
/// will resolve to the sdk/ directory (e.g. '../../../').
Uri devtoolsAppUri({required String prefix}) {
const pathFromSdkDirectory = 'third_party/devtools/web';
return Platform.script.resolve('$prefix$pathFromSdkDirectory');
return io.Platform.script.resolve('$prefix$pathFromSdkDirectory');
}
bool _isTestee() {
return io.Platform.environment.containsKey(_TESTEE_ENV_KEY);
}
Uri _getTestUri(String script) {
if (io.Platform.script.isScheme('data')) {
// If running from pub we can assume that we're in the root of the package
// directory.
return Uri.parse('test/$script');
} else if (io.Platform.script.toFilePath().endsWith('out.aotsnapshot')) {
// We're running an AOT test. In this case, we need to use the exact URI we
// launched with.
return io.Platform.script;
} else {
// Resolve the script to ensure that test will fail if the provided script
// name doesn't match the actual script.
return io.Platform.script.resolve(script);
}
}
class _ServiceTesteeRunner {
Future<void> run({
Function()? testeeBefore,
Function()? testeeConcurrent,
bool pauseOnStart = false,
bool pauseOnExit = false,
}) async {
if (!pauseOnStart) {
if (testeeBefore != null) {
final result = testeeBefore();
if (result is Future) {
await result;
}
}
print(''); // Print blank line to signal that testeeBefore has run.
}
if (testeeConcurrent != null) {
final result = testeeConcurrent();
if (result is Future) {
await result;
}
}
if (!pauseOnExit) {
// Wait around for the process to be killed.
await io.stdin.first.then((_) => io.exit(0));
}
}
void runSync({
void Function()? testeeBeforeSync,
void Function()? testeeConcurrentSync,
bool pauseOnStart = false,
bool pauseOnExit = false,
}) {
if (!pauseOnStart) {
if (testeeBeforeSync != null) {
testeeBeforeSync();
}
print(''); // Print blank line to signal that testeeBefore has run.
}
if (testeeConcurrentSync != null) {
testeeConcurrentSync();
}
if (!pauseOnExit) {
// Wait around for the process to be killed.
io.stdin.first.then((_) => io.exit(0));
}
}
}
class _ServiceTesteeLauncher {
io.Process? process;
List<String> args;
bool killedByTester = false;
final _exitCodeCompleter = Completer<int>();
_ServiceTesteeLauncher(String script)
: args = [_getTestUri(script).toFilePath()];
Future<int> get exitCode => _exitCodeCompleter.future;
// Spawn the testee process.
Future<io.Process> _spawnProcess(
bool pauseOnStart,
bool pauseOnExit,
bool pauseOnUnhandledExceptions,
bool testeeControlsServer,
bool useAuthToken,
List<String>? experiments,
List<String>? extraArgs,
) {
return _spawnDartProcess(
pauseOnStart,
pauseOnExit,
pauseOnUnhandledExceptions,
testeeControlsServer,
useAuthToken,
experiments,
extraArgs,
);
}
Future<io.Process> _spawnDartProcess(
bool pauseOnStart,
bool pauseOnExit,
bool pauseOnUnhandledExceptions,
bool testeeControlsServer,
bool useAuthToken,
List<String>? experiments,
List<String>? extraArgs,
) {
final String dartExecutable = io.Platform.executable;
final fullArgs = <String>[];
if (pauseOnStart) {
fullArgs.add('--pause-isolates-on-start');
}
if (pauseOnExit) {
fullArgs.add('--pause-isolates-on-exit');
}
if (!useAuthToken) {
fullArgs.add('--disable-service-auth-codes');
}
if (pauseOnUnhandledExceptions) {
fullArgs.add('--pause-isolates-on-unhandled-exceptions');
}
fullArgs.add('--profiler');
if (experiments != null) {
fullArgs.addAll(experiments.map((e) => '--enable-experiment=$e'));
}
if (extraArgs != null) {
fullArgs.addAll(extraArgs);
}
fullArgs.addAll(io.Platform.executableArguments);
if (!testeeControlsServer) {
fullArgs.add('--enable-vm-service:0');
}
fullArgs.addAll(args);
return _spawnCommon(dartExecutable, fullArgs, <String, String>{});
}
Future<io.Process> _spawnCommon(
String executable,
List<String> arguments,
Map<String, String> dartEnvironment,
) {
final environment = _TESTEE_SPAWN_ENV;
final bashEnvironment = StringBuffer();
environment.forEach((k, v) => bashEnvironment.write('$k=$v '));
dartEnvironment.forEach((k, v) {
arguments.insert(0, '-D$k=$v');
});
print('** Launching $bashEnvironment$executable ${arguments.join(' ')}');
return io.Process.start(
executable,
arguments,
environment: environment,
);
}
Future<Uri> launch(
bool pauseOnStart,
bool pauseOnExit,
bool pauseOnUnhandledExceptions,
bool testeeControlsServer,
bool useAuthToken,
List<String>? experiments,
List<String>? extraArgs,
) {
return _spawnProcess(
pauseOnStart,
pauseOnExit,
pauseOnUnhandledExceptions,
testeeControlsServer,
useAuthToken,
experiments,
extraArgs,
).then((p) {
final Completer<Uri> completer = Completer<Uri>();
process = p;
Uri? uri;
bool blank = false;
var first = true;
process!.stdout
.transform(utf8.decoder)
.transform(LineSplitter())
.listen((line) {
const kDartVMServiceListening = 'The Dart VM service is listening on ';
if (line.startsWith(kDartVMServiceListening)) {
uri = Uri.parse(line.substring(kDartVMServiceListening.length));
}
if (pauseOnStart || line == '') {
// Received blank line.
blank = true;
}
if ((uri != null) && (blank == true) && (first == true)) {
completer.complete(uri!);
// Stop repeat completions.
first = false;
print('** Signaled to run test queries on $uri');
}
io.stdout.write('>testee>out> $line\n');
});
process!.stderr
.transform(utf8.decoder)
.transform(LineSplitter())
.listen((line) {
io.stdout.write('>testee>err> $line\n');
});
process!.exitCode.then(_exitCodeCompleter.complete);
return completer.future;
});
}
void requestExit() {
if (process != null) {
print('** Killing script');
if (process!.kill()) {
killedByTester = true;
}
}
}
}
void setupAddresses(Uri /*!*/ serverAddress) {
serviceWebsocketAddress =
'ws://${serverAddress.authority}${serverAddress.path}ws';
serviceHttpAddress = 'http://${serverAddress.authority}${serverAddress.path}';
}
class _ServiceTesterRunner {
Future<void> run({
List<String>? mainArgs,
List<String>? extraArgs,
List<String>? experiments,
List<VMTest>? vmTests,
List<IsolateTest>? isolateTests,
required String scriptName,
bool pauseOnStart = false,
bool pauseOnExit = false,
bool verboseVm = false,
bool pauseOnUnhandledExceptions = false,
bool testeeControlsServer = false,
bool useAuthToken = false,
bool allowForNonZeroExitCode = false,
VmServiceFactory serviceFactory = VmService.defaultFactory,
}) async {
final process = _ServiceTesteeLauncher(scriptName);
late VmService vm;
late IsolateRef isolate;
setUp(() async {
await process
.launch(
pauseOnStart,
pauseOnExit,
pauseOnUnhandledExceptions,
testeeControlsServer,
useAuthToken,
experiments,
extraArgs,
)
.then((Uri serverAddress) async {
if (mainArgs!.contains('--gdb')) {
final pid = process.process!.pid;
final wait = Duration(seconds: 10);
print('Testee has pid $pid, waiting $wait before continuing');
io.sleep(wait);
}
setupAddresses(serverAddress);
vm = await vmServiceConnectUriWithFactory(
serviceWebsocketAddress,
vmServiceFactory: serviceFactory,
);
print('Done loading VM');
isolate = await getFirstIsolate(vm);
});
});
final name = _getTestUri(scriptName).pathSegments.last;
test(
name,
() async {
// Run vm tests.
if (vmTests != null) {
var testIndex = 1;
final totalTests = vmTests.length;
for (var t in vmTests) {
print('$name [$testIndex/$totalTests]');
await t(vm);
testIndex++;
}
}
// Run isolate tests.
if (isolateTests != null) {
var testIndex = 1;
final totalTests = isolateTests.length;
for (var t in isolateTests) {
print('$name [$testIndex/$totalTests]');
await t(vm, isolate);
testIndex++;
}
}
},
retry: 0,
timeout: Timeout.none,
);
tearDown(() {
print('All service tests completed successfully.');
process.requestExit();
});
final exitCode = await process.exitCode;
if (exitCode != 0) {
if (!(process.killedByTester || allowForNonZeroExitCode)) {
throw 'Testee exited with unexpected exitCode: $exitCode';
}
}
print('** Process exited: $exitCode');
}
Future<IsolateRef> getFirstIsolate(VmService service) async {
var vm = await service.getVM();
final vmIsolates = vm.isolates!;
if (vmIsolates.isNotEmpty) {
return vmIsolates.first;
}
Completer<dynamic>? completer = Completer();
late StreamSubscription subscription;
subscription = service.onIsolateEvent.listen((Event event) async {
if (completer == null) {
await subscription.cancel();
return;
}
if (event.kind == EventKind.kIsolateRunnable) {
vm = await service.getVM();
await subscription.cancel();
await service.streamCancel(EventStreams.kIsolate);
completer!.complete(event.isolate!);
completer = null;
}
});
await service.streamListen(EventStreams.kIsolate);
// The isolate may have started before we subscribed.
vm = await service.getVM();
if (vmIsolates.isNotEmpty) {
await subscription.cancel();
completer!.complete(vmIsolates.first);
completer = null;
}
return (await completer!.future) as IsolateRef;
}
}
/// Runs [tests] in sequence, each of which should take an [Isolate] and
/// return a [Future]. Code for setting up state can run before and/or
/// concurrently with the tests. Uses [mainArgs] to determine whether
/// to run tests or testee in this invocation of the script.
Future<void> runIsolateTests(
List<String> mainArgs,
List<IsolateTest> tests,
String scriptName, {
Function()? testeeBefore,
Function()? testeeConcurrent,
bool pauseOnStart = false,
bool pauseOnExit = false,
bool verboseVm = false,
bool pauseOnUnhandledExceptions = false,
bool testeeControlsServer = false,
bool useAuthToken = false,
bool allowForNonZeroExitCode = false,
List<String>? experiments,
List<String>? extraArgs,
}) async {
assert(!pauseOnStart || testeeBefore == null);
if (_isTestee()) {
await _ServiceTesteeRunner().run(
testeeBefore: testeeBefore,
testeeConcurrent: testeeConcurrent,
pauseOnStart: pauseOnStart,
pauseOnExit: pauseOnExit,
);
} else {
await _ServiceTesterRunner().run(
mainArgs: mainArgs,
scriptName: scriptName,
extraArgs: extraArgs,
isolateTests: tests,
pauseOnStart: pauseOnStart,
pauseOnExit: pauseOnExit,
verboseVm: verboseVm,
experiments: experiments,
pauseOnUnhandledExceptions: pauseOnUnhandledExceptions,
testeeControlsServer: testeeControlsServer,
useAuthToken: useAuthToken,
allowForNonZeroExitCode: allowForNonZeroExitCode,
);
}
}
/// Runs [tests] in sequence, each of which should take an [Isolate] and
/// return a [Future]. Code for setting up state can run before and/or
/// concurrently with the tests. Uses [mainArgs] to determine whether
/// to run tests or testee in this invocation of the script.
///
/// This is a special version of this test harness specifically for the
/// pause_on_unhandled_exceptions_test, which cannot properly function
/// in an async context (because exceptions are *always* handled in async
/// functions).
void runIsolateTestsSynchronous(
List<String> mainArgs,
List<IsolateTest> tests,
String scriptName, {
void Function()? testeeBefore,
void Function()? testeeConcurrent,
bool pauseOnStart = false,
bool pauseOnExit = false,
bool verboseVm = false,
bool pauseOnUnhandledExceptions = false,
List<String>? extraArgs,
}) {
assert(!pauseOnStart || testeeBefore == null);
if (_isTestee()) {
_ServiceTesteeRunner().runSync(
testeeBeforeSync: testeeBefore,
testeeConcurrentSync: testeeConcurrent,
pauseOnStart: pauseOnStart,
pauseOnExit: pauseOnExit,
);
} else {
_ServiceTesterRunner().run(
mainArgs: mainArgs,
scriptName: scriptName,
extraArgs: extraArgs,
isolateTests: tests,
pauseOnStart: pauseOnStart,
pauseOnExit: pauseOnExit,
verboseVm: verboseVm,
pauseOnUnhandledExceptions: pauseOnUnhandledExceptions,
);
}
}
/// Runs [tests] in sequence, each of which should take an [Isolate] and
/// return a [Future]. Code for setting up state can run before and/or
/// concurrently with the tests. Uses [mainArgs] to determine whether
/// to run tests or testee in this invocation of the script.
Future<void> runVMTests(
List<String> mainArgs,
List<VMTest> tests,
String scriptName, {
Function()? testeeBefore,
Function()? testeeConcurrent,
bool pauseOnStart = false,
bool pauseOnExit = false,
bool verboseVm = false,
bool pauseOnUnhandledExceptions = false,
List<String>? extraArgs,
VmServiceFactory serviceFactory = VmService.defaultFactory,
}) async {
if (_isTestee()) {
await _ServiceTesteeRunner().run(
testeeBefore: testeeBefore,
testeeConcurrent: testeeConcurrent,
pauseOnStart: pauseOnStart,
pauseOnExit: pauseOnExit,
);
} else {
await _ServiceTesterRunner().run(
mainArgs: mainArgs,
scriptName: scriptName,
extraArgs: extraArgs,
vmTests: tests,
pauseOnStart: pauseOnStart,
pauseOnExit: pauseOnExit,
verboseVm: verboseVm,
pauseOnUnhandledExceptions: pauseOnUnhandledExceptions,
serviceFactory: serviceFactory,
);
}
}
@@ -0,0 +1,25 @@
// 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.
import 'package:test/test.dart';
import 'package:vm_service/vm_service.dart';
import 'common/test_helper.dart';
final tests = <VMTest>[
// Ensure the DDS allows for listening to a custom stream.
(VmService service) async {
try {
await service.streamListen('Foo');
} catch (e) {
fail('Unable to subscribe to a custom stream: $e');
}
}
];
void main([args = const <String>[]]) => runVMTests(
args,
tests,
'custom_stream_listen_test.dart',
);
@@ -1,42 +1,39 @@
// Copyright (c) 2021, 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.
import 'dart:async';
import 'package:dds/dds.dart';
import 'package:observatory/service_io.dart';
import 'package:test/test.dart';
import 'package:vm_service/vm_service.dart';
import 'test_helper.dart';
import 'common/test_helper.dart';
final tests = <VMTest>[
(VM vm) async {
(VmService service) async {
late DartDevelopmentService dds;
final waitForDDS = Completer<void>();
final serviceMessageCompleter = Completer<void>();
// The original VM service client is connected.
expect(vm.isConnected, true);
// A service event is sent to all existing clients when DDS connects before
// their connection is closed.
await vm.listenEventStream('Service', (ServiceEvent event) async {
service.onServiceEvent.listen((event) async {
// Wait for dds to be set before checking the server's URI.
await waitForDDS.future;
final message =
'A Dart Developer Service instance has connected and this direct '
'connection to the VM service will now be closed. Please reconnect to '
'the Dart Development Service at ${dds.uri}.';
expect(event.kind, ServiceEvent.kDartDevelopmentServiceConnected);
expect(event.message, message);
expect(event.uri, dds.uri);
expect(event.kind, 'DartDevelopmentServiceConnected');
expect(event.json!['message'], message);
expect(event.json!['uri'], dds.uri.toString());
serviceMessageCompleter.complete();
});
// Start DDS, which should result in the original VM service client being
// disconnected from the VM service.
final remote = Uri.parse(vm.target.networkAddress);
final remote = Uri.parse(service.wsUri!);
dds = await DartDevelopmentService.startDartDevelopmentService(
remote.replace(
scheme: 'http',
@@ -49,13 +46,14 @@ final tests = <VMTest>[
waitForDDS.complete();
expect(dds.isRunning, true);
await serviceMessageCompleter.future;
await vm.onDisconnect;
await service.onDone;
await dds.shutdown();
}
];
main(args) async => runVMTests(
void main([args = const <String>[]]) => runVMTests(
args,
tests,
enableDds: false,
'dds_disconnects_existing_clients_test.dart',
extraArgs: ['--no-dds'],
);
@@ -0,0 +1,53 @@
// 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.
import 'dart:async';
import 'dart:developer';
import 'package:test/test.dart';
import 'package:vm_service/vm_service.dart';
import 'common/service_test_common.dart';
import 'common/test_helper.dart';
Future testMain() async {
// Post a total of 9 events
for (int i = 1; i <= 9; ++i) {
postEvent('Test', {
'id': i,
});
}
}
final tests = <IsolateTest>[
hasPausedAtStart,
resumeIsolate,
(VmService service, IsolateRef isolateRef) async {
final completer = Completer<void>();
int i = 1;
service.onExtensionEvent.listen((event) async {
expect(event.extensionKind, 'Test');
expect(event.extensionData!.data['id'], i);
i++;
if (i == 10) {
await service.streamCancel(EventStreams.kExtension);
completer.complete();
} else if (i > 10) {
fail('Too many "Test" extension events');
}
});
await service.streamListen(EventStreams.kExtension);
await completer.future;
},
];
void main([args = const <String>[]]) => runIsolateTests(
args,
tests,
'extension_event_history_test.dart',
testeeConcurrent: testMain,
pauseOnStart: true,
pauseOnExit: true,
);
@@ -0,0 +1,38 @@
// 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.
import 'package:dds_service_extensions/dds_service_extensions.dart';
import 'package:test/test.dart';
import 'package:vm_service/vm_service.dart';
import 'common/test_helper.dart';
void fooBar() {}
final test = <IsolateTest>[
(VmService service, IsolateRef isolate) async {
// Each client has a default name based on the order of connection to the
// service.
var clientName = await service.getClientName();
expect(clientName.name, 'client1');
// Set a custom client name and check it was set properly.
await service.setClientName('foobar');
clientName = await service.getClientName();
expect(clientName.name, 'foobar');
// Clear the client name and check that we're using the default again.
await service.setClientName();
clientName = await service.getClientName();
expect(clientName.name, 'client1');
},
];
void main([args = const <String>[]]) => runIsolateTests(
args,
test,
'get_client_name_rpc_test.dart',
testeeConcurrent: fooBar,
pauseOnStart: true,
);
@@ -0,0 +1,78 @@
// 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.
import 'dart:async';
import 'dart:developer';
import 'package:dds_service_extensions/dds_service_extensions.dart';
import 'package:test/test.dart';
import 'package:vm_service/vm_service.dart';
import 'common/service_test_common.dart';
import 'common/test_helper.dart';
const kMaxLogHistorySize = 100000;
const kExpectedMaxLogIndex = kMaxLogHistorySize + 10;
void testMain() {
// Log a total of 100,010 messages
for (int i = 1; i <= kExpectedMaxLogIndex; i++) {
log('All work and no play makes Ben a dull boy ($i)');
}
debugger();
}
final tests = <IsolateTest>[
hasPausedAtStart,
(VmService service, IsolateRef isolateRef) async {
final isolateId = isolateRef.id!;
final initialSize = (await service.getLogHistorySize(isolateId)).size;
try {
await service.setLogHistorySize(isolateId, kMaxLogHistorySize + 1);
} on RPCError catch (e) {
expect(e.message, "'size' must be less than $kMaxLogHistorySize");
}
expect((await service.getLogHistorySize(isolateId)).size, initialSize);
},
(VmService service, IsolateRef isolateRef) async {
await service.setLogHistorySize(isolateRef.id!, kMaxLogHistorySize);
expect(
(await service.getLogHistorySize(isolateRef.id!)).size,
kMaxLogHistorySize,
);
},
resumeIsolate,
// Wait for the process to finish logging
hasStoppedAtBreakpoint,
(VmService service, IsolateRef isolateRef) async {
final completer = Completer<void>();
// We've logged kMaxLogHistorySize + 10 messages, but we only expect to
// receive kMaxLogHistorySize logs..
int i = 11;
service.onLoggingEvent.listen((event) async {
expect(
event.logRecord!.message!.valueAsString,
'All work and no play makes Ben a dull boy ($i)',
);
if (i == kExpectedMaxLogIndex) {
await service.streamCancel(EventStreams.kLogging);
completer.complete();
}
i++;
});
// Subscribing to the Logging stream will cause all the log events to be
// sent immediately.
await service.streamListen(EventStreams.kLogging);
await completer.future;
}
];
void main([args = const <String>[]]) => runIsolateTests(
args,
tests,
'log_history_size_gigantic_test.dart',
testeeConcurrent: testMain,
pauseOnStart: true,
pauseOnExit: true,
);
@@ -0,0 +1,91 @@
// 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.
import 'dart:async';
import 'dart:developer';
import 'package:dds_service_extensions/dds_service_extensions.dart';
import 'package:test/test.dart';
import 'package:vm_service/vm_service.dart';
import 'package:vm_service/vm_service_io.dart';
import 'common/service_test_common.dart';
import 'common/test_helper.dart';
void testMain() {
// Log a total of 9 messages
for (int i = 1; i <= 9; ++i) {
log('log$i');
}
debugger();
log('log10');
}
final tests = <IsolateTest>[
hasPausedAtStart,
resumeIsolate,
(VmService service, IsolateRef isolateRef) async {
final isolateId = isolateRef.id!;
// Check that resizing does the right thing.
await service.setLogHistorySize(isolateId, 10);
expect((await service.getLogHistorySize(isolateId)).size, 10);
final completer = Completer<void>();
int i = 1;
service.onLoggingEvent.listen((event) async {
expect(event.logRecord!.message!.valueAsString, 'log$i');
i++;
if (i == 10) {
await service.streamCancel(EventStreams.kLogging);
completer.complete();
} else if (i > 10) {
fail('Too many log messages');
}
});
await service.streamListen(EventStreams.kLogging);
await completer.future;
},
(VmService service, IsolateRef isolateRef) async {
// Resize to be smaller
final isolateId = isolateRef.id!;
// Check that resizing does the right thing.
await service.setLogHistorySize(isolateId, 5);
expect((await service.getLogHistorySize(isolateId)).size, 5);
},
resumeIsolate,
(VmService service, IsolateRef isolateRef) async {
final completer = Completer<void>();
// Create a new client as we want to get log messages from the entire
// history buffer.
final client = await vmServiceConnectUri(service.wsUri!);
int i = 6;
client.onLoggingEvent.listen((event) async {
expect(event.logRecord!.message!.valueAsString, 'log$i');
i++;
if (i == 11) {
await client.streamCancel(EventStreams.kLogging);
completer.complete();
} else if (i > 11) {
fail('Too many log messages');
}
});
await client.streamListen(EventStreams.kLogging);
await completer.future;
client.dispose();
},
];
void main([args = const <String>[]]) => runIsolateTests(
args,
tests,
'log_history_size_simple_test.dart',
testeeConcurrent: testMain,
pauseOnStart: true,
pauseOnExit: true,
);
+83
View File
@@ -0,0 +1,83 @@
// 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.
import 'dart:async';
import 'dart:developer';
import 'package:dds_service_extensions/dds_service_extensions.dart';
import 'package:test/test.dart';
import 'package:vm_service/vm_service.dart';
import 'common/service_test_common.dart';
import 'common/test_helper.dart';
void testMain() {
// Initial logging history should be 0, so these messages won't be buffered.
log('log1');
log('log2');
// Setting the log history length does not apply retroactively.
debugger();
// Log a total of 30 messages
for (int i = 3; i <= 30; ++i) {
log('log$i');
}
}
late final String isolateId;
final tests = <IsolateTest>[
hasPausedAtStart,
(VmService service, IsolateRef isolateRef) async {
isolateId = isolateRef.id!;
await service.setLogHistorySize(isolateId, 0);
expect((await service.getLogHistorySize(isolateId)).size, 0);
},
resumeIsolate,
hasStoppedAtBreakpoint,
(VmService service, IsolateRef isolateRef) async {
await service.setLogHistorySize(isolateId, 20);
expect((await service.getLogHistorySize(isolateId)).size, 20);
},
resumeIsolate,
(VmService service, IsolateRef isolateRef) async {
final completer = Completer<void>();
await Future.delayed(const Duration(seconds: 1));
// With the log history set to 20, the first log message should be 'log11'.
int i = 11;
service.onLoggingEvent.listen((event) async {
expect(event.logRecord!.message!.valueAsString, 'log$i');
i++;
if (i == 30) {
await service.streamCancel(EventStreams.kLogging);
completer.complete();
}
});
await service.streamListen(EventStreams.kLogging);
await completer.future;
},
(VmService service, IsolateRef isolateRef) async {
try {
// Try to set an invalid history size
await service.setLogHistorySize(isolateId, -1);
fail('Successfully set invalid size');
} on RPCError catch (e) {
expect(e.message, "'size' must be greater or equal to zero");
}
expect((await service.getLogHistorySize(isolateId)).size, 20);
}
];
void main([args = const <String>[]]) => runIsolateTests(
args,
tests,
'log_history_size_test.dart',
testeeConcurrent: testMain,
pauseOnStart: true,
pauseOnExit: true,
);
@@ -0,0 +1,76 @@
// 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.
import 'dart:async';
import 'dart:io';
import 'package:test/test.dart';
import 'package:vm_service/vm_service.dart';
import 'common/service_test_common.dart';
import 'common/test_helper.dart';
void testMain() {
// Log a total of 9 messages
for (int i = 1; i <= 9; ++i) {
print('Stdout log$i');
stderr.writeln('Stderr log$i');
}
}
Future<void> streamHistoryTest(
VmService service,
IsolateRef isolateRef,
String stream,
) async {
final completer = Completer<void>();
int i = 1;
service.onEvent(stream).listen((event) async {
final string = decodeBase64(event.bytes!);
if (stream == EventStreams.kStdout) {
if (!string.startsWith(stream)) {
// Likely "The Dart VM service is listening..." or one of the other
// messages printed when the VM service is enabled.
return;
}
expect(string, '$stream log$i\n');
} else {
// Newlines are sent as separate events for some reason. Ignore them.
if (!string.startsWith(stream)) {
return;
}
expect(string, '$stream log$i');
}
i++;
if (i == 10) {
await service.streamCancel(stream);
completer.complete();
} else if (i > 10) {
fail('Too many log messages');
}
});
await service.streamListen(stream);
await completer.future;
}
final tests = <IsolateTest>[
hasPausedAtStart,
resumeIsolate,
(VmService service, IsolateRef isolateRef) async {
await streamHistoryTest(service, isolateRef, EventStreams.kStdout);
},
(VmService service, IsolateRef isolateRef) async {
await streamHistoryTest(service, isolateRef, EventStreams.kStderr);
},
];
void main([args = const <String>[]]) => runIsolateTests(
args,
tests,
'stdout_stderr_history_test.dart',
testeeConcurrent: testMain,
pauseOnStart: true,
pauseOnExit: true,
);
+10
View File
@@ -1,3 +1,13 @@
# 1.7.0
- Added:
- `ClientName`
- `DdsExtension.getClientName`
- `DdsExtension.getLogHistorySize`
- `DdsExtension.setClientName`
- `DdsExtension.setLogHistorySize`
- `DdsExtension.requirePermissionToResume`
- `Size`
# 1.6.3
- Updated `vm_service` version to `^14.0.0`.
@@ -95,6 +95,32 @@ extension DdsExtension on VmService {
);
}
/// The [getLogHistorySize] RPC is used to retrieve the current size of the
/// log history buffer.
///
/// If the returned [Size] is zero, then log history is disabled.
Future<Size> getLogHistorySize(String isolateId) async {
// No version check needed, present since v1.0 of the protocol.
return _callHelper<Size>('getLogHistorySize', args: {
'isolateId': isolateId,
});
}
/// The [setLogHistorySize] RPC is used to set the size of the ring buffer
/// used for caching a limited set of historical log messages.
///
/// If [size] is 0, logging history will be disabled.
///
/// The maximum history size is 100,000 messages, with the default set to
/// 10,000 messages.
Future<Success> setLogHistorySize(String isolateId, int size) async {
// No version check needed, present since v1.0 of the protocol.
return _callHelper<Success>('setLogHistorySize', args: {
'isolateId': isolateId,
'size': size,
});
}
/// Retrieve the event history for `stream`.
///
/// If `stream` does not have event history collected, a parameter error is
@@ -176,6 +202,83 @@ extension DdsExtension on VmService {
Stream<Event> get onExtensionEventWithHistory =>
onEventWithHistory('Extension');
/// The [getClientName] RPC is used to retrieve the name associated with the
/// currently connected VM service client.
///
/// If no name was previously set through the [setClientName] RPC, a default
/// name will be returned.
Future<ClientName> getClientName() async {
// No version check needed, present since v1.0 of the protocol.
return _callHelper<ClientName>(
'getClientName',
);
}
/// The [setClientName] RPC is used to set a name to be associated with the
/// currently connected VM service client.
///
/// If the [name] parameter is a non-empty string, [name] will become the new
/// name associated with the client. If [name] is an empty string, the
/// client's name will be reset to its default name.
Future<Success> setClientName([String name = '']) async {
// No version check needed, present since v1.0 of the protocol.
return _callHelper<Success>(
'setClientName',
args: {
'name': name,
},
);
}
/// The [requirePermissionToResume] RPC is used to change the pause/resume
/// behavior of isolates.
///
/// This provides a way for the VM service to wait for approval to resume
/// from some set of clients. This is useful for clients which want to
/// perform some operation on an isolate after a pause without it being
/// resumed by another client.
///
/// If the [onPauseStart] parameter is `true`, isolates will not resume after
/// pausing on start until the client sends a `resume` request and all other
/// clients which need to provide resume approval for this pause type have
/// done so.
///
/// If the [onPauseReload] parameter is `true`, isolates will not resume
/// after pausing after a reload until the client sends a `resume` request
/// and all other clients which need to provide resume approval for this
/// pause type have done so.
///
/// If the [onPauseExit] parameter is `true`, isolates will not resume after
/// pausing on exit until the client sends a `resume` request and all other
/// clients which need to provide resume approval for this pause type have
/// done so.
///
/// **Important Notes:**
///
/// - All clients with the same client name share resume permissions. Only a
/// single client of a given name is required to provide resume approval.
/// - When a client requiring approval disconnects from the service, a paused
/// isolate may resume if all other clients requiring resume approval have
/// already given approval. In the case that no other client requires
/// resume approval for the current pause event, the isolate will be
/// resumed if at least one other client has attempted to resume the
/// isolate.
Future<Success> requirePermissionToResume({
bool onPauseStart = false,
bool onPauseReload = false,
bool onPauseExit = false,
}) async {
// No version check needed, present since v1.0 of the protocol.
return _callHelper<Success>(
'requirePermissionToResume',
args: {
'onPauseStart': onPauseStart,
'onPauseReload': onPauseReload,
'onPauseExit': onPauseExit,
},
);
}
Future<bool> _versionCheck(int major, int minor) async {
_ddsVersion ??= await getDartDevelopmentServiceVersion();
return ((_ddsVersion!.major == major && _ddsVersion!.minor! >= minor) ||
@@ -203,10 +306,38 @@ extension DdsExtension on VmService {
AvailableCachedCpuSamples.parse,
);
addTypeFactory('CachedCpuSamples', CachedCpuSamples.parse);
addTypeFactory('Size', Size.parse);
addTypeFactory('ClientName', ClientName.parse);
_factoriesRegistered = true;
}
}
/// A simple object representing the name of a DDS client.
///
/// See [DdsExtension.getClientName] and [DdsExtension.setClientName].
class ClientName extends Response {
static ClientName? parse(Map<String, dynamic>? json) =>
json == null ? null : ClientName._fromJson(json);
ClientName({required this.name});
ClientName._fromJson(Map<String, dynamic> json) : name = json['name'];
final String name;
}
/// A simple object representing a size response.
class Size extends Response {
static Size? parse(Map<String, dynamic>? json) =>
json == null ? null : Size._fromJson(json);
Size({required this.size});
Size._fromJson(Map<String, dynamic> json) : size = json['size'];
final int size;
}
/// A collection of historical [Event]s from some stream.
class StreamHistory extends Response {
static StreamHistory? parse(Map<String, dynamic>? json) =>
+1 -1
View File
@@ -1,5 +1,5 @@
name: dds_service_extensions
version: 1.6.3
version: 1.7.0
description: >-
Extension methods for `package:vm_service`, used to make requests a
Dart Development Service (DDS) instance.
@@ -1,70 +0,0 @@
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:observatory/service_io.dart';
import 'package:test/test.dart';
import 'test_helper.dart';
var tests = <VMTest>[
(VM vm) async {
final defaultClientName = 'client1';
final clientName = 'agent-007';
var result = await vm.invokeRpcNoUpgrade('getClientName', {});
expect(result['type'], 'ClientName');
expect(result['name'], defaultClientName);
// Set the name for this client.
result = await vm.invokeRpcNoUpgrade(
'setClientName',
{
'name': clientName,
},
);
expect(result['type'], 'Success');
// Check it was set properly.
result = await vm.invokeRpcNoUpgrade('getClientName', {});
expect(result['type'], 'ClientName');
expect(result['name'], clientName);
// Check clearing works properly.
result = await vm.invokeRpcNoUpgrade(
'setClientName',
{
'name': '',
},
);
expect(result['type'], 'Success');
result = await vm.invokeRpcNoUpgrade('getClientName', {});
expect(result['type'], 'ClientName');
expect(result['name'], defaultClientName);
},
// Try to set an invalid agent name for this client.
(VM vm) async {
try {
await vm.invokeRpcNoUpgrade(
'setClientName',
{
'name': 42,
},
);
fail('Successfully set invalid client name');
} on ServerRpcException {/* expected */}
},
// Missing parameters.
(VM vm) async {
try {
await vm.invokeRpcNoUpgrade('setClientName', {});
fail('Successfully set name with no type');
} on ServerRpcException {/* expected */}
},
];
main(args) async => runVMTests(
args,
tests,
enableService: false,
);
@@ -1,70 +0,0 @@
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:observatory/service_io.dart';
import 'package:test/test.dart';
import 'client_resume_approvals_common.dart';
import 'service_test_common.dart';
import 'test_helper.dart';
void fooBar() {
int i = 0;
print(i);
}
late WebSocketVM client1;
late WebSocketVM client2;
final test = <IsolateTest>[
hasPausedAtStart,
(Isolate isolate) async {
client1 = await createClient(isolate.owner as WebSocketVM);
await setRequireApprovalForResume(
client1,
isolate,
pauseOnStart: true,
pauseOnExit: true,
);
client2 = await createClient(
isolate.owner as WebSocketVM,
clientName: otherClientName,
);
await setRequireApprovalForResume(
client2,
isolate,
pauseOnStart: true,
pauseOnExit: true,
);
// Give resume approval for client1 to ensure approval state is cleaned up
// properly when both client1 and client2 have disconnected.
await resume(client1, isolate);
expect(await isPausedAtStart(isolate), true);
// Once client1 is disconnected, we should still be paused.
client1.disconnect();
expect(await isPausedAtStart(isolate), true);
// Once client2 disconnects, there are no clients which require resume
// approval. Since there were no resume requests made by clients which are
// still connected, the isolate remains paused.
client2.disconnect();
expect(await isPausedAtStart(isolate), true);
await isolate.resume();
},
hasStoppedAtExit,
];
Future<void> main(args) => runIsolateTests(
args,
test,
testeeConcurrent: fooBar,
pause_on_start: true,
pause_on_exit: true,
enableService: false,
);
@@ -1,85 +0,0 @@
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:observatory/service_io.dart';
import 'service_test_common.dart';
const String clientName = 'TestClient';
const String otherClientName = 'OtherTestClient';
Future<void> setClientName(WebSocketVM client, String name) async =>
await client.invokeRpc('setClientName', {'name': name});
Future<WebSocketVM> createClient(WebSocketVM vm,
{String clientName = clientName}) async {
final client = WebSocketVM(vm.target);
await client.load();
await setClientName(client, clientName);
return client;
}
Future<void> setRequireApprovalForResume(
WebSocketVM vm,
Isolate isolate, {
bool pauseOnStart = false,
bool pauseOnExit = false,
bool pauseOnReload = false,
}) async {
int pauseTypeMask = 0;
if (pauseOnStart) {
pauseTypeMask |= 1;
}
if (pauseOnReload) {
pauseTypeMask |= 2;
}
if (pauseOnExit) {
pauseTypeMask |= 4;
}
await vm.invokeRpc('requirePermissionToResume', {
'isolateId': isolate.id,
'pauseTypeMask': pauseTypeMask,
'onPauseStart': pauseOnStart,
'onPauseReload': pauseOnReload,
'onPauseExit': pauseOnExit,
});
}
Future<void> resume(WebSocketVM vm, Isolate isolate) async =>
await vm.invokeRpc('resume', {
'isolateId': isolate.id,
});
Future<bool> isPausedAtStart(Isolate isolate) async {
await isolate.reload();
return ((isolate.pauseEvent != null) &&
isEventOfKind(isolate.pauseEvent, ServiceEvent.kPauseStart));
}
Future<bool> isPausedAtExit(Isolate isolate) async {
await isolate.reload();
return ((isolate.pauseEvent != null) &&
isEventOfKind(isolate.pauseEvent, ServiceEvent.kPauseExit));
}
Future<bool> isPausedPostRequest(Isolate isolate) async {
await isolate.reload();
return ((isolate.pauseEvent != null) &&
isEventOfKind(isolate.pauseEvent, ServiceEvent.kPausePostRequest));
}
Future<void> waitForResume(Isolate isolate) async {
final completer = Completer<void>();
isolate.vm.getEventStream(VM.kDebugStream).then((stream) {
var subscription;
subscription = stream.listen((ServiceEvent event) {
if (event.kind == ServiceEvent.kResume) {
subscription.cancel();
completer.complete();
}
});
});
await completer.future;
}
@@ -1,68 +0,0 @@
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:observatory/service_io.dart';
import 'package:test/test.dart';
import 'client_resume_approvals_common.dart';
import 'service_test_common.dart';
import 'test_helper.dart';
void fooBar() {
int i = 0;
print(i);
}
late WebSocketVM client1;
late WebSocketVM client2;
final test = <IsolateTest>[
// Multiple clients, disconnect client awaiting approval.
hasPausedAtStart,
(Isolate isolate) async {
client1 = await createClient(isolate.owner as WebSocketVM);
await setRequireApprovalForResume(
client1,
isolate,
pauseOnStart: true,
pauseOnExit: true,
);
client2 = await createClient(
isolate.owner as WebSocketVM,
clientName: otherClientName,
);
await setRequireApprovalForResume(
client2,
isolate,
pauseOnStart: true,
pauseOnExit: true,
);
// Send a resume request on the test client so we'll resume once the other
// clients which require approval disconnect.
await isolate.resume();
expect(await isPausedAtStart(isolate), true);
// Once client1 is disconnected, we should still be paused.
client1.disconnect();
expect(await isPausedAtStart(isolate), true);
// Once client2 disconnects, there are no clients which require resume
// approval. Ensure we resume immediately so we don't deadlock waiting for
// approvals from disconnected clients.
client2.disconnect();
},
hasStoppedAtExit,
];
Future<void> main(args) => runIsolateTests(
args,
test,
testeeConcurrent: fooBar,
pause_on_start: true,
pause_on_exit: true,
enableService: false,
);
@@ -1,50 +0,0 @@
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:observatory/service_io.dart';
import 'package:test/test.dart';
import 'client_resume_approvals_common.dart';
import 'service_test_common.dart';
import 'test_helper.dart';
void fooBar() {
int i = 0;
print(i);
}
late WebSocketVM client1;
late WebSocketVM client2;
final sameClientNamesTest = <IsolateTest>[
// Multiple clients, same client names.
(Isolate isolate) async {
final resumeFuture = waitForResume(isolate);
client1 = await createClient(isolate.owner as WebSocketVM);
await setRequireApprovalForResume(
client1,
isolate,
pauseOnStart: true,
pauseOnExit: true,
);
client2 = await createClient(isolate.owner as WebSocketVM);
expect(await isPausedAtStart(isolate), true);
await resume(client2, isolate);
await resumeFuture;
},
hasStoppedAtExit,
];
Future<void> main(args) => runIsolateTests(
args,
sameClientNamesTest,
testeeConcurrent: fooBar,
pause_on_start: true,
pause_on_exit: true,
enableService: false,
);
@@ -1,69 +0,0 @@
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:observatory/service_io.dart';
import 'package:test/test.dart';
import 'client_resume_approvals_common.dart';
import 'service_test_common.dart';
import 'test_helper.dart';
void fooBar() {
int i = 0;
print(i);
}
late WebSocketVM client1;
late WebSocketVM client2;
late WebSocketVM client3;
final multipleClientNamesTest = <IsolateTest>[
// Multiple clients, different client names.
(Isolate isolate) async {
client1 = await createClient(isolate.owner as WebSocketVM);
await setRequireApprovalForResume(
client1,
isolate,
pauseOnStart: true,
pauseOnExit: true,
);
client2 = await createClient(
isolate.owner as WebSocketVM,
clientName: otherClientName,
);
client3 = await createClient(isolate.owner as WebSocketVM,
clientName: 'DummyClient');
final resumeFuture = waitForResume(isolate);
expect(await isPausedAtStart(isolate), true);
await resume(client2, isolate);
expect(await isPausedAtStart(isolate), true);
await resume(client1, isolate);
await resumeFuture;
expect(await isPausedAtStart(isolate), false);
},
hasStoppedAtExit,
(Isolate isolate) async {
await setRequireApprovalForResume(
client2,
isolate,
pauseOnExit: true,
);
await resume(client1, isolate);
expect(await isPausedAtExit(isolate), true);
await resume(client2, isolate);
await waitForTargetVMExit(isolate.vm);
},
];
Future<void> main(args) => runIsolateTests(
args,
multipleClientNamesTest,
testeeConcurrent: fooBar,
pause_on_start: true,
pause_on_exit: true,
enableService: false,
);
@@ -1,62 +0,0 @@
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:observatory/service_io.dart';
import 'package:test/test.dart';
import 'client_resume_approvals_common.dart';
import 'service_test_common.dart';
import 'test_helper.dart';
void fooBar() {
int i = 0;
print(i);
}
late WebSocketVM client1;
late WebSocketVM client2;
late WebSocketVM client3;
final nameChangeTest = <IsolateTest>[
// Remove required approvals via name change.
(Isolate isolate) async {
waitForResume(isolate);
// Create two clients with the same name.
client1 = await createClient(isolate.owner as WebSocketVM);
client2 = await createClient(isolate.owner as WebSocketVM);
await setRequireApprovalForResume(
client1,
isolate,
pauseOnStart: true,
pauseOnExit: true,
);
client3 = await createClient(isolate.owner as WebSocketVM,
clientName: otherClientName);
// Check that client3 can't resume the isolate on its own.
expect(await isPausedAtStart(isolate), true);
await resume(client3, isolate);
expect(await isPausedAtStart(isolate), true);
// Change the name of client1. Since client2 has the same name that client1
// originally had, the service still requires approval to resume the
// isolate.
await setClientName(client1, 'foobar');
expect(await isPausedAtStart(isolate), true);
await setClientName(client2, 'baz');
},
hasStoppedAtExit,
];
Future<void> main(args) => runIsolateTests(
args,
nameChangeTest,
testeeConcurrent: fooBar,
pause_on_start: true,
pause_on_exit: true,
enableService: false,
);
@@ -1,68 +0,0 @@
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:observatory/service_io.dart';
import 'package:test/test.dart';
import 'client_resume_approvals_common.dart';
import 'service_test_common.dart';
import 'test_helper.dart';
void fooBar() {
int _ = 0;
while (true) {
_++;
}
}
late WebSocketVM client1;
late WebSocketVM client2;
final hotReloadTest = <IsolateTest>[
// Multiple clients, hot reload approval.
(Isolate isolate) async {
waitForResume(isolate);
client1 = await createClient(isolate.owner as WebSocketVM);
await setRequireApprovalForResume(
client1,
isolate,
pauseOnReload: true,
);
client2 = await createClient(
isolate.owner as WebSocketVM,
clientName: otherClientName,
);
await setRequireApprovalForResume(
client2,
isolate,
pauseOnReload: true,
);
},
// Paused on start, resume.
resumeIsolate,
// Reload and then pause.
reloadSources(true),
hasStoppedPostRequest,
(Isolate isolate) async {
// Check that client2 can't resume the isolate on its own.
expect(await isPausedPostRequest(isolate), true);
await resume(client2, isolate);
expect(await isPausedPostRequest(isolate), true);
final resumeFuture = waitForResume(isolate);
await resume(client1, isolate);
await resumeFuture;
expect(await isPausedPostRequest(isolate), false);
},
];
Future<void> main(args) => runIsolateTests(
args,
hotReloadTest,
testeeConcurrent: fooBar,
pause_on_start: true,
enableService: false,
);
@@ -1,34 +0,0 @@
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:observatory/service_io.dart';
import 'package:test/test.dart';
import 'test_helper.dart';
Future streamListen(VM vm, String streamId) async =>
await vm.invokeRpcNoUpgrade(
'streamListen',
{
'streamId': streamId,
},
);
var tests = <VMTest>[
// Ensure the DDS allows for listening to a custom stream
(VM vm) async {
try {
await streamListen(vm, 'Foo');
} catch (e) {
fail('Unable to subscribe to a custom stream: $e');
}
}
];
main(args) => runVMTests(
args,
tests,
enableService: false,
enableDds: true,
);
@@ -1,52 +0,0 @@
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:developer';
import 'package:observatory/service_io.dart';
import 'package:test/test.dart';
import 'client_resume_approvals_common.dart';
import 'service_test_common.dart';
import 'test_helper.dart';
Future testMain() async {
// Post a total of 9 events
for (int i = 1; i <= 9; ++i) {
postEvent('Test', {
'id': i,
});
}
}
var tests = <IsolateTest>[
isPausedAtStart,
resumeIsolate,
(Isolate isolate) async {
final completer = Completer<void>();
int i = 1;
await subscribeToStream(isolate.vm, 'Extension', (event) async {
expect(event.extensionKind, 'Test');
expect(event.extensionData!['id'], i);
i++;
if (i == 10) {
await cancelStreamSubscription('Extension');
completer.complete();
} else if (i > 10) {
fail('Too many log messages');
}
});
await completer.future;
},
];
main(args) => runIsolateTests(
args,
tests,
enableService: false, // DDS specific feature
testeeConcurrent: testMain,
pause_on_start: true,
pause_on_exit: true,
);
@@ -1,83 +0,0 @@
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:developer';
import 'package:observatory/service_io.dart';
import 'package:test/test.dart';
import 'service_test_common.dart';
import 'test_helper.dart';
Future testMain() async {
// Log a total of 30 messages
for (int i = 1; i <= maxLogHistorySize + 10; ++i) {
log('All work and no play makes Ben a dull boy ($i)');
}
debugger();
}
const maxLogHistorySize = 100000;
Future setLogHistorySize(Isolate isolate, int size) async {
return await isolate.invokeRpcNoUpgrade('setLogHistorySize', {
'size': size,
});
}
Future<int> getLogHistorySize(Isolate isolate) async {
final result = await isolate.invokeRpcNoUpgrade('getLogHistorySize', {});
expect(result['type'], 'Size');
return result['size'] as int;
}
var tests = <IsolateTest>[
hasPausedAtStart,
(Isolate isolate) async {
final initialSize = await getLogHistorySize(isolate);
try {
await setLogHistorySize(isolate, maxLogHistorySize + 1);
} on ServerRpcException catch (e) {
expect(e.message, "'size' must be less than $maxLogHistorySize");
}
expect(await getLogHistorySize(isolate), initialSize);
},
(Isolate isolate) async {
final result = await setLogHistorySize(isolate, maxLogHistorySize);
expect(result['type'], 'Success');
expect(await getLogHistorySize(isolate), maxLogHistorySize);
},
resumeIsolate,
hasStoppedAtBreakpoint,
(Isolate isolate) async {
print("Starting step 6");
final completer = Completer<void>();
await Future.delayed(const Duration(seconds: 1));
int i = 11;
await subscribeToStream(isolate.vm, 'Logging', (event) async {
expect(
event.logRecord!['message'].valueAsString,
'All work and no play makes Ben a dull boy ($i)',
);
i++;
if (i == maxLogHistorySize + 10) {
await cancelStreamSubscription('Logging');
completer.complete();
}
});
await completer.future;
},
];
main(args) => runIsolateTests(
args,
tests,
enableService: false, // DDS specific feature
testeeConcurrent: testMain,
pause_on_start: true,
pause_on_exit: true,
);
@@ -1,98 +0,0 @@
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:developer';
import 'package:observatory/service_io.dart';
import 'package:test/test.dart';
import 'client_resume_approvals_common.dart';
import 'service_test_common.dart';
import 'test_helper.dart';
Future testMain() async {
// Log a total of 9 messages
for (int i = 1; i <= 9; ++i) {
log('log$i');
}
debugger();
log('log10');
}
Future setLogHistorySize(Isolate isolate, int size) async {
return await isolate.invokeRpcNoUpgrade('setLogHistorySize', {
'size': size,
});
}
Future<int> getLogHistorySize(Isolate isolate) async {
final result = await isolate.invokeRpcNoUpgrade('getLogHistorySize', {});
expect(result['type'], 'Size');
return result['size'] as int;
}
var tests = <IsolateTest>[
isPausedAtStart,
resumeIsolate,
(Isolate isolate) async {
// Check that resizing does the right thing.
final result = await setLogHistorySize(isolate, 10);
expect(result['type'], 'Success');
expect(await getLogHistorySize(isolate), 10);
final completer = Completer<void>();
int i = 1;
await subscribeToStream(isolate.vm, 'Logging', (event) async {
expect(event.logRecord!['message'].valueAsString, 'log$i');
i++;
if (i == 10) {
await cancelStreamSubscription('Logging');
completer.complete();
} else if (i > 10) {
fail('Too many log messages');
}
});
await completer.future;
},
(Isolate isolate) async {
// Resize to be smaller
final result = await setLogHistorySize(isolate, 5);
expect(result['type'], 'Success');
expect(await getLogHistorySize(isolate), 5);
},
resumeIsolate,
(Isolate isolate) async {
final completer = Completer<void>();
// Create a new client as we want to get log messages from the entire
// history buffer.
final client = await createClient(isolate.vm as WebSocketVM);
int i = 6;
await subscribeToStream(client, 'Logging', (event) async {
expect(event.logRecord!['message'].valueAsString, 'log$i');
i++;
if (i == 11) {
await cancelStreamSubscription('Logging');
completer.complete();
} else if (i > 11) {
fail('Too many log messages');
}
});
await completer.future;
client.disconnect();
},
];
main(args) => runIsolateTests(
args,
tests,
enableService: false, // DDS specific feature
testeeConcurrent: testMain,
pause_on_start: true,
pause_on_exit: true,
);
@@ -1,91 +0,0 @@
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:developer';
import 'package:observatory/service_io.dart';
import 'package:test/test.dart';
import 'service_test_common.dart';
import 'test_helper.dart';
Future testMain() async {
// Initial logging history should be 0, so these messages won't be buffered.
log('log1');
log('log2');
// Setting the log history length does not apply retroactively.
debugger();
// Log a total of 30 messages
for (int i = 3; i <= 30; ++i) {
log('log$i');
}
}
Future setLogHistorySize(Isolate isolate, int size) async {
return await isolate.invokeRpcNoUpgrade('setLogHistorySize', {
'size': size,
});
}
Future<int> getLogHistorySize(Isolate isolate) async {
final result = await isolate.invokeRpcNoUpgrade('getLogHistorySize', {});
expect(result['type'], 'Size');
return result['size'] as int;
}
var tests = <IsolateTest>[
hasPausedAtStart,
(Isolate isolate) async {
final result = await setLogHistorySize(isolate, 0);
expect(result['type'], 'Success');
expect(await getLogHistorySize(isolate), 0);
},
resumeIsolate,
hasStoppedAtBreakpoint,
(Isolate isolate) async {
final result = await setLogHistorySize(isolate, 20);
expect(await getLogHistorySize(isolate), 20);
expect(result['type'], 'Success');
},
resumeIsolate,
(Isolate isolate) async {
final completer = Completer<void>();
await Future.delayed(const Duration(seconds: 1));
// With the log history set to 20, the first log message should be 'log11'
int i = 11;
await subscribeToStream(isolate.vm, 'Logging', (event) async {
expect(event.logRecord!['message'].valueAsString, 'log$i');
i++;
if (i == 30) {
await cancelStreamSubscription('Logging');
completer.complete();
}
});
await completer.future;
},
(Isolate isolate) async {
try {
// Try to set an invalid history size
await setLogHistorySize(isolate, -1);
fail('Successfully set invalid size');
} on ServerRpcException catch (e) {
expect(e.message, "'size' must be greater or equal to zero");
}
expect(await getLogHistorySize(isolate), 20);
}
];
main(args) => runIsolateTests(
args,
tests,
enableService: false, // DDS specific feature
testeeConcurrent: testMain,
pause_on_start: true,
pause_on_exit: true,
);
@@ -1,65 +0,0 @@
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:io';
import 'package:observatory/service_io.dart';
import 'package:test/test.dart';
import 'client_resume_approvals_common.dart';
import 'service_test_common.dart';
import 'test_helper.dart';
Future testMain() async {
// Log a total of 9 messages
for (int i = 1; i <= 9; ++i) {
print('Stdout log$i');
stderr.writeln('Stderr log$i');
}
}
Future streamHistoryTest(Isolate isolate, String stream) async {
final completer = Completer<void>();
int i = 1;
await subscribeToStream(isolate.vm, stream, (event) async {
if (stream == 'Stdout') {
expect(event.bytesAsString, '$stream log$i\n');
} else {
// Newlines are sent as separate events for some reason. Ignore them.
if (!event.bytesAsString!.startsWith(stream)) {
return;
}
expect(event.bytesAsString, '$stream log$i');
}
i++;
if (i == 10) {
await cancelStreamSubscription(stream);
completer.complete();
} else if (i > 10) {
fail('Too many log messages');
}
});
await completer.future;
}
var tests = <IsolateTest>[
isPausedAtStart,
resumeIsolate,
(Isolate isolate) async {
await streamHistoryTest(isolate, 'Stdout');
},
(Isolate isolate) async {
await streamHistoryTest(isolate, 'Stderr');
},
];
main(args) => runIsolateTests(
args,
tests,
enableService: false, // DDS specific feature
testeeConcurrent: testMain,
pause_on_start: true,
pause_on_exit: true,
);
@@ -1,45 +0,0 @@
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:observatory/service_io.dart';
import 'package:test/test.dart';
import 'test_helper.dart';
void fooBar() {}
Future<String> getClientName(Isolate isolate) async {
final result = await isolate.vm.invokeRpcNoUpgrade('getClientName', {});
return result['name'] as String;
}
Future<void> setClientName(Isolate isolate, String name) async =>
await isolate.vm.invokeRpcNoUpgrade('setClientName', {
'name': name,
});
final test = <IsolateTest>[
(Isolate isolate) async {
// Each client has a default name based on the order of connection to the
// service.
expect(await getClientName(isolate), 'client1');
// Set a custom client name and check it was set properly.
await setClientName(isolate, 'foobar');
expect(await getClientName(isolate), 'foobar');
// Clear the client name and check that we're using the default again.
await setClientName(isolate, '');
expect(await getClientName(isolate), 'client1');
},
];
Future<void> main(args) => runIsolateTests(
args,
test,
testeeBefore: fooBar,
enableService: false,
);