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

diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart
index 4135286..ce53e93 100644
--- a/packages/flutter_tools/lib/src/context_runner.dart
+++ b/packages/flutter_tools/lib/src/context_runner.dart
@@ -145,8 +145,20 @@
       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 d5b91f4..bda9b96 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -458,7 +458,7 @@
   // 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 b3c7f74..4f3873b 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/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 @@
 
 /// Interpreter of Xcode projects.
 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 final RegExp _versionRegex = RegExp(r'Xcode ([0-9.]+)');
 
   void _updateVersion() {
-    if (!globals.platform.isMacOS || !globals.fs.file(_executable).existsSync()) {
+    if (!_platform.isMacOS || !_fileSystem.file(_executable).existsSync()) {
       return;
     }
     try {
-      final RunResult result = processUtils.runSync(
+      final RunResult result = _processUtils.runSync(
         <String>[_executable, '-version'],
       );
       if (result.exitCode != 0) {
@@ -283,26 +304,26 @@
     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<String> showBuildSettingsCommand = <String>[
       _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 @@
           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 <String, String>{};
     } finally {
       status.stop();
@@ -325,7 +346,7 @@
   }
 
   void cleanWorkspace(String workspacePath, String scheme) {
-    processUtils.runSync(<String>[
+    _processUtils.runSync(<String>[
       _executable,
       '-workspace',
       workspacePath,
@@ -333,8 +354,8 @@
       scheme,
       '-quiet',
       'clean',
-      ...environmentVariablesAsXcodeBuildSettings()
-    ], workingDirectory: globals.fs.currentDirectory.path);
+      ...environmentVariablesAsXcodeBuildSettings(_platform)
+    ], workingDirectory: _fileSystem.currentDirectory.path);
   }
 
   Future<XcodeProjectInfo> getInfo(String projectPath, {String projectFilename}) async {
@@ -342,7 +363,7 @@
     // * -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(
       <String>[
         _executable,
         '-list',
@@ -363,9 +384,9 @@
 /// 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<String> environmentVariablesAsXcodeBuildSettings() {
+List<String> environmentVariablesAsXcodeBuildSettings(Platform platform) {
   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);
   }).expand<String>((MapEntry<String, String> 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 d0c528a..a18fd1b 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 @@
       '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 51c9f71..5ab378d 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 @@
   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(
           <String>['/usr/bin/xcode-select', '--print-path'],
         ).stdout.trim();
       } on ProcessException {
@@ -64,21 +86,21 @@
     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(
           <String>['/usr/bin/xcrun', 'clang'],
         );
         if (result.stdout != null && result.stdout.contains('license')) {
@@ -103,7 +125,7 @@
       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(
           <String>['/usr/bin/xcrun', 'simctl', 'list'],
         );
         _isSimctlInstalled = result.stderr == null || result.stderr == '';
@@ -115,7 +137,7 @@
   }
 
   bool get isVersionSatisfactory {
-    if (!xcodeProjectInterpreter.isInstalled) {
+    if (!_xcodeProjectInterpreter.isInstalled) {
       return false;
     }
     if (majorVersion > kXcodeRequiredVersionMajor) {
@@ -128,14 +150,14 @@
   }
 
   Future<RunResult> cc(List<String> args) {
-    return processUtils.run(
+    return _processUtils.run(
       <String>['xcrun', 'cc', ...args],
       throwOnError: true,
     );
   }
 
   Future<RunResult> clang(List<String> args) {
-    return processUtils.run(
+    return _processUtils.run(
       <String>['xcrun', 'clang', ...args],
       throwOnError: true,
     );
@@ -143,7 +165,7 @@
 
   Future<String> sdkLocation(SdkType sdk) async {
     assert(sdk != null);
-    final RunResult runResult = await processUtils.run(
+    final RunResult runResult = await _processUtils.run(
       <String>['xcrun', '--sdk', getNameForSdk(sdk), '--show-sdk-path'],
       throwOnError: true,
     );
@@ -158,10 +180,10 @@
       return null;
     }
     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(
-      (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 8ffead4..a84d944 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: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 @@
 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: <Type, Generator>{
-        Platform: () => macOS,
-        FileSystem: () => fs,
-        ProcessManager: () => mockProcessManager,
-      });
-    }
+  testWithoutContext('xcodebuild versionText returns null when xcodebuild is not installed', () {
+    when(processManager.runSync(<String>[xcodebuild, '-version']))
+        .thenThrow(const ProcessException(xcodebuild, <String>['-version']));
 
-    testUsingOsxContext('versionText returns null when xcodebuild is not installed', () {
-      when(mockProcessManager.runSync(<String>[xcodebuild, '-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', () {
-      when(mockProcessManager.runSync(<String>[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(<String>[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(<String>[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(<String>[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(<String>[xcodebuild, '-version']))
-          .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', ''));
-      expect(xcodeProjectInterpreter.majorVersion, isNull);
-    });
-
-    testUsingOsxContext('minorVersion returns minor version', () {
-      when(mockProcessManager.runSync(<String>[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(<String>[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(<String>[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: <Type, Generator>{
-      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(<String>[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(<String>[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(<String>[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 <String, String>{});
-    });
-
-    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 <String, String>{});
-      // 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: <Type, Generator>{
-      Platform: () => macOS,
-      FileSystem: () => fs,
-      ProcessManager: () => mockProcessManager,
-    });
-
-    testUsingOsxContext('build settings contains Flutter Xcode environment variables', () async {
-      macOS.environment = Map<String, String>.unmodifiable(<String, String>{
-        'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual',
-        'FLUTTER_XCODE_ARCHS': 'arm64'
-      });
-      when(mockProcessManager.runSync(<String>[
-        xcodebuild,
-        '-project',
-        macOS.pathSeparator,
-        '-target',
+  testWithoutContext('xcodebuild versionText returns null when xcodebuild is not fully installed', () {
+    when(processManager.runSync(<String>[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 <String, String>{});
-    });
+      ),
+    );
 
-    testUsingOsxContext('clean contains Flutter Xcode environment variables', () async {
-      macOS.environment = Map<String, String>.unmodifiable(<String, String>{
-        '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<dynamic> captured = verify(mockProcessManager.runSync(
-        captureAny,
-        workingDirectory: anyNamed('workingDirectory'),
-        environment: anyNamed('environment'))).captured;
-      expect(captured.first, <String>[
-        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(<String>[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: <Type, Generator>{
-        Platform: () => macOS,
-        FileSystem: () => fs,
-        ProcessManager: () => mockProcessManager,
-      });
-    }
-
-    testUsingOsxContext('getInfo returns something when xcodebuild -list succeeds', () async {
-      const String workingDirectory = '/';
-      when(mockProcessManager.run(
-        <String>[xcodebuild, '-list'],
-        environment: anyNamed('environment'),
-        workingDirectory: workingDirectory),
-      ).thenAnswer((_) {
-        return Future<ProcessResult>.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(
-        <String>[xcodebuild, '-list'],
-        environment: anyNamed('environment'),
-        workingDirectory: workingDirectory),
-      ).thenAnswer((_) {
-        return Future<ProcessResult>.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(<String>[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(<String>[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(<String>[xcodebuild, '-version']))
+        .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', ''));
+
+    expect(xcodeProjectInterpreter.majorVersion, isNull);
+  });
+
+  testWithoutContext('xcodebuild inorVersion returns minor version', () {
+    when(processManager.runSync(<String>[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(<String>[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(<String>[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(<String>[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(<String>[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(<String>[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 <String, String>{});
+  });
+
+  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 <String, String>{});
+    // 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<String, String>.unmodifiable(<String, String>{
+      'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual',
+      'FLUTTER_XCODE_ARCHS': 'arm64'
+    });
+    when(processManager.runSync(<String>[
+      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 <String, String>{});
+  });
+
+  testWithoutContext('xcodebuild clean contains Flutter Xcode environment variables', () async {
+    platform.environment = Map<String, String>.unmodifiable(<String, String>{
+      '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<dynamic> captured = verify(processManager.runSync(
+      captureAny,
+      workingDirectory: anyNamed('workingDirectory'),
+      environment: anyNamed('environment'))).captured;
+
+    expect(captured.first, <String>[
+      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(
+      <String>[xcodebuild, '-list'],
+      environment: anyNamed('environment'),
+      workingDirectory: workingDirectory),
+    ).thenAnswer((_) {
+      return Future<ProcessResult>.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(
+      <String>[xcodebuild, '-list'],
+      environment: anyNamed('environment'),
+      workingDirectory: workingDirectory),
+    ).thenAnswer((_) {
+      return Future<ProcessResult>.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 @@
         Runner
 
 ''';
-      final XcodeProjectInfo info = XcodeProjectInfo.fromXcodeBuildOutput(output);
-      expect(info.targets, <String>['Runner']);
-      expect(info.schemes, <String>['Runner']);
-      expect(info.buildConfigurations, <String>['Debug', 'Release']);
-    });
-    test('properties from project with custom schemes can be parsed', () {
-      const String output = '''
+    final XcodeProjectInfo info = XcodeProjectInfo.fromXcodeBuildOutput(output);
+    expect(info.targets, <String>['Runner']);
+    expect(info.schemes, <String>['Runner']);
+    expect(info.buildConfigurations, <String>['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 @@
         Paid
 
 ''';
-      final XcodeProjectInfo info = XcodeProjectInfo.fromXcodeBuildOutput(output);
-      expect(info.targets, <String>['Runner']);
-      expect(info.schemes, <String>['Free', 'Paid']);
-      expect(info.buildConfigurations, <String>['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(<String>['Runner'], <String>['Debug', 'Release'], <String>['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(<String>['Runner'], <String>['Debug', 'Profile', 'Release'], <String>['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(
-        <String>['Runner'],
-        <String>['Debug (Free)', 'Debug (Paid)', 'Release (Free)', 'Release (Paid)'],
-        <String>['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(
-        <String>['Runner'],
-        <String>['debug (free)', 'Debug paid', 'profile - Free', 'Profile-Paid', 'release - Free', 'Release-Paid'],
-        <String>['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(
-        <String>['Runner'],
-        <String>['Debug-F', 'Dbg Paid', 'Rel Free', 'Release Full'],
-        <String>['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, <String>['Runner']);
+    expect(info.schemes, <String>['Free', 'Paid']);
+    expect(info.buildConfigurations, <String>['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(<String>['Runner'], <String>['Debug', 'Release'], <String>['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(<String>['Runner'], <String>['Debug', 'Profile', 'Release'], <String>['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(
+      <String>['Runner'],
+      <String>['Debug (Free)', 'Debug (Paid)', 'Release (Free)', 'Release (Paid)'],
+      <String>['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(
+      <String>['Runner'],
+      <String>['debug (free)', 'Debug paid', 'profile - Free', 'Profile-Paid', 'release - Free', 'Release-Paid'],
+      <String>['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(
+      <String>['Runner'],
+      <String>['Debug-F', 'Dbg Paid', 'Rel Free', 'Release Full'],
+      <String>['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<String, String>.unmodifiable(<String, String>{
         'Ignored': 'Bogus',
         'FLUTTER_NOT_XCODE': 'Bogus',
         'FLUTTER_XCODE_CODE_SIGN_STYLE': 'Manual',
         'FLUTTER_XCODE_ARCHS': 'arm64'
       });
-      final List<String> environmentVariablesAsBuildSettings = environmentVariablesAsXcodeBuildSettings();
+      final List<String> environmentVariablesAsBuildSettings = environmentVariablesAsXcodeBuildSettings(platform);
       expect(environmentVariablesAsBuildSettings, <String>['CODE_SIGN_STYLE=Manual', 'ARCHS=arm64']);
-    }, overrides: <Type, Generator>{
-      Platform: () => platform
     });
   });
 
@@ -729,4 +754,9 @@
 
 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 90407de..93b1bc1 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 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(<String>['/usr/bin/xcode-select', '--print-path']))
-          .thenThrow(const ProcessException('/usr/bin/xcode-select', <String>['--print-path']));
-      expect(xcode.xcodeSelectPath, isNull);
-      when(mockProcessManager.runSync(<String>['/usr/bin/xcode-select', '--print-path']))
-          .thenThrow(ArgumentError('Invalid argument(s): Cannot find executable for /usr/bin/xcode-select'));
-      expect(xcode.xcodeSelectPath, isNull);
-    }, overrides: <Type, Generator>{
-      ProcessManager: () => mockProcessManager,
-    });
+  testWithoutContext('xcodeSelectPath returns null when xcode-select is not installed', () {
+    when(processManager.runSync(<String>['/usr/bin/xcode-select', '--print-path']))
+        .thenThrow(const ProcessException('/usr/bin/xcode-select', <String>['--print-path']));
+    expect(xcode.xcodeSelectPath, isNull);
+    when(processManager.runSync(<String>['/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(<String>['/usr/bin/xcode-select', '--print-path']))
-          .thenReturn(ProcessResult(1, 0, xcodePath, ''));
-      expect(xcode.xcodeSelectPath, xcodePath);
-    }, overrides: <Type, Generator>{
-      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: <Type, Generator>{
-      XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
-    });
-
-    testUsingContext('xcodeVersionSatisfactory is false when xcodebuild tools are not installed', () {
-      when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false);
-      expect(xcode.isVersionSatisfactory, isFalse);
-    }, overrides: <Type, Generator>{
-      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: <Type, Generator>{
-      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: <Type, Generator>{
-      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: <Type, Generator>{
-      XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
-    });
-
-    testUsingContext('isInstalledAndMeetsVersionCheck is false when not macOS', () {
-      when(mockPlatform.isMacOS).thenReturn(false);
-      expect(xcode.isInstalledAndMeetsVersionCheck, isFalse);
-    }, overrides: <Type, Generator>{
-      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(<String>['/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(<String>['/usr/bin/xcode-select', '--print-path']))
         .thenReturn(ProcessResult(1, 0, xcodePath, ''));
 
-      when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false);
-      expect(xcode.isInstalledAndMeetsVersionCheck, isFalse);
-    }, overrides: <Type, Generator>{
-      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(<String>['/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: <Type, Generator>{
-      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(<String>['/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: <Type, Generator>{
-      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(<String>['/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: <Type, Generator>{
-      XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
-      Platform: () => mockPlatform,
-      ProcessManager: () => mockProcessManager,
-    });
+    expect(xcode.isVersionSatisfactory, isTrue);
+  });
 
-    testUsingContext('eulaSigned is false when clang is not installed', () {
-      when(mockProcessManager.runSync(<String>['/usr/bin/xcrun', 'clang']))
-          .thenThrow(const ProcessException('/usr/bin/xcrun', <String>['clang']));
-      expect(xcode.eulaSigned, isFalse);
-    }, overrides: <Type, Generator>{
-      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(<String>['/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: <Type, Generator>{
-      ProcessManager: () => mockProcessManager,
-    });
+    expect(xcode.isInstalledAndMeetsVersionCheck, isFalse);
+  });
 
-    testUsingContext('eulaSigned is true when clang output indicates EULA has been accepted', () {
-      when(mockProcessManager.runSync(<String>['/usr/bin/xcrun', 'clang']))
-          .thenReturn(ProcessResult(1, 1, '', 'clang: error: no input files'));
-      expect(xcode.eulaSigned, isTrue);
-    }, overrides: <Type, Generator>{
-      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(<String>['/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(<String>['/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(<String>['/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(<String>['/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(<String>['/usr/bin/xcrun', 'clang']))
+        .thenThrow(const ProcessException('/usr/bin/xcrun', <String>['clang']));
+
+    expect(xcode.eulaSigned, isFalse);
+  });
+
+  testWithoutContext('eulaSigned is false when clang output indicates EULA not yet accepted', () {
+    when(processManager.runSync(<String>['/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(<String>['/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 {}