[flutter_tools] Remove context from Xcode and most of Xcodeproj (#48661)

This commit is contained in:
Jonah Williams 2020-01-23 15:03:04 -08:00 committed by Flutter GitHub Bot
parent 7cf2ff1f1e
commit ef15eac821
7 changed files with 584 additions and 519 deletions

View File

@ -145,8 +145,20 @@ Future<T> runInContext<T>(
VisualStudioValidator: () => const VisualStudioValidator(), VisualStudioValidator: () => const VisualStudioValidator(),
WebWorkflow: () => const WebWorkflow(), WebWorkflow: () => const WebWorkflow(),
WindowsWorkflow: () => const WindowsWorkflow(), WindowsWorkflow: () => const WindowsWorkflow(),
Xcode: () => Xcode(), Xcode: () => Xcode(
XcodeProjectInterpreter: () => XcodeProjectInterpreter(), logger: globals.logger,
processManager: globals.processManager,
platform: globals.platform,
fileSystem: globals.fs,
xcodeProjectInterpreter: xcodeProjectInterpreter,
),
XcodeProjectInterpreter: () => XcodeProjectInterpreter(
logger: globals.logger,
processManager: globals.processManager,
platform: globals.platform,
fileSystem: globals.fs,
terminal: globals.terminal,
),
XcodeValidator: () => const XcodeValidator(), XcodeValidator: () => const XcodeValidator(),
}, },
); );

View File

@ -458,7 +458,7 @@ Future<XcodeBuildResult> buildXcodeProject({
// e.g. `flutter build bundle`. // e.g. `flutter build bundle`.
buildCommands.add('FLUTTER_SUPPRESS_ANALYTICS=true'); buildCommands.add('FLUTTER_SUPPRESS_ANALYTICS=true');
buildCommands.add('COMPILER_INDEX_STORE_ENABLE=NO'); buildCommands.add('COMPILER_INDEX_STORE_ENABLE=NO');
buildCommands.addAll(environmentVariablesAsXcodeBuildSettings()); buildCommands.addAll(environmentVariablesAsXcodeBuildSettings(globals.platform));
final Stopwatch sw = Stopwatch()..start(); final Stopwatch sw = Stopwatch()..start();
initialBuildStatus = globals.logger.startProgress('Running Xcode build...', timeout: timeoutConfiguration.fastOperation); initialBuildStatus = globals.logger.startProgress('Running Xcode build...', timeout: timeoutConfiguration.fastOperation);

View File

@ -5,6 +5,8 @@
import 'dart:async'; import 'dart:async';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:platform/platform.dart';
import 'package:process/process.dart';
import '../artifacts.dart'; import '../artifacts.dart';
import '../base/common.dart'; import '../base/common.dart';
@ -14,6 +16,7 @@ import '../base/io.dart';
import '../base/logger.dart'; import '../base/logger.dart';
import '../base/os.dart'; import '../base/os.dart';
import '../base/process.dart'; import '../base/process.dart';
import '../base/terminal.dart';
import '../base/utils.dart'; import '../base/utils.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../cache.dart'; import '../cache.dart';
@ -221,15 +224,33 @@ XcodeProjectInterpreter get xcodeProjectInterpreter => context.get<XcodeProjectI
/// Interpreter of Xcode projects. /// Interpreter of Xcode projects.
class XcodeProjectInterpreter { class XcodeProjectInterpreter {
XcodeProjectInterpreter({
@required Platform platform,
@required ProcessManager processManager,
@required Logger logger,
@required FileSystem fileSystem,
@required AnsiTerminal terminal,
}) : _platform = platform,
_fileSystem = fileSystem,
_terminal = terminal,
_logger = logger,
_processUtils = ProcessUtils(logger: logger, processManager: processManager);
final Platform _platform;
final FileSystem _fileSystem;
final ProcessUtils _processUtils;
final AnsiTerminal _terminal;
final Logger _logger;
static const String _executable = '/usr/bin/xcodebuild'; static const String _executable = '/usr/bin/xcodebuild';
static final RegExp _versionRegex = RegExp(r'Xcode ([0-9.]+)'); static final RegExp _versionRegex = RegExp(r'Xcode ([0-9.]+)');
void _updateVersion() { void _updateVersion() {
if (!globals.platform.isMacOS || !globals.fs.file(_executable).existsSync()) { if (!_platform.isMacOS || !_fileSystem.file(_executable).existsSync()) {
return; return;
} }
try { try {
final RunResult result = processUtils.runSync( final RunResult result = _processUtils.runSync(
<String>[_executable, '-version'], <String>[_executable, '-version'],
); );
if (result.exitCode != 0) { if (result.exitCode != 0) {
@ -283,26 +304,26 @@ class XcodeProjectInterpreter {
Duration timeout = const Duration(minutes: 1), Duration timeout = const Duration(minutes: 1),
}) async { }) async {
final Status status = Status.withSpinner( final Status status = Status.withSpinner(
timeout: timeoutConfiguration.fastOperation, timeout: const TimeoutConfiguration().fastOperation,
timeoutConfiguration: timeoutConfiguration, timeoutConfiguration: const TimeoutConfiguration(),
platform: globals.platform, platform: _platform,
stopwatch: Stopwatch(), stopwatch: Stopwatch(),
supportsColor: globals.terminal.supportsColor, supportsColor: _terminal.supportsColor,
); );
final List<String> showBuildSettingsCommand = <String>[ final List<String> showBuildSettingsCommand = <String>[
_executable, _executable,
'-project', '-project',
globals.fs.path.absolute(projectPath), _fileSystem.path.absolute(projectPath),
'-target', '-target',
target, target,
'-showBuildSettings', '-showBuildSettings',
...environmentVariablesAsXcodeBuildSettings() ...environmentVariablesAsXcodeBuildSettings(_platform)
]; ];
try { try {
// showBuildSettings is reported to occasionally timeout. Here, we give it // showBuildSettings is reported to occasionally timeout. Here, we give it
// a lot of wiggle room (locally on Flutter Gallery, this takes ~1s). // a lot of wiggle room (locally on Flutter Gallery, this takes ~1s).
// When there is a timeout, we retry once. // When there is a timeout, we retry once.
final RunResult result = await processUtils.run( final RunResult result = await _processUtils.run(
showBuildSettingsCommand, showBuildSettingsCommand,
throwOnError: true, throwOnError: true,
workingDirectory: projectPath, workingDirectory: projectPath,
@ -317,7 +338,7 @@ class XcodeProjectInterpreter {
command: showBuildSettingsCommand.join(' '), command: showBuildSettingsCommand.join(' '),
).send(); ).send();
} }
globals.printTrace('Unexpected failure to get the build settings: $error.'); _logger.printTrace('Unexpected failure to get the build settings: $error.');
return const <String, String>{}; return const <String, String>{};
} finally { } finally {
status.stop(); status.stop();
@ -325,7 +346,7 @@ class XcodeProjectInterpreter {
} }
void cleanWorkspace(String workspacePath, String scheme) { void cleanWorkspace(String workspacePath, String scheme) {
processUtils.runSync(<String>[ _processUtils.runSync(<String>[
_executable, _executable,
'-workspace', '-workspace',
workspacePath, workspacePath,
@ -333,8 +354,8 @@ class XcodeProjectInterpreter {
scheme, scheme,
'-quiet', '-quiet',
'clean', 'clean',
...environmentVariablesAsXcodeBuildSettings() ...environmentVariablesAsXcodeBuildSettings(_platform)
], workingDirectory: globals.fs.currentDirectory.path); ], workingDirectory: _fileSystem.currentDirectory.path);
} }
Future<XcodeProjectInfo> getInfo(String projectPath, {String projectFilename}) async { Future<XcodeProjectInfo> getInfo(String projectPath, {String projectFilename}) async {
@ -342,7 +363,7 @@ class XcodeProjectInterpreter {
// * -project is passed and the given project isn't there, or // * -project is passed and the given project isn't there, or
// * no -project is passed and there isn't a project. // * no -project is passed and there isn't a project.
const int missingProjectExitCode = 66; const int missingProjectExitCode = 66;
final RunResult result = await processUtils.run( final RunResult result = await _processUtils.run(
<String>[ <String>[
_executable, _executable,
'-list', '-list',
@ -363,9 +384,9 @@ class XcodeProjectInterpreter {
/// This allows developers to pass arbitrary build settings in without the tool needing to make a flag /// This allows developers to pass arbitrary build settings in without the tool needing to make a flag
/// for or be aware of each one. This could be used to set code signing build settings in a CI /// for or be aware of each one. This could be used to set code signing build settings in a CI
/// environment without requiring settings changes in the Xcode project. /// environment without requiring settings changes in the Xcode project.
List<String> environmentVariablesAsXcodeBuildSettings() { List<String> environmentVariablesAsXcodeBuildSettings(Platform platform) {
const String xcodeBuildSettingPrefix = 'FLUTTER_XCODE_'; const String xcodeBuildSettingPrefix = 'FLUTTER_XCODE_';
return globals.platform.environment.entries.where((MapEntry<String, String> mapEntry) { return platform.environment.entries.where((MapEntry<String, String> mapEntry) {
return mapEntry.key.startsWith(xcodeBuildSettingPrefix); return mapEntry.key.startsWith(xcodeBuildSettingPrefix);
}).expand<String>((MapEntry<String, String> mapEntry) { }).expand<String>((MapEntry<String, String> mapEntry) {
// Remove FLUTTER_XCODE_ prefix from the environment variable to get the build setting. // Remove FLUTTER_XCODE_ prefix from the environment variable to get the build setting.

View File

@ -85,7 +85,7 @@ Future<void> buildMacOS({
'OBJROOT=${globals.fs.path.join(flutterBuildDir.absolute.path, 'Build', 'Intermediates.noindex')}', 'OBJROOT=${globals.fs.path.join(flutterBuildDir.absolute.path, 'Build', 'Intermediates.noindex')}',
'SYMROOT=${globals.fs.path.join(flutterBuildDir.absolute.path, 'Build', 'Products')}', 'SYMROOT=${globals.fs.path.join(flutterBuildDir.absolute.path, 'Build', 'Products')}',
'COMPILER_INDEX_STORE_ENABLE=NO', 'COMPILER_INDEX_STORE_ENABLE=NO',
...environmentVariablesAsXcodeBuildSettings() ...environmentVariablesAsXcodeBuildSettings(globals.platform)
], trace: true); ], trace: true);
} finally { } finally {
status.cancel(); status.cancel();

View File

@ -4,11 +4,16 @@
import 'dart:async'; import 'dart:async';
import 'package:meta/meta.dart';
import 'package:platform/platform.dart';
import 'package:process/process.dart';
import '../base/common.dart'; import '../base/common.dart';
import '../base/context.dart'; import '../base/context.dart';
import '../base/file_system.dart';
import '../base/io.dart'; import '../base/io.dart';
import '../base/logger.dart';
import '../base/process.dart'; import '../base/process.dart';
import '../globals.dart' as globals;
import '../ios/xcodeproj.dart'; import '../ios/xcodeproj.dart';
const int kXcodeRequiredVersionMajor = 10; const int kXcodeRequiredVersionMajor = 10;
@ -41,14 +46,31 @@ String getNameForSdk(SdkType sdk) {
return null; return null;
} }
/// A utility class for interacting with Xcode command line tools.
class Xcode { class Xcode {
bool get isInstalledAndMeetsVersionCheck => globals.platform.isMacOS && isInstalled && isVersionSatisfactory; Xcode({
@required Platform platform,
@required ProcessManager processManager,
@required Logger logger,
@required FileSystem fileSystem,
@required XcodeProjectInterpreter xcodeProjectInterpreter,
}) : _platform = platform,
_fileSystem = fileSystem,
_xcodeProjectInterpreter = xcodeProjectInterpreter,
_processUtils = ProcessUtils(logger: logger, processManager: processManager);
final Platform _platform;
final ProcessUtils _processUtils;
final FileSystem _fileSystem;
final XcodeProjectInterpreter _xcodeProjectInterpreter;
bool get isInstalledAndMeetsVersionCheck => _platform.isMacOS && isInstalled && isVersionSatisfactory;
String _xcodeSelectPath; String _xcodeSelectPath;
String get xcodeSelectPath { String get xcodeSelectPath {
if (_xcodeSelectPath == null) { if (_xcodeSelectPath == null) {
try { try {
_xcodeSelectPath = processUtils.runSync( _xcodeSelectPath = _processUtils.runSync(
<String>['/usr/bin/xcode-select', '--print-path'], <String>['/usr/bin/xcode-select', '--print-path'],
).stdout.trim(); ).stdout.trim();
} on ProcessException { } on ProcessException {
@ -64,21 +86,21 @@ class Xcode {
if (xcodeSelectPath == null || xcodeSelectPath.isEmpty) { if (xcodeSelectPath == null || xcodeSelectPath.isEmpty) {
return false; return false;
} }
return xcodeProjectInterpreter.isInstalled; return _xcodeProjectInterpreter.isInstalled;
} }
int get majorVersion => xcodeProjectInterpreter.majorVersion; int get majorVersion => _xcodeProjectInterpreter.majorVersion;
int get minorVersion => xcodeProjectInterpreter.minorVersion; int get minorVersion => _xcodeProjectInterpreter.minorVersion;
String get versionText => xcodeProjectInterpreter.versionText; String get versionText => _xcodeProjectInterpreter.versionText;
bool _eulaSigned; bool _eulaSigned;
/// Has the EULA been signed? /// Has the EULA been signed?
bool get eulaSigned { bool get eulaSigned {
if (_eulaSigned == null) { if (_eulaSigned == null) {
try { try {
final RunResult result = processUtils.runSync( final RunResult result = _processUtils.runSync(
<String>['/usr/bin/xcrun', 'clang'], <String>['/usr/bin/xcrun', 'clang'],
); );
if (result.stdout != null && result.stdout.contains('license')) { if (result.stdout != null && result.stdout.contains('license')) {
@ -103,7 +125,7 @@ class Xcode {
try { try {
// This command will error if additional components need to be installed in // This command will error if additional components need to be installed in
// xcode 9.2 and above. // xcode 9.2 and above.
final RunResult result = processUtils.runSync( final RunResult result = _processUtils.runSync(
<String>['/usr/bin/xcrun', 'simctl', 'list'], <String>['/usr/bin/xcrun', 'simctl', 'list'],
); );
_isSimctlInstalled = result.stderr == null || result.stderr == ''; _isSimctlInstalled = result.stderr == null || result.stderr == '';
@ -115,7 +137,7 @@ class Xcode {
} }
bool get isVersionSatisfactory { bool get isVersionSatisfactory {
if (!xcodeProjectInterpreter.isInstalled) { if (!_xcodeProjectInterpreter.isInstalled) {
return false; return false;
} }
if (majorVersion > kXcodeRequiredVersionMajor) { if (majorVersion > kXcodeRequiredVersionMajor) {
@ -128,14 +150,14 @@ class Xcode {
} }
Future<RunResult> cc(List<String> args) { Future<RunResult> cc(List<String> args) {
return processUtils.run( return _processUtils.run(
<String>['xcrun', 'cc', ...args], <String>['xcrun', 'cc', ...args],
throwOnError: true, throwOnError: true,
); );
} }
Future<RunResult> clang(List<String> args) { Future<RunResult> clang(List<String> args) {
return processUtils.run( return _processUtils.run(
<String>['xcrun', 'clang', ...args], <String>['xcrun', 'clang', ...args],
throwOnError: true, throwOnError: true,
); );
@ -143,7 +165,7 @@ class Xcode {
Future<String> sdkLocation(SdkType sdk) async { Future<String> sdkLocation(SdkType sdk) async {
assert(sdk != null); assert(sdk != null);
final RunResult runResult = await processUtils.run( final RunResult runResult = await _processUtils.run(
<String>['xcrun', '--sdk', getNameForSdk(sdk), '--show-sdk-path'], <String>['xcrun', '--sdk', getNameForSdk(sdk), '--show-sdk-path'],
throwOnError: true, throwOnError: true,
); );
@ -158,10 +180,10 @@ class Xcode {
return null; return null;
} }
final List<String> searchPaths = <String>[ final List<String> searchPaths = <String>[
globals.fs.path.join(xcodeSelectPath, 'Applications', 'Simulator.app'), _fileSystem.path.join(xcodeSelectPath, 'Applications', 'Simulator.app'),
]; ];
return searchPaths.where((String p) => p != null).firstWhere( return searchPaths.where((String p) => p != null).firstWhere(
(String p) => globals.fs.directory(p).existsSync(), (String p) => _fileSystem.directory(p).existsSync(),
orElse: () => null, orElse: () => null,
); );
} }

View File

@ -8,6 +8,8 @@ import 'package:file/memory.dart';
import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/project.dart';
@ -24,36 +26,41 @@ import '../../src/pubspec_schema.dart';
const String xcodebuild = '/usr/bin/xcodebuild'; const String xcodebuild = '/usr/bin/xcodebuild';
void main() { void main() {
group('xcodebuild commands', () { mocks.MockProcessManager processManager;
mocks.MockProcessManager mockProcessManager;
XcodeProjectInterpreter xcodeProjectInterpreter; XcodeProjectInterpreter xcodeProjectInterpreter;
FakePlatform macOS; FakePlatform platform;
FileSystem fs; FileSystem fileSystem;
BufferLogger logger;
AnsiTerminal terminal;
setUp(() { setUp(() {
mockProcessManager = mocks.MockProcessManager(); processManager = mocks.MockProcessManager();
xcodeProjectInterpreter = XcodeProjectInterpreter(); platform = fakePlatform('macos');
macOS = fakePlatform('macos'); fileSystem = MemoryFileSystem();
fs = MemoryFileSystem(); fileSystem.file(xcodebuild).createSync(recursive: true);
fs.file(xcodebuild).createSync(recursive: true); terminal = MockAnsiTerminal();
logger = BufferLogger(
outputPreferences: OutputPreferences.test(),
terminal: terminal
);
xcodeProjectInterpreter = XcodeProjectInterpreter(
logger: logger,
fileSystem: fileSystem,
platform: platform,
processManager: processManager,
terminal: terminal,
);
}); });
void testUsingOsxContext(String description, dynamic testMethod()) { testWithoutContext('xcodebuild versionText returns null when xcodebuild is not installed', () {
testUsingContext(description, testMethod, overrides: <Type, Generator>{ when(processManager.runSync(<String>[xcodebuild, '-version']))
Platform: () => macOS,
FileSystem: () => fs,
ProcessManager: () => mockProcessManager,
});
}
testUsingOsxContext('versionText returns null when xcodebuild is not installed', () {
when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
.thenThrow(const ProcessException(xcodebuild, <String>['-version'])); .thenThrow(const ProcessException(xcodebuild, <String>['-version']));
expect(xcodeProjectInterpreter.versionText, isNull); expect(xcodeProjectInterpreter.versionText, isNull);
}); });
testUsingOsxContext('versionText returns null when xcodebuild is not fully installed', () { testWithoutContext('xcodebuild versionText returns null when xcodebuild is not fully installed', () {
when(mockProcessManager.runSync(<String>[xcodebuild, '-version'])).thenReturn( when(processManager.runSync(<String>[xcodebuild, '-version'])).thenReturn(
ProcessResult( ProcessResult(
0, 0,
1, 1,
@ -63,65 +70,81 @@ void main() {
'', '',
), ),
); );
expect(xcodeProjectInterpreter.versionText, isNull); expect(xcodeProjectInterpreter.versionText, isNull);
}); });
testUsingOsxContext('versionText returns formatted version text', () { testWithoutContext('xcodebuild versionText returns formatted version text', () {
when(mockProcessManager.runSync(<String>[xcodebuild, '-version'])) when(processManager.runSync(<String>[xcodebuild, '-version']))
.thenReturn(ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', '')); .thenReturn(ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', ''));
expect(xcodeProjectInterpreter.versionText, 'Xcode 8.3.3, Build version 8E3004b'); expect(xcodeProjectInterpreter.versionText, 'Xcode 8.3.3, Build version 8E3004b');
}); });
testUsingOsxContext('versionText handles Xcode version string with unexpected format', () { testWithoutContext('xcodebuild versionText handles Xcode version string with unexpected format', () {
when(mockProcessManager.runSync(<String>[xcodebuild, '-version'])) when(processManager.runSync(<String>[xcodebuild, '-version']))
.thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', '')); .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', ''));
expect(xcodeProjectInterpreter.versionText, 'Xcode Ultra5000, Build version 8E3004b'); expect(xcodeProjectInterpreter.versionText, 'Xcode Ultra5000, Build version 8E3004b');
}); });
testUsingOsxContext('majorVersion returns major version', () { testWithoutContext('xcodebuild majorVersion returns major version', () {
when(mockProcessManager.runSync(<String>[xcodebuild, '-version'])) when(processManager.runSync(<String>[xcodebuild, '-version']))
.thenReturn(ProcessResult(1, 0, 'Xcode 10.3.3\nBuild version 8E3004b', '')); .thenReturn(ProcessResult(1, 0, 'Xcode 10.3.3\nBuild version 8E3004b', ''));
expect(xcodeProjectInterpreter.majorVersion, 10); expect(xcodeProjectInterpreter.majorVersion, 10);
}); });
testUsingOsxContext('majorVersion is null when version has unexpected format', () { testWithoutContext('xcodebuild majorVersion is null when version has unexpected format', () {
when(mockProcessManager.runSync(<String>[xcodebuild, '-version'])) when(processManager.runSync(<String>[xcodebuild, '-version']))
.thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', '')); .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', ''));
expect(xcodeProjectInterpreter.majorVersion, isNull); expect(xcodeProjectInterpreter.majorVersion, isNull);
}); });
testUsingOsxContext('minorVersion returns minor version', () { testWithoutContext('xcodebuild inorVersion returns minor version', () {
when(mockProcessManager.runSync(<String>[xcodebuild, '-version'])) when(processManager.runSync(<String>[xcodebuild, '-version']))
.thenReturn(ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', '')); .thenReturn(ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', ''));
expect(xcodeProjectInterpreter.minorVersion, 3); expect(xcodeProjectInterpreter.minorVersion, 3);
}); });
testUsingOsxContext('minorVersion returns 0 when minor version is unspecified', () { testWithoutContext('xcodebuild minorVersion returns 0 when minor version is unspecified', () {
when(mockProcessManager.runSync(<String>[xcodebuild, '-version'])) when(processManager.runSync(<String>[xcodebuild, '-version']))
.thenReturn(ProcessResult(1, 0, 'Xcode 8\nBuild version 8E3004b', '')); .thenReturn(ProcessResult(1, 0, 'Xcode 8\nBuild version 8E3004b', ''));
expect(xcodeProjectInterpreter.minorVersion, 0); expect(xcodeProjectInterpreter.minorVersion, 0);
}); });
testUsingOsxContext('minorVersion is null when version has unexpected format', () { testWithoutContext('xcodebuild minorVersion is null when version has unexpected format', () {
when(mockProcessManager.runSync(<String>[xcodebuild, '-version'])) when(processManager.runSync(<String>[xcodebuild, '-version']))
.thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', '')); .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', ''));
expect(xcodeProjectInterpreter.minorVersion, isNull); expect(xcodeProjectInterpreter.minorVersion, isNull);
}); });
testUsingContext('isInstalled is false when not on MacOS', () { testWithoutContext('xcodebuild isInstalled is false when not on MacOS', () {
fs.file(xcodebuild).deleteSync(); final Platform platform = fakePlatform('notMacOS');
expect(xcodeProjectInterpreter.isInstalled, isFalse); xcodeProjectInterpreter = XcodeProjectInterpreter(
}, overrides: <Type, Generator>{ logger: logger,
Platform: () => fakePlatform('notMacOS'), fileSystem: fileSystem,
}); platform: platform,
processManager: processManager,
terminal: terminal,
);
fileSystem.file(xcodebuild).deleteSync();
testUsingOsxContext('isInstalled is false when xcodebuild does not exist', () {
fs.file(xcodebuild).deleteSync();
expect(xcodeProjectInterpreter.isInstalled, isFalse); expect(xcodeProjectInterpreter.isInstalled, isFalse);
}); });
testUsingOsxContext('isInstalled is false when Xcode is not fully installed', () { testWithoutContext('xcodebuild isInstalled is false when xcodebuild does not exist', () {
when(mockProcessManager.runSync(<String>[xcodebuild, '-version'])).thenReturn( fileSystem.file(xcodebuild).deleteSync();
expect(xcodeProjectInterpreter.isInstalled, isFalse);
});
testWithoutContext('xcodebuild isInstalled is false when Xcode is not fully installed', () {
when(processManager.runSync(<String>[xcodebuild, '-version'])).thenReturn(
ProcessResult( ProcessResult(
0, 0,
1, 1,
@ -131,58 +154,59 @@ void main() {
'', '',
), ),
); );
expect(xcodeProjectInterpreter.isInstalled, isFalse); expect(xcodeProjectInterpreter.isInstalled, isFalse);
}); });
testUsingOsxContext('isInstalled is false when version has unexpected format', () { testWithoutContext('xcodebuild isInstalled is false when version has unexpected format', () {
when(mockProcessManager.runSync(<String>[xcodebuild, '-version'])) when(processManager.runSync(<String>[xcodebuild, '-version']))
.thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', '')); .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', ''));
expect(xcodeProjectInterpreter.isInstalled, isFalse); expect(xcodeProjectInterpreter.isInstalled, isFalse);
}); });
testUsingOsxContext('isInstalled is true when version has expected format', () { testWithoutContext('xcodebuild isInstalled is true when version has expected format', () {
when(mockProcessManager.runSync(<String>[xcodebuild, '-version'])) when(processManager.runSync(<String>[xcodebuild, '-version']))
.thenReturn(ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', '')); .thenReturn(ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', ''));
expect(xcodeProjectInterpreter.isInstalled, isTrue); expect(xcodeProjectInterpreter.isInstalled, isTrue);
}); });
testUsingOsxContext('build settings is empty when xcodebuild failed to get the build settings', () async { testWithoutContext('xcodebuild build settings is empty when xcodebuild failed to get the build settings', () async {
when(mockProcessManager.runSync( when(processManager.runSync(
argThat(contains(xcodebuild)), argThat(contains(xcodebuild)),
workingDirectory: anyNamed('workingDirectory'), workingDirectory: anyNamed('workingDirectory'),
environment: anyNamed('environment'))) environment: anyNamed('environment')))
.thenReturn(ProcessResult(0, 1, '', '')); .thenReturn(ProcessResult(0, 1, '', ''));
expect(await xcodeProjectInterpreter.getBuildSettings('', ''), const <String, String>{}); expect(await xcodeProjectInterpreter.getBuildSettings('', ''), const <String, String>{});
}); });
testUsingContext('build settings flakes', () async { testWithoutContext('xcodebuild build settings flakes', () async {
const Duration delay = Duration(seconds: 1); const Duration delay = Duration(seconds: 1);
mockProcessManager.processFactory = mocks.flakyProcessFactory( processManager.processFactory = mocks.flakyProcessFactory(
flakes: 1, flakes: 1,
delay: delay + const Duration(seconds: 1), delay: delay + const Duration(seconds: 1),
); );
expect(await xcodeProjectInterpreter.getBuildSettings( expect(await xcodeProjectInterpreter.getBuildSettings(
'', '', timeout: delay), '', '', timeout: delay),
const <String, String>{}); const <String, String>{});
// build settings times out and is killed once, then succeeds. // build settings times out and is killed once, then succeeds.
verify(mockProcessManager.killPid(any)).called(1); verify(processManager.killPid(any)).called(1);
// The verbose logs should tell us something timed out. // The verbose logs should tell us something timed out.
expect(testLogger.traceText, contains('timed out')); expect(logger.traceText, contains('timed out'));
}, overrides: <Type, Generator>{
Platform: () => macOS,
FileSystem: () => fs,
ProcessManager: () => mockProcessManager,
}); });
testUsingOsxContext('build settings contains Flutter Xcode environment variables', () async { testWithoutContext('xcodebuild build settings contains Flutter Xcode environment variables', () async {
macOS.environment = Map<String, String>.unmodifiable(<String, String>{ platform.environment = Map<String, String>.unmodifiable(<String, String>{
'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual', 'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual',
'FLUTTER_XCODE_ARCHS': 'arm64' 'FLUTTER_XCODE_ARCHS': 'arm64'
}); });
when(mockProcessManager.runSync(<String>[ when(processManager.runSync(<String>[
xcodebuild, xcodebuild,
'-project', '-project',
macOS.pathSeparator, platform.pathSeparator,
'-target', '-target',
'', '',
'-showBuildSettings', '-showBuildSettings',
@ -195,20 +219,21 @@ void main() {
expect(await xcodeProjectInterpreter.getBuildSettings('', ''), const <String, String>{}); expect(await xcodeProjectInterpreter.getBuildSettings('', ''), const <String, String>{});
}); });
testUsingOsxContext('clean contains Flutter Xcode environment variables', () async { testWithoutContext('xcodebuild clean contains Flutter Xcode environment variables', () async {
macOS.environment = Map<String, String>.unmodifiable(<String, String>{ platform.environment = Map<String, String>.unmodifiable(<String, String>{
'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual', 'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual',
'FLUTTER_XCODE_ARCHS': 'arm64' 'FLUTTER_XCODE_ARCHS': 'arm64'
}); });
when(mockProcessManager.runSync( when(processManager.runSync(
any, any,
workingDirectory: anyNamed('workingDirectory'))) workingDirectory: anyNamed('workingDirectory')))
.thenReturn(ProcessResult(1, 0, '', '')); .thenReturn(ProcessResult(1, 0, '', ''));
xcodeProjectInterpreter.cleanWorkspace('workspace_path', 'Runner'); xcodeProjectInterpreter.cleanWorkspace('workspace_path', 'Runner');
final List<dynamic> captured = verify(mockProcessManager.runSync( final List<dynamic> captured = verify(processManager.runSync(
captureAny, captureAny,
workingDirectory: anyNamed('workingDirectory'), workingDirectory: anyNamed('workingDirectory'),
environment: anyNamed('environment'))).captured; environment: anyNamed('environment'))).captured;
expect(captured.first, <String>[ expect(captured.first, <String>[
xcodebuild, xcodebuild,
'-workspace', '-workspace',
@ -221,60 +246,51 @@ void main() {
'ARCHS=arm64' 'ARCHS=arm64'
]); ]);
}); });
});
group('xcodebuild -list', () { testWithoutContext('xcodebuild -list getInfo returns something when xcodebuild -list succeeds', () async {
mocks.MockProcessManager mockProcessManager;
FakePlatform macOS;
FileSystem fs;
setUp(() {
mockProcessManager = mocks.MockProcessManager();
macOS = fakePlatform('macos');
fs = MemoryFileSystem();
fs.file(xcodebuild).createSync(recursive: true);
});
void testUsingOsxContext(String description, dynamic testMethod()) {
testUsingContext(description, testMethod, overrides: <Type, Generator>{
Platform: () => macOS,
FileSystem: () => fs,
ProcessManager: () => mockProcessManager,
});
}
testUsingOsxContext('getInfo returns something when xcodebuild -list succeeds', () async {
const String workingDirectory = '/'; const String workingDirectory = '/';
when(mockProcessManager.run( when(processManager.run(
<String>[xcodebuild, '-list'], <String>[xcodebuild, '-list'],
environment: anyNamed('environment'), environment: anyNamed('environment'),
workingDirectory: workingDirectory), workingDirectory: workingDirectory),
).thenAnswer((_) { ).thenAnswer((_) {
return Future<ProcessResult>.value(ProcessResult(1, 0, '', '')); return Future<ProcessResult>.value(ProcessResult(1, 0, '', ''));
}); });
final XcodeProjectInterpreter xcodeProjectInterpreter = XcodeProjectInterpreter(); final XcodeProjectInterpreter xcodeProjectInterpreter = XcodeProjectInterpreter(
logger: logger,
fileSystem: fileSystem,
platform: platform,
processManager: processManager,
terminal: terminal,
);
expect(await xcodeProjectInterpreter.getInfo(workingDirectory), isNotNull); expect(await xcodeProjectInterpreter.getInfo(workingDirectory), isNotNull);
}); });
testUsingOsxContext('getInfo throws a tool exit when it is unable to find a project', () async { testWithoutContext('xcodebuild -list getInfo throws a tool exit when it is unable to find a project', () async {
const String workingDirectory = '/'; const String workingDirectory = '/';
const String stderr = 'Useful Xcode failure message about missing project.'; const String stderr = 'Useful Xcode failure message about missing project.';
when(mockProcessManager.run( when(processManager.run(
<String>[xcodebuild, '-list'], <String>[xcodebuild, '-list'],
environment: anyNamed('environment'), environment: anyNamed('environment'),
workingDirectory: workingDirectory), workingDirectory: workingDirectory),
).thenAnswer((_) { ).thenAnswer((_) {
return Future<ProcessResult>.value(ProcessResult(1, 66, '', stderr)); return Future<ProcessResult>.value(ProcessResult(1, 66, '', stderr));
}); });
final XcodeProjectInterpreter xcodeProjectInterpreter = XcodeProjectInterpreter(); final XcodeProjectInterpreter xcodeProjectInterpreter = XcodeProjectInterpreter(
logger: logger,
fileSystem: fileSystem,
platform: platform,
processManager: processManager,
terminal: terminal,
);
expect( expect(
() async => await xcodeProjectInterpreter.getInfo(workingDirectory), () async => await xcodeProjectInterpreter.getInfo(workingDirectory),
throwsToolExit(message: stderr)); throwsToolExit(message: stderr));
}); });
});
group('Xcode project properties', () { testWithoutContext('Xcode project properties from default project can be parsed', () {
test('properties from default project can be parsed', () {
const String output = ''' const String output = '''
Information about project "Runner": Information about project "Runner":
Targets: Targets:
@ -295,7 +311,8 @@ Information about project "Runner":
expect(info.schemes, <String>['Runner']); expect(info.schemes, <String>['Runner']);
expect(info.buildConfigurations, <String>['Debug', 'Release']); expect(info.buildConfigurations, <String>['Debug', 'Release']);
}); });
test('properties from project with custom schemes can be parsed', () {
testWithoutContext('Xcode project properties from project with custom schemes can be parsed', () {
const String output = ''' const String output = '''
Information about project "Runner": Information about project "Runner":
Targets: Targets:
@ -319,63 +336,75 @@ Information about project "Runner":
expect(info.schemes, <String>['Free', 'Paid']); expect(info.schemes, <String>['Free', 'Paid']);
expect(info.buildConfigurations, <String>['Debug (Free)', 'Debug (Paid)', 'Release (Free)', 'Release (Paid)']); expect(info.buildConfigurations, <String>['Debug (Free)', 'Debug (Paid)', 'Release (Free)', 'Release (Paid)']);
}); });
test('expected scheme for non-flavored build is Runner', () {
testWithoutContext('expected scheme for non-flavored build is Runner', () {
expect(XcodeProjectInfo.expectedSchemeFor(BuildInfo.debug), 'Runner'); expect(XcodeProjectInfo.expectedSchemeFor(BuildInfo.debug), 'Runner');
expect(XcodeProjectInfo.expectedSchemeFor(BuildInfo.profile), 'Runner'); expect(XcodeProjectInfo.expectedSchemeFor(BuildInfo.profile), 'Runner');
expect(XcodeProjectInfo.expectedSchemeFor(BuildInfo.release), 'Runner'); expect(XcodeProjectInfo.expectedSchemeFor(BuildInfo.release), 'Runner');
}); });
test('expected build configuration for non-flavored build is derived from BuildMode', () {
testWithoutContext('expected build configuration for non-flavored build is derived from BuildMode', () {
expect(XcodeProjectInfo.expectedBuildConfigurationFor(BuildInfo.debug, 'Runner'), 'Debug'); expect(XcodeProjectInfo.expectedBuildConfigurationFor(BuildInfo.debug, 'Runner'), 'Debug');
expect(XcodeProjectInfo.expectedBuildConfigurationFor(BuildInfo.profile, 'Runner'), 'Profile'); expect(XcodeProjectInfo.expectedBuildConfigurationFor(BuildInfo.profile, 'Runner'), 'Profile');
expect(XcodeProjectInfo.expectedBuildConfigurationFor(BuildInfo.release, 'Runner'), 'Release'); expect(XcodeProjectInfo.expectedBuildConfigurationFor(BuildInfo.release, 'Runner'), 'Release');
}); });
test('expected scheme for flavored build is the title-cased flavor', () {
testWithoutContext('expected scheme for flavored build is the title-cased flavor', () {
expect(XcodeProjectInfo.expectedSchemeFor(const BuildInfo(BuildMode.debug, 'hello')), 'Hello'); expect(XcodeProjectInfo.expectedSchemeFor(const BuildInfo(BuildMode.debug, 'hello')), 'Hello');
expect(XcodeProjectInfo.expectedSchemeFor(const BuildInfo(BuildMode.profile, 'HELLO')), 'HELLO'); expect(XcodeProjectInfo.expectedSchemeFor(const BuildInfo(BuildMode.profile, 'HELLO')), 'HELLO');
expect(XcodeProjectInfo.expectedSchemeFor(const BuildInfo(BuildMode.release, 'Hello')), 'Hello'); expect(XcodeProjectInfo.expectedSchemeFor(const BuildInfo(BuildMode.release, 'Hello')), 'Hello');
}); });
test('expected build configuration for flavored build is Mode-Flavor', () { testWithoutContext('expected build configuration for flavored build is Mode-Flavor', () {
expect(XcodeProjectInfo.expectedBuildConfigurationFor(const BuildInfo(BuildMode.debug, 'hello'), 'Hello'), 'Debug-Hello'); expect(XcodeProjectInfo.expectedBuildConfigurationFor(const BuildInfo(BuildMode.debug, 'hello'), 'Hello'), 'Debug-Hello');
expect(XcodeProjectInfo.expectedBuildConfigurationFor(const BuildInfo(BuildMode.profile, 'HELLO'), 'Hello'), 'Profile-Hello'); expect(XcodeProjectInfo.expectedBuildConfigurationFor(const BuildInfo(BuildMode.profile, 'HELLO'), 'Hello'), 'Profile-Hello');
expect(XcodeProjectInfo.expectedBuildConfigurationFor(const BuildInfo(BuildMode.release, 'Hello'), 'Hello'), 'Release-Hello'); expect(XcodeProjectInfo.expectedBuildConfigurationFor(const BuildInfo(BuildMode.release, 'Hello'), 'Hello'), 'Release-Hello');
}); });
test('scheme for default project is Runner', () {
testWithoutContext('scheme for default project is Runner', () {
final XcodeProjectInfo info = XcodeProjectInfo(<String>['Runner'], <String>['Debug', 'Release'], <String>['Runner']); final XcodeProjectInfo info = XcodeProjectInfo(<String>['Runner'], <String>['Debug', 'Release'], <String>['Runner']);
expect(info.schemeFor(BuildInfo.debug), 'Runner'); expect(info.schemeFor(BuildInfo.debug), 'Runner');
expect(info.schemeFor(BuildInfo.profile), 'Runner'); expect(info.schemeFor(BuildInfo.profile), 'Runner');
expect(info.schemeFor(BuildInfo.release), 'Runner'); expect(info.schemeFor(BuildInfo.release), 'Runner');
expect(info.schemeFor(const BuildInfo(BuildMode.debug, 'unknown')), isNull); expect(info.schemeFor(const BuildInfo(BuildMode.debug, 'unknown')), isNull);
}); });
test('build configuration for default project is matched against BuildMode', () {
testWithoutContext('build configuration for default project is matched against BuildMode', () {
final XcodeProjectInfo info = XcodeProjectInfo(<String>['Runner'], <String>['Debug', 'Profile', 'Release'], <String>['Runner']); final XcodeProjectInfo info = XcodeProjectInfo(<String>['Runner'], <String>['Debug', 'Profile', 'Release'], <String>['Runner']);
expect(info.buildConfigurationFor(BuildInfo.debug, 'Runner'), 'Debug'); expect(info.buildConfigurationFor(BuildInfo.debug, 'Runner'), 'Debug');
expect(info.buildConfigurationFor(BuildInfo.profile, 'Runner'), 'Profile'); expect(info.buildConfigurationFor(BuildInfo.profile, 'Runner'), 'Profile');
expect(info.buildConfigurationFor(BuildInfo.release, 'Runner'), 'Release'); expect(info.buildConfigurationFor(BuildInfo.release, 'Runner'), 'Release');
}); });
test('scheme for project with custom schemes is matched against flavor', () {
testWithoutContext('scheme for project with custom schemes is matched against flavor', () {
final XcodeProjectInfo info = XcodeProjectInfo( final XcodeProjectInfo info = XcodeProjectInfo(
<String>['Runner'], <String>['Runner'],
<String>['Debug (Free)', 'Debug (Paid)', 'Release (Free)', 'Release (Paid)'], <String>['Debug (Free)', 'Debug (Paid)', 'Release (Free)', 'Release (Paid)'],
<String>['Free', 'Paid'], <String>['Free', 'Paid'],
); );
expect(info.schemeFor(const BuildInfo(BuildMode.debug, 'free')), 'Free'); expect(info.schemeFor(const BuildInfo(BuildMode.debug, 'free')), 'Free');
expect(info.schemeFor(const BuildInfo(BuildMode.profile, 'Free')), 'Free'); expect(info.schemeFor(const BuildInfo(BuildMode.profile, 'Free')), 'Free');
expect(info.schemeFor(const BuildInfo(BuildMode.release, 'paid')), 'Paid'); expect(info.schemeFor(const BuildInfo(BuildMode.release, 'paid')), 'Paid');
expect(info.schemeFor(const BuildInfo(BuildMode.debug, null)), isNull); expect(info.schemeFor(const BuildInfo(BuildMode.debug, null)), isNull);
expect(info.schemeFor(const BuildInfo(BuildMode.debug, 'unknown')), isNull); expect(info.schemeFor(const BuildInfo(BuildMode.debug, 'unknown')), isNull);
}); });
test('build configuration for project with custom schemes is matched against BuildMode and flavor', () {
testWithoutContext('build configuration for project with custom schemes is matched against BuildMode and flavor', () {
final XcodeProjectInfo info = XcodeProjectInfo( final XcodeProjectInfo info = XcodeProjectInfo(
<String>['Runner'], <String>['Runner'],
<String>['debug (free)', 'Debug paid', 'profile - Free', 'Profile-Paid', 'release - Free', 'Release-Paid'], <String>['debug (free)', 'Debug paid', 'profile - Free', 'Profile-Paid', 'release - Free', 'Release-Paid'],
<String>['Free', 'Paid'], <String>['Free', 'Paid'],
); );
expect(info.buildConfigurationFor(const BuildInfo(BuildMode.debug, 'free'), 'Free'), 'debug (free)'); expect(info.buildConfigurationFor(const BuildInfo(BuildMode.debug, 'free'), 'Free'), 'debug (free)');
expect(info.buildConfigurationFor(const BuildInfo(BuildMode.debug, 'Paid'), 'Paid'), 'Debug paid'); expect(info.buildConfigurationFor(const BuildInfo(BuildMode.debug, 'Paid'), 'Paid'), 'Debug paid');
expect(info.buildConfigurationFor(const BuildInfo(BuildMode.profile, 'FREE'), 'Free'), 'profile - Free'); expect(info.buildConfigurationFor(const BuildInfo(BuildMode.profile, 'FREE'), 'Free'), 'profile - Free');
expect(info.buildConfigurationFor(const BuildInfo(BuildMode.release, 'paid'), 'Paid'), 'Release-Paid'); expect(info.buildConfigurationFor(const BuildInfo(BuildMode.release, 'paid'), 'Paid'), 'Release-Paid');
}); });
test('build configuration for project with inconsistent naming is null', () {
testWithoutContext('build configuration for project with inconsistent naming is null', () {
final XcodeProjectInfo info = XcodeProjectInfo( final XcodeProjectInfo info = XcodeProjectInfo(
<String>['Runner'], <String>['Runner'],
<String>['Debug-F', 'Dbg Paid', 'Rel Free', 'Release Full'], <String>['Debug-F', 'Dbg Paid', 'Rel Free', 'Release Full'],
@ -385,8 +414,6 @@ Information about project "Runner":
expect(info.buildConfigurationFor(const BuildInfo(BuildMode.profile, 'Free'), 'Free'), null); expect(info.buildConfigurationFor(const BuildInfo(BuildMode.profile, 'Free'), 'Free'), null);
expect(info.buildConfigurationFor(const BuildInfo(BuildMode.release, 'Paid'), 'Paid'), null); expect(info.buildConfigurationFor(const BuildInfo(BuildMode.release, 'Paid'), 'Paid'), null);
}); });
});
group('environmentVariablesAsXcodeBuildSettings', () { group('environmentVariablesAsXcodeBuildSettings', () {
FakePlatform platform; FakePlatform platform;
@ -394,17 +421,15 @@ Information about project "Runner":
platform = fakePlatform('ignored'); platform = fakePlatform('ignored');
}); });
testUsingContext('environment variables as Xcode build settings', () { testWithoutContext('environment variables as Xcode build settings', () {
platform.environment = Map<String, String>.unmodifiable(<String, String>{ platform.environment = Map<String, String>.unmodifiable(<String, String>{
'Ignored': 'Bogus', 'Ignored': 'Bogus',
'FLUTTER_NOT_XCODE': 'Bogus', 'FLUTTER_NOT_XCODE': 'Bogus',
'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual', 'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual',
'FLUTTER_XCODE_ARCHS': 'arm64' 'FLUTTER_XCODE_ARCHS': 'arm64'
}); });
final List<String> environmentVariablesAsBuildSettings = environmentVariablesAsXcodeBuildSettings(); final List<String> environmentVariablesAsBuildSettings = environmentVariablesAsXcodeBuildSettings(platform);
expect(environmentVariablesAsBuildSettings, <String>['CODE_SIGN_STYLE=Manual', 'ARCHS=arm64']); expect(environmentVariablesAsBuildSettings, <String>['CODE_SIGN_STYLE=Manual', 'ARCHS=arm64']);
}, overrides: <Type, Generator>{
Platform: () => platform
}); });
}); });
@ -729,4 +754,9 @@ FakePlatform fakePlatform(String name) {
class MockLocalEngineArtifacts extends Mock implements LocalEngineArtifacts {} class MockLocalEngineArtifacts extends Mock implements LocalEngineArtifacts {}
class MockProcessManager extends Mock implements ProcessManager {} class MockProcessManager extends Mock implements ProcessManager {}
class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter { } class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {}
class MockLogger extends Mock implements Logger {}
class MockAnsiTerminal extends Mock implements AnsiTerminal {
@override
bool get supportsColor => false;
}

View File

@ -2,7 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:file/memory.dart';
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/base/io.dart' show ProcessException, ProcessResult;
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:flutter_tools/src/macos/xcode.dart'; import 'package:flutter_tools/src/macos/xcode.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
@ -17,184 +20,161 @@ class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterprete
class MockPlatform extends Mock implements Platform {} class MockPlatform extends Mock implements Platform {}
void main() { void main() {
group('Xcode', () { ProcessManager processManager;
MockProcessManager mockProcessManager;
Xcode xcode; Xcode xcode;
MockXcodeProjectInterpreter mockXcodeProjectInterpreter; MockXcodeProjectInterpreter mockXcodeProjectInterpreter;
MockPlatform mockPlatform; MockPlatform platform;
Logger logger;
FileSystem fileSystem;
setUp(() { setUp(() {
mockProcessManager = MockProcessManager(); logger = MockLogger();
fileSystem = MemoryFileSystem();
processManager = MockProcessManager();
mockXcodeProjectInterpreter = MockXcodeProjectInterpreter(); mockXcodeProjectInterpreter = MockXcodeProjectInterpreter();
xcode = Xcode(); platform = MockPlatform();
mockPlatform = MockPlatform(); xcode = Xcode(
logger: logger,
platform: platform,
fileSystem: fileSystem,
processManager: processManager,
xcodeProjectInterpreter: mockXcodeProjectInterpreter,
);
}); });
testUsingContext('xcodeSelectPath returns null when xcode-select is not installed', () { testWithoutContext('xcodeSelectPath returns null when xcode-select is not installed', () {
when(mockProcessManager.runSync(<String>['/usr/bin/xcode-select', '--print-path'])) when(processManager.runSync(<String>['/usr/bin/xcode-select', '--print-path']))
.thenThrow(const ProcessException('/usr/bin/xcode-select', <String>['--print-path'])); .thenThrow(const ProcessException('/usr/bin/xcode-select', <String>['--print-path']));
expect(xcode.xcodeSelectPath, isNull); expect(xcode.xcodeSelectPath, isNull);
when(mockProcessManager.runSync(<String>['/usr/bin/xcode-select', '--print-path'])) when(processManager.runSync(<String>['/usr/bin/xcode-select', '--print-path']))
.thenThrow(ArgumentError('Invalid argument(s): Cannot find executable for /usr/bin/xcode-select')); .thenThrow(ArgumentError('Invalid argument(s): Cannot find executable for /usr/bin/xcode-select'));
expect(xcode.xcodeSelectPath, isNull); expect(xcode.xcodeSelectPath, isNull);
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
}); });
testUsingContext('xcodeSelectPath returns path when xcode-select is installed', () { testWithoutContext('xcodeSelectPath returns path when xcode-select is installed', () {
const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer'; const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer';
when(mockProcessManager.runSync(<String>['/usr/bin/xcode-select', '--print-path'])) when(processManager.runSync(<String>['/usr/bin/xcode-select', '--print-path']))
.thenReturn(ProcessResult(1, 0, xcodePath, '')); .thenReturn(ProcessResult(1, 0, xcodePath, ''));
expect(xcode.xcodeSelectPath, xcodePath); expect(xcode.xcodeSelectPath, xcodePath);
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
}); });
testUsingContext('xcodeVersionSatisfactory is false when version is less than minimum', () { testWithoutContext('xcodeVersionSatisfactory is false when version is less than minimum', () {
when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
when(mockXcodeProjectInterpreter.majorVersion).thenReturn(9); when(mockXcodeProjectInterpreter.majorVersion).thenReturn(9);
when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0); when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
expect(xcode.isVersionSatisfactory, isFalse); expect(xcode.isVersionSatisfactory, isFalse);
}, overrides: <Type, Generator>{
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
}); });
testUsingContext('xcodeVersionSatisfactory is false when xcodebuild tools are not installed', () { testWithoutContext('xcodeVersionSatisfactory is false when xcodebuild tools are not installed', () {
when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false); when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false);
expect(xcode.isVersionSatisfactory, isFalse); expect(xcode.isVersionSatisfactory, isFalse);
}, overrides: <Type, Generator>{
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
}); });
testUsingContext('xcodeVersionSatisfactory is true when version meets minimum', () { testWithoutContext('xcodeVersionSatisfactory is true when version meets minimum', () {
when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10); when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10);
when(mockXcodeProjectInterpreter.minorVersion).thenReturn(2); when(mockXcodeProjectInterpreter.minorVersion).thenReturn(2);
expect(xcode.isVersionSatisfactory, isTrue); expect(xcode.isVersionSatisfactory, isTrue);
}, overrides: <Type, Generator>{
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
}); });
testUsingContext('xcodeVersionSatisfactory is true when major version exceeds minimum', () { testWithoutContext('xcodeVersionSatisfactory is true when major version exceeds minimum', () {
when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
when(mockXcodeProjectInterpreter.majorVersion).thenReturn(11); when(mockXcodeProjectInterpreter.majorVersion).thenReturn(11);
when(mockXcodeProjectInterpreter.minorVersion).thenReturn(2); when(mockXcodeProjectInterpreter.minorVersion).thenReturn(2);
expect(xcode.isVersionSatisfactory, isTrue); expect(xcode.isVersionSatisfactory, isTrue);
}, overrides: <Type, Generator>{
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
}); });
testUsingContext('xcodeVersionSatisfactory is true when minor version exceeds minimum', () { testWithoutContext('xcodeVersionSatisfactory is true when minor version exceeds minimum', () {
when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10); when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10);
when(mockXcodeProjectInterpreter.minorVersion).thenReturn(3); when(mockXcodeProjectInterpreter.minorVersion).thenReturn(3);
expect(xcode.isVersionSatisfactory, isTrue); expect(xcode.isVersionSatisfactory, isTrue);
}, overrides: <Type, Generator>{
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
}); });
testUsingContext('isInstalledAndMeetsVersionCheck is false when not macOS', () { testWithoutContext('isInstalledAndMeetsVersionCheck is false when not macOS', () {
when(mockPlatform.isMacOS).thenReturn(false); when(platform.isMacOS).thenReturn(false);
expect(xcode.isInstalledAndMeetsVersionCheck, isFalse); expect(xcode.isInstalledAndMeetsVersionCheck, isFalse);
}, overrides: <Type, Generator>{
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
Platform: () => mockPlatform,
}); });
testUsingContext('isInstalledAndMeetsVersionCheck is false when not installed', () { testWithoutContext('isInstalledAndMeetsVersionCheck is false when not installed', () {
when(mockPlatform.isMacOS).thenReturn(true); when(platform.isMacOS).thenReturn(true);
const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer'; const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer';
when(mockProcessManager.runSync(<String>['/usr/bin/xcode-select', '--print-path'])) when(processManager.runSync(<String>['/usr/bin/xcode-select', '--print-path']))
.thenReturn(ProcessResult(1, 0, xcodePath, '')); .thenReturn(ProcessResult(1, 0, xcodePath, ''));
when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false); when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false);
expect(xcode.isInstalledAndMeetsVersionCheck, isFalse); expect(xcode.isInstalledAndMeetsVersionCheck, isFalse);
}, overrides: <Type, Generator>{
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
Platform: () => mockPlatform,
ProcessManager: () => mockProcessManager,
}); });
testUsingContext('isInstalledAndMeetsVersionCheck is false when no xcode-select', () { testWithoutContext('isInstalledAndMeetsVersionCheck is false when no xcode-select', () {
when(mockPlatform.isMacOS).thenReturn(true); when(platform.isMacOS).thenReturn(true);
when(processManager.runSync(<String>['/usr/bin/xcode-select', '--print-path']))
when(mockProcessManager.runSync(<String>['/usr/bin/xcode-select', '--print-path']))
.thenReturn(ProcessResult(1, 127, '', 'ERROR')); .thenReturn(ProcessResult(1, 127, '', 'ERROR'));
when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10); when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10);
when(mockXcodeProjectInterpreter.minorVersion).thenReturn(2); when(mockXcodeProjectInterpreter.minorVersion).thenReturn(2);
expect(xcode.isInstalledAndMeetsVersionCheck, isFalse); expect(xcode.isInstalledAndMeetsVersionCheck, isFalse);
}, overrides: <Type, Generator>{
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
Platform: () => mockPlatform,
ProcessManager: () => mockProcessManager,
}); });
testUsingContext('isInstalledAndMeetsVersionCheck is false when version not satisfied', () { testWithoutContext('isInstalledAndMeetsVersionCheck is false when version not satisfied', () {
when(mockPlatform.isMacOS).thenReturn(true); when(platform.isMacOS).thenReturn(true);
const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer'; const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer';
when(mockProcessManager.runSync(<String>['/usr/bin/xcode-select', '--print-path'])) when(processManager.runSync(<String>['/usr/bin/xcode-select', '--print-path']))
.thenReturn(ProcessResult(1, 0, xcodePath, '')); .thenReturn(ProcessResult(1, 0, xcodePath, ''));
when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
when(mockXcodeProjectInterpreter.majorVersion).thenReturn(9); when(mockXcodeProjectInterpreter.majorVersion).thenReturn(9);
when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0); when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0);
expect(xcode.isInstalledAndMeetsVersionCheck, isFalse); expect(xcode.isInstalledAndMeetsVersionCheck, isFalse);
}, overrides: <Type, Generator>{
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
Platform: () => mockPlatform,
ProcessManager: () => mockProcessManager,
}); });
testUsingContext('isInstalledAndMeetsVersionCheck is true when macOS and installed and version is satisfied', () { testWithoutContext('isInstalledAndMeetsVersionCheck is true when macOS and installed and version is satisfied', () {
when(mockPlatform.isMacOS).thenReturn(true); when(platform.isMacOS).thenReturn(true);
const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer'; const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer';
when(mockProcessManager.runSync(<String>['/usr/bin/xcode-select', '--print-path'])) when(processManager.runSync(<String>['/usr/bin/xcode-select', '--print-path']))
.thenReturn(ProcessResult(1, 0, xcodePath, '')); .thenReturn(ProcessResult(1, 0, xcodePath, ''));
when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10); when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10);
when(mockXcodeProjectInterpreter.minorVersion).thenReturn(2); when(mockXcodeProjectInterpreter.minorVersion).thenReturn(2);
expect(xcode.isInstalledAndMeetsVersionCheck, isTrue); expect(xcode.isInstalledAndMeetsVersionCheck, isTrue);
}, overrides: <Type, Generator>{
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
Platform: () => mockPlatform,
ProcessManager: () => mockProcessManager,
}); });
testUsingContext('eulaSigned is false when clang is not installed', () { testWithoutContext('eulaSigned is false when clang is not installed', () {
when(mockProcessManager.runSync(<String>['/usr/bin/xcrun', 'clang'])) when(processManager.runSync(<String>['/usr/bin/xcrun', 'clang']))
.thenThrow(const ProcessException('/usr/bin/xcrun', <String>['clang'])); .thenThrow(const ProcessException('/usr/bin/xcrun', <String>['clang']));
expect(xcode.eulaSigned, isFalse); expect(xcode.eulaSigned, isFalse);
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
}); });
testUsingContext('eulaSigned is false when clang output indicates EULA not yet accepted', () { testWithoutContext('eulaSigned is false when clang output indicates EULA not yet accepted', () {
when(mockProcessManager.runSync(<String>['/usr/bin/xcrun', 'clang'])) when(processManager.runSync(<String>['/usr/bin/xcrun', 'clang']))
.thenReturn(ProcessResult(1, 1, '', 'Xcode EULA has not been accepted.\nLaunch Xcode and accept the license.')); .thenReturn(ProcessResult(1, 1, '', 'Xcode EULA has not been accepted.\nLaunch Xcode and accept the license.'));
expect(xcode.eulaSigned, isFalse); expect(xcode.eulaSigned, isFalse);
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
}); });
testUsingContext('eulaSigned is true when clang output indicates EULA has been accepted', () { testWithoutContext('eulaSigned is true when clang output indicates EULA has been accepted', () {
when(mockProcessManager.runSync(<String>['/usr/bin/xcrun', 'clang'])) when(processManager.runSync(<String>['/usr/bin/xcrun', 'clang']))
.thenReturn(ProcessResult(1, 1, '', 'clang: error: no input files')); .thenReturn(ProcessResult(1, 1, '', 'clang: error: no input files'));
expect(xcode.eulaSigned, isTrue); expect(xcode.eulaSigned, isTrue);
}, overrides: <Type, Generator>{
ProcessManager: () => mockProcessManager,
}); });
testUsingContext('SDK name', () { testWithoutContext('SDK name', () {
expect(getNameForSdk(SdkType.iPhone), 'iphoneos'); expect(getNameForSdk(SdkType.iPhone), 'iphoneos');
expect(getNameForSdk(SdkType.iPhoneSimulator), 'iphonesimulator'); expect(getNameForSdk(SdkType.iPhoneSimulator), 'iphonesimulator');
expect(getNameForSdk(SdkType.macOS), 'macosx'); expect(getNameForSdk(SdkType.macOS), 'macosx');
}); });
});
} }
class MockLogger extends Mock implements Logger {}