// 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:collection/collection.dart'; import 'package:dds/dap.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/convert.dart'; import 'package:flutter_tools/src/debug_adapters/flutter_adapter.dart'; import 'package:flutter_tools/src/debug_adapters/flutter_test_adapter.dart'; /// A [FlutterDebugAdapter] that captures what process/args will be launched. class MockFlutterDebugAdapter extends FlutterDebugAdapter { factory MockFlutterDebugAdapter({ required FileSystem fileSystem, required Platform platform, bool simulateAppStarted = true, bool supportsRestart = true, FutureOr Function(MockFlutterDebugAdapter adapter)? preAppStart, }) { final StreamController> stdinController = StreamController>(); final StreamController> stdoutController = StreamController>(); final ByteStreamServerChannel channel = ByteStreamServerChannel(stdinController.stream, stdoutController.sink, null); final ByteStreamServerChannel clientChannel = ByteStreamServerChannel(stdoutController.stream, stdinController.sink, null); return MockFlutterDebugAdapter._( channel, clientChannel: clientChannel, fileSystem: fileSystem, platform: platform, simulateAppStarted: simulateAppStarted, supportsRestart: supportsRestart, preAppStart: preAppStart, ); } MockFlutterDebugAdapter._( super.channel, { required this.clientChannel, required super.fileSystem, required super.platform, this.simulateAppStarted = true, this.supportsRestart = true, this.preAppStart, }) { clientChannel.listen((ProtocolMessage message) { _handleDapToClientMessage(message); }); } int _seq = 1; final ByteStreamServerChannel clientChannel; final bool simulateAppStarted; final bool supportsRestart; final FutureOr Function(MockFlutterDebugAdapter adapter)? preAppStart; late String executable; late List processArgs; late Map? env; /// Overrides base implementation of [sendLogsToClient] which requires valid /// `args` to have been set which may not be the case for mocks. @override bool get sendLogsToClient => false; final StreamController> _dapToClientMessagesController = StreamController>.broadcast(); /// A stream of all messages sent from the adapter back to the client. Stream> get dapToClientMessages => _dapToClientMessagesController.stream; /// A stream of all progress events sent from the adapter back to the client. Stream> get dapToClientProgressEvents { const List progressEventTypes = ['progressStart', 'progressUpdate', 'progressEnd']; return dapToClientMessages .where((Map message) => progressEventTypes.contains(message['event'] as String?)); } /// A list of all messages sent from the adapter to the `flutter run` processes `stdin`. final List> dapToFlutterMessages = >[]; /// The `method`s of all messages sent to the `flutter run` processes `stdin` /// by the debug adapter. List get dapToFlutterRequests => dapToFlutterMessages .map((Map message) => message['method'] as String?) .whereNotNull() .toList(); /// A handler for the 'app.exposeUrl' reverse-request. String Function(String)? exposeUrlHandler; @override Future launchAsProcess({ required String executable, required List processArgs, required Map? env, }) async { this.executable = executable; 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) { simulateStdoutMessage({ 'event': 'app.start', 'params': { 'appId': 'TEST', 'supportsRestart': supportsRestart, 'deviceId': 'flutter-tester', 'mode': 'debug', } }); simulateStdoutMessage({ 'event': 'app.started', }); } } /// Handles messages sent from the debug adapter back to the client. void _handleDapToClientMessage(ProtocolMessage message) { _dapToClientMessagesController.add(message.toJson()); // Pretend to be the client, delegating any reverse-requests to the relevant // handler that is provided by the test. if (message is Event && message.event == 'flutter.forwardedRequest') { final Map body = message.body! as Map; final String method = body['method']! as String; final Map? params = body['params'] as Map?; final Object? result = _handleReverseRequest(method, params); // Send the result back in the same way the client would. clientChannel.sendRequest(Request( seq: _seq++, command: 'flutter.sendForwardedRequestResponse', arguments: { 'id': body['id'], 'result': result, }, )); } } Object? _handleReverseRequest(String method, Map? params) { switch (method) { case 'app.exposeUrl': final String url = params!['url']! as String; return exposeUrlHandler!(url); default: throw ArgumentError('Reverse-request $method is unknown'); } } /// 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 message) { // Messages are wrapped in a list because Flutter only processes messages // wrapped in brackets. handleStdout(jsonEncode([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 message) { dapToFlutterMessages.add(message); // Don't call super because it will try to write to the process that we // didn't actually spawn. } @override Future get debuggerInitialized { // If we were mocking debug mode, then simulate the debugger initializing. return enableDebugger ? Future.value() : throw StateError('Invalid attempt to wait for debuggerInitialized when not debugging'); } } /// A [FlutterTestDebugAdapter] that captures what process/args will be launched. class MockFlutterTestDebugAdapter extends FlutterTestDebugAdapter { factory MockFlutterTestDebugAdapter({ required FileSystem fileSystem, required Platform platform, }) { final StreamController> stdinController = StreamController>(); final StreamController> stdoutController = StreamController>(); final ByteStreamServerChannel channel = ByteStreamServerChannel(stdinController.stream, stdoutController.sink, null); return MockFlutterTestDebugAdapter._( stdinController.sink, stdoutController.stream, channel, fileSystem: fileSystem, platform: platform, ); } MockFlutterTestDebugAdapter._( this.stdin, this.stdout, ByteStreamServerChannel channel, { required FileSystem fileSystem, required Platform platform, }) : super(channel, fileSystem: fileSystem, platform: platform); final StreamSink> stdin; final Stream> stdout; late String executable; late List processArgs; late Map? env; @override Future launchAsProcess({ required String executable, required List processArgs, required Map? env, }) async { this.executable = executable; this.processArgs = processArgs; this.env = env; } @override Future get debuggerInitialized { // If we were mocking debug mode, then simulate the debugger initializing. return enableDebugger ? Future.value() : throw StateError('Invalid attempt to wait for debuggerInitialized when not debugging'); } } class MockRequest extends Request { MockRequest() : super.fromMap({ 'command': 'mock_command', 'type': 'mock_type', 'seq': _requestId++, }); static int _requestId = 1; }