Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

Commit

Permalink
[flutter_tools] Update DAP progress when waiting for Dart Debug exten…
Browse files Browse the repository at this point in the history
…sion connection (#116892)

Fixes Dart-Code/Dart-Code#4293.
  • Loading branch information
DanTup committed Dec 19, 2022
1 parent 3eefb7a commit 9f9010f
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 12 deletions.
23 changes: 19 additions & 4 deletions packages/flutter_tools/lib/src/debug_adapters/flutter_adapter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter {
/// The appId of the current running Flutter app.
String? _appId;

/// A progress reporter for the applications launch progress.
///
/// `null` if a launch is not in progress (or has completed).
DapProgressReporter? launchProgress;

/// The ID to use for the next request sent to the Flutter run daemon.
int _flutterRequestId = 1;

Expand Down Expand Up @@ -123,12 +128,11 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter {
Future<void> attachImpl() async {
final FlutterAttachRequestArguments args = this.args as FlutterAttachRequestArguments;

final DapProgressReporter progress = startProgressNotification(
launchProgress = startProgressNotification(
'launch',
'Flutter',
message: 'Attaching…',
);
unawaited(_appStartedCompleter.future.then((_) => progress.end()));

final String? vmServiceUri = args.vmServiceUri;
final List<String> toolArgs = <String>[
Expand Down Expand Up @@ -230,12 +234,11 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter {
Future<void> launchImpl() async {
final FlutterLaunchRequestArguments args = this.args as FlutterLaunchRequestArguments;

final DapProgressReporter progress = startProgressNotification(
launchProgress = startProgressNotification(
'launch',
'Flutter',
message: 'Launching…',
);
unawaited(_appStartedCompleter.future.then((_) => progress.end()));

final List<String> toolArgs = <String>[
'run',
Expand Down Expand Up @@ -398,6 +401,8 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter {

/// Handles the app.started event from Flutter.
Future<void> _handleAppStarted() async {
launchProgress?.end();
launchProgress = null;
_appStartedCompleter.complete();

// Send a custom event so the editor knows the app has started.
Expand Down Expand Up @@ -591,6 +596,16 @@ class FlutterDebugAdapter extends FlutterBaseDebugAdapter {
// If the output wasn't valid JSON, it was standard stdout that should
// be passed through to the user.
sendOutput(outputCategory, data);

// Detect if the output contains a prompt about using the Dart Debug
// extension and also update the progress notification to make it clearer
// we're waiting for the user to do something.
if (data.contains('Waiting for connection from Dart debug extension')) {
launchProgress?.update(
message: 'Please click the Dart Debug extension button in the spawned browser window',
);
}

return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,47 @@ void main() {

expect(adapter.dapToFlutterRequests, isNot(contains('app.stop')));
});

test('includes Dart Debug extension progress update', () async {
final MockFlutterDebugAdapter adapter = MockFlutterDebugAdapter(
fileSystem: MemoryFileSystem.test(style: fsStyle),
platform: platform,
preAppStart: (MockFlutterDebugAdapter adapter) {
adapter.simulateRawStdout('Waiting for connection from Dart debug extension…');
}
);
final Completer<void> responseCompleter = Completer<void>();

final FlutterLaunchRequestArguments args = FlutterLaunchRequestArguments(
cwd: '/project',
program: 'foo.dart',
);

// Begin listening for progress events up until `progressEnd` (but don't await yet).
final Future<List<List<Object?>>> progressEventsFuture =
adapter.dapToClientProgressEvents
.takeWhile((Map<String, Object?> message) => message['event'] != 'progressEnd')
.map((Map<String, Object?> message) => <Object?>[message['event'], (message['body']! as Map<String, Object?>)['message']])
.toList();

// Initialize with progress support.
await adapter.initializeRequest(
MockRequest(),
InitializeRequestArguments(adapterID: 'test', supportsProgressReporting: true, ),
(_) {},
);
await adapter.configurationDoneRequest(MockRequest(), null, () {});
await adapter.launchRequest(MockRequest(), args, responseCompleter.complete);
await responseCompleter.future;

// Ensure we got the expected events prior to the
final List<List<Object?>> progressEvents = await progressEventsFuture;
expect(progressEvents, containsAllInOrder(<List<String>>[
<String>['progressStart', 'Launching…'],
<String>['progressUpdate', 'Please click the Dart Debug extension button in the spawned browser window'],
// progressEnd isn't included because we used takeWhile to stop when it arrived above.
]));
});
});

group('attachRequest', () {
Expand Down Expand Up @@ -221,6 +262,11 @@ void main() {
platform: platform,
);

// Start listening for the forwarded event (don't await it yet, it won't
// be triggered until the call below).
final Future<Map<String, Object?>> forwardedEvent = adapter.dapToClientMessages
.firstWhere((Map<String, Object?> data) => data['event'] == 'flutter.forwardedEvent');

// Simulate Flutter asking for a URL to be launched.
adapter.simulateStdoutMessage(<String, Object?>{
'event': 'app.webLaunchUrl',
Expand All @@ -230,11 +276,8 @@ void main() {
}
});

// Allow the handler to be processed.
await pumpEventQueue(times: 5000);

// Find the forwarded event.
final Map<String, Object?> message = adapter.dapToClientMessages.singleWhere((Map<String, Object?> data) => data['event'] == 'flutter.forwardedEvent');
// Wait for the forwarded event.
final Map<String, Object?> message = await forwardedEvent;
// Ensure the body of the event matches the original event sent by Flutter.
expect(message['body'], <String, Object?>{
'event': 'app.webLaunchUrl',
Expand Down
32 changes: 29 additions & 3 deletions packages/flutter_tools/test/general.shard/dap/mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter {
required FileSystem fileSystem,
required Platform platform,
bool simulateAppStarted = true,
FutureOr<void> Function(MockFlutterDebugAdapter adapter)? preAppStart,
}) {
final StreamController<List<int>> stdinController = StreamController<List<int>>();
final StreamController<List<int>> stdoutController = StreamController<List<int>>();
Expand All @@ -30,6 +31,7 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter {
fileSystem: fileSystem,
platform: platform,
simulateAppStarted: simulateAppStarted,
preAppStart: preAppStart,
);
}

Expand All @@ -39,6 +41,7 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter {
required super.fileSystem,
required super.platform,
this.simulateAppStarted = true,
this.preAppStart,
}) {
clientChannel.listen((ProtocolMessage message) {
_handleDapToClientMessage(message);
Expand All @@ -48,13 +51,24 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter {
int _seq = 1;
final ByteStreamServerChannel clientChannel;
final bool simulateAppStarted;
final FutureOr<void> Function(MockFlutterDebugAdapter adapter)? preAppStart;

late String executable;
late List<String> processArgs;
late Map<String, String>? env;

/// A list of all messages sent from the adapter back to the client.
final List<Map<String, Object?>> dapToClientMessages = <Map<String, Object?>>[];
final StreamController<Map<String, Object?>> _dapToClientMessagesController = StreamController<Map<String, Object?>>.broadcast();

/// A stream of all messages sent from the adapter back to the client.
Stream<Map<String, Object?>> get dapToClientMessages => _dapToClientMessagesController.stream;

/// A stream of all progress events sent from the adapter back to the client.
Stream<Map<String, Object?>> get dapToClientProgressEvents {
const List<String> progressEventTypes = <String>['progressStart', 'progressUpdate', 'progressEnd'];

return dapToClientMessages
.where((Map<String, Object?> message) => progressEventTypes.contains(message['event'] as String?));
}

/// A list of all messages sent from the adapter to the `flutter run` processes `stdin`.
final List<Map<String, Object?>> dapToFlutterMessages = <Map<String, Object?>>[];
Expand All @@ -79,6 +93,8 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter {
this.processArgs = processArgs;
this.env = env;

await preAppStart?.call(this);

// Simulate the app starting by triggering handling of events that Flutter
// would usually write to stdout.
if (simulateAppStarted) {
Expand All @@ -96,7 +112,7 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter {

/// Handles messages sent from the debug adapter back to the client.
void _handleDapToClientMessage(ProtocolMessage message) {
dapToClientMessages.add(message.toJson());
_dapToClientMessagesController.add(message.toJson());

// Pretend to be the client, delegating any reverse-requests to the relevant
// handler that is provided by the test.
Expand Down Expand Up @@ -131,12 +147,22 @@ class MockFlutterDebugAdapter extends FlutterDebugAdapter {

/// Simulates a message emitted by the `flutter run` process by directly
/// calling the debug adapters [handleStdout] method.
///
/// Use [simulateRawStdout] to simulate non-daemon text output.
void simulateStdoutMessage(Map<String, Object?> message) {
// Messages are wrapped in a list because Flutter only processes messages
// wrapped in brackets.
handleStdout(jsonEncode(<Object?>[message]));
}

/// Simulates a string emitted by the `flutter run` process by directly
/// calling the debug adapters [handleStdout] method.
///
/// Use [simulateStdoutMessage] to simulate a daemon JSON message.
void simulateRawStdout(String output) {
handleStdout(output);
}

@override
void sendFlutterMessage(Map<String, Object?> message) {
dapToFlutterMessages.add(message);
Expand Down

0 comments on commit 9f9010f

Please sign in to comment.