xcodeproj_test.dart 21 KB
Newer Older
1 2 3
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
4

5 6
import 'dart:async';

7
import 'package:file/memory.dart';
8
import 'package:flutter_tools/src/artifacts.dart';
9 10
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
11 12
import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart';
13
import 'package:flutter_tools/src/project.dart';
14 15 16 17
import 'package:mockito/mockito.dart';
import 'package:platform/platform.dart';
import 'package:process/process.dart';

18
import '../src/common.dart';
19
import '../src/context.dart';
20
import '../src/pubspec_schema.dart';
21 22

const String xcodebuild = '/usr/bin/xcodebuild';
23 24

void main() {
25 26 27 28 29 30 31
  group('xcodebuild versioning', () {
    MockProcessManager mockProcessManager;
    XcodeProjectInterpreter xcodeProjectInterpreter;
    FakePlatform macOS;
    FileSystem fs;

    setUp(() {
32 33
      mockProcessManager = MockProcessManager();
      xcodeProjectInterpreter = XcodeProjectInterpreter();
34
      macOS = fakePlatform('macos');
35
      fs = MemoryFileSystem();
36 37 38 39 40 41 42 43 44 45 46 47 48
      fs.file(xcodebuild).createSync(recursive: true);
    });

    void testUsingOsxContext(String description, dynamic testMethod()) {
      testUsingContext(description, testMethod, overrides: <Type, Generator>{
        ProcessManager: () => mockProcessManager,
        Platform: () => macOS,
        FileSystem: () => fs,
      });
    }

    testUsingOsxContext('versionText returns null when xcodebuild is not installed', () {
      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
49
          .thenThrow(const ProcessException(xcodebuild, <String>['-version']));
50 51 52 53 54
      expect(xcodeProjectInterpreter.versionText, isNull);
    });

    testUsingOsxContext('versionText returns null when xcodebuild is not fully installed', () {
      when(mockProcessManager.runSync(<String>[xcodebuild, '-version'])).thenReturn(
55
        ProcessResult(
56 57 58 59 60 61 62 63 64 65 66 67 68
          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']))
69
          .thenReturn(ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', ''));
70 71 72 73 74
      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']))
75
          .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', ''));
76 77 78 79 80
      expect(xcodeProjectInterpreter.versionText, 'Xcode Ultra5000, Build version 8E3004b');
    });

    testUsingOsxContext('majorVersion returns major version', () {
      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
81
          .thenReturn(ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', ''));
82 83 84 85 86
      expect(xcodeProjectInterpreter.majorVersion, 8);
    });

    testUsingOsxContext('majorVersion is null when version has unexpected format', () {
      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
87
          .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', ''));
88 89 90 91 92
      expect(xcodeProjectInterpreter.majorVersion, isNull);
    });

    testUsingOsxContext('minorVersion returns minor version', () {
      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
93
          .thenReturn(ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', ''));
94 95 96 97 98
      expect(xcodeProjectInterpreter.minorVersion, 3);
    });

    testUsingOsxContext('minorVersion returns 0 when minor version is unspecified', () {
      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
99
          .thenReturn(ProcessResult(1, 0, 'Xcode 8\nBuild version 8E3004b', ''));
100 101 102 103 104
      expect(xcodeProjectInterpreter.minorVersion, 0);
    });

    testUsingOsxContext('minorVersion is null when version has unexpected format', () {
      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
105
          .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', ''));
106 107 108 109 110 111 112
      expect(xcodeProjectInterpreter.minorVersion, isNull);
    });

    testUsingContext('isInstalled is false when not on MacOS', () {
      fs.file(xcodebuild).deleteSync();
      expect(xcodeProjectInterpreter.isInstalled, isFalse);
    }, overrides: <Type, Generator>{
113
      Platform: () => fakePlatform('notMacOS'),
114 115 116 117 118 119 120 121 122
    });

    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(
123
        ProcessResult(
124 125 126 127 128 129 130 131 132 133 134 135 136
          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']))
137
          .thenReturn(ProcessResult(1, 0, 'Xcode Ultra5000\nBuild version 8E3004b', ''));
138 139 140 141 142
      expect(xcodeProjectInterpreter.isInstalled, isFalse);
    });

    testUsingOsxContext('isInstalled is true when version has expected format', () {
      when(mockProcessManager.runSync(<String>[xcodebuild, '-version']))
143
          .thenReturn(ProcessResult(1, 0, 'Xcode 8.3.3\nBuild version 8E3004b', ''));
144 145 146
      expect(xcodeProjectInterpreter.isInstalled, isTrue);
    });
  });
147 148
  group('Xcode project properties', () {
    test('properties from default project can be parsed', () {
149
      const String output = '''
150 151 152 153 154 155 156 157 158 159 160 161 162 163
Information about project "Runner":
    Targets:
        Runner

    Build Configurations:
        Debug
        Release

    If no build configuration is specified and -scheme is not passed then "Release" is used.

    Schemes:
        Runner

''';
164
      final XcodeProjectInfo info = XcodeProjectInfo.fromXcodeBuildOutput(output);
165 166 167 168 169
      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', () {
170
      const String output = '''
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
Information about project "Runner":
    Targets:
        Runner

    Build Configurations:
        Debug (Free)
        Debug (Paid)
        Release (Free)
        Release (Paid)

    If no build configuration is specified and -scheme is not passed then "Release (Free)" is used.

    Schemes:
        Free
        Paid

''';
188
      final XcodeProjectInfo info = XcodeProjectInfo.fromXcodeBuildOutput(output);
189 190 191 192 193 194 195 196 197 198 199
      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');
200
      expect(XcodeProjectInfo.expectedBuildConfigurationFor(BuildInfo.profile, 'Runner'), 'Profile');
201 202 203 204 205 206 207 208 209
      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');
210
      expect(XcodeProjectInfo.expectedBuildConfigurationFor(const BuildInfo(BuildMode.profile, 'HELLO'), 'Hello'), 'Profile-Hello');
211 212 213
      expect(XcodeProjectInfo.expectedBuildConfigurationFor(const BuildInfo(BuildMode.release, 'Hello'), 'Hello'), 'Release-Hello');
    });
    test('scheme for default project is Runner', () {
214
      final XcodeProjectInfo info = XcodeProjectInfo(<String>['Runner'], <String>['Debug', 'Release'], <String>['Runner']);
215 216 217 218 219 220
      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', () {
221
      final XcodeProjectInfo info = XcodeProjectInfo(<String>['Runner'], <String>['Debug', 'Profile', 'Release'], <String>['Runner']);
222
      expect(info.buildConfigurationFor(BuildInfo.debug, 'Runner'), 'Debug');
223
      expect(info.buildConfigurationFor(BuildInfo.profile, 'Runner'), 'Profile');
224 225 226
      expect(info.buildConfigurationFor(BuildInfo.release, 'Runner'), 'Release');
    });
    test('scheme for project with custom schemes is matched against flavor', () {
227
      final XcodeProjectInfo info = XcodeProjectInfo(
228 229 230 231 232 233 234 235 236 237 238
        <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', () {
239
      final XcodeProjectInfo info = XcodeProjectInfo(
240
        <String>['Runner'],
241
        <String>['debug (free)', 'Debug paid', 'profile - Free', 'Profile-Paid', 'release - Free', 'Release-Paid'],
242 243 244 245
        <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');
246
      expect(info.buildConfigurationFor(const BuildInfo(BuildMode.profile, 'FREE'), 'Free'), 'profile - Free');
247 248 249
      expect(info.buildConfigurationFor(const BuildInfo(BuildMode.release, 'paid'), 'Paid'), 'Release-Paid');
    });
    test('build configuration for project with inconsistent naming is null', () {
250
      final XcodeProjectInfo info = XcodeProjectInfo(
251 252 253 254 255 256 257 258 259
        <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);
    });
  });
260 261 262 263 264 265 266 267

  group('updateGeneratedXcodeProperties', () {
    MockLocalEngineArtifacts mockArtifacts;
    MockProcessManager mockProcessManager;
    FakePlatform macOS;
    FileSystem fs;

    setUp(() {
268 269 270
      fs = MemoryFileSystem();
      mockArtifacts = MockLocalEngineArtifacts();
      mockProcessManager = MockProcessManager();
271 272 273 274 275 276 277 278 279 280 281 282 283
      macOS = fakePlatform('macos');
      fs.file(xcodebuild).createSync(recursive: true);
    });

    void testUsingOsxContext(String description, dynamic testMethod()) {
      testUsingContext(description, testMethod, overrides: <Type, Generator>{
        Artifacts: () => mockArtifacts,
        ProcessManager: () => mockProcessManager,
        Platform: () => macOS,
        FileSystem: () => fs,
      });
    }

284
    testUsingOsxContext('sets ARCHS=armv7 when armv7 local engine is set', () async {
285
      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework, TargetPlatform.ios, any)).thenReturn('engine');
286
      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile_arm'));
287

288
      const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null, targetPlatform: TargetPlatform.ios);
289
      final FlutterProject project = await FlutterProject.fromPath('path/to/project');
290 291
      await updateGeneratedXcodeProperties(
        project: project,
292 293 294 295 296 297 298 299 300 301
        buildInfo: buildInfo,
      );

      final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
      expect(config.existsSync(), isTrue);

      final String contents = config.readAsStringSync();
      expect(contents.contains('ARCHS=armv7'), isTrue);
    });

302
    testUsingOsxContext('sets TRACK_WIDGET_CREATION=true when trackWidgetCreation is true', () async {
303
      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework, TargetPlatform.ios, any)).thenReturn('engine');
304
      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile_arm'));
305
      const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null, trackWidgetCreation: true, targetPlatform: TargetPlatform.ios);
306
      final FlutterProject project = await FlutterProject.fromPath('path/to/project');
307 308
      await updateGeneratedXcodeProperties(
        project: project,
309 310 311 312 313 314 315 316 317 318 319
        buildInfo: buildInfo,
      );

      final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
      expect(config.existsSync(), isTrue);

      final String contents = config.readAsStringSync();
      expect(contents.contains('TRACK_WIDGET_CREATION=true'), isTrue);
    });

    testUsingOsxContext('does not set TRACK_WIDGET_CREATION when trackWidgetCreation is false', () async {
320
      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework, TargetPlatform.ios, any)).thenReturn('engine');
321
      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile_arm'));
322
      const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null, targetPlatform: TargetPlatform.ios);
323
      final FlutterProject project = await FlutterProject.fromPath('path/to/project');
324 325
      await updateGeneratedXcodeProperties(
        project: project,
326 327 328 329 330 331 332 333 334 335
        buildInfo: buildInfo,
      );

      final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
      expect(config.existsSync(), isTrue);

      final String contents = config.readAsStringSync();
      expect(contents.contains('TRACK_WIDGET_CREATION=true'), isFalse);
    });

336
    testUsingOsxContext('sets ARCHS=armv7 when armv7 local engine is set', () async {
337
      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework, TargetPlatform.ios, any)).thenReturn('engine');
338
      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile'));
339
      const BuildInfo buildInfo = BuildInfo(BuildMode.debug, null, targetPlatform: TargetPlatform.ios);
340

341
      final FlutterProject project = await FlutterProject.fromPath('path/to/project');
342 343
      await updateGeneratedXcodeProperties(
        project: project,
344 345 346 347 348 349 350 351 352
        buildInfo: buildInfo,
      );

      final File config = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
      expect(config.existsSync(), isTrue);

      final String contents = config.readAsStringSync();
      expect(contents.contains('ARCHS=arm64'), isTrue);
    });
