Unverified Commit 102ab1e6 authored by Christopher Fujino's avatar Christopher Fujino Committed by GitHub

Reland bundle ios deps (#36093)

This updates the flutter tool cache to download binary files for ideviceinstaller, ios-deploy, libimobiledevice, and dynamically linked dependencies from Flutter's GCP bucket.
parent e3ee5c6b
......@@ -21,7 +21,7 @@ task:
fingerprint_script: echo $OS; cat bin/internal/engine.version
artifacts_cache:
folder: bin/cache/artifacts
fingerprint_script: echo $OS; cat bin/internal/engine.version
fingerprint_script: echo $OS; cat bin/internal/*.version
setup_script: ./dev/bots/cirrus_setup.sh
matrix:
- name: docs
......@@ -298,7 +298,7 @@ task:
fingerprint_script: echo %OS% & type bin\internal\engine.version
artifacts_cache:
folder: bin\cache\artifacts
fingerprint_script: echo %OS% & type bin\internal\engine.version
fingerprint_script: echo %OS% & type bin\internal\*.version
setup_script:
- flutter config --no-analytics
- flutter doctor -v
......
ab9352110092cf651b5602301371cd00691c7e13
ea5583388ac0ca035f6b991fd7955bea6492c68c
398c1208731cb887c64a31c2ae111048b079f80d
17546f53ac1377b0d4f45a800aaec7366ba5b6a0
03da376ff7504c63a1d00d57cf41bd7b7e93ff65
60109fdef47dfe0badfb558a6a2105e8fb23660a
......@@ -392,9 +392,34 @@ class IosDeviceDiscovery implements DeviceDiscovery {
_workingDevice = allDevices[math.Random().nextInt(allDevices.length)];
}
// Returns the path to cached binaries relative to devicelab directory
String get _artifactDirPath {
return path.normalize(
path.join(
path.current,
'../../bin/cache/artifacts',
)
);
}
// Returns a colon-separated environment variable that contains the paths
// of linked libraries for idevice_id
Map<String, String> get _ideviceIdEnvironment {
final String libPath = const <String>[
'libimobiledevice',
'usbmuxd',
'libplist',
'openssl',
'ideviceinstaller',
'ios-deploy',
].map((String packageName) => path.join(_artifactDirPath, packageName)).join(':');
return <String, String>{'DYLD_LIBRARY_PATH': libPath};
}
@override
Future<List<String>> discoverDevices() async {
final List<String> iosDeviceIDs = LineSplitter.split(await eval('idevice_id', <String>['-l']))
final String ideviceIdPath = path.join(_artifactDirPath, 'libimobiledevice', 'idevice_id');
final List<String> iosDeviceIDs = LineSplitter.split(await eval(ideviceIdPath, <String>['-l'], environment: _ideviceIdEnvironment))
.map<String>((String line) => line.trim())
.where((String line) => line.isNotEmpty)
.toList();
......
......@@ -40,6 +40,14 @@ enum Artifact {
kernelWorkerSnapshot,
/// The root of the web implementation of the dart SDK.
flutterWebSdk,
iosDeploy,
ideviceinfo,
ideviceId,
idevicename,
idevicesyslog,
idevicescreenshot,
ideviceinstaller,
iproxy,
/// The root of the Linux desktop sources.
linuxDesktopPath,
/// The root of the Windows desktop sources.
......@@ -93,6 +101,22 @@ String _artifactToFileName(Artifact artifact, [ TargetPlatform platform, BuildMo
return 'dartdevc.dart.snapshot';
case Artifact.kernelWorkerSnapshot:
return 'kernel_worker.dart.snapshot';
case Artifact.iosDeploy:
return 'ios-deploy';
case Artifact.ideviceinfo:
return 'ideviceinfo';
case Artifact.ideviceId:
return 'idevice_id';
case Artifact.idevicename:
return 'idevicename';
case Artifact.idevicesyslog:
return 'idevicesyslog';
case Artifact.idevicescreenshot:
return 'idevicescreenshot';
case Artifact.ideviceinstaller:
return 'ideviceinstaller';
case Artifact.iproxy:
return 'iproxy';
case Artifact.linuxDesktopPath:
if (platform != TargetPlatform.linux_x64) {
throw Exception('${getNameForTargetPlatform(platform)} does not support'
......@@ -187,13 +211,26 @@ class CachedArtifacts extends Artifacts {
}
String _getIosArtifactPath(Artifact artifact, TargetPlatform platform, BuildMode mode) {
final String engineDir = _getEngineArtifactsPath(platform, mode);
final String artifactFileName = _artifactToFileName(artifact);
switch (artifact) {
case Artifact.genSnapshot:
case Artifact.snapshotDart:
case Artifact.flutterFramework:
case Artifact.frontendServerSnapshotForEngineDartSdk:
return fs.path.join(engineDir, _artifactToFileName(artifact));
final String engineDir = _getEngineArtifactsPath(platform, mode);
return fs.path.join(engineDir, artifactFileName);
case Artifact.ideviceId:
case Artifact.ideviceinfo:
case Artifact.idevicescreenshot:
case Artifact.idevicesyslog:
case Artifact.idevicename:
return cache.getArtifactDirectory('libimobiledevice').childFile(artifactFileName).path;
case Artifact.iosDeploy:
return cache.getArtifactDirectory('ios-deploy').childFile(artifactFileName).path;
case Artifact.ideviceinstaller:
return cache.getArtifactDirectory('ideviceinstaller').childFile(artifactFileName).path;
case Artifact.iproxy:
return cache.getArtifactDirectory('usbmuxd').childFile(artifactFileName).path;
default:
assert(false, 'Artifact $artifact not available for platform $platform.');
return null;
......@@ -302,24 +339,25 @@ class LocalEngineArtifacts extends Artifacts {
@override
String getArtifactPath(Artifact artifact, { TargetPlatform platform, BuildMode mode }) {
final String artifactFileName = _artifactToFileName(artifact);
switch (artifact) {
case Artifact.snapshotDart:
return fs.path.join(_engineSrcPath, 'flutter', 'lib', 'snapshot', _artifactToFileName(artifact));
return fs.path.join(_engineSrcPath, 'flutter', 'lib', 'snapshot', artifactFileName);
case Artifact.genSnapshot:
return _genSnapshotPath();
case Artifact.flutterTester:
return _flutterTesterPath(platform);
case Artifact.isolateSnapshotData:
case Artifact.vmSnapshotData:
return fs.path.join(engineOutPath, 'gen', 'flutter', 'lib', 'snapshot', _artifactToFileName(artifact));
return fs.path.join(engineOutPath, 'gen', 'flutter', 'lib', 'snapshot', artifactFileName);
case Artifact.platformKernelDill:
return fs.path.join(_getFlutterPatchedSdkPath(mode), _artifactToFileName(artifact));
return fs.path.join(_getFlutterPatchedSdkPath(mode), artifactFileName);
case Artifact.platformLibrariesJson:
return fs.path.join(_getFlutterPatchedSdkPath(mode), 'lib', _artifactToFileName(artifact));
return fs.path.join(_getFlutterPatchedSdkPath(mode), 'lib', artifactFileName);
case Artifact.flutterFramework:
return fs.path.join(engineOutPath, _artifactToFileName(artifact));
return fs.path.join(engineOutPath, artifactFileName);
case Artifact.flutterMacOSFramework:
return fs.path.join(engineOutPath, _artifactToFileName(artifact));
return fs.path.join(engineOutPath, artifactFileName);
case Artifact.flutterPatchedSdkPath:
// When using local engine always use [BuildMode.debug] regardless of
// what was specified in [mode] argument because local engine will
......@@ -329,23 +367,35 @@ class LocalEngineArtifacts extends Artifacts {
case Artifact.flutterWebSdk:
return _getFlutterWebSdkPath();
case Artifact.frontendServerSnapshotForEngineDartSdk:
return fs.path.join(_hostEngineOutPath, 'gen', _artifactToFileName(artifact));
return fs.path.join(_hostEngineOutPath, 'gen', artifactFileName);
case Artifact.engineDartSdkPath:
return fs.path.join(_hostEngineOutPath, 'dart-sdk');
case Artifact.engineDartBinary:
return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', _artifactToFileName(artifact));
return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', artifactFileName);
case Artifact.dart2jsSnapshot:
return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', 'snapshots', _artifactToFileName(artifact));
return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', 'snapshots', artifactFileName);
case Artifact.dartdevcSnapshot:
return fs.path.join(dartSdkPath, 'bin', 'snapshots', _artifactToFileName(artifact));
return fs.path.join(dartSdkPath, 'bin', 'snapshots', artifactFileName);
case Artifact.kernelWorkerSnapshot:
return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', 'snapshots', _artifactToFileName(artifact));
return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', 'snapshots', artifactFileName);
case Artifact.ideviceId:
case Artifact.ideviceinfo:
case Artifact.idevicename:
case Artifact.idevicescreenshot:
case Artifact.idevicesyslog:
return cache.getArtifactDirectory('libimobiledevice').childFile(artifactFileName).path;
case Artifact.ideviceinstaller:
return cache.getArtifactDirectory('ideviceinstaller').childFile(artifactFileName).path;
case Artifact.iosDeploy:
return cache.getArtifactDirectory('ios-deploy').childFile(artifactFileName).path;
case Artifact.iproxy:
return cache.getArtifactDirectory('usbmuxd').childFile(artifactFileName).path;
case Artifact.linuxDesktopPath:
return fs.path.join(_hostEngineOutPath, _artifactToFileName(artifact));
return fs.path.join(_hostEngineOutPath, artifactFileName);
case Artifact.windowsDesktopPath:
return fs.path.join(_hostEngineOutPath, _artifactToFileName(artifact));
return fs.path.join(_hostEngineOutPath, artifactFileName);
case Artifact.skyEnginePath:
return fs.path.join(_hostEngineOutPath, 'gen', 'dart-pkg', _artifactToFileName(artifact));
return fs.path.join(_hostEngineOutPath, 'gen', 'dart-pkg', artifactFileName);
}
assert(false, 'Invalid artifact $artifact.');
return null;
......
......@@ -263,20 +263,26 @@ Future<RunResult> runCheckedAsync(
return result;
}
bool exitsHappy(List<String> cli) {
bool exitsHappy(
List<String> cli, {
Map<String, String> environment,
}) {
_traceCommand(cli);
try {
return processManager.runSync(cli).exitCode == 0;
return processManager.runSync(cli, environment: environment).exitCode == 0;
} catch (error) {
printTrace('$cli failed with $error');
return false;
}
}
Future<bool> exitsHappyAsync(List<String> cli) async {
Future<bool> exitsHappyAsync(
List<String> cli, {
Map<String, String> environment,
}) async {
_traceCommand(cli);
try {
return (await processManager.run(cli)).exitCode == 0;
return (await processManager.run(cli, environment: environment)).exitCode == 0;
} catch (error) {
printTrace('$cli failed with $error');
return false;
......
......@@ -145,44 +145,6 @@ class UserMessages {
'Once installed, run:\n'
' sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer';
// Messages used in IOSValidator
String get iOSIMobileDeviceMissing =>
'libimobiledevice and ideviceinstaller are not installed. To install with Brew, run:\n'
' brew update\n'
' brew install --HEAD usbmuxd\n'
' brew link usbmuxd\n'
' brew install --HEAD libimobiledevice\n'
' brew install ideviceinstaller';
String get iOSIMobileDeviceBroken =>
'Verify that all connected devices have been paired with this computer in Xcode.\n'
'If all devices have been paired, libimobiledevice and ideviceinstaller may require updating.\n'
'To update with Brew, run:\n'
' brew update\n'
' brew uninstall --ignore-dependencies libimobiledevice\n'
' brew uninstall --ignore-dependencies usbmuxd\n'
' brew install --HEAD usbmuxd\n'
' brew unlink usbmuxd\n'
' brew link usbmuxd\n'
' brew install --HEAD libimobiledevice\n'
' brew install ideviceinstaller';
String get iOSDeviceInstallerMissing =>
'ideviceinstaller is not installed; this is used to discover connected iOS devices.\n'
'To install with Brew, run:\n'
' brew install --HEAD usbmuxd\n'
' brew link usbmuxd\n'
' brew install --HEAD libimobiledevice\n'
' brew install ideviceinstaller';
String iOSDeployVersion(String version) => 'ios-deploy $version';
String iOSDeployOutdated(String minVersion) =>
'ios-deploy out of date ($minVersion is required). To upgrade with Brew:\n'
' brew upgrade ios-deploy';
String get iOSDeployMissing =>
'ios-deploy not installed. To install:\n'
' brew install ios-deploy';
String get iOSBrewMissing =>
'Brew can be used to install tools for iOS device development.\n'
'Download brew at https://brew.sh/.';
// Messages used in CocoaPodsValidator
String cocoaPodsVersion(String version) => 'CocoaPods version $version';
String cocoaPodsUninitialized(String consequence) =>
......@@ -206,9 +168,6 @@ class UserMessages {
'$consequence\n'
'To upgrade:\n'
'$upgradeInstructions';
String get cocoaPodsBrewMissing =>
'Brew can be used to install CocoaPods.\n'
'Download brew at https://brew.sh/.';
// Messages used in VsCodeValidator
String vsCodeVersion(String version) => 'version $version';
......
......@@ -53,7 +53,7 @@ class DevelopmentArtifact {
/// Artifacts required by all developments.
static const DevelopmentArtifact universal = DevelopmentArtifact._('universal');
/// The vaulues of DevelopmentArtifacts.
/// The values of DevelopmentArtifacts.
static final List<DevelopmentArtifact> values = <DevelopmentArtifact>[
android,
iOS,
......@@ -83,6 +83,9 @@ class Cache {
_artifacts.add(LinuxEngineArtifacts(this));
_artifacts.add(LinuxFuchsiaSDKArtifacts(this));
_artifacts.add(MacOSFuchsiaSDKArtifacts(this));
for (String artifactName in IosUsbArtifacts.artifactNames) {
_artifacts.add(IosUsbArtifacts(artifactName, this));
}
} else {
_artifacts.addAll(artifacts);
}
......@@ -221,6 +224,22 @@ class Cache {
return getCacheArtifacts().childDirectory(name);
}
MapEntry<String, String> get dyLdLibEntry {
if (_dyLdLibEntry != null) {
return _dyLdLibEntry;
}
final List<String> paths = <String>[];
for (CachedArtifact artifact in _artifacts) {
final String currentPath = artifact.dyLdLibPath;
if (currentPath.isNotEmpty) {
paths.add(currentPath);
}
}
_dyLdLibEntry = MapEntry<String, String>('DYLD_LIBRARY_PATH', paths.join(':'));
return _dyLdLibEntry;
}
MapEntry<String, String> _dyLdLibEntry;
/// The web sdk has to be co-located with the dart-sdk so that they can share source
/// code.
Directory getWebSdkDirectory() {
......@@ -328,6 +347,9 @@ abstract class CachedArtifact {
// artifact name.
String get stampName => name;
/// Returns a string to be set as environment DYLD_LIBARY_PATH variable
String get dyLdLibPath => '';
/// All development artifacts this cache provides.
final Set<DevelopmentArtifact> developmentArtifacts;
......@@ -890,6 +912,39 @@ class MacOSFuchsiaSDKArtifacts extends _FuchsiaSDKArtifacts {
}
}
/// Cached iOS/USB binary artifacts.
class IosUsbArtifacts extends CachedArtifact {
IosUsbArtifacts(String name, Cache cache) : super(
name,
cache,
// This is universal to ensure every command checks for them first
const <DevelopmentArtifact>{ DevelopmentArtifact.universal },
);
static const List<String> artifactNames = <String>[
'libimobiledevice',
'usbmuxd',
'libplist',
'openssl',
'ideviceinstaller',
'ios-deploy',
];
@override
String get dyLdLibPath {
return cache.getArtifactDirectory(name).path;
}
@override
Future<void> updateInner() {
if (!platform.isMacOS) {
return Future<void>.value();
}
final Uri archiveUri = Uri.parse('$_storageBaseUrl/flutter_infra/ios-usb-dependencies/$name/$version/$name.zip');
return _downloadZipArchive('Downloading $name...', archiveUri, location);
}
}
// Many characters are problematic in filenames, especially on Windows.
final Map<int, List<int>> _flattenNameSubstitutions = <int, List<int>>{
r'@'.codeUnitAt(0): '@@'.codeUnits,
......
......@@ -87,9 +87,8 @@ Future<T> runInContext<T>(
FuchsiaWorkflow: () => FuchsiaWorkflow(),
GenSnapshot: () => const GenSnapshot(),
HotRunnerConfig: () => HotRunnerConfig(),
IMobileDevice: () => const IMobileDevice(),
IMobileDevice: () => IMobileDevice(),
IOSSimulatorUtils: () => IOSSimulatorUtils(),
IOSValidator: () => const IOSValidator(),
IOSWorkflow: () => const IOSWorkflow(),
KernelCompilerFactory: () => const KernelCompilerFactory(),
LinuxWorkflow: () => const LinuxWorkflow(),
......
......@@ -72,8 +72,6 @@ class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider {
GroupedValidator(<DoctorValidator>[androidValidator, androidLicenseValidator]),
if (iosWorkflow.appliesToHostPlatform || macOSWorkflow.appliesToHostPlatform)
GroupedValidator(<DoctorValidator>[xcodeValidator, cocoapodsValidator]),
if (iosWorkflow.appliesToHostPlatform)
iosValidator,
if (webWorkflow.appliesToHostPlatform)
const WebValidator(),
// Add desktop doctors to workflow if the flag is enabled.
......
......@@ -7,6 +7,7 @@ import 'dart:async';
import 'package:meta/meta.dart';
import '../application_package.dart';
import '../artifacts.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
......@@ -23,10 +24,6 @@ import 'code_signing.dart';
import 'ios_workflow.dart';
import 'mac.dart';
const String _kIdeviceinstallerInstructions =
'To work with iOS devices, please install ideviceinstaller. To install, run:\n'
'brew install ideviceinstaller.';
class IOSDeploy {
const IOSDeploy();
......@@ -37,9 +34,15 @@ class IOSDeploy {
@required String bundlePath,
@required List<String> launchArguments,
}) async {
final List<String> launchCommand = <String>[
final String iosDeployPath = artifacts.getArtifactPath(Artifact.iosDeploy, platform: TargetPlatform.ios);
// TODO(fujino): remove fallback once g3 updated
const List<String> fallbackIosDeployPath = <String>[
'/usr/bin/env',
'ios-deploy',
];
final List<String> commandList = iosDeployPath != null ? <String>[iosDeployPath] : fallbackIosDeployPath;
final List<String> launchCommand = <String>[
...commandList,
'--id',
deviceId,
'--bundle',
......@@ -61,6 +64,7 @@ class IOSDeploy {
// it.
final Map<String, String> iosDeployEnv = Map<String, String>.from(platform.environment);
iosDeployEnv['PATH'] = '/usr/bin:${iosDeployEnv['PATH']}';
iosDeployEnv.addEntries(<MapEntry<String, String>>[cache.dyLdLibEntry]);
return await runCommandAndStreamOutput(
launchCommand,
......@@ -120,8 +124,20 @@ class IOSDevice extends Device {
platformType: PlatformType.ios,
ephemeral: true,
) {
_installerPath = _checkForCommand('ideviceinstaller');
_iproxyPath = _checkForCommand('iproxy');
if (!platform.isMacOS) {
printError('Cannot control iOS devices or simulators. ideviceinstaller and iproxy are not available on your platform.');
_installerPath = null;
_iproxyPath = null;
return;
}
_installerPath = artifacts.getArtifactPath(
Artifact.ideviceinstaller,
platform: TargetPlatform.ios
) ?? 'ideviceinstaller'; // TODO(fujino): remove fallback once g3 updated
_iproxyPath = artifacts.getArtifactPath(
Artifact.iproxy,
platform: TargetPlatform.ios
) ?? 'iproxy'; // TODO(fujino): remove fallback once g3 updated
}
String _installerPath;
......@@ -173,23 +189,6 @@ class IOSDevice extends Device {
return devices;
}
static String _checkForCommand(
String command, [
String macInstructions = _kIdeviceinstallerInstructions,
]) {
try {
command = runCheckedSync(<String>['which', command]).trim();
} catch (e) {
if (platform.isMacOS) {
printError('$command not found. $macInstructions');
} else {
printError('Cannot control iOS devices or simulators. $command is not available on your platform.');
}
return null;
}
return command;
}
@override
Future<bool> isAppInstalled(ApplicationPackage app) async {
try {
......@@ -589,12 +588,17 @@ class _IOSDevicePortForwarder extends DevicePortForwarder {
while (!connected) {
printTrace('attempting to forward device port $devicePort to host port $hostPort');
// Usage: iproxy LOCAL_TCP_PORT DEVICE_TCP_PORT UDID
process = await runCommand(<String>[
device._iproxyPath,
hostPort.toString(),
devicePort.toString(),
device.id,
]);
process = await runCommand(
<String>[
device._iproxyPath,
hostPort.toString(),
devicePort.toString(),
device.id,
],
environment: Map<String, String>.fromEntries(
<MapEntry<String, String>>[cache.dyLdLibEntry],
),
);
// TODO(ianh): This is a flakey race condition, https://github.com/libimobiledevice/libimobiledevice/issues/674
connected = !await process.stdout.isEmpty.timeout(_kiProxyPortForwardTimeout, onTimeout: () => false);
if (!connected) {
......
......@@ -2,21 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import '../base/context.dart';
import '../base/os.dart';
import '../base/platform.dart';
import '../base/process.dart';
import '../base/user_messages.dart';
import '../base/version.dart';
import '../doctor.dart';
import '../macos/xcode.dart';
import 'mac.dart';
import 'plist_utils.dart' as plist;
IOSWorkflow get iosWorkflow => context.get<IOSWorkflow>();
IOSValidator get iosValidator => context.get<IOSValidator>();
class IOSWorkflow implements Workflow {
const IOSWorkflow();
......@@ -40,86 +32,3 @@ class IOSWorkflow implements Workflow {
return plist.getValueFromFile(path, key);
}
}
class IOSValidator extends DoctorValidator {
const IOSValidator() : super('iOS tools - develop for iOS devices');
Future<bool> get hasIDeviceInstaller => exitsHappyAsync(<String>['ideviceinstaller', '-h']);
Future<bool> get hasIosDeploy => exitsHappyAsync(<String>['ios-deploy', '--version']);
String get iosDeployMinimumVersion => '1.9.4';
// ios-deploy <= v1.9.3 declares itself as v2.0.0
List<String> get iosDeployBadVersions => <String>['2.0.0'];
Future<String> get iosDeployVersionText async => (await runAsync(<String>['ios-deploy', '--version'])).processResult.stdout.replaceAll('\n', '');
bool get hasHomebrew => os.which('brew') != null;
Future<String> get macDevMode async => (await runAsync(<String>['DevToolsSecurity', '-status'])).processResult.stdout;
Future<bool> get _iosDeployIsInstalledAndMeetsVersionCheck async {
if (!await hasIosDeploy)
return false;
try {
final Version version = Version.parse(await iosDeployVersionText);
return version >= Version.parse(iosDeployMinimumVersion)
&& !iosDeployBadVersions.map((String v) => Version.parse(v)).contains(version);
} on FormatException catch (_) {
return false;
}
}
// Change this value if the number of checks for packages needed for installation changes
static const int totalChecks = 4;
@override
Future<ValidationResult> validate() async {
final List<ValidationMessage> messages = <ValidationMessage>[];
ValidationType packageManagerStatus = ValidationType.installed;
int checksFailed = 0;
if (!iMobileDevice.isInstalled) {
checksFailed += 3;
packageManagerStatus = ValidationType.partial;
messages.add(ValidationMessage.error(userMessages.iOSIMobileDeviceMissing));
} else if (!await iMobileDevice.isWorking) {
checksFailed += 2;
packageManagerStatus = ValidationType.partial;
messages.add(ValidationMessage.error(userMessages.iOSIMobileDeviceBroken));
} else if (!await hasIDeviceInstaller) {
checksFailed += 1;
packageManagerStatus = ValidationType.partial;
messages.add(ValidationMessage.error(userMessages.iOSDeviceInstallerMissing));
}
final bool iHasIosDeploy = await hasIosDeploy;
// Check ios-deploy is installed at meets version requirements.
if (iHasIosDeploy) {
messages.add(ValidationMessage(userMessages.iOSDeployVersion(await iosDeployVersionText)));
}
if (!await _iosDeployIsInstalledAndMeetsVersionCheck) {
packageManagerStatus = ValidationType.partial;
if (iHasIosDeploy) {
messages.add(ValidationMessage.error(userMessages.iOSDeployOutdated(iosDeployMinimumVersion)));
} else {
checksFailed += 1;
messages.add(ValidationMessage.error(userMessages.iOSDeployMissing));
}
}
// If one of the checks for the packages failed, we may need brew so that we can install
// the necessary packages. If they're all there, however, we don't even need it.
if (checksFailed == totalChecks)
packageManagerStatus = ValidationType.missing;
if (checksFailed > 0 && !hasHomebrew) {
messages.add(ValidationMessage.hint(userMessages.iOSBrewMissing));
}
return ValidationResult(packageManagerStatus, messages);
}
}
......@@ -7,6 +7,7 @@ import 'dart:async';
import 'package:meta/meta.dart';
import '../application_package.dart';
import '../artifacts.dart';
import '../base/common.dart';
import '../base/context.dart';
import '../base/file_system.dart';
......@@ -42,35 +43,99 @@ class IOSDeviceNotFoundError implements Exception {
}
class IMobileDevice {
const IMobileDevice();
bool get isInstalled => exitsHappy(<String>['idevice_id', '-h']);
IMobileDevice()
: _ideviceIdPath = artifacts.getArtifactPath(Artifact.ideviceId, platform: TargetPlatform.ios)
?? 'idevice_id', // TODO(fujino): remove fallback once g3 updated
_ideviceinfoPath = artifacts.getArtifactPath(Artifact.ideviceinfo, platform: TargetPlatform.ios)
?? 'ideviceinfo', // TODO(fujino): remove fallback once g3 updated
_idevicenamePath = artifacts.getArtifactPath(Artifact.idevicename, platform: TargetPlatform.ios)
?? 'idevicename', // TODO(fujino): remove fallback once g3 updated
_idevicesyslogPath = artifacts.getArtifactPath(Artifact.idevicesyslog, platform: TargetPlatform.ios)
?? 'idevicesyslog', // TODO(fujino): remove fallback once g3 updated
_idevicescreenshotPath = artifacts.getArtifactPath(Artifact.idevicescreenshot, platform: TargetPlatform.ios)
?? 'idevicescreenshot' { // TODO(fujino): remove fallback once g3 updated
}
final String _ideviceIdPath;
final String _ideviceinfoPath;
final String _idevicenamePath;
final String _idevicesyslogPath;
final String _idevicescreenshotPath;
bool get isInstalled {
_isInstalled ??= exitsHappy(
<String>[
_ideviceIdPath,
'-h'
],
environment: Map<String, String>.fromEntries(
<MapEntry<String, String>>[cache.dyLdLibEntry]
),
);
return _isInstalled;
}
bool _isInstalled;
/// Returns true if libimobiledevice is installed and working as expected.
///
/// Older releases of libimobiledevice fail to work with iOS 10.3 and above.
Future<bool> get isWorking async {
if (!isInstalled)
return false;
if (_isWorking != null) {
return _isWorking;
}
if (!isInstalled) {
_isWorking = false;
return _isWorking;
}
// If usage info is printed in a hyphenated id, we need to update.
const String fakeIphoneId = '00008020-001C2D903C42002E';
final ProcessResult ideviceResult = (await runAsync(<String>['ideviceinfo', '-u', fakeIphoneId])).processResult;
final Map<String, String> executionEnv = Map<String, String>.fromEntries(
<MapEntry<String, String>>[cache.dyLdLibEntry]
);
final ProcessResult ideviceResult = (await runAsync(
<String>[
_ideviceinfoPath,
'-u',
fakeIphoneId
],
environment: executionEnv,
)).processResult;
if (ideviceResult.stdout.contains('Usage: ideviceinfo')) {
return false;
_isWorking = false;
return _isWorking;
}
// If no device is attached, we're unable to detect any problems. Assume all is well.
final ProcessResult result = (await runAsync(<String>['idevice_id', '-l'])).processResult;
if (result.exitCode == 0 && result.stdout.isEmpty)
return true;
// Check that we can look up the names of any attached devices.
return await exitsHappyAsync(<String>['idevicename']);
final ProcessResult result = (await runAsync(
<String>[
_ideviceIdPath,
'-l',
],
environment: executionEnv,
)).processResult;
if (result.exitCode == 0 && result.stdout.isEmpty) {
_isWorking = true;
} else {
// Check that we can look up the names of any attached devices.
_isWorking = await exitsHappyAsync(
<String>[_idevicenamePath],
environment: executionEnv,
);
}
return _isWorking;
}
bool _isWorking;
Future<String> getAvailableDeviceIDs() async {
try {
final ProcessResult result = await processManager.run(<String>['idevice_id', '-l']);
final ProcessResult result = await processManager.run(
<String>[
_ideviceIdPath,
'-l'
],
environment: Map<String, String>.fromEntries(
<MapEntry<String, String>>[cache.dyLdLibEntry]
),
);
if (result.exitCode != 0)
throw ToolExit('idevice_id returned an error:\n${result.stderr}');
return result.stdout;
......@@ -81,7 +146,18 @@ class IMobileDevice {
Future<String> getInfoForDevice(String deviceID, String key) async {
try {
final ProcessResult result = await processManager.run(<String>['ideviceinfo', '-u', deviceID, '-k', key]);
final ProcessResult result = await processManager.run(
<String>[
_ideviceinfoPath,
'-u',
deviceID,
'-k',
key
],
environment: Map<String, String>.fromEntries(
<MapEntry<String, String>>[cache.dyLdLibEntry]
),
);
if (result.exitCode == 255 && result.stdout != null && result.stdout.contains('No device found'))
throw IOSDeviceNotFoundError('ideviceinfo could not find device:\n${result.stdout}');
if (result.exitCode != 0)
......@@ -93,11 +169,30 @@ class IMobileDevice {
}
/// Starts `idevicesyslog` and returns the running process.
Future<Process> startLogger(String deviceID) => runCommand(<String>['idevicesyslog', '-u', deviceID]);
Future<Process> startLogger(String deviceID) {
return runCommand(
<String>[
_idevicesyslogPath,
'-u',
deviceID,
],
environment: Map<String, String>.fromEntries(
<MapEntry<String, String>>[cache.dyLdLibEntry]
),
);
}
/// Captures a screenshot to the specified outputFile.
Future<void> takeScreenshot(File outputFile) {
return runCheckedAsync(<String>['idevicescreenshot', outputFile.path]);
return runCheckedAsync(
<String>[
_idevicescreenshotPath,
outputFile.path
],
environment: Map<String, String>.fromEntries(
<MapEntry<String, String>>[cache.dyLdLibEntry]
),
);
}
}
......
......@@ -30,11 +30,11 @@ const String unknownCocoaPodsConsequence = '''
Ensure that the output of 'pod --version' contains only digits and . to be recognized by Flutter.''';
const String cocoaPodsInstallInstructions = '''
brew install cocoapods
sudo gem install cocoapods
pod setup''';
const String cocoaPodsUpgradeInstructions = '''
brew upgrade cocoapods
sudo gem install cocoapods
pod setup''';
CocoaPods get cocoaPods => context.get<CocoaPods>();
......
......@@ -5,7 +5,6 @@
import 'dart:async';
import '../base/context.dart';
import '../base/os.dart';
import '../base/user_messages.dart';
import '../doctor.dart';
import 'cocoapods.dart';
......@@ -15,8 +14,6 @@ CocoaPodsValidator get cocoapodsValidator => context.get<CocoaPodsValidator>();
class CocoaPodsValidator extends DoctorValidator {
const CocoaPodsValidator() : super('CocoaPods subvalidator');
bool get hasHomebrew => os.which('brew') != null;
@override
Future<ValidationResult> validate() async {
final List<ValidationMessage> messages = <ValidationMessage>[];
......@@ -48,11 +45,6 @@ class CocoaPodsValidator extends DoctorValidator {
}
}
// Only check/report homebrew status if CocoaPods isn't installed.
if (status == ValidationType.missing && !hasHomebrew) {
messages.add(ValidationMessage.hint(userMessages.cocoaPodsBrewMissing));
}
return ValidationResult(status, messages);
}
}
\ No newline at end of file
}
......@@ -107,6 +107,22 @@ void main() {
verifyNever(artifact1.update(<DevelopmentArtifact>{}));
verify(artifact2.update(<DevelopmentArtifact>{}));
});
testUsingContext('getter dyLdLibEntry concatenates the output of each artifact\'s dyLdLibEntry getter', () async {
final CachedArtifact artifact1 = MockCachedArtifact();
final CachedArtifact artifact2 = MockCachedArtifact();
final CachedArtifact artifact3 = MockCachedArtifact();
when(artifact1.dyLdLibPath).thenReturn('/path/to/alpha:/path/to/beta');
when(artifact2.dyLdLibPath).thenReturn('/path/to/gamma:/path/to/delta:/path/to/epsilon');
when(artifact3.dyLdLibPath).thenReturn(''); // Empty output
final Cache cache = Cache(artifacts: <CachedArtifact>[artifact1, artifact2, artifact3]);
expect(cache.dyLdLibEntry.key, 'DYLD_LIBRARY_PATH');
expect(
cache.dyLdLibEntry.value,
'/path/to/alpha:/path/to/beta:/path/to/gamma:/path/to/delta:/path/to/epsilon',
);
}, overrides: <Type, Generator>{
Cache: ()=> mockCache,
});
testUsingContext('failed storage.googleapis.com download shows China warning', () async {
final CachedArtifact artifact1 = MockCachedArtifact();
final CachedArtifact artifact2 = MockCachedArtifact();
......
......@@ -425,7 +425,7 @@ void main() {
final Map<String, String> signingConfigs = await getCodeSigningIdentityDevelopmentTeam(iosApp: app);
expect(
testLogger.errorText,
testLogger.errorText.replaceAll('\n', ' '),
contains('Saved signing certificate "iPhone Developer: Invalid Profile" is not a valid development certificate'),
);
expect(
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/doctor.dart';
import 'package:flutter_tools/src/ios/ios_workflow.dart';
import 'package:flutter_tools/src/ios/mac.dart';
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
import '../../src/common.dart';
import '../../src/context.dart';
void main() {
group('iOS Workflow validation', () {
MockIMobileDevice iMobileDevice;
MockIMobileDevice iMobileDeviceUninstalled;
MockProcessManager processManager;
FileSystem fs;
setUp(() {
iMobileDevice = MockIMobileDevice();
iMobileDeviceUninstalled = MockIMobileDevice(isInstalled: false);
processManager = MockProcessManager();
fs = MemoryFileSystem();
});
testUsingContext('Emit missing status when nothing is installed', () async {
final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget(
hasHomebrew: false,
hasIosDeploy: false,
hasIDeviceInstaller: false,
iosDeployVersionText: '0.0.0',
);
final ValidationResult result = await workflow.validate();
expect(result.type, ValidationType.missing);
}, overrides: <Type, Generator>{
IMobileDevice: () => iMobileDeviceUninstalled,
});
testUsingContext('Emits installed status when homebrew not installed, but not needed', () async {
final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget(hasHomebrew: false);
final ValidationResult result = await workflow.validate();
expect(result.type, ValidationType.installed);
}, overrides: <Type, Generator>{
IMobileDevice: () => iMobileDevice,
});
testUsingContext('Emits partial status when libimobiledevice is not installed', () async {
final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget();
final ValidationResult result = await workflow.validate();
expect(result.type, ValidationType.partial);
}, overrides: <Type, Generator>{
IMobileDevice: () => MockIMobileDevice(isInstalled: false, isWorking: false),
});
testUsingContext('Emits partial status when libimobiledevice is installed but not working', () async {
final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget();
final ValidationResult result = await workflow.validate();
expect(result.type, ValidationType.partial);
}, overrides: <Type, Generator>{
IMobileDevice: () => MockIMobileDevice(isWorking: false),
});
testUsingContext('Emits partial status when libimobiledevice is installed but not working', () async {
when(processManager.run(
<String>['ideviceinfo', '-u', '00008020-001C2D903C42002E'],
workingDirectory: anyNamed('workingDirectory'),
environment: anyNamed('environment')),
).thenAnswer((Invocation _) async {
final MockProcessResult result = MockProcessResult();
when<String>(result.stdout).thenReturn(r'''
Usage: ideviceinfo [OPTIONS]
Show information about a connected device.
-d, --debug enable communication debugging
-s, --simple use a simple connection to avoid auto-pairing with the device
-u, --udid UDID target specific device by its 40-digit device UDID
-q, --domain NAME set domain of query to NAME. Default: None
-k, --key NAME only query key specified by NAME. Default: All keys.
-x, --xml output information as xml plist instead of key/value pairs
-h, --help prints usage information
''');
return null;
});
final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget();
final ValidationResult result = await workflow.validate();
expect(result.type, ValidationType.partial);
}, overrides: <Type, Generator>{
ProcessManager: () => processManager,
});
testUsingContext('Emits partial status when ios-deploy is not installed', () async {
final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget(hasIosDeploy: false);
final ValidationResult result = await workflow.validate();
expect(result.type, ValidationType.partial);
}, overrides: <Type, Generator>{
IMobileDevice: () => iMobileDevice,
});
testUsingContext('Emits partial status when ios-deploy version is too low', () async {
final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget(iosDeployVersionText: '1.8.0');
final ValidationResult result = await workflow.validate();
expect(result.type, ValidationType.partial);
}, overrides: <Type, Generator>{
IMobileDevice: () => iMobileDevice,
});
testUsingContext('Emits partial status when ios-deploy version is a known bad version', () async {
final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget(iosDeployVersionText: '2.0.0');
final ValidationResult result = await workflow.validate();
expect(result.type, ValidationType.partial);
}, overrides: <Type, Generator>{
IMobileDevice: () => iMobileDevice,
});
testUsingContext('Succeeds when all checks pass', () async {
final ValidationResult result = await IOSWorkflowTestTarget().validate();
expect(result.type, ValidationType.installed);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
IMobileDevice: () => iMobileDevice,
ProcessManager: () => processManager,
});
});
}
final ProcessResult exitsHappy = ProcessResult(
1, // pid
0, // exitCode
'', // stdout
'', // stderr
);
class MockIMobileDevice extends IMobileDevice {
MockIMobileDevice({
this.isInstalled = true,
bool isWorking = true,
}) : isWorking = Future<bool>.value(isWorking);
@override
final bool isInstalled;
@override
final Future<bool> isWorking;
}
class MockProcessManager extends Mock implements ProcessManager {}
class MockProcessResult extends Mock implements ProcessResult {}
class IOSWorkflowTestTarget extends IOSValidator {
IOSWorkflowTestTarget({
this.hasHomebrew = true,
bool hasIosDeploy = true,
String iosDeployVersionText = '1.9.4',
bool hasIDeviceInstaller = true,
}) : hasIosDeploy = Future<bool>.value(hasIosDeploy),
iosDeployVersionText = Future<String>.value(iosDeployVersionText),
hasIDeviceInstaller = Future<bool>.value(hasIDeviceInstaller);
@override
final bool hasHomebrew;
@override
final Future<bool> hasIosDeploy;
@override
final Future<String> iosDeployVersionText;
@override
final Future<bool> hasIDeviceInstaller;
}
......@@ -9,6 +9,8 @@ import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart' show ProcessException, ProcessResult;
import 'package:flutter_tools/src/ios/mac.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:mockito/mockito.dart';
import 'package:platform/platform.dart';
......@@ -22,6 +24,8 @@ final Map<Type, Generator> noColorTerminalOverride = <Type, Generator>{
Platform: _kNoColorTerminalPlatform,
};
class MockArtifacts extends Mock implements Artifacts {}
class MockCache extends Mock implements Cache {}
class MockProcessManager extends Mock implements ProcessManager {}
class MockFile extends Mock implements File {}
class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {}
......@@ -32,41 +36,80 @@ void main() {
final FakePlatform osx = FakePlatform.fromPlatform(const LocalPlatform())
..operatingSystem = 'macos';
MockProcessManager mockProcessManager;
final String libimobiledevicePath = fs.path.join('bin', 'cache', 'artifacts', 'libimobiledevice');
final String ideviceIdPath = fs.path.join(libimobiledevicePath, 'idevice_id');
final String ideviceInfoPath = fs.path.join(libimobiledevicePath, 'ideviceinfo');
final String idevicescreenshotPath = fs.path.join(libimobiledevicePath, 'idevicescreenshot');
MockArtifacts mockArtifacts;
MockCache mockCache;
setUp(() {
mockProcessManager = MockProcessManager();
mockCache = MockCache();
mockArtifacts = MockArtifacts();
when(mockArtifacts.getArtifactPath(Artifact.ideviceId, platform: anyNamed('platform'))).thenReturn(ideviceIdPath);
when(mockCache.dyLdLibEntry).thenReturn(
MapEntry<String, String>('DYLD_LIBRARY_PATH', libimobiledevicePath)
);
});
testUsingContext('isWorking returns false if libimobiledevice is not installed', () async {
when(mockProcessManager.runSync(
<String>[ideviceIdPath, '-h'], environment: anyNamed('environment')
)).thenReturn(ProcessResult(123, 1, '', ''));
expect(await iMobileDevice.isWorking, false);
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
Artifacts: () => mockArtifacts,
});
testUsingContext('getAvailableDeviceIDs throws ToolExit when libimobiledevice is not installed', () async {
when(mockProcessManager.run(<String>['idevice_id', '-l']))
.thenThrow(const ProcessException('idevice_id', <String>['-l']));
when(mockProcessManager.run(
<String>[ideviceIdPath, '-l'],
environment: <String, String>{'DYLD_LIBRARY_PATH': libimobiledevicePath},
)).thenThrow(ProcessException(ideviceIdPath, <String>['-l']));
expect(() async => await iMobileDevice.getAvailableDeviceIDs(), throwsToolExit());
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
Cache: () => mockCache,
Artifacts: () => mockArtifacts,
});
testUsingContext('getAvailableDeviceIDs throws ToolExit when idevice_id returns non-zero', () async {
when(mockProcessManager.run(<String>['idevice_id', '-l']))
.thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(1, 1, '', 'Sad today')));
when(mockProcessManager.run(
<String>[ideviceIdPath, '-l'],
environment: <String, String>{'DYLD_LIBRARY_PATH': libimobiledevicePath},
)).thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(1, 1, '', 'Sad today')));
expect(() async => await iMobileDevice.getAvailableDeviceIDs(), throwsToolExit());
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
Cache: () => mockCache,
Artifacts: () => mockArtifacts,
});
testUsingContext('getAvailableDeviceIDs returns idevice_id output when installed', () async {
when(mockProcessManager.run(<String>['idevice_id', '-l']))
.thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(1, 0, 'foo', '')));
when(mockProcessManager.run(
<String>[ideviceIdPath, '-l'],
environment: <String, String>{'DYLD_LIBRARY_PATH': libimobiledevicePath},
)).thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(1, 0, 'foo', '')));
expect(await iMobileDevice.getAvailableDeviceIDs(), 'foo');
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
Cache: () => mockCache,
Artifacts: () => mockArtifacts,
});
testUsingContext('getInfoForDevice throws IOSDeviceNotFoundError when ideviceinfo returns specific error code and message', () async {
when(mockProcessManager.run(<String>['ideviceinfo', '-u', 'foo', '-k', 'bar']))
.thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(1, 255, 'No device found with udid foo, is it plugged in?', '')));
when(mockArtifacts.getArtifactPath(Artifact.ideviceinfo, platform: anyNamed('platform'))).thenReturn(ideviceInfoPath);
when(mockProcessManager.run(
<String>[ideviceInfoPath, '-u', 'foo', '-k', 'bar'],
environment: <String, String>{'DYLD_LIBRARY_PATH': libimobiledevicePath},
)).thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(1, 255, 'No device found with udid foo, is it plugged in?', '')));
expect(() async => await iMobileDevice.getInfoForDevice('foo', 'bar'), throwsA(isInstanceOf<IOSDeviceNotFoundError>()));
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
Cache: () => mockCache,
Artifacts: () => mockArtifacts,
});
group('screenshot', () {
......@@ -77,14 +120,15 @@ void main() {
setUp(() {
mockProcessManager = MockProcessManager();
mockOutputFile = MockFile();
when(mockArtifacts.getArtifactPath(Artifact.idevicescreenshot, platform: anyNamed('platform'))).thenReturn(idevicescreenshotPath);
});
testUsingContext('error if idevicescreenshot is not installed', () async {
when(mockOutputFile.path).thenReturn(outputPath);
// Let `idevicescreenshot` fail with exit code 1.
when(mockProcessManager.run(<String>['idevicescreenshot', outputPath],
environment: null,
when(mockProcessManager.run(<String>[idevicescreenshotPath, outputPath],
environment: <String, String>{'DYLD_LIBRARY_PATH': libimobiledevicePath},
workingDirectory: null,
)).thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(4, 1, '', '')));
......@@ -92,20 +136,23 @@ void main() {
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
Platform: () => osx,
Cache: () => mockCache,
});
testUsingContext('idevicescreenshot captures and returns screenshot', () async {
when(mockOutputFile.path).thenReturn(outputPath);
when(mockProcessManager.run(any, environment: null, workingDirectory: null)).thenAnswer(
when(mockProcessManager.run(any, environment: anyNamed('environment'), workingDirectory: null)).thenAnswer(
(Invocation invocation) => Future<ProcessResult>.value(ProcessResult(4, 0, '', '')));
await iMobileDevice.takeScreenshot(mockOutputFile);
verify(mockProcessManager.run(<String>['idevicescreenshot', outputPath],
environment: null,
verify(mockProcessManager.run(<String>[idevicescreenshotPath, outputPath],
environment: <String, String>{'DYLD_LIBRARY_PATH': libimobiledevicePath},
workingDirectory: null,
));
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
Cache: () => mockCache,
Artifacts: () => mockArtifacts,
});
});
});
......
......@@ -23,7 +23,7 @@ void main() {
});
testUsingContext('Emits installed status when CocoaPods is installed', () async {
final CocoaPodsTestTarget workflow = CocoaPodsTestTarget();
const CocoaPodsValidator workflow = CocoaPodsValidator();
final ValidationResult result = await workflow.validate();
expect(result.type, ValidationType.installed);
}, overrides: <Type, Generator>{
......@@ -33,7 +33,7 @@ void main() {
testUsingContext('Emits missing status when CocoaPods is not installed', () async {
when(cocoaPods.evaluateCocoaPodsInstallation)
.thenAnswer((_) async => CocoaPodsStatus.notInstalled);
final CocoaPodsTestTarget workflow = CocoaPodsTestTarget();
const CocoaPodsValidator workflow = CocoaPodsValidator();
final ValidationResult result = await workflow.validate();
expect(result.type, ValidationType.missing);
}, overrides: <Type, Generator>{
......@@ -43,7 +43,7 @@ void main() {
testUsingContext('Emits partial status when CocoaPods is installed with unknown version', () async {
when(cocoaPods.evaluateCocoaPodsInstallation)
.thenAnswer((_) async => CocoaPodsStatus.unknownVersion);
final CocoaPodsTestTarget workflow = CocoaPodsTestTarget();
const CocoaPodsValidator workflow = CocoaPodsValidator();
final ValidationResult result = await workflow.validate();
expect(result.type, ValidationType.partial);
}, overrides: <Type, Generator>{
......@@ -52,7 +52,7 @@ void main() {
testUsingContext('Emits partial status when CocoaPods is not initialized', () async {
when(cocoaPods.isCocoaPodsInitialized).thenAnswer((_) async => false);
final CocoaPodsTestTarget workflow = CocoaPodsTestTarget();
const CocoaPodsValidator workflow = CocoaPodsValidator();
final ValidationResult result = await workflow.validate();
expect(result.type, ValidationType.partial);
}, overrides: <Type, Generator>{
......@@ -62,30 +62,13 @@ void main() {
testUsingContext('Emits partial status when CocoaPods version is too low', () async {
when(cocoaPods.evaluateCocoaPodsInstallation)
.thenAnswer((_) async => CocoaPodsStatus.belowRecommendedVersion);
final CocoaPodsTestTarget workflow = CocoaPodsTestTarget();
const CocoaPodsValidator workflow = CocoaPodsValidator();
final ValidationResult result = await workflow.validate();
expect(result.type, ValidationType.partial);
}, overrides: <Type, Generator>{
CocoaPods: () => cocoaPods,
});
testUsingContext('Emits installed status when homebrew not installed, but not needed', () async {
final CocoaPodsTestTarget workflow = CocoaPodsTestTarget(hasHomebrew: false);
final ValidationResult result = await workflow.validate();
expect(result.type, ValidationType.installed);
}, overrides: <Type, Generator>{
CocoaPods: () => cocoaPods,
});
});
}
class MockCocoaPods extends Mock implements CocoaPods {}
class CocoaPodsTestTarget extends CocoaPodsValidator {
CocoaPodsTestTarget({
this.hasHomebrew = true,
});
@override
final bool hasHomebrew;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment