Refactor et run (and friends). (flutter/engine#55537)

Does a few things:

- Refactors `run_command_test` significantly to reduce global fixtures
- Replaced stringly-typed things with enum-like objects
- Adds a lot stronger coverage for `run_command` to make future refactors safer
- Takes advantage of `package:test` having a workable matchers system and uses it
- Changes `return 1` into `throw FatalError(...)` where it makes sense in `run_command`

As a result of the refactoring work, I also fixed a bug: Closes https://github.com/flutter/flutter/issues/147646.
This commit is contained in:
Matan Lurey 2024-10-03 12:32:08 -07:00 committed by GitHub
parent 3ad82894c5
commit 6c9af1435d
10 changed files with 1561 additions and 360 deletions

View File

@ -5,11 +5,15 @@
import 'dart:io' show ProcessStartMode; import 'dart:io' show ProcessStartMode;
import 'package:engine_build_configs/engine_build_configs.dart'; import 'package:engine_build_configs/engine_build_configs.dart';
import 'package:meta/meta.dart';
import 'package:process_runner/process_runner.dart'; import 'package:process_runner/process_runner.dart';
import '../build_utils.dart'; import '../build_utils.dart';
import '../flutter_tool_interop/device.dart';
import '../flutter_tool_interop/flutter_tool.dart';
import '../flutter_tool_interop/target_platform.dart';
import '../label.dart'; import '../label.dart';
import '../run_utils.dart'; import '../logger.dart';
import 'command.dart'; import 'command.dart';
import 'flags.dart'; import 'flags.dart';
@ -21,6 +25,7 @@ final class RunCommand extends CommandBase {
required Map<String, BuilderConfig> configs, required Map<String, BuilderConfig> configs,
super.help = false, super.help = false,
super.usageLineLength, super.usageLineLength,
@visibleForTesting FlutterTool? flutterTool,
}) { }) {
// When printing the help/usage for this command, only list all builds // When printing the help/usage for this command, only list all builds
// when the --verbose flag is supplied. // when the --verbose flag is supplied.
@ -41,8 +46,13 @@ final class RunCommand extends CommandBase {
defaultsTo: environment.hasRbeConfigInTree(), defaultsTo: environment.hasRbeConfigInTree(),
help: 'RBE is enabled by default when available.', help: 'RBE is enabled by default when available.',
); );
_flutterTool = flutterTool ?? FlutterTool.fromEnvironment(environment);
} }
/// Flutter tool.
late final FlutterTool _flutterTool;
/// List of compatible builds. /// List of compatible builds.
late final List<Build> builds; late final List<Build> builds;
@ -58,7 +68,7 @@ Run a Flutter app with a local engine build.
See `flutter run --help` for a listing See `flutter run --help` for a listing
'''; ''';
Build? _lookup(String configName) { Build? _findTargetBuild(String configName) {
final String demangledName = demangleConfigName(environment, configName); final String demangledName = demangleConfigName(environment, configName);
return builds return builds
.where((Build build) => build.name == demangledName) .where((Build build) => build.name == demangledName)
@ -78,11 +88,11 @@ See `flutter run --help` for a listing
final String ci = final String ci =
mangledName.startsWith('ci') ? mangledName.substring(0, 3) : ''; mangledName.startsWith('ci') ? mangledName.substring(0, 3) : '';
if (mangledName.contains('_debug')) { if (mangledName.contains('_debug')) {
return _lookup('${ci}host_debug'); return _findTargetBuild('${ci}host_debug');
} else if (mangledName.contains('_profile')) { } else if (mangledName.contains('_profile')) {
return _lookup('${ci}host_profile'); return _findTargetBuild('${ci}host_profile');
} else if (mangledName.contains('_release')) { } else if (mangledName.contains('_release')) {
return _lookup('${ci}host_release'); return _findTargetBuild('${ci}host_release');
} }
return null; return null;
} }
@ -114,62 +124,59 @@ See `flutter run --help` for a listing
return mode; return mode;
} }
late final Future<RunTarget?> _runTarget = late final Future<RunTarget?> _runTarget = (() async {
detectAndSelectRunTarget(environment, _getDeviceId()); final devices = await _flutterTool.devices();
return RunTarget.detectAndSelect(devices, idPrefix: _getDeviceId());
})();
Future<String?> _selectTargetConfig() async { Future<String> _selectTargetConfig() async {
final String configName = argResults![configFlag] as String; final configName = argResults![configFlag] as String;
if (configName.isNotEmpty) { if (configName.isNotEmpty) {
return demangleConfigName(environment, configName); return configName;
} }
final RunTarget? target = await _runTarget; final target = await _runTarget;
if (target == null) { if (target == null) {
return demangleConfigName(environment, 'host_debug'); return 'host_debug';
} }
environment.logger.status(
'Building to run on "${target.name}" running ${target.targetPlatform}'); final result = target.buildConfigFor(_getMode());
return target.buildConfigFor(_getMode()); environment.logger.status('Building to run on $result');
return result;
} }
@override @override
Future<int> run() async { Future<int> run() async {
if (!environment.processRunner.processManager.canRun('flutter')) { if (!environment.processRunner.processManager.canRun('flutter')) {
environment.logger.error('Cannot find the flutter command in your path'); throw FatalError('Cannot find the "flutter" command in your PATH');
return 1;
}
final String? configName = await _selectTargetConfig();
if (configName == null) {
environment.logger.error('Could not find target config');
return 1;
}
final Build? build = _lookup(configName);
final Build? hostBuild = _findHostBuild(build);
if (build == null) {
environment.logger.error('Could not find build $configName');
return 1;
}
if (hostBuild == null) {
environment.logger.error('Could not find host build for $configName');
return 1;
} }
final bool useRbe = argResults![rbeFlag] as bool; final configName = await _selectTargetConfig();
if (useRbe && !environment.hasRbeConfigInTree()) { final targetBuild = _findTargetBuild(configName);
environment.logger.error('RBE was requested but no RBE config was found'); if (targetBuild == null) {
return 1; throw FatalError('Could not find build $configName');
} }
final List<String> extraGnArgs = <String>[
final hostBuild = _findHostBuild(targetBuild);
if (hostBuild == null) {
throw FatalError('Could not find host build for $configName');
}
final useRbe = argResults!.flag(rbeFlag);
if (useRbe && !environment.hasRbeConfigInTree()) {
throw FatalError('RBE was requested but no RBE config was found');
}
final extraGnArgs = [
if (!useRbe) '--no-rbe', if (!useRbe) '--no-rbe',
]; ];
final RunTarget? target = await _runTarget; final target = await _runTarget;
final List<Label> buildTargetsForShell = final buildTargetsForShell = target?.buildTargetsForShell ?? [];
target?.buildTargetsForShell() ?? <Label>[];
final String dashJ = argResults![concurrencyFlag] as String; final concurrency = int.tryParse(argResults![concurrencyFlag] as String);
final int? concurrency = int.tryParse(dashJ);
if (concurrency == null || concurrency < 0) { if (concurrency == null || concurrency < 0) {
environment.logger.error('-j must specify a positive integer.'); throw FatalError(
return 1; '--$concurrencyFlag (-j) must specify a positive integer.',
);
} }
// First build the host. // First build the host.
@ -181,25 +188,26 @@ See `flutter run --help` for a listing
enableRbe: useRbe, enableRbe: useRbe,
); );
if (r != 0) { if (r != 0) {
return r; throw FatalError('Failed to build host (${hostBuild.name})');
} }
// Now build the target if it isn't the same. // Now build the target if it isn't the same.
if (hostBuild.name != build.name) { if (hostBuild.name != targetBuild.name) {
r = await runBuild( r = await runBuild(
environment, environment,
build, targetBuild,
concurrency: concurrency, concurrency: concurrency,
extraGnArgs: extraGnArgs, extraGnArgs: extraGnArgs,
enableRbe: useRbe, enableRbe: useRbe,
targets: buildTargetsForShell, targets: buildTargetsForShell,
); );
if (r != 0) { if (r != 0) {
return r; throw FatalError('Failed to build target (${targetBuild.name})');
} }
} }
final String mangledBuildName = mangleConfigName(environment, build.name); final String mangledBuildName =
mangleConfigName(environment, targetBuild.name);
final String mangledHostBuildName = final String mangledHostBuildName =
mangleConfigName(environment, hostBuild.name); mangleConfigName(environment, hostBuild.name);
@ -227,3 +235,173 @@ See `flutter run --help` for a listing
return result.exitCode; return result.exitCode;
} }
} }
/// Metadata about a target to run `flutter run` on.
///
/// This class translates between the `flutter devices` output and the build
/// configurations supported by the engine, including the build targets needed
/// to build the shell for the target platform.
@visibleForTesting
@immutable
final class RunTarget {
/// Construct a run target from a device returned by `flutter devices`.
@visibleForTesting
const RunTarget.fromDevice(this.device);
/// Device to run on.
final Device device;
/// Given a list of devices, returns a build target for the first matching.
///
/// If [idPrefix] is provided, only devices with an id that starts with the
/// prefix will be considered, otherwise the first device is selected. If no
/// devices are available, or none match the prefix, `null` is returned.
@visibleForTesting
static RunTarget? detectAndSelect(
Iterable<Device> devices, {
String idPrefix = '',
}) {
if (devices.isEmpty) {
return null;
}
for (final device in devices) {
if (idPrefix.isNotEmpty) {
if (device.id.startsWith(idPrefix)) {
return RunTarget.fromDevice(device);
}
}
}
if (idPrefix.isNotEmpty) {
return null;
}
return RunTarget.fromDevice(devices.first);
}
/// Returns the build configuration for the current platform and given [mode].
///
/// The [mode] is typically one of `debug`, `profile`, or `release`.
///
/// Throws a [FatalError] if the target platform is not supported.
String buildConfigFor(String mode) {
return switch (device.targetPlatform) {
// Supported platforms with known mappings.
// -----------------------------------------------------------------------
// ANDROID
TargetPlatform.androidUnspecified => 'android_$mode',
TargetPlatform.androidX86 => 'android_${mode}_x86',
TargetPlatform.androidX64 => 'android_${mode}_x64',
TargetPlatform.androidArm64 => 'android_${mode}_arm64',
// DESKTOP (MacOS, Linux, Windows)
// We do not support cross-builds, so implicitly assume the host platform.
TargetPlatform.darwinUnspecified ||
TargetPlatform.darwinX64 ||
TargetPlatform.linuxX64 ||
TargetPlatform.windowsX64 =>
'host_$mode',
TargetPlatform.darwinArm64 ||
TargetPlatform.linuxArm64 ||
TargetPlatform.windowsArm64 =>
'host_${mode}_arm64',
// WEB
TargetPlatform.webJavascript => 'chrome_$mode',
// Unsupported platforms.
// -----------------------------------------------------------------------
// iOS.
// TODO(matanlurey): https://github.com/flutter/flutter/issues/155960
TargetPlatform.iOSUnspecified ||
TargetPlatform.iOSX64 ||
TargetPlatform.iOSArm64 =>
throw FatalError(
'iOS targets are currently unsupported.\n\nIf you are an '
'iOS engine developer, and have a need for this, please either +1 or '
'help us implement https://github.com/flutter/flutter/issues/155960.',
),
// LEGACY ANDROID
TargetPlatform.androidArm => throw FatalError(
'Legacy Android targets are not supported. '
'Please use android-arm64 or android-x64.',
),
// FUCHSIA
TargetPlatform.fuchsiaArm64 ||
TargetPlatform.fuchsiaX64 =>
throw FatalError('Fuchsia is not supported.'),
// TESTER
TargetPlatform.tester =>
throw FatalError('flutter_tester is not supported.'),
// Platforms that maybe could be supported, but we don't know about.
_ => throw FatalError(
'Unknown target platform: ${device.targetPlatform.identifier}.\n\nIf '
'this is a new platform that should be supported, please file a bug: '
'https://github.com/flutter/flutter/issues/new?labels=e:%20engine-tool.',
),
};
}
/// Minimal build targets needed to build the shell for the target platform.
List<Label> get buildTargetsForShell {
return switch (device.targetPlatform) {
// Supported platforms with known mappings.
// -----------------------------------------------------------------------
// ANDROID
TargetPlatform.androidUnspecified ||
TargetPlatform.androidX86 ||
TargetPlatform.androidX64 ||
TargetPlatform.androidArm64 =>
[Label.parseGn('//flutter/shell/platform/android:android_jar')],
// iOS.
TargetPlatform.iOSUnspecified ||
TargetPlatform.iOSX64 ||
TargetPlatform.iOSArm64 =>
[
Label.parseGn('//flutter/shell/platform/darwin/ios:flutter_framework')
],
// Desktop (MacOS).
TargetPlatform.darwinUnspecified ||
TargetPlatform.darwinX64 ||
TargetPlatform.darwinArm64 =>
[
Label.parseGn(
'//flutter/shell/platform/darwin/macos:flutter_framework',
)
],
// Desktop (Linux).
TargetPlatform.linuxX64 || TargetPlatform.linuxArm64 => [
Label.parseGn(
'//flutter/shell/platform/linux:flutter_linux_gtk',
)
],
// Desktop (Windows).
TargetPlatform.windowsX64 || TargetPlatform.windowsArm64 => [
Label.parseGn(
'//flutter/shell/platform/windows',
)
],
// Web.
TargetPlatform.webJavascript => [
Label.parseGn(
'//flutter/web_sdk:flutter_web_sdk_archive',
)
],
// Unsupported platforms.
// -----------------------------------------------------------------------
_ => throw FatalError(
'Unknown target platform: ${device.targetPlatform.identifier}.\n\nIf '
'this is a new platform that should be supported, please file a bug: '
'https://github.com/flutter/flutter/issues/new?labels=e:%20engine-tool.',
),
};
}
}

