[dds] Switch DAP server mode to directly using stdin/stdout

Change-Id: Id2a8d8c9274b96cf32788e11d4452954a4f9c091
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/205500
Reviewed-by: Ben Konyi <bkonyi@google.com>
This commit is contained in:
Danny Tuppeny 2021-07-22 14:38:21 +00:00 committed by Ben Konyi
parent 66bd92dcaa
commit 6b4fc2670c
13 changed files with 586 additions and 751 deletions

View file

@ -53,7 +53,9 @@ class DartCliDebugAdapter extends DartDebugAdapter<DartLaunchRequestArguments> {
enableDds: enableDds,
enableAuthCodes: enableAuthCodes,
logger: logger,
);
) {
channel.closed.then((_) => shutdown());
}
Future<void> debuggerConnected(vm.VM vmInfo) async {
if (!isAttach) {

View file

@ -4,7 +4,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
class InvalidEncodingError {
final String headers;
@ -107,28 +106,3 @@ class ProtocolHeaders {
final String? encoding;
ProtocolHeaders(this.rawHeaders, this.contentLength, this.encoding);
}
/// Transforms a stream of [Uint8List]s to [List<int>]s. Used because
/// [ServerSocket] and [Socket] use [Uint8List] but stdin and stdout use
/// [List<int>] and the LSP server needs to operate against both.
class Uint8ListTransformer extends StreamTransformerBase<Uint8List, List<int>> {
// TODO(dantup): Is there a built-in (or better) way to do this?
@override
Stream<List<int>> bind(Stream<Uint8List> stream) {
late StreamSubscription<Uint8List> input;
late StreamController<List<int>> _output;
_output = StreamController<List<int>>(
onListen: () {
input = stream.listen(
(uints) => _output.add(uints),
onError: _output.addError,
onDone: _output.close,
);
},
onPause: () => input.pause(),
onResume: () => input.resume(),
onCancel: () => input.cancel(),
);
return _output.stream;
}
}

View file

@ -3,92 +3,45 @@
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:io';
import 'package:dds/src/dap/adapters/dart.dart';
import 'package:pedantic/pedantic.dart';
import 'adapters/dart_cli.dart';
import 'logging.dart';
import 'protocol_stream.dart';
import 'protocol_stream_transformers.dart';
/// A DAP server that binds to a port and runs in multi-session mode.
/// A DAP server that communicates over a [ByteStreamServerChannel], usually
/// constructed from the processes stdin/stdout streams.
///
/// The server runs in single-user mode and services only a single client. For
/// multiple debug sessions, there would be multiple servers (and the editor
/// would have a client for each of them).
class DapServer {
final ServerSocket _socket;
final ByteStreamServerChannel channel;
late final DartDebugAdapter adapter;
final bool ipv6;
final bool enableDds;
final bool enableAuthCodes;
final Logger? logger;
final _channels = <ByteStreamServerChannel>{};
final _adapters = <DartDebugAdapter>{};
DapServer._(
this._socket, {
DapServer(
Stream<List<int>> _input,
StreamSink<List<int>> _output, {
this.ipv6 = false,
this.enableDds = true,
this.enableAuthCodes = true,
this.logger,
}) {
_socket.listen(_acceptConnection);
}
String get host => _socket.address.host;
int get port => _socket.port;
Future<void> stop() async {
_channels.forEach((client) => client.close());
await _socket.close();
}
void _acceptConnection(Socket client) {
final address = client.remoteAddress;
logger?.call('Accepted connection from $address');
client.done.then((_) {
logger?.call('Connection from $address closed');
});
_createAdapter(client.transform(Uint8ListTransformer()), client);
}
void _createAdapter(Stream<List<int>> _input, StreamSink<List<int>> _output) {
// TODO(dantup): This is hard-coded to DartCliDebugAdapter but will
// ultimately need to support having a factory passed in to support
// tests and/or being used in flutter_tools.
final channel = ByteStreamServerChannel(_input, _output, logger);
final adapter = DartCliDebugAdapter(
}) : channel = ByteStreamServerChannel(_input, _output, logger) {
adapter = DartCliDebugAdapter(
channel,
ipv6: ipv6,
enableDds: enableDds,
enableAuthCodes: enableAuthCodes,
logger: logger,
);
_channels.add(channel);
_adapters.add(adapter);
unawaited(channel.closed.then((_) {
_channels.remove(channel);
_adapters.remove(adapter);
adapter.shutdown();
}));
}
/// Starts a DAP Server listening on [host]:[port].
static Future<DapServer> create({
String? host,
int port = 0,
bool ipv6 = false,
bool enableDdds = true,
bool enableAuthCodes = true,
Logger? logger,
}) async {
final hostFallback =
ipv6 ? InternetAddress.loopbackIPv6 : InternetAddress.loopbackIPv4;
final _socket = await ServerSocket.bind(host ?? hostFallback, port);
return DapServer._(
_socket,
ipv6: ipv6,
enableDds: enableDdds,
enableAuthCodes: enableAuthCodes,
logger: logger,
);
void stop() {
channel.close();
}
}

View file

@ -9,56 +9,61 @@ import 'test_scripts.dart';
import 'test_support.dart';
main() {
testDap((dap) async {
group('debug mode breakpoints', () {
test('stops at a line breakpoint', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
late DapTestSession dap;
setUp(() async {
dap = await DapTestSession.setUp();
});
tearDown(() => dap.tearDown());
await client.hitBreakpoint(testFile, breakpointLine);
});
group('debug mode breakpoints', () {
test('stops at a line breakpoint', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
test('stops at a line breakpoint and can be resumed', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
await client.hitBreakpoint(testFile, breakpointLine);
});
// Hit the initial breakpoint.
final stop = await client.hitBreakpoint(testFile, breakpointLine);
test('stops at a line breakpoint and can be resumed', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
// Resume and expect termination (as the script will get to the end).
await Future.wait([
client.event('terminated'),
client.continue_(stop.threadId!),
], eagerError: true);
});
// Hit the initial breakpoint.
final stop = await client.hitBreakpoint(testFile, breakpointLine);
test('stops at a line breakpoint and can step over (next)', () async {
final testFile = dap.createTestFile(r'''
// Resume and expect termination (as the script will get to the end).
await Future.wait([
client.event('terminated'),
client.continue_(stop.threadId!),
], eagerError: true);
});
test('stops at a line breakpoint and can step over (next)', () async {
final testFile = dap.createTestFile(r'''
void main(List<String> args) async {
print('Hello!'); // BREAKPOINT
print('Hello!'); // STEP
}
''');
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
final stepLine = lineWith(testFile, '// STEP');
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
final stepLine = lineWith(testFile, '// STEP');
// Hit the initial breakpoint.
final stop = await dap.client.hitBreakpoint(testFile, breakpointLine);
// Hit the initial breakpoint.
final stop = await dap.client.hitBreakpoint(testFile, breakpointLine);
// Step and expect stopping on the next line with a 'step' stop type.
await Future.wait([
dap.client.expectStop('step', file: testFile, line: stepLine),
dap.client.next(stop.threadId!),
], eagerError: true);
});
// Step and expect stopping on the next line with a 'step' stop type.
await Future.wait([
dap.client.expectStop('step', file: testFile, line: stepLine),
dap.client.next(stop.threadId!),
], eagerError: true);
});
test(
'stops at a line breakpoint and can step over (next) an async boundary',
() async {
final client = dap.client;
final testFile = dap.createTestFile(r'''
test(
'stops at a line breakpoint and can step over (next) an async boundary',
() async {
final client = dap.client;
final testFile = dap.createTestFile(r'''
Future<void> main(List<String> args) async {
await asyncPrint('Hello!'); // BREAKPOINT
await asyncPrint('Hello!'); // STEP
@ -68,30 +73,30 @@ Future<void> asyncPrint(String message) async {
await Future.delayed(const Duration(milliseconds: 1));
}
''');
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
final stepLine = lineWith(testFile, '// STEP');
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
final stepLine = lineWith(testFile, '// STEP');
// Hit the initial breakpoint.
final stop = await dap.client.hitBreakpoint(testFile, breakpointLine);
// Hit the initial breakpoint.
final stop = await dap.client.hitBreakpoint(testFile, breakpointLine);
// The first step will move from `asyncPrint` to the `await`.
await Future.wait([
client.expectStop('step', file: testFile, line: breakpointLine),
client.next(stop.threadId!),
], eagerError: true);
// The first step will move from `asyncPrint` to the `await`.
await Future.wait([
client.expectStop('step', file: testFile, line: breakpointLine),
client.next(stop.threadId!),
], eagerError: true);
// The next step should go over the async boundary and to stepLine (if
// we did not correctly send kOverAsyncSuspension we would end up in
// the asyncPrint method).
await Future.wait([
client.expectStop('step', file: testFile, line: stepLine),
client.next(stop.threadId!),
], eagerError: true);
});
// The next step should go over the async boundary and to stepLine (if
// we did not correctly send kOverAsyncSuspension we would end up in
// the asyncPrint method).
await Future.wait([
client.expectStop('step', file: testFile, line: stepLine),
client.next(stop.threadId!),
], eagerError: true);
});
test('stops at a line breakpoint and can step in', () async {
final client = dap.client;
final testFile = dap.createTestFile(r'''
test('stops at a line breakpoint and can step in', () async {
final client = dap.client;
final testFile = dap.createTestFile(r'''
void main(List<String> args) async {
log('Hello!'); // BREAKPOINT
}
@ -100,22 +105,22 @@ void log(String message) { // STEP
print(message);
}
''');
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
final stepLine = lineWith(testFile, '// STEP');
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
final stepLine = lineWith(testFile, '// STEP');
// Hit the initial breakpoint.
final stop = await client.hitBreakpoint(testFile, breakpointLine);
// Hit the initial breakpoint.
final stop = await client.hitBreakpoint(testFile, breakpointLine);
// Step and expect stopping in the inner function with a 'step' stop type.
await Future.wait([
client.expectStop('step', file: testFile, line: stepLine),
client.stepIn(stop.threadId!),
], eagerError: true);
});
// Step and expect stopping in the inner function with a 'step' stop type.
await Future.wait([
client.expectStop('step', file: testFile, line: stepLine),
client.stepIn(stop.threadId!),
], eagerError: true);
});
test('stops at a line breakpoint and can step out', () async {
final client = dap.client;
final testFile = dap.createTestFile(r'''
test('stops at a line breakpoint and can step out', () async {
final client = dap.client;
final testFile = dap.createTestFile(r'''
void main(List<String> args) async {
log('Hello!');
log('Hello!'); // STEP
@ -125,87 +130,85 @@ void log(String message) {
print(message); // BREAKPOINT
}
''');
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
final stepLine = lineWith(testFile, '// STEP');
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
final stepLine = lineWith(testFile, '// STEP');
// Hit the initial breakpoint.
final stop = await client.hitBreakpoint(testFile, breakpointLine);
// Hit the initial breakpoint.
final stop = await client.hitBreakpoint(testFile, breakpointLine);
// Step and expect stopping in the inner function with a 'step' stop type.
await Future.wait([
client.expectStop('step', file: testFile, line: stepLine),
client.stepOut(stop.threadId!),
], eagerError: true);
});
// Step and expect stopping in the inner function with a 'step' stop type.
await Future.wait([
client.expectStop('step', file: testFile, line: stepLine),
client.stepOut(stop.threadId!),
], eagerError: true);
});
test('does not step into SDK code with debugSdkLibraries=false',
() async {
final client = dap.client;
final testFile = dap.createTestFile(r'''
test('does not step into SDK code with debugSdkLibraries=false', () async {
final client = dap.client;
final testFile = dap.createTestFile(r'''
void main(List<String> args) async {
print('Hello!'); // BREAKPOINT
print('Hello!'); // STEP
}
''');
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
final stepLine = lineWith(testFile, '// STEP');
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
final stepLine = lineWith(testFile, '// STEP');
// Hit the initial breakpoint.
final stop = await client.hitBreakpoint(testFile, breakpointLine);
// Hit the initial breakpoint.
final stop = await client.hitBreakpoint(testFile, breakpointLine);
// Step in and expect stopping on the next line (don't go into print).
await Future.wait([
client.expectStop('step', file: testFile, line: stepLine),
client.stepIn(stop.threadId!),
], eagerError: true);
});
// Step in and expect stopping on the next line (don't go into print).
await Future.wait([
client.expectStop('step', file: testFile, line: stepLine),
client.stepIn(stop.threadId!),
], eagerError: true);
});
test('steps into SDK code with debugSdkLibraries=true', () async {
final client = dap.client;
final testFile = dap.createTestFile(r'''
test('steps into SDK code with debugSdkLibraries=true', () async {
final client = dap.client;
final testFile = dap.createTestFile(r'''
void main(List<String> args) async {
print('Hello!'); // BREAKPOINT
print('Hello!');
}
''');
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
// Hit the initial breakpoint.
final stop = await dap.client.hitBreakpoint(
testFile,
breakpointLine,
launch: () => client.launch(
testFile.path,
debugSdkLibraries: true,
),
);
// Hit the initial breakpoint.
final stop = await dap.client.hitBreakpoint(
testFile,
breakpointLine,
launch: () => client.launch(
testFile.path,
debugSdkLibraries: true,
),
);
// Step in and expect to go into print.
await Future.wait([
client.expectStop('step', sourceName: 'dart:core/print.dart'),
client.stepIn(stop.threadId!),
], eagerError: true);
});
// Step in and expect to go into print.
await Future.wait([
client.expectStop('step', sourceName: 'dart:core/print.dart'),
client.stepIn(stop.threadId!),
], eagerError: true);
});
test(
'does not step into external package code with debugExternalPackageLibraries=false',
() {
// TODO(dantup): Support for debugExternalPackageLibraries
}, skip: true);
test(
'does not step into external package code with debugExternalPackageLibraries=false',
() {
// TODO(dantup): Support for debugExternalPackageLibraries
}, skip: true);
test(
'steps into external package code with debugExternalPackageLibraries=true',
() {
// TODO(dantup): Support for debugExternalPackageLibraries
}, skip: true);
test(
'steps into external package code with debugExternalPackageLibraries=true',
() {
// TODO(dantup): Support for debugExternalPackageLibraries
}, skip: true);
test('allows changing debug settings during session', () {
// TODO(dantup): !
// Dart-Code's DAP has a custom method that allows an editor to change
// the debug settings (debugSdkLibraries/debugExternalPackageLibraries)
// during a debug session.
}, skip: true);
// These tests can be slow due to starting up the external server process.
}, timeout: Timeout.none);
});
test('allows changing debug settings during session', () {
// TODO(dantup): !
// Dart-Code's DAP has a custom method that allows an editor to change
// the debug settings (debugSdkLibraries/debugExternalPackageLibraries)
// during a debug session.
}, skip: true);
// These tests can be slow due to starting up the external server process.
}, timeout: Timeout.none);
}

View file

@ -10,126 +10,131 @@ import 'test_scripts.dart';
import 'test_support.dart';
main() {
testDap((dap) async {
group('debug mode evaluation', () {
test('evaluates expressions with simple results', () async {
final client = dap.client;
final testFile = await dap.createTestFile(r'''
late DapTestSession dap;
setUp(() async {
dap = await DapTestSession.setUp();
});
tearDown(() => dap.tearDown());
group('debug mode evaluation', () {
test('evaluates expressions with simple results', () async {
final client = dap.client;
final testFile = await dap.createTestFile(r'''
void main(List<String> args) {
var a = 1;
var b = 2;
var c = 'test';
print('Hello!'); // BREAKPOINT
}''');
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
final stop = await client.hitBreakpoint(testFile, breakpointLine);
await client.expectTopFrameEvalResult(stop.threadId!, 'a', '1');
await client.expectTopFrameEvalResult(stop.threadId!, 'a * b', '2');
await client.expectTopFrameEvalResult(stop.threadId!, 'c', '"test"');
});
final stop = await client.hitBreakpoint(testFile, breakpointLine);
await client.expectTopFrameEvalResult(stop.threadId!, 'a', '1');
await client.expectTopFrameEvalResult(stop.threadId!, 'a * b', '2');
await client.expectTopFrameEvalResult(stop.threadId!, 'c', '"test"');
});
test('evaluates expressions with complex results', () async {
final client = dap.client;
final testFile = await dap.createTestFile(simpleBreakpointProgram);
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
test('evaluates expressions with complex results', () async {
final client = dap.client;
final testFile = await dap.createTestFile(simpleBreakpointProgram);
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
final stop = await client.hitBreakpoint(testFile, breakpointLine);
final result = await client.expectTopFrameEvalResult(
stop.threadId!,
'DateTime(2000, 1, 1)',
'DateTime',
);
final stop = await client.hitBreakpoint(testFile, breakpointLine);
final result = await client.expectTopFrameEvalResult(
stop.threadId!,
'DateTime(2000, 1, 1)',
'DateTime',
);
// Check we got a variablesReference that maps on to the fields.
expect(result.variablesReference, greaterThan(0));
await client.expectVariables(
result.variablesReference,
'''
// Check we got a variablesReference that maps on to the fields.
expect(result.variablesReference, greaterThan(0));
await client.expectVariables(
result.variablesReference,
'''
isUtc: false
''',
);
});
);
});
test(
'evaluates complex expressions expressions with evaluateToStringInDebugViews=true',
() async {
final client = dap.client;
final testFile = await dap.createTestFile(simpleBreakpointProgram);
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
test(
'evaluates complex expressions expressions with evaluateToStringInDebugViews=true',
() async {
final client = dap.client;
final testFile = await dap.createTestFile(simpleBreakpointProgram);
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
final stop = await client.hitBreakpoint(
testFile,
breakpointLine,
launch: () =>
client.launch(testFile.path, evaluateToStringInDebugViews: true),
);
final stop = await client.hitBreakpoint(
testFile,
breakpointLine,
launch: () =>
client.launch(testFile.path, evaluateToStringInDebugViews: true),
);
await client.expectTopFrameEvalResult(
stop.threadId!,
'DateTime(2000, 1, 1)',
'DateTime (2000-01-01 00:00:00.000)',
);
});
await client.expectTopFrameEvalResult(
stop.threadId!,
'DateTime(2000, 1, 1)',
'DateTime (2000-01-01 00:00:00.000)',
);
});
test(
'evaluates $threadExceptionExpression to the threads exception (simple type)',
() async {
final client = dap.client;
final testFile = await dap.createTestFile(r'''
test(
'evaluates $threadExceptionExpression to the threads exception (simple type)',
() async {
final client = dap.client;
final testFile = await dap.createTestFile(r'''
void main(List<String> args) {
throw 'my error';
}''');
final stop = await client.hitException(testFile);
final stop = await client.hitException(testFile);
final result = await client.expectTopFrameEvalResult(
stop.threadId!,
threadExceptionExpression,
'"my error"',
);
expect(result.variablesReference, equals(0));
});
final result = await client.expectTopFrameEvalResult(
stop.threadId!,
threadExceptionExpression,
'"my error"',
);
expect(result.variablesReference, equals(0));
});
test(
'evaluates $threadExceptionExpression to the threads exception (complex type)',
() async {
final client = dap.client;
final testFile = await dap.createTestFile(r'''
test(
'evaluates $threadExceptionExpression to the threads exception (complex type)',
() async {
final client = dap.client;
final testFile = await dap.createTestFile(r'''
void main(List<String> args) {
throw Exception('my error');
}''');
final stop = await client.hitException(testFile);
final result = await client.expectTopFrameEvalResult(
stop.threadId!,
threadExceptionExpression,
'_Exception',
);
expect(result.variablesReference, greaterThan(0));
});
final stop = await client.hitException(testFile);
final result = await client.expectTopFrameEvalResult(
stop.threadId!,
threadExceptionExpression,
'_Exception',
);
expect(result.variablesReference, greaterThan(0));
});
test(
'evaluates $threadExceptionExpression.x.y to x.y on the threads exception',
() async {
final client = dap.client;
final testFile = await dap.createTestFile(r'''
test(
'evaluates $threadExceptionExpression.x.y to x.y on the threads exception',
() async {
final client = dap.client;
final testFile = await dap.createTestFile(r'''
void main(List<String> args) {
throw Exception('12345');
}
''');
final stop = await client.hitException(testFile);
await client.expectTopFrameEvalResult(
stop.threadId!,
'$threadExceptionExpression.message.length',
'5',
);
});
final stop = await client.hitException(testFile);
await client.expectTopFrameEvalResult(
stop.threadId!,
'$threadExceptionExpression.message.length',
'5',
);
});
test('can evaluate expressions in non-top frames', () async {
final client = dap.client;
final testFile = await dap.createTestFile(r'''
test('can evaluate expressions in non-top frames', () async {
final client = dap.client;
final testFile = await dap.createTestFile(r'''
void main(List<String> args) {
var a = 999;
foo();
@ -138,17 +143,16 @@ void main(List<String> args) {
void foo() {
var a = 111; // BREAKPOINT
}''');
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
final stop = await client.hitBreakpoint(testFile, breakpointLine);
final stack = await client.getValidStack(stop.threadId!,
startFrame: 0, numFrames: 2);
final secondFrameId = stack.stackFrames[1].id;
final stop = await client.hitBreakpoint(testFile, breakpointLine);
final stack = await client.getValidStack(stop.threadId!,
startFrame: 0, numFrames: 2);
final secondFrameId = stack.stackFrames[1].id;
await client.expectEvalResult(secondFrameId, 'a', '999');
});
await client.expectEvalResult(secondFrameId, 'a', '999');
});
// These tests can be slow due to starting up the external server process.
}, timeout: Timeout.none);
});
// These tests can be slow due to starting up the external server process.
}, timeout: Timeout.none);
}

View file

@ -7,10 +7,15 @@ import 'package:test/test.dart';
import 'test_support.dart';
main() {
testDap((dap) async {
group('debug mode', () {
test('prints messages from dart:developer log()', () async {
final testFile = dap.createTestFile(r'''
late DapTestSession dap;
setUp(() async {
dap = await DapTestSession.setUp();
});
tearDown(() => dap.tearDown());
group('debug mode', () {
test('prints messages from dart:developer log()', () async {
final testFile = dap.createTestFile(r'''
import 'dart:developer';
void main(List<String> args) async {
@ -19,19 +24,18 @@ void main(List<String> args) async {
}
''');
var outputEvents = await dap.client.collectOutput(file: testFile);
var outputEvents = await dap.client.collectOutput(file: testFile);
// Skip the first line because it's the VM Service connection info.
final output = outputEvents.skip(1).map((e) => e.output).join();
expectLines(output, [
'[log] this is a test',
' across two lines',
'[foo] this is a test',
'',
'Exited.',
]);
});
// These tests can be slow due to starting up the external server process.
}, timeout: Timeout.none);
});
// Skip the first line because it's the VM Service connection info.
final output = outputEvents.skip(1).map((e) => e.output).join();
expectLines(output, [
'[log] this is a test',
' across two lines',
'[foo] this is a test',
'',
'Exited.',
]);
});
// These tests can be slow due to starting up the external server process.
}, timeout: Timeout.none);
}

View file

@ -10,10 +10,15 @@ import 'test_scripts.dart';
import 'test_support.dart';
main() {
testDap((dap) async {
group('debug mode', () {
test('runs a simple script', () async {
final testFile = dap.createTestFile(r'''
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(r'''
void main(List<String> args) async {
print('Hello!');
print('World!');
@ -21,100 +26,99 @@ void main(List<String> args) async {
}
''');
final outputEvents = await dap.client.collectOutput(
launch: () => dap.client.launch(
testFile.path,
args: ['one', 'two'],
),
);
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 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.',
]);
});
// Expect the normal applications output.
final output = outputEvents.skip(1).map((e) => e.output).join();
expectLines(output, [
'Hello!',
'World!',
'args: [one, two]',
'',
'Exited.',
]);
});
test('provides a list of threads', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
test('provides a list of threads', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
await client.hitBreakpoint(testFile, breakpointLine);
final response = await client.getValidThreads();
await client.hitBreakpoint(testFile, breakpointLine);
final response = await client.getValidThreads();
expect(response.threads, hasLength(1));
expect(response.threads.first.name, equals('main'));
});
expect(response.threads, hasLength(1));
expect(response.threads.first.name, equals('main'));
});
test('runs with DDS', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
test('runs with DDS by default', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
await client.hitBreakpoint(testFile, breakpointLine);
expect(await client.ddsAvailable, isTrue);
});
// These tests can be slow due to starting up the external server process.
}, timeout: Timeout.none);
await client.hitBreakpoint(testFile, breakpointLine);
expect(await client.ddsAvailable, isTrue);
});
test('runs with auth codes enabled', () async {
test('runs with auth codes enabled by default', () async {
final testFile = dap.createTestFile(emptyProgram);
final outputEvents = await dap.client.collectOutput(file: testFile);
expect(_hasAuthCode(outputEvents.first), isTrue);
final vmServiceUri = _extractVmServiceUri(outputEvents.first);
expect(vmServiceUri.path, matches(vmServiceAuthCodePathPattern));
});
});
// These tests can be slow due to starting up the external server process.
}, timeout: Timeout.none);
testDap((dap) async {
group('debug mode', () {
test('runs without DDS', () async {
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
group('debug mode', () {
test('can run without DDS', () async {
final dap = await DapTestSession.setUp(additionalArgs: ['--no-dds']);
addTearDown(dap.tearDown);
await client.hitBreakpoint(testFile, breakpointLine);
final client = dap.client;
final testFile = dap.createTestFile(simpleBreakpointProgram);
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
expect(await client.ddsAvailable, isFalse);
});
await client.hitBreakpoint(testFile, breakpointLine);
test('runs with auth tokens disabled', () async {
final testFile = dap.createTestFile(emptyProgram);
expect(await client.ddsAvailable, isFalse);
});
final outputEvents = await dap.client.collectOutput(file: testFile);
expect(_hasAuthCode(outputEvents.first), isFalse);
});
// These tests can be slow due to starting up the external server process.
}, timeout: Timeout.none);
}, additionalArgs: ['--no-dds', '--no-auth-codes']);
test('can run without auth codes', () async {
final dap =
await DapTestSession.setUp(additionalArgs: ['--no-auth-codes']);
addTearDown(dap.tearDown);
testDap((dap) async {
group('debug mode', () {
test('can run with ipv6', () async {
final testFile = dap.createTestFile(emptyProgram);
final testFile = dap.createTestFile(emptyProgram);
final outputEvents = await dap.client.collectOutput(file: testFile);
final vmServiceUri = _extractVmServiceUri(outputEvents.first);
expect(vmServiceUri.path, isNot(matches(vmServiceAuthCodePathPattern)));
});
final outputEvents = await dap.client.collectOutput(file: testFile);
final vmServiceUri = _extractVmServiceUri(outputEvents.first);
test('can run with ipv6', () async {
final dap = await DapTestSession.setUp(additionalArgs: ['--ipv6']);
addTearDown(dap.tearDown);
// Check DAP server host.
expect(dap.server.host, equals('::1'));
// Check VM Service/DDS host.
expect(vmServiceUri.host, equals('::1'));
});
// These tests can be slow due to starting up the external server process.
}, timeout: Timeout.none);
}, additionalArgs: ['--ipv6']);
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
@ -122,15 +126,6 @@ void main(List<String> args) async {
Uri _extractVmServiceUri(OutputEventBody vmConnectionBanner) {
// TODO(dantup): Change this to use the dart.debuggerUris custom event
// if implemented (whch VS Code also needs).
final vmServiceUriPattern = RegExp(r'Connecting to VM Service at ([^\s]+)\s');
final match = vmServiceUriPattern.firstMatch(vmConnectionBanner.output);
return Uri.parse(match!.group(1)!);
}
/// Checks for the presence of an auth token in a VM Service URI in the
/// "Connecting to VM Service" [OutputEvent].
bool _hasAuthCode(OutputEventBody vmConnectionBanner) {
final vmServiceUri = _extractVmServiceUri(vmConnectionBanner);
final authCodePattern = RegExp(r'^/[\w=]{5,15}/ws');
return authCodePattern.hasMatch(vmServiceUri.path);
}

View file

@ -8,11 +8,16 @@ import 'test_client.dart';
import 'test_support.dart';
main() {
testDap((dap) async {
group('debug mode variables', () {
test('provides variable list for frames', () async {
final client = dap.client;
final testFile = await dap.createTestFile(r'''
late DapTestSession dap;
setUp(() async {
dap = await DapTestSession.setUp();
});
tearDown(() => dap.tearDown());
group('debug mode variables', () {
test('provides variable list for frames', () async {
final client = dap.client;
final testFile = await dap.createTestFile(r'''
void main(List<String> args) {
final myVariable = 1;
foo();
@ -23,135 +28,135 @@ void foo() {
print('Hello!'); // BREAKPOINT
}
''');
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
final stop = await client.hitBreakpoint(testFile, breakpointLine);
final stack = await client.getValidStack(
stop.threadId!,
startFrame: 0,
numFrames: 2,
);
final stop = await client.hitBreakpoint(testFile, breakpointLine);
final stack = await client.getValidStack(
stop.threadId!,
startFrame: 0,
numFrames: 2,
);
// Check top two frames (in `foo` and in `main`).
await client.expectScopeVariables(
stack.stackFrames[0].id, // Top frame: foo
'Variables',
'''
// Check top two frames (in `foo` and in `main`).
await client.expectScopeVariables(
stack.stackFrames[0].id, // Top frame: foo
'Variables',
'''
b: 2
''',
);
await client.expectScopeVariables(
stack.stackFrames[1].id, // Second frame: main
'Variables',
'''
);
await client.expectScopeVariables(
stack.stackFrames[1].id, // Second frame: main
'Variables',
'''
args: List (0 items)
myVariable: 1
''',
);
});
);
});
test('provides simple exception types for frames', () async {
final client = dap.client;
final testFile = await dap.createTestFile(r'''
test('provides simple exception types for frames', () async {
final client = dap.client;
final testFile = await dap.createTestFile(r'''
void main(List<String> args) {
throw 'my error';
}
''');
final stop = await client.hitException(testFile);
final stack = await client.getValidStack(
stop.threadId!,
startFrame: 0,
numFrames: 1,
);
final topFrameId = stack.stackFrames.first.id;
final stop = await client.hitException(testFile);
final stack = await client.getValidStack(
stop.threadId!,
startFrame: 0,
numFrames: 1,
);
final topFrameId = stack.stackFrames.first.id;
// Check for an additional Scope named "Exceptions" that includes the
// exception.
await client.expectScopeVariables(
topFrameId,
'Exceptions',
'''
// Check for an additional Scope named "Exceptions" that includes the
// exception.
await client.expectScopeVariables(
topFrameId,
'Exceptions',
'''
String: "my error"
''',
);
});
);
});
test('provides complex exception types frames', () async {
final client = dap.client;
final testFile = await dap.createTestFile(r'''
test('provides complex exception types frames', () async {
final client = dap.client;
final testFile = await dap.createTestFile(r'''
void main(List<String> args) {
throw ArgumentError.notNull('args');
}
''');
final stop = await client.hitException(testFile);
final stack = await client.getValidStack(
stop.threadId!,
startFrame: 0,
numFrames: 1,
);
final topFrameId = stack.stackFrames.first.id;
final stop = await client.hitException(testFile);
final stack = await client.getValidStack(
stop.threadId!,
startFrame: 0,
numFrames: 1,
);
final topFrameId = stack.stackFrames.first.id;
// Check for an additional Scope named "Exceptions" that includes the
// exception.
await client.expectScopeVariables(
topFrameId,
'Exceptions',
// TODO(dantup): evaluateNames
'''
// Check for an additional Scope named "Exceptions" that includes the
// exception.
await client.expectScopeVariables(
topFrameId,
'Exceptions',
// TODO(dantup): evaluateNames
'''
invalidValue: null
message: "Must not be null"
name: "args"
''',
);
});
);
});
test('includes simple variable fields', () async {
final client = dap.client;
final testFile = await dap.createTestFile(r'''
test('includes simple variable fields', () async {
final client = dap.client;
final testFile = await dap.createTestFile(r'''
void main(List<String> args) {
final myVariable = DateTime(2000, 1, 1);
print('Hello!'); // BREAKPOINT
}
''');
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
final stop = await client.hitBreakpoint(testFile, breakpointLine);
await client.expectLocalVariable(
stop.threadId!,
expectedName: 'myVariable',
expectedDisplayString: 'DateTime',
expectedVariables: '''
final stop = await client.hitBreakpoint(testFile, breakpointLine);
await client.expectLocalVariable(
stop.threadId!,
expectedName: 'myVariable',
expectedDisplayString: 'DateTime',
expectedVariables: '''
isUtc: false
''',
);
});
);
});
test('includes variable getters when evaluateGettersInDebugViews=true',
() async {
final client = dap.client;
final testFile = await dap.createTestFile(r'''
test('includes variable getters when evaluateGettersInDebugViews=true',
() async {
final client = dap.client;
final testFile = await dap.createTestFile(r'''
void main(List<String> args) {
final myVariable = DateTime(2000, 1, 1);
print('Hello!'); // BREAKPOINT
}
''');
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
final stop = await client.hitBreakpoint(
testFile,
breakpointLine,
launch: () => client.launch(
testFile.path,
evaluateGettersInDebugViews: true,
),
);
await client.expectLocalVariable(
stop.threadId!,
expectedName: 'myVariable',
expectedDisplayString: 'DateTime',
expectedVariables: '''
final stop = await client.hitBreakpoint(
testFile,
breakpointLine,
launch: () => client.launch(
testFile.path,
evaluateGettersInDebugViews: true,
),
);
await client.expectLocalVariable(
stop.threadId!,
expectedName: 'myVariable',
expectedDisplayString: 'DateTime',
expectedVariables: '''
day: 1
hour: 0
isUtc: false
@ -165,72 +170,71 @@ void main(List<String> args) {
weekday: 6
year: 2000
''',
ignore: {
// Don't check fields that may very based on timezone as it'll make
// these tests fragile, and this isn't really what's being tested.
'timeZoneName',
'microsecondsSinceEpoch',
'millisecondsSinceEpoch',
},
);
});
ignore: {
// Don't check fields that may very based on timezone as it'll make
// these tests fragile, and this isn't really what's being tested.
'timeZoneName',
'microsecondsSinceEpoch',
'millisecondsSinceEpoch',
},
);
});
test('renders a simple list', () async {
final client = dap.client;
final testFile = await dap.createTestFile(r'''
test('renders a simple list', () async {
final client = dap.client;
final testFile = await dap.createTestFile(r'''
void main(List<String> args) {
final myVariable = ["first", "second", "third"];
print('Hello!'); // BREAKPOINT
}
''');
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
final stop = await client.hitBreakpoint(testFile, breakpointLine);
await client.expectLocalVariable(
stop.threadId!,
expectedName: 'myVariable',
expectedDisplayString: 'List (3 items)',
// TODO(dantup): evaluateNames
expectedVariables: '''
final stop = await client.hitBreakpoint(testFile, breakpointLine);
await client.expectLocalVariable(
stop.threadId!,
expectedName: 'myVariable',
expectedDisplayString: 'List (3 items)',
// TODO(dantup): evaluateNames
expectedVariables: '''
0: "first"
1: "second"
2: "third"
''',
);
});
);
});
test('renders a simple list subset', () async {
final client = dap.client;
final testFile = await dap.createTestFile(r'''
test('renders a simple list subset', () async {
final client = dap.client;
final testFile = await dap.createTestFile(r'''
void main(List<String> args) {
final myVariable = ["first", "second", "third"];
print('Hello!'); // BREAKPOINT
}
''');
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
final breakpointLine = lineWith(testFile, '// BREAKPOINT');
final stop = await client.hitBreakpoint(testFile, breakpointLine);
await client.expectLocalVariable(
stop.threadId!,
expectedName: 'myVariable',
expectedDisplayString: 'List (3 items)',
// TODO(dantup): evaluateNames
expectedVariables: '''
final stop = await client.hitBreakpoint(testFile, breakpointLine);
await client.expectLocalVariable(
stop.threadId!,
expectedName: 'myVariable',
expectedDisplayString: 'List (3 items)',
// TODO(dantup): evaluateNames
expectedVariables: '''
1: "second"
''',
start: 1,
count: 1,
);
});
start: 1,
count: 1,
);
});
test('renders a simple map', () {
// TODO(dantup): Implement this (inc evaluateNames)
}, skip: true);
test('renders a simple map', () {
// TODO(dantup): Implement this (inc evaluateNames)
}, skip: true);
test('renders a simple map subset', () {
// TODO(dantup): Implement this (inc evaluateNames)
}, skip: true);
// These tests can be slow due to starting up the external server process.
}, timeout: Timeout.none);
});
test('renders a simple map subset', () {
// TODO(dantup): Implement this (inc evaluateNames)
}, skip: true);
// These tests can be slow due to starting up the external server process.
}, timeout: Timeout.none);
}

View file

@ -7,10 +7,15 @@ import 'package:test/test.dart';
import 'test_support.dart';
main() {
testDap((dap) async {
group('noDebug mode', () {
test('runs a simple script', () async {
final testFile = dap.createTestFile(r'''
late DapTestSession dap;
setUp(() async {
dap = await DapTestSession.setUp();
});
tearDown(() => dap.tearDown());
group('noDebug mode', () {
test('runs a simple script', () async {
final testFile = dap.createTestFile(r'''
void main(List<String> args) async {
print('Hello!');
print('World!');
@ -18,24 +23,23 @@ void main(List<String> args) async {
}
''');
final outputEvents = await dap.client.collectOutput(
launch: () => dap.client.launch(
testFile.path,
noDebug: true,
args: ['one', 'two'],
),
);
final outputEvents = await dap.client.collectOutput(
launch: () => dap.client.launch(
testFile.path,
noDebug: true,
args: ['one', 'two'],
),
);
final output = outputEvents.map((e) => e.output).join();
expectLines(output, [
'Hello!',
'World!',
'args: [one, two]',
'',
'Exited.',
]);
});
// These tests can be slow due to starting up the external server process.
}, timeout: Timeout.none);
});
final output = outputEvents.map((e) => e.output).join();
expectLines(output, [
'Hello!',
'World!',
'args: [one, two]',
'',
'Exited.',
]);
});
// These tests can be slow due to starting up the external server process.
}, timeout: Timeout.none);
}

View file

@ -10,7 +10,6 @@ import 'package:dds/src/dap/adapters/dart.dart';
import 'package:dds/src/dap/logging.dart';
import 'package:dds/src/dap/protocol_generated.dart';
import 'package:dds/src/dap/protocol_stream.dart';
import 'package:dds/src/dap/protocol_stream_transformers.dart';
import 'package:test/test.dart';
import 'package:vm_service/vm_service.dart' as vm;
@ -22,19 +21,17 @@ import 'test_server.dart';
/// Methods on this class should map directly to protocol methods. Additional
/// helpers are available in [DapTestClientExtension].
class DapTestClient {
final Socket _socket;
final ByteStreamServerChannel _channel;
late final StreamSubscription<String> _subscription;
final Logger? _logger;
final bool captureVmServiceTraffic;
final _requestWarningDuration = const Duration(seconds: 2);
final _requestWarningDuration = const Duration(seconds: 5);
final Map<int, _OutgoingRequest> _pendingRequests = {};
final _eventController = StreamController<Event>.broadcast();
int _seq = 1;
DapTestClient._(
this._socket,
this._channel,
this._logger, {
this.captureVmServiceTraffic = false,
@ -226,7 +223,6 @@ class DapTestClient {
Future<void> stop() async {
_channel.close();
await _socket.close();
await _subscription.cancel();
}
@ -305,16 +301,12 @@ class DapTestClient {
/// Creates a [DapTestClient] that connects the server listening on
/// [host]:[port].
static Future<DapTestClient> connect(
String host,
int port, {
DapTestServer server, {
bool captureVmServiceTraffic = false,
Logger? logger,
}) async {
final socket = await Socket.connect(host, port);
final channel = ByteStreamServerChannel(
socket.transform(Uint8ListTransformer()), socket, logger);
return DapTestClient._(socket, channel, logger,
final channel = ByteStreamServerChannel(server.stream, server.sink, logger);
return DapTestClient._(channel, logger,
captureVmServiceTraffic: captureVmServiceTraffic);
}
}

View file

@ -13,10 +13,9 @@ import 'package:path/path.dart' as path;
import 'package:pedantic/pedantic.dart';
abstract class DapTestServer {
List<String> get errorLogs;
String get host;
int get port;
Future<void> stop();
StreamSink<List<int>> get sink;
Stream<List<int>> get stream;
}
/// An instance of a DAP server running in-process (to aid debugging).
@ -25,22 +24,27 @@ abstract class DapTestServer {
/// serialized and deserialized but it's not quite the same running out of
/// process.
class InProcessDapTestServer extends DapTestServer {
final DapServer _server;
late final DapServer _server;
final stdinController = StreamController<List<int>>();
final stdoutController = StreamController<List<int>>();
InProcessDapTestServer._(this._server);
StreamSink<List<int>> get sink => stdinController.sink;
Stream<List<int>> get stream => stdoutController.stream;
String get host => _server.host;
int get port => _server.port;
List<String> get errorLogs => const []; // In-proc errors just throw in-line.
InProcessDapTestServer._() {
_server = DapServer(stdinController.stream, stdoutController.sink);
}
@override
Future<void> stop() async {
await _server.stop();
_server.stop();
}
static Future<InProcessDapTestServer> create({Logger? logger}) async {
final DapServer server = await DapServer.create(logger: logger);
return InProcessDapTestServer._(server);
static Future<InProcessDapTestServer> create({
Logger? logger,
List<String>? additionalArgs,
}) async {
return InProcessDapTestServer._();
}
}
@ -52,28 +56,22 @@ class InProcessDapTestServer extends DapTestServer {
class OutOfProcessDapTestServer extends DapTestServer {
var _isShuttingDown = false;
final Process _process;
final int port;
final String host;
final List<String> _errors = [];
List<String> get errorLogs => _errors;
StreamSink<List<int>> get sink => _process.stdin;
Stream<List<int>> get stream => _process.stdout;
OutOfProcessDapTestServer._(
this._process,
this.host,
this.port,
Logger? logger,
) {
// Treat anything written to stderr as the DAP crashing and fail the test.
_process.stderr.transform(utf8.decoder).listen((error) {
logger?.call(error);
_errors.add(error);
throw error;
});
unawaited(_process.exitCode.then((code) {
final message = 'Out-of-process DAP server terminated with code $code';
logger?.call(message);
_errors.add(message);
if (!_isShuttingDown && code != 0) {
throw message;
}
@ -107,27 +105,6 @@ class OutOfProcessDapTestServer extends DapTestServer {
],
);
final startedCompleter = Completer<void>();
late String host;
late int port;
// Scrape the `started` event to get the host/port. Any other output
// should be sent to the logger (as it may be verbose output for diagnostic
// purposes).
_process.stdout.transform(utf8.decoder).listen((text) {
if (!startedCompleter.isCompleted) {
final event = jsonDecode(text);
if (event['state'] == 'started') {
host = event['dapHost'];
port = event['dapPort'];
startedCompleter.complete();
return;
}
}
logger?.call(text);
});
await startedCompleter.future;
return OutOfProcessDapTestServer._(_process, host, port, logger);
return OutOfProcessDapTestServer._(_process, logger);
}
}

View file

@ -12,20 +12,6 @@ import 'package:test/test.dart';
import 'test_client.dart';
import 'test_server.dart';
/// A logger to use to log all traffic (both DAP and VM) to stdout.
///
/// If the enviroment variable is `DAP_TEST_VERBOSE` then `print` will be used,
/// otherwise there will be no verbose logging.
///
/// DAP_TEST_VERBOSE=true pub run test --chain-stack-traces test/dap/integration
///
///
/// When using the out-of-process DAP, this causes `--verbose` to be passed to
/// the server which causes it to write all traffic to `stdout` which is then
/// picked up by [OutOfProcessDapTestServer] and passed to this logger.
final logger =
Platform.environment['DAP_TEST_VERBOSE'] == 'true' ? print : null;
/// Whether to run the DAP server in-process with the tests, or externally in
/// another process.
///
@ -35,6 +21,14 @@ final logger =
/// simplified in VS Code by using a launch config with custom CodeLens links).
final useInProcessDap = Platform.environment['DAP_TEST_INTERNAL'] == 'true';
/// A [RegExp] that matches the `path` part of a VM Service URI that contains
/// an authentication token.
final vmServiceAuthCodePathPattern = RegExp(r'^/[\w_\-=]{5,15}/ws$');
/// A [RegExp] that matches the "Connecting to VM Service" banner that is sent
/// as the first output event for a debug session.
final vmServiceUriPattern = RegExp(r'Connecting to VM Service at ([^\s]+)\s');
/// Expects [actual] to equal the lines [expected], ignoring differences in line
/// endings.
void expectLines(String actual, List<String> expected) {
@ -45,33 +39,13 @@ void expectLines(String actual, List<String> expected) {
int lineWith(File file, String searchText) =>
file.readAsLinesSync().indexWhere((line) => line.contains(searchText)) + 1;
/// A helper function to wrap all tests in a library with setup/teardown functions
/// to start a shared server for all tests in the library and an individual
/// client for each test.
testDap(
Future<void> Function(DapTestSession session) tests, {
List<String>? additionalArgs,
}) {
final session = DapTestSession(additionalArgs: additionalArgs);
setUpAll(session.setUpAll);
tearDownAll(session.tearDownAll);
setUp(session.setUp);
tearDown(session.tearDown);
return tests(session);
}
/// A helper class provided to DAP integration tests run with [testDap] to
/// easily share setup/teardown without sharing state across tests from different
/// files.
/// A helper class containing the DAP server/client for DAP integration tests.
class DapTestSession {
late DapTestServer server;
late DapTestClient client;
DapTestServer server;
DapTestClient client;
final _testFolders = <Directory>[];
final List<String>? additionalArgs;
DapTestSession({this.additionalArgs});
DapTestSession._(this.server, this.client);
/// Creates a file in a temporary folder to be used as an application for testing.
///
@ -84,60 +58,32 @@ class DapTestSession {
return testFile;
}
Future<void> setUp() async {
client = await _startClient(server);
static Future<DapTestSession> setUp({List<String>? additionalArgs}) async {
final server = await _startServer(additionalArgs: additionalArgs);
final client = await DapTestClient.connect(server);
return DapTestSession._(server, client);
}
Future<void> setUpAll() async {
server = await _startServer(logger: logger, additionalArgs: additionalArgs);
}
Future<void> tearDown() => client.stop();
Future<void> tearDownAll() async {
Future<void> tearDown() async {
await client.stop();
await server.stop();
// Clean up any temp folders created during the test runs.
_testFolders.forEach((dir) => dir.deleteSync(recursive: true));
}
/// Creates and connects a new [DapTestClient] to [server].
Future<DapTestClient> _startClient(DapTestServer server) async {
// Since we don't get a signal from the DAP server when it's ready and we
// just started it, add a short retry to connections.
// Since the bots can be quite slow, it may take 6-7 seconds for the server
// to initially start up (including compilation).
var attempt = 1;
while (attempt++ <= 100) {
try {
return await DapTestClient.connect(server.host, server.port);
} catch (e) {
await Future.delayed(const Duration(milliseconds: 200));
}
}
final errorMessage = StringBuffer();
errorMessage.writeln(
'Failed to connect to DAP server on port ${server.port}'
' after $attempt attempts. Did the server start correctly?',
);
final serverErrorLogs = server.errorLogs;
if (serverErrorLogs.isNotEmpty) {
errorMessage.writeln('Server errors:');
errorMessage.writeAll(serverErrorLogs);
}
throw Exception(errorMessage.toString());
_testFolders
..forEach((dir) => dir.deleteSync(recursive: true))
..clear();
}
/// Starts a DAP server that can be shared across tests.
Future<DapTestServer> _startServer({
static Future<DapTestServer> _startServer({
Logger? logger,
List<String>? additionalArgs,
}) async {
return useInProcessDap
? await InProcessDapTestServer.create(logger: logger)
? await InProcessDapTestServer.create(
logger: logger,
additionalArgs: additionalArgs,
)
: await OutOfProcessDapTestServer.create(
logger: logger,
additionalArgs: additionalArgs,

View file

@ -2,7 +2,7 @@
// 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:convert';
import 'dart:async';
import 'dart:io';
import 'package:args/command_runner.dart';
@ -12,7 +12,7 @@ Future<void> main(List<String> arguments) async {
// TODO(dantup): "dap_tool" is a placeholder and will likely eventually be a
// "dart" command.
final runner = CommandRunner('dap_tool', 'Dart DAP Tool')
..addCommand(DapCommand());
..addCommand(DapCommand(stdin, stdout.nonBlocking));
try {
await runner.run(arguments);
@ -23,33 +23,22 @@ Future<void> main(List<String> arguments) async {
}
class DapCommand extends Command {
static const argHost = 'host';
static const argPort = 'port';
static const argIpv6 = 'ipv6';
static const argDds = 'dds';
static const argAuthCodes = 'auth-codes';
static const argVerbose = 'verbose';
final Stream<List<int>> _inputStream;
final StreamSink<List<int>> _outputSink;
@override
final String description = 'Start a DAP debug server.';
@override
final String name = 'dap';
DapCommand() {
DapCommand(this._inputStream, this._outputSink) {
argParser
..addOption(
argHost,
help: 'The hostname/IP to bind the server to. If not supplied, will'
' use the appropriate loopback address depending on whether'
' --ipv6 is set',
)
..addOption(
argPort,
abbr: 'p',
defaultsTo: '0',
help: 'The port to bind the server to',
)
..addFlag(
argIpv6,
help: 'Whether to bind DAP/VM Service/DDS to IPv6 addresses',
@ -63,33 +52,21 @@ class DapCommand extends Command {
argAuthCodes,
defaultsTo: true,
help: 'Whether to enable authentication codes for VM Services',
)
..addFlag(
argVerbose,
abbr: 'v',
help: 'Whether to print diagnostic output to stdout',
);
}
Future<void> run() async {
final args = argResults!;
final port = int.parse(args[argPort]);
final host = args[argHost];
final ipv6 = args[argIpv6] as bool;
final server = await DapServer.create(
host: host,
port: port,
final server = DapServer(
_inputStream,
_outputSink,
ipv6: ipv6,
enableDdds: args[argDds],
enableDds: args[argDds],
enableAuthCodes: args[argAuthCodes],
logger: args[argVerbose] ? print : null,
);
stdout.write(jsonEncode({
'state': 'started',
'dapHost': server.host,
'dapPort': server.port,
}));
await server.channel.closed;
}
}