Unverified Commit de966d8a authored by Danny Tuppeny's avatar Danny Tuppeny Committed by GitHub

Add a "flutter debug_adapter" command that runs a DAP server (#91802)

parent 8b88645b
...@@ -26,6 +26,7 @@ import 'src/commands/config.dart'; ...@@ -26,6 +26,7 @@ import 'src/commands/config.dart';
import 'src/commands/create.dart'; import 'src/commands/create.dart';
import 'src/commands/custom_devices.dart'; import 'src/commands/custom_devices.dart';
import 'src/commands/daemon.dart'; import 'src/commands/daemon.dart';
import 'src/commands/debug_adapter.dart';
import 'src/commands/devices.dart'; import 'src/commands/devices.dart';
import 'src/commands/doctor.dart'; import 'src/commands/doctor.dart';
import 'src/commands/downgrade.dart'; import 'src/commands/downgrade.dart';
...@@ -160,6 +161,7 @@ List<FlutterCommand> generateCommands({ ...@@ -160,6 +161,7 @@ List<FlutterCommand> generateCommands({
), ),
CreateCommand(verboseHelp: verboseHelp), CreateCommand(verboseHelp: verboseHelp),
DaemonCommand(hidden: !verboseHelp), DaemonCommand(hidden: !verboseHelp),
DebugAdapterCommand(verboseHelp: verboseHelp),
DevicesCommand(verboseHelp: verboseHelp), DevicesCommand(verboseHelp: verboseHelp),
DoctorCommand(verbose: verbose), DoctorCommand(verbose: verbose),
DowngradeCommand(verboseHelp: verboseHelp), DowngradeCommand(verboseHelp: verboseHelp),
......
// 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.
// @dart = 2.8
import 'dart:async';
import '../debug_adapters/server.dart';
import '../globals.dart' as globals;
import '../runner/flutter_command.dart';
/// This command will start up a Debug Adapter that communicates using the Debug Adapter Protocol (DAP).
///
/// This is for use by editors and IDEs that have DAP clients to launch and
/// debug Flutter apps/tests. It extends the standard Dart DAP implementation
/// from DDS with Flutter-specific functionality (such as Hot Restart).
///
/// The server is intended to be single-use. It should live only for the
/// duration of a single debug session in the editor, and terminate when the
/// user stops debugging. If a user starts multiple debug sessions
/// simultaneously it is expected that the editor will start multiple debug
/// adapters.
///
/// The DAP specification can be found at
/// https://microsoft.github.io/debug-adapter-protocol/.
class DebugAdapterCommand extends FlutterCommand {
DebugAdapterCommand({ bool verboseHelp = false}) : hidden = !verboseHelp {
usesIpv6Flag(verboseHelp: verboseHelp);
addDdsOptions(verboseHelp: verboseHelp);
}
@override
final String name = 'debug-adapter';
@override
List<String> get aliases => const <String>['debug_adapter'];
@override
final String description = 'Run a Debug Adapter Protocol (DAP) server to communicate with the Flutter tool.';
@override
final String category = FlutterCommandCategory.tools;
@override
final bool hidden;
@override
Future<FlutterCommandResult> runCommand() async {
final DapServer server = DapServer(
globals.stdio.stdin,
globals.stdio.stdout.nonBlocking,
fileSystem: globals.fs,
platform: globals.platform,
ipv6: ipv6,
enableDds: enableDds,
);
await server.channel.closed;
return FlutterCommandResult.success();
}
}
# Debug Adapter Protocol (DAP)
This document is Flutter-specific. For information on the standard Dart DAP implementation, [see this document](https://github.com/dart-lang/sdk/blob/main/pkg/dds/tool/dap/README.md).
Flutter includes support for debugging using [the Debug Adapter Protocol](https://microsoft.github.io/debug-adapter-protocol/) as an alternative to using the [VM Service](https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md) directly, simplying the integration for new editors.
The debug adapter is started with the `flutter debug-adapter` command and is intended to be consumed by DAP-compliant tools such as Flutter-specific extensions for editors, or configured by users whose editors include generic configurable DAP clients.
Because in the DAP protocol the client speaks first, running this command from the terminal will result in no output (nor will the process terminate). This is expected behaviour.
For details on the standard DAP functionality, see [the Debug Adapter Protocol Overview](https://microsoft.github.io/debug-adapter-protocol/) and [the Debug Adapter Protocol Specification](https://microsoft.github.io/debug-adapter-protocol/specification). Custom extensions are detailed below.
## Launch/Attach Arguments
Arguments common to both `launchRequest` and `attachRequest` are:
- `bool? debugExternalPackageLibraries` - whether to enable debugging for packages that are not inside the current workspace (if not supplied, defaults to `true`)
- `bool? debugSdkLibraries` - whether to enable debugging for SDK libraries (if not supplied, defaults to `true`)
- `bool? evaluateGettersInDebugViews` - whether to evaluate getters in expression evaluation requests (inc. hovers/watch windows) (if not supplied, defaults to `false`)
- `bool? evaluateToStringInDebugViews` - whether to invoke `toString()` in expression evaluation requests (inc. hovers/watch windows) (if not supplied, defaults to `false`)
- `bool? sendLogsToClient` - used to proxy all VM Service traffic back to the client in custom `dart.log` events (has performance implications, intended for troubleshooting) (if not supplied, defaults to `false`)
- `List<String>? additionalProjectPaths` - paths of any projects (outside of `cwd`) that are open in the users workspace
- `String? cwd` - the working directory for the Dart process to be spawned in
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`)
`attachRequest` is not currently supported, but will be documented here when it is.
## Custom Requests
Some custom requests are available for clients to call. Below are the Flutter-specific custom requests, but the standard Dart DAP custom requests are also [documented here](https://github.com/dart-lang/sdk/blob/main/pkg/dds/tool/dap/README.md#custom-requests).
### `hotReload`
`hotReload` injects updated source code files into the running VM and then rebuilds the widget tree. An optional `reason` can be provided and should usually be `"manual"` or `"save"` to indicate what how the reload was triggered (for example by the user clicking a button, versus a hot-reload-on-save feature).
```
{
"reason": "manual"
}
```
### `hotRestart`
`hotRestart` updates the code on the device and performs a full restart (which does not preserve state). An optional `reason` can be provided and should usually be `"manual"` or `"save"` to indicate what how the reload was triggered (for example by the user clicking a button, versus a hot-reload-on-save feature).
```
{
"reason": "manual"
}
```
## Custom Events
The debug adapter may emit several custom events that are useful to clients. There are not currently any custom Flutter events, but the standard Dart DAP custom requests are [documented here](https://github.com/dart-lang/sdk/blob/main/pkg/dds/tool/dap/README.md#custom-events).
// 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' hide PidTracker, PackageConfigUtils;
import 'package:vm_service/vm_service.dart' as vm;
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/platform.dart';
import '../cache.dart';
import '../convert.dart';
import 'flutter_adapter_args.dart';
import 'mixins.dart';
/// A DAP Debug Adapter for running and debugging Flutter applications.
class FlutterDebugAdapter extends DartDebugAdapter<FlutterLaunchRequestArguments, FlutterAttachRequestArguments>
with PidTracker, PackageConfigUtils {
FlutterDebugAdapter(
ByteStreamServerChannel channel, {
required this.fileSystem,
required this.platform,
bool ipv6 = false,
bool enableDds = true,
bool enableAuthCodes = true,
Logger? logger,
}) : super(
channel,
ipv6: ipv6,
enableDds: enableDds,
enableAuthCodes: enableAuthCodes,
logger: logger,
);
@override
FileSystem fileSystem;
Platform platform;
Process? _process;
@override
final FlutterLaunchRequestArguments Function(Map<String, Object?> obj)
parseLaunchArgs = FlutterLaunchRequestArguments.fromJson;
@override
final FlutterAttachRequestArguments Function(Map<String, Object?> obj)
parseAttachArgs = FlutterAttachRequestArguments.fromJson;
/// A completer that completes when the app.started event has been received.
final Completer<void> _appStartedCompleter = Completer<void>();
/// Whether or not the app.started event has been received.
bool get _receivedAppStarted => _appStartedCompleter.isCompleted;
/// The VM Service URI received from the app.debugPort event.
Uri? _vmServiceUri;
/// The appId of the current running Flutter app.
String? _appId;
/// The ID to use for the next request sent to the Flutter run daemon.
int _flutterRequestId = 1;
/// Outstanding requests that have been sent to the Flutter run daemon and
/// their handlers.
final Map<int, Completer<Object?>> _flutterRequestCompleters = <int, Completer<Object?>>{};
/// Whether or not this adapter can handle the restartRequest.
///
/// For Flutter apps we can handle this with a Hot Restart rather than having
/// the whole debug session stopped and restarted.
@override
bool get supportsRestartRequest => true;
/// Whether the VM Service closing should be used as a signal to terminate the debug session.
///
/// Since we always have a process for Flutter (whether run or attach) we'll
/// always use its termination instead, so this is always false.
@override
bool get terminateOnVmServiceClose => false;
/// Called by [attachRequest] to request that we actually connect to the app to be debugged.
@override
Future<void> attachImpl() async {
sendOutput('console', '\nAttach is not currently supported');
handleSessionTerminate();
}
/// [customRequest] handles any messages that do not match standard messages in the spec.
///
/// This is used to allow a client/DA to have custom methods outside of the
/// spec. It is up to the client/DA to negotiate which custom messages are
/// allowed.
///
/// [sendResponse] must be called when handling a message, even if it is with
/// a null response. Otherwise the client will never be informed that the
/// request has completed.
///
/// Any requests not handled must call super which will respond with an error
/// that the message was not supported.
///
/// Unless they start with _ to indicate they are private, custom messages
/// should not change in breaking ways if client IDEs/editors may be calling
/// them.
@override
Future<void> customRequest(
Request request,
RawRequestArguments? args,
void Function(Object?) sendResponse,
) async {
switch (request.command) {
case 'hotRestart':
case 'hotReload':
final bool isFullRestart = request.command == 'hotRestart';
await _performRestart(isFullRestart, args?.args['reason'] as String?);
sendResponse(null);
break;
default:
await super.customRequest(request, args, sendResponse);
}
}
@override
Future<void> debuggerConnected(vm.VM vmInfo) async {
// Capture the PID from the VM Service so that we can terminate it when
// cleaning up. Terminating the process might not be enough as it could be
// just a shell script (e.g. flutter.bat on Windows) and may not pass the
// signal on correctly.
// See: https://github.com/Dart-Code/Dart-Code/issues/907
final int? pid = vmInfo.pid;
if (pid != null) {
pidsToTerminate.add(pid);
}
}
/// Called by [disconnectRequest] to request that we forcefully shut down the app being run (or in the case of an attach, disconnect).
///
/// Client IDEs/editors should send a terminateRequest before a
/// disconnectRequest to allow a graceful shutdown. This method must terminate
/// quickly and therefore may leave orphaned processes.
@override
Future<void> disconnectImpl() async {
terminatePids(ProcessSignal.sigkill);
}
/// Called by [launchRequest] to request that we actually start the app to be run/debugged.
///
/// For debugging, this should start paused, connect to the VM Service, set
/// breakpoints, and resume.
@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"
// from VS Code (eg. not want to hit breakpoints/etc.) but still be running
// a debug build.
final bool debug = !(args.noDebug ?? false);
final String? program = args.program;
final List<String> toolArgs = <String>[
'run',
'--machine',
if (debug) ...<String>[
'--start-paused',
],
];
final List<String> processArgs = <String>[
...toolArgs,
...?args.toolArgs,
if (program != null) ...<String>[
'--target',
program,
],
...?args.args,
];
// Find the package_config file for this script. This is used by the
// debugger to map package: URIs to file paths to check whether they're in
// the editors workspace (args.cwd/args.additionalProjectPaths) so they can
// be correctly classes as "my code", "sdk" or "external packages".
// TODO(dantup): Remove this once https://github.com/dart-lang/sdk/issues/45530
// is done as it will not be necessary.
final String? possibleRoot = program == null
? args.cwd
: fileSystem.path.isAbsolute(program)
? fileSystem.path.dirname(program)
: fileSystem.path.dirname(
fileSystem.path.normalize(fileSystem.path.join(args.cwd ?? '', args.program)));
if (possibleRoot != null) {
final File? packageConfig = findPackageConfigFile(possibleRoot);
if (packageConfig != null) {
usePackageConfigFile(packageConfig);
}
}
logger?.call('Spawning $flutterToolPath with $processArgs in ${args.cwd}');
final Process process = await Process.start(
flutterToolPath,
processArgs,
workingDirectory: args.cwd,
);
_process = process;
pidsToTerminate.add(process.pid);
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).
///
/// For Flutter, we handle this ourselves be sending a Hot Restart request
/// to the running app.
@override
Future<void> restartRequest(
Request request,
RestartArguments? args,
void Function() sendResponse,
) async {
await _performRestart(true);
sendResponse();
}
/// Sends a request to the Flutter daemon that is running/attaching to the app and waits for a response.
///
/// If [failSilently] is `true` (the default) and there is no process, the
/// message will be silently ignored (this is common during the application
/// being stopped, where async messages may be processed). Setting it to
/// `false` will cause a [DebugAdapterException] to be thrown in that case.
Future<Object?> sendFlutterRequest(
String method,
Map<String, Object?>? params, {
bool failSilently = true,
}) async {
final Process? process = _process;
if (process == null) {
if (failSilently) {
return null;
} else {
throw DebugAdapterException(
'Unable to Restart because Flutter process is not available',
);
}
}
final Completer<Object?> completer = Completer<Object?>();
final int id = _flutterRequestId++;
_flutterRequestCompleters[id] = completer;
// Flutter requests are always wrapped in brackets as an array.
final String messageString = jsonEncode(
<String, Object?>{'id': id, 'method': method, 'params': params},
);
final String payload = '[$messageString]\n';
process.stdin.writeln(payload);
return completer.future;
}
/// Called by [terminateRequest] to request that we gracefully shut down the app being run (or in the case of an attach, disconnect).
@override
Future<void> terminateImpl() async {
terminatePids(ProcessSignal.sigterm);
await _process?.exitCode;
}
/// Connects to the VM Service if the app.started event has fired, and a VM Service URI is available.
void _connectDebuggerIfReady() {
final Uri? serviceUri = _vmServiceUri;
if (_receivedAppStarted && serviceUri != null) {
connectDebugger(serviceUri, resumeIfStarting: true);
}
}
/// Handles the app.start event from Flutter.
void _handleAppStart(Map<String, Object?> params) {
_appId = params['appId'] as String?;
assert(_appId != null);
}
/// Handles the app.started event from Flutter.
void _handleAppStarted() {
_appStartedCompleter.complete();
_connectDebuggerIfReady();
}
/// Handles the app.debugPort event from Flutter, connecting to the VM Service if everything else is ready.
void _handleDebugPort(Map<String, Object?> params) {
// When running in noDebug mode, Flutter may still provide us a VM Service
// URI, but we will not connect it because we don't want to do any debugging.
final FlutterLaunchRequestArguments args = this.args as FlutterLaunchRequestArguments;
final bool debug = !(args.noDebug ?? false);
if (!debug) {
return;
}
// Capture the VM Service URL which we'll connect to when we get app.started.
final String? wsUri = params['wsUri'] as String?;
if (wsUri != null) {
_vmServiceUri = Uri.parse(wsUri);
}
_connectDebuggerIfReady();
}
/// Handles the Flutter process exiting, terminating the debug session if it has not already begun terminating.
void _handleExitCode(int code) {
final String codeSuffix = code == 0 ? '' : ' ($code)';
logger?.call('Process exited ($code)');
handleSessionTerminate(codeSuffix);
}
/// Handles incoming JSON events from `flutter run --machine`.
void _handleJsonEvent(String event, Map<String, Object?>? params) {
params ??= <String, Object?>{};
switch (event) {
case 'app.debugPort':
_handleDebugPort(params);
break;
case 'app.start':
_handleAppStart(params);
break;
case 'app.started':
_handleAppStarted();
break;
}
}
/// Handles incoming JSON messages from `flutter run --machine` that are responses to requests that we sent.
void _handleJsonResponse(int id, Map<String, Object?> response) {
final Completer<Object?>? handler = _flutterRequestCompleters.remove(id);
if (handler == null) {
logger?.call(
'Received response from Flutter run daemon with ID $id '
'but had not matching handler',
);
return;
}
final Object? error = response['error'];
final Object? result = response['result'];
if (error != null) {
handler.completeError(DebugAdapterException('$error'));
} else {
handler.complete(result);
}
}
void _handleStderr(List<int> data) {
logger?.call('stderr: $data');
sendOutput('stderr', utf8.decode(data));
}
/// Handles stdout from the `flutter run --machine` process, decoding the JSON and calling the appropriate handlers.
void _handleStdout(String data) {
// Output intended for us to parse is JSON wrapped in brackets:
// [{"event":"app.foo","params":{"bar":"baz"}}]
// However, it's also possible a user printed things that look a little like
// this so try to detect only things we're interested in:
// - parses as JSON
// - is a List of only a single item that is a Map<String, Object?>
// - the item has an "event" field that is a String
// - the item has a "params" field that is a Map<String, Object?>?
logger?.call('stdout: $data');
// Output is sent as console (eg. output from tooling) until the app has
// started, then stdout (users output). This is so info like
// "Launching lib/main.dart on Device foo" is formatted differently to
// general output printed by the user.
final String outputCategory = _receivedAppStarted ? 'stdout' : 'console';
// Output in stdout can include both user output (eg. print) and Flutter
// daemon output. Since it's not uncommon for users to print JSON while
// debugging, we must try to detect which messages are likely Flutter
// messages as reliably as possible, as trying to process users output
// as a Flutter message may result in an unhandled error that will
// terminate the debug adater in a way that does not provide feedback
// because the standard crash violates the DAP protocol.
Object? jsonData;
try {
jsonData = jsonDecode(data);
} on FormatException {
// If the output wasn't valid JSON, it was standard stdout that should
// be passed through to the user.
sendOutput(outputCategory, data);
return;
}
final Map<String, Object?>? payload = jsonData is List &&
jsonData.length == 1 &&
jsonData.first is Map<String, Object?>
? jsonData.first as Map<String, Object?>
: null;
if (payload == null) {
// JSON didn't match expected format for Flutter responses, so treat as
// standard user output.
sendOutput(outputCategory, data);
return;
}
final Object? event = payload['event'];
final Object? params = payload['params'];
final Object? id = payload['id'];
if (event is String && params is Map<String, Object?>?) {
_handleJsonEvent(event, params);
} else if (id is int && _flutterRequestCompleters.containsKey(id)) {
_handleJsonResponse(id, payload);
} else {
// If it wasn't processed above,
sendOutput(outputCategory, data);
}
}
/// Performs a restart/reload by sending the `app.restart` message to the `flutter run --machine` process.
Future<void> _performRestart(
bool fullRestart, [
String? reason,
]) async {
final DartCommonLaunchAttachRequestArguments args = this.args;
final bool debug =
args is! FlutterLaunchRequestArguments || args.noDebug != true;
try {
await sendFlutterRequest('app.restart', <String, Object?>{
'appId': _appId,
'fullRestart': fullRestart,
'pause': debug,
'reason': reason,
'debounce': true,
});
} on DebugAdapterException catch (error) {
final String action = fullRestart ? 'Hot Restart' : 'Hot Reload';
sendOutput('console', 'Failed to $action: $error');
}
}
}
// 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 'package:dds/dap.dart';
/// An implementation of [AttachRequestArguments] that includes all fields used by the Flutter debug adapter.
///
/// This class represents the data passed from the client editor to the debug
/// adapter in attachRequest, which is a request to start debugging an
/// application.
class FlutterAttachRequestArguments
extends DartCommonLaunchAttachRequestArguments
implements AttachRequestArguments {
FlutterAttachRequestArguments({
Object? restart,
String? name,
String? cwd,
List<String>? additionalProjectPaths,
bool? debugSdkLibraries,
bool? debugExternalPackageLibraries,
bool? evaluateGettersInDebugViews,
bool? evaluateToStringInDebugViews,
bool? sendLogsToClient,
}) : super(
name: name,
cwd: cwd,
restart: restart,
additionalProjectPaths: additionalProjectPaths,
debugSdkLibraries: debugSdkLibraries,
debugExternalPackageLibraries: debugExternalPackageLibraries,
evaluateGettersInDebugViews: evaluateGettersInDebugViews,
evaluateToStringInDebugViews: evaluateToStringInDebugViews,
sendLogsToClient: sendLogsToClient,
);
FlutterAttachRequestArguments.fromMap(Map<String, Object?> obj):
super.fromMap(obj);
static FlutterAttachRequestArguments fromJson(Map<String, Object?> obj) =>
FlutterAttachRequestArguments.fromMap(obj);
}
/// An implementation of [LaunchRequestArguments] that includes all fields used by the Flutter debug adapter.
///
/// This class represents the data passed from the client editor to the debug
/// adapter in launchRequest, which is a request to start debugging an
/// application.
class FlutterLaunchRequestArguments
extends DartCommonLaunchAttachRequestArguments
implements LaunchRequestArguments {
FlutterLaunchRequestArguments({
this.noDebug,
required this.program,
this.args,
this.toolArgs,
Object? restart,
String? name,
String? cwd,
List<String>? additionalProjectPaths,
bool? debugSdkLibraries,
bool? debugExternalPackageLibraries,
bool? evaluateGettersInDebugViews,
bool? evaluateToStringInDebugViews,
bool? sendLogsToClient,
}) : super(
restart: restart,
name: name,
cwd: cwd,
additionalProjectPaths: additionalProjectPaths,
debugSdkLibraries: debugSdkLibraries,
debugExternalPackageLibraries: debugExternalPackageLibraries,
evaluateGettersInDebugViews: evaluateGettersInDebugViews,
evaluateToStringInDebugViews: evaluateToStringInDebugViews,
sendLogsToClient: sendLogsToClient,
);
FlutterLaunchRequestArguments.fromMap(Map<String, Object?> obj)
: noDebug = obj['noDebug'] as bool?,
program = obj['program'] as String?,
args = (obj['args'] as List<Object?>?)?.cast<String>(),
toolArgs = (obj['toolArgs'] as List<Object?>?)?.cast<String>(),
super.fromMap(obj);
/// If noDebug is true the launch request should launch the program without enabling debugging.
@override
final bool? noDebug;
/// The program/Flutter app to be run.
final String? program;
/// Arguments to be passed to [program].
final List<String>? args;
/// Arguments to be passed to the tool that will run [program] (for example, the VM or Flutter tool).
final List<String>? toolArgs;
@override
Map<String, Object?> toJson() => <String, Object?>{
...super.toJson(),
if (noDebug != null) 'noDebug': noDebug,
if (program != null) 'program': program,
if (args != null) 'args': args,
if (toolArgs != null) 'toolArgs': toolArgs,
};
static FlutterLaunchRequestArguments fromJson(Map<String, Object?> obj) =>
FlutterLaunchRequestArguments.fromMap(obj);
}
// 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 '../base/file_system.dart';
import '../base/io.dart';
/// A mixin providing some utility functions for locating/working with
/// package_config.json files.
///
/// Adapted from package:dds/src/dap/adapters/mixins.dart to use Flutter's
/// dart:io wrappers.
mixin PackageConfigUtils {
abstract FileSystem fileSystem;
/// Find the `package_config.json` file for the program being launched.
File? findPackageConfigFile(String possibleRoot) {
// TODO(dantup): Remove this once
// https://github.com/dart-lang/sdk/issues/45530 is done as it will not be
// necessary.
File? packageConfig;
while (true) {
packageConfig = fileSystem.file(
fileSystem.path.join(possibleRoot, '.dart_tool', 'package_config.json'),
);
// If this packageconfig exists, use it.
if (packageConfig.existsSync()) {
break;
}
final String parent = fileSystem.path.dirname(possibleRoot);
// If we can't go up anymore, the search failed.
if (parent == possibleRoot) {
packageConfig = null;
break;
}
possibleRoot = parent;
}
return packageConfig;
}
}
/// A mixin for tracking additional PIDs that can be shut down at the end of a debug session.
///
/// Adapted from package:dds/src/dap/adapters/mixins.dart to use Flutter's
/// dart:io wrappers.
mixin PidTracker {
/// Process IDs to terminate during shutdown.
///
/// This may be populated with pids from the VM Service to ensure we clean up
/// properly where signals may not be passed through the shell to the
/// underlying VM process.
/// https://github.com/Dart-Code/Dart-Code/issues/907
final Set<int> pidsToTerminate = <int>{};
/// Terminates all processes with the PIDs registered in [pidsToTerminate].
void terminatePids(ProcessSignal signal) {
pidsToTerminate.forEach(signal.send);
}
}
// 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' hide DapServer;
import '../base/file_system.dart';
import '../base/platform.dart';
import '../debug_adapters/flutter_adapter.dart';
import '../debug_adapters/flutter_adapter_args.dart';
/// A DAP server that communicates over a [ByteStreamServerChannel], usually constructed from the processes stdin/stdout streams.
///
/// The server is intended to be single-use. It should live only for the
/// duration of a single debug session in the editor, and terminate when the
/// user stops debugging. If a user starts multiple debug sessions
/// simultaneously it is expected that the editor will start multiple debug
/// adapters.
class DapServer {
DapServer(
Stream<List<int>> _input,
StreamSink<List<int>> _output, {
required FileSystem fileSystem,
required Platform platform,
this.ipv6 = false,
this.enableDds = true,
this.enableAuthCodes = true,
this.logger,
}) : channel = ByteStreamServerChannel(_input, _output, logger) {
adapter = FlutterDebugAdapter(channel,
fileSystem: fileSystem,
platform: platform,
ipv6: ipv6,
enableDds: enableDds,
enableAuthCodes: enableAuthCodes,
logger: logger);
}
final ByteStreamServerChannel channel;
late final DartDebugAdapter<FlutterLaunchRequestArguments, FlutterAttachRequestArguments> adapter;
final bool ipv6;
final bool enableDds;
final bool enableAuthCodes;
final Logger? logger;
void stop() {
channel.close();
}
}
// 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.
// @dart = 2.8
import 'dart:async';
import 'package:dds/src/dap/protocol_generated.dart';
import 'package:file/file.dart';
import 'package:flutter_tools/src/cache.dart';
import '../../src/common.dart';
import '../test_data/basic_project.dart';
import '../test_data/compile_error_project.dart';
import '../test_utils.dart';
import 'test_client.dart';
import 'test_support.dart';
void main() {
Directory tempDir;
/*late*/ DapTestSession dap;
final String relativeMainPath = 'lib${fileSystem.path.separator}main.dart';
setUpAll(() {
Cache.flutterRoot = getFlutterRoot();
});
setUp(() async {
tempDir = createResolvedTempDirectorySync('debug_adapter_test.');
dap = await DapTestSession.setUp();
});
tearDown(() async {
await dap.tearDown();
tryToDelete(tempDir);
});
testWithoutContext('can run and terminate a Flutter app in debug mode', () async {
final BasicProject _project = BasicProject();
await _project.setUpIn(tempDir);
// Once the "topLevelFunction" output arrives, we can terminate the app.
unawaited(
dap.client.outputEvents
.firstWhere((OutputEventBody output) => output.output.startsWith('topLevelFunction'))
.whenComplete(() => dap.client.terminate()),
);
final List<OutputEventBody> outputEvents = await dap.client.collectAllOutput(
launch: () => dap.client
.launch(
cwd: _project.dir.path,
toolArgs: <String>['-d', 'flutter-tester'],
),
);
final String output = _uniqueOutputLines(outputEvents);
expectLines(output, <Object>[
'Launching $relativeMainPath on Flutter test device in debug mode...',
startsWith('Connecting to VM Service at'),
'topLevelFunction',
'',
startsWith('Exited'),
]);
});
testWithoutContext('can run and terminate a Flutter app in noDebug mode', () async {
final BasicProject _project = BasicProject();
await _project.setUpIn(tempDir);
// Once the "topLevelFunction" output arrives, we can terminate the app.
unawaited(
dap.client.outputEvents
.firstWhere((OutputEventBody output) => output.output.startsWith('topLevelFunction'))
.whenComplete(() => dap.client.terminate()),
);
final List<OutputEventBody> outputEvents = await dap.client.collectAllOutput(
launch: () => dap.client
.launch(
cwd: _project.dir.path,
noDebug: true,
toolArgs: <String>['-d', 'flutter-tester'],
),
);
final String output = _uniqueOutputLines(outputEvents);
expectLines(output, <Object>[
'Launching $relativeMainPath on Flutter test device in debug mode...',
'topLevelFunction',
'',
startsWith('Exited'),
]);
});
testWithoutContext('correctly outputs launch errors and terminates', () async {
final CompileErrorProject _project = CompileErrorProject();
await _project.setUpIn(tempDir);
final List<OutputEventBody> outputEvents = await dap.client.collectAllOutput(
launch: () => dap.client
.launch(
cwd: _project.dir.path,
toolArgs: <String>['-d', 'flutter-tester'],
),
);
final String output = _uniqueOutputLines(outputEvents);
expect(output, contains('this code does not compile'));
expect(output, contains('Exception: Failed to build'));
expect(output, contains('Exited (1)'));
});
testWithoutContext('can hot reload', () async {
final BasicProject _project = BasicProject();
await _project.setUpIn(tempDir);
// Launch the app and wait for it to print "topLevelFunction".
await Future.wait(<Future<Object>>[
dap.client.outputEvents.firstWhere((OutputEventBody output) => output.output.startsWith('topLevelFunction')),
dap.client.start(
launch: () => dap.client.launch(
cwd: _project.dir.path,
noDebug: true,
toolArgs: <String>['-d', 'flutter-tester'],
),
),
], eagerError: true);
// Capture the next two output events that we expect to be the Reload
// notification and then topLevelFunction being printed again.
final Future<List<String>> outputEventsFuture = dap.client.output
// But skip any topLevelFunctions that come before the reload.
.skipWhile((String output) => output.startsWith('topLevelFunction'))
.take(2)
.toList();
await dap.client.hotReload();
expectLines(
(await outputEventsFuture).join(),
<Object>[
startsWith('Reloaded'),
'topLevelFunction',
],
);
await dap.client.terminate();
});
testWithoutContext('can hot restart', () async {
final BasicProject _project = BasicProject();
await _project.setUpIn(tempDir);
// Launch the app and wait for it to print "topLevelFunction".
await Future.wait(<Future<Object>>[
dap.client.outputEvents.firstWhere((OutputEventBody output) => output.output.startsWith('topLevelFunction')),
dap.client.start(
launch: () => dap.client.launch(
cwd: _project.dir.path,
noDebug: true,
toolArgs: <String>['-d', 'flutter-tester'],
),
),
], eagerError: true);
// Capture the next two output events that we expect to be the Restart
// notification and then topLevelFunction being printed again.
final Future<List<String>> outputEventsFuture = dap.client.output
// But skip any topLevelFunctions that come before the restart.
.skipWhile((String output) => output.startsWith('topLevelFunction'))
.take(2)
.toList();
await dap.client.hotRestart();
expectLines(
(await outputEventsFuture).join(),
<Object>[
startsWith('Restarted application'),
'topLevelFunction',
],
);
await dap.client.terminate();
});
}
/// Extracts the output from a set of [OutputEventBody], removing any
/// adjacent duplicates and combining into a single string.
String _uniqueOutputLines(List<OutputEventBody> outputEvents) {
String/*?*/ lastItem;
return outputEvents
.map((OutputEventBody e) => e.output)
.where((String output) {
// Skip the item if it's the same as the previous one.
final bool isDupe = output == lastItem;
lastItem = output;
return !isDupe;
})
.join();
}
// 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 the string output from [OutputEventBody] events.
Stream<String> get output => outputEvents.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 'Did not recieve $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);
}
/// 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 Dart program.
Future<Response> launch({
String? program,
List<String>? args,
List<String>? toolArgs,
String? cwd,
bool? noDebug,
List<String>? additionalProjectPaths,
String? console,
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
// (DartLaunchRequestArguments).
overrideCommand: 'launch',
);
}
/// 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;
}
/// Initializes the debug adapter and launches [program]/[cwd] or calls the
/// custom [launch] method.
Future<void> start({
String? program,
String? cwd,
Future<Object?> Function()? launch,
}) {
return Future.wait(<Future<Object?>>[
initialize(),
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.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);
}
}
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 recieved, 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<Response> 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;
}
}
// 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 'dart:convert';
import 'dart:io';
import 'package:dds/src/dap/logging.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/debug_adapters/server.dart';
import 'package:flutter_tools/src/globals_null_migrated.dart' as globals;
/// Enable to run from local source when running out-of-process (useful in
/// development to avoid having to keep rebuilding the flutter tool).
const bool _runFromSource = false;
abstract class DapTestServer {
Future<void> stop();
StreamSink<List<int>> get sink;
Stream<List<int>> get stream;
}
/// An instance of a DAP server running in-process (to aid debugging).
///
/// All communication still goes over the socket to ensure all messages are
/// serialized and deserialized but it's not quite the same running out of
/// process.
class InProcessDapTestServer extends DapTestServer {
InProcessDapTestServer._(List<String> args) {
_server = DapServer(
stdinController.stream,
stdoutController.sink,
fileSystem: globals.fs,
platform: globals.platform,
// Simulate flags based on the args to aid testing.
enableDds: !args.contains('--no-dds'),
ipv6: args.contains('--ipv6'),
);
}
late final DapServer _server;
final StreamController<List<int>> stdinController = StreamController<List<int>>();
final StreamController<List<int>> stdoutController = StreamController<List<int>>();
@override
StreamSink<List<int>> get sink => stdinController.sink;
@override
Stream<List<int>> get stream => stdoutController.stream;
@override
Future<void> stop() async {
_server.stop();
}
static Future<InProcessDapTestServer> create({
Logger? logger,
List<String>? additionalArgs,
}) async {
return InProcessDapTestServer._(additionalArgs ?? <String>[]);
}
}
/// An instance of a DAP server running out-of-process.
///
/// This is how an editor will usually consume DAP so is a more accurate test
/// but will be a little more difficult to debug tests as the debugger will not
/// be attached to the process.
class OutOfProcessDapTestServer extends DapTestServer {
OutOfProcessDapTestServer._(
this._process,
Logger? logger,
) {
// Treat anything written to stderr as the DAP crashing and fail the test
// unless it's "Waiting for another flutter command to release the startup lock".
_process.stderr
.transform(utf8.decoder)
.where((String error) => !error.contains('Waiting for another flutter command to release the startup lock'))
.listen((String error) {
logger?.call(error);
throw error;
});
unawaited(_process.exitCode.then((int code) {
final String message = 'Out-of-process DAP server terminated with code $code';
logger?.call(message);
if (!_isShuttingDown && code != 0) {
throw message;
}
}));
}
bool _isShuttingDown = false;
final Process _process;
@override
StreamSink<List<int>> get sink => _process.stdin;
@override
Stream<List<int>> get stream => _process.stdout;
@override
Future<void> stop() async {
_isShuttingDown = true;
_process.kill();
await _process.exitCode;
}
static Future<OutOfProcessDapTestServer> create({
Logger? logger,
List<String>? additionalArgs,
}) async {
// runFromSource=true will run "dart bin/flutter_tools.dart ..." to avoid
// having to rebuild the flutter_tools snapshot.
// runFromSource=false will run "flutter ..."
final String flutterToolPath = globals.fs.path.join(Cache.flutterRoot!, 'bin', Platform.isWindows ? 'flutter.bat' : 'flutter');
final String flutterToolsEntryScript = globals.fs.path.join(Cache.flutterRoot!, 'packages', 'flutter_tools', 'bin', 'flutter_tools.dart');
// When running from source, run "dart bin/flutter_tools.dart debug_adapter"
// instead of directly using "flutter debug_adapter".
final String executable = _runFromSource
? Platform.resolvedExecutable
: flutterToolPath;
final List<String> args = <String>[
if (_runFromSource) flutterToolsEntryScript,
'debug-adapter',
...?additionalArgs,
];
final Process _process = await Process.start(executable, args);
return OutOfProcessDapTestServer._(_process, logger);
}
}
// 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 'dart:io';
import 'package:dds/src/dap/logging.dart';
import 'package:test/test.dart';
import 'test_client.dart';
import 'test_server.dart';
/// Whether to run the DAP server in-process with the tests, or externally in
/// another process.
///
/// By default tests will run the DAP server out-of-process to match the real
/// use from editors, but this complicates debugging the adapter. Set this env
/// variables to run the server in-process for easier debugging (this can be
/// simplified in VS Code by using a launch config with custom CodeLens links).
final bool useInProcessDap = Platform.environment['DAP_TEST_INTERNAL'] == 'true';
/// Whether to print all protocol traffic to stdout while running tests.
///
/// This is useful for debugging locally or on the bots and will include both
/// DAP traffic (between the test DAP client and the DAP server) and the VM
/// Service traffic (wrapped in a custom 'dart.log' event).
final bool verboseLogging = Platform.environment['DAP_TEST_VERBOSE'] == 'true';
/// Expects the lines in [actual] to match the relevant matcher in [expected],
/// ignoring differences in line endings and trailing whitespace.
void expectLines(String actual, List<Object> expected) {
expect(
actual.replaceAll('\r\n', '\n').trim().split('\n'),
equals(expected),
);
}
/// A helper class containing the DAP server/client for DAP integration tests.
class DapTestSession {
DapTestSession._(this.server, this.client);
DapTestServer server;
DapTestClient client;
Future<void> tearDown() async {
await client.stop();
await server.stop();
}
static Future<DapTestSession> setUp({List<String>? additionalArgs}) async {
final DapTestServer server = await _startServer(additionalArgs: additionalArgs);
final DapTestClient client = await DapTestClient.connect(
server,
captureVmServiceTraffic: verboseLogging,
logger: verboseLogging ? print : null,
);
return DapTestSession._(server, client);
}
/// Starts a DAP server that can be shared across tests.
static Future<DapTestServer> _startServer({
Logger? logger,
List<String>? additionalArgs,
}) async {
return useInProcessDap
? await InProcessDapTestServer.create(
logger: logger,
additionalArgs: additionalArgs,
)
: await OutOfProcessDapTestServer.create(
logger: logger,
additionalArgs: additionalArgs,
);
}
}
...@@ -33,7 +33,6 @@ import 'dart:convert'; ...@@ -33,7 +33,6 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:pedantic/pedantic.dart';
import 'package:process/process.dart'; import 'package:process/process.dart';
import '../src/common.dart'; import '../src/common.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.
// @dart = 2.8
import 'project.dart';
class CompileErrorProject extends Project {
@override
final String pubspec = '''
name: test
environment:
sdk: ">=2.12.0-0 <3.0.0"
dependencies:
flutter:
sdk: flutter
''';
@override
final String main = r'''
import 'dart:async';
import 'package:flutter/material.dart';
Future<void> main() async {
this code does not compile
}
''';
}
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