View File

@ -0,0 +1,65 @@
import 'dart:convert';
import 'package:meta/meta.dart';
import '../typed_json.dart';
import 'target_platform.dart';
/// A representation of the parsed device from the `flutter devices` command.
///
/// See <https://github.com/flutter/flutter/blob/9441f9d48fce1d0b425628731dd6ecab8c8b0826/packages/flutter_tools/lib/src/device.dart#L892-L911>.
@immutable
final class Device {
/// Creates a device with the given [name], [id], and [targetPlatform].
const Device({
required this.name,
required this.id,
required this.targetPlatform,
});
/// Parses a device from the given [json].
factory Device.fromJson(Map<String, Object?> json) {
return JsonObject(json).map((o) {
return Device(
name: o.string('name'),
id: o.string('id'),
targetPlatform: TargetPlatform.parse(o.string('targetPlatform')),
);
});
}
/// Name of the device.
final String name;
/// Identifier of the device.
final String id;
/// Target platform of the device.
final TargetPlatform targetPlatform;
@override
bool operator ==(Object other) {
return other is Device &&
other.name == name &&
other.id == id &&
other.targetPlatform == targetPlatform;
}
@override
int get hashCode => Object.hash(name, id, targetPlatform);
/// Converts this device to a JSON object, for use within tests.
@visibleForTesting
JsonObject toJson() {
return JsonObject({
'name': name,
'id': id,
'targetPlatform': targetPlatform.identifier,
});
}
@override
String toString() {
return 'Device ${const JsonEncoder.withIndent(' ').convert(this)}';
}
}

View File

@ -0,0 +1,66 @@
import 'dart:convert';
import '../environment.dart';
import '../logger.dart';
import 'device.dart';
/// An interface to the `flutter` command-line tool.
interface class FlutterTool {
/// Creates a new Flutter tool interface using the given [environment].
///
/// A `flutter`[^1] binary must exist on the `PATH`.
///
/// [^1]: On Windows, the binary is named `flutter.bat`.
const FlutterTool.fromEnvironment(this._environment);
final Environment _environment;
String get _toolPath {
return _environment.platform.isWindows ? 'flutter.bat' : 'flutter';
}
/// Returns a list of devices available via the `flutter devices` command.
Future<List<Device>> devices() async {
final result = await _environment.processRunner.runProcess(
[
_toolPath,
'devices',
'--machine',
],
failOk: true,
);
if (result.exitCode != 0) {
throw FatalError(
'Failed to run `flutter devices --machine`.\n\n'
'EXITED: ${result.exitCode}\n'
'STDOUT:\n${result.stdout}\n'
'STDERR:\n${result.stderr}',
);
}
final List<Object?> jsonDevices;
try {
jsonDevices = jsonDecode(result.stdout) as List<Object?>;
} on FormatException catch (e) {
throw FatalError(
'Failed to parse `flutter devices --machine` output:\n$e\n\n'
'STDOUT:\n${result.stdout}',
);
}
final parsedDevices = <Device>[];
for (final device in jsonDevices) {
if (device is! Map<String, Object?>) {
_environment.logger.error(
'Skipping device: Expected a JSON Object, but got:\n$device',
);
continue;
}
try {
parsedDevices.add(Device.fromJson(device));
} on FormatException catch (e) {
_environment.logger.error(
'Skipping device: Failed to parse JSON Object:\n$device\n\n$e',
);
}
}
return parsedDevices;
}
}

View File

@ -0,0 +1,154 @@
// Copyright 2013 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';
/// Platforms that are supported by the Flutter tool.
///
/// This is partially based on the `flutter_tools` `TargetPlatform` class:
/// <https://github.com/flutter/flutter/blob/master/packages/flutter_tools/lib/src/build_info.dart>
///
/// This class is used to represent the target platform of a device.
@immutable
final class TargetPlatform {
const TargetPlatform._(this.identifier);
/// Android, host architecture left unspecified.
static const androidUnspecified = TargetPlatform._('android');
/// Android ARM.
static const androidArm = TargetPlatform._('android-arm');
/// Android ARM64.
static const androidArm64 = TargetPlatform._('android-arm64');
/// Android x64.
static const androidX64 = TargetPlatform._('android-x64');
/// Android x86.
static const androidX86 = TargetPlatform._('android-x86');
/// Linux ARM64.
static const linuxArm64 = TargetPlatform._('linux-arm64');
/// Linux x64.
static const linuxX64 = TargetPlatform._('linux-x64');
/// Windows ARM64.
static const windowsArm64 = TargetPlatform._('windows-arm64');
/// Windows x64.
static const windowsX64 = TargetPlatform._('windows-x64');
/// Fuchsia ARM64.
static const fuchsiaArm64 = TargetPlatform._('fuchsia-arm64');
/// Fuchsia x64.
static const fuchsiaX64 = TargetPlatform._('fuchsia-x64');
/// Darwin, host architecture left unspecified.
static const darwinUnspecified = TargetPlatform._('darwin');
/// Darwin ARM64.
static const darwinArm64 = TargetPlatform._('darwin-arm64');
/// Darwin x64.
static const darwinX64 = TargetPlatform._('darwin-x64');
/// iOS, host architecture left unspecified.
static const iOSUnspecified = TargetPlatform._('ios');
/// iOS, ARM64.
static const iOSArm64 = TargetPlatform._('ios-arm64');
/// iOS, x64.
static const iOSX64 = TargetPlatform._('ios-x64');
/// Flutter tester.
static const tester = TargetPlatform._('flutter-tester');
/// Web/Javascript.
static const webJavascript = TargetPlatform._('web-javascript');
/// Platforms that are recognized by the Flutter tool.
///
/// There is no reason to use or iterate this list in non-test code; to check
/// if a platform is recognized, use [tryParse] and check for `null` instead:
///
/// ```dart
/// final platform = TargetPlatform.tryParse('android-arm');
/// if (platform == null) {
/// // Handle unrecognized platform.
/// }
/// ```
@visibleForTesting
static const knownPlatforms = [
androidUnspecified,
androidArm,
androidArm64,
androidX64,
androidX86,
linuxArm64,
linuxX64,
windowsArm64,
windowsX64,
fuchsiaArm64,
fuchsiaX64,
darwinUnspecified,
darwinArm64,
darwinX64,
iOSUnspecified,
iOSArm64,
iOSX64,
tester,
webJavascript,
];
/// Parses the [TargetPlatform] for a given [identifier].
///
/// Returns `null` if the [identifier] is not recognized.
static TargetPlatform? tryParse(String identifier) {
for (final platform in knownPlatforms) {
if (platform.identifier == identifier) {
return platform;
}
}
return null;
}
/// Parses the [TargetPlatform] for a given [identifier].
///
/// Throws a [FormatException] if the [identifier] is not recognized.
static TargetPlatform parse(String identifier) {
final platform = tryParse(identifier);
if (platform == null) {
throw FormatException(
'Unrecognized TargetPlatform. It is possible that "$identifier" is '
'a new platform that is recognized by the `flutter` tool, but has not '
'been added to engine_tool, or, if this is a test, an intentionally '
'unrecognized platform was used.',
identifier,
);
}
return platform;
}
/// String-based identifier that is returned by `flutter device --machine`.
///
/// See:
/// - <https://github.com/flutter/flutter/blob/9441f9d48fce1d0b425628731dd6ecab8c8b0826/packages/flutter_tools/lib/src/device.dart#L878>.
/// - <https://github.com/flutter/flutter/blob/master/packages/flutter_tools/lib/src/build_info.dart#L736>.
final String identifier;
@override
bool operator ==(Object other) {
return other is TargetPlatform && other.identifier == identifier;
}
@override
int get hashCode => identifier.hashCode;
@override
String toString() => 'TargetPlatform <$identifier>';
}

View File

@ -1,162 +0,0 @@
// Copyright 2013 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:convert';
import 'package:process_runner/process_runner.dart';
import 'environment.dart';
import 'label.dart';
import 'typed_json.dart';
const String _targetPlatformKey = 'targetPlatform';
const String _nameKey = 'name';
const String _idKey = 'id';
/// Target to run a flutter application on.
class RunTarget {
/// Construct a RunTarget from a JSON map.
factory RunTarget.fromJson(Map<String, Object> map) {
return JsonObject(map).map(
(JsonObject json) => RunTarget._(
json.string(_nameKey),
json.string(_idKey),
json.string(_targetPlatformKey),
), onError: (JsonObject source, JsonMapException e) {
throw FormatException(
'Failed to parse RunTarget: $e', source.toPrettyString());
});
}
RunTarget._(this.name, this.id, this.targetPlatform);
/// Name of target device.
final String name;
/// Id of target device.
final String id;
/// Target platform of device.
final String targetPlatform;
/// BuildConfig name for compilation mode.
String buildConfigFor(String mode) {
switch (targetPlatform) {
case 'android-arm64':
return 'android_${mode}_arm64';
case 'darwin':
return 'host_$mode';
case 'web-javascript':
return 'chrome_$mode';
default:
throw UnimplementedError('No mapping for $targetPlatform');
}
}
/// Returns the minimum set of build targets needed to build the shell for
/// this target platform.
List<Label> buildTargetsForShell() {
final List<Label> labels = <Label>[];
switch (targetPlatform) {
case 'android-arm64':
case 'android-arm32':
{
labels.add(
Label.parseGn('//flutter/shell/platform/android:android_jar'));
break;
}
// TODO(cbracken): iOS and MacOS share the same target platform but
// have different build targets. For now hard code iOS.
case 'darwin':
{
labels.add(Label.parseGn(
'//flutter/shell/platform/darwin/ios:flutter_framework'));
break;
}
default:
throw UnimplementedError('No mapping for $targetPlatform');
// For the future:
// //flutter/shell/platform/darwin/macos:flutter_framework
// //flutter/shell/platform/linux:flutter_linux_gtk
// //flutter/shell/platform/windows
// //flutter/web_sdk:flutter_web_sdk_archive
}
return labels;
}
}
/// Parse the raw output of `flutter devices --machine`.
List<RunTarget> parseDevices(Environment env, String flutterDevicesMachine) {
late final List<dynamic> decoded;
try {
decoded = jsonDecode(flutterDevicesMachine) as List<dynamic>;
} on FormatException catch (e) {
env.logger.error(
'Failed to parse flutter devices output: $e\n\n$flutterDevicesMachine\n\n');
return <RunTarget>[];
}
final List<RunTarget> r = <RunTarget>[];
for (final dynamic device in decoded) {
if (device is! Map<String, Object?>) {
return <RunTarget>[];
}
if (!device.containsKey(_nameKey) || !device.containsKey(_idKey)) {
env.logger.error('device is missing required fields:\n$device\n');
return <RunTarget>[];
}
if (!device.containsKey(_targetPlatformKey)) {
env.logger.warning('Skipping ${device[_nameKey]}: '
'Could not find $_targetPlatformKey in device description.');
continue;
}
late final RunTarget target;
try {
target = RunTarget.fromJson(device.cast<String, Object>());
} on FormatException catch (e) {
env.logger.error(e);
return <RunTarget>[];
}
r.add(target);
}
return r;
}
/// Return the default device to be used.
RunTarget? defaultDevice(Environment env, List<RunTarget> targets) {
if (targets.isEmpty) {
return null;
}
return targets.first;
}
/// Select a run target.
RunTarget? selectRunTarget(Environment env, String flutterDevicesMachine,
[String? idPrefix]) {
final List<RunTarget> targets = parseDevices(env, flutterDevicesMachine);
if (idPrefix != null && idPrefix.isNotEmpty) {
for (final RunTarget target in targets) {
if (target.id.startsWith(idPrefix)) {
return target;
}
}
}
return defaultDevice(env, targets);
}
/// Detects available targets and then selects one.
Future<RunTarget?> detectAndSelectRunTarget(Environment env,
[String? idPrefix]) async {
final ProcessRunnerResult result = await env.processRunner
.runProcess(<String>['flutter', 'devices', '--machine']);
if (result.exitCode != 0) {
env.logger.error('flutter devices --machine failed:\n'
'EXIT_CODE:${result.exitCode}\n'
'STDOUT:\n${result.stdout}'
'STDERR:\n${result.stderr}');
return null;
}
return selectRunTarget(env, result.stdout, idPrefix);
}

View File

@ -0,0 +1,208 @@
// Copyright 2013 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:convert';
import 'package:engine_tool/src/flutter_tool_interop/device.dart';
import 'package:engine_tool/src/flutter_tool_interop/flutter_tool.dart';
import 'package:engine_tool/src/flutter_tool_interop/target_platform.dart';
import 'package:engine_tool/src/logger.dart';
import 'package:test/test.dart';
import '../src/matchers.dart';
import '../utils.dart';
void main() {
test('devices handles a non-zero exit code', () async {
final testEnv = TestEnvironment.withTestEngine(
cannedProcesses: [
CannedProcess(
(List<String> command) => command.contains('devices'),
exitCode: 1,
stdout: 'stdout',
stderr: 'stderr',
),
],
);
addTearDown(testEnv.cleanup);
final flutterTool = FlutterTool.fromEnvironment(testEnv.environment);
expect(
() => flutterTool.devices(),
throwsA(
isA<FatalError>().having(
(a) => a.toString(),
'toString()',
allOf([
contains('Failed to run'),
contains('EXITED: 1'),
contains('STDOUT:\nstdout'),
contains('STDERR:\nstderr'),
]),
),
),
);
});
test('devices handles unparseable data', () async {
final testEnv = TestEnvironment.withTestEngine(
cannedProcesses: [
CannedProcess(
(List<String> command) => command.contains('devices'),
stdout: 'not json',
),
],
);
addTearDown(testEnv.cleanup);
final flutterTool = FlutterTool.fromEnvironment(testEnv.environment);
expect(
() => flutterTool.devices(),
throwsA(
isA<FatalError>().having(
(a) => a.toString(),
'toString()',
allOf([
contains('Failed to parse'),
contains('STDOUT:\nnot json'),
]),
),
),
);
});
test('parses a single device successfully', () async {
const testAndroidArm64Device = Device(
name: 'test_device',
id: 'test_id',
targetPlatform: TargetPlatform.androidArm64,
);
final testEnv = TestEnvironment.withTestEngine(
cannedProcesses: [
CannedProcess(
(List<String> command) => command.contains('devices'),
stdout: jsonEncode([testAndroidArm64Device]),
),
],
);
addTearDown(testEnv.cleanup);
final flutterTool = FlutterTool.fromEnvironment(testEnv.environment);
final devices = await flutterTool.devices();
expect(devices, equals([testAndroidArm64Device]));
});
test('parses multiple devices successfully', () async {
const testAndroidArm64Device = Device(
name: 'test_device',
id: 'test_id',
targetPlatform: TargetPlatform.androidArm64,
);
const testIosArm64Device = Device(
name: 'test_ios_device',
id: 'test_ios_id',
targetPlatform: TargetPlatform.iOSArm64,
);
final testEnv = TestEnvironment.withTestEngine(
cannedProcesses: [
CannedProcess(
(List<String> command) => command.contains('devices'),
stdout: jsonEncode([testAndroidArm64Device, testIosArm64Device]),
),
],
);
addTearDown(testEnv.cleanup);
final flutterTool = FlutterTool.fromEnvironment(testEnv.environment);
final devices = await flutterTool.devices();
expect(devices, equals([testAndroidArm64Device, testIosArm64Device]));
});
test('skips entry that is not a JSON map and emits a log error', () async {
final testEnv = TestEnvironment.withTestEngine(
cannedProcesses: [
CannedProcess(
(List<String> command) => command.contains('devices'),
stdout: jsonEncode([
'not a map',
]),
),
],
);
addTearDown(testEnv.cleanup);
final flutterTool = FlutterTool.fromEnvironment(testEnv.environment);
final devices = await flutterTool.devices();
expect(devices, isEmpty);
expect(
testEnv.testLogs,
contains(logRecord(
contains('Skipping device: Expected a JSON Object'),
level: Logger.errorLevel,
)),
);
});
test('skips entry that is missing an expected property', () async {
final testEnv = TestEnvironment.withTestEngine(
cannedProcesses: [
CannedProcess(
(List<String> command) => command.contains('devices'),
stdout: jsonEncode([
<String, Object?>{
'name': 'test_device',
'id': 'test_id',
},
]),
),
],
);
addTearDown(testEnv.cleanup);
final flutterTool = FlutterTool.fromEnvironment(testEnv.environment);
final devices = await flutterTool.devices();
expect(devices, isEmpty);
expect(
testEnv.testLogs,
contains(logRecord(
contains('Skipping device: Failed to parse JSON Object'),
level: Logger.errorLevel,
)),
);
});
test('skips entry with an unrecognized targetPlatform', () async {
final testEnv = TestEnvironment.withTestEngine(
cannedProcesses: [
CannedProcess(
(List<String> command) => command.contains('devices'),
stdout: jsonEncode([
<String, Object?>{
'name': 'test_device',
'id': 'test_id',
'targetPlatform': 'unknown',
},
]),
),
],
);
addTearDown(testEnv.cleanup);
final flutterTool = FlutterTool.fromEnvironment(testEnv.environment);
final devices = await flutterTool.devices();
expect(devices, isEmpty);
expect(
testEnv.testLogs,
contains(logRecord(
contains('Unrecognized TargetPlatform'),
level: Logger.errorLevel,
)),
);
});
}

