diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index 2dbca80d49..3c961e7c97 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -86,7 +86,7 @@ class Xcode { String get xcodeSelectPath { if (_xcodeSelectPath == null) { try { - _xcodeSelectPath = runSync(['/usr/bin/xcode-select', '--print-path'])?.trim(); + _xcodeSelectPath = processManager.runSync(['/usr/bin/xcode-select', '--print-path']).stdout.trim(); } on ProcessException { // Ignore: return null below. } @@ -125,7 +125,7 @@ class Xcode { String get xcodeVersionText { if (_xcodeVersionText == null) { try { - _xcodeVersionText = runSync(['/usr/bin/xcodebuild', '-version']).replaceAll('\n', ', '); + _xcodeVersionText = processManager.runSync(['/usr/bin/xcodebuild', '-version']).stdout.replaceAll('\n', ', '); } on ProcessException { // Ignore: return null below. } @@ -155,10 +155,15 @@ class Xcode { } Future getAvailableDevices() async { - final RunResult result = await runAsync(['/usr/bin/instruments', '-s', 'devices']); - if (result.exitCode != 0) + try { + final ProcessResult result = await processManager.run( + ['/usr/bin/instruments', '-s', 'devices']); + if (result.exitCode != 0) + throw new ToolExit('/usr/bin/instruments returned an error:\n${result.stderr}'); + return result.stdout; + } on ProcessException { throw new ToolExit('Failed to invoke /usr/bin/instruments. Is Xcode installed?'); - return result.stdout; + } } } diff --git a/packages/flutter_tools/test/ios/mac_test.dart b/packages/flutter_tools/test/ios/mac_test.dart index 69d8dc2947..fbc5603fcb 100644 --- a/packages/flutter_tools/test/ios/mac_test.dart +++ b/packages/flutter_tools/test/ios/mac_test.dart @@ -7,13 +7,14 @@ import 'dart:async'; import 'package:file/file.dart'; import 'package:flutter_tools/src/application_package.dart'; import 'package:flutter_tools/src/base/file_system.dart'; -import 'package:flutter_tools/src/base/io.dart' show ProcessResult; +import 'package:flutter_tools/src/base/io.dart' show ProcessException, ProcessResult; import 'package:flutter_tools/src/ios/mac.dart'; import 'package:mockito/mockito.dart'; import 'package:platform/platform.dart'; import 'package:process/process.dart'; import 'package:test/test.dart'; +import '../src/common.dart'; import '../src/context.dart'; class MockProcessManager extends Mock implements ProcessManager {} @@ -65,6 +66,97 @@ void main() { }); }); + group('Xcode', () { + MockProcessManager mockProcessManager; + Xcode xcode; + + setUp(() { + mockProcessManager = new MockProcessManager(); + xcode = new Xcode(); + }); + + 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', const ['--print-path'])); + expect(xcode.xcodeSelectPath, isNull); + }, overrides: { + ProcessManager: () => mockProcessManager, + }); + + testUsingContext('xcodeSelectPath returns path when xcode-select is installed', () { + final String xcodePath = '/Applications/Xcode8.0.app/Contents/Developer'; + when(mockProcessManager.runSync(['/usr/bin/xcode-select', '--print-path'])) + .thenReturn(new ProcessResult(1, 0, xcodePath, '')); + expect(xcode.xcodeSelectPath, xcodePath); + }, overrides: { + ProcessManager: () => mockProcessManager, + }); + + testUsingContext('xcodeVersionText returns null when xcodebuild is not installed', () { + when(mockProcessManager.runSync(['/usr/bin/xcodebuild', '-version'])) + .thenThrow(const ProcessException('/usr/bin/xcodebuild', const ['-version'])); + expect(xcode.xcodeVersionText, isNull); + }, overrides: { + ProcessManager: () => mockProcessManager, + }); + + testUsingContext('xcodeVersionText returns null when xcodebuild is not installed', () { + when(mockProcessManager.runSync(['/usr/bin/xcodebuild', '-version'])) + .thenReturn(new ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', '')); + expect(xcode.xcodeVersionText, 'Xcode 8.3.3, Build version 8E3004b'); + }, overrides: { + ProcessManager: () => mockProcessManager, + }); + + testUsingContext('eulaSigned is false when clang is not installed', () { + when(mockProcessManager.runSync(['/usr/bin/xcrun', 'clang'])) + .thenThrow(const ProcessException('/usr/bin/xcrun', const ['clang'])); + expect(xcode.eulaSigned, isFalse); + }, overrides: { + ProcessManager: () => mockProcessManager, + }); + + testUsingContext('eulaSigned is false when clang output indicates EULA not yet accepted', () { + when(mockProcessManager.runSync(['/usr/bin/xcrun', 'clang'])) + .thenReturn(new ProcessResult(1, 1, '', 'Xcode EULA has not been accepted.\nLaunch Xcode and accept the license.')); + expect(xcode.eulaSigned, isFalse); + }, overrides: { + ProcessManager: () => mockProcessManager, + }); + + testUsingContext('eulaSigned is true when clang output indicates EULA has been accepted', () { + when(mockProcessManager.runSync(['/usr/bin/xcrun', 'clang'])) + .thenReturn(new ProcessResult(1, 1, '', 'clang: error: no input files')); + expect(xcode.eulaSigned, isTrue); + }, overrides: { + ProcessManager: () => mockProcessManager, + }); + + testUsingContext('getAvailableDevices throws ToolExit when instruments is not installed', () async { + when(mockProcessManager.run(['/usr/bin/instruments', '-s', 'devices'])) + .thenThrow(const ProcessException('/usr/bin/instruments', const ['-s', 'devices'])); + expect(() async => await xcode.getAvailableDevices(), throwsToolExit()); + }, overrides: { + ProcessManager: () => mockProcessManager, + }); + + testUsingContext('getAvailableDevices throws ToolExit when instruments returns non-zero', () async { + when(mockProcessManager.run(['/usr/bin/instruments', '-s', 'devices'])) + .thenReturn(new ProcessResult(1, 1, '', 'Sad today')); + expect(() async => await xcode.getAvailableDevices(), throwsToolExit()); + }, overrides: { + ProcessManager: () => mockProcessManager, + }); + + testUsingContext('getAvailableDevices returns instruments output when installed', () async { + when(mockProcessManager.run(['/usr/bin/instruments', '-s', 'devices'])) + .thenReturn(new ProcessResult(1, 0, 'Known Devices:\niPhone 6s (10.3.3) [foo]', '')); + expect(await xcode.getAvailableDevices(), 'Known Devices:\niPhone 6s (10.3.3) [foo]'); + }, overrides: { + ProcessManager: () => mockProcessManager, + }); + }); + group('Diagnose Xcode build failure', () { BuildableIOSApp app;