353 354 355 356 357 358 359 360 361 362 363

    String propertyFor(String key, File file) {
      final List<String> properties = file
          .readAsLinesSync()
          .where((String line) => line.startsWith('$key='))
          .map((String line) => line.split('=')[1])
          .toList();
      return properties.isEmpty ? null : properties.first;
    }

    Future<void> checkBuildVersion({
364
      String manifestString,
365 366 367 368
      BuildInfo buildInfo,
      String expectedBuildName,
      String expectedBuildNumber,
    }) async {
369 370 371 372 373 374 375 376
      when(mockArtifacts.getArtifactPath(Artifact.flutterFramework, TargetPlatform.ios, any)).thenReturn('engine');
      when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios'));

      final File manifestFile = fs.file('path/to/project/pubspec.yaml');
      manifestFile.createSync(recursive: true);
      manifestFile.writeAsStringSync(manifestString);

      // write schemaData otherwise pubspec.yaml file can't be loaded
377
      writeEmptySchemaFile(fs);
378

379
      await updateGeneratedXcodeProperties(
380
        project: await FlutterProject.fromPath('path/to/project'),
381 382 383
        buildInfo: buildInfo,
      );

384
      final File localPropertiesFile = fs.file('path/to/project/ios/Flutter/Generated.xcconfig');
385 386 387 388
      expect(propertyFor('FLUTTER_BUILD_NAME', localPropertiesFile), expectedBuildName);
      expect(propertyFor('FLUTTER_BUILD_NUMBER', localPropertiesFile), expectedBuildNumber);
    }

