Unverified Commit e8ad0309 authored by Hannes Winkler's avatar Hannes Winkler Committed by GitHub

[flutter_tools] Add support for custom devices (#78113)

parent c1ca47b4
......@@ -28,11 +28,12 @@ class Config {
required FileSystem fileSystem,
required Logger logger,
required Platform platform,
bool deleteFileOnFormatException = true
}) {
final String filePath = _configPath(platform, fileSystem, name);
final File file = fileSystem.file(filePath);
file.parent.createSync(recursive: true);
return Config.createForTesting(file, logger);
return Config.createForTesting(file, logger, deleteFileOnFormatException: deleteFileOnFormatException);
}
/// Constructs a new [Config] object from a file called [name] in
......@@ -43,14 +44,19 @@ class Config {
String name = 'test',
Directory? directory,
Logger? logger,
bool deleteFileOnFormatException = true
}) {
directory ??= MemoryFileSystem.test().directory('/');
return Config.createForTesting(directory.childFile('.${kConfigDir}_$name'), logger ?? BufferLogger.test());
return Config.createForTesting(
directory.childFile('.${kConfigDir}_$name'),
logger ?? BufferLogger.test(),
deleteFileOnFormatException: deleteFileOnFormatException
);
}
/// Test only access to the Config constructor.
@visibleForTesting
Config.createForTesting(File file, Logger logger) : _file = file, _logger = logger {
Config.createForTesting(File file, Logger logger, {bool deleteFileOnFormatException = true}) : _file = file, _logger = logger {
if (!_file.existsSync()) {
return;
}
......@@ -65,7 +71,10 @@ class Config {
'You may need to reapply any previously saved configuration '
'with the "flutter config" command.',
);
if (deleteFileOnFormatException) {
_file.deleteSync();
}
} on Exception catch (err) {
_logger
..printError('Could not read preferences in ${file.path}.\n$err')
......
......@@ -389,3 +389,55 @@ bool isWhitespace(_AnsiRun run) {
rune == 0x3000 ||
rune == 0xFEFF;
}
final RegExp _interpolationRegex = RegExp(r'\$\{([^}]*)\}');
/// Given a string that possibly contains string interpolation sequences
/// (so for example, something like `ping -n 1 ${host}`), replace all those
/// interpolation sequences with the matching value given in [replacementValues].
///
/// If the value could not be found inside [replacementValues], an empty
/// string will be substituted instead.
///
/// However, if the dollar sign inside the string is preceded with a backslash,
/// the sequences won't be substituted at all.
///
/// Example:
/// ```dart
/// final interpolated = _interpolateString(r'ping -n 1 ${host}', {'host': 'raspberrypi'});
/// print(interpolated); // will print 'ping -n 1 raspberrypi'
///
/// final interpolated2 = _interpolateString(r'ping -n 1 ${_host}', {'host': 'raspberrypi'});
/// print(interpolated2); // will print 'ping -n 1 '
/// ```
String interpolateString(String toInterpolate, Map<String, String> replacementValues) {
return toInterpolate.replaceAllMapped(_interpolationRegex, (Match match) {
/// The name of the variable to be inserted into the string.
/// Example: If the source string is 'ping -n 1 ${host}',
/// `name` would be 'host'
final String name = match.group(1)!;
return replacementValues.containsKey(name) ? replacementValues[name]! : '';
});
}
/// Given a list of strings possibly containing string interpolation sequences
/// (so for example, something like `['ping', '-n', '1', '${host}']`), replace
/// all those interpolation sequences with the matching value given in [replacementValues].
///
/// If the value could not be found inside [replacementValues], an empty
/// string will be substituted instead.
///
/// However, if the dollar sign inside the string is preceded with a backslash,
/// the sequences won't be substituted at all.
///
/// Example:
/// ```dart
/// final interpolated = _interpolateString(['ping', '-n', '1', r'${host}'], {'host': 'raspberrypi'});
/// print(interpolated); // will print '[ping, -n, 1, raspberrypi]'
///
/// final interpolated2 = _interpolateString(['ping', '-n', '1', r'${_host}'], {'host': 'raspberrypi'});
/// print(interpolated2); // will print '[ping, -n, 1, ]'
/// ```
List<String> interpolateStringList(List<String> toInterpolate, Map<String, String> replacementValues) {
return toInterpolate.map((String s) => interpolateString(s, replacementValues)).toList();
}
......@@ -391,6 +391,9 @@ class DaemonDomain extends Domain {
if (featureFlags.isFuchsiaEnabled && flutterProject.fuchsia.existsSync()) {
result.add('fuchsia');
}
if (featureFlags.areCustomDevicesEnabled) {
result.add('custom');
}
return <String, Object>{
'platforms': result,
};
......
......@@ -30,6 +30,7 @@ import 'base/user_messages.dart';
import 'build_info.dart';
import 'build_system/build_system.dart';
import 'cache.dart';
import 'custom_devices/custom_devices_config.dart';
import 'dart/pub.dart';
import 'devfs.dart';
import 'device.dart';
......@@ -197,6 +198,11 @@ Future<T> runInContext<T>(
),
operatingSystemUtils: globals.os,
terminal: globals.terminal,
customDevicesConfig: CustomDevicesConfig(
fileSystem: globals.fs,
logger: globals.logger,
platform: globals.platform
),
),
DevtoolsLauncher: () => DevtoolsServerLauncher(
processManager: globals.processManager,
......
// 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:meta/meta.dart';
/// A single configured custom device.
///
/// In the custom devices config file on disk, there may be multiple custom
/// devices configured.
@immutable
class CustomDeviceConfig {
const CustomDeviceConfig({
required this.id,
required this.label,
required this.sdkNameAndVersion,
required this.disabled,
required this.pingCommand,
this.pingSuccessRegex,
required this.postBuildCommand,
required this.installCommand,
required this.uninstallCommand,
required this.runDebugCommand,
this.forwardPortCommand,
this.forwardPortSuccessRegex
}) : assert(forwardPortCommand == null || forwardPortSuccessRegex != null);
factory CustomDeviceConfig.fromJson(dynamic json) {
final Map<String, Object> typedMap = (json as Map<dynamic, dynamic>).cast<String, Object>();
return CustomDeviceConfig(
id: typedMap[_kId]! as String,
label: typedMap[_kLabel]! as String,
sdkNameAndVersion: typedMap[_kSdkNameAndVersion]! as String,
disabled: typedMap[_kDisabled]! as bool,
pingCommand: _castStringList(typedMap[_kPingCommand]!),
pingSuccessRegex: _convertToRegexOrNull(typedMap[_kPingSuccessRegex]),
postBuildCommand: _castStringListOrNull(typedMap[_kPostBuildCommand]),
installCommand: _castStringList(typedMap[_kInstallCommand]!),
uninstallCommand: _castStringList(typedMap[_kUninstallCommand]!),
runDebugCommand: _castStringList(typedMap[_kRunDebugCommand]!),
forwardPortCommand: _castStringListOrNull(typedMap[_kForwardPortCommand]),
forwardPortSuccessRegex: _convertToRegexOrNull(typedMap[_kForwardPortSuccessRegex])
);
}
static const String _kId = 'id';
static const String _kLabel = 'label';
static const String _kSdkNameAndVersion = 'sdkNameAndVersion';
static const String _kDisabled = 'disabled';
static const String _kPingCommand = 'ping';
static const String _kPingSuccessRegex = 'pingSuccessRegex';
static const String _kPostBuildCommand = 'postBuild';
static const String _kInstallCommand = 'install';
static const String _kUninstallCommand = 'uninstall';
static const String _kRunDebugCommand = 'runDebug';
static const String _kForwardPortCommand = 'forwardPort';
static const String _kForwardPortSuccessRegex = 'forwardPortSuccessRegex';
/// An example device config used for creating the default config file.
static final CustomDeviceConfig example = CustomDeviceConfig(
id: 'test1',
label: 'Test Device',
sdkNameAndVersion: 'Test Device 4 Model B+',
disabled: true,
pingCommand: const <String>['ping', '-w', '500', '-n', '1', 'raspberrypi'],
pingSuccessRegex: RegExp('ms TTL='),
postBuildCommand: null,
installCommand: const <String>['scp', '-r', r'${localPath}', r'pi@raspberrypi:/tmp/${appName}'],
uninstallCommand: const <String>['ssh', 'pi@raspberrypi', r'rm -rf "/tmp/${appName}"'],
runDebugCommand: const <String>['ssh', 'pi@raspberrypi', r'flutter-pi "/tmp/${appName}"'],
forwardPortCommand: const <String>['ssh', '-o', 'ExitOnForwardFailure=yes', '-L', r'127.0.0.1:${hostPort}:127.0.0.1:${devicePort}', 'pi@raspberrypi'],
forwardPortSuccessRegex: RegExp('Linux')
);
final String id;
final String label;
final String sdkNameAndVersion;
final bool disabled;
final List<String> pingCommand;
final RegExp? pingSuccessRegex;
final List<String>? postBuildCommand;
final List<String> installCommand;
final List<String> uninstallCommand;
final List<String> runDebugCommand;
final List<String>? forwardPortCommand;
final RegExp? forwardPortSuccessRegex;
bool get usesPortForwarding => forwardPortCommand != null;
static List<String> _castStringList(Object object) {
return (object as List<dynamic>).cast<String>();
}
static List<String>? _castStringListOrNull(Object? object) {
return object == null ? null : _castStringList(object);
}
static RegExp? _convertToRegexOrNull(Object? object) {
return object == null ? null : RegExp(object as String);
}
dynamic toJson() {
return <String, Object?>{
_kId: id,
_kLabel: label,
_kSdkNameAndVersion: sdkNameAndVersion,
_kDisabled: disabled,
_kPingCommand: pingCommand,
_kPingSuccessRegex: pingSuccessRegex?.pattern,
_kPostBuildCommand: postBuildCommand,
_kInstallCommand: installCommand,
_kUninstallCommand: uninstallCommand,
_kRunDebugCommand: runDebugCommand,
_kForwardPortCommand: forwardPortCommand,
_kForwardPortSuccessRegex: forwardPortSuccessRegex?.pattern
};
}
CustomDeviceConfig copyWith({
String? id,
String? label,
String? sdkNameAndVersion,
bool? disabled,
List<String>? pingCommand,
bool explicitPingSuccessRegex = false,
RegExp? pingSuccessRegex,
bool explicitPostBuildCommand = false,
List<String>? postBuildCommand,
List<String>? installCommand,
List<String>? uninstallCommand,
List<String>? runDebugCommand,
bool explicitForwardPortCommand = false,
List<String>? forwardPortCommand,
bool explicitForwardPortSuccessRegex = false,
RegExp? forwardPortSuccessRegex
}) {
return CustomDeviceConfig(
id: id ?? this.id,
label: label ?? this.label,
sdkNameAndVersion: sdkNameAndVersion ?? this.sdkNameAndVersion,
disabled: disabled ?? this.disabled,
pingCommand: pingCommand ?? this.pingCommand,
pingSuccessRegex: explicitPingSuccessRegex ? pingSuccessRegex : (pingSuccessRegex ?? this.pingSuccessRegex),
postBuildCommand: explicitPostBuildCommand ? postBuildCommand : (postBuildCommand ?? this.postBuildCommand),
installCommand: installCommand ?? this.installCommand,
uninstallCommand: uninstallCommand ?? this.uninstallCommand,
runDebugCommand: runDebugCommand ?? this.runDebugCommand,
forwardPortCommand: explicitForwardPortCommand ? forwardPortCommand : (forwardPortCommand ?? this.forwardPortCommand),
forwardPortSuccessRegex: explicitForwardPortSuccessRegex ? forwardPortSuccessRegex : (forwardPortSuccessRegex ?? this.forwardPortSuccessRegex)
);
}
}
// 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 'package:meta/meta.dart';
import '../doctor.dart';
import '../features.dart';
/// The custom-devices-specific implementation of a [Workflow].
///
/// Will apply to the host platform / be able to launch & list devices only if
/// the custom devices feature is enabled in the featureFlags argument.
///
/// Can't list emulators at all.
@immutable
class CustomDeviceWorkflow implements Workflow {
const CustomDeviceWorkflow({
@required FeatureFlags featureFlags
}) : _featureFlags = featureFlags;
final FeatureFlags _featureFlags;
@override
bool get appliesToHostPlatform => _featureFlags.areCustomDevicesEnabled;
@override
bool get canLaunchDevices => _featureFlags.areCustomDevicesEnabled;
@override
bool get canListDevices => _featureFlags.areCustomDevicesEnabled;
@override
bool get canListEmulators => false;
}
// 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 'package:meta/meta.dart';
import '../base/config.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
import '../base/platform.dart';
import '../cache.dart';
import 'custom_device_config.dart';
/// Represents the custom devices config file on disk which in turn
/// contains a list of individual custom device configs.
class CustomDevicesConfig {
/// Load a [CustomDevicesConfig] from a (possibly non-existent) location on disk.
///
/// The config is loaded on construction. Any error while loading will be logged
/// but will not result in an exception being thrown. The file will not be deleted
/// when it's not valid JSON (which other configurations do) and will not
/// be implicitly created when it doesn't exist.
CustomDevicesConfig({
@required FileSystem fileSystem,
@required Logger logger,
@required Platform platform
}) : _fileSystem = fileSystem,
_config = Config(
_kCustomDevicesConfigName,
fileSystem: fileSystem,
logger: logger,
platform: platform,
deleteFileOnFormatException: false
)
{
ensureFileExists();
}
@visibleForTesting
CustomDevicesConfig.test({
@required FileSystem fileSystem,
Directory directory,
@required Logger logger
}) : _fileSystem = fileSystem,
_config = Config.test(
name: _kCustomDevicesConfigName,
directory: directory,
logger: logger,
deleteFileOnFormatException: false
)
{
ensureFileExists();
}
static const String _kCustomDevicesConfigName = 'custom_devices.json';
static const String _kCustomDevicesConfigKey = 'custom-devices';
static const String _kSchema = r'$schema';
static const String _kCustomDevices = 'custom-devices';
final FileSystem _fileSystem;
final Config _config;
String get _defaultSchema {
final Uri uri = _fileSystem
.directory(Cache.flutterRoot)
.childDirectory('packages')
.childDirectory('flutter_tools')
.childDirectory('static')
.childFile('custom-devices.schema.json')
.uri;
// otherwise it won't contain the Uri schema, so the file:// at the start
// will be missing
assert(uri.isAbsolute);
return uri.toString();
}
/// Ensure the config file exists on disk by creating one with default values
/// if it doesn't exist yet.
///
/// The config file should always be present so we can give the user a path
/// to a file they can edit.
void ensureFileExists() {
if (!_fileSystem.file(_config.configPath).existsSync()) {
_config.setValue(_kSchema, _defaultSchema);
_config.setValue(_kCustomDevices, <dynamic>[CustomDeviceConfig.example.toJson()]);
}
}
/// Get the list of [CustomDeviceConfig]s that are listed in the config file
/// including disabled ones.
///
/// Returns an empty list when the config could not be loaded.
List<CustomDeviceConfig> get devices {
final dynamic json = _config.getValue(_kCustomDevicesConfigKey);
if (json == null) {
return <CustomDeviceConfig>[];
}
final List<dynamic> typedList = json as List<dynamic>;
return typedList.map((dynamic e) => CustomDeviceConfig.fromJson(e)).toList();
}
// We don't have a setter for devices here because we don't need it and
// it also may overwrite any things done by the user that aren't explicitly
// tracked by the JSON-representation. For example comments (not possible right now,
// but they'd be useful so maybe in the future) or formatting.
}
......@@ -52,6 +52,7 @@ class PlatformType {
static const PlatformType macos = PlatformType._('macos');
static const PlatformType windows = PlatformType._('windows');
static const PlatformType fuchsia = PlatformType._('fuchsia');
static const PlatformType custom = PlatformType._('custom');
final String value;
......
......@@ -48,6 +48,9 @@ abstract class FeatureFlags {
/// Whether fuchsia is enabled.
bool get isFuchsiaEnabled => true;
/// Whether custom devices are enabled.
bool get areCustomDevicesEnabled => false;
/// Whether fast single widget reloads are enabled.
bool get isSingleWidgetReloadEnabled => false;
......@@ -97,6 +100,9 @@ class FlutterFeatureFlags implements FeatureFlags {
@override
bool get isFuchsiaEnabled => isEnabled(flutterFuchsiaFeature);
@override
bool get areCustomDevicesEnabled => isEnabled(flutterCustomDevicesFeature);
@override
bool get isSingleWidgetReloadEnabled => isEnabled(singleWidgetReload);
......@@ -140,6 +146,7 @@ const List<Feature> allFeatures = <Feature>[
flutterAndroidFeature,
flutterIOSFeature,
flutterFuchsiaFeature,
flutterCustomDevicesFeature,
experimentalInvalidationStrategy,
];
......@@ -297,6 +304,28 @@ const Feature flutterFuchsiaFeature = Feature(
),
);
const Feature flutterCustomDevicesFeature = Feature(
name: 'Early support for custom device types',
configSetting: 'enable-custom-devices',
environmentOverride: 'FLUTTER_CUSTOM_DEVICES',
master: FeatureChannelSetting(
available: true,
enabledByDefault: false,
),
dev: FeatureChannelSetting(
available: true,
enabledByDefault: false,
),
beta: FeatureChannelSetting(
available: false,
enabledByDefault: false,
),
stable: FeatureChannelSetting(
available: false,
enabledByDefault: false,
)
);
/// The fast hot reload feature for https://github.com/flutter/flutter/issues/61407.
const Feature singleWidgetReload = Feature(
name: 'Hot reload optimization for changes to class body of a single widget',
......@@ -371,7 +400,7 @@ class Feature {
this.master = const FeatureChannelSetting(),
this.dev = const FeatureChannelSetting(),
this.beta = const FeatureChannelSetting(),
this.stable = const FeatureChannelSetting(),
this.stable = const FeatureChannelSetting()
});
/// The user visible name for this feature.
......
......@@ -18,6 +18,8 @@ import 'base/os.dart';
import 'base/platform.dart';
import 'base/terminal.dart';
import 'base/user_messages.dart' hide userMessages;
import 'custom_devices/custom_device.dart';
import 'custom_devices/custom_devices_config.dart';
import 'device.dart';
import 'features.dart';
import 'fuchsia/fuchsia_device.dart';
......@@ -58,6 +60,7 @@ class FlutterDeviceManager extends DeviceManager {
@required OperatingSystemUtils operatingSystemUtils,
@required WindowsWorkflow windowsWorkflow,
@required Terminal terminal,
@required CustomDevicesConfig customDevicesConfig,
}) : deviceDiscoverers = <DeviceDiscovery>[
AndroidDevices(
logger: logger,
......@@ -123,6 +126,12 @@ class FlutterDeviceManager extends DeviceManager {
processManager: processManager,
logger: logger,
),
CustomDevices(
featureFlags: featureFlags,
processManager: processManager,
logger: logger,
config: customDevicesConfig
),
], super(
logger: logger,
terminal: terminal,
......
......@@ -149,8 +149,7 @@ class ProtocolDiscovery {
hostUri = deviceUri.replace(port: actualHostPort);
}
assert(InternetAddress(hostUri.host).isLoopback);
if (ipv6) {
if (InternetAddress(hostUri.host).isLoopback && ipv6) {
hostUri = hostUri.replace(host: InternetAddress.loopbackIPv6.host);
}
return hostUri;
......
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://raw.githubusercontent.com/flutter/flutter/stable/packages/flutter_tools/static/custom-devices.schema.json",
"title": "Flutter Custom Devices",
"description": "The schema for the flutter custom devices config file.",
"type": "object",
"properties": {
"custom-devices": {
"description": "The actual list of custom devices.",
"type": "array",
"items": {
"description": "A single custom device to be configured.",
"type": "object",
"properties": {
"id": {
"description": "A unique, short identification string for this device. Used for example as an argument to the flutter run command.",
"type": "string"
},
"label": {
"description": "A more descriptive, user-friendly label for the device.",
"type": "string"
},
"sdkNameAndVersion": {
"description": "Additional information about the device. For other devices, this is the SDK (for example Android SDK, Windows SDK) name and version.",
"type": "string"
},
"disabled": {
"description": "If true, this device will be ignored completely by the flutter SDK and none of the commands configured will be called. You can use this as a way to comment out device configs you're still working on, for example.",
"type": "boolean"
},
"ping": {
"description": "The command to be invoked to ping the device. Used to find out whether its available or not. Every exit code unequal 0 will be treated as \"unavailable\". On Windows, consider providing a \"pingSuccessRegex\" since Windows' ping will also return 0 on failure. Make sure the command times out internally since it's not guaranteed the flutter SDK will enforce a timeout itself.",
"type": "array",
"items": {
"type": "string"
},
"minItems": 1,
"default": [
"ping", "-w", "500", "-n", "1", "raspberrypi"
]
},
"pingSuccessRegex": {
"description": "When the output of the ping command matches this regex (and the ping command finished with exit code 0), the ping will be considered successful and the pinged device reachable. If this regex is not provided the ping command will be considered successful when it returned with exit code 0. The regex syntax is the standard dart syntax.",
"type": ["string", "null"],
"format": "regex",
"default": "ms TTL=",
"required": false
},
"postBuild": {
"description": "The command to be invoked after the build process is done, to do any additional packaging for example.",
"type": ["array", "null"],
"items": {
"type": "string"
},
"minItems": 1,
"default": null,
"required": false
},
"install": {
"description": "The command to be invoked to install the app on the device. The path to the directory / file to be installed (copied over) to the device is available via the ${localPath} string interpolation and the name of the app to be installed via the ${appName} string interpolation.",
"type": "array",
"items": {
"type": "string"
},
"minItems": 1,
"default": [
"scp", "-r", "${localPath}", "pi@raspberrypi:/tmp/${appName}"
]
},
"uninstall": {
"description": "The command to be invoked to remove the app from the device. Invoked before every invocation of the app install command. The name of the app to be removed is available via the ${appName} string interpolation.",
"type": "array",
"items": {
"type": "string"
},
"minItems": 1,
"default": [
"ssh", "pi@raspberrypi", "rm -rf \"/tmp/${appName}\""
]
},
"runDebug": {
"description": "The command to be invoked to run the app in debug mode. The name of the app to be started is available via the ${appName} string interpolation. Make sure the flutter cmdline output is available via this commands stdout/stderr since the SDK needs the \"Observatory is now listening on ...\" message to function. If the forwardPort command is not specified, the observatory URL will be connected to as-is, without any port forwarding. In that case you need to make sure it is reachable from your host device, possibly via the \"--observatory-host=<ip>\" engine flag.",
"type": "array",
"items": {
"type": "string"
},
"minItems": 1,
"default": [
"ssh", "pi@raspberrypi", "flutter-pi /tmp/${appName} --observatory-host=192.168.178.123"
]
},
"forwardPort": {
"description": "The command to be invoked to forward a specific device port to a port on the host device. The host port is available via ${hostPort} and the device port via ${devicePort}. On success, the command should stay running for the duration of the forwarding. The command will be terminated using SIGTERM when the forwarding should be stopped. When using ssh, make sure ssh quits when the forwarding fails since thats not the default behaviour.",
"type": ["array", "null"],
"items": {
"type": "string"
},
"minItems": 1,
"default": [
"ssh", "-o", "ExitOnForwardFailure=yes", "-L", "127.0.0.1:${hostPort}:127.0.0.1:${devicePort}", "pi@raspberrypi"
],
"required": false
},
"forwardPortSuccessRegex": {
"description": "A regular expression to be used to classify a successful port forwarding. As soon as any line of stdout or stderr of the forward port command matches this regex, the port forwarding is considered successful. The regex syntax is the standard dart syntax. This value needs to be present & non-null when \"forwardPort\" specified.",
"type": ["string", "null"],
"format": "regex",
"default": "Linux",
"required": false
}
}
}
}
}
}
// 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 'package:flutter_tools/src/custom_devices/custom_device_workflow.dart';
import '../../src/common.dart';
import '../../src/fakes.dart';
void main() {
testWithoutContext('CustomDeviceWorkflow reports false when custom devices feature is disabled', () {
final CustomDeviceWorkflow workflow = CustomDeviceWorkflow(featureFlags: TestFeatureFlags(areCustomDevicesEnabled: false));
expect(workflow.appliesToHostPlatform, false);
expect(workflow.canLaunchDevices, false);
expect(workflow.canListDevices, false);
expect(workflow.canListEmulators, false);
});
testWithoutContext('CustomDeviceWorkflow reports true for everything except canListEmulators when custom devices feature is enabled', () {
final CustomDeviceWorkflow workflow = CustomDeviceWorkflow(featureFlags: TestFeatureFlags(areCustomDevicesEnabled: true));
expect(workflow.appliesToHostPlatform, true);
expect(workflow.canLaunchDevices, true);
expect(workflow.canListDevices, true);
expect(workflow.canListEmulators, false);
});
}
......@@ -12,7 +12,9 @@ import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/build_system/build_system.dart';
import 'package:flutter_tools/src/bundle.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/dart/pub.dart';
......@@ -560,6 +562,7 @@ class TestFeatureFlags implements FeatureFlags {
this.isAndroidEnabled = true,
this.isIOSEnabled = true,
this.isFuchsiaEnabled = false,
this.areCustomDevicesEnabled = false,
this.isExperimentalInvalidationStrategyEnabled = false,
this.isWindowsUwpEnabled = false,
});
......@@ -588,6 +591,9 @@ class TestFeatureFlags implements FeatureFlags {
@override
final bool isFuchsiaEnabled;
@override
final bool areCustomDevicesEnabled;
@override
final bool isExperimentalInvalidationStrategyEnabled;
......@@ -613,6 +619,8 @@ class TestFeatureFlags implements FeatureFlags {
return isIOSEnabled;
case flutterFuchsiaFeature:
return isFuchsiaEnabled;
case flutterCustomDevicesFeature:
return areCustomDevicesEnabled;
case experimentalInvalidationStrategy:
return isExperimentalInvalidationStrategyEnabled;
case windowsUwpEmbedding:
......@@ -692,3 +700,22 @@ class TestBuildSystem implements BuildSystem {
return _results[_nextResult++];
}
}
class FakeBundleBuilder extends Fake implements BundleBuilder {
@override
Future<void> build({
TargetPlatform platform,
BuildInfo buildInfo,
String mainPath,
String manifestPath = defaultManifestPath,
String applicationKernelFilePath,
String depfilePath,
String assetDirPath,
bool trackWidgetCreation = false,
List<String> extraFrontEndOptions = const <String>[],
List<String> extraGenSnapshotOptions = const <String>[],
List<String> fileSystemRoots,
String fileSystemScheme,
bool treeShakeIcons
}) => Future<void>.value();
}
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