blob: fbe9a015d8f9bf21f3a6102cbfe41f4db5dd3acf [file] [log] [blame]
// 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<void> Function(MockFlutterDebugAdapter adapter)? preAppStart,
}) {
final StreamController<List<int>> stdinController = StreamController<List<int>>();
final StreamController<List<int>> stdoutController = StreamController<List<int>>();
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<void> Function(MockFlutterDebugAdapter adapter)? preAppStart;
late String executable;
late List<String> processArgs;
late Map<String, String>? 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<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?>>[];
/// The `method`s of all messages sent to the `flutter run` processes `stdin`
/// by the debug adapter.
List<String> get dapToFlutterRequests => dapToFlutterMessages
.map((Map<String, Object?> message) => message['method'] as String?)
.whereNotNull()
.toList();
/// A handler for the 'app.exposeUrl' reverse-request.
String Function(String)? exposeUrlHandler;
@override
Future<void> launchAsProcess({
required String executable,
required List<String> processArgs,
required Map<String, String>? env,
}) async {
this.executable = executable;
this.processArgs = processArgs;
this.env = env;
await preAppStart?.call(this);
void sendLaunchProgress({required bool finished, String? message}) {
assert(finished == (message == null));
simulateStdoutMessage(<String, Object?>{
'event': 'app.progress',
'params': <String, Object?>{
'id': 'launch',
'message': message,
'finished': finished,
}
});
}
// Simulate the app starting by triggering handling of events that Flutter
// would usually write to stdout.
if (simulateAppStarted) {
sendLaunchProgress(message: 'Step 1…', finished: false);
simulateStdoutMessage(<String, Object?>{
'event': 'app.start',
'params': <String, Object?>{
'appId': 'TEST',
'supportsRestart': supportsRestart,
'deviceId': 'flutter-tester',
'mode': 'debug',
}
});
sendLaunchProgress(message: 'Step 2…', finished: false);
sendLaunchProgress(finished: true);
simulateStdoutMessage(<String, Object?>{
'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<String, Object?> body = message.body! as Map<String, Object?>;
final String method = body['method']! as String;
final Map<String, Object?>? params = body['params'] as Map<String, Object?>?;
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: <String, Object?>{
'id': body['id'],
'result': result,
},
));
}
}
Object? _handleReverseRequest(String method, Map<String, Object?>? 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<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);
// Don't call super because it will try to write to the process that we
// didn't actually spawn.
}
@override
Future<void> get debuggerInitialized {
// If we were mocking debug mode, then simulate the debugger initializing.
return enableDebugger
? Future<void>.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<List<int>> stdinController = StreamController<List<int>>();
final StreamController<List<int>> stdoutController = StreamController<List<int>>();
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<List<int>> stdin;
final Stream<List<int>> stdout;
late String executable;
late List<String> processArgs;
late Map<String, String>? env;
@override
Future<void> launchAsProcess({
required String executable,
required List<String> processArgs,
required Map<String, String>? env,
}) async {
this.executable = executable;
this.processArgs = processArgs;
this.env = env;
}
@override
Future<void> get debuggerInitialized {
// If we were mocking debug mode, then simulate the debugger initializing.
return enableDebugger
? Future<void>.value()
: throw StateError('Invalid attempt to wait for debuggerInitialized when not debugging');
}
}
class MockRequest extends Request {
MockRequest()
: super.fromMap(<String, Object?>{
'command': 'mock_command',
'type': 'mock_type',
'seq': _requestId++,
});
static int _requestId = 1;
}