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:
parent
3ad82894c5
commit
6c9af1435d
@ -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.',
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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)}';
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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>';
|
||||||
|
}
|
@ -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);
|
|
||||||
}
|
|
@ -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,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
@ -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 {
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
197
engine/src/flutter/tools/engine_tool/test/run_target_test.dart
Normal file
197
engine/src/flutter/tools/engine_tool/test/run_target_test.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
92
engine/src/flutter/tools/engine_tool/test/src/matchers.dart
Normal file
92
engine/src/flutter/tools/engine_tool/test/src/matchers.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user