blob: ecb74055b076d7ce9f8fb9c80d3af92036a83689 [file]
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:collection' show HashMap;
import 'dart:io' as io;
import 'dart:typed_data';
import 'package:path/path.dart' as p;
import 'package:test/test.dart';
import 'package:vm_service/vm_service.dart';
import 'package:vm_service_protos/vm_service_protos.dart' hide Frame;
import 'service_test_common.dart' as service_test_common;
import 'test_helper.dart'
show runIsolateTests, runIsolateTestsSynchronous, runVMTests;
abstract class _TestHarness<T> {
final List<String> args;
final String _scriptName;
final List<T> _tests = [];
final TestScriptParser _scriptParser;
_TestHarness(this._scriptName, this.args)
: _scriptParser = TestScriptParser(_scriptName);
List<T> get tests => _tests;
}
class VMTestHarness extends _TestHarness<VMTest> {
VMTestHarness(super.scriptName, super.args);
VMTestHarness addTest(VMTest test) {
_tests.add(test);
return this;
}
VMTestHarness addTestWithParser(VMTestWithParser test) {
_tests.add((service) => test(service, _scriptParser));
return this;
}
Future<void> run({
bool pauseOnStart = false,
bool pauseOnExit = false,
bool pauseOnUnhandledExceptions = false,
bool allowForNonZeroExitCode = false,
bool useAuthToken = false,
List<String>? extraArgs,
VmServiceFactory serviceFactory = VmService.defaultFactory,
required Future<void> Function(List<String> args) testeeMain,
}) =>
runVMTests(
args,
_tests,
_scriptName,
pauseOnStart: pauseOnStart,
pauseOnExit: pauseOnExit,
pauseOnUnhandledExceptions: pauseOnUnhandledExceptions,
extraArgs: extraArgs,
serviceFactory: serviceFactory,
testeeMain: testeeMain,
);
}
class IsolateTestHarness extends _TestHarness<IsolateTest> {
final List<String> _recordedStops = [];
IsolateTestHarness(super.scriptName, super.args);
IsolateTestHarness addCustomTestWithParser(IsolateTestWithParser test) {
_tests
.add((service, isolateRef) => test(service, isolateRef, _scriptParser));
return this;
}
IsolateTestHarness addCustomTest(IsolateTest test) {
_tests.add(test);
return this;
}
Future<void> run({
bool pauseOnStart = false,
bool pauseOnExit = false,
bool pauseOnUnhandledExceptions = false,
bool launchTesteeWithDartRunResident = false,
bool allowForNonZeroExitCode = false,
bool useAuthToken = false,
List<String>? extraArgs,
Map<String, String>? testeeEnvironment,
required Future<void> Function(List<String> args) testeeMain,
}) =>
runIsolateTests(
args,
_tests,
_scriptName,
pauseOnStart: pauseOnStart,
pauseOnExit: pauseOnExit,
pauseOnUnhandledExceptions: pauseOnUnhandledExceptions,
launchTesteeWithDartRunResident: launchTesteeWithDartRunResident,
allowForNonZeroExitCode: allowForNonZeroExitCode,
useAuthToken: useAuthToken,
extraArgs: extraArgs,
testeeEnvironment: testeeEnvironment,
testeeMain: testeeMain,
);
void runSync({
bool pauseOnStart = false,
bool pauseOnExit = false,
List<String>? extraArgs,
required Future<void> Function(List<String> args) testeeMain,
}) =>
runIsolateTestsSynchronous(
args,
_tests,
_scriptName,
pauseOnStart: pauseOnStart,
pauseOnExit: pauseOnExit,
extraArgs: extraArgs,
testeeMain: testeeMain,
);
IsolateTestHarness stoppedAtLine(String lineTag) {
_tests.add(
service_test_common.stoppedAtLine(_scriptParser.lineForTag(lineTag)));
return this;
}
IsolateTestHarness stoppedAtLineColumnWithTag({
required String lineTag,
int? column,
}) {
_tests.add(service_test_common.stoppedAtLineColumn(
line: _scriptParser.lineForTag(lineTag), column: column));
return this;
}
IsolateTestHarness setBreakpointAtLine(String lineTag) {
_tests.add(service_test_common.setBreakpointAtUriAndLine(
_scriptName, _scriptParser.lineForTag(lineTag)));
return this;
}
IsolateTestHarness setBreakpointAtUriAndLine(String uri, String lineTag) {
_tests.add((service, isolateRef) async {
final resolvedUri = uri.startsWith('package:')
? (await service.lookupResolvedPackageUris(isolateRef.id!, [uri]))
.uris![0]!
: uri;
return service_test_common.setBreakpointAtUriAndLine(
uri, _scriptParser.lineForTag(lineTag, script: resolvedUri))(
service, isolateRef);
});
return this;
}
IsolateTestHarness setBreakpointAtLineColumn(String lineTag, int column) {
_tests.add(service_test_common.setBreakpointAtUriLineColumn(
_scriptName, _scriptParser.lineForTag(lineTag), column));
return this;
}
IsolateTestHarness hasStoppedAtBreakpoint() {
_tests.add(service_test_common.hasStoppedAtBreakpoint);
return this;
}
IsolateTestHarness hasPausedAtStart() {
_tests.add(service_test_common.hasPausedAtStart);
return this;
}
IsolateTestHarness hasStoppedPostRequest() {
_tests.add(service_test_common.hasStoppedPostRequest);
return this;
}
IsolateTestHarness hasStoppedWithUnhandledException() {
_tests.add(service_test_common.hasStoppedWithUnhandledException);
return this;
}
IsolateTestHarness hasStoppedAtExit() {
_tests.add(service_test_common.hasStoppedAtExit);
return this;
}
IsolateTestHarness markDartColonLibrariesDebuggable() {
_tests.add(service_test_common.markDartColonLibrariesDebuggable);
return this;
}
IsolateTestHarness reloadSources({bool pause = false}) {
_tests.add(service_test_common.reloadSources(pause: pause));
return this;
}
IsolateTestHarness hasLocalVarInTopStackFrame(String varName) {
_tests.add(service_test_common.hasLocalVarInTopStackFrame(varName));
return this;
}
IsolateTestHarness stoppedInFunction(String functionName) {
_tests.add(service_test_common.stoppedInFunction(functionName));
return this;
}
IsolateTestHarness stepOver() {
_tests.add(service_test_common.stepOver);
return this;
}
IsolateTestHarness stepInto() {
_tests.add(service_test_common.stepInto);
return this;
}
IsolateTestHarness stepOut() {
_tests.add(service_test_common.stepOut);
return this;
}
IsolateTestHarness smartNext() {
_tests.add(service_test_common.smartNext);
return this;
}
IsolateTestHarness asyncNext() {
_tests.add(service_test_common.asyncNext);
return this;
}
IsolateTestHarness syncNext() {
_tests.add(service_test_common.syncNext);
return this;
}
IsolateTestHarness resumeIsolate() {
_tests.add(service_test_common.resumeIsolate);
return this;
}
IsolateTestHarness resumeProgramRecordingStops(
bool includeCaller,
) {
_tests.add(service_test_common.resumeProgramRecordingStops(
_recordedStops, includeCaller));
return this;
}
IsolateTestHarness runStepThroughProgramRecordingStops() {
_tests.add(service_test_common
.runStepThroughProgramRecordingStops(_recordedStops));
return this;
}
IsolateTestHarness runStepIntoThroughProgramRecordingStops() {
_tests.add(service_test_common
.runStepIntoThroughProgramRecordingStops(_recordedStops));
return this;
}
IsolateTestHarness checkRecordedStops({
bool removeDuplicates = false,
bool debugPrint = false,
String? debugPrintFile,
int? debugPrintLine,
}) {
final goldenUri =
io.Platform.script.resolve('${io.Platform.script.path}.stops');
_tests.add(service_test_common.checkRecordedStops(
_recordedStops, goldenUri.toFilePath(),
removeDuplicates: removeDuplicates,
debugPrint: debugPrint,
debugPrintFile: debugPrintFile,
debugPrintLine: debugPrintLine,
updateGoldens: args.contains('--update-goldens')));
return this;
}
IsolateTestHarness validateRecordedStops(
void Function(List<String> recordedStops) validator,
) {
_tests.add((service, isolateRef) async {
validator(_recordedStops);
});
return this;
}
}
class TestScriptParser {
final String _mainScript;
final Map<String, _ParserFileData> _fileData = {};
TestScriptParser(this._mainScript);
int lineForTag(String lineTag, {String? script}) {
script ??= _mainScript;
return _fileData
.putIfAbsent(script, () => _ParserFileData(script!))
.lineForTag(lineTag);
}
int lineForRegExp(RegExp regExp, {String? script}) {
script ??= _mainScript;
return _fileData
.putIfAbsent(script, () => _ParserFileData(script!))
.lineForRegExp(regExp);
}
int offsetForTag(String lineTag, {String? script}) {
script ??= _mainScript;
return _fileData
.putIfAbsent(script, () => _ParserFileData(script!))
.offsetForTag(lineTag);
}
}
class _ParserFileData {
final String script;
late final Map<String, int> _lineTags = _generateLineTags();
late final Map<String, int> _offsetTags = _generateOffsetTags();
static final RegExp _lineEndTagRegex = RegExp(r'// LINE_(\S+)$');
static final RegExp _lineAnyTagRegex = RegExp(r'/\* LINE_(\S+) \*/');
static final RegExp _offsetTagRegex = RegExp(r'/\* OFFSET_(\S+) \*/');
static final RegExp _nonWhitespaceRegex = RegExp(r'\S');
_ParserFileData(this.script);
Map<String, int> _generateOffsetTags() {
var text =
io.File.fromUri(io.Platform.script.resolve(script)).readAsStringSync();
final offsetTags = <String, int>{};
RegExpMatch? match;
int offsetCounter = 0;
while ((match = _offsetTagRegex.firstMatch(text)) != null) {
final matchString = match!.group(0)!;
final offsetTag = matchString.substring(3, matchString.length - 3);
final nextCharOffset = text.indexOf(_nonWhitespaceRegex, match.end);
offsetTags[offsetTag] = nextCharOffset + offsetCounter;
text = text.substring(match.end);
offsetCounter += match.end;
}
return offsetTags;
}
Map<String, int> _generateLineTags() {
final lines =
io.File.fromUri(io.Platform.script.resolve(script)).readAsLinesSync();
final lineTags = <String, int>{};
int lineNum = 1;
for (final line in lines) {
if (_lineEndTagRegex.firstMatch(line) case final match?) {
final lineTag = match.group(0)!.substring(3);
lineTags[lineTag] = lineNum;
} else if (_lineAnyTagRegex.firstMatch(line) case final match?) {
final matchString = match.group(0)!;
final lineTag = matchString.substring(3, matchString.length - 3);
lineTags[lineTag] = lineNum;
}
lineNum++;
}
return lineTags;
}
int lineForRegExp(RegExp regExp) {
final lines =
io.File.fromUri(io.Platform.script.resolve(script)).readAsLinesSync();
int lineNum = 1;
for (final line in lines) {
if (regExp.hasMatch(line)) {
return lineNum;
}
lineNum++;
}
throw 'RegExp $regExp not found in $script';
}
int lineForTag(String lineTag) {
final line = _lineTags[lineTag];
if (line == null) {
throw 'Line tag $lineTag not found in $script';
}
return line;
}
int offsetForTag(String offsetTag) {
final offset = _offsetTags[offsetTag];
if (offset == null) {
throw 'Offset tag $offsetTag not found in $script';
}
return offset;
}
}
typedef IsolateTest = Future<void> Function(
VmService service,
IsolateRef isolate,
);
typedef IsolateTestWithParser = Future<void> Function(
VmService service,
IsolateRef isolate,
TestScriptParser scriptParser,
);
typedef VMTest = Future<void> Function(VmService service);
typedef VMTestWithParser = Future<void> Function(
VmService service, TestScriptParser scriptParser);
Future<void> smartNext(VmService service, IsolateRef isolateRef) async {
print('smartNext');
final isolate = await service.getIsolate(isolateRef.id!);
final Event event = isolate.pauseEvent!;
if (event.kind == EventKind.kPauseBreakpoint) {
// TODO(bkonyi): remove needless refetching of isolate object.
if (event.atAsyncSuspension ?? false) {
return asyncNext(service, isolateRef);
} else {
return syncNext(service, isolateRef);
}
} else {
throw 'The program is already running';
}
}
Future<void> asyncNext(VmService service, IsolateRef isolateRef) async {
print('asyncNext');
final id = isolateRef.id!;
final isolate = await service.getIsolate(id);
final event = isolate.pauseEvent!;
if (event.kind == EventKind.kPauseBreakpoint) {
final dynamic event = isolate.pauseEvent;
if (!event.atAsyncSuspension) {
throw 'No async continuation at this location';
} else {
await service.resume(id, step: 'OverAsyncSuspension');
}
} else {
throw 'The program is already running';
}
}
Future<void> syncNext(VmService service, IsolateRef isolateRef) async {
print('syncNext');
final id = isolateRef.id!;
final isolate = await service.getIsolate(id);
final event = isolate.pauseEvent!;
if (event.kind == EventKind.kPauseBreakpoint) {
await service.resume(id, step: 'Over');
} else {
throw 'The program is already running';
}
}
// WARNING: interleaving calls based on hasPausedFor using Future.wait() may
// cause the debug stream to be cancelled after one of the checks completes.
// If another check is waiting on an event, it will no longer be notified of
// the event, causing the test to hang.
Future<void> hasPausedFor(
VmService service,
IsolateRef isolateRef,
String kind,
) async {
final completer = Completer<void>();
late StreamSubscription<Event> subscription;
bool completed = false;
// Synchronously guard the entry to complete() before any async yields to
// prevent concurrent execution paths (e.g., the stream listener and
// getIsolate fallback) from double-completing it and throwing a StateError.
Future<void> complete() async {
if (completed) return;
completed = true;
try {
await subscription.cancel();
await _unsubscribeDebugStream(service);
} catch (_) {}
completer.complete();
}
subscription = service.onDebugEvent.listen((event) {
if ((isolateRef.id == event.isolate!.id) && (event.kind == kind)) {
unawaited(complete());
}
});
await _subscribeDebugStream(service);
// Pause may have happened before we subscribed.
final id = isolateRef.id!;
final isolate = await service.getIsolate(id);
final event = isolate.pauseEvent!;
if (event.kind == kind) {
await complete();
}
return completer.future; // Will complete when breakpoint hit.
}
// WARNING: interleaving calls based on hasPausedFor using Future.wait() may
// cause the debug stream to be cancelled after one of the checks completes.
// If another check is waiting on an event, it will no longer be notified of
// the event, causing the test to hang.
Future<void> hasStoppedAtBreakpoint(VmService service, IsolateRef isolate) {
return hasPausedFor(service, isolate, EventKind.kPauseBreakpoint);
}
// WARNING: interleaving calls based on hasPausedFor using Future.wait() may
// cause the debug stream to be cancelled after one of the checks completes.
// If another check is waiting on an event, it will no longer be notified of
// the event, causing the test to hang.
Future<void> hasStoppedPostRequest(VmService service, IsolateRef isolate) {
return hasPausedFor(service, isolate, EventKind.kPausePostRequest);
}
// WARNING: interleaving calls based on hasPausedFor using Future.wait() may
// cause the debug stream to be cancelled after one of the checks completes.
// If another check is waiting on an event, it will no longer be notified of
// the event, causing the test to hang.
Future<void> hasStoppedWithUnhandledException(
VmService service,
IsolateRef isolate,
) {
return hasPausedFor(service, isolate, EventKind.kPauseException);
}
// WARNING: interleaving calls based on hasPausedFor using Future.wait() may
// cause the debug stream to be cancelled after one of the checks completes.
// If another check is waiting on an event, it will no longer be notified of
// the event, causing the test to hang.
Future<void> hasStoppedAtExit(VmService service, IsolateRef isolate) {
return hasPausedFor(service, isolate, EventKind.kPauseExit);
}
// WARNING: interleaving calls based on hasPausedFor using Future.wait() may
// cause the debug stream to be cancelled after one of the checks completes.
// If another check is waiting on an event, it will no longer be notified of
// the event, causing the test to hang.
Future<void> hasPausedAtStart(VmService service, IsolateRef isolate) {
return hasPausedFor(service, isolate, EventKind.kPauseStart);
}
Future<void> markDartColonLibrariesDebuggable(
VmService service,
IsolateRef isolateRef,
) async {
final isolateId = isolateRef.id!;
final isolate = await service.getIsolate(isolateId);
final requests = <Future>[];
for (final libRef in isolate.libraries!) {
final lib = await service.getObject(isolateId, libRef.id!) as Library;
if (lib.uri!.startsWith('dart:') && !lib.uri!.startsWith('dart:_')) {
requests.add(service.setLibraryDebuggable(isolateId, lib.id!, true));
}
}
await Future.wait(requests);
}
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 setBreakpointAtUriLineColumn(String uri, int line, int column) {
return (VmService service, IsolateRef isolateRef) async {
print('Setting breakpoint for line $line column $column');
final Breakpoint bpt = await service
.addBreakpointWithScriptUri(isolateRef.id!, uri, line, column: column);
print('Breakpoint is $bpt');
expect(bpt, isNotNull);
};
}
extension BreakpointLocation on Breakpoint {
Future<(String uri, (int line, int column))> getLocation(
VmService service,
IsolateRef isolateRef,
) async {
if (location?.tokenPos == null) {
return ('<unknown>', (-1, -1));
}
final script = (await service.getObject(
isolateRef.id!,
location!.script!.id!,
)) as Script;
return (
script.uri!,
(
script.getLineNumberFromTokenPos(location!.tokenPos!) ?? -1,
script.getColumnNumberFromTokenPos(location!.tokenPos!) ?? -1,
),
);
}
}
extension FrameLocation on Frame {
Future<(String uri, (int line, int column))> getLocation(
VmService service,
IsolateRef isolateRef,
) async {
if (location?.tokenPos == null) {
return ('<unknown>', (-1, -1));
}
final script = (await service.getObject(
isolateRef.id!,
location!.script!.id!,
)) as Script;
return (
script.uri!,
(
script.getLineNumberFromTokenPos(location!.tokenPos!) ?? -1,
script.getColumnNumberFromTokenPos(location!.tokenPos!) ?? -1,
),
);
}
}
Future<String> formatFrames(
VmService service,
IsolateRef isolateRef,
List<Frame> frames,
) async {
final sb = StringBuffer();
for (Frame f in frames) {
sb.write(' $f');
if (f.function case final funcRef?) {
sb.write(' ');
sb.write(await qualifiedFunctionName(service, isolateRef, funcRef));
}
if (f.location != null) {
final (uri, (lineNo)) = await f.getLocation(service, isolateRef);
sb.write(' $uri:$lineNo');
}
sb.writeln();
}
return sb.toString();
}
Future<String> formatStack(
VmService service,
IsolateRef isolateRef,
Stack stack,
) async {
final sb = StringBuffer();
sb.write('Full stack trace:\n');
sb.writeln(await formatFrames(service, isolateRef, stack.frames!));
if (stack.asyncCausalFrames case final asyncFrames?) {
sb.write('\nFull async stack trace:\n');
sb.writeln(await formatFrames(service, isolateRef, asyncFrames));
}
return sb.toString();
}
/// If column is [null], this function checks that the isolate under test is
/// currently paused at line [line]. Otherwise, this function checks that the
/// isolate under test is currently paused at the location specified by [line]
/// and [column].
IsolateTest stoppedAtLineColumn({required int line, int? column}) {
return (VmService service, IsolateRef isolateRef) async {
if (column == null) {
print('Checking we are at line $line');
} else {
print('Checking we are at $line:$column');
}
// 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 (_, (actualLine, actualColumn)) = await top.getLocation(
service,
isolateRef,
);
if (actualLine != line) {
final sb = StringBuffer();
sb.writeln(
'Expected to be at line $line but actually at line $actualLine',
);
sb.writeln(await formatStack(service, isolateRef, stack));
throw sb.toString();
} else if (column != null && actualColumn != column) {
final sb = StringBuffer();
sb.writeln(
'Expected to be at $line:$column but actually at $line:$actualColumn',
);
sb.writeln(await formatStack(service, isolateRef, stack));
throw sb.toString();
} else {
print('Program is stopped at $actualLine:$actualColumn');
}
};
}
IsolateTest stoppedAtLine(int line) {
return stoppedAtLineColumn(line: line);
}
Future<void> resumeIsolate(VmService service, IsolateRef isolate) async {
Completer<void>? completer = Completer<void>();
// Capture the future synchronously before any async yields or nullification
// to ensure the caller always awaits the actual completion of the cleanup
// tasks.
final future = completer.future;
late StreamSubscription<Event> subscription;
bool cancelStreamAfterResume = false;
// Synchronously nullify the completer before any async yields to prevent
// concurrent execution paths (e.g., multiple stream events) from
// double-completing it and throwing a StateError.
Future<void> complete() async {
if (completer != null) {
final c = completer!;
completer = null; // Synchronously set to null
if (cancelStreamAfterResume) {
await _unsubscribeDebugStream(service);
}
await subscription.cancel();
c.complete();
}
}
subscription = service.onDebugEvent.listen((event) {
if (event.kind == EventKind.kResume && event.isolate?.id == isolate.id) {
complete();
}
});
cancelStreamAfterResume = await _subscribeDebugStream(service);
await service.resume(isolate.id!);
return future;
}
Future<bool> _subscribeDebugStream(VmService service) async {
try {
await service.streamListen(EventStreams.kDebug);
return true;
} catch (_) {
/* swallow exception */
return false;
}
}
Future<void> _unsubscribeDebugStream(VmService service) async {
try {
await service.streamCancel(EventStreams.kDebug);
} catch (_) {
/* swallow exception */
}
}
Future<void> resumeAndAwaitEvent(
VmService service,
IsolateRef isolateRef,
String streamId,
Function(Event) onEvent,
) async {
final completer = Completer<void>();
late final StreamSubscription sub;
sub = service.onEvent(streamId).listen((event) async {
await onEvent(event);
await sub.cancel();
await service.streamCancel(streamId);
completer.complete();
});
await service.streamListen(streamId);
await service.resume(isolateRef.id!);
return completer.future;
}
IsolateTest resumeIsolateAndAwaitEvent(
String streamId,
Function(Event) onEvent,
) {
return (VmService service, IsolateRef isolate) async =>
resumeAndAwaitEvent(service, isolate, streamId, onEvent);
}
Future<void> stepOver(VmService service, IsolateRef isolateRef) async {
await _subscribeDebugStream(service);
await service.resume(isolateRef.id!, step: 'Over');
await hasStoppedAtBreakpoint(service, isolateRef);
await _unsubscribeDebugStream(service);
}
Future<void> stepInto(VmService service, IsolateRef isolateRef) async {
await _subscribeDebugStream(service);
await service.resume(isolateRef.id!, step: 'Into');
await hasStoppedAtBreakpoint(service, isolateRef);
await _unsubscribeDebugStream(service);
}
Future<void> stepOut(VmService service, IsolateRef isolateRef) async {
await _subscribeDebugStream(service);
await service.resume(isolateRef.id!, step: 'Out');
await hasStoppedAtBreakpoint(service, isolateRef);
await _unsubscribeDebugStream(service);
}
IsolateTest resumeProgramRecordingStops(
List<String> recordStops,
bool includeCaller,
) {
return (VmService service, IsolateRef isolateRef) async {
final completer = Completer<void>();
late StreamSubscription subscription;
subscription = service.onDebugEvent.listen((event) async {
if (event.kind == EventKind.kPauseBreakpoint) {
final stack = await service.getStack(isolateRef.id!);
expect(stack.frames!.length, greaterThanOrEqualTo(2));
String brokeAt = await _locationToString(
service,
isolateRef,
stack.frames![0],
);
if (includeCaller) {
brokeAt =
'$brokeAt (${await _locationToString(service, isolateRef, stack.frames![1])})';
}
recordStops.add(brokeAt);
await service.resume(isolateRef.id!);
} else if (event.kind == EventKind.kPauseExit) {
await subscription.cancel();
await service.streamCancel(EventStreams.kDebug);
completer.complete();
}
});
await service.streamListen(EventStreams.kDebug);
await service.resume(isolateRef.id!);
return completer.future;
};
}
Future<String> _locationToString(
VmService service,
IsolateRef isolateRef,
Frame frame,
) async {
final buffer = StringBuffer();
final location = frame.location!;
final script =
await service.getObject(isolateRef.id!, location.script!.id!) as Script;
final scriptName = p.basename(script.uri!);
buffer.write(scriptName);
final tokenPos = location.tokenPos!;
final line = script.getLineNumberFromTokenPos(tokenPos);
if (line != null) {
buffer.write(':$line');
final column = script.getColumnNumberFromTokenPos(tokenPos);
if (column != null) {
buffer.write(':$column');
}
}
return buffer.toString();
}
IsolateTest runStepThroughProgramRecordingStops(List<String> recordStops) {
return (VmService service, IsolateRef isolateRef) async {
final completer = Completer<void>();
late StreamSubscription subscription;
subscription = service.onDebugEvent.listen((event) async {
if (event.kind == EventKind.kPauseBreakpoint) {
final isolate = await service.getIsolate(isolateRef.id!);
final frame = isolate.pauseEvent!.topFrame!;
recordStops.add(await _locationToString(service, isolateRef, frame));
if (event.atAsyncSuspension ?? false) {
await service.resume(
isolateRef.id!,
step: StepOption.kOverAsyncSuspension,
);
} else {
await service.resume(isolateRef.id!, step: StepOption.kOver);
}
} else if (event.kind == EventKind.kPauseExit) {
await subscription.cancel();
await service.streamCancel(EventStreams.kDebug);
completer.complete();
}
});
await service.streamListen(EventStreams.kDebug);
await service.resume(isolateRef.id!);
return completer.future;
};
}
IsolateTest runStepIntoThroughProgramRecordingStops(List<String> recordStops) {
return (VmService service, IsolateRef isolateRef) async {
final completer = Completer<void>();
late StreamSubscription subscription;
subscription = service.onDebugEvent.listen((event) async {
if (event.kind == EventKind.kPauseBreakpoint) {
final isolate = await service.getIsolate(isolateRef.id!);
final frame = isolate.pauseEvent!.topFrame!;
recordStops.add(await _locationToString(service, isolateRef, frame));
await service.resume(isolateRef.id!, step: StepOption.kInto);
} else if (event.kind == EventKind.kPauseExit) {
await subscription.cancel();
await service.streamCancel(EventStreams.kDebug);
completer.complete();
}
});
await service.streamListen(EventStreams.kDebug);
await service.resume(isolateRef.id!);
return completer.future;
};
}
IsolateTest checkRecordedStops(
List<String> recordStops,
String goldenFileName, {
bool removeDuplicates = false,
bool debugPrint = false,
String? debugPrintFile,
int? debugPrintLine,
bool updateGoldens = false,
}) {
String formatLine(String line) {
String output = line;
if (debugPrintFile != null && debugPrintLine != null) {
final int firstColon = line.indexOf(':');
final int lastColon = line.lastIndexOf(':');
if (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';
}
}
}
return output;
}
return (VmService service, IsolateRef isolate) async {
var updatedStops =
recordStops.takeWhile((s) => !s.contains('test_helper.dart')).toList();
if (debugPrint) {
for (int i = 0; i < updatedStops.length; i++) {
final line = updatedStops[i];
final output = formatLine(line);
final String comma = i == updatedStops.length - 1 ? '' : ',';
print("'$output'$comma");
}
}
if (removeDuplicates) {
updatedStops = removeAdjacentDuplicates(updatedStops);
}
print('Loading golden file from: $goldenFileName');
final goldenFile = io.File(goldenFileName);
if (updateGoldens) {
goldenFile.writeAsStringSync(
'// This file is generated by running the associated test with '
'--update-goldens. Do not edit this file directly.\n\n'
'${updatedStops.join('\n')}\n',
);
print('Updated golden file: ${goldenFile.path}');
return;
}
if (!goldenFile.existsSync()) {
throw 'Golden file not found: ${goldenFile.path}. Run with '
'--update-goldens to generate.';
}
// Skip the first 3 lines which are comments.
List<String> expectedStops = goldenFile.readAsLinesSync().skip(3).toList();
if (removeDuplicates) {
expectedStops = removeAdjacentDuplicates(expectedStops);
}
int i = 0;
int j = 0;
while (i < updatedStops.length && j < expectedStops.length) {
if (updatedStops[i] != expectedStops[j]) {
int k = i + 1;
while (k < updatedStops.length && updatedStops[k] != expectedStops[j]) {
k++;
}
if (k < updatedStops.length) {
if (debugPrint) {
print('Skipping recorded stops [$i, $k)');
}
i = k;
} else {
expect(
formatLine(updatedStops[i]),
formatLine(expectedStops[j]),
reason: 'Recorded stop $i does not match expected stop $j. '
'To regenerate golden, run with test with --update-goldens.',
);
}
}
if (debugPrint) {
print(
'Recorded stop $i matches expected stop $j: '
'${formatLine(updatedStops[i])}',
);
}
i++;
j++;
}
expect(
updatedStops.length >= expectedStops.length,
true,
reason: 'Expects at least ${expectedStops.length} breaks, '
'got ${updatedStops.length}. To regenerate golden, run with test '
'with --update-goldens.',
);
};
}
List<String> removeAdjacentDuplicates(List<String> fromList) {
final List<String> result = <String>[];
String? latestLine;
for (String s in fromList) {
if (s == latestLine) continue;
latestLine = s;
result.add(s);
}
return result;
}
typedef ServiceExtensionHandler = Future<Map<String, dynamic>> Function(
Map<String, dynamic> cb,
);
/// Registers a service extension and returns the actual service name used to
/// invoke the service.
Future<String> registerServiceHelper(
VmService primaryClient,
VmService serviceRegisterClient,
String serviceName,
ServiceExtensionHandler callback,
) async {
final serviceNameCompleter = Completer<String>();
late final StreamSubscription sub;
sub = primaryClient.onServiceEvent.listen((event) {
if (event.kind == EventKind.kServiceRegistered &&
event.method!.endsWith(serviceName)) {
serviceNameCompleter.complete(event.method!);
sub.cancel();
}
});
// TODO(bkonyi): if we end up in a situation where this call throws due to a
// prior subscription to the Service stream, we should do something similar
// to _subscribeDebugStream in this method.
await primaryClient.streamListen(EventStreams.kService);
// Register the service.
serviceRegisterClient.registerServiceCallback(serviceName, callback);
await serviceRegisterClient.registerService(serviceName, serviceName);
// Wait for the service registered event on the non-registering client to get
// the actual service name.
final actualServiceName = await serviceNameCompleter.future;
print("Service '$serviceName' registered as '$actualServiceName'");
await primaryClient.streamCancel(EventStreams.kService);
return actualServiceName;
}
Future<void> evaluateInFrameAndExpect(
VmService service,
String isolateId,
String expression,
String expected, {
Map<String, String>? scope,
String? kind,
int topFrame = 0,
}) async {
final result = await service.evaluateInFrame(
isolateId,
topFrame,
expression,
scope: scope,
) as InstanceRef;
expect(result.valueAsString, expected);
if (kind != null) {
expect(result.kind!, kind);
}
}
Future<void> evaluateAndExpect(
VmService service,
String isolateId,
String targetId,
String expression,
String expected, {
Map<String, String>? scope,
String? kind,
}) async {
final result = await service.evaluate(
isolateId,
targetId,
expression,
scope: scope,
) as InstanceRef;
expect(result.valueAsString, expected);
if (kind != null) {
expect(result.kind!, kind);
}
}
Future<HeapSnapshotGraph> fetchHeapSnapshot(
VmService service,
IsolateRef isolateRef,
) async {
final isolateId = isolateRef.id!;
final completer = Completer<void>();
late final StreamSubscription sub;
final data = <ByteData>[];
sub = service.onHeapSnapshotEvent.listen((event) async {
data.add(event.data!);
if (event.last == true) {
await sub.cancel();
await service.streamCancel(EventStreams.kHeapSnapshot);
completer.complete();
}
});
await service.streamListen(EventStreams.kHeapSnapshot);
await service.requestHeapSnapshot(isolateId);
await completer.future;
return HeapSnapshotGraph.fromChunks(data);
}
IsolateTest reloadSources({bool pause = false}) {
return (VmService service, IsolateRef isolateRef) async {
await service.reloadSources(isolateRef.id!, pause: pause);
};
}
IsolateTest hasLocalVarInTopStackFrame(String varName) {
return (VmService service, IsolateRef isolateRef) async {
print("Checking we have variable '$varName' in the top frame");
final isolateId = isolateRef.id!;
// Make sure that the isolate has stopped.
final isolate = await service.getIsolate(isolateId);
expect(isolate.pauseEvent, isNotNull);
expect(isolate.pauseEvent!.kind, isNot(EventKind.kResume));
final stack = await service.getStack(isolateId);
final frames = stack.frames!;
expect(frames.length, greaterThanOrEqualTo(1));
final top = frames[0];
final vars = top.vars!;
for (final variable in vars) {
if (variable.name == varName) {
return;
}
}
final sb = StringBuffer();
sb.write('Expected to find $varName in top awaiter stack frame, found ');
if (vars.isEmpty) {
sb.writeln('no variables');
} else {
sb.writeln('these instead:');
for (var variable in vars) {
sb.writeln('\t${variable.name}');
}
}
throw sb.toString();
};
}
IsolateTest stoppedInFunction(String functionName) {
return (VmService service, IsolateRef isolateRef) async {
print('Checking we are in function: $functionName');
final isolateId = isolateRef.id!;
final stack = await service.getStack(isolateId);
final frames = stack.frames!;
expect(frames, isNotEmpty);
final topFrame = frames[0];
final function =
await service.getObject(isolateId, topFrame.function!.id!) as Func;
final name = function.name!;
if (name != functionName) {
final sb = StringBuffer();
sb.writeln(
'Expected to be in function $functionName but '
'actually in function $name',
);
sb.writeln(await formatStack(service, isolateRef, stack));
throw sb.toString();
} else {
print('Program is stopped in function: $functionName');
}
};
}
Future<String> qualifiedFunctionName(
VmService service,
IsolateRef isolate,
FuncRef func,
) async {
final funcName = func.name ?? '<unknown>';
switch (func.owner) {
case final FuncRef parentFuncRef:
final parentFuncName = await qualifiedFunctionName(
service,
isolate,
parentFuncRef,
);
return '$parentFuncName.$funcName';
case final ClassRef parentClass:
return '${parentClass.name!}.$funcName';
case _:
return funcName;
}
}
Future<void> expectFrame(
VmService service,
IsolateRef isolate,
Frame frame, {
String kind = 'Regular',
String? functionName,
int? line,
}) async {
expect(frame.kind, equals(kind));
if (functionName != null) {
expect(
await qualifiedFunctionName(service, isolate, frame.function!),
equals(functionName),
);
}
if (line != null) {
expect(frame.location, isNotNull);
final script = await service.getObject(
isolate.id!,
frame.location!.script!.id!,
) as Script;
expect(
script.getLineNumberFromTokenPos(frame.location!.tokenPos!),
equals(line),
);
}
}
Future<String> getCurrentExceptionAsString(
VmService service,
IsolateRef isolateRef,
) async {
final isolate = await service.getIsolate(isolateRef.id!);
final event = isolate.pauseEvent!;
final exception =
await service.getObject(isolateRef.id!, event.exception!.id!) as Instance;
return exception.valueAsString!;
}
typedef ExpectedFrame = ({String? functionName, int? line});
const ExpectedFrame asyncGap = (functionName: null, line: null);
IsolateTest resumePastUnhandledException(String exceptionAsString) {
return (service, isolateRef) async {
do {
await resumeIsolate(service, isolateRef);
await hasStoppedWithUnhandledException(service, isolateRef);
} while (await getCurrentExceptionAsString(service, isolateRef) ==
exceptionAsString);
};
}
IsolateTest expectUnhandledExceptionWithFrames({
List<ExpectedFrame>? expectedFrames,
String? exceptionAsString,
}) {
return (VmService service, IsolateRef isolateRef) async {
await hasStoppedWithUnhandledException(service, isolateRef);
if (exceptionAsString != null) {
expect(
await getCurrentExceptionAsString(service, isolateRef),
equals(exceptionAsString),
);
}
if (expectedFrames == null) {
return;
}
final stack = await service.getStack(isolateRef.id!);
final frames = stack.asyncCausalFrames!;
var currentKind = 'Regular';
for (var i = 0; i < expectedFrames.length; i++) {
final expected = expectedFrames[i];
final got = frames[i];
await expectFrame(
service,
isolateRef,
got,
kind: expected == asyncGap ? 'AsyncSuspensionMarker' : currentKind,
functionName: expected.functionName,
line: expected.line,
);
if (expected == asyncGap) {
currentKind = 'AsyncCausal';
}
}
};
}
/// This helper does 3 things:
/// 1) makes sure it's at the expected frame ([topFrameName]).
/// 2) checks that the expected variables are available (by name)
/// ([availableVariables]).
/// 3) Evaluates the given expression(s) and checks their (valueAsString) result
/// ([evaluations]).
IsolateTest testExpressionEvaluationAndAvailableVariables(
String topFrameName,
List<String> availableVariables,
List<(String evaluate, String evaluationResult)> evaluations,
) {
return (VmService service, IsolateRef isolateRef) async {
final isolateId = isolateRef.id!;
final stack = await service.getStack(isolateId);
// Make sure we are in the right place.
expect(stack.frames!.length, greaterThanOrEqualTo(1));
expect(stack.frames![0].function!.name, topFrameName);
// Check variables.
expect(
(stack.frames![0].vars ?? []).map((v) => v.name).toList(),
equals(availableVariables),
);
// Evaluate.
for (final (expression, expectedResult) in evaluations) {
final result = await service.evaluateInFrame(
isolateId,
/* frame = */ 0,
expression,
) as InstanceRef;
print(result.valueAsString);
expect(result.valueAsString, equals(expectedResult));
}
};
}
Map<String, String> mapFromListOfDebugAnnotations(
List<DebugAnnotation> debugAnnotations,
) {
return HashMap.fromEntries(
debugAnnotations.map((a) {
if (a.hasStringValue()) {
return MapEntry(a.name, a.stringValue);
} else if (a.hasLegacyJsonValue()) {
return MapEntry(a.name, a.legacyJsonValue);
} else {
throw 'We should not be writing annotations without values';
}
}),
);
}
int computeTimeOriginNanos(List<TracePacket> packets) {
final packetsWithPerfSamples =
packets.where((packet) => packet.hasPerfSample()).toList();
if (packetsWithPerfSamples.isEmpty) {
return 0;
}
int smallest = packetsWithPerfSamples.first.timestamp.toInt();
for (int i = 0; i < packetsWithPerfSamples.length; i++) {
if (packetsWithPerfSamples[i].timestamp < smallest) {
smallest = packetsWithPerfSamples[i].timestamp.toInt();
}
}
return smallest;
}
int computeTimeExtentNanos(List<TracePacket> packets, int timeOrigin) {
final packetsWithPerfSamples =
packets.where((packet) => packet.hasPerfSample()).toList();
if (packetsWithPerfSamples.isEmpty) {
return 0;
}
int largestExtent = packetsWithPerfSamples[0].timestamp.toInt() - timeOrigin;
for (var i = 0; i < packetsWithPerfSamples.length; i++) {
final int duration =
packetsWithPerfSamples[i].timestamp.toInt() - timeOrigin;
if (duration > largestExtent) {
largestExtent = duration;
}
}
return largestExtent;
}
Iterable<PerfSample> extractPerfSamplesFromTracePackets(
List<TracePacket> packets,
) {
return packets
.where((packet) => packet.hasPerfSample())
.map((packet) => packet.perfSample);
}