From d48a76e988fbc8d6fbf4bb5e25d1a048c594eba8 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Fri, 26 Jan 2024 22:45:14 +0000 Subject: [PATCH] [ 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 Commit-Queue: Ben Konyi --- pkg/dds/test/client_name_rpc_test.dart | 37 + ..._test.dart => client_post_event_test.dart} | 0 ...pprovals_approve_then_disconnect_test.dart | 62 ++ .../test/client_resume_approvals_common.dart | 24 + ...ient_resume_approvals_disconnect_test.dart | 59 ++ ...resume_approvals_identical_names_test.dart | 44 ++ ..._resume_approvals_multiple_names_test.dart | 62 ++ ...ent_resume_approvals_name_change_test.dart | 64 ++ .../client_resume_approvals_reload_test.dart | 60 ++ pkg/dds/test/common/service_test_common.dart | 717 ++++++++++++++++++ pkg/dds/test/common/test_helper.dart | 530 ++++++++++++- pkg/dds/test/custom_stream_listen_test.dart | 25 + ...dds_disconnects_existing_clients_test.dart | 28 +- .../test/extension_event_history_test.dart | 53 ++ pkg/dds/test/get_client_name_rpc_test.dart | 38 + .../test/log_history_size_gigantic_test.dart | 78 ++ .../test/log_history_size_simple_test.dart | 91 +++ pkg/dds/test/log_history_size_test.dart | 83 ++ pkg/dds/test/stdout_stderr_history_test.dart | 76 ++ pkg/dds_service_extensions/CHANGELOG.md | 10 + .../lib/dds_service_extensions.dart | 131 ++++ pkg/dds_service_extensions/pubspec.yaml | 2 +- .../tests/service/client_name_rpc_test.dart | 70 -- ...pprovals_approve_then_disconnect_test.dart | 70 -- .../client_resume_approvals_common.dart | 85 --- ...ient_resume_approvals_disconnect_test.dart | 68 -- ...resume_approvals_identical_names_test.dart | 50 -- ..._resume_approvals_multiple_names_test.dart | 69 -- ...ent_resume_approvals_name_change_test.dart | 62 -- .../client_resume_approvals_reload_test.dart | 68 -- .../dds_custom_stream_listen_test.dart | 34 - .../dds_extension_event_history_test.dart | 52 -- .../dds_log_history_size_gigantic_test.dart | 83 -- .../dds_log_history_size_simple_test.dart | 98 --- .../service/dds_log_history_size_test.dart | 91 --- .../dds_stdout_stderr_history_test.dart | 65 -- .../service/get_client_name_rpc_test.dart | 45 -- 37 files changed, 2248 insertions(+), 1036 deletions(-) create mode 100644 pkg/dds/test/client_name_rpc_test.dart rename pkg/dds/test/{dds_client_post_event_test.dart => client_post_event_test.dart} (100%) create mode 100644 pkg/dds/test/client_resume_approvals_approve_then_disconnect_test.dart create mode 100644 pkg/dds/test/client_resume_approvals_common.dart create mode 100644 pkg/dds/test/client_resume_approvals_disconnect_test.dart create mode 100644 pkg/dds/test/client_resume_approvals_identical_names_test.dart create mode 100644 pkg/dds/test/client_resume_approvals_multiple_names_test.dart create mode 100644 pkg/dds/test/client_resume_approvals_name_change_test.dart create mode 100644 pkg/dds/test/client_resume_approvals_reload_test.dart create mode 100644 pkg/dds/test/common/service_test_common.dart create mode 100644 pkg/dds/test/custom_stream_listen_test.dart rename {runtime/observatory/tests/service => pkg/dds/test}/dds_disconnects_existing_clients_test.dart (70%) create mode 100644 pkg/dds/test/extension_event_history_test.dart create mode 100644 pkg/dds/test/get_client_name_rpc_test.dart create mode 100644 pkg/dds/test/log_history_size_gigantic_test.dart create mode 100644 pkg/dds/test/log_history_size_simple_test.dart create mode 100644 pkg/dds/test/log_history_size_test.dart create mode 100644 pkg/dds/test/stdout_stderr_history_test.dart delete mode 100644 runtime/observatory/tests/service/client_name_rpc_test.dart delete mode 100644 runtime/observatory/tests/service/client_resume_approvals_approve_then_disconnect_test.dart delete mode 100644 runtime/observatory/tests/service/client_resume_approvals_common.dart delete mode 100644 runtime/observatory/tests/service/client_resume_approvals_disconnect_test.dart delete mode 100644 runtime/observatory/tests/service/client_resume_approvals_identical_names_test.dart delete mode 100644 runtime/observatory/tests/service/client_resume_approvals_multiple_names_test.dart delete mode 100644 runtime/observatory/tests/service/client_resume_approvals_name_change_test.dart delete mode 100644 runtime/observatory/tests/service/client_resume_approvals_reload_test.dart delete mode 100644 runtime/observatory/tests/service/dds_custom_stream_listen_test.dart delete mode 100644 runtime/observatory/tests/service/dds_extension_event_history_test.dart delete mode 100644 runtime/observatory/tests/service/dds_log_history_size_gigantic_test.dart delete mode 100644 runtime/observatory/tests/service/dds_log_history_size_simple_test.dart delete mode 100644 runtime/observatory/tests/service/dds_log_history_size_test.dart delete mode 100644 runtime/observatory/tests/service/dds_stdout_stderr_history_test.dart delete mode 100644 runtime/observatory/tests/service/get_client_name_rpc_test.dart diff --git a/pkg/dds/test/client_name_rpc_test.dart b/pkg/dds/test/client_name_rpc_test.dart new file mode 100644 index 00000000000..1e0c8a9bb0c --- /dev/null +++ b/pkg/dds/test/client_name_rpc_test.dart @@ -0,0 +1,37 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +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 = [ + (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 []]) => runVMTests( + args, + tests, + 'client_name_rpc_test.dart', + ); diff --git a/pkg/dds/test/dds_client_post_event_test.dart b/pkg/dds/test/client_post_event_test.dart similarity index 100% rename from pkg/dds/test/dds_client_post_event_test.dart rename to pkg/dds/test/client_post_event_test.dart diff --git a/pkg/dds/test/client_resume_approvals_approve_then_disconnect_test.dart b/pkg/dds/test/client_resume_approvals_approve_then_disconnect_test.dart new file mode 100644 index 00000000000..15f42f896da --- /dev/null +++ b/pkg/dds/test/client_resume_approvals_approve_then_disconnect_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 = [ + 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 []]) => runIsolateTests( + args, + test, + 'client_resume_approvals_approve_then_disconnect_test.dart', + testeeConcurrent: fooBar, + pauseOnStart: true, + pauseOnExit: true, + ); diff --git a/pkg/dds/test/client_resume_approvals_common.dart b/pkg/dds/test/client_resume_approvals_common.dart new file mode 100644 index 00000000000..3c5d1f1c338 --- /dev/null +++ b/pkg/dds/test/client_resume_approvals_common.dart @@ -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 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; +} diff --git a/pkg/dds/test/client_resume_approvals_disconnect_test.dart b/pkg/dds/test/client_resume_approvals_disconnect_test.dart new file mode 100644 index 00000000000..f5901cc957d --- /dev/null +++ b/pkg/dds/test/client_resume_approvals_disconnect_test.dart @@ -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 = [ + // 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 []]) => runIsolateTests( + args, + test, + 'client_resume_approvals_disconnect_test.dart', + testeeConcurrent: fooBar, + pauseOnStart: true, + pauseOnExit: true, + ); diff --git a/pkg/dds/test/client_resume_approvals_identical_names_test.dart b/pkg/dds/test/client_resume_approvals_identical_names_test.dart new file mode 100644 index 00000000000..213c761a4ef --- /dev/null +++ b/pkg/dds/test/client_resume_approvals_identical_names_test.dart @@ -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 = [ + // 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 []]) => runIsolateTests( + args, + test, + 'client_resume_approvals_identical_names_test.dart', + testeeConcurrent: fooBar, + pauseOnStart: true, + pauseOnExit: true, + ); diff --git a/pkg/dds/test/client_resume_approvals_multiple_names_test.dart b/pkg/dds/test/client_resume_approvals_multiple_names_test.dart new file mode 100644 index 00000000000..47a836904aa --- /dev/null +++ b/pkg/dds/test/client_resume_approvals_multiple_names_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: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 = [ + // 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 []]) => runIsolateTests( + args, + test, + 'client_resume_approvals_multiple_names_test.dart', + testeeConcurrent: fooBar, + pauseOnStart: true, + pauseOnExit: true, + ); diff --git a/pkg/dds/test/client_resume_approvals_name_change_test.dart b/pkg/dds/test/client_resume_approvals_name_change_test.dart new file mode 100644 index 00000000000..6293f6b036b --- /dev/null +++ b/pkg/dds/test/client_resume_approvals_name_change_test.dart @@ -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 = [ + // 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 []]) => runIsolateTests( + args, + test, + 'client_resume_approvals_name_change_test.dart', + testeeConcurrent: fooBar, + pauseOnStart: true, + pauseOnExit: true, + ); diff --git a/pkg/dds/test/client_resume_approvals_reload_test.dart b/pkg/dds/test/client_resume_approvals_reload_test.dart new file mode 100644 index 00000000000..3cc1dc3b531 --- /dev/null +++ b/pkg/dds/test/client_resume_approvals_reload_test.dart @@ -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 = [ + // 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 []]) => runIsolateTests( + args, + test, + 'client_resume_approvals_reload_test.dart', + testeeConcurrent: fooBar, + pauseOnStart: true, + ); diff --git a/pkg/dds/test/common/service_test_common.dart b/pkg/dds/test/common/service_test_common.dart new file mode 100644 index 00000000000..5b98fc2bd5f --- /dev/null +++ b/pkg/dds/test/common/service_test_common.dart @@ -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 Function( + VmService service, + IsolateRef isolate, +); +typedef VMTest = Future Function(VmService service); + +Future 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 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 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 hasPausedFor( + VmService service, + IsolateRef isolateRef, + String kind, +) async { + Completer? completer = Completer(); + late StreamSubscription 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 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 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 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 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 hasPausedAtStart(VmService service, IsolateRef isolate) { + return hasPausedFor(service, isolate, EventKind.kPauseStart); +} + +Future markDartColonLibrariesDebuggable( + VmService service, + IsolateRef isolateRef, +) async { + final isolateId = isolateRef.id!; + final isolate = await service.getIsolate(isolateId); + final requests = []; + 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 resumeIsolate(VmService service, IsolateRef isolate) async { + final Completer completer = Completer(); + late StreamSubscription 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 _subscribeDebugStream(VmService service) async { + try { + await service.streamListen(EventStreams.kDebug); + return true; + } catch (_) { + /* swallow exception */ + return false; + } +} + +Future _unsubscribeDebugStream(VmService service) async { + try { + await service.streamCancel(EventStreams.kDebug); + } catch (_) { + /* swallow exception */ + } +} + +Future resumeAndAwaitEvent( + VmService service, + IsolateRef isolateRef, + String streamId, + Function(Event) onEvent, +) async { + final completer = Completer(); + 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 stepOver(VmService service, IsolateRef isolateRef) async { + await _subscribeDebugStream(service); + await service.resume(isolateRef.id!, step: 'Over'); + await hasStoppedAtBreakpoint(service, isolateRef); + await _unsubscribeDebugStream(service); +} + +Future stepInto(VmService service, IsolateRef isolateRef) async { + await _subscribeDebugStream(service); + await service.resume(isolateRef.id!, step: 'Into'); + await hasStoppedAtBreakpoint(service, isolateRef); + await _unsubscribeDebugStream(service); +} + +Future 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 recordStops, + bool includeCaller, +) { + return (VmService service, IsolateRef isolateRef) async { + final completer = Completer(); + + 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 _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 recordStops) { + return (VmService service, IsolateRef isolateRef) async { + final completer = Completer(); + + 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 recordStops) { + return (VmService service, IsolateRef isolateRef) async { + final completer = Completer(); + + 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 recordStops, + List 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 removeAdjacentDuplicates(List fromList) { + final List result = []; + 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 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> Function( + Map cb, +); + +/// Registers a service extension and returns the actual service name used to +/// invoke the service. +Future registerServiceHelper( + VmService primaryClient, + VmService serviceRegisterClient, + String serviceName, + ServiceExtensionHandler callback, +) async { + final serviceNameCompleter = Completer(); + 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 evaluateInFrameAndExpect( + VmService service, + String isolateId, + String expression, + String expected, { + Map? 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 evaluateAndExpect( + VmService service, + String isolateId, + String targetId, + String expression, + String expected, { + Map? 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 fetchHeapSnapshot( + VmService service, + IsolateRef isolateRef, +) async { + final isolateId = isolateRef.id!; + final completer = Completer(); + late final StreamSubscription sub; + final data = []; + 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'); + } + }; +} diff --git a/pkg/dds/test/common/test_helper.dart b/pkg/dds/test/common/test_helper.dart index 35dc0cfc34c..ae05eb46c40 100644 --- a/pkg/dds/test/common/test_helper.dart +++ b/pkg/dds/test/common/test_helper.dart @@ -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 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 _TESTEE_SPAWN_ENV = {_TESTEE_ENV_KEY: 'true'}; late Uri remoteVmServiceUri; -Future spawnDartProcess( +Future 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 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 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 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 args; + + bool killedByTester = false; + final _exitCodeCompleter = Completer(); + + _ServiceTesteeLauncher(String script) + : args = [_getTestUri(script).toFilePath()]; + + Future get exitCode => _exitCodeCompleter.future; + + // Spawn the testee process. + Future _spawnProcess( + bool pauseOnStart, + bool pauseOnExit, + bool pauseOnUnhandledExceptions, + bool testeeControlsServer, + bool useAuthToken, + List? experiments, + List? extraArgs, + ) { + return _spawnDartProcess( + pauseOnStart, + pauseOnExit, + pauseOnUnhandledExceptions, + testeeControlsServer, + useAuthToken, + experiments, + extraArgs, + ); + } + + Future _spawnDartProcess( + bool pauseOnStart, + bool pauseOnExit, + bool pauseOnUnhandledExceptions, + bool testeeControlsServer, + bool useAuthToken, + List? experiments, + List? extraArgs, + ) { + final String dartExecutable = io.Platform.executable; + + final fullArgs = []; + 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, {}); + } + + Future _spawnCommon( + String executable, + List arguments, + Map 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 launch( + bool pauseOnStart, + bool pauseOnExit, + bool pauseOnUnhandledExceptions, + bool testeeControlsServer, + bool useAuthToken, + List? experiments, + List? extraArgs, + ) { + return _spawnProcess( + pauseOnStart, + pauseOnExit, + pauseOnUnhandledExceptions, + testeeControlsServer, + useAuthToken, + experiments, + extraArgs, + ).then((p) { + final Completer completer = Completer(); + 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 run({ + List? mainArgs, + List? extraArgs, + List? experiments, + List? vmTests, + List? 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 getFirstIsolate(VmService service) async { + var vm = await service.getVM(); + final vmIsolates = vm.isolates!; + if (vmIsolates.isNotEmpty) { + return vmIsolates.first; + } + Completer? 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 runIsolateTests( + List mainArgs, + List 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? experiments, + List? 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 mainArgs, + List tests, + String scriptName, { + void Function()? testeeBefore, + void Function()? testeeConcurrent, + bool pauseOnStart = false, + bool pauseOnExit = false, + bool verboseVm = false, + bool pauseOnUnhandledExceptions = false, + List? 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 runVMTests( + List mainArgs, + List tests, + String scriptName, { + Function()? testeeBefore, + Function()? testeeConcurrent, + bool pauseOnStart = false, + bool pauseOnExit = false, + bool verboseVm = false, + bool pauseOnUnhandledExceptions = false, + List? 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, + ); + } } diff --git a/pkg/dds/test/custom_stream_listen_test.dart b/pkg/dds/test/custom_stream_listen_test.dart new file mode 100644 index 00000000000..b931ff99cd9 --- /dev/null +++ b/pkg/dds/test/custom_stream_listen_test.dart @@ -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 = [ + // 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 []]) => runVMTests( + args, + tests, + 'custom_stream_listen_test.dart', + ); diff --git a/runtime/observatory/tests/service/dds_disconnects_existing_clients_test.dart b/pkg/dds/test/dds_disconnects_existing_clients_test.dart similarity index 70% rename from runtime/observatory/tests/service/dds_disconnects_existing_clients_test.dart rename to pkg/dds/test/dds_disconnects_existing_clients_test.dart index 584b1cc67df..5204b6ca09f 100644 --- a/runtime/observatory/tests/service/dds_disconnects_existing_clients_test.dart +++ b/pkg/dds/test/dds_disconnects_existing_clients_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 = [ - (VM vm) async { + (VmService service) async { late DartDevelopmentService dds; final waitForDDS = Completer(); final serviceMessageCompleter = Completer(); - // 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 = [ 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 []]) => runVMTests( args, tests, - enableDds: false, + 'dds_disconnects_existing_clients_test.dart', + extraArgs: ['--no-dds'], ); diff --git a/pkg/dds/test/extension_event_history_test.dart b/pkg/dds/test/extension_event_history_test.dart new file mode 100644 index 00000000000..699ca4f1e58 --- /dev/null +++ b/pkg/dds/test/extension_event_history_test.dart @@ -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 = [ + hasPausedAtStart, + resumeIsolate, + (VmService service, IsolateRef isolateRef) async { + final completer = Completer(); + 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 []]) => runIsolateTests( + args, + tests, + 'extension_event_history_test.dart', + testeeConcurrent: testMain, + pauseOnStart: true, + pauseOnExit: true, + ); diff --git a/pkg/dds/test/get_client_name_rpc_test.dart b/pkg/dds/test/get_client_name_rpc_test.dart new file mode 100644 index 00000000000..8321a65c03f --- /dev/null +++ b/pkg/dds/test/get_client_name_rpc_test.dart @@ -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 = [ + (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 []]) => runIsolateTests( + args, + test, + 'get_client_name_rpc_test.dart', + testeeConcurrent: fooBar, + pauseOnStart: true, + ); diff --git a/pkg/dds/test/log_history_size_gigantic_test.dart b/pkg/dds/test/log_history_size_gigantic_test.dart new file mode 100644 index 00000000000..be99b5d9010 --- /dev/null +++ b/pkg/dds/test/log_history_size_gigantic_test.dart @@ -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 = [ + 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(); + // 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 []]) => runIsolateTests( + args, + tests, + 'log_history_size_gigantic_test.dart', + testeeConcurrent: testMain, + pauseOnStart: true, + pauseOnExit: true, + ); diff --git a/pkg/dds/test/log_history_size_simple_test.dart b/pkg/dds/test/log_history_size_simple_test.dart new file mode 100644 index 00000000000..c3983409433 --- /dev/null +++ b/pkg/dds/test/log_history_size_simple_test.dart @@ -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 = [ + 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(); + + 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(); + + // 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 []]) => runIsolateTests( + args, + tests, + 'log_history_size_simple_test.dart', + testeeConcurrent: testMain, + pauseOnStart: true, + pauseOnExit: true, + ); diff --git a/pkg/dds/test/log_history_size_test.dart b/pkg/dds/test/log_history_size_test.dart new file mode 100644 index 00000000000..0381424f74d --- /dev/null +++ b/pkg/dds/test/log_history_size_test.dart @@ -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 = [ + 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(); + + 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 []]) => runIsolateTests( + args, + tests, + 'log_history_size_test.dart', + testeeConcurrent: testMain, + pauseOnStart: true, + pauseOnExit: true, + ); diff --git a/pkg/dds/test/stdout_stderr_history_test.dart b/pkg/dds/test/stdout_stderr_history_test.dart new file mode 100644 index 00000000000..0d59d706957 --- /dev/null +++ b/pkg/dds/test/stdout_stderr_history_test.dart @@ -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 streamHistoryTest( + VmService service, + IsolateRef isolateRef, + String stream, +) async { + final completer = Completer(); + 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 = [ + 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 []]) => runIsolateTests( + args, + tests, + 'stdout_stderr_history_test.dart', + testeeConcurrent: testMain, + pauseOnStart: true, + pauseOnExit: true, + ); diff --git a/pkg/dds_service_extensions/CHANGELOG.md b/pkg/dds_service_extensions/CHANGELOG.md index db7e79b66d1..3917f2d87a0 100644 --- a/pkg/dds_service_extensions/CHANGELOG.md +++ b/pkg/dds_service_extensions/CHANGELOG.md @@ -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`. diff --git a/pkg/dds_service_extensions/lib/dds_service_extensions.dart b/pkg/dds_service_extensions/lib/dds_service_extensions.dart index fa63afda5ec..02f7ed043bd 100644 --- a/pkg/dds_service_extensions/lib/dds_service_extensions.dart +++ b/pkg/dds_service_extensions/lib/dds_service_extensions.dart @@ -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 getLogHistorySize(String isolateId) async { + // No version check needed, present since v1.0 of the protocol. + return _callHelper('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 setLogHistorySize(String isolateId, int size) async { + // No version check needed, present since v1.0 of the protocol. + return _callHelper('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 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 getClientName() async { + // No version check needed, present since v1.0 of the protocol. + return _callHelper( + '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 setClientName([String name = '']) async { + // No version check needed, present since v1.0 of the protocol. + return _callHelper( + '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 requirePermissionToResume({ + bool onPauseStart = false, + bool onPauseReload = false, + bool onPauseExit = false, + }) async { + // No version check needed, present since v1.0 of the protocol. + return _callHelper( + 'requirePermissionToResume', + args: { + 'onPauseStart': onPauseStart, + 'onPauseReload': onPauseReload, + 'onPauseExit': onPauseExit, + }, + ); + } + Future _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? json) => + json == null ? null : ClientName._fromJson(json); + + ClientName({required this.name}); + + ClientName._fromJson(Map json) : name = json['name']; + + final String name; +} + +/// A simple object representing a size response. +class Size extends Response { + static Size? parse(Map? json) => + json == null ? null : Size._fromJson(json); + + Size({required this.size}); + + Size._fromJson(Map json) : size = json['size']; + + final int size; +} + /// A collection of historical [Event]s from some stream. class StreamHistory extends Response { static StreamHistory? parse(Map? json) => diff --git a/pkg/dds_service_extensions/pubspec.yaml b/pkg/dds_service_extensions/pubspec.yaml index 908a28d2a94..414709c2d98 100644 --- a/pkg/dds_service_extensions/pubspec.yaml +++ b/pkg/dds_service_extensions/pubspec.yaml @@ -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. diff --git a/runtime/observatory/tests/service/client_name_rpc_test.dart b/runtime/observatory/tests/service/client_name_rpc_test.dart deleted file mode 100644 index 33a0db23fca..00000000000 --- a/runtime/observatory/tests/service/client_name_rpc_test.dart +++ /dev/null @@ -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 = [ - (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, - ); diff --git a/runtime/observatory/tests/service/client_resume_approvals_approve_then_disconnect_test.dart b/runtime/observatory/tests/service/client_resume_approvals_approve_then_disconnect_test.dart deleted file mode 100644 index af841611d35..00000000000 --- a/runtime/observatory/tests/service/client_resume_approvals_approve_then_disconnect_test.dart +++ /dev/null @@ -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 = [ - 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 main(args) => runIsolateTests( - args, - test, - testeeConcurrent: fooBar, - pause_on_start: true, - pause_on_exit: true, - enableService: false, - ); diff --git a/runtime/observatory/tests/service/client_resume_approvals_common.dart b/runtime/observatory/tests/service/client_resume_approvals_common.dart deleted file mode 100644 index 863ea803e4c..00000000000 --- a/runtime/observatory/tests/service/client_resume_approvals_common.dart +++ /dev/null @@ -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 setClientName(WebSocketVM client, String name) async => - await client.invokeRpc('setClientName', {'name': name}); - -Future createClient(WebSocketVM vm, - {String clientName = clientName}) async { - final client = WebSocketVM(vm.target); - await client.load(); - await setClientName(client, clientName); - return client; -} - -Future 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 resume(WebSocketVM vm, Isolate isolate) async => - await vm.invokeRpc('resume', { - 'isolateId': isolate.id, - }); - -Future isPausedAtStart(Isolate isolate) async { - await isolate.reload(); - return ((isolate.pauseEvent != null) && - isEventOfKind(isolate.pauseEvent, ServiceEvent.kPauseStart)); -} - -Future isPausedAtExit(Isolate isolate) async { - await isolate.reload(); - return ((isolate.pauseEvent != null) && - isEventOfKind(isolate.pauseEvent, ServiceEvent.kPauseExit)); -} - -Future isPausedPostRequest(Isolate isolate) async { - await isolate.reload(); - return ((isolate.pauseEvent != null) && - isEventOfKind(isolate.pauseEvent, ServiceEvent.kPausePostRequest)); -} - -Future waitForResume(Isolate isolate) async { - final completer = Completer(); - 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; -} diff --git a/runtime/observatory/tests/service/client_resume_approvals_disconnect_test.dart b/runtime/observatory/tests/service/client_resume_approvals_disconnect_test.dart deleted file mode 100644 index e94b52813dd..00000000000 --- a/runtime/observatory/tests/service/client_resume_approvals_disconnect_test.dart +++ /dev/null @@ -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 = [ - // 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 main(args) => runIsolateTests( - args, - test, - testeeConcurrent: fooBar, - pause_on_start: true, - pause_on_exit: true, - enableService: false, - ); diff --git a/runtime/observatory/tests/service/client_resume_approvals_identical_names_test.dart b/runtime/observatory/tests/service/client_resume_approvals_identical_names_test.dart deleted file mode 100644 index 4618441151a..00000000000 --- a/runtime/observatory/tests/service/client_resume_approvals_identical_names_test.dart +++ /dev/null @@ -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 = [ - // 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 main(args) => runIsolateTests( - args, - sameClientNamesTest, - testeeConcurrent: fooBar, - pause_on_start: true, - pause_on_exit: true, - enableService: false, - ); diff --git a/runtime/observatory/tests/service/client_resume_approvals_multiple_names_test.dart b/runtime/observatory/tests/service/client_resume_approvals_multiple_names_test.dart deleted file mode 100644 index 60a8282d16e..00000000000 --- a/runtime/observatory/tests/service/client_resume_approvals_multiple_names_test.dart +++ /dev/null @@ -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 = [ - // 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 main(args) => runIsolateTests( - args, - multipleClientNamesTest, - testeeConcurrent: fooBar, - pause_on_start: true, - pause_on_exit: true, - enableService: false, - ); diff --git a/runtime/observatory/tests/service/client_resume_approvals_name_change_test.dart b/runtime/observatory/tests/service/client_resume_approvals_name_change_test.dart deleted file mode 100644 index 3cd1618c9af..00000000000 --- a/runtime/observatory/tests/service/client_resume_approvals_name_change_test.dart +++ /dev/null @@ -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 = [ - // 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 main(args) => runIsolateTests( - args, - nameChangeTest, - testeeConcurrent: fooBar, - pause_on_start: true, - pause_on_exit: true, - enableService: false, - ); diff --git a/runtime/observatory/tests/service/client_resume_approvals_reload_test.dart b/runtime/observatory/tests/service/client_resume_approvals_reload_test.dart deleted file mode 100644 index 2e3d4e1c1c7..00000000000 --- a/runtime/observatory/tests/service/client_resume_approvals_reload_test.dart +++ /dev/null @@ -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 = [ - // 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 main(args) => runIsolateTests( - args, - hotReloadTest, - testeeConcurrent: fooBar, - pause_on_start: true, - enableService: false, - ); diff --git a/runtime/observatory/tests/service/dds_custom_stream_listen_test.dart b/runtime/observatory/tests/service/dds_custom_stream_listen_test.dart deleted file mode 100644 index cfa54620553..00000000000 --- a/runtime/observatory/tests/service/dds_custom_stream_listen_test.dart +++ /dev/null @@ -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 = [ - // 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, - ); diff --git a/runtime/observatory/tests/service/dds_extension_event_history_test.dart b/runtime/observatory/tests/service/dds_extension_event_history_test.dart deleted file mode 100644 index 0c3d1656331..00000000000 --- a/runtime/observatory/tests/service/dds_extension_event_history_test.dart +++ /dev/null @@ -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 = [ - isPausedAtStart, - resumeIsolate, - (Isolate isolate) async { - final completer = Completer(); - 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, - ); diff --git a/runtime/observatory/tests/service/dds_log_history_size_gigantic_test.dart b/runtime/observatory/tests/service/dds_log_history_size_gigantic_test.dart deleted file mode 100644 index d8cd47ae33e..00000000000 --- a/runtime/observatory/tests/service/dds_log_history_size_gigantic_test.dart +++ /dev/null @@ -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 getLogHistorySize(Isolate isolate) async { - final result = await isolate.invokeRpcNoUpgrade('getLogHistorySize', {}); - expect(result['type'], 'Size'); - return result['size'] as int; -} - -var tests = [ - 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(); - - 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, - ); diff --git a/runtime/observatory/tests/service/dds_log_history_size_simple_test.dart b/runtime/observatory/tests/service/dds_log_history_size_simple_test.dart deleted file mode 100644 index fc1e0df3de2..00000000000 --- a/runtime/observatory/tests/service/dds_log_history_size_simple_test.dart +++ /dev/null @@ -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 getLogHistorySize(Isolate isolate) async { - final result = await isolate.invokeRpcNoUpgrade('getLogHistorySize', {}); - expect(result['type'], 'Size'); - return result['size'] as int; -} - -var tests = [ - 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(); - - 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(); - - // 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, - ); diff --git a/runtime/observatory/tests/service/dds_log_history_size_test.dart b/runtime/observatory/tests/service/dds_log_history_size_test.dart deleted file mode 100644 index 5186f1ed38f..00000000000 --- a/runtime/observatory/tests/service/dds_log_history_size_test.dart +++ /dev/null @@ -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 getLogHistorySize(Isolate isolate) async { - final result = await isolate.invokeRpcNoUpgrade('getLogHistorySize', {}); - expect(result['type'], 'Size'); - return result['size'] as int; -} - -var tests = [ - 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(); - - 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, - ); diff --git a/runtime/observatory/tests/service/dds_stdout_stderr_history_test.dart b/runtime/observatory/tests/service/dds_stdout_stderr_history_test.dart deleted file mode 100644 index 2c3aaf07517..00000000000 --- a/runtime/observatory/tests/service/dds_stdout_stderr_history_test.dart +++ /dev/null @@ -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(); - 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 = [ - 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, - ); diff --git a/runtime/observatory/tests/service/get_client_name_rpc_test.dart b/runtime/observatory/tests/service/get_client_name_rpc_test.dart deleted file mode 100644 index e66ac4a8c1e..00000000000 --- a/runtime/observatory/tests/service/get_client_name_rpc_test.dart +++ /dev/null @@ -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 getClientName(Isolate isolate) async { - final result = await isolate.vm.invokeRpcNoUpgrade('getClientName', {}); - return result['name'] as String; -} - -Future setClientName(Isolate isolate, String name) async => - await isolate.vm.invokeRpcNoUpgrade('setClientName', { - 'name': name, - }); - -final test = [ - (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 main(args) => runIsolateTests( - args, - test, - testeeBefore: fooBar, - enableService: false, - );