mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
421 lines
16 KiB
Dart
421 lines
16 KiB
Dart
// Copyright 2014 The Flutter Authors. 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 'package:dds/src/dap/logging.dart';
|
|
import 'package:dds/src/dap/protocol_generated.dart';
|
|
import 'package:dds/src/dap/protocol_stream.dart';
|
|
import 'package:flutter_tools/src/debug_adapters/flutter_adapter_args.dart';
|
|
|
|
import 'test_server.dart';
|
|
|
|
/// A helper class to simplify acting as a client for interacting with the
|
|
/// [DapTestServer] in tests.
|
|
///
|
|
/// Methods on this class should map directly to protocol methods. Additional
|
|
/// helpers are available in [DapTestClientExtension].
|
|
class DapTestClient {
|
|
DapTestClient._(
|
|
this._channel,
|
|
this._logger, {
|
|
this.captureVmServiceTraffic = false,
|
|
}) {
|
|
// Set up a future that will complete when the 'dart.debuggerUris' event is
|
|
// emitted by the debug adapter so tests have easy access to it.
|
|
vmServiceUri = event('dart.debuggerUris').then<Uri?>((Event event) {
|
|
final Map<String, Object?> body = event.body! as Map<String, Object?>;
|
|
return Uri.parse(body['vmServiceUri']! as String);
|
|
}).catchError((Object? e) => null);
|
|
|
|
_subscription = _channel.listen(
|
|
_handleMessage,
|
|
onDone: () {
|
|
if (_pendingRequests.isNotEmpty) {
|
|
_logger?.call(
|
|
'Application terminated without a response to ${_pendingRequests.length} requests');
|
|
}
|
|
_pendingRequests.forEach((int id, _OutgoingRequest request) => request.completer.completeError(
|
|
'Application terminated without a response to request $id (${request.name})'));
|
|
_pendingRequests.clear();
|
|
},
|
|
);
|
|
}
|
|
|
|
final ByteStreamServerChannel _channel;
|
|
late final StreamSubscription<String> _subscription;
|
|
final Logger? _logger;
|
|
final bool captureVmServiceTraffic;
|
|
final Map<int, _OutgoingRequest> _pendingRequests = <int, _OutgoingRequest>{};
|
|
final StreamController<Event> _eventController = StreamController<Event>.broadcast();
|
|
int _seq = 1;
|
|
late final Future<Uri?> vmServiceUri;
|
|
|
|
/// Returns a stream of [OutputEventBody] events.
|
|
Stream<OutputEventBody> get outputEvents => events('output')
|
|
.map((Event e) => OutputEventBody.fromJson(e.body! as Map<String, Object?>));
|
|
|
|
/// Returns a stream of [StoppedEventBody] events.
|
|
Stream<StoppedEventBody> get stoppedEvents => events('stopped')
|
|
.map((Event e) => StoppedEventBody.fromJson(e.body! as Map<String, Object?>));
|
|
|
|
/// Returns a stream of the string output from [OutputEventBody] events.
|
|
Stream<String> get output => outputEvents.map((OutputEventBody output) => output.output);
|
|
|
|
/// Returns a stream of the string output from [OutputEventBody] events with the category 'stdout'.
|
|
Stream<String> get stdoutOutput => outputEvents
|
|
.where((OutputEventBody output) => output.category == 'stdout')
|
|
.map((OutputEventBody output) => output.output);
|
|
|
|
/// Sends a custom request to the server and waits for a response.
|
|
Future<Response> custom(String name, [Object? args]) async {
|
|
return sendRequest(args, overrideCommand: name);
|
|
}
|
|
|
|
/// Returns a Future that completes with the next [event] event.
|
|
Future<Event> event(String event) => _eventController.stream.firstWhere(
|
|
(Event e) => e.event == event,
|
|
orElse: () => throw Exception('Did not receive $event event before stream closed'));
|
|
|
|
/// Returns a stream for [event] events.
|
|
Stream<Event> events(String event) {
|
|
return _eventController.stream.where((Event e) => e.event == event);
|
|
}
|
|
|
|
/// Returns a stream of custom 'dart.serviceExtensionAdded' events.
|
|
Stream<Map<String, Object?>> get serviceExtensionAddedEvents =>
|
|
events('dart.serviceExtensionAdded')
|
|
.map((Event e) => e.body! as Map<String, Object?>);
|
|
|
|
/// Returns a stream of custom 'flutter.serviceExtensionStateChanged' events.
|
|
Stream<Map<String, Object?>> get serviceExtensionStateChangedEvents =>
|
|
events('flutter.serviceExtensionStateChanged')
|
|
.map((Event e) => e.body! as Map<String, Object?>);
|
|
|
|
/// Returns a stream of 'dart.testNotification' custom events from the
|
|
/// package:test JSON reporter.
|
|
Stream<Map<String, Object?>> get testNotificationEvents =>
|
|
events('dart.testNotification')
|
|
.map((Event e) => e.body! as Map<String, Object?>);
|
|
|
|
/// Sends a custom request to the debug adapter to trigger a Hot Reload.
|
|
Future<Response> hotReload() {
|
|
return custom('hotReload');
|
|
}
|
|
|
|
/// Sends a custom request to the debug adapter to trigger a Hot Restart.
|
|
Future<Response> hotRestart() {
|
|
return custom('hotRestart');
|
|
}
|
|
|
|
/// Send an initialize request to the server.
|
|
///
|
|
/// This occurs before the request to start running/debugging a script and is
|
|
/// used to exchange capabilities and send breakpoints and other settings.
|
|
Future<Response> initialize({
|
|
String exceptionPauseMode = 'None',
|
|
bool? supportsRunInTerminalRequest,
|
|
}) async {
|
|
final List<ProtocolMessage> responses = await Future.wait(<Future<ProtocolMessage>>[
|
|
event('initialized'),
|
|
sendRequest(InitializeRequestArguments(
|
|
adapterID: 'test',
|
|
supportsRunInTerminalRequest: supportsRunInTerminalRequest,
|
|
)),
|
|
sendRequest(
|
|
SetExceptionBreakpointsArguments(
|
|
filters: <String>[exceptionPauseMode],
|
|
),
|
|
),
|
|
]);
|
|
await sendRequest(ConfigurationDoneArguments());
|
|
return responses[1] as Response; // Return the initialize response.
|
|
}
|
|
|
|
/// Send a launchRequest to the server, asking it to start a Flutter app.
|
|
Future<Response> launch({
|
|
String? program,
|
|
List<String>? args,
|
|
List<String>? toolArgs,
|
|
String? cwd,
|
|
bool? noDebug,
|
|
List<String>? additionalProjectPaths,
|
|
bool? debugSdkLibraries,
|
|
bool? debugExternalPackageLibraries,
|
|
bool? evaluateGettersInDebugViews,
|
|
bool? evaluateToStringInDebugViews,
|
|
}) {
|
|
return sendRequest(
|
|
FlutterLaunchRequestArguments(
|
|
noDebug: noDebug,
|
|
program: program,
|
|
cwd: cwd,
|
|
args: args,
|
|
toolArgs: toolArgs,
|
|
additionalProjectPaths: additionalProjectPaths,
|
|
debugSdkLibraries: debugSdkLibraries,
|
|
debugExternalPackageLibraries: debugExternalPackageLibraries,
|
|
evaluateGettersInDebugViews: evaluateGettersInDebugViews,
|
|
evaluateToStringInDebugViews: evaluateToStringInDebugViews,
|
|
// When running out of process, VM Service traffic won't be available
|
|
// to the client-side logger, so force logging on which sends VM Service
|
|
// traffic in a custom event.
|
|
sendLogsToClient: captureVmServiceTraffic,
|
|
),
|
|
// We can't automatically pick the command when using a custom type
|
|
// (FlutterLaunchRequestArguments).
|
|
overrideCommand: 'launch',
|
|
);
|
|
}
|
|
|
|
/// Send an attachRequest to the server, asking it to attach to an already-running Flutter app.
|
|
Future<Response> attach({
|
|
List<String>? toolArgs,
|
|
String? vmServiceUri,
|
|
String? cwd,
|
|
List<String>? additionalProjectPaths,
|
|
bool? debugSdkLibraries,
|
|
bool? debugExternalPackageLibraries,
|
|
bool? evaluateGettersInDebugViews,
|
|
bool? evaluateToStringInDebugViews,
|
|
}) {
|
|
return sendRequest(
|
|
FlutterAttachRequestArguments(
|
|
cwd: cwd,
|
|
toolArgs: toolArgs,
|
|
vmServiceUri: vmServiceUri,
|
|
additionalProjectPaths: additionalProjectPaths,
|
|
debugSdkLibraries: debugSdkLibraries,
|
|
debugExternalPackageLibraries: debugExternalPackageLibraries,
|
|
evaluateGettersInDebugViews: evaluateGettersInDebugViews,
|
|
evaluateToStringInDebugViews: evaluateToStringInDebugViews,
|
|
// When running out of process, VM Service traffic won't be available
|
|
// to the client-side logger, so force logging on which sends VM Service
|
|
// traffic in a custom event.
|
|
sendLogsToClient: captureVmServiceTraffic,
|
|
),
|
|
// We can't automatically pick the command when using a custom type
|
|
// (FlutterAttachRequestArguments).
|
|
overrideCommand: 'attach',
|
|
);
|
|
}
|
|
|
|
/// Sends an arbitrary request to the server.
|
|
///
|
|
/// Returns a Future that completes when the server returns a corresponding
|
|
/// response.
|
|
Future<Response> sendRequest(Object? arguments,
|
|
{bool allowFailure = false, String? overrideCommand}) {
|
|
final String command = overrideCommand ?? commandTypes[arguments.runtimeType]!;
|
|
final Request request =
|
|
Request(seq: _seq++, command: command, arguments: arguments);
|
|
final Completer<Response> completer = Completer<Response>();
|
|
_pendingRequests[request.seq] =
|
|
_OutgoingRequest(completer, command, allowFailure);
|
|
_channel.sendRequest(request);
|
|
return completer.future;
|
|
}
|
|
|
|
/// Returns a Future that completes with the next serviceExtensionAdded
|
|
/// event for [extension].
|
|
Future<Map<String, Object?>> serviceExtensionAdded(String extension) => serviceExtensionAddedEvents.firstWhere(
|
|
(Map<String, Object?> body) => body['extensionRPC'] == extension,
|
|
orElse: () => throw Exception('Did not receive $extension extension added event before stream closed'));
|
|
|
|
/// Returns a Future that completes with the next serviceExtensionStateChanged
|
|
/// event for [extension].
|
|
Future<Map<String, Object?>> serviceExtensionStateChanged(String extension) => serviceExtensionStateChangedEvents.firstWhere(
|
|
(Map<String, Object?> body) => body['extension'] == extension,
|
|
orElse: () => throw Exception('Did not receive $extension extension state changed event before stream closed'));
|
|
|
|
/// Initializes the debug adapter and launches [program]/[cwd] or calls the
|
|
/// custom [launch] method.
|
|
Future<void> start({
|
|
String? program,
|
|
String? cwd,
|
|
String exceptionPauseMode = 'None',
|
|
Future<Object?> Function()? launch,
|
|
}) {
|
|
return Future.wait(<Future<Object?>>[
|
|
initialize(exceptionPauseMode: exceptionPauseMode),
|
|
launch?.call() ?? this.launch(program: program, cwd: cwd),
|
|
], eagerError: true);
|
|
}
|
|
|
|
Future<void> stop() async {
|
|
_channel.close();
|
|
await _subscription.cancel();
|
|
}
|
|
|
|
Future<Response> terminate() => sendRequest(TerminateArguments());
|
|
|
|
/// Handles an incoming message from the server, completing the relevant request
|
|
/// of raising the appropriate event.
|
|
Future<void> _handleMessage(Object? message) async {
|
|
if (message is Response) {
|
|
final _OutgoingRequest? pendingRequest = _pendingRequests.remove(message.requestSeq);
|
|
if (pendingRequest == null) {
|
|
return;
|
|
}
|
|
final Completer<Response> completer = pendingRequest.completer;
|
|
if (message.success || pendingRequest.allowFailure) {
|
|
completer.complete(message);
|
|
} else {
|
|
completer.completeError(message);
|
|
}
|
|
} else if (message is Event && !_eventController.isClosed) {
|
|
_eventController.add(message);
|
|
|
|
// When we see a terminated event, close the event stream so if any
|
|
// tests are waiting on something that will never come, they fail at
|
|
// a useful location.
|
|
if (message.event == 'terminated') {
|
|
unawaited(_eventController.close());
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Creates a [DapTestClient] that connects the server listening on
|
|
/// [host]:[port].
|
|
static Future<DapTestClient> connect(
|
|
DapTestServer server, {
|
|
bool captureVmServiceTraffic = false,
|
|
Logger? logger,
|
|
}) async {
|
|
final ByteStreamServerChannel channel = ByteStreamServerChannel(server.stream, server.sink, logger);
|
|
return DapTestClient._(channel, logger,
|
|
captureVmServiceTraffic: captureVmServiceTraffic);
|
|
}
|
|
}
|
|
|
|
/// Useful events produced by the debug adapter during a debug session.
|
|
class TestEvents {
|
|
TestEvents({
|
|
required this.output,
|
|
required this.testNotifications,
|
|
});
|
|
|
|
final List<OutputEventBody> output;
|
|
final List<Map<String, Object?>> testNotifications;
|
|
}
|
|
|
|
class _OutgoingRequest {
|
|
_OutgoingRequest(this.completer, this.name, this.allowFailure);
|
|
|
|
final Completer<Response> completer;
|
|
final String name;
|
|
final bool allowFailure;
|
|
}
|
|
|
|
/// Additional helper method for tests to simplify interaction with [DapTestClient].
|
|
///
|
|
/// Unlike the methods on [DapTestClient] these methods might not map directly
|
|
/// onto protocol methods. They may call multiple protocol methods and/or
|
|
/// simplify assertion specific conditions/results.
|
|
extension DapTestClientExtension on DapTestClient {
|
|
/// Collects all output events until the program terminates.
|
|
///
|
|
/// These results include all events in the order they are received, including
|
|
/// console, stdout and stderr.
|
|
///
|
|
/// Only one of [start] or [launch] may be provided. Use [start] to customise
|
|
/// the whole start of the session (including initialise) or [launch] to only
|
|
/// customise the [launchRequest].
|
|
Future<List<OutputEventBody>> collectAllOutput({
|
|
String? program,
|
|
String? cwd,
|
|
Future<void> Function()? start,
|
|
Future<Response> Function()? launch,
|
|
bool skipInitialPubGetOutput = true
|
|
}) async {
|
|
assert(
|
|
start == null || launch == null,
|
|
'Only one of "start" or "launch" may be provided',
|
|
);
|
|
final Future<List<OutputEventBody>> outputEventsFuture = outputEvents.toList();
|
|
|
|
// Don't await these, in case they don't complete (eg. an error prevents
|
|
// the app from starting).
|
|
if (start != null) {
|
|
unawaited(start());
|
|
} else {
|
|
unawaited(this.start(program: program, cwd: cwd, launch: launch));
|
|
}
|
|
|
|
final List<OutputEventBody> output = await outputEventsFuture;
|
|
|
|
// TODO(dantup): Integration tests currently trigger "flutter pub get" at
|
|
// the start due to some timestamp manipulation writing the pubspec.
|
|
// It may be possible to remove this if
|
|
// https://github.com/flutter/flutter/pull/91300 lands.
|
|
return skipInitialPubGetOutput
|
|
? output.skipWhile((OutputEventBody output) => output.output.startsWith('Running "flutter pub get"')).toList()
|
|
: output;
|
|
}
|
|
|
|
/// Collects all output and test events until the program terminates.
|
|
///
|
|
/// These results include all events in the order they are received, including
|
|
/// console, stdout, stderr and test notifications from the test JSON reporter.
|
|
///
|
|
/// Only one of [start] or [launch] may be provided. Use [start] to customise
|
|
/// the whole start of the session (including initialise) or [launch] to only
|
|
/// customise the [launchRequest].
|
|
Future<TestEvents> collectTestOutput({
|
|
String? program,
|
|
String? cwd,
|
|
Future<Response> Function()? start,
|
|
Future<Object?> Function()? launch,
|
|
}) async {
|
|
assert(
|
|
start == null || launch == null,
|
|
'Only one of "start" or "launch" may be provided',
|
|
);
|
|
|
|
final Future<List<OutputEventBody>> outputEventsFuture = outputEvents.toList();
|
|
final Future<List<Map<String, Object?>>> testNotificationEventsFuture = testNotificationEvents.toList();
|
|
|
|
if (start != null) {
|
|
await start();
|
|
} else {
|
|
await this.start(program: program, cwd: cwd, launch: launch);
|
|
}
|
|
|
|
return TestEvents(
|
|
output: await outputEventsFuture,
|
|
testNotifications: await testNotificationEventsFuture,
|
|
);
|
|
}
|
|
|
|
/// Sets a breakpoint at [line] in [file].
|
|
Future<void> setBreakpoint(String filePath, int line) async {
|
|
await sendRequest(
|
|
SetBreakpointsArguments(
|
|
source: Source(path: filePath),
|
|
breakpoints: <SourceBreakpoint>[
|
|
SourceBreakpoint(line: line),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Sends a continue request for the given thread.
|
|
///
|
|
/// Returns a Future that completes when the server returns a corresponding
|
|
/// response.
|
|
Future<Response> continue_(int threadId) =>
|
|
sendRequest(ContinueArguments(threadId: threadId));
|
|
|
|
/// Clears breakpoints in [file].
|
|
Future<void> clearBreakpoints(String filePath) async {
|
|
await sendRequest(
|
|
SetBreakpointsArguments(
|
|
source: Source(path: filePath),
|
|
breakpoints: <SourceBreakpoint>[],
|
|
),
|
|
);
|
|
}
|
|
|
|
}
|