Unverified Commit 7e1e98c3 authored by Danny Tuppeny's avatar Danny Tuppeny Committed by GitHub

Add support for executing custom tools in place of Flutter for DAP (#94475)

parent 79bc1bfa
......@@ -34,9 +34,9 @@ Arguments specific to `launchRequest` are:
- `bool? noDebug` - whether to run in debug or noDebug mode (if not supplied, defaults to debug)
- `String program` - the path of the Flutter application to run
- `List<String>? args` - arguments to be passed to the Flutter program
- `List<String>? toolArgs` - arguments for the `flutter` tool
- `String? console` - if set to `"terminal"` or `"externalTerminal"` will be run using the `runInTerminal` reverse-request; otherwise the debug adapter spawns the Dart process
- `bool? enableAsserts` - whether to enable asserts (if not supplied, defaults to `true`)
- `List<String>? toolArgs` - arguments for the `flutter run` or `flutter test` commands
- `String? customTool` - an optional tool to run instead of `flutter` - the custom tool must be completely compatible with the tool/command it is replacing
- `int? customToolReplacesArgs` - the number of arguments to delete from the beginning of the argument list when invoking `customTool` - e.g. setting `customTool` to `flutter_test_wrapper` and `customToolReplacesArgs` to `1` for a test run would invoke `flutter_test_wrapper foo_test.dart` instead of `flutter test foo_test.dart` (if larger than the number of computed arguments all arguments will be removed, if not supplied will default to `0`)
`attachRequest` is not currently supported, but will be documented here when it is.
......
......@@ -3,8 +3,10 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:math' as math;
import 'package:dds/dap.dart' hide PidTracker, PackageConfigUtils;
import 'package:meta/meta.dart';
import 'package:vm_service/vm_service.dart' as vm;
import '../base/file_system.dart';
......@@ -48,10 +50,11 @@ class FlutterDebugAdapter extends DartDebugAdapter<FlutterLaunchRequestArguments
parseAttachArgs = FlutterAttachRequestArguments.fromJson;
/// A completer that completes when the app.started event has been received.
final Completer<void> _appStartedCompleter = Completer<void>();
@visibleForTesting
final Completer<void> appStartedCompleter = Completer<void>();
/// Whether or not the app.started event has been received.
bool get _receivedAppStarted => _appStartedCompleter.isCompleted;
bool get _receivedAppStarted => appStartedCompleter.isCompleted;
/// The VM Service URI received from the app.debugPort event.
Uri? _vmServiceUri;
......@@ -167,7 +170,6 @@ class FlutterDebugAdapter extends DartDebugAdapter<FlutterLaunchRequestArguments
@override
Future<void> launchImpl() async {
final FlutterLaunchRequestArguments args = this.args as FlutterLaunchRequestArguments;
final String flutterToolPath = fileSystem.path.join(Cache.flutterRoot!, 'bin', platform.isWindows ? 'flutter.bat' : 'flutter');
// "debug"/"noDebug" refers to the DAP "debug" mode and not the Flutter
// debug mode (vs Profile/Release). It is possible for the user to "Run"
......@@ -181,6 +183,14 @@ class FlutterDebugAdapter extends DartDebugAdapter<FlutterLaunchRequestArguments
'--machine',
if (debug) '--start-paused',
];
// Handle customTool and deletion of any arguments for it.
final String executable = args.customTool ?? fileSystem.path.join(Cache.flutterRoot!, 'bin', platform.isWindows ? 'flutter.bat' : 'flutter');
final int? removeArgs = args.customToolReplacesArgs;
if (args.customTool != null && removeArgs != null) {
toolArgs.removeRange(0, math.min(removeArgs, toolArgs.length));
}
final List<String> processArgs = <String>[
...toolArgs,
...?args.toolArgs,
......@@ -210,9 +220,21 @@ class FlutterDebugAdapter extends DartDebugAdapter<FlutterLaunchRequestArguments
}
}
logger?.call('Spawning $flutterToolPath with $processArgs in ${args.cwd}');
await launchAsProcess(executable, processArgs);
// Delay responding until the app is launched and (optionally) the debugger
// is connected.
await appStartedCompleter.future;
if (debug) {
await debuggerInitialized;
}
}
@visibleForOverriding
Future<void> launchAsProcess(String executable, List<String> processArgs) async {
logger?.call('Spawning $executable with $processArgs in ${args.cwd}');
final Process process = await Process.start(
flutterToolPath,
executable,
processArgs,
workingDirectory: args.cwd,
);
......@@ -222,13 +244,6 @@ class FlutterDebugAdapter extends DartDebugAdapter<FlutterLaunchRequestArguments
process.stdout.transform(ByteToLineTransformer()).listen(_handleStdout);
process.stderr.listen(_handleStderr);
unawaited(process.exitCode.then(_handleExitCode));
// Delay responding until the app is launched and (optionally) the debugger
// is connected.
await _appStartedCompleter.future;
if (debug) {
await debuggerInitialized;
}
}
/// restart is called by the client when the user invokes a restart (for example with the button on the debug toolbar).
......@@ -307,7 +322,7 @@ class FlutterDebugAdapter extends DartDebugAdapter<FlutterLaunchRequestArguments
/// Handles the app.started event from Flutter.
void _handleAppStarted() {
_appStartedCompleter.complete();
appStartedCompleter.complete();
_connectDebuggerIfReady();
}
......
......@@ -54,6 +54,8 @@ class FlutterLaunchRequestArguments
required this.program,
this.args,
this.toolArgs,
this.customTool,
this.customToolReplacesArgs,
Object? restart,
String? name,
String? cwd,
......@@ -80,6 +82,8 @@ class FlutterLaunchRequestArguments
program = obj['program'] as String?,
args = (obj['args'] as List<Object?>?)?.cast<String>(),
toolArgs = (obj['toolArgs'] as List<Object?>?)?.cast<String>(),
customTool = obj['customTool'] as String?,
customToolReplacesArgs = obj['customToolReplacesArgs'] as int?,
super.fromMap(obj);
/// If noDebug is true the launch request should launch the program without enabling debugging.
......@@ -95,6 +99,24 @@ class FlutterLaunchRequestArguments
/// Arguments to be passed to the tool that will run [program] (for example, the VM or Flutter tool).
final List<String>? toolArgs;
/// An optional tool to run instead of "flutter".
///
/// In combination with [customToolReplacesArgs] allows invoking a custom
/// tool instead of "flutter" to launch scripts/tests. The custom tool must be
/// completely compatible with the tool/command it is replacing.
///
/// This field should be a full absolute path if the tool may not be available
/// in `PATH`.
final String? customTool;
/// The number of arguments to delete from the beginning of the argument list
/// when invoking [customTool].
///
/// For example, setting [customTool] to `flutter_test_wrapper` and
/// `customToolReplacesArgs` to `1` for a test run would invoke
/// `flutter_test_wrapper foo_test.dart` instead of `flutter test foo_test.dart`.
final int? customToolReplacesArgs;
@override
Map<String, Object?> toJson() => <String, Object?>{
...super.toJson(),
......@@ -102,6 +124,8 @@ class FlutterLaunchRequestArguments
if (program != null) 'program': program,
if (args != null) 'args': args,
if (toolArgs != null) 'toolArgs': toolArgs,
if (customTool != null) 'customTool': customTool,
if (customToolReplacesArgs != null) 'customToolReplacesArgs': customToolReplacesArgs,
};
static FlutterLaunchRequestArguments fromJson(Map<String, Object?> obj) =>
......
......@@ -3,8 +3,10 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:math' as math;
import 'package:dds/dap.dart' hide PidTracker, PackageConfigUtils;
import 'package:meta/meta.dart';
import 'package:vm_service/vm_service.dart' as vm;
import '../base/file_system.dart';
......@@ -90,7 +92,6 @@ class FlutterTestDebugAdapter extends DartDebugAdapter<FlutterLaunchRequestArgum
@override
Future<void> launchImpl() async {
final FlutterLaunchRequestArguments args = this.args as FlutterLaunchRequestArguments;
final String flutterToolPath = fileSystem.path.join(Cache.flutterRoot!, 'bin', platform.isWindows ? 'flutter.bat' : 'flutter');
final bool debug = !(args.noDebug ?? false);
final String? program = args.program;
......@@ -100,6 +101,14 @@ class FlutterTestDebugAdapter extends DartDebugAdapter<FlutterLaunchRequestArgum
'--machine',
if (debug) '--start-paused',
];
// Handle customTool and deletion of any arguments for it.
final String executable = args.customTool ?? fileSystem.path.join(Cache.flutterRoot!, 'bin', platform.isWindows ? 'flutter.bat' : 'flutter');
final int? removeArgs = args.customToolReplacesArgs;
if (args.customTool != null && removeArgs != null) {
toolArgs.removeRange(0, math.min(removeArgs, toolArgs.length));
}
final List<String> processArgs = <String>[
...toolArgs,
...?args.toolArgs,
......@@ -126,9 +135,19 @@ class FlutterTestDebugAdapter extends DartDebugAdapter<FlutterLaunchRequestArgum
}
}
logger?.call('Spawning $flutterToolPath with $processArgs in ${args.cwd}');
await launchAsProcess(executable, processArgs);
// Delay responding until the debugger is connected.
if (debug) {
await debuggerInitialized;
}
}
@visibleForOverriding
Future<void> launchAsProcess(String executable, List<String> processArgs) async {
logger?.call('Spawning $executable with $processArgs in ${args.cwd}');
final Process process = await Process.start(
flutterToolPath,
executable,
processArgs,
workingDirectory: args.cwd,
);
......@@ -138,11 +157,6 @@ class FlutterTestDebugAdapter extends DartDebugAdapter<FlutterLaunchRequestArgum
process.stdout.transform(ByteToLineTransformer()).listen(_handleStdout);
process.stderr.listen(_handleStderr);
unawaited(process.exitCode.then(_handleExitCode));
// Delay responding until the debugger is connected.
if (debug) {
await debuggerInitialized;
}
}
/// Called by [terminateRequest] to request that we gracefully shut down the app being run (or in the case of an attach, disconnect).
......
// 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:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/debug_adapters/flutter_adapter_args.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:test/test.dart';
import 'mocks.dart';
void main() {
group('flutter adapter', () {
setUpAll(() {
Cache.flutterRoot = '/fake/flutter';
});
test('includes toolArgs', () async {
final MockFlutterDebugAdapter adapter = MockFlutterDebugAdapter(fileSystem: globals.fs, platform: globals.platform);
final Completer<void> responseCompleter = Completer<void>();
final FlutterLaunchRequestArguments args = FlutterLaunchRequestArguments(
cwd: '/project',
program: 'foo.dart',
toolArgs: <String>['tool_arg'],
noDebug: true,
);
await adapter.configurationDoneRequest(MockRequest(), null, () {});
await adapter.launchRequest(MockRequest(), args, responseCompleter.complete);
await responseCompleter.future;
expect(adapter.executable, equals('/fake/flutter/bin/flutter'));
expect(adapter.processArgs, contains('tool_arg'));
});
group('includes customTool', () {
test('with no args replaced', () async {
final MockFlutterDebugAdapter adapter = MockFlutterDebugAdapter(fileSystem: globals.fs, platform: globals.platform);
final FlutterLaunchRequestArguments args = FlutterLaunchRequestArguments(
cwd: '/project',
program: 'foo.dart',
customTool: '/custom/flutter',
noDebug: true,
);
await adapter.configurationDoneRequest(MockRequest(), null, () {});
final Completer<void> responseCompleter = Completer<void>();
await adapter.launchRequest(MockRequest(), args, responseCompleter.complete);
await responseCompleter.future;
expect(adapter.executable, equals('/custom/flutter'));
// args should be in-tact
expect(adapter.processArgs, contains('--machine'));
});
test('with all args replaced', () async {
final MockFlutterDebugAdapter adapter = MockFlutterDebugAdapter(fileSystem: globals.fs, platform: globals.platform);
final FlutterLaunchRequestArguments args = FlutterLaunchRequestArguments(
cwd: '/project',
program: 'foo.dart',
customTool: '/custom/flutter',
customToolReplacesArgs: 9999, // replaces all built-in args
noDebug: true,
toolArgs: <String>['tool_args'], // should still be in args
);
await adapter.configurationDoneRequest(MockRequest(), null, () {});
final Completer<void> responseCompleter = Completer<void>();
await adapter.launchRequest(MockRequest(), args, responseCompleter.complete);
await responseCompleter.future;
expect(adapter.executable, equals('/custom/flutter'));
// normal built-in args are replaced by customToolReplacesArgs, but
// user-provided toolArgs are not.
expect(adapter.processArgs, isNot(contains('--machine')));
expect(adapter.processArgs, contains('tool_args'));
});
});
});
}
// 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:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/debug_adapters/flutter_adapter_args.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:test/test.dart';
import 'mocks.dart';
void main() {
group('flutter test adapter', () {
setUpAll(() {
Cache.flutterRoot = '/fake/flutter';
});
test('includes toolArgs', () async {
final MockFlutterTestDebugAdapter adapter = MockFlutterTestDebugAdapter(
fileSystem: globals.fs,
platform: globals.platform,
);
final Completer<void> responseCompleter = Completer<void>();
final MockRequest request = MockRequest();
final FlutterLaunchRequestArguments args = FlutterLaunchRequestArguments(
cwd: '/project',
program: 'foo.dart',
toolArgs: <String>['tool_arg'],
noDebug: true,
);
await adapter.configurationDoneRequest(request, null, () {});
await adapter.launchRequest(request, args, responseCompleter.complete);
await responseCompleter.future;
expect(adapter.executable, equals('/fake/flutter/bin/flutter'));
expect(adapter.processArgs, contains('tool_arg'));
});
group('includes customTool', () {
test('with no args replaced', () async {
final MockFlutterTestDebugAdapter adapter = MockFlutterTestDebugAdapter(fileSystem: globals.fs,
platform: globals.platform,);
final Completer<void> responseCompleter = Completer<void>();
final MockRequest request = MockRequest();
final FlutterLaunchRequestArguments args = FlutterLaunchRequestArguments(
cwd: '/project',
program: 'foo.dart',
customTool: '/custom/flutter',
noDebug: true,
);
await adapter.configurationDoneRequest(request, null, () {});
await adapter.launchRequest(request, args, responseCompleter.complete);
await responseCompleter.future;
expect(adapter.executable, equals('/custom/flutter'));
// args should be in-tact
expect(adapter.processArgs, contains('--machine'));
});
test('with all args replaced', () async {
final MockFlutterTestDebugAdapter adapter = MockFlutterTestDebugAdapter(fileSystem: globals.fs,
platform: globals.platform,);
final Completer<void> responseCompleter = Completer<void>();
final MockRequest request = MockRequest();
final FlutterLaunchRequestArguments args = FlutterLaunchRequestArguments(
cwd: '/project',
program: 'foo.dart',
customTool: '/custom/flutter',
customToolReplacesArgs: 9999, // replaces all built-in args
noDebug: true,
toolArgs: <String>['tool_args'], // should still be in args
);
await adapter.configurationDoneRequest(request, null, () {});
await adapter.launchRequest(request, args, responseCompleter.complete);
await responseCompleter.future;
expect(adapter.executable, equals('/custom/flutter'));
// normal built-in args are replaced by customToolReplacesArgs, but
// user-provided toolArgs are not.
expect(adapter.processArgs, isNot(contains('--machine')));
expect(adapter.processArgs, contains('tool_args'));
});
});
});
}
// 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/dap.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/platform.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,
}) {
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 MockFlutterDebugAdapter._(
stdinController.sink,
stdoutController.stream,
channel,
fileSystem: fileSystem,
platform: platform,
);
}
MockFlutterDebugAdapter._(
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;
@override
Future<void> launchAsProcess(String executable, List<String> processArgs) async {
this.executable = executable;
this.processArgs = processArgs;
// Pretend we launched the app and got the app.started event so that
// launchRequest will complete.
appStartedCompleter.complete();
}
}
/// 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;
@override
Future<void> launchAsProcess(String executable, List<String> processArgs,) async {
this.executable = executable;
this.processArgs = processArgs;
}
}
class MockRequest extends Request {
MockRequest()
: super.fromMap(<String, Object?>{
'command': 'mock_command',
'type': 'mock_type',
'seq': _requestId++,
});
static int _requestId = 1;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment