cocoapods_test.dart 31.5 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
// @dart = 2.8

7 8 9
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/io.dart';
10 11
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
12
import 'package:flutter_tools/src/build_info.dart';
13
import 'package:flutter_tools/src/cache.dart';
14
import 'package:flutter_tools/src/flutter_plugins.dart';
15 16
import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:flutter_tools/src/macos/cocoapods.dart';
17
import 'package:flutter_tools/src/project.dart';
18
import 'package:flutter_tools/src/reporting/reporting.dart';
19
import 'package:test/fake.dart';
20

21 22
import '../../src/common.dart';
import '../../src/context.dart';
23
import '../../src/fake_process_manager.dart';
24

25
typedef InvokeProcess = Future<ProcessResult> Function();
26

27
void main() {
28
  FileSystem fileSystem;
29
  FakeProcessManager fakeProcessManager;
30
  CocoaPods cocoaPodsUnderTest;
31
  BufferLogger logger;
32
  TestUsage usage;
33 34

  void pretendPodVersionFails() {
35 36 37 38 39 40
    fakeProcessManager.addCommand(
      const FakeCommand(
        command: <String>['pod', '--version'],
        exitCode: 1,
      ),
    );
41 42 43
  }

  void pretendPodVersionIs(String versionText) {
44 45 46 47 48 49
    fakeProcessManager.addCommand(
      FakeCommand(
        command: const <String>['pod', '--version'],
        stdout: versionText,
      ),
    );
50
  }
51

52
  void podsIsInHomeDir() {
53
    fileSystem.directory(fileSystem.path.join(
54 55 56 57
      '.cocoapods',
      'repos',
      'master',
    )).createSync(recursive: true);
58 59
  }

60 61 62 63 64 65 66 67
  FlutterProject setupProjectUnderTest() {
    // This needs to be run within testWithoutContext and not setUp since FlutterProject uses context.
    final FlutterProject projectUnderTest = FlutterProject.fromDirectory(fileSystem.directory('project'));
    projectUnderTest.ios.xcodeProject.createSync(recursive: true);
    projectUnderTest.macos.xcodeProject.createSync(recursive: true);
    return projectUnderTest;
  }

68
  setUp(() async {
69
    Cache.flutterRoot = 'flutter';
70
    fileSystem = MemoryFileSystem.test();
71
    fakeProcessManager = FakeProcessManager.empty();
72
    logger = BufferLogger.test();
73
    usage = TestUsage();
74
    cocoaPodsUnderTest = CocoaPods(
75
      fileSystem: fileSystem,
76
      processManager: fakeProcessManager,
77
      logger: logger,
78
      platform: FakePlatform(operatingSystem: 'macos'),
79
      xcodeProjectInterpreter: FakeXcodeProjectInterpreter(),
80
      usage: usage,
81
    );
82
    fileSystem.file(fileSystem.path.join(
83
      Cache.flutterRoot, 'packages', 'flutter_tools', 'templates', 'cocoapods', 'Podfile-ios-objc',
84 85
    ))
        ..createSync(recursive: true)
86
        ..writeAsStringSync('Objective-C iOS podfile template');
87
    fileSystem.file(fileSystem.path.join(
88
      Cache.flutterRoot, 'packages', 'flutter_tools', 'templates', 'cocoapods', 'Podfile-ios-swift',
89 90
    ))
        ..createSync(recursive: true)
91
        ..writeAsStringSync('Swift iOS podfile template');
92
    fileSystem.file(fileSystem.path.join(
93 94 95 96
      Cache.flutterRoot, 'packages', 'flutter_tools', 'templates', 'cocoapods', 'Podfile-macos',
    ))
        ..createSync(recursive: true)
        ..writeAsStringSync('macOS podfile template');
97 98
  });

99
  void pretendPodIsNotInstalled() {
100 101 102 103 104 105
    fakeProcessManager.addCommand(
      const FakeCommand(
        command: <String>['which', 'pod'],
        exitCode: 1,
      ),
    );
106 107
  }

108
  void pretendPodIsBroken() {
109 110 111 112 113 114 115 116 117 118 119
    fakeProcessManager.addCommands(<FakeCommand>[
      // it is present
      const FakeCommand(
        command: <String>['which', 'pod'],
      ),
      // but is not working
      const FakeCommand(
        command: <String>['pod', '--version'],
        exitCode: 1,
      ),
    ]);
120 121
  }

122
  void pretendPodIsInstalled() {
123 124 125 126 127
    fakeProcessManager.addCommands(<FakeCommand>[
      const FakeCommand(
        command: <String>['which', 'pod'],
      ),
    ]);
128 129
  }

130
  group('Evaluate installation', () {
131
    testWithoutContext('detects not installed, if pod exec does not exist', () async {
132 133 134 135
      pretendPodIsNotInstalled();
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.notInstalled);
    });

136
    testWithoutContext('detects not installed, if pod is installed but version fails', () async {
137
      pretendPodIsInstalled();
138
      pretendPodVersionFails();
139
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.brokenInstall);
140 141
    });

142
    testWithoutContext('detects installed', () async {
143
      pretendPodIsInstalled();
144 145 146 147
      pretendPodVersionIs('0.0.1');
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, isNot(CocoaPodsStatus.notInstalled));
    });

