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 'package:engine_build_configs/engine_build_configs.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:process_runner/process_runner.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 '../run_utils.dart';
|
||||
import '../logger.dart';
|
||||
import 'command.dart';
|
||||
import 'flags.dart';
|
||||
|
||||
@ -21,6 +25,7 @@ final class RunCommand extends CommandBase {
|
||||
required Map<String, BuilderConfig> configs,
|
||||
super.help = false,
|
||||
super.usageLineLength,
|
||||
@visibleForTesting FlutterTool? flutterTool,
|
||||
}) {
|
||||
// When printing the help/usage for this command, only list all builds
|
||||
// when the --verbose flag is supplied.
|
||||
@ -41,8 +46,13 @@ final class RunCommand extends CommandBase {
|
||||
defaultsTo: environment.hasRbeConfigInTree(),
|
||||
help: 'RBE is enabled by default when available.',
|
||||
);
|
||||
|
||||
_flutterTool = flutterTool ?? FlutterTool.fromEnvironment(environment);
|
||||
}
|
||||
|
||||
/// Flutter tool.
|
||||
late final FlutterTool _flutterTool;
|
||||
|
||||
/// List of compatible 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
|
||||
''';
|
||||
|
||||
Build? _lookup(String configName) {
|
||||
Build? _findTargetBuild(String configName) {
|
||||
final String demangledName = demangleConfigName(environment, configName);
|
||||
return builds
|
||||
.where((Build build) => build.name == demangledName)
|
||||
@ -78,11 +88,11 @@ See `flutter run --help` for a listing
|
||||
final String ci =
|
||||
mangledName.startsWith('ci') ? mangledName.substring(0, 3) : '';
|
||||
if (mangledName.contains('_debug')) {
|
||||
return _lookup('${ci}host_debug');
|
||||
return _findTargetBuild('${ci}host_debug');
|
||||
} else if (mangledName.contains('_profile')) {
|
||||
return _lookup('${ci}host_profile');
|
||||
return _findTargetBuild('${ci}host_profile');
|
||||
} else if (mangledName.contains('_release')) {
|
||||
return _lookup('${ci}host_release');
|
||||
return _findTargetBuild('${ci}host_release');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -114,62 +124,59 @@ See `flutter run --help` for a listing
|
||||
return mode;
|
||||
}
|
||||
|
||||
late final Future<RunTarget?> _runTarget =
|
||||
detectAndSelectRunTarget(environment, _getDeviceId());
|
||||
late final Future<RunTarget?> _runTarget = (() async {
|
||||
final devices = await _flutterTool.devices();
|
||||
return RunTarget.detectAndSelect(devices, idPrefix: _getDeviceId());
|
||||
})();
|
||||
|
||||
Future<String?> _selectTargetConfig() async {
|
||||
final String configName = argResults![configFlag] as String;
|
||||
Future<String> _selectTargetConfig() async {
|
||||
final configName = argResults![configFlag] as String;
|
||||
if (configName.isNotEmpty) {
|
||||
return demangleConfigName(environment, configName);
|
||||
return configName;
|
||||
}
|
||||
final RunTarget? target = await _runTarget;
|
||||
final target = await _runTarget;
|
||||
if (target == null) {
|
||||
return demangleConfigName(environment, 'host_debug');
|
||||
return 'host_debug';
|
||||
}
|
||||
environment.logger.status(
|
||||
'Building to run on "${target.name}" running ${target.targetPlatform}');
|
||||
return target.buildConfigFor(_getMode());
|
||||
|
||||
final result = target.buildConfigFor(_getMode());
|
||||
environment.logger.status('Building to run on $result');
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> run() async {
|
||||
if (!environment.processRunner.processManager.canRun('flutter')) {
|
||||
environment.logger.error('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;
|
||||
throw FatalError('Cannot find the "flutter" command in your PATH');
|
||||
}
|
||||
|
||||
final bool useRbe = argResults![rbeFlag] as bool;
|
||||
if (useRbe && !environment.hasRbeConfigInTree()) {
|
||||
environment.logger.error('RBE was requested but no RBE config was found');
|
||||
return 1;
|
||||
final configName = await _selectTargetConfig();
|
||||
final targetBuild = _findTargetBuild(configName);
|
||||
if (targetBuild == null) {
|
||||
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',
|
||||
];
|
||||
final RunTarget? target = await _runTarget;
|
||||
final List<Label> buildTargetsForShell =
|
||||
target?.buildTargetsForShell() ?? <Label>[];
|
||||
final target = await _runTarget;
|
||||
final buildTargetsForShell = target?.buildTargetsForShell ?? [];
|
||||
|
||||
final String dashJ = argResults![concurrencyFlag] as String;
|
||||
final int? concurrency = int.tryParse(dashJ);
|
||||
final concurrency = int.tryParse(argResults![concurrencyFlag] as String);
|
||||
if (concurrency == null || concurrency < 0) {
|
||||
environment.logger.error('-j must specify a positive integer.');
|
||||
return 1;
|
||||
throw FatalError(
|
||||
'--$concurrencyFlag (-j) must specify a positive integer.',
|
||||
);
|
||||
}
|
||||
|
||||
// First build the host.
|
||||
@ -181,25 +188,26 @@ See `flutter run --help` for a listing
|
||||
enableRbe: useRbe,
|
||||
);
|
||||
if (r != 0) {
|
||||
return r;
|
||||
throw FatalError('Failed to build host (${hostBuild.name})');
|
||||
}
|
||||
|
||||
// Now build the target if it isn't the same.
|
||||
if (hostBuild.name != build.name) {
|
||||
if (hostBuild.name != targetBuild.name) {
|
||||
r = await runBuild(
|
||||
environment,
|
||||
build,
|
||||
targetBuild,
|
||||
concurrency: concurrency,
|
||||
extraGnArgs: extraGnArgs,
|
||||
enableRbe: useRbe,
|
||||
targets: buildTargetsForShell,
|
||||
);
|
||||
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 =
|
||||
mangleConfigName(environment, hostBuild.name);
|
||||
@ -227,3 +235,173 @@ See `flutter run --help` for a listing
|
||||
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:test/test.dart';
|
||||
|
||||
import 'utils.dart';
|
||||
import '../utils.dart';
|
||||
|
||||
void main() {
|
||||
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
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:convert' as convert;
|
||||
import 'dart:ffi' as ffi show Abi;
|
||||
import 'dart:convert';
|
||||
import 'dart:ffi' show Abi;
|
||||
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_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/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/run_utils.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:platform/platform.dart';
|
||||
import 'package:process_fakes/process_fakes.dart';
|
||||
import 'package:process_runner/process_runner.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'fixtures.dart' as fixtures;
|
||||
import 'src/test_build_configs.dart';
|
||||
|
||||
void main() {
|
||||
final Engine engine;
|
||||
try {
|
||||
engine = Engine.findWithin();
|
||||
} catch (e) {
|
||||
io.stderr.writeln(e);
|
||||
io.exitCode = 1;
|
||||
return;
|
||||
late io.Directory tempRoot;
|
||||
late TestEngine testEngine;
|
||||
|
||||
setUp(() {
|
||||
tempRoot = io.Directory.systemTemp.createTempSync('engine_tool_test');
|
||||
testEngine = TestEngine.createTemp(rootDir: tempRoot);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
tempRoot.deleteSync(recursive: true);
|
||||
});
|
||||
|
||||
test('fails if flutter is not on your PATH', () async {
|
||||
final failsCanRun = FakeProcessManager(
|
||||
canRun: (executable, {workingDirectory}) {
|
||||
if (executable == 'flutter') {
|
||||
return false;
|
||||
}
|
||||
|
||||
final BuilderConfig linuxTestConfig = BuilderConfig.fromJson(
|
||||
path: 'ci/builders/linux_test_config.json',
|
||||
map: convert.jsonDecode(fixtures.testConfig('Linux', Platform.linux))
|
||||
as Map<String, Object?>,
|
||||
fail('Unexpected');
|
||||
},
|
||||
);
|
||||
|
||||
final BuilderConfig macTestConfig = BuilderConfig.fromJson(
|
||||
path: 'ci/builders/mac_test_config.json',
|
||||
map: convert.jsonDecode(fixtures.testConfig('Mac-12', Platform.macOS))
|
||||
as Map<String, Object?>,
|
||||
);
|
||||
|
||||
final BuilderConfig winTestConfig = BuilderConfig.fromJson(
|
||||
path: 'ci/builders/win_test_config.json',
|
||||
map: convert.jsonDecode(fixtures.testConfig('Windows-11', Platform.windows))
|
||||
as Map<String, Object?>,
|
||||
);
|
||||
|
||||
final Map<String, BuilderConfig> configs = <String, BuilderConfig>{
|
||||
'linux_test_config': linuxTestConfig,
|
||||
'mac_test_config': macTestConfig,
|
||||
'win_test_config': winTestConfig,
|
||||
};
|
||||
|
||||
(Environment, List<List<String>>) linuxEnv(Logger logger) {
|
||||
final List<List<String>> runHistory = <List<String>>[];
|
||||
return (
|
||||
Environment(
|
||||
abi: ffi.Abi.linuxX64,
|
||||
engine: engine,
|
||||
platform: FakePlatform(
|
||||
operatingSystem: Platform.linux,
|
||||
resolvedExecutable: io.Platform.resolvedExecutable,
|
||||
pathSeparator: '/',
|
||||
numberOfProcessors: 32,
|
||||
),
|
||||
final testEnvironment = Environment(
|
||||
abi: Abi.macosArm64,
|
||||
engine: testEngine,
|
||||
logger: Logger.test((_) {}),
|
||||
platform: _fakePlatform(Platform.linux),
|
||||
processRunner: ProcessRunner(
|
||||
processManager: FakeProcessManager(onStart: (List<String> command) {
|
||||
runHistory.add(command);
|
||||
defaultWorkingDirectory: tempRoot,
|
||||
processManager: failsCanRun,
|
||||
),
|
||||
);
|
||||
|
||||
final et = _engineTool(
|
||||
RunCommand(
|
||||
environment: testEnvironment,
|
||||
// Intentionally left blank, none of these builds make it far enough.
|
||||
configs: {},
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
() => et.run(['run']),
|
||||
throwsA(
|
||||
isA<FatalError>().having(
|
||||
(a) => a.toString(),
|
||||
'toString()',
|
||||
contains('"flutter" command in your PATH'),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
group('configuration failures', () {
|
||||
final unusedProcessManager = FakeProcessManager(
|
||||
canRun: (_, {workingDirectory}) => true,
|
||||
);
|
||||
|
||||
late List<LogRecord> testLogs;
|
||||
late Environment testEnvironment;
|
||||
late _FakeFlutterTool flutterTool;
|
||||
|
||||
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: fixtures.attachedDevices());
|
||||
case ['flutter', 'devices', '--machine', ..._]:
|
||||
return FakeProcess(stdout: jsonEncode(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 Logger logger = Logger.test((_) {});
|
||||
final (Environment env, List<List<String>> runHistory) = linuxEnv(logger);
|
||||
final ToolCommandRunner runner = ToolCommandRunner(
|
||||
environment: env,
|
||||
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(
|
||||
defaultTarget.buildConfigFor('debug'), equals('android_debug_arm64'));
|
||||
});
|
||||
|
||||
test('device select', () async {
|
||||
final Logger logger = Logger.test((_) {});
|
||||
final (Environment env, _) = linuxEnv(logger);
|
||||
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 =
|
||||
await runner.run(<String>['run', '--', '-d', 'emulator']);
|
||||
expect(result, equals(0));
|
||||
expect(runHistory.length, greaterThanOrEqualTo(6));
|
||||
// Observe that we selected android_debug_arm64 as the target.
|
||||
expect(
|
||||
runHistory[5],
|
||||
containsAllInOrder(<String>[
|
||||
// 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',
|
||||
'--config=android_debug_arm64',
|
||||
]),
|
||||
throwsA(
|
||||
isA<FatalError>().having(
|
||||
(a) => a.toString(),
|
||||
'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',
|
||||
'-d',
|
||||
'emulator'
|
||||
]));
|
||||
]),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
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