View File

@ -7,7 +7,7 @@ import 'package:engine_tool/src/label.dart';
import 'package:engine_tool/src/logger.dart'; import 'package:engine_tool/src/logger.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'utils.dart'; import '../utils.dart';
void main() { void main() {
test('gn.desc handles a non-zero exit code', () async { test('gn.desc handles a non-zero exit code', () async {

View File

@ -2,177 +2,580 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:convert' as convert; import 'dart:convert';
import 'dart:ffi' as ffi show Abi; import 'dart:ffi' show Abi;
import 'dart:io' as io; import 'dart:io' as io;
import 'package:engine_build_configs/engine_build_configs.dart'; import 'package:args/command_runner.dart';
import 'package:engine_repo_tools/engine_repo_tools.dart'; import 'package:engine_repo_tools/engine_repo_tools.dart';
import 'package:engine_tool/src/commands/command_runner.dart'; import 'package:engine_tool/src/commands/run_command.dart';
import 'package:engine_tool/src/environment.dart'; import 'package:engine_tool/src/environment.dart';
import 'package:engine_tool/src/label.dart'; import 'package:engine_tool/src/flutter_tool_interop/device.dart';
import 'package:engine_tool/src/flutter_tool_interop/flutter_tool.dart';
import 'package:engine_tool/src/logger.dart'; import 'package:engine_tool/src/logger.dart';
import 'package:engine_tool/src/run_utils.dart'; import 'package:path/path.dart' as p;
import 'package:platform/platform.dart'; import 'package:platform/platform.dart';
import 'package:process_fakes/process_fakes.dart'; import 'package:process_fakes/process_fakes.dart';
import 'package:process_runner/process_runner.dart'; import 'package:process_runner/process_runner.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'fixtures.dart' as fixtures; import 'src/test_build_configs.dart';
void main() { void main() {
final Engine engine; late io.Directory tempRoot;
try { late TestEngine testEngine;
engine = Engine.findWithin();
} catch (e) {
io.stderr.writeln(e);
io.exitCode = 1;
return;
}
final BuilderConfig linuxTestConfig = BuilderConfig.fromJson( setUp(() {
path: 'ci/builders/linux_test_config.json', tempRoot = io.Directory.systemTemp.createTempSync('engine_tool_test');
map: convert.jsonDecode(fixtures.testConfig('Linux', Platform.linux)) testEngine = TestEngine.createTemp(rootDir: tempRoot);
as Map<String, Object?>, });
);
final BuilderConfig macTestConfig = BuilderConfig.fromJson( tearDown(() {
path: 'ci/builders/mac_test_config.json', tempRoot.deleteSync(recursive: true);
map: convert.jsonDecode(fixtures.testConfig('Mac-12', Platform.macOS)) });
as Map<String, Object?>,
);
final BuilderConfig winTestConfig = BuilderConfig.fromJson( test('fails if flutter is not on your PATH', () async {
path: 'ci/builders/win_test_config.json', final failsCanRun = FakeProcessManager(
map: convert.jsonDecode(fixtures.testConfig('Windows-11', Platform.windows)) canRun: (executable, {workingDirectory}) {
as Map<String, Object?>, if (executable == 'flutter') {
); return false;
}
fail('Unexpected');
},
);
final Map<String, BuilderConfig> configs = <String, BuilderConfig>{ final testEnvironment = Environment(
'linux_test_config': linuxTestConfig, abi: Abi.macosArm64,
'mac_test_config': macTestConfig, engine: testEngine,
'win_test_config': winTestConfig, logger: Logger.test((_) {}),
}; platform: _fakePlatform(Platform.linux),
processRunner: ProcessRunner(
(Environment, List<List<String>>) linuxEnv(Logger logger) { defaultWorkingDirectory: tempRoot,
final List<List<String>> runHistory = <List<String>>[]; processManager: failsCanRun,
return (
Environment(
abi: ffi.Abi.linuxX64,
engine: engine,
platform: FakePlatform(
operatingSystem: Platform.linux,
resolvedExecutable: io.Platform.resolvedExecutable,
pathSeparator: '/',
numberOfProcessors: 32,
),
processRunner: ProcessRunner(
processManager: FakeProcessManager(onStart: (List<String> command) {
runHistory.add(command);
switch (command) {
case ['flutter', 'devices', '--machine']:
return FakeProcess(stdout: fixtures.attachedDevices());
default:
return FakeProcess();
}
}, onRun: (List<String> command) {
// Should not be executed.
assert(false);
return io.ProcessResult(81, 1, '', '');
}),
),
logger: logger,
), ),
runHistory
); );
}
test('run command invokes flutter run', () async { final et = _engineTool(
final Logger logger = Logger.test((_) {}); RunCommand(
final (Environment env, List<List<String>> runHistory) = linuxEnv(logger); environment: testEnvironment,
final ToolCommandRunner runner = ToolCommandRunner( // Intentionally left blank, none of these builds make it far enough.
environment: env, configs: {},
configs: configs, ),
); );
final int result =
await runner.run(<String>['run', '--', '--weird_argument']);
expect(result, equals(0));
expect(runHistory.length, greaterThanOrEqualTo(6));
expect(runHistory[5],
containsAllInOrder(<String>['flutter', 'run', '--weird_argument']));
});
test('parse devices list', () async {
final Logger logger = Logger.test((_) {});
final (Environment env, _) = linuxEnv(logger);
final List<RunTarget> targets =
parseDevices(env, fixtures.attachedDevices());
expect(targets.length, equals(4));
final RunTarget android = targets[0];
expect(android.name, contains('gphone64'));
expect(android.buildConfigFor('debug'), equals('android_debug_arm64'));
});
test('target specific shell build', () async {
final Logger logger = Logger.test((_) {});
final (Environment env, _) = linuxEnv(logger);
final List<RunTarget> targets =
parseDevices(env, fixtures.attachedDevices());
final RunTarget android = targets[0];
expect(android.name, contains('gphone64'));
final List<Label> shellLabels = <Label>[
Label.parseGn('//flutter/shell/platform/android:android_jar')
];
expect(android.buildTargetsForShell(), equals(shellLabels));
});
test('default device', () async {
final Logger logger = Logger.test((_) {});
final (Environment env, _) = linuxEnv(logger);
final List<RunTarget> targets =
parseDevices(env, fixtures.attachedDevices());
expect(targets.length, equals(4));
final RunTarget? defaultTarget = defaultDevice(env, targets);
expect(defaultTarget, isNotNull);
expect(defaultTarget!.name, contains('gphone64'));
expect( expect(
defaultTarget.buildConfigFor('debug'), equals('android_debug_arm64')); () => et.run(['run']),
}); throwsA(
isA<FatalError>().having(
test('device select', () async { (a) => a.toString(),
final Logger logger = Logger.test((_) {}); 'toString()',
final (Environment env, _) = linuxEnv(logger); contains('"flutter" command in your PATH'),
RunTarget target = selectRunTarget(env, fixtures.attachedDevices())!; ),
expect(target.name, contains('gphone64')); ),
target = selectRunTarget(env, fixtures.attachedDevices(), 'mac')!;
expect(target.name, contains('macOS'));
});
test('flutter run device select', () async {
final Logger logger = Logger.test((_) {});
final (Environment env, List<List<String>> runHistory) = linuxEnv(logger);
final ToolCommandRunner runner = ToolCommandRunner(
environment: env,
configs: configs,
); );
// Request that the emulator device is used. The emulator is an Android });
// ARM64 device.
final int result = group('configuration failures', () {
await runner.run(<String>['run', '--', '-d', 'emulator']); final unusedProcessManager = FakeProcessManager(
expect(result, equals(0)); canRun: (_, {workingDirectory}) => true,
expect(runHistory.length, greaterThanOrEqualTo(6)); );
// Observe that we selected android_debug_arm64 as the target.
expect( late List<LogRecord> testLogs;
runHistory[5], late Environment testEnvironment;
containsAllInOrder(<String>[ late _FakeFlutterTool flutterTool;
'flutter',
setUp(() {
testLogs = [];
testEnvironment = Environment(
abi: Abi.linuxX64,
engine: testEngine,
logger: Logger.test(testLogs.add),
platform: _fakePlatform(Platform.linux),
processRunner: ProcessRunner(
defaultWorkingDirectory: tempRoot,
processManager: unusedProcessManager,
),
);
flutterTool = _FakeFlutterTool();
});
test('fails if a host build could not be found', () async {
final builders = TestBuilderConfig();
builders.addBuild(
name: 'linux/android_debug_arm64',
dimension: TestDroneDimension.linux,
targetDir: 'android_debug_arm64',
);
final et = _engineTool(RunCommand(
environment: testEnvironment,
configs: {
'linux_test_config': builders.buildConfig(
path: 'ci/builders/linux_test_config.json',
),
},
flutterTool: flutterTool,
));
expect(
() => et.run(['run', '--config=android_debug_arm64']),
throwsA(
isA<FatalError>().having(
(a) => a.toString(),
'toString()',
contains('Could not find host build'),
),
),
);
});
test('fails if RBE was requested but no RBE config was found', () async {
final builders = TestBuilderConfig();
builders.addBuild(
name: 'linux/android_debug_arm64',
dimension: TestDroneDimension.linux,
targetDir: 'android_debug_arm64',
);
builders.addBuild(
name: 'linux/host_debug',
dimension: TestDroneDimension.linux,
targetDir: 'host_debug',
);
final et = _engineTool(RunCommand(
environment: testEnvironment,
configs: {
'linux_test_config': builders.buildConfig(
path: 'ci/builders/linux_test_config.json',
),
},
flutterTool: flutterTool,
));
expect(
() => et.run(['run', '--rbe', '--config=android_debug_arm64']),
throwsA(
isA<FatalError>().having(
(a) => a.toString(),
'toString()',
contains('RBE was requested but no RBE config was found'),
),
),
);
});
group('fails if -j is not a positive integer', () {
for (final arg in ['-1', 'foo']) {
test('fails if -j is $arg', () async {
final builders = TestBuilderConfig();
builders.addBuild(
name: 'linux/android_debug_arm64',
dimension: TestDroneDimension.linux,
targetDir: 'android_debug_arm64',
);
builders.addBuild(
name: 'linux/host_debug',
dimension: TestDroneDimension.linux,
targetDir: 'host_debug',
);
final et = _engineTool(RunCommand(
environment: testEnvironment,
configs: {
'linux_test_config': builders.buildConfig(
path: 'ci/builders/linux_test_config.json',
),
},
flutterTool: flutterTool,
));
expect(
() => et.run([
'run',
'--config=android_debug_arm64',
'--concurrency=$arg',
]),
throwsA(
isA<FatalError>().having(
(a) => a.toString(),
'toString()',
contains('concurrency (-j) must specify a positive integer'),
),
),
);
});
}
});
});
group('builds and executes `flutter run`', () {
late CommandRunner<int> et;
late io.Directory rbeDir;
var commandsRun = <List<String>>[];
var testLogs = <LogRecord>[];
var attachedDevices = <Device>[];
var interceptCommands = <(String, FakeProcess? Function(List<String>))>[];
setUp(() {
// Builder configuration doesn't change for these tests.
final builders = TestBuilderConfig();
builders.addBuild(
name: 'linux/android_debug_arm64',
dimension: TestDroneDimension.linux,
targetDir: 'android_debug_arm64',
enableRbe: true,
);
builders.addBuild(
name: 'linux/host_debug',
dimension: TestDroneDimension.linux,
targetDir: 'host_debug',
enableRbe: true,
);
// Be very permissive on process execution, and check usage below instead.
final permissiveProcessManager = FakeProcessManager(
canRun: (_, {workingDirectory}) => true,
onRun: (command) {
commandsRun.add(command);
return io.ProcessResult(81, 0, '', '');
},
onStart: (command) {
commandsRun.add(command);
for (final entry in interceptCommands) {
if (command.first.endsWith(entry.$1)) {
final result = entry.$2(command);
if (result != null) {
return result;
}
}
}
switch (command) {
case ['flutter', 'devices', '--machine', ..._]:
return FakeProcess(stdout: jsonEncode(attachedDevices));
default:
return FakeProcess();
}
},
);
// Create an RBE directory by default.
rbeDir = io.Directory(p.join(
testEngine.srcDir.path,
'flutter',
'build',
'rbe',
));
rbeDir.createSync(recursive: true);
// Set up the environment for the test.
final testEnvironment = Environment(
abi: Abi.linuxX64,
engine: testEngine,
logger: Logger.test((log) {
testLogs.add(log);
}),
platform: _fakePlatform(Platform.linux),
processRunner: ProcessRunner(
defaultWorkingDirectory: tempRoot,
processManager: permissiveProcessManager,
),
);
// Set up the Flutter tool for the test.
et = _engineTool(RunCommand(
environment: testEnvironment,
configs: {
'linux_test_config': builders.buildConfig(
path: 'ci/builders/linux_test_config.json',
),
},
));
// Reset logs.
commandsRun = [];
attachedDevices = [];
testLogs = [];
interceptCommands = [];
});
tearDown(() {
printOnFailure('Commands run:\n${commandsRun.map((c) => c.join('\n'))}');
printOnFailure('Logs:\n${testLogs.map((l) => l.message).join('\n')}');
});
test('build includes RBE flags when enabled implicitly', () async {
await et.run([
'run',
'--config=android_debug_arm64',
]);
expect(
commandsRun,
containsAllInOrder([
// ./tools/gn --rbe
containsAllInOrder([
endsWith('tools/gn'),
contains('--rbe'),
]),
// ./reclient/bootstrap
containsAllInOrder([
endsWith('reclient/bootstrap'),
]),
]),
);
});
test('build excludes RBE flags when disabled', () async {
await et.run([
'run',
'--config=android_debug_arm64',
'--no-rbe',
]);
expect(
commandsRun,
containsAllInOrder([
// ./tools/gn --no-rbe
containsAllInOrder([
endsWith('tools/gn'),
contains('--no-rbe'),
]),
// ./reclient/bootstrap
isNot(containsAllInOrder([
endsWith('reclient/bootstrap'),
])),
]),
);
});
test('picks a default concurrency for RBE builds', () async {
await et.run([
'run',
'--config=android_debug_arm64',
]);
expect(
commandsRun,
containsAllInOrder([
// ninja -C out/android_debug_arm64 -j 1000 (or whatever is picked)
containsAllInOrder([
endsWith('ninja/ninja'),
contains('-j'),
isA<String>().having(int.tryParse, 'concurrency', greaterThan(100)),
]),
]),
);
});
test('does not define a default concurrency for non-RBE builds', () async {
await et.run([
'run',
'--no-rbe',
'--config=android_debug_arm64',
]);
expect(
commandsRun,
containsAllInOrder([
containsAllInOrder([
endsWith('ninja/ninja'),
isNot(contains('-j')),
]),
]),
);
});
test('define a user-specified concurrency for non-RBE builds', () async {
await et.run([
'run',
'--concurrency=42',
'--no-rbe',
'--config=android_debug_arm64',
]);
expect(
commandsRun,
containsAllInOrder([
containsAllInOrder([
endsWith('ninja/ninja'),
contains('-j'),
'42',
]),
]),
);
});
test('handles host build failures', () async {
interceptCommands.add((
'ninja',
(command) {
print(command);
if (command.any((c) => c.contains('host_debug'))) {
return FakeProcess(exitCode: 1);
}
return null;
},
));
expect(
() => et.run([
'run', 'run',
'--local-engine', '--config=android_debug_arm64',
'android_debug_arm64', ]),
'--local-engine-host', throwsA(
'host_debug', isA<FatalError>().having(
'-d', (a) => a.toString(),
'emulator' 'toString()',
])); contains('Failed to build host'),
),
),
);
});
test('handles target build failures', () async {
interceptCommands.add((
'ninja',
(command) {
if (command.any((c) => c.contains('android_debug_arm64'))) {
return FakeProcess(exitCode: 1);
}
return null;
},
));
expect(
() => et.run([
'run',
'--config=android_debug_arm64',
]),
throwsA(
isA<FatalError>().having(
(a) => a.toString(),
'toString()',
contains('Failed to build target'),
),
),
);
});
test('builds only once if the target and host are the same', () async {
await et.run([
'run',
'--config=host_debug',
]);
expect(
commandsRun,
containsOnce(
containsAllInOrder([
endsWith('ninja'),
]),
),
);
});
test('builds both the target and host if they are different', () async {
await et.run([
'run',
'--config=android_debug_arm64',
]);
expect(
commandsRun,
containsAllInOrder([
containsAllInOrder([
endsWith('ninja'),
contains('host_debug'),
]),
containsAllInOrder([
endsWith('ninja'),
contains('android_debug_arm64'),
]),
]),
);
});
test('delegates to `flutter run` with --local-engine flags', () async {
await et.run([
'run',
'--config=android_debug_arm64',
]);
expect(
commandsRun,
containsAllInOrder([
containsAllInOrder([
endsWith('flutter'),
contains('run'),
'--local-engine-src-path',
testEngine.srcDir.path,
'--local-engine',
'android_debug_arm64',
'--local-engine-host',
'host_debug',
]),
]),
);
});
group('delegates to `flutter run` in mode', () {
for (final mode in const ['debug', 'profile', 'release']) {
test('$mode mode', () async {
await et.run([
'run',
'--config=android_debug_arm64',
'--',
'--$mode',
]);
expect(
commandsRun,
containsAllInOrder([
containsAllInOrder([
endsWith('flutter'),
contains('run'),
contains('--$mode'),
]),
]),
);
});
}
});
}); });
// FIXME: Add positive tests.
// ^^^ Both sets, calls flutter run as expected n stuff.
// ... and check debug/profile/release
}
CommandRunner<int> _engineTool(RunCommand runCommand) {
return CommandRunner<int>(
'et',
'Fake tool with a single instrumented command.',
)..addCommand(runCommand);
}
Platform _fakePlatform(
String os, {
int numberOfProcessors = 32,
String pathSeparator = '/',
}) {
return FakePlatform(
operatingSystem: os,
resolvedExecutable: io.Platform.resolvedExecutable,
numberOfProcessors: numberOfProcessors,
pathSeparator: pathSeparator,
);
}
final class _FakeFlutterTool implements FlutterTool {
List<Device> respondWithDevices = [];
@override
Future<List<Device>> devices() async {
return respondWithDevices;
}
} }

View File

@ -0,0 +1,197 @@
// Copyright 2013 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:engine_tool/src/commands/run_command.dart';
import 'package:engine_tool/src/flutter_tool_interop/device.dart';
import 'package:engine_tool/src/flutter_tool_interop/target_platform.dart';
import 'package:engine_tool/src/label.dart';
import 'package:test/test.dart';
import 'src/matchers.dart';
void main() {
group('detectAndSelect', () {
test('returns null on an empty list', () {
final target = RunTarget.detectAndSelect([]);
expect(target, isNull);
});
test('returns the only target', () {
final device = _device(TargetPlatform.androidArm64);
final target = RunTarget.detectAndSelect([device]);
expect(
target,
isA<RunTarget>().having((t) => t.device, 'device', device),
);
});
test('returns the first target if multiple are available', () {
final device1 = _device(TargetPlatform.androidArm64);
final device2 = _device(TargetPlatform.androidX64);
final target = RunTarget.detectAndSelect([device1, device2]);
expect(
target,
isA<RunTarget>().having((t) => t.device, 'device', device1),
);
});
test('returns the android target', () {
final device1 = _device(TargetPlatform.darwinArm64);
final device2 = _device(TargetPlatform.androidArm64);
final target = RunTarget.detectAndSelect(
[device1, device2],
idPrefix: 'android',
);
expect(
target,
isA<RunTarget>().having((t) => t.device, 'device', device2),
);
});
test('returns the first android target', () {
final device1 = _device(TargetPlatform.androidArm64);
final device2 = _device(TargetPlatform.androidX64);
final target = RunTarget.detectAndSelect(
[device1, device2],
idPrefix: 'android',
);
expect(
target,
isA<RunTarget>().having((t) => t.device, 'device', device1),
);
});
test('returns null if no android targets are available', () {
final device1 = _device(TargetPlatform.darwinArm64);
final device2 = _device(TargetPlatform.darwinX64);
final target = RunTarget.detectAndSelect(
[device1, device2],
idPrefix: 'android',
);
expect(target, isNull);
});
});
group('buildConfigFor', () {
final expectedDebugTargets = {
TargetPlatform.androidUnspecified: 'android_debug',
TargetPlatform.androidX86: 'android_debug_x86',
TargetPlatform.androidX64: 'android_debug_x64',
TargetPlatform.androidArm64: 'android_debug_arm64',
TargetPlatform.darwinUnspecified: 'host_debug',
TargetPlatform.darwinX64: 'host_debug',
TargetPlatform.darwinArm64: 'host_debug_arm64',
TargetPlatform.linuxX64: 'host_debug',
TargetPlatform.linuxArm64: 'host_debug_arm64',
TargetPlatform.windowsX64: 'host_debug',
TargetPlatform.windowsArm64: 'host_debug_arm64',
TargetPlatform.webJavascript: 'chrome_debug',
};
for (final platform in TargetPlatform.knownPlatforms) {
if (expectedDebugTargets.containsKey(platform)) {
test('${platform.identifier} => ${expectedDebugTargets[platform]}', () {
final device = _device(platform);
final target = RunTarget.fromDevice(device);
expect(
target.buildConfigFor('debug'),
expectedDebugTargets[platform],
);
});
} else {
test('${platform.identifier} => FatalError', () {
final device = _device(platform);
final target = RunTarget.fromDevice(device);
expect(() => target.buildConfigFor('debug'), throwsFatalError);
});
}
}
});
group('buildTargetsForShell', () {
final expectedShellTargets = {
TargetPlatform.androidUnspecified: [
Label.parseGn('//flutter/shell/platform/android:android_jar')
],
TargetPlatform.androidX86: [
Label.parseGn('//flutter/shell/platform/android:android_jar')
],
TargetPlatform.androidX64: [
Label.parseGn('//flutter/shell/platform/android:android_jar')
],
TargetPlatform.androidArm64: [
Label.parseGn('//flutter/shell/platform/android:android_jar')
],
TargetPlatform.iOSUnspecified: [
Label.parseGn('//flutter/shell/platform/darwin/ios:flutter_framework')
],
TargetPlatform.iOSX64: [
Label.parseGn('//flutter/shell/platform/darwin/ios:flutter_framework')
],
TargetPlatform.iOSArm64: [
Label.parseGn('//flutter/shell/platform/darwin/ios:flutter_framework')
],
TargetPlatform.darwinUnspecified: [
Label.parseGn('//flutter/shell/platform/darwin/macos:flutter_framework')
],
TargetPlatform.darwinX64: [
Label.parseGn('//flutter/shell/platform/darwin/macos:flutter_framework')
],
TargetPlatform.darwinArm64: [
Label.parseGn('//flutter/shell/platform/darwin/macos:flutter_framework')
],
TargetPlatform.linuxX64: [
Label.parseGn('//flutter/shell/platform/linux:flutter_linux_gtk')
],
TargetPlatform.linuxArm64: [
Label.parseGn('//flutter/shell/platform/linux:flutter_linux_gtk')
],
TargetPlatform.windowsX64: [
Label.parseGn('//flutter/shell/platform/windows')
],
TargetPlatform.windowsArm64: [
Label.parseGn('//flutter/shell/platform/windows')
],
TargetPlatform.webJavascript: [
Label.parseGn('//flutter/web_sdk:flutter_web_sdk_archive')
],
};
for (final platform in TargetPlatform.knownPlatforms) {
if (expectedShellTargets.containsKey(platform)) {
test('${platform.identifier} => ${expectedShellTargets[platform]}', () {
final device = _device(platform);
final target = RunTarget.fromDevice(device);
expect(
target.buildTargetsForShell,
expectedShellTargets[platform],
);
});
} else {
test('${platform.identifier} => FatalError', () {
final device = _device(platform);
final target = RunTarget.fromDevice(device);
expect(() => target.buildTargetsForShell, throwsFatalError);
});
}
}
});
}
Device _device(TargetPlatform platform) {
return Device(
name: 'Test Device <${platform.identifier}>',
id: platform.identifier,
targetPlatform: platform,
);
}

View File

@ -0,0 +1,92 @@
import 'package:engine_tool/src/logger.dart';
import 'package:logging/logging.dart';
import 'package:test/test.dart';
/// Matches a thrown [FatalError].
final throwsFatalError = throwsA(isA<FatalError>());
/// Returns a matcher that matches a [LogRecord] with a [message].
///
/// If [message] is a [String], it uses [equals] to match the message, otherwise
/// [message] must be a subtype of [Matcher].
///
/// Optionally, you can provide a [level] to match the log level, which defaults
/// to [anything], but can otherwise either be a [Level] or a subtype of
/// [Matcher].
Matcher logRecord(Object message, {Object level = anything}) {
final Matcher messageMatcher = switch (message) {
String() => equals(message),
Matcher() => message,
_ => throw ArgumentError.value(
message,
'message',
'must be a String or Matcher',
),
};
final Matcher levelMatcher = switch (level) {
Level() => equals(level),
Matcher() => level,
_ => throw ArgumentError.value(
level,
'level',
'must be a Level or Matcher',
),
};
return _LogRecordMatcher(levelMatcher, messageMatcher);
}
final class _LogRecordMatcher extends Matcher {
_LogRecordMatcher(this._levelMatcher, this._messageMatcher);
final Matcher _levelMatcher;
final Matcher _messageMatcher;
@override
bool matches(Object? item, Map<Object?, Object?> matchState) {
if (item is! LogRecord) {
return false;
}
if (!_levelMatcher.matches(item.level, matchState)) {
return false;
}
if (!_messageMatcher.matches(item.message, matchState)) {
return false;
}
return true;
}
@override
Description describe(Description description) {
return description
.add('LogRecord with level matching ')
.addDescriptionOf(_levelMatcher)
.add(' and message matching ')
.addDescriptionOf(_messageMatcher);
}
@override
Description describeMismatch(
Object? item,
Description mismatchDescription,
Map<Object?, Object?> matchState,
bool verbose,
) {
if (item is! LogRecord) {
return mismatchDescription.add('was not a LogRecord');
}
if (!_levelMatcher.matches(item.level, matchState)) {
return mismatchDescription
.add('level ')
.addDescriptionOf(item.level)
.add(' did not match ')
.addDescriptionOf(_levelMatcher);
}
if (!_messageMatcher.matches(item.message, matchState)) {
return mismatchDescription
.add('message ')
.addDescriptionOf(item.message)
.add(' did not match ')
.addDescriptionOf(_messageMatcher);
}
return mismatchDescription;
}
}