389
    testUsingOsxContext('extract build name and number from pubspec.yaml', () async {
390 391 392 393 394 395 396 397 398
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';

399
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null);
400
      await checkBuildVersion(
401
        manifestString: manifest,
402 403 404 405 406 407
        buildInfo: buildInfo,
        expectedBuildName: '1.0.0',
        expectedBuildNumber: '1',
      );
    });

408
    testUsingOsxContext('extract build name from pubspec.yaml', () async {
409 410 411 412 413 414 415 416
      const String manifest = '''
name: test
version: 1.0.0
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
417
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null);
418
      await checkBuildVersion(
419
        manifestString: manifest,
420 421 422 423 424 425
        buildInfo: buildInfo,
        expectedBuildName: '1.0.0',
        expectedBuildNumber: null,
      );
    });

426
    testUsingOsxContext('allow build info to override build name', () async {
427 428 429 430 431 432 433 434
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
435
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2');
436
      await checkBuildVersion(
437
        manifestString: manifest,
438 439 440 441 442 443
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '1',
      );
    });

444
    testUsingOsxContext('allow build info to override build number', () async {
445 446 447 448 449 450 451 452
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
453
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildNumber: '3');
454
      await checkBuildVersion(
455
        manifestString: manifest,
456 457 458 459 460 461
        buildInfo: buildInfo,
        expectedBuildName: '1.0.0',
        expectedBuildNumber: '3',
      );
    });

462
    testUsingOsxContext('allow build info to override build name and number', () async {
463 464 465 466 467 468 469 470
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
471
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3');
472
      await checkBuildVersion(
473
        manifestString: manifest,
474 475 476 477 478 479
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '3',
      );
    });

480
    testUsingOsxContext('allow build info to override build name and set number', () async {
481 482 483 484 485 486 487 488
      const String manifest = '''
name: test
version: 1.0.0
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
489
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3');
490
      await checkBuildVersion(
491
        manifestString: manifest,
492 493 494 495 496 497
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '3',
      );
    });

498
    testUsingOsxContext('allow build info to set build name and number', () async {
499 500 501 502 503 504 505
      const String manifest = '''
name: test
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
506
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3');
507
      await checkBuildVersion(
508
        manifestString: manifest,
509 510 511 512 513 514
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '3',
      );
    });
  });
515
}
516 517

Platform fakePlatform(String name) {
518
  return FakePlatform.fromPlatform(const LocalPlatform())..operatingSystem = name;
519 520
}

521
class MockLocalEngineArtifacts extends Mock implements LocalEngineArtifacts {}
522 523
class MockProcessManager extends Mock implements ProcessManager {}
class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter { }