// 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. 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'); } }; }