148
    testWithoutContext('detects unknown version', () async {
149
      pretendPodIsInstalled();
150 151 152 153
      pretendPodVersionIs('Plugin loaded.\n1.5.3');
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.unknownVersion);
    });

154
    testWithoutContext('detects below minimum version', () async {
155
      pretendPodIsInstalled();
156
      pretendPodVersionIs('1.6.0');
157 158 159
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.belowMinimumVersion);
    });

160
    testWithoutContext('detects below recommended version', () async {
161
      pretendPodIsInstalled();
162
      pretendPodVersionIs('1.9.0');
163 164 165
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.belowRecommendedVersion);
    });

166
    testWithoutContext('detects at recommended version', () async {
167
      pretendPodIsInstalled();
168
      pretendPodVersionIs('1.10.0');
169 170 171
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.recommended);
    });

172
    testWithoutContext('detects above recommended version', () async {
173
      pretendPodIsInstalled();
174
      pretendPodVersionIs('1.10.1');
175 176
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.recommended);
    });
177 178
  });

179
  group('Setup Podfile', () {
180 181
    testUsingContext('creates objective-c Podfile when not present', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
182
      await cocoaPodsUnderTest.setupPodfile(projectUnderTest.ios);
183

184
      expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Objective-C iOS podfile template');
185
    });
186

187 188
    testUsingContext('creates swift Podfile if swift', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
189 190 191 192 193 194 195 196 197 198 199
      final FakeXcodeProjectInterpreter fakeXcodeProjectInterpreter = FakeXcodeProjectInterpreter(buildSettings: <String, String>{
        'SWIFT_VERSION': '5.0',
      });
      final CocoaPods cocoaPodsUnderTest = CocoaPods(
        fileSystem: fileSystem,
        processManager: fakeProcessManager,
        logger: logger,
        platform: FakePlatform(operatingSystem: 'macos'),
        xcodeProjectInterpreter: fakeXcodeProjectInterpreter,
        usage: usage,
      );
200

201
      final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project'));
202
      await cocoaPodsUnderTest.setupPodfile(project.ios);
203

204
      expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Swift iOS podfile template');
205
    });
206

207 208
    testUsingContext('creates macOS Podfile when not present', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
209
      projectUnderTest.macos.xcodeProject.createSync(recursive: true);
210
      await cocoaPodsUnderTest.setupPodfile(projectUnderTest.macos);
211 212 213 214

      expect(projectUnderTest.macos.podfile.readAsStringSync(), 'macOS podfile template');
    });

215 216
    testUsingContext('does not recreate Podfile when already present', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
217
      projectUnderTest.ios.podfile..createSync()..writeAsStringSync('Existing Podfile');
218

219
      final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project'));
220
      await cocoaPodsUnderTest.setupPodfile(project.ios);
221

222
      expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Existing Podfile');
223 224
    });

