blob: 6655b580bc83c3a36d0368f5c66956c9731d40c7 [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:dds/src/dap/protocol_generated.dart';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';
import 'test_client.dart';
import 'test_scripts.dart';
import 'test_support.dart';
main() {
group('debug mode', () {
late DapTestSession dap;
setUp(() async {
dap = await DapTestSession.setUp();
});
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, equals('console'));
// Expect the normal applications output.
final output = outputEvents.skip(1).map((e) => e.output).join();
expectLines(output, [
'Hello!',
'World!',
'args: [one, two]',
'',
'Exited.',
]);
});
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, testFile.path]),
);
expect(proc!.pid, isPositive);
expect(proc!.exitCode, completes);
});
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 download source code from the VM', () 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 should have a sourceReference and no path.
expect(topFrame.source!.path, isNull);
expect(topFrame.source!.sourceReference, isPositive);
// Source code should contain the implementation/signature of print().
final source = await client.getValidSource(topFrame.source!);
expect(source.content, contains('void print(Object? object) {'));
// Skipped because this test is not currently valid as source for print
// is mapped to local sources.
}, skip: true);
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);
// Terminate the app immediately upon recieving 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 not go unhandled since they are
// normal during shutdown.
unawaited(dap.client.event('thread').then((_) => dap.client.terminate()));
await dap.client.start(file: testFile);
});
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));
await dap.client.hotReload();
// Expect the new text.
await dap.client.outputEvents
.firstWhere((event) => event.output.trim() == newText);
await dap.client.terminate();
});
// These tests can be slow due to starting up the external server process.
}, timeout: Timeout.none);
group('debug mode', () {
test('can run without DDS', () async {
final dap = await DapTestSession.setUp(additionalArgs: ['--no-dds']);
addTearDown(dap.tearDown);
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
final breakpointLine = lineWith(testFile, breakpointMarker);
await client.hitBreakpoint(testFile, breakpointLine);
expect(await client.ddsAvailable, isFalse);
});
test('can run without auth codes', () async {
final dap =
await DapTestSession.setUp(additionalArgs: ['--no-auth-codes']);
addTearDown(dap.tearDown);
final testFile = dap.createTestFile(emptyProgram);
final outputEvents = await dap.client.collectOutput(file: testFile);
final vmServiceUri = _extractVmServiceUri(outputEvents.first);
expect(vmServiceUri.path, isNot(matches(vmServiceAuthCodePathPattern)));
});
test('can run with ipv6', () 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 (whch VS Code also needs).
final match = dapVmServiceBannerPattern.firstMatch(vmConnectionBanner.output);
return Uri.parse(match!.group(1)!);
}