blob: 27165d14cfa63c91cf7fe86cb6a3e0768e3b9b60 [file] [log] [blame]
// 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)!);
}