225 226
    testUsingContext('does not create Podfile when we cannot interpret Xcode projects', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
227 228 229 230 231 232 233 234
      final CocoaPods cocoaPodsUnderTest = CocoaPods(
        fileSystem: fileSystem,
        processManager: fakeProcessManager,
        logger: logger,
        platform: FakePlatform(operatingSystem: 'macos'),
        xcodeProjectInterpreter: FakeXcodeProjectInterpreter(isInstalled: false),
        usage: usage,
      );
235

236
      final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project'));
237
      await cocoaPodsUnderTest.setupPodfile(project.ios);
238

239
      expect(projectUnderTest.ios.podfile.existsSync(), false);
240 241
    });

242 243
    testUsingContext('includes Pod config in xcconfig files, if not present', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
244 245 246 247 248 249 250
      projectUnderTest.ios.podfile..createSync()..writeAsStringSync('Existing Podfile');
      projectUnderTest.ios.xcodeConfigFor('Debug')
        ..createSync(recursive: true)
        ..writeAsStringSync('Existing debug config');
      projectUnderTest.ios.xcodeConfigFor('Release')
        ..createSync(recursive: true)
        ..writeAsStringSync('Existing release config');
251

252
      final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project'));
253
      await cocoaPodsUnderTest.setupPodfile(project.ios);
254

255
      final String debugContents = projectUnderTest.ios.xcodeConfigFor('Debug').readAsStringSync();
256
      expect(debugContents, contains(
257
          '#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"\n'));
258
      expect(debugContents, contains('Existing debug config'));
259
      final String releaseContents = projectUnderTest.ios.xcodeConfigFor('Release').readAsStringSync();
260
      expect(releaseContents, contains(
261
          '#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"\n'));
262 263
      expect(releaseContents, contains('Existing release config'));
    });
264

265 266
    testUsingContext('does not include Pod config in xcconfig files, if legacy non-option include present', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
267 268 269 270 271 272 273 274 275 276 277
      projectUnderTest.ios.podfile..createSync()..writeAsStringSync('Existing Podfile');

      const String legacyDebugInclude = '#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig';
      projectUnderTest.ios.xcodeConfigFor('Debug')
        ..createSync(recursive: true)
        ..writeAsStringSync(legacyDebugInclude);
      const String legacyReleaseInclude = '#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig';
      projectUnderTest.ios.xcodeConfigFor('Release')
        ..createSync(recursive: true)
        ..writeAsStringSync(legacyReleaseInclude);

278
      final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project'));
279 280 281 282 283 284 285 286 287 288 289
      await cocoaPodsUnderTest.setupPodfile(project.ios);

      final String debugContents = projectUnderTest.ios.xcodeConfigFor('Debug').readAsStringSync();
      // Redundant contains check, but this documents what we're testing--that the optional
      // #include? doesn't get written in addition to the previous style #include.
      expect(debugContents, isNot(contains('#include?')));
      expect(debugContents, equals(legacyDebugInclude));
      final String releaseContents = projectUnderTest.ios.xcodeConfigFor('Release').readAsStringSync();
      expect(releaseContents, isNot(contains('#include?')));
      expect(releaseContents, equals(legacyReleaseInclude));
    });
290 291 292 293
  });

  group('Update xcconfig', () {
    testUsingContext('includes Pod config in xcconfig files, if the user manually added Pod dependencies without using Flutter plugins', () async {
294
      final FlutterProject projectUnderTest = setupProjectUnderTest();
295
      fileSystem.file(fileSystem.path.join('project', 'foo', '.packages'))
296 297
        ..createSync(recursive: true)
        ..writeAsStringSync('\n');
298 299 300 301 302 303 304 305 306 307
      projectUnderTest.ios.podfile..createSync()..writeAsStringSync('Custom Podfile');
      projectUnderTest.ios.podfileLock..createSync()..writeAsStringSync('Podfile.lock from user executed `pod install`');
      projectUnderTest.packagesFile..createSync()..writeAsStringSync('');
      projectUnderTest.ios.xcodeConfigFor('Debug')
        ..createSync(recursive: true)
        ..writeAsStringSync('Existing debug config');
      projectUnderTest.ios.xcodeConfigFor('Release')
        ..createSync(recursive: true)
        ..writeAsStringSync('Existing release config');

308
      final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project'));
309
      await injectPlugins(project, iosPlatform: true);
310 311 312

      final String debugContents = projectUnderTest.ios.xcodeConfigFor('Debug').readAsStringSync();
      expect(debugContents, contains(
313
          '#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"\n'));
314 315
      expect(debugContents, contains('Existing debug config'));
      final String releaseContents = projectUnderTest.ios.xcodeConfigFor('Release').readAsStringSync();
316
      expect(releaseContents, contains(
317
          '#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"\n'));
318 319
      expect(releaseContents, contains('Existing release config'));
    }, overrides: <Type, Generator>{
320
      FileSystem: () => fileSystem,
321
      ProcessManager: () => FakeProcessManager.any(),
322 323 324 325
    });
  });

  group('Process pods', () {
326 327 328 329
    setUp(() {
      podsIsInHomeDir();
    });

330 331
    testUsingContext('throwsToolExit if CocoaPods is not installed', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
332
      pretendPodIsNotInstalled();
333
      projectUnderTest.ios.podfile.createSync();
334
      await expectLater(cocoaPodsUnderTest.processPods(
335
        xcodeProject: projectUnderTest.ios,
336
        buildMode: BuildMode.debug,
337
      ), throwsToolExit(message: 'CocoaPods not installed or not in valid state'));
338
      expect(fakeProcessManager, hasNoRemainingExpectations);
339
      expect(fakeProcessManager, hasNoRemainingExpectations);
340 341
    });

342 343
    testUsingContext('throwsToolExit if CocoaPods install is broken', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
344 345
      pretendPodIsBroken();
      projectUnderTest.ios.podfile.createSync();
346
      await expectLater(cocoaPodsUnderTest.processPods(
347
        xcodeProject: projectUnderTest.ios,
348
        buildMode: BuildMode.debug,
349
      ), throwsToolExit(message: 'CocoaPods not installed or not in valid state'));
350
      expect(fakeProcessManager, hasNoRemainingExpectations);
351
      expect(fakeProcessManager, hasNoRemainingExpectations);
352
    });
353

354 355
    testUsingContext('exits if Podfile creates the Flutter engine symlink', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
356
      fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile'))
357 358 359 360 361 362 363
        ..createSync()
        ..writeAsStringSync('Existing Podfile');

      final Directory symlinks = projectUnderTest.ios.symlinks
        ..createSync(recursive: true);
      symlinks.childLink('flutter').createSync('cache');

364
      await expectLater(cocoaPodsUnderTest.processPods(
365
        xcodeProject: projectUnderTest.ios,
366 367
        buildMode: BuildMode.debug,
      ), throwsToolExit(message: 'Podfile is out of date'));
368
      expect(fakeProcessManager, hasNoRemainingExpectations);
369 370
    });

371 372
    testUsingContext('exits if iOS Podfile parses .flutter-plugins', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
373
      fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile'))
374
        ..createSync()
375
        ..writeAsStringSync("plugin_pods = parse_KV_file('../.flutter-plugins')");
376

377
      await expectLater(cocoaPodsUnderTest.processPods(
378
        xcodeProject: projectUnderTest.ios,
379 380
        buildMode: BuildMode.debug,
      ), throwsToolExit(message: 'Podfile is out of date'));
381
      expect(fakeProcessManager, hasNoRemainingExpectations);
382 383
    });

384 385
    testUsingContext('prints warning if macOS Podfile parses .flutter-plugins', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
386
      pretendPodIsInstalled();
387
      pretendPodVersionIs('1.10.0');
388 389
      fakeProcessManager.addCommands(const <FakeCommand>[
        FakeCommand(
390 391
          command: <String>['pod', 'install', '--verbose'],
        ),
392 393 394 395
        FakeCommand(
          command: <String>['touch', 'project/macos/Podfile.lock'],
        ),
      ]);
396

397
      projectUnderTest.macos.podfile
398
        ..createSync()
399
        ..writeAsStringSync("plugin_pods = parse_KV_file('../.flutter-plugins')");
400 401 402
      projectUnderTest.macos.podfileLock
        ..createSync()
        ..writeAsStringSync('Existing lock file.');
403 404 405 406 407 408 409 410

      await cocoaPodsUnderTest.processPods(
        xcodeProject: projectUnderTest.macos,
        buildMode: BuildMode.debug,
      );

      expect(logger.errorText, contains('Warning: Podfile is out of date'));
      expect(logger.errorText, contains('rm macos/Podfile'));
411
      expect(fakeProcessManager, hasNoRemainingExpectations);
412 413
    });

414 415
    testUsingContext('throws, if Podfile is missing.', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
416 417 418 419
      await expectLater(cocoaPodsUnderTest.processPods(
        xcodeProject: projectUnderTest.ios,
        buildMode: BuildMode.debug,
      ), throwsToolExit(message: 'Podfile missing'));
420
      expect(fakeProcessManager, hasNoRemainingExpectations);
421
    });
422

423 424
    testUsingContext('throws, if specs repo is outdated.', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
425
      pretendPodIsInstalled();
426
      pretendPodVersionIs('1.10.0');
427
      fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile'))
428
        ..createSync()
429
        ..writeAsStringSync('Existing Podfile');
430

431 432 433 434 435 436 437 438 439 440
      fakeProcessManager.addCommand(
        const FakeCommand(
          command: <String>['pod', 'install', '--verbose'],
          workingDirectory: 'project/ios',
          environment: <String, String>{
            'COCOAPODS_DISABLE_STATS': 'true',
            'LANG': 'en_US.UTF-8',
          },
          exitCode: 1,
          stdout: '''
441 442 443 444 445 446 447 448 449 450 451 452 453
[!] Unable to satisfy the following requirements:

- `Firebase/Auth` required by `Podfile`
- `Firebase/Auth (= 4.0.0)` required by `Podfile.lock`

None of your spec sources contain a spec satisfying the dependencies: `Firebase/Auth, Firebase/Auth (= 4.0.0)`.

You have either:
 * out-of-date source repos which you can update with `pod repo update` or with `pod install --repo-update`.
 * mistyped the name or version.
 * not added the source repo that hosts the Podspec to your Podfile.

Note: as of CocoaPods 1.0, `pod repo update` does not happen on `pod install` by default.''',
454 455 456 457 458 459 460 461 462 463 464 465
        ),
      );

      await expectLater(cocoaPodsUnderTest.processPods(
        xcodeProject: projectUnderTest.ios,
        buildMode: BuildMode.debug,
      ), throwsToolExit());
      expect(
        logger.errorText,
        contains(
            "CocoaPods's specs repository is too out-of-date to satisfy dependencies"),
      );
466
    });
467

468 469
    testUsingContext('ffi failure on ARM macOS prompts gem install', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
470
      pretendPodIsInstalled();
471
      pretendPodVersionIs('1.10.0');
472 473 474 475
      fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile'))
        ..createSync()
        ..writeAsStringSync('Existing Podfile');

476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494
      fakeProcessManager.addCommands(<FakeCommand>[
        const FakeCommand(
          command: <String>['pod', 'install', '--verbose'],
          workingDirectory: 'project/ios',
          environment: <String, String>{
            'COCOAPODS_DISABLE_STATS': 'true',
            'LANG': 'en_US.UTF-8',
          },
          exitCode: 1,
          stdout: 'LoadError - dlsym(0x7fbbeb6837d0, Init_ffi_c): symbol not found - /Library/Ruby/Gems/2.6.0/gems/ffi-1.13.1/lib/ffi_c.bundle',
        ),
        const FakeCommand(
          command: <String>['which', 'sysctl'],
        ),
        const FakeCommand(
          command: <String>['sysctl', 'hw.optional.arm64'],
          stdout: 'hw.optional.arm64: 1',
        ),
      ]);
495

496 497 498 499 500 501 502
      await expectToolExitLater(
        cocoaPodsUnderTest.processPods(
          xcodeProject: projectUnderTest.ios,
          buildMode: BuildMode.debug,
        ),
        equals('Error running pod install'),
      );
503 504 505 506
      expect(
        logger.errorText,
        contains('set up CocoaPods for ARM macOS'),
      );
507
      expect(usage.events, contains(const TestUsageEvent('pod-install-failure', 'arm-ffi')));
508 509
    });

510 511
    testUsingContext('ffi failure on x86 macOS does not prompt gem install', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
512
      pretendPodIsInstalled();
513
      pretendPodVersionIs('1.10.0');
514 515 516 517
      fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile'))
        ..createSync()
        ..writeAsStringSync('Existing Podfile');

518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536
      fakeProcessManager.addCommands(<FakeCommand>[
        const FakeCommand(
          command: <String>['pod', 'install', '--verbose'],
          workingDirectory: 'project/ios',
          environment: <String, String>{
            'COCOAPODS_DISABLE_STATS': 'true',
            'LANG': 'en_US.UTF-8',
          },
          exitCode: 1,
          stderr: 'LoadError - dlsym(0x7fbbeb6837d0, Init_ffi_c): symbol not found - /Library/Ruby/Gems/2.6.0/gems/ffi-1.13.1/lib/ffi_c.bundle',
        ),
        const FakeCommand(
          command: <String>['which', 'sysctl'],
        ),
        const FakeCommand(
          command: <String>['sysctl', 'hw.optional.arm64'],
          exitCode: 1,
        ),
      ]);
537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553

      // Capture Usage.test() events.
      final StringBuffer buffer =
      await capturedConsolePrint(() => expectToolExitLater(
        cocoaPodsUnderTest.processPods(
          xcodeProject: projectUnderTest.ios,
          buildMode: BuildMode.debug,
        ),
        equals('Error running pod install'),
      ));
      expect(
        logger.errorText,
        isNot(contains('ARM macOS')),
      );
      expect(buffer.isEmpty, true);
    });

554 555
    testUsingContext('run pod install, if Podfile.lock is missing', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
556
      pretendPodIsInstalled();
557
      pretendPodVersionIs('1.10.0');
558
      projectUnderTest.ios.podfile
559
        ..createSync()
560
        ..writeAsStringSync('Existing Podfile');
561
      projectUnderTest.ios.podManifestLock
562
        ..createSync(recursive: true)
563
        ..writeAsStringSync('Existing lock file.');
564

565 566
      fakeProcessManager.addCommands(const <FakeCommand>[
        FakeCommand(
567 568 569 570
          command: <String>['pod', 'install', '--verbose'],
          workingDirectory: 'project/ios',
          environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'},
        ),
571
      ]);
572
      final bool didInstall = await cocoaPodsUnderTest.processPods(
573
        xcodeProject: projectUnderTest.ios,
574
        buildMode: BuildMode.debug,
575
        dependenciesChanged: false,
576
      );
577
      expect(didInstall, isTrue);
578
      expect(fakeProcessManager, hasNoRemainingExpectations);
579
    });
580

581 582
    testUsingContext('runs iOS pod install, if Manifest.lock is missing', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
583
      pretendPodIsInstalled();
584
      pretendPodVersionIs('1.10.0');
585
      projectUnderTest.ios.podfile
586
        ..createSync()
587
        ..writeAsStringSync('Existing Podfile');
588
      projectUnderTest.ios.podfileLock
589
        ..createSync()
590
        ..writeAsStringSync('Existing lock file.');
591 592
      fakeProcessManager.addCommands(const <FakeCommand>[
        FakeCommand(
593 594 595 596
          command: <String>['pod', 'install', '--verbose'],
          workingDirectory: 'project/ios',
          environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'},
        ),
597 598 599 600
        FakeCommand(
          command: <String>['touch', 'project/ios/Podfile.lock'],
        ),
      ]);
601
      final bool didInstall = await cocoaPodsUnderTest.processPods(
602
        xcodeProject: projectUnderTest.ios,
603
        buildMode: BuildMode.debug,
604
        dependenciesChanged: false,
605
      );
606
      expect(didInstall, isTrue);
607
      expect(fakeProcessManager, hasNoRemainingExpectations);
608 609
    });

610 611
    testUsingContext('runs macOS pod install, if Manifest.lock is missing', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
612
      pretendPodIsInstalled();
613
      pretendPodVersionIs('1.10.0');
614 615 616 617 618 619
      projectUnderTest.macos.podfile
        ..createSync()
        ..writeAsStringSync('Existing Podfile');
      projectUnderTest.macos.podfileLock
        ..createSync()
        ..writeAsStringSync('Existing lock file.');
620 621
      fakeProcessManager.addCommands(const <FakeCommand>[
        FakeCommand(
622 623 624 625
          command: <String>['pod', 'install', '--verbose'],
          workingDirectory: 'project/macos',
          environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'},
        ),
626 627 628 629
        FakeCommand(
          command: <String>['touch', 'project/macos/Podfile.lock'],
        ),
      ]);
630 631 632 633 634 635
      final bool didInstall = await cocoaPodsUnderTest.processPods(
        xcodeProject: projectUnderTest.macos,
        buildMode: BuildMode.debug,
        dependenciesChanged: false,
      );
      expect(didInstall, isTrue);
636
      expect(fakeProcessManager, hasNoRemainingExpectations);
637 638
    });

639 640
    testUsingContext('runs pod install, if Manifest.lock different from Podspec.lock', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
641
      pretendPodIsInstalled();
642
      pretendPodVersionIs('1.10.0');
643
      projectUnderTest.ios.podfile
644
        ..createSync()
645
        ..writeAsStringSync('Existing Podfile');
646
      projectUnderTest.ios.podfileLock
647
        ..createSync()
648
        ..writeAsStringSync('Existing lock file.');
649
      projectUnderTest.ios.podManifestLock
650
        ..createSync(recursive: true)
651
        ..writeAsStringSync('Different lock file.');
652 653
      fakeProcessManager.addCommands(const <FakeCommand>[
        FakeCommand(
654 655 656 657
          command: <String>['pod', 'install', '--verbose'],
          workingDirectory: 'project/ios',
          environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'},
        ),
658 659 660 661
        FakeCommand(
          command: <String>['touch', 'project/ios/Podfile.lock'],
        ),
      ]);
662
      final bool didInstall = await cocoaPodsUnderTest.processPods(
663
        xcodeProject: projectUnderTest.ios,
664
        buildMode: BuildMode.debug,
665
        dependenciesChanged: false,
666
      );
667
      expect(didInstall, isTrue);
668
      expect(fakeProcessManager, hasNoRemainingExpectations);
669 670
    });

671 672
    testUsingContext('runs pod install, if flutter framework changed', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
673
      pretendPodIsInstalled();
674
      pretendPodVersionIs('1.10.0');
675
      projectUnderTest.ios.podfile
676
        ..createSync()
677
        ..writeAsStringSync('Existing Podfile');
678
      projectUnderTest.ios.podfileLock
679
        ..createSync()
680
        ..writeAsStringSync('Existing lock file.');
681
      projectUnderTest.ios.podManifestLock
682
        ..createSync(recursive: true)
683
        ..writeAsStringSync('Existing lock file.');
684 685
      fakeProcessManager.addCommands(const <FakeCommand>[
        FakeCommand(
686 687 688 689
          command: <String>['pod', 'install', '--verbose'],
          workingDirectory: 'project/ios',
          environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'},
        ),
690 691 692 693
        FakeCommand(
          command: <String>['touch', 'project/ios/Podfile.lock'],
        ),
      ]);
694
      final bool didInstall = await cocoaPodsUnderTest.processPods(
695
        xcodeProject: projectUnderTest.ios,
696
        buildMode: BuildMode.debug,
697
        dependenciesChanged: true,
698
      );
699
      expect(didInstall, isTrue);
700
      expect(fakeProcessManager, hasNoRemainingExpectations);
701 702
    });

703 704
    testUsingContext('runs pod install, if Podfile.lock is older than Podfile', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
705
      pretendPodIsInstalled();
706
      pretendPodVersionIs('1.10.0');
707
      projectUnderTest.ios.podfile
708 709
        ..createSync()
        ..writeAsStringSync('Existing Podfile');
710
      projectUnderTest.ios.podfileLock
711 712
        ..createSync()
        ..writeAsStringSync('Existing lock file.');
713
      projectUnderTest.ios.podManifestLock
714 715
        ..createSync(recursive: true)
        ..writeAsStringSync('Existing lock file.');
716
      await Future<void>.delayed(const Duration(milliseconds: 10));
717
      projectUnderTest.ios.podfile
718
        .writeAsStringSync('Updated Podfile');
719 720
      fakeProcessManager.addCommands(const <FakeCommand>[
        FakeCommand(
721 722 723 724
          command: <String>['pod', 'install', '--verbose'],
          workingDirectory: 'project/ios',
          environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'},
        ),
725 726 727 728
        FakeCommand(
          command: <String>['touch', 'project/ios/Podfile.lock'],
        ),
      ]);
729
      await cocoaPodsUnderTest.processPods(
730
        xcodeProject: projectUnderTest.ios,
731
        buildMode: BuildMode.debug,
732 733
        dependenciesChanged: false,
      );
734
      expect(fakeProcessManager, hasNoRemainingExpectations);
735 736
    });

737 738
    testUsingContext('skips pod install, if nothing changed', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
739
      projectUnderTest.ios.podfile
740
        ..createSync()
741
        ..writeAsStringSync('Existing Podfile');
742
      projectUnderTest.ios.podfileLock
743
        ..createSync()
744
        ..writeAsStringSync('Existing lock file.');
745
      projectUnderTest.ios.podManifestLock
746
        ..createSync(recursive: true)
747
        ..writeAsStringSync('Existing lock file.');
748
      final bool didInstall = await cocoaPodsUnderTest.processPods(
749
        xcodeProject: projectUnderTest.ios,
750
        buildMode: BuildMode.debug,
751
        dependenciesChanged: false,
752
      );
753
      expect(didInstall, isFalse);
754
      expect(fakeProcessManager, hasNoRemainingExpectations);
755
    });
756

757 758
    testUsingContext('a failed pod install deletes Pods/Manifest.lock', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
759
      pretendPodIsInstalled();
760
      pretendPodVersionIs('1.10.0');
761
      projectUnderTest.ios.podfile
762
        ..createSync()
763
        ..writeAsStringSync('Existing Podfile');
764
      projectUnderTest.ios.podfileLock
765
        ..createSync()
766
        ..writeAsStringSync('Existing lock file.');
767
      projectUnderTest.ios.podManifestLock
768
        ..createSync(recursive: true)
769
        ..writeAsStringSync('Existing lock file.');
770 771 772 773 774 775 776
      fakeProcessManager.addCommand(
        const FakeCommand(
          command: <String>['pod', 'install', '--verbose'],
          workingDirectory: 'project/ios',
          environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'},
          exitCode: 1,
        ),
777
      );
778

779
      await expectLater(cocoaPodsUnderTest.processPods(
780
        xcodeProject: projectUnderTest.ios,
781
        buildMode: BuildMode.debug,
782 783
      ), throwsToolExit(message: 'Error running pod install'));
      expect(projectUnderTest.ios.podManifestLock.existsSync(), isFalse);
784 785
    });
  });
786 787
}

788 789 790 791 792 793 794 795 796
class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterpreter {
  FakeXcodeProjectInterpreter({this.isInstalled = true, this.buildSettings = const <String, String>{}});

  @override
  final bool isInstalled;

  @override
  Future<Map<String, String>> getBuildSettings(
    String projectPath, {
797
    XcodeProjectBuildContext buildContext,
798 799 800 801 802
    Duration timeout = const Duration(minutes: 1),
  }) async => buildSettings;

  final Map<String, String> buildSettings;
}