25bd38bbe2
I'm struggling to reproduce the flakes noted in https://github.com/dart-lang/sdk/issues/60187 so hopefully verbose logs will help understand what's wrong. Change-Id: Id76b30b4efc9fb63cfacd01d2df6dc5a8ea4c7eb Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/411681 Commit-Queue: Derek Xu <derekx@google.com> Reviewed-by: Derek Xu <derekx@google.com> Reviewed-by: Jessy Yameogo <yjessy@google.com>
795 lines
29 KiB
Dart
795 lines
29 KiB
Dart
// Copyright (c) 2021, 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:dap/dap.dart';
|
|
import 'package:path/path.dart' as path;
|
|
import 'package:test/test.dart';
|
|
|
|
import 'test_client.dart';
|
|
import 'test_scripts.dart';
|
|
import 'test_server.dart';
|
|
import 'test_support.dart';
|
|
|
|
main() {
|
|
group('debug mode', () {
|
|
late DapTestSession dap;
|
|
setUp(() async {
|
|
// Temporarily enable verbose logging to debug some flakes on the bots
|
|
// https://github.com/dart-lang/sdk/issues/60187
|
|
dap = await DapTestSession.setUp(forceVerboseLogging: true);
|
|
});
|
|
tearDown(() => dap.tearDown());
|
|
|
|
test('runs a simple script', () async {
|
|
final testFile = dap.createTestFile(simpleArgPrintingProgram);
|
|
|
|
final outputEvents = await dap.client.collectOutput(
|
|
launch: () => dap.client.launch(
|
|
testFile.path,
|
|
args: ['one', 'two'],
|
|
),
|
|
);
|
|
|
|
// Expect a "console" output event that prints the URI of the VM Service
|
|
// the debugger connects to.
|
|
final vmConnection = outputEvents.first;
|
|
expect(vmConnection.output,
|
|
startsWith('Connecting to VM Service at ws://127.0.0.1:'));
|
|
expect(vmConnection.category, anyOf('console', isNull));
|
|
|
|
// Expect the normal applications output.
|
|
final output = outputEvents.skip(2).map((e) => e.output).join();
|
|
expectLines(output, [
|
|
'Hello!',
|
|
'World!',
|
|
'args: [one, two]',
|
|
'',
|
|
'Exited.',
|
|
]);
|
|
});
|
|
|
|
test('does not include empty output events when output ends with a newline',
|
|
() async {
|
|
final testFile = dap.createTestFile(simpleArgPrintingProgram);
|
|
|
|
final outputEvents = await dap.client.collectOutput(
|
|
launch: () => dap.client.launch(
|
|
testFile.path,
|
|
args: ['one', 'two'],
|
|
),
|
|
);
|
|
|
|
// The sample application uses `print()` which includes newlines on
|
|
// each output. Output is split by `\n` when scanning for stack frames
|
|
// and previously would include empty output events at the end if the
|
|
// content ended with a newline.
|
|
// https://github.com/flutter/flutter/pull/147250#issuecomment-2075128834
|
|
for (var output in outputEvents) {
|
|
expect(output.output, isNotEmpty);
|
|
}
|
|
});
|
|
|
|
test('runs a simple script using runInTerminal request', () async {
|
|
final testFile = dap.createTestFile(emptyProgram);
|
|
|
|
// Set up a handler to handle the server calling the clients runInTerminal
|
|
// request and capture the args.
|
|
RunInTerminalRequestArguments? runInTerminalArgs;
|
|
Process? proc;
|
|
dap.client.handleRequest(
|
|
'runInTerminal',
|
|
(args) async {
|
|
runInTerminalArgs = RunInTerminalRequestArguments.fromJson(
|
|
args as Map<String, Object?>,
|
|
);
|
|
|
|
// Run the requested process (emulating what the editor would do) so
|
|
// that the DA will pick up the service info file, connect to the VM,
|
|
// resume, and then detect its termination.
|
|
final runArgs = runInTerminalArgs!;
|
|
proc = await Process.start(
|
|
runArgs.args.first,
|
|
runArgs.args.skip(1).toList(),
|
|
workingDirectory: runArgs.cwd,
|
|
);
|
|
|
|
return RunInTerminalResponseBody(processId: proc!.pid);
|
|
},
|
|
);
|
|
|
|
// Run the script until we get a TerminatedEvent.
|
|
await Future.wait([
|
|
dap.client.event('terminated'),
|
|
dap.client.initialize(supportsRunInTerminalRequest: true),
|
|
dap.client.launch(testFile.path, console: "terminal"),
|
|
], eagerError: true);
|
|
|
|
expect(runInTerminalArgs, isNotNull);
|
|
expect(proc, isNotNull);
|
|
expect(
|
|
runInTerminalArgs!.args,
|
|
containsAllInOrder([
|
|
Platform.resolvedExecutable,
|
|
uppercaseDriveLetter(testFile.path),
|
|
]),
|
|
);
|
|
expect(proc!.pid, isPositive);
|
|
expect(proc!.exitCode, completes);
|
|
});
|
|
|
|
test('runs a simple script with commas in the filename', () async {
|
|
final (packageUri, _) = await dap.createFooPackage('foo,foo.dart');
|
|
final testFile = dap.createTestFile(
|
|
'''
|
|
import '$packageUri';
|
|
void main() {
|
|
foo();
|
|
}
|
|
''',
|
|
);
|
|
|
|
final outputEvents = await dap.client.collectOutput(
|
|
launch: () => dap.client.launch(
|
|
testFile.path,
|
|
args: ['one', 'two'],
|
|
),
|
|
);
|
|
|
|
// Expect the normal applications output. This means we set up the
|
|
// debugger without crashing, even though we imported files with commas
|
|
// in the name.
|
|
final output = outputEvents.skip(2).map((e) => e.output).join();
|
|
expectLines(output, [
|
|
'Hello!',
|
|
'World!',
|
|
'args: [one, two]',
|
|
'',
|
|
'Exited.',
|
|
]);
|
|
}, skip: 'Fails because of https://github.com/dart-lang/sdk/issues/52632');
|
|
|
|
test('does not resume isolates if user passes --pause-isolates-on-exit',
|
|
() async {
|
|
// Internally we always pass --pause-isolates-on-exit and resume the
|
|
// isolates after waiting for any output events to complete (in case they
|
|
// need to resolve URIs that involve API calls on an Isolate).
|
|
//
|
|
// However if a user passes this flag explicitly, we should not
|
|
// auto-resume because they might be trying to debug something.
|
|
final testFile = dap.createTestFile(simpleArgPrintingProgram);
|
|
|
|
// Run the script, expecting a Stopped event.
|
|
final stop = dap.client.expectStop('exit');
|
|
await Future.wait([
|
|
stop,
|
|
dap.client.initialize(),
|
|
dap.client
|
|
.launch(testFile.path, toolArgs: ["--pause-isolates-on-exit"]),
|
|
], eagerError: true);
|
|
|
|
// Resume and expect termination.
|
|
await Future.wait([
|
|
dap.client.event('terminated'),
|
|
dap.client.continue_((await stop).threadId!),
|
|
], eagerError: true);
|
|
});
|
|
|
|
test('does not resume isolates if user passes --pause-isolates-on-start',
|
|
() async {
|
|
// Internally we always pass --pause-isolates-on-start and resume the
|
|
// isolates after setting any breakpoints.
|
|
//
|
|
// However if a user passes this flag explicitly, we should not
|
|
// auto-resume because they might be trying to debug something.
|
|
final testFile = dap.createTestFile(simpleArgPrintingProgram);
|
|
|
|
// Run the script, expecting a Stopped event.
|
|
final stop = dap.client.expectStop('entry');
|
|
await Future.wait([
|
|
stop,
|
|
dap.client.initialize(),
|
|
dap.client.launch(
|
|
testFile.path,
|
|
toolArgs: ["--pause-isolates-on-start"],
|
|
),
|
|
], eagerError: true);
|
|
|
|
// Resume and expect termination.
|
|
await Future.wait([
|
|
dap.client.event('terminated'),
|
|
dap.client.continue_((await stop).threadId!),
|
|
], eagerError: true);
|
|
});
|
|
|
|
test('receives thread, stopped, continued events during pause/resume',
|
|
() async {
|
|
final client = dap.client;
|
|
final testFile = dap.createTestFile(debuggerPauseAndPrintManyProgram);
|
|
|
|
// Collect interesting events that we want to verify exist and in the
|
|
// right order.
|
|
final interestingEvents = const {
|
|
'thread',
|
|
'stopped',
|
|
'continued',
|
|
'terminated'
|
|
};
|
|
final eventsFuture = client.allEvents
|
|
.where((e) => interestingEvents.contains(e.event))
|
|
.map((e) {
|
|
// Create a descriptive string for verifying later.
|
|
final body = e.body as Map<String, Object?>;
|
|
const interestingFields = [
|
|
'reason',
|
|
'threadId',
|
|
'allThreadsContinued',
|
|
'allThreadsStopped'
|
|
];
|
|
final description = interestingFields
|
|
.where(body.containsKey)
|
|
.map((field) => '$field: ${body[field]}')
|
|
.join(', ');
|
|
return description.isNotEmpty ? '${e.event} ($description)' : e.event;
|
|
}).toList();
|
|
|
|
// Start the program and wait to pause on `debugger()`.
|
|
final stoppedFuture = client.expectStop('step');
|
|
await client.start(file: testFile);
|
|
final threadId = (await stoppedFuture).threadId!;
|
|
|
|
// Step 3 times and wait for the corresponding stop.
|
|
for (var i = 0; i < 3; i++) {
|
|
client.next(threadId);
|
|
await client.stoppedEvents.first;
|
|
}
|
|
|
|
// Resume to run to end.
|
|
client.continue_(threadId);
|
|
|
|
// Verify we had the expected events.
|
|
expect(
|
|
await eventsFuture,
|
|
[
|
|
'thread (reason: started, threadId: $threadId)',
|
|
'stopped (reason: entry, threadId: $threadId, allThreadsStopped: false)',
|
|
'continued (threadId: $threadId, allThreadsContinued: false)',
|
|
// stop on debugger()
|
|
'stopped (reason: step, threadId: $threadId, allThreadsStopped: false)',
|
|
// step 1
|
|
'continued (threadId: $threadId, allThreadsContinued: false)',
|
|
'stopped (reason: step, threadId: $threadId, allThreadsStopped: false)',
|
|
// step 2
|
|
'continued (threadId: $threadId, allThreadsContinued: false)',
|
|
'stopped (reason: step, threadId: $threadId, allThreadsStopped: false)',
|
|
// step 3
|
|
'continued (threadId: $threadId, allThreadsContinued: false)',
|
|
'stopped (reason: step, threadId: $threadId, allThreadsStopped: false)',
|
|
// continue
|
|
'continued (threadId: $threadId, allThreadsContinued: false)',
|
|
// pause-on-exit to drain stdout and handle looking up URIs
|
|
'stopped (reason: exit, threadId: $threadId, allThreadsStopped: false)',
|
|
// finished
|
|
'thread (reason: exited, threadId: $threadId)',
|
|
'terminated'
|
|
],
|
|
);
|
|
});
|
|
|
|
for (final outputKind in ['stdout', 'stderr']) {
|
|
test('sends $outputKind output events in the correct order', () async {
|
|
// Output events that have their URIs mapped will be processed slowly due
|
|
// the async requests for resolving the package URI. This should not cause
|
|
// them to appear out-of-order with other lines that do not require this
|
|
// work.
|
|
//
|
|
// Use a sample program that prints output to stderr that includes:
|
|
// - non stack frame lines
|
|
// - stack frames with file:// URIs
|
|
// - stack frames with package URIs (that need asynchronously resolving)
|
|
// - stack frames with dart URIs (that need asynchronously resolving)
|
|
final fileUri = Uri.file(dap.createTestFile('').path);
|
|
final (packageUri, _) = await dap.createFooPackage();
|
|
final dartUri = Uri.parse('dart:isolate-patch/isolate_patch.dart');
|
|
final testFile = dap.createTestFile(
|
|
stackPrintingProgram(outputKind, fileUri, packageUri, dartUri),
|
|
);
|
|
|
|
var outputEvents = await dap.client.collectOutput(
|
|
launch: () => dap.client.launch(testFile.path),
|
|
);
|
|
outputEvents =
|
|
outputEvents.where((e) => e.category == outputKind).toList();
|
|
|
|
// Verify the order of the stderr output events.
|
|
final output = outputEvents
|
|
.map((e) => e.output.trim())
|
|
.where((output) => output.isNotEmpty)
|
|
.join('\n');
|
|
expectLines(output, [
|
|
'Start',
|
|
'#0 main ($fileUri:1:2)',
|
|
'#1 main2 ($packageUri:3:4)',
|
|
'#2 main3 ($dartUri:5:6)',
|
|
'End',
|
|
]);
|
|
|
|
// As a sanity check, verify we did actually do the async path mapping and
|
|
// got both frames with paths in our test folder.
|
|
final stackFramesWithPaths = outputEvents.where((e) =>
|
|
e.source?.path != null &&
|
|
path.isWithin(dap.testDir.path, e.source!.path!));
|
|
expect(
|
|
stackFramesWithPaths,
|
|
hasLength(2),
|
|
reason: 'Expected two frames within path ${dap.testDir.path}',
|
|
);
|
|
});
|
|
|
|
test(
|
|
'fades $outputKind stack frames that are not part of our project when allowAnsiColorOutput=true',
|
|
() async {
|
|
// Use a sample program that prints output to stderr that includes:
|
|
// - non stack frame lines
|
|
// - stack frames with file:// URIs
|
|
// - stack frames with package URIs (that need asynchronously resolving)
|
|
// - stack frames with dart URIs (that need asynchronously resolving)
|
|
final fileUri = Uri.file(dap.createTestFile('').path);
|
|
final (packageUri, _) = await dap.createFooPackage();
|
|
final dartUri = Uri.parse('dart:isolate-patch/isolate_patch.dart');
|
|
final testFile = dap.createTestFile(
|
|
stackPrintingProgram(outputKind, fileUri, packageUri, dartUri),
|
|
);
|
|
|
|
var outputEvents = await dap.client.collectOutput(
|
|
launch: () => dap.client.launch(testFile.path,
|
|
allowAnsiColorOutput: true,
|
|
// Include package:foo as being user-code, to ensure it's not faded.
|
|
additionalProjectPaths: [
|
|
path.join(dap.testPackagesDir.path, 'foo'),
|
|
]),
|
|
);
|
|
outputEvents =
|
|
outputEvents.where((e) => e.category == outputKind).toList();
|
|
|
|
// Verify the order of the stderr output events.
|
|
final output = outputEvents
|
|
.map((e) => e.output.trim())
|
|
.where((output) => output.isNotEmpty)
|
|
.join('\n');
|
|
expectLines(output, [
|
|
'Start',
|
|
'#0 main ($fileUri:1:2)',
|
|
'#1 main2 ($packageUri:3:4)',
|
|
'\u001B[2m#2 main3 ($dartUri:5:6)\u001B[0m',
|
|
'End',
|
|
]);
|
|
});
|
|
|
|
test(
|
|
'includes correct Source.name for SDK and package sources in $outputKind output',
|
|
() async {
|
|
// Use a sample program that prints output to stderr that includes:
|
|
// - non stack frame lines
|
|
// - stack frames with file:// URIs
|
|
// - stack frames with package URIs (that need asynchronously resolving)
|
|
// - stack frames with dart URIs (that need asynchronously resolving)
|
|
final fileUri = Uri.file(dap.createTestFile('').path);
|
|
final (packageUri, _) = await dap.createFooPackage();
|
|
final dartUri = Uri.parse('dart:isolate-patch/isolate_patch.dart');
|
|
final testFile = dap.createTestFile(
|
|
stackPrintingProgram(outputKind, fileUri, packageUri, dartUri),
|
|
);
|
|
|
|
final outputEvents = await dap.client.collectOutput(file: testFile);
|
|
final outputSourceNames = outputEvents
|
|
.where((e) => e.category == outputKind)
|
|
.map((output) => output.source?.name)
|
|
.where((sourceName) => (sourceName?.isNotEmpty ?? false))
|
|
.toList();
|
|
|
|
expect(
|
|
outputSourceNames,
|
|
[
|
|
fileUri.toFilePath(),
|
|
packageUri.toString(),
|
|
dartUri.toString(),
|
|
],
|
|
);
|
|
});
|
|
}
|
|
|
|
group('progress notifications', () {
|
|
/// Helper to verify [events] are the expected start/update/end events
|
|
/// in-order for a debug session starting.
|
|
void verifyLaunchProgressEvents(List<Event> events) {
|
|
final bodies =
|
|
events.map((e) => e.body as Map<String, Object?>).toList();
|
|
final start = ProgressStartEventBody.fromMap(bodies[0]);
|
|
final update = ProgressUpdateEventBody.fromMap(bodies[1]);
|
|
final end = ProgressEndEventBody.fromMap(bodies[2]);
|
|
|
|
expect(start.progressId, isNotNull);
|
|
expect(start.title, 'Debugger');
|
|
expect(start.message, 'Starting…');
|
|
expect(update.progressId, start.progressId);
|
|
expect(update.message, 'Connecting…');
|
|
expect(end.progressId, start.progressId);
|
|
expect(end.message, isNull);
|
|
}
|
|
|
|
test('sends no events by default', () async {
|
|
final testFile = dap.createTestFile(simpleArgPrintingProgram);
|
|
|
|
final standardEvents = dap.client.standardProgressEvents().toList();
|
|
final customEvents = dap.client.customProgressEvents().toList();
|
|
|
|
// Run the script to completion.
|
|
await Future.wait([
|
|
dap.client.event('terminated'),
|
|
dap.client.initialize(),
|
|
dap.client.launch(testFile.path),
|
|
], eagerError: true);
|
|
|
|
expect(await standardEvents, isEmpty);
|
|
expect(await customEvents, isEmpty);
|
|
});
|
|
|
|
test('sends standard events when supported', () async {
|
|
final testFile = dap.createTestFile(simpleArgPrintingProgram);
|
|
|
|
final standardEventsFuture =
|
|
dap.client.standardProgressEvents().toList();
|
|
final customEventsFuture = dap.client.customProgressEvents().toList();
|
|
|
|
// Run the script to completion.
|
|
await Future.wait([
|
|
dap.client.event('terminated'),
|
|
dap.client.initialize(
|
|
supportsProgressReporting: true,
|
|
),
|
|
dap.client.launch(testFile.path),
|
|
], eagerError: true);
|
|
|
|
final standardEvents = await standardEventsFuture;
|
|
final customEvents = await customEventsFuture;
|
|
|
|
// Verify the standard launch events.
|
|
expect(
|
|
standardEvents.map((e) => e.event),
|
|
['progressStart', 'progressUpdate', 'progressEnd'],
|
|
);
|
|
verifyLaunchProgressEvents(standardEvents);
|
|
// And no custom events.
|
|
expect(customEvents, isEmpty);
|
|
});
|
|
|
|
test('sends custom events when requested', () async {
|
|
final testFile = dap.createTestFile(simpleArgPrintingProgram);
|
|
|
|
final standardEventsFuture =
|
|
dap.client.standardProgressEvents().toList();
|
|
final customEventsFuture = dap.client.customProgressEvents().toList();
|
|
|
|
// Run the script to completion.
|
|
await Future.wait([
|
|
dap.client.event('terminated'),
|
|
dap.client.initialize(),
|
|
dap.client.launch(
|
|
testFile.path,
|
|
sendCustomProgressEvents: true,
|
|
),
|
|
], eagerError: true);
|
|
|
|
final standardEvents = await standardEventsFuture;
|
|
final customEvents = await customEventsFuture;
|
|
|
|
// Verify no standard events.
|
|
expect(standardEvents, isEmpty);
|
|
// But custom events are sent.
|
|
expect(
|
|
customEvents.map((e) => e.event),
|
|
['dart.progressStart', 'dart.progressUpdate', 'dart.progressEnd'],
|
|
);
|
|
verifyLaunchProgressEvents(customEvents);
|
|
});
|
|
});
|
|
|
|
test('provides a list of threads', () async {
|
|
final client = dap.client;
|
|
final testFile = dap.createTestFile(simpleBreakpointProgram);
|
|
final breakpointLine = lineWith(testFile, breakpointMarker);
|
|
|
|
await client.hitBreakpoint(testFile, breakpointLine);
|
|
final response = await client.getValidThreads();
|
|
|
|
expect(response.threads, hasLength(1));
|
|
expect(response.threads.first.name, equals('main'));
|
|
});
|
|
|
|
test('runs with DDS by default', () async {
|
|
final client = dap.client;
|
|
final testFile = dap.createTestFile(simpleBreakpointProgram);
|
|
final breakpointLine = lineWith(testFile, breakpointMarker);
|
|
|
|
await client.hitBreakpoint(testFile, breakpointLine);
|
|
expect(await client.ddsAvailable, isTrue);
|
|
});
|
|
|
|
test('runs with auth codes enabled by default', () async {
|
|
final testFile = dap.createTestFile(emptyProgram);
|
|
|
|
final outputEvents = await dap.client.collectOutput(file: testFile);
|
|
final vmServiceUri = _extractVmServiceUri(outputEvents.first);
|
|
expect(vmServiceUri.path, matches(vmServiceAuthCodePathPattern));
|
|
});
|
|
|
|
test('can map SDK source code to a local path', () async {
|
|
final client = dap.client;
|
|
final testFile = dap.createTestFile(simpleBreakpointProgram);
|
|
final breakpointLine = lineWith(testFile, breakpointMarker);
|
|
|
|
// Hit the initial breakpoint.
|
|
final stop = await dap.client.hitBreakpoint(
|
|
testFile,
|
|
breakpointLine,
|
|
launch: () => client.launch(
|
|
testFile.path,
|
|
debugSdkLibraries: true,
|
|
),
|
|
);
|
|
|
|
// Step in to go into print.
|
|
final responses = await Future.wait([
|
|
client.expectStop('step', sourceName: 'dart:core/print.dart'),
|
|
client.stepIn(stop.threadId!),
|
|
], eagerError: true);
|
|
final stopResponse = responses.first as StoppedEventBody;
|
|
|
|
// Fetch the top stack frame (which should be inside print).
|
|
final stack = await client.getValidStack(
|
|
stopResponse.threadId!,
|
|
startFrame: 0,
|
|
numFrames: 1,
|
|
);
|
|
final topFrame = stack.stackFrames.first;
|
|
|
|
// SDK sources that have been mapped have no sourceReference but a path.
|
|
expect(
|
|
topFrame.source!.path,
|
|
equals(path.join(sdkRoot, 'lib', 'core', 'print.dart')),
|
|
);
|
|
expect(topFrame.source!.sourceReference, isNull);
|
|
});
|
|
|
|
test('can shutdown during startup', () async {
|
|
final testFile = dap.createTestFile(simpleArgPrintingProgram);
|
|
|
|
// Request termination immediately upon receiving the first Thread event.
|
|
// The DAP is also responding to this event to configure the isolate (eg.
|
|
// set breakpoints and exception pause behaviour) and will cause it to
|
|
// receive "Service has disappeared" responses if these are in-flight as
|
|
// the process terminates. These should be silently discarded since they
|
|
// are normal during shutdown.
|
|
unawaited(dap.client.event('thread').then((_) => dap.client.terminate()));
|
|
|
|
// Start the program and expect termination.
|
|
await Future.wait([
|
|
dap.client.event('terminated'),
|
|
dap.client.start(file: testFile),
|
|
], eagerError: true);
|
|
});
|
|
|
|
test('can hot reload', () async {
|
|
const originalText = 'ORIGINAL TEXT';
|
|
const newText = 'NEW TEXT';
|
|
|
|
// Create a script that prints 'ORIGINAL TEXT'.
|
|
final testFile = dap.createTestFile(stringPrintingProgram(originalText));
|
|
|
|
// Start the program and wait for 'ORIGINAL TEXT' to be printed.
|
|
await Future.wait([
|
|
dap.client.initialize(),
|
|
dap.client.launch(testFile.path),
|
|
], eagerError: true);
|
|
|
|
// Expect the original text.
|
|
await dap.client.outputEvents
|
|
.firstWhere((event) => event.output.trim() == originalText);
|
|
|
|
// Update the file and hot reload.
|
|
testFile.writeAsStringSync(stringPrintingProgram(newText), flush: true);
|
|
// Set a future date to ensure hot reload detects it as being modified.
|
|
testFile.setLastModifiedSync(
|
|
DateTime.now().add(const Duration(seconds: 2)),
|
|
);
|
|
await dap.client.hotReload();
|
|
|
|
// Expect the new text.
|
|
await dap.client.outputEvents
|
|
.firstWhere((event) => event.output.trim() == newText);
|
|
|
|
await dap.client.terminate();
|
|
|
|
// If we're running out of process, ensure the server process terminates.
|
|
final server = dap.server;
|
|
if (server is OutOfProcessDapTestServer) {
|
|
await server.exitCode;
|
|
}
|
|
});
|
|
|
|
test('can pause', () async {
|
|
final testFile = dap.createTestFile(infiniteRunningProgram);
|
|
|
|
// Start a program and hit a breakpoint.
|
|
final client = dap.client;
|
|
final threadFuture = client.threadEvents.first;
|
|
|
|
// Start the app and wait for it to start printing output.
|
|
await Future.wait([
|
|
client.initialize(),
|
|
client.launch(testFile.path),
|
|
dap.client.outputEvents
|
|
.firstWhere((event) => event.output.contains('Looping'))
|
|
]);
|
|
|
|
// Ensure we can pause.
|
|
final thread = await threadFuture;
|
|
await Future.wait([
|
|
client.expectStop('pause'),
|
|
client.pause(thread.threadId),
|
|
], eagerError: true);
|
|
});
|
|
|
|
test('can restart frame', () async {
|
|
final client = dap.client;
|
|
final testFile = dap.createTestFile(restartFrameProgram);
|
|
final breakpointLine = lineWith(testFile, breakpointMarker);
|
|
final outputEventsFuture = dap.client.outputEvents.toList();
|
|
|
|
// Stop at the breakpoint in the printMessage function.
|
|
final stop = await client.hitBreakpoint(testFile, breakpointLine);
|
|
final threadId = stop.threadId!;
|
|
|
|
// Restart back to the main function and expect a new stop event.
|
|
final stack =
|
|
await client.getValidStack(threadId, startFrame: 0, numFrames: 2);
|
|
final mainFunctionFrame = stack.stackFrames[1];
|
|
await Future.wait([
|
|
client.expectStop('step'),
|
|
client.restartFrame(mainFunctionFrame.id),
|
|
], eagerError: true);
|
|
|
|
// Resume, hit breakpoint again, resume to end.
|
|
await Future.wait([
|
|
client.expectStop('breakpoint'),
|
|
client.continue_(threadId),
|
|
], eagerError: true);
|
|
await Future.wait([
|
|
client.event('terminated'),
|
|
client.continue_(threadId),
|
|
], eagerError: true);
|
|
|
|
// Finally, verify we got output that shows we restarted and re-ran the
|
|
// code before the breakpoint.
|
|
final outputEvents = await outputEventsFuture;
|
|
final outputMessages = outputEvents.map((e) => e.output.trim());
|
|
|
|
expect(
|
|
outputMessages,
|
|
containsAll(['Hello', 'Hello', 'World']),
|
|
);
|
|
});
|
|
|
|
test('forwards tool events to client', () async {
|
|
final testFile = dap.createTestFile(simpleToolEventProgram);
|
|
|
|
// Capture any `dart.toolEvent` events.
|
|
final toolEventsFuture = dap.client.events('dart.toolEvent').toList();
|
|
|
|
// Run the script to completion.
|
|
await Future.wait([
|
|
dap.client.event('terminated'),
|
|
dap.client.initialize(),
|
|
dap.client.launch(testFile.path),
|
|
], eagerError: true);
|
|
|
|
// Verify we got exactly the event in the sample program.
|
|
final toolEvents = await toolEventsFuture;
|
|
expect(toolEvents, hasLength(1));
|
|
final toolEvent = toolEvents.single;
|
|
expect(toolEvent.body, {
|
|
'kind': 'navigate',
|
|
'data': {
|
|
'uri': 'file:///file.dart',
|
|
},
|
|
});
|
|
});
|
|
|
|
test('resolves URIs in tool events to file:///', () async {
|
|
final client = dap.client;
|
|
final testFile =
|
|
dap.createTestFile(simpleToolEventWithDartCoreUriProgram);
|
|
|
|
// Capture the `dart.toolEvent` event.
|
|
final toolEventsFuture = client.events('dart.toolEvent').first;
|
|
|
|
// Run the script until we get the event (which means mapping has
|
|
// completed).
|
|
await Future.wait([
|
|
toolEventsFuture,
|
|
client.initialize(),
|
|
client.launch(testFile.path),
|
|
], eagerError: true);
|
|
|
|
// Terminate the app (since the test script has a delay to ensure it
|
|
// doesn't terminate before the async mapping code completes).
|
|
await client.terminate();
|
|
|
|
// Verify we got the right fileUri.
|
|
final toolEvent = await toolEventsFuture;
|
|
final body = toolEvent.body as Map<String, Object?>;
|
|
final data = body['data'] as Map<String, Object?>;
|
|
final uri = data['uri'] as String;
|
|
final resolvedUri = data['resolvedUri'] as String;
|
|
expect(uri, 'dart:core');
|
|
expect(resolvedUri, startsWith('file:///'));
|
|
expect(resolvedUri, endsWith('lib/core/core.dart'));
|
|
});
|
|
// These tests can be slow due to starting up the external server process.
|
|
}, timeout: Timeout.none);
|
|
|
|
group('debug mode', () {
|
|
test('can be run without DDS using vmAdditionalArgs', () async {
|
|
final dap = await DapTestSession.setUp();
|
|
addTearDown(dap.tearDown);
|
|
|
|
final client = dap.client;
|
|
final testFile = dap.createTestFile(simpleBreakpointProgram);
|
|
final breakpointLine = lineWith(testFile, breakpointMarker);
|
|
|
|
await client.hitBreakpoint(testFile, breakpointLine,
|
|
vmAdditionalArgs: ['--no-dds']);
|
|
|
|
expect(await client.ddsAvailable, isFalse);
|
|
});
|
|
|
|
test('can run without auth codes using vmAdditionalArgs', () async {
|
|
final dap = await DapTestSession.setUp();
|
|
addTearDown(dap.tearDown);
|
|
|
|
final testFile = dap.createTestFile(emptyProgram);
|
|
final outputEvents = await dap.client.collectOutput(
|
|
launch: () => dap.client.launch(testFile.path,
|
|
vmAdditionalArgs: ['--disable-service-auth-codes']),
|
|
);
|
|
final vmServiceUri = _extractVmServiceUri(outputEvents.first);
|
|
expect(vmServiceUri.path, isNot(matches(vmServiceAuthCodePathPattern)));
|
|
});
|
|
|
|
test('can run with ipv6 with a DAP flag', () async {
|
|
final dap = await DapTestSession.setUp(additionalArgs: ['--ipv6']);
|
|
addTearDown(dap.tearDown);
|
|
|
|
final testFile = dap.createTestFile(emptyProgram);
|
|
final outputEvents = await dap.client.collectOutput(file: testFile);
|
|
final vmServiceUri = _extractVmServiceUri(outputEvents.first);
|
|
|
|
expect(vmServiceUri.host, equals('::1'));
|
|
});
|
|
// These tests can be slow due to starting up the external server process.
|
|
}, timeout: Timeout.none);
|
|
}
|
|
|
|
/// Extracts the VM Service URI from the "Connecting to ..." banner output by
|
|
/// the DAP server upon connection.
|
|
Uri _extractVmServiceUri(OutputEventBody vmConnectionBanner) {
|
|
// TODO(dantup): Change this to use the dart.debuggerUris custom event
|
|
// if implemented (which VS Code also needs).
|
|
final match = dapVmServiceBannerPattern.firstMatch(vmConnectionBanner.output);
|
|
return Uri.parse(match!.group(1)!);
|
|
}
|