diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart index 4135286189c..ce53e933ba2 100644 --- a/packages/flutter_tools/lib/src/context_runner.dart +++ b/packages/flutter_tools/lib/src/context_runner.dart @@ -145,8 +145,20 @@ Future runInContext( VisualStudioValidator: () => const VisualStudioValidator(), WebWorkflow: () => const WebWorkflow(), WindowsWorkflow: () => const WindowsWorkflow(), - Xcode: () => Xcode(), - XcodeProjectInterpreter: () => XcodeProjectInterpreter(), + Xcode: () => Xcode( + 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(), }, ); diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index d5b91f42bcb..bda9b967c69 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -458,7 +458,7 @@ Future buildXcodeProject({ // e.g. `flutter build bundle`. buildCommands.add('FLUTTER_SUPPRESS_ANALYTICS=true'); buildCommands.add('COMPILER_INDEX_STORE_ENABLE=NO'); - buildCommands.addAll(environmentVariablesAsXcodeBuildSettings()); + buildCommands.addAll(environmentVariablesAsXcodeBuildSettings(globals.platform)); final Stopwatch sw = Stopwatch()..start(); initialBuildStatus = globals.logger.startProgress('Running Xcode build...', timeout: timeoutConfiguration.fastOperation); diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart index b3c7f742db0..4f3873b733c 100644 --- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart +++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart @@ -5,6 +5,8 @@ import 'dart:async'; import 'package:meta/meta.dart'; +import 'package:platform/platform.dart'; +import 'package:process/process.dart'; import '../artifacts.dart'; import '../base/common.dart'; @@ -14,6 +16,7 @@ import '../base/io.dart'; import '../base/logger.dart'; import '../base/os.dart'; import '../base/process.dart'; +import '../base/terminal.dart'; import '../base/utils.dart'; import '../build_info.dart'; import '../cache.dart'; @@ -221,15 +224,33 @@ XcodeProjectInterpreter get xcodeProjectInterpreter => context.get[_executable, '-version'], ); if (result.exitCode != 0) { @@ -283,26 +304,26 @@ class XcodeProjectInterpreter { Duration timeout = const Duration(minutes: 1), }) async { final Status status = Status.withSpinner( - timeout: timeoutConfiguration.fastOperation, - timeoutConfiguration: timeoutConfiguration, - platform: globals.platform, + timeout: const TimeoutConfiguration().fastOperation, + timeoutConfiguration: const TimeoutConfiguration(), + platform: _platform, stopwatch: Stopwatch(), - supportsColor: globals.terminal.supportsColor, + supportsColor: _terminal.supportsColor, ); final List showBuildSettingsCommand = [ _executable, '-project', - globals.fs.path.absolute(projectPath), + _fileSystem.path.absolute(projectPath), '-target', target, '-showBuildSettings', - ...environmentVariablesAsXcodeBuildSettings() + ...environmentVariablesAsXcodeBuildSettings(_platform) ]; try { // showBuildSettings is reported to occasionally timeout. Here, we give it // a lot of wiggle room (locally on Flutter Gallery, this takes ~1s). // When there is a timeout, we retry once. - final RunResult result = await processUtils.run( + final RunResult result = await _processUtils.run( showBuildSettingsCommand, throwOnError: true, workingDirectory: projectPath, @@ -317,7 +338,7 @@ class XcodeProjectInterpreter { command: showBuildSettingsCommand.join(' '), ).send(); } - globals.printTrace('Unexpected failure to get the build settings: $error.'); + _logger.printTrace('Unexpected failure to get the build settings: $error.'); return const {}; } finally { status.stop(); @@ -325,7 +346,7 @@ class XcodeProjectInterpreter { } void cleanWorkspace(String workspacePath, String scheme) { - processUtils.runSync([ + _processUtils.runSync([ _executable, '-workspace', workspacePath, @@ -333,8 +354,8 @@ class XcodeProjectInterpreter { scheme, '-quiet', 'clean', - ...environmentVariablesAsXcodeBuildSettings() - ], workingDirectory: globals.fs.currentDirectory.path); + ...environmentVariablesAsXcodeBuildSettings(_platform) + ], workingDirectory: _fileSystem.currentDirectory.path); } Future getInfo(String projectPath, {String projectFilename}) async { @@ -342,7 +363,7 @@ class XcodeProjectInterpreter { // * -project is passed and the given project isn't there, or // * no -project is passed and there isn't a project. const int missingProjectExitCode = 66; - final RunResult result = await processUtils.run( + final RunResult result = await _processUtils.run( [ _executable, '-list', @@ -363,9 +384,9 @@ class XcodeProjectInterpreter { /// 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 /// environment without requiring settings changes in the Xcode project. -List environmentVariablesAsXcodeBuildSettings() { +List environmentVariablesAsXcodeBuildSettings(Platform platform) { const String xcodeBuildSettingPrefix = 'FLUTTER_XCODE_'; - return globals.platform.environment.entries.where((MapEntry mapEntry) { + return platform.environment.entries.where((MapEntry mapEntry) { return mapEntry.key.startsWith(xcodeBuildSettingPrefix); }).expand((MapEntry mapEntry) { // Remove FLUTTER_XCODE_ prefix from the environment variable to get the build setting. diff --git a/packages/flutter_tools/lib/src/macos/build_macos.dart b/packages/flutter_tools/lib/src/macos/build_macos.dart index d0c528a4fea..a18fd1b2a8b 100644 --- a/packages/flutter_tools/lib/src/macos/build_macos.dart +++ b/packages/flutter_tools/lib/src/macos/build_macos.dart @@ -85,7 +85,7 @@ Future buildMacOS({ 'OBJROOT=${globals.fs.path.join(flutterBuildDir.absolute.path, 'Build', 'Intermediates.noindex')}', 'SYMROOT=${globals.fs.path.join(flutterBuildDir.absolute.path, 'Build', 'Products')}', 'COMPILER_INDEX_STORE_ENABLE=NO', - ...environmentVariablesAsXcodeBuildSettings() + ...environmentVariablesAsXcodeBuildSettings(globals.platform) ], trace: true); } finally { status.cancel(); diff --git a/packages/flutter_tools/lib/src/macos/xcode.dart b/packages/flutter_tools/lib/src/macos/xcode.dart index 51c9f71172e..5ab378dfc85 100644 --- a/packages/flutter_tools/lib/src/macos/xcode.dart +++ b/packages/flutter_tools/lib/src/macos/xcode.dart @@ -4,11 +4,16 @@ import 'dart:async'; +import 'package:meta/meta.dart'; +import 'package:platform/platform.dart'; +import 'package:process/process.dart'; + import '../base/common.dart'; import '../base/context.dart'; +import '../base/file_system.dart'; import '../base/io.dart'; +import '../base/logger.dart'; import '../base/process.dart'; -import '../globals.dart' as globals; import '../ios/xcodeproj.dart'; const int kXcodeRequiredVersionMajor = 10; @@ -41,14 +46,31 @@ String getNameForSdk(SdkType sdk) { return null; } +/// A utility class for interacting with Xcode command line tools. 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 get xcodeSelectPath { if (_xcodeSelectPath == null) { try { - _xcodeSelectPath = processUtils.runSync( + _xcodeSelectPath = _processUtils.runSync( ['/usr/bin/xcode-select', '--print-path'], ).stdout.trim(); } on ProcessException { @@ -64,21 +86,21 @@ class Xcode { if (xcodeSelectPath == null || xcodeSelectPath.isEmpty) { 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; /// Has the EULA been signed? bool get eulaSigned { if (_eulaSigned == null) { try { - final RunResult result = processUtils.runSync( + final RunResult result = _processUtils.runSync( ['/usr/bin/xcrun', 'clang'], ); if (result.stdout != null && result.stdout.contains('license')) { @@ -103,7 +125,7 @@ class Xcode { try { // This command will error if additional components need to be installed in // xcode 9.2 and above. - final RunResult result = processUtils.runSync( + final RunResult result = _processUtils.runSync( ['/usr/bin/xcrun', 'simctl', 'list'], ); _isSimctlInstalled = result.stderr == null || result.stderr == ''; @@ -115,7 +137,7 @@ class Xcode { } bool get isVersionSatisfactory { - if (!xcodeProjectInterpreter.isInstalled) { + if (!_xcodeProjectInterpreter.isInstalled) { return false; } if (majorVersion > kXcodeRequiredVersionMajor) { @@ -128,14 +150,14 @@ class Xcode { } Future cc(List args) { - return processUtils.run( + return _processUtils.run( ['xcrun', 'cc', ...args], throwOnError: true, ); } Future clang(List args) { - return processUtils.run( + return _processUtils.run( ['xcrun', 'clang', ...args], throwOnError: true, ); @@ -143,7 +165,7 @@ class Xcode { Future sdkLocation(SdkType sdk) async { assert(sdk != null); - final RunResult runResult = await processUtils.run( + final RunResult runResult = await _processUtils.run( ['xcrun', '--sdk', getNameForSdk(sdk), '--show-sdk-path'], throwOnError: true, ); @@ -158,10 +180,10 @@ class Xcode { return null; } final List searchPaths = [ - globals.fs.path.join(xcodeSelectPath, 'Applications', 'Simulator.app'), + _fileSystem.path.join(xcodeSelectPath, 'Applications', 'Simulator.app'), ]; return searchPaths.where((String p) => p != null).firstWhere( - (String p) => globals.fs.directory(p).existsSync(), + (String p) => _fileSystem.directory(p).existsSync(), orElse: () => null, ); } diff --git a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart index 8ffead4f3aa..a84d9443a4a 100644 --- a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart @@ -8,6 +8,8 @@ import 'package:file/memory.dart'; import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/base/file_system.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/ios/xcodeproj.dart'; import 'package:flutter_tools/src/project.dart'; @@ -24,258 +26,272 @@ import '../../src/pubspec_schema.dart'; const String xcodebuild = '/usr/bin/xcodebuild'; void main() { - group('xcodebuild commands', () { - mocks.MockProcessManager mockProcessManager; - XcodeProjectInterpreter xcodeProjectInterpreter; - FakePlatform macOS; - FileSystem fs; + mocks.MockProcessManager processManager; + XcodeProjectInterpreter xcodeProjectInterpreter; + FakePlatform platform; + FileSystem fileSystem; + BufferLogger logger; + AnsiTerminal terminal; - setUp(() { - mockProcessManager = mocks.MockProcessManager(); - xcodeProjectInterpreter = XcodeProjectInterpreter(); - macOS = fakePlatform('macos'); - fs = MemoryFileSystem(); - fs.file(xcodebuild).createSync(recursive: true); - }); + setUp(() { + processManager = mocks.MockProcessManager(); + platform = fakePlatform('macos'); + fileSystem = MemoryFileSystem(); + fileSystem.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()) { - testUsingContext(description, testMethod, overrides: { - Platform: () => macOS, - FileSystem: () => fs, - ProcessManager: () => mockProcessManager, - }); - } + testWithoutContext('xcodebuild versionText returns null when xcodebuild is not installed', () { + when(processManager.runSync([xcodebuild, '-version'])) + .thenThrow(const ProcessException(xcodebuild, ['-version'])); - testUsingOsxContext('versionText returns null when xcodebuild is not installed', () { - when(mockProcessManager.runSync([xcodebuild, '-version'])) - .thenThrow(const ProcessException(xcodebuild, ['-version'])); - expect(xcodeProjectInterpreter.versionText, isNull); - }); + expect(xcodeProjectInterpreter.versionText, isNull); + }); - testUsingOsxContext('versionText returns null when xcodebuild is not fully installed', () { - when(mockProcessManager.runSync([xcodebuild, '-version'])).thenReturn( - ProcessResult( - 0, - 1, - "xcode-select: error: tool 'xcodebuild' requires Xcode, " - "but active developer directory '/Library/Developer/CommandLineTools' " - 'is a command line tools instance', - '', - ), - ); - expect(xcodeProjectInterpreter.versionText, isNull); - }); - - testUsingOsxContext('versionText returns formatted version text', () { - when(mockProcessManager.runSync([xcodebuild, '-version'])) - .thenReturn(ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', '')); - expect(xcodeProjectInterpreter.versionText, 'Xcode 8.3.3, Build version 8E3004b'); - }); - - testUsingOsxContext('versionText handles Xcode version string with unexpected format', () { - when(mockProcessManager.runSync([xcodebuild, '-version'])) - .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', '')); - expect(xcodeProjectInterpreter.versionText, 'Xcode Ultra5000, Build version 8E3004b'); - }); - - testUsingOsxContext('majorVersion returns major version', () { - when(mockProcessManager.runSync([xcodebuild, '-version'])) - .thenReturn(ProcessResult(1, 0, 'Xcode 10.3.3\nBuild version 8E3004b', '')); - expect(xcodeProjectInterpreter.majorVersion, 10); - }); - - testUsingOsxContext('majorVersion is null when version has unexpected format', () { - when(mockProcessManager.runSync([xcodebuild, '-version'])) - .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', '')); - expect(xcodeProjectInterpreter.majorVersion, isNull); - }); - - testUsingOsxContext('minorVersion returns minor version', () { - when(mockProcessManager.runSync([xcodebuild, '-version'])) - .thenReturn(ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', '')); - expect(xcodeProjectInterpreter.minorVersion, 3); - }); - - testUsingOsxContext('minorVersion returns 0 when minor version is unspecified', () { - when(mockProcessManager.runSync([xcodebuild, '-version'])) - .thenReturn(ProcessResult(1, 0, 'Xcode 8\nBuild version 8E3004b', '')); - expect(xcodeProjectInterpreter.minorVersion, 0); - }); - - testUsingOsxContext('minorVersion is null when version has unexpected format', () { - when(mockProcessManager.runSync([xcodebuild, '-version'])) - .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', '')); - expect(xcodeProjectInterpreter.minorVersion, isNull); - }); - - testUsingContext('isInstalled is false when not on MacOS', () { - fs.file(xcodebuild).deleteSync(); - expect(xcodeProjectInterpreter.isInstalled, isFalse); - }, overrides: { - Platform: () => fakePlatform('notMacOS'), - }); - - testUsingOsxContext('isInstalled is false when xcodebuild does not exist', () { - fs.file(xcodebuild).deleteSync(); - expect(xcodeProjectInterpreter.isInstalled, isFalse); - }); - - testUsingOsxContext('isInstalled is false when Xcode is not fully installed', () { - when(mockProcessManager.runSync([xcodebuild, '-version'])).thenReturn( - ProcessResult( - 0, - 1, - "xcode-select: error: tool 'xcodebuild' requires Xcode, " - "but active developer directory '/Library/Developer/CommandLineTools' " - 'is a command line tools instance', - '', - ), - ); - expect(xcodeProjectInterpreter.isInstalled, isFalse); - }); - - testUsingOsxContext('isInstalled is false when version has unexpected format', () { - when(mockProcessManager.runSync([xcodebuild, '-version'])) - .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', '')); - expect(xcodeProjectInterpreter.isInstalled, isFalse); - }); - - testUsingOsxContext('isInstalled is true when version has expected format', () { - when(mockProcessManager.runSync([xcodebuild, '-version'])) - .thenReturn(ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', '')); - expect(xcodeProjectInterpreter.isInstalled, isTrue); - }); - - testUsingOsxContext('build settings is empty when xcodebuild failed to get the build settings', () async { - when(mockProcessManager.runSync( - argThat(contains(xcodebuild)), - workingDirectory: anyNamed('workingDirectory'), - environment: anyNamed('environment'))) - .thenReturn(ProcessResult(0, 1, '', '')); - expect(await xcodeProjectInterpreter.getBuildSettings('', ''), const {}); - }); - - testUsingContext('build settings flakes', () async { - const Duration delay = Duration(seconds: 1); - mockProcessManager.processFactory = mocks.flakyProcessFactory( - flakes: 1, - delay: delay + const Duration(seconds: 1), - ); - expect(await xcodeProjectInterpreter.getBuildSettings( - '', '', timeout: delay), - const {}); - // build settings times out and is killed once, then succeeds. - verify(mockProcessManager.killPid(any)).called(1); - // The verbose logs should tell us something timed out. - expect(testLogger.traceText, contains('timed out')); - }, overrides: { - Platform: () => macOS, - FileSystem: () => fs, - ProcessManager: () => mockProcessManager, - }); - - testUsingOsxContext('build settings contains Flutter Xcode environment variables', () async { - macOS.environment = Map.unmodifiable({ - 'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual', - 'FLUTTER_XCODE_ARCHS': 'arm64' - }); - when(mockProcessManager.runSync([ - xcodebuild, - '-project', - macOS.pathSeparator, - '-target', + testWithoutContext('xcodebuild versionText returns null when xcodebuild is not fully installed', () { + when(processManager.runSync([xcodebuild, '-version'])).thenReturn( + ProcessResult( + 0, + 1, + "xcode-select: error: tool 'xcodebuild' requires Xcode, " + "but active developer directory '/Library/Developer/CommandLineTools' " + 'is a command line tools instance', '', - '-showBuildSettings', - 'CODE_SIGN_STYLE=Manual', - 'ARCHS=arm64' - ], - workingDirectory: anyNamed('workingDirectory'), - environment: anyNamed('environment'))) - .thenReturn(ProcessResult(1, 0, '', '')); - expect(await xcodeProjectInterpreter.getBuildSettings('', ''), const {}); - }); + ), + ); - testUsingOsxContext('clean contains Flutter Xcode environment variables', () async { - macOS.environment = Map.unmodifiable({ - 'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual', - 'FLUTTER_XCODE_ARCHS': 'arm64' - }); - when(mockProcessManager.runSync( - any, - workingDirectory: anyNamed('workingDirectory'))) - .thenReturn(ProcessResult(1, 0, '', '')); - xcodeProjectInterpreter.cleanWorkspace('workspace_path', 'Runner'); - final List captured = verify(mockProcessManager.runSync( - captureAny, - workingDirectory: anyNamed('workingDirectory'), - environment: anyNamed('environment'))).captured; - expect(captured.first, [ - xcodebuild, - '-workspace', - 'workspace_path', - '-scheme', - 'Runner', - '-quiet', - 'clean', - 'CODE_SIGN_STYLE=Manual', - 'ARCHS=arm64' - ]); - }); + expect(xcodeProjectInterpreter.versionText, isNull); }); - group('xcodebuild -list', () { - mocks.MockProcessManager mockProcessManager; - FakePlatform macOS; - FileSystem fs; + testWithoutContext('xcodebuild versionText returns formatted version text', () { + when(processManager.runSync([xcodebuild, '-version'])) + .thenReturn(ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', '')); - 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: { - Platform: () => macOS, - FileSystem: () => fs, - ProcessManager: () => mockProcessManager, - }); - } - - testUsingOsxContext('getInfo returns something when xcodebuild -list succeeds', () async { - const String workingDirectory = '/'; - when(mockProcessManager.run( - [xcodebuild, '-list'], - environment: anyNamed('environment'), - workingDirectory: workingDirectory), - ).thenAnswer((_) { - return Future.value(ProcessResult(1, 0, '', '')); - }); - final XcodeProjectInterpreter xcodeProjectInterpreter = XcodeProjectInterpreter(); - expect(await xcodeProjectInterpreter.getInfo(workingDirectory), isNotNull); - }); - - testUsingOsxContext('getInfo throws a tool exit when it is unable to find a project', () async { - const String workingDirectory = '/'; - const String stderr = 'Useful Xcode failure message about missing project.'; - when(mockProcessManager.run( - [xcodebuild, '-list'], - environment: anyNamed('environment'), - workingDirectory: workingDirectory), - ).thenAnswer((_) { - return Future.value(ProcessResult(1, 66, '', stderr)); - }); - final XcodeProjectInterpreter xcodeProjectInterpreter = XcodeProjectInterpreter(); - expect( - () async => await xcodeProjectInterpreter.getInfo(workingDirectory), - throwsToolExit(message: stderr)); - }); + expect(xcodeProjectInterpreter.versionText, 'Xcode 8.3.3, Build version 8E3004b'); }); - group('Xcode project properties', () { - test('properties from default project can be parsed', () { - const String output = ''' + testWithoutContext('xcodebuild versionText handles Xcode version string with unexpected format', () { + when(processManager.runSync([xcodebuild, '-version'])) + .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', '')); + + expect(xcodeProjectInterpreter.versionText, 'Xcode Ultra5000, Build version 8E3004b'); + }); + + testWithoutContext('xcodebuild majorVersion returns major version', () { + when(processManager.runSync([xcodebuild, '-version'])) + .thenReturn(ProcessResult(1, 0, 'Xcode 10.3.3\nBuild version 8E3004b', '')); + + expect(xcodeProjectInterpreter.majorVersion, 10); + }); + + testWithoutContext('xcodebuild majorVersion is null when version has unexpected format', () { + when(processManager.runSync([xcodebuild, '-version'])) + .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', '')); + + expect(xcodeProjectInterpreter.majorVersion, isNull); + }); + + testWithoutContext('xcodebuild inorVersion returns minor version', () { + when(processManager.runSync([xcodebuild, '-version'])) + .thenReturn(ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', '')); + + expect(xcodeProjectInterpreter.minorVersion, 3); + }); + + testWithoutContext('xcodebuild minorVersion returns 0 when minor version is unspecified', () { + when(processManager.runSync([xcodebuild, '-version'])) + .thenReturn(ProcessResult(1, 0, 'Xcode 8\nBuild version 8E3004b', '')); + + expect(xcodeProjectInterpreter.minorVersion, 0); + }); + + testWithoutContext('xcodebuild minorVersion is null when version has unexpected format', () { + when(processManager.runSync([xcodebuild, '-version'])) + .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', '')); + + expect(xcodeProjectInterpreter.minorVersion, isNull); + }); + + testWithoutContext('xcodebuild isInstalled is false when not on MacOS', () { + final Platform platform = fakePlatform('notMacOS'); + xcodeProjectInterpreter = XcodeProjectInterpreter( + logger: logger, + fileSystem: fileSystem, + platform: platform, + processManager: processManager, + terminal: terminal, + ); + fileSystem.file(xcodebuild).deleteSync(); + + expect(xcodeProjectInterpreter.isInstalled, isFalse); + }); + + testWithoutContext('xcodebuild isInstalled is false when xcodebuild does not exist', () { + fileSystem.file(xcodebuild).deleteSync(); + + expect(xcodeProjectInterpreter.isInstalled, isFalse); + }); + + testWithoutContext('xcodebuild isInstalled is false when Xcode is not fully installed', () { + when(processManager.runSync([xcodebuild, '-version'])).thenReturn( + ProcessResult( + 0, + 1, + "xcode-select: error: tool 'xcodebuild' requires Xcode, " + "but active developer directory '/Library/Developer/CommandLineTools' " + 'is a command line tools instance', + '', + ), + ); + + expect(xcodeProjectInterpreter.isInstalled, isFalse); + }); + + testWithoutContext('xcodebuild isInstalled is false when version has unexpected format', () { + when(processManager.runSync([xcodebuild, '-version'])) + .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', '')); + + expect(xcodeProjectInterpreter.isInstalled, isFalse); + }); + + testWithoutContext('xcodebuild isInstalled is true when version has expected format', () { + when(processManager.runSync([xcodebuild, '-version'])) + .thenReturn(ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', '')); + + expect(xcodeProjectInterpreter.isInstalled, isTrue); + }); + + testWithoutContext('xcodebuild build settings is empty when xcodebuild failed to get the build settings', () async { + when(processManager.runSync( + argThat(contains(xcodebuild)), + workingDirectory: anyNamed('workingDirectory'), + environment: anyNamed('environment'))) + .thenReturn(ProcessResult(0, 1, '', '')); + + expect(await xcodeProjectInterpreter.getBuildSettings('', ''), const {}); + }); + + testWithoutContext('xcodebuild build settings flakes', () async { + const Duration delay = Duration(seconds: 1); + processManager.processFactory = mocks.flakyProcessFactory( + flakes: 1, + delay: delay + const Duration(seconds: 1), + ); + + expect(await xcodeProjectInterpreter.getBuildSettings( + '', '', timeout: delay), + const {}); + // build settings times out and is killed once, then succeeds. + verify(processManager.killPid(any)).called(1); + // The verbose logs should tell us something timed out. + expect(logger.traceText, contains('timed out')); + }); + + testWithoutContext('xcodebuild build settings contains Flutter Xcode environment variables', () async { + platform.environment = Map.unmodifiable({ + 'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual', + 'FLUTTER_XCODE_ARCHS': 'arm64' + }); + when(processManager.runSync([ + xcodebuild, + '-project', + platform.pathSeparator, + '-target', + '', + '-showBuildSettings', + 'CODE_SIGN_STYLE=Manual', + 'ARCHS=arm64' + ], + workingDirectory: anyNamed('workingDirectory'), + environment: anyNamed('environment'))) + .thenReturn(ProcessResult(1, 0, '', '')); + expect(await xcodeProjectInterpreter.getBuildSettings('', ''), const {}); + }); + + testWithoutContext('xcodebuild clean contains Flutter Xcode environment variables', () async { + platform.environment = Map.unmodifiable({ + 'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual', + 'FLUTTER_XCODE_ARCHS': 'arm64' + }); + when(processManager.runSync( + any, + workingDirectory: anyNamed('workingDirectory'))) + .thenReturn(ProcessResult(1, 0, '', '')); + xcodeProjectInterpreter.cleanWorkspace('workspace_path', 'Runner'); + final List captured = verify(processManager.runSync( + captureAny, + workingDirectory: anyNamed('workingDirectory'), + environment: anyNamed('environment'))).captured; + + expect(captured.first, [ + xcodebuild, + '-workspace', + 'workspace_path', + '-scheme', + 'Runner', + '-quiet', + 'clean', + 'CODE_SIGN_STYLE=Manual', + 'ARCHS=arm64' + ]); + }); + + testWithoutContext('xcodebuild -list getInfo returns something when xcodebuild -list succeeds', () async { + const String workingDirectory = '/'; + when(processManager.run( + [xcodebuild, '-list'], + environment: anyNamed('environment'), + workingDirectory: workingDirectory), + ).thenAnswer((_) { + return Future.value(ProcessResult(1, 0, '', '')); + }); + final XcodeProjectInterpreter xcodeProjectInterpreter = XcodeProjectInterpreter( + logger: logger, + fileSystem: fileSystem, + platform: platform, + processManager: processManager, + terminal: terminal, + ); + + expect(await xcodeProjectInterpreter.getInfo(workingDirectory), isNotNull); + }); + + testWithoutContext('xcodebuild -list getInfo throws a tool exit when it is unable to find a project', () async { + const String workingDirectory = '/'; + const String stderr = 'Useful Xcode failure message about missing project.'; + when(processManager.run( + [xcodebuild, '-list'], + environment: anyNamed('environment'), + workingDirectory: workingDirectory), + ).thenAnswer((_) { + return Future.value(ProcessResult(1, 66, '', stderr)); + }); + final XcodeProjectInterpreter xcodeProjectInterpreter = XcodeProjectInterpreter( + logger: logger, + fileSystem: fileSystem, + platform: platform, + processManager: processManager, + terminal: terminal, + ); + + expect( + () async => await xcodeProjectInterpreter.getInfo(workingDirectory), + throwsToolExit(message: stderr)); + }); + + testWithoutContext('Xcode project properties from default project can be parsed', () { + const String output = ''' Information about project "Runner": Targets: Runner @@ -290,13 +306,14 @@ Information about project "Runner": Runner '''; - final XcodeProjectInfo info = XcodeProjectInfo.fromXcodeBuildOutput(output); - expect(info.targets, ['Runner']); - expect(info.schemes, ['Runner']); - expect(info.buildConfigurations, ['Debug', 'Release']); - }); - test('properties from project with custom schemes can be parsed', () { - const String output = ''' + final XcodeProjectInfo info = XcodeProjectInfo.fromXcodeBuildOutput(output); + expect(info.targets, ['Runner']); + expect(info.schemes, ['Runner']); + expect(info.buildConfigurations, ['Debug', 'Release']); + }); + + testWithoutContext('Xcode project properties from project with custom schemes can be parsed', () { + const String output = ''' Information about project "Runner": Targets: Runner @@ -314,97 +331,105 @@ Information about project "Runner": Paid '''; - final XcodeProjectInfo info = XcodeProjectInfo.fromXcodeBuildOutput(output); - expect(info.targets, ['Runner']); - expect(info.schemes, ['Free', 'Paid']); - expect(info.buildConfigurations, ['Debug (Free)', 'Debug (Paid)', 'Release (Free)', 'Release (Paid)']); - }); - test('expected scheme for non-flavored build is Runner', () { - expect(XcodeProjectInfo.expectedSchemeFor(BuildInfo.debug), 'Runner'); - expect(XcodeProjectInfo.expectedSchemeFor(BuildInfo.profile), 'Runner'); - expect(XcodeProjectInfo.expectedSchemeFor(BuildInfo.release), 'Runner'); - }); - test('expected build configuration for non-flavored build is derived from BuildMode', () { - expect(XcodeProjectInfo.expectedBuildConfigurationFor(BuildInfo.debug, 'Runner'), 'Debug'); - expect(XcodeProjectInfo.expectedBuildConfigurationFor(BuildInfo.profile, 'Runner'), 'Profile'); - expect(XcodeProjectInfo.expectedBuildConfigurationFor(BuildInfo.release, 'Runner'), 'Release'); - }); - test('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.profile, 'HELLO')), 'HELLO'); - expect(XcodeProjectInfo.expectedSchemeFor(const BuildInfo(BuildMode.release, 'Hello')), 'Hello'); - }); - test('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.profile, 'HELLO'), 'Hello'), 'Profile-Hello'); - expect(XcodeProjectInfo.expectedBuildConfigurationFor(const BuildInfo(BuildMode.release, 'Hello'), 'Hello'), 'Release-Hello'); - }); - test('scheme for default project is Runner', () { - final XcodeProjectInfo info = XcodeProjectInfo(['Runner'], ['Debug', 'Release'], ['Runner']); - expect(info.schemeFor(BuildInfo.debug), 'Runner'); - expect(info.schemeFor(BuildInfo.profile), 'Runner'); - expect(info.schemeFor(BuildInfo.release), 'Runner'); - expect(info.schemeFor(const BuildInfo(BuildMode.debug, 'unknown')), isNull); - }); - test('build configuration for default project is matched against BuildMode', () { - final XcodeProjectInfo info = XcodeProjectInfo(['Runner'], ['Debug', 'Profile', 'Release'], ['Runner']); - expect(info.buildConfigurationFor(BuildInfo.debug, 'Runner'), 'Debug'); - expect(info.buildConfigurationFor(BuildInfo.profile, 'Runner'), 'Profile'); - expect(info.buildConfigurationFor(BuildInfo.release, 'Runner'), 'Release'); - }); - test('scheme for project with custom schemes is matched against flavor', () { - final XcodeProjectInfo info = XcodeProjectInfo( - ['Runner'], - ['Debug (Free)', 'Debug (Paid)', 'Release (Free)', 'Release (Paid)'], - ['Free', 'Paid'], - ); - expect(info.schemeFor(const BuildInfo(BuildMode.debug, '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.debug, null)), isNull); - expect(info.schemeFor(const BuildInfo(BuildMode.debug, 'unknown')), isNull); - }); - test('build configuration for project with custom schemes is matched against BuildMode and flavor', () { - final XcodeProjectInfo info = XcodeProjectInfo( - ['Runner'], - ['debug (free)', 'Debug paid', 'profile - Free', 'Profile-Paid', 'release - Free', 'Release-Paid'], - ['Free', 'Paid'], - ); - 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.profile, 'FREE'), 'Free'), 'profile - Free'); - expect(info.buildConfigurationFor(const BuildInfo(BuildMode.release, 'paid'), 'Paid'), 'Release-Paid'); - }); - test('build configuration for project with inconsistent naming is null', () { - final XcodeProjectInfo info = XcodeProjectInfo( - ['Runner'], - ['Debug-F', 'Dbg Paid', 'Rel Free', 'Release Full'], - ['Free', 'Paid'], - ); - expect(info.buildConfigurationFor(const BuildInfo(BuildMode.debug, 'Free'), 'Free'), null); - expect(info.buildConfigurationFor(const BuildInfo(BuildMode.profile, 'Free'), 'Free'), null); - expect(info.buildConfigurationFor(const BuildInfo(BuildMode.release, 'Paid'), 'Paid'), null); - }); + final XcodeProjectInfo info = XcodeProjectInfo.fromXcodeBuildOutput(output); + expect(info.targets, ['Runner']); + expect(info.schemes, ['Free', 'Paid']); + expect(info.buildConfigurations, ['Debug (Free)', 'Debug (Paid)', 'Release (Free)', 'Release (Paid)']); }); - group('environmentVariablesAsXcodeBuildSettings', () { + testWithoutContext('expected scheme for non-flavored build is Runner', () { + expect(XcodeProjectInfo.expectedSchemeFor(BuildInfo.debug), 'Runner'); + expect(XcodeProjectInfo.expectedSchemeFor(BuildInfo.profile), 'Runner'); + expect(XcodeProjectInfo.expectedSchemeFor(BuildInfo.release), 'Runner'); + }); + + testWithoutContext('expected build configuration for non-flavored build is derived from BuildMode', () { + expect(XcodeProjectInfo.expectedBuildConfigurationFor(BuildInfo.debug, 'Runner'), 'Debug'); + expect(XcodeProjectInfo.expectedBuildConfigurationFor(BuildInfo.profile, 'Runner'), 'Profile'); + expect(XcodeProjectInfo.expectedBuildConfigurationFor(BuildInfo.release, 'Runner'), 'Release'); + }); + + 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.profile, 'HELLO')), 'HELLO'); + expect(XcodeProjectInfo.expectedSchemeFor(const BuildInfo(BuildMode.release, 'Hello')), 'Hello'); + }); + 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.profile, 'HELLO'), 'Hello'), 'Profile-Hello'); + expect(XcodeProjectInfo.expectedBuildConfigurationFor(const BuildInfo(BuildMode.release, 'Hello'), 'Hello'), 'Release-Hello'); + }); + + testWithoutContext('scheme for default project is Runner', () { + final XcodeProjectInfo info = XcodeProjectInfo(['Runner'], ['Debug', 'Release'], ['Runner']); + + expect(info.schemeFor(BuildInfo.debug), 'Runner'); + expect(info.schemeFor(BuildInfo.profile), 'Runner'); + expect(info.schemeFor(BuildInfo.release), 'Runner'); + expect(info.schemeFor(const BuildInfo(BuildMode.debug, 'unknown')), isNull); + }); + + testWithoutContext('build configuration for default project is matched against BuildMode', () { + final XcodeProjectInfo info = XcodeProjectInfo(['Runner'], ['Debug', 'Profile', 'Release'], ['Runner']); + + expect(info.buildConfigurationFor(BuildInfo.debug, 'Runner'), 'Debug'); + expect(info.buildConfigurationFor(BuildInfo.profile, 'Runner'), 'Profile'); + expect(info.buildConfigurationFor(BuildInfo.release, 'Runner'), 'Release'); + }); + + testWithoutContext('scheme for project with custom schemes is matched against flavor', () { + final XcodeProjectInfo info = XcodeProjectInfo( + ['Runner'], + ['Debug (Free)', 'Debug (Paid)', 'Release (Free)', 'Release (Paid)'], + ['Free', 'Paid'], + ); + + expect(info.schemeFor(const BuildInfo(BuildMode.debug, '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.debug, null)), isNull); + expect(info.schemeFor(const BuildInfo(BuildMode.debug, 'unknown')), isNull); + }); + + testWithoutContext('build configuration for project with custom schemes is matched against BuildMode and flavor', () { + final XcodeProjectInfo info = XcodeProjectInfo( + ['Runner'], + ['debug (free)', 'Debug paid', 'profile - Free', 'Profile-Paid', 'release - Free', 'Release-Paid'], + ['Free', 'Paid'], + ); + + 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.profile, 'FREE'), 'Free'), 'profile - Free'); + expect(info.buildConfigurationFor(const BuildInfo(BuildMode.release, 'paid'), 'Paid'), 'Release-Paid'); + }); + + testWithoutContext('build configuration for project with inconsistent naming is null', () { + final XcodeProjectInfo info = XcodeProjectInfo( + ['Runner'], + ['Debug-F', 'Dbg Paid', 'Rel Free', 'Release Full'], + ['Free', 'Paid'], + ); + expect(info.buildConfigurationFor(const BuildInfo(BuildMode.debug, 'Free'), 'Free'), null); + expect(info.buildConfigurationFor(const BuildInfo(BuildMode.profile, 'Free'), 'Free'), null); + expect(info.buildConfigurationFor(const BuildInfo(BuildMode.release, 'Paid'), 'Paid'), null); + }); + group('environmentVariablesAsXcodeBuildSettings', () { FakePlatform platform; setUp(() { platform = fakePlatform('ignored'); }); - testUsingContext('environment variables as Xcode build settings', () { + testWithoutContext('environment variables as Xcode build settings', () { platform.environment = Map.unmodifiable({ 'Ignored': 'Bogus', 'FLUTTER_NOT_XCODE': 'Bogus', 'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual', 'FLUTTER_XCODE_ARCHS': 'arm64' }); - final List environmentVariablesAsBuildSettings = environmentVariablesAsXcodeBuildSettings(); + final List environmentVariablesAsBuildSettings = environmentVariablesAsXcodeBuildSettings(platform); expect(environmentVariablesAsBuildSettings, ['CODE_SIGN_STYLE=Manual', 'ARCHS=arm64']); - }, overrides: { - Platform: () => platform }); }); @@ -729,4 +754,9 @@ FakePlatform fakePlatform(String name) { class MockLocalEngineArtifacts extends Mock implements LocalEngineArtifacts {} 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; +} diff --git a/packages/flutter_tools/test/general.shard/macos/xcode_test.dart b/packages/flutter_tools/test/general.shard/macos/xcode_test.dart index 90407de0ea8..93b1bc19488 100644 --- a/packages/flutter_tools/test/general.shard/macos/xcode_test.dart +++ b/packages/flutter_tools/test/general.shard/macos/xcode_test.dart @@ -2,7 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // 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/logger.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart'; import 'package:flutter_tools/src/macos/xcode.dart'; import 'package:mockito/mockito.dart'; @@ -17,184 +20,161 @@ class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterprete class MockPlatform extends Mock implements Platform {} void main() { - group('Xcode', () { - MockProcessManager mockProcessManager; - Xcode xcode; - MockXcodeProjectInterpreter mockXcodeProjectInterpreter; - MockPlatform mockPlatform; + ProcessManager processManager; + Xcode xcode; + MockXcodeProjectInterpreter mockXcodeProjectInterpreter; + MockPlatform platform; + Logger logger; + FileSystem fileSystem; - setUp(() { - mockProcessManager = MockProcessManager(); - mockXcodeProjectInterpreter = MockXcodeProjectInterpreter(); - xcode = Xcode(); - mockPlatform = MockPlatform(); - }); + setUp(() { + logger = MockLogger(); + fileSystem = MemoryFileSystem(); + processManager = MockProcessManager(); + mockXcodeProjectInterpreter = MockXcodeProjectInterpreter(); + platform = MockPlatform(); + xcode = Xcode( + logger: logger, + platform: platform, + fileSystem: fileSystem, + processManager: processManager, + xcodeProjectInterpreter: mockXcodeProjectInterpreter, + ); + }); - testUsingContext('xcodeSelectPath returns null when xcode-select is not installed', () { - when(mockProcessManager.runSync(['/usr/bin/xcode-select', '--print-path'])) - .thenThrow(const ProcessException('/usr/bin/xcode-select', ['--print-path'])); - expect(xcode.xcodeSelectPath, isNull); - when(mockProcessManager.runSync(['/usr/bin/xcode-select', '--print-path'])) - .thenThrow(ArgumentError('Invalid argument(s): Cannot find executable for /usr/bin/xcode-select')); - expect(xcode.xcodeSelectPath, isNull); - }, overrides: { - ProcessManager: () => mockProcessManager, - }); + testWithoutContext('xcodeSelectPath returns null when xcode-select is not installed', () { + when(processManager.runSync(['/usr/bin/xcode-select', '--print-path'])) + .thenThrow(const ProcessException('/usr/bin/xcode-select', ['--print-path'])); + expect(xcode.xcodeSelectPath, isNull); + when(processManager.runSync(['/usr/bin/xcode-select', '--print-path'])) + .thenThrow(ArgumentError('Invalid argument(s): Cannot find executable for /usr/bin/xcode-select')); - testUsingContext('xcodeSelectPath returns path when xcode-select is installed', () { - const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer'; - when(mockProcessManager.runSync(['/usr/bin/xcode-select', '--print-path'])) - .thenReturn(ProcessResult(1, 0, xcodePath, '')); - expect(xcode.xcodeSelectPath, xcodePath); - }, overrides: { - ProcessManager: () => mockProcessManager, - }); + expect(xcode.xcodeSelectPath, isNull); + }); - testUsingContext('xcodeVersionSatisfactory is false when version is less than minimum', () { - when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); - when(mockXcodeProjectInterpreter.majorVersion).thenReturn(9); - when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0); - expect(xcode.isVersionSatisfactory, isFalse); - }, overrides: { - XcodeProjectInterpreter: () => mockXcodeProjectInterpreter, - }); - - testUsingContext('xcodeVersionSatisfactory is false when xcodebuild tools are not installed', () { - when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false); - expect(xcode.isVersionSatisfactory, isFalse); - }, overrides: { - XcodeProjectInterpreter: () => mockXcodeProjectInterpreter, - }); - - testUsingContext('xcodeVersionSatisfactory is true when version meets minimum', () { - when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); - when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10); - when(mockXcodeProjectInterpreter.minorVersion).thenReturn(2); - expect(xcode.isVersionSatisfactory, isTrue); - }, overrides: { - XcodeProjectInterpreter: () => mockXcodeProjectInterpreter, - }); - - testUsingContext('xcodeVersionSatisfactory is true when major version exceeds minimum', () { - when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); - when(mockXcodeProjectInterpreter.majorVersion).thenReturn(11); - when(mockXcodeProjectInterpreter.minorVersion).thenReturn(2); - expect(xcode.isVersionSatisfactory, isTrue); - }, overrides: { - XcodeProjectInterpreter: () => mockXcodeProjectInterpreter, - }); - - testUsingContext('xcodeVersionSatisfactory is true when minor version exceeds minimum', () { - when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); - when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10); - when(mockXcodeProjectInterpreter.minorVersion).thenReturn(3); - expect(xcode.isVersionSatisfactory, isTrue); - }, overrides: { - XcodeProjectInterpreter: () => mockXcodeProjectInterpreter, - }); - - testUsingContext('isInstalledAndMeetsVersionCheck is false when not macOS', () { - when(mockPlatform.isMacOS).thenReturn(false); - expect(xcode.isInstalledAndMeetsVersionCheck, isFalse); - }, overrides: { - XcodeProjectInterpreter: () => mockXcodeProjectInterpreter, - Platform: () => mockPlatform, - }); - - testUsingContext('isInstalledAndMeetsVersionCheck is false when not installed', () { - when(mockPlatform.isMacOS).thenReturn(true); - - const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer'; - when(mockProcessManager.runSync(['/usr/bin/xcode-select', '--print-path'])) + testWithoutContext('xcodeSelectPath returns path when xcode-select is installed', () { + const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer'; + when(processManager.runSync(['/usr/bin/xcode-select', '--print-path'])) .thenReturn(ProcessResult(1, 0, xcodePath, '')); - when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false); - expect(xcode.isInstalledAndMeetsVersionCheck, isFalse); - }, overrides: { - XcodeProjectInterpreter: () => mockXcodeProjectInterpreter, - Platform: () => mockPlatform, - ProcessManager: () => mockProcessManager, - }); + expect(xcode.xcodeSelectPath, xcodePath); + }); - testUsingContext('isInstalledAndMeetsVersionCheck is false when no xcode-select', () { - when(mockPlatform.isMacOS).thenReturn(true); + testWithoutContext('xcodeVersionSatisfactory is false when version is less than minimum', () { + when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); + when(mockXcodeProjectInterpreter.majorVersion).thenReturn(9); + when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0); - when(mockProcessManager.runSync(['/usr/bin/xcode-select', '--print-path'])) - .thenReturn(ProcessResult(1, 127, '', 'ERROR')); + expect(xcode.isVersionSatisfactory, isFalse); + }); - when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); - when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10); - when(mockXcodeProjectInterpreter.minorVersion).thenReturn(2); + testWithoutContext('xcodeVersionSatisfactory is false when xcodebuild tools are not installed', () { + when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false); - expect(xcode.isInstalledAndMeetsVersionCheck, isFalse); - }, overrides: { - XcodeProjectInterpreter: () => mockXcodeProjectInterpreter, - Platform: () => mockPlatform, - ProcessManager: () => mockProcessManager, - }); + expect(xcode.isVersionSatisfactory, isFalse); + }); - testUsingContext('isInstalledAndMeetsVersionCheck is false when version not satisfied', () { - when(mockPlatform.isMacOS).thenReturn(true); + testWithoutContext('xcodeVersionSatisfactory is true when version meets minimum', () { + when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); + when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10); + when(mockXcodeProjectInterpreter.minorVersion).thenReturn(2); - const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer'; - when(mockProcessManager.runSync(['/usr/bin/xcode-select', '--print-path'])) - .thenReturn(ProcessResult(1, 0, xcodePath, '')); + expect(xcode.isVersionSatisfactory, isTrue); + }); - when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); - when(mockXcodeProjectInterpreter.majorVersion).thenReturn(9); - when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0); - expect(xcode.isInstalledAndMeetsVersionCheck, isFalse); - }, overrides: { - XcodeProjectInterpreter: () => mockXcodeProjectInterpreter, - Platform: () => mockPlatform, - ProcessManager: () => mockProcessManager, - }); + testWithoutContext('xcodeVersionSatisfactory is true when major version exceeds minimum', () { + when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); + when(mockXcodeProjectInterpreter.majorVersion).thenReturn(11); + when(mockXcodeProjectInterpreter.minorVersion).thenReturn(2); - testUsingContext('isInstalledAndMeetsVersionCheck is true when macOS and installed and version is satisfied', () { - when(mockPlatform.isMacOS).thenReturn(true); + expect(xcode.isVersionSatisfactory, isTrue); + }); - const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer'; - when(mockProcessManager.runSync(['/usr/bin/xcode-select', '--print-path'])) - .thenReturn(ProcessResult(1, 0, xcodePath, '')); + testWithoutContext('xcodeVersionSatisfactory is true when minor version exceeds minimum', () { + when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); + when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10); + when(mockXcodeProjectInterpreter.minorVersion).thenReturn(3); - when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); - when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10); - when(mockXcodeProjectInterpreter.minorVersion).thenReturn(2); - expect(xcode.isInstalledAndMeetsVersionCheck, isTrue); - }, overrides: { - XcodeProjectInterpreter: () => mockXcodeProjectInterpreter, - Platform: () => mockPlatform, - ProcessManager: () => mockProcessManager, - }); + expect(xcode.isVersionSatisfactory, isTrue); + }); - testUsingContext('eulaSigned is false when clang is not installed', () { - when(mockProcessManager.runSync(['/usr/bin/xcrun', 'clang'])) - .thenThrow(const ProcessException('/usr/bin/xcrun', ['clang'])); - expect(xcode.eulaSigned, isFalse); - }, overrides: { - ProcessManager: () => mockProcessManager, - }); + testWithoutContext('isInstalledAndMeetsVersionCheck is false when not macOS', () { + when(platform.isMacOS).thenReturn(false); - testUsingContext('eulaSigned is false when clang output indicates EULA not yet accepted', () { - when(mockProcessManager.runSync(['/usr/bin/xcrun', 'clang'])) - .thenReturn(ProcessResult(1, 1, '', 'Xcode EULA has not been accepted.\nLaunch Xcode and accept the license.')); - expect(xcode.eulaSigned, isFalse); - }, overrides: { - ProcessManager: () => mockProcessManager, - }); + expect(xcode.isInstalledAndMeetsVersionCheck, isFalse); + }); - testUsingContext('eulaSigned is true when clang output indicates EULA has been accepted', () { - when(mockProcessManager.runSync(['/usr/bin/xcrun', 'clang'])) - .thenReturn(ProcessResult(1, 1, '', 'clang: error: no input files')); - expect(xcode.eulaSigned, isTrue); - }, overrides: { - ProcessManager: () => mockProcessManager, - }); + testWithoutContext('isInstalledAndMeetsVersionCheck is false when not installed', () { + when(platform.isMacOS).thenReturn(true); + const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer'; + when(processManager.runSync(['/usr/bin/xcode-select', '--print-path'])) + .thenReturn(ProcessResult(1, 0, xcodePath, '')); + when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false); - testUsingContext('SDK name', () { - expect(getNameForSdk(SdkType.iPhone), 'iphoneos'); - expect(getNameForSdk(SdkType.iPhoneSimulator), 'iphonesimulator'); - expect(getNameForSdk(SdkType.macOS), 'macosx'); - }); + expect(xcode.isInstalledAndMeetsVersionCheck, isFalse); + }); + + testWithoutContext('isInstalledAndMeetsVersionCheck is false when no xcode-select', () { + when(platform.isMacOS).thenReturn(true); + when(processManager.runSync(['/usr/bin/xcode-select', '--print-path'])) + .thenReturn(ProcessResult(1, 127, '', 'ERROR')); + when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); + when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10); + when(mockXcodeProjectInterpreter.minorVersion).thenReturn(2); + + expect(xcode.isInstalledAndMeetsVersionCheck, isFalse); + }); + + testWithoutContext('isInstalledAndMeetsVersionCheck is false when version not satisfied', () { + when(platform.isMacOS).thenReturn(true); + const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer'; + when(processManager.runSync(['/usr/bin/xcode-select', '--print-path'])) + .thenReturn(ProcessResult(1, 0, xcodePath, '')); + when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); + when(mockXcodeProjectInterpreter.majorVersion).thenReturn(9); + when(mockXcodeProjectInterpreter.minorVersion).thenReturn(0); + + expect(xcode.isInstalledAndMeetsVersionCheck, isFalse); + }); + + testWithoutContext('isInstalledAndMeetsVersionCheck is true when macOS and installed and version is satisfied', () { + when(platform.isMacOS).thenReturn(true); + const String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer'; + when(processManager.runSync(['/usr/bin/xcode-select', '--print-path'])) + .thenReturn(ProcessResult(1, 0, xcodePath, '')); + when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true); + when(mockXcodeProjectInterpreter.majorVersion).thenReturn(10); + when(mockXcodeProjectInterpreter.minorVersion).thenReturn(2); + + expect(xcode.isInstalledAndMeetsVersionCheck, isTrue); + }); + + testWithoutContext('eulaSigned is false when clang is not installed', () { + when(processManager.runSync(['/usr/bin/xcrun', 'clang'])) + .thenThrow(const ProcessException('/usr/bin/xcrun', ['clang'])); + + expect(xcode.eulaSigned, isFalse); + }); + + testWithoutContext('eulaSigned is false when clang output indicates EULA not yet accepted', () { + when(processManager.runSync(['/usr/bin/xcrun', 'clang'])) + .thenReturn(ProcessResult(1, 1, '', 'Xcode EULA has not been accepted.\nLaunch Xcode and accept the license.')); + + expect(xcode.eulaSigned, isFalse); + }); + + testWithoutContext('eulaSigned is true when clang output indicates EULA has been accepted', () { + when(processManager.runSync(['/usr/bin/xcrun', 'clang'])) + .thenReturn(ProcessResult(1, 1, '', 'clang: error: no input files')); + + expect(xcode.eulaSigned, isTrue); + }); + + testWithoutContext('SDK name', () { + expect(getNameForSdk(SdkType.iPhone), 'iphoneos'); + expect(getNameForSdk(SdkType.iPhoneSimulator), 'iphonesimulator'); + expect(getNameForSdk(SdkType.macOS), 'macosx'); }); } + +class MockLogger extends Mock implements Logger {}