Files
sdk/pkg/dds/test/dap/integration/debug_attach_test.dart
T
Danny Tuppeny 7a6352a9b1 [dds/dap] Ensure DAP always responds to initialize() before sending initializedEvent
Changes to support DAP in DDS introduced a subtle bug where we could send the `InitializedEvent` before the response to the `Initialize` request because `sendResponse` used a `Completer` and would delay adding the response to the stream until the next iteration of the task queue.

Although there were comments stating this order must be preserved, there were no tests that verified this and it breaks VS Code (https://github.com/dart-lang/sdk/issues/52222).

I've added tests for this, and changed how this works slightly so instead of using a `Completer` we pass in a "responseWriter" function that must write the response to the stream synchronously.

Since the DDS path isn't using events yet and it would require a little more refactoring, I've used a Completer there and added a TODO.

Fixes https://github.com/dart-lang/sdk/issues/52222.

Change-Id: I1832126f5015b466c721dbda192da4b8a5d83b62
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/300601
Reviewed-by: Ben Konyi <bkonyi@google.com>
Commit-Queue: Ben Konyi <bkonyi@google.com>
2023-05-02 18:35:26 +00:00

169 lines
5.5 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 '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('can attach to a simple script using vmServiceUri', () async {
final testFile = dap.createTestFile(simpleArgPrintingProgram);
final args = ['one', 'two'];
final proc = await startDartProcessPaused(
testFile.path,
args,
cwd: dap.testAppDir.path,
);
final vmServiceUri = await waitForStdoutVmServiceBanner(proc);
final outputEvents = await dap.client.collectOutput(
launch: () => dap.client.attach(
vmServiceUri: vmServiceUri.toString(),
autoResume: true,
cwd: dap.testAppDir.path,
),
);
// 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(1)
.map((e) => e.output)
// The stdout also contains the VM Service+DevTools banners.
.where(
(line) =>
!line.startsWith('The Dart VM service is listening on') &&
!line.startsWith(
'The Dart DevTools debugger and profiler is available at'),
)
.join();
expectLines(output, [
'Hello!',
'World!',
'args: [one, two]',
'',
'Exited.',
]);
});
test('can attach to a simple script using vmServiceInfoFile', () async {
final testFile = dap.createTestFile(simpleArgPrintingProgram);
// Spawn the program using --write-service-info which we'll pass the path
// of directly to the DAP to read.
final vmServiceInfoFilePath = path.join(
dap.testAppDir.path,
'vmServiceInfo.json',
);
await startDartProcessPaused(
testFile.path,
['one', 'two'],
cwd: dap.testAppDir.path,
vmArgs: ['--write-service-info=${Uri.file(vmServiceInfoFilePath)}'],
);
final outputEvents = await dap.client.collectOutput(
launch: () => dap.client.attach(
vmServiceInfoFile: vmServiceInfoFilePath,
autoResume: true,
cwd: dap.testAppDir.path,
),
);
// 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(1)
.map((e) => e.output)
// The stdout also contains the VM Service+DevTools banners.
.where(
(line) =>
!line.startsWith('The Dart VM service is listening on') &&
!line.startsWith(
'The Dart DevTools debugger and profiler is available at'),
)
.join();
expectLines(output, [
'Hello!',
'World!',
'args: [one, two]',
'',
'Exited.',
]);
});
test('removes breakpoints/pause and resumes on detach', () async {
final testFile = dap.createTestFile(simpleBreakpointAndThrowProgram);
final proc = await startDartProcessPaused(
testFile.path,
[],
cwd: dap.testAppDir.path,
);
final vmServiceUri = await waitForStdoutVmServiceBanner(proc);
// Attach to the paused script without resuming and wait for the startup
// pause event.
await Future.wait([
dap.client.expectStop('entry'),
dap.client.start(
launch: () => dap.client.attach(
vmServiceUri: vmServiceUri.toString(),
autoResume: false,
cwd: dap.testAppDir.path,
),
),
]);
// Set a breakpoint that we expect not to be hit, as detach should disable
// it and resume.
final breakpointLine = lineWith(testFile, breakpointMarker);
await dap.client.setBreakpoint(testFile, breakpointLine);
// Expect that termination is reported as 'Detached' when we explicitly
// requested a detach.
expect(dap.client.outputEvents.map((output) => output.output.trim()),
emits('Detached.'));
// Detach using terminateRequest. Despite the name, terminateRequest is
// the request for a graceful detach (and disconnectRequest is the
// forceful shutdown).
await Future.wait([
dap.client.event('terminated'),
dap.client.terminate(),
]);
// Expect the process terminates (and hasn't got stuck on the breakpoint
// or exception).
await proc.exitCode;
});
// These tests can be slow due to starting up the external server process.
}, timeout: Timeout.none);
}