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

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

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

void main() {
23 24 25 26 27
  late FileSystem fileSystem;
  late FakeProcessManager fakeProcessManager;
  late CocoaPods cocoaPodsUnderTest;
  late BufferLogger logger;
  late TestUsage usage;
28 29

  void pretendPodVersionFails() {
30 31 32 33 34 35
    fakeProcessManager.addCommand(
      const FakeCommand(
        command: <String>['pod', '--version'],
        exitCode: 1,
      ),
    );
36 37 38
  }

  void pretendPodVersionIs(String versionText) {
39 40 41 42 43 44
    fakeProcessManager.addCommand(
      FakeCommand(
        command: const <String>['pod', '--version'],
        stdout: versionText,
      ),
    );
45
  }
46

47
  void podsIsInHomeDir() {
48
    fileSystem.directory(fileSystem.path.join(
49 50 51 52
      '.cocoapods',
      'repos',
      'master',
    )).createSync(recursive: true);
53 54
  }

55 56 57 58 59 60 61 62
  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;
  }

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

94
  void pretendPodIsNotInstalled() {
95 96 97 98 99 100
    fakeProcessManager.addCommand(
      const FakeCommand(
        command: <String>['which', 'pod'],
        exitCode: 1,
      ),
    );
101 102
  }

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

117
  void pretendPodIsInstalled() {
118 119 120 121 122
    fakeProcessManager.addCommands(<FakeCommand>[
      const FakeCommand(
        command: <String>['which', 'pod'],
      ),
    ]);
123 124
  }

125
  group('Evaluate installation', () {
126
    testWithoutContext('detects not installed, if pod exec does not exist', () async {
127 128 129 130
      pretendPodIsNotInstalled();
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.notInstalled);
    });

131
    testWithoutContext('detects not installed, if pod is installed but version fails', () async {
132
      pretendPodIsInstalled();
133
      pretendPodVersionFails();
134
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.brokenInstall);
135 136
    });

137
    testWithoutContext('detects installed', () async {
138
      pretendPodIsInstalled();
139 140 141 142
      pretendPodVersionIs('0.0.1');
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, isNot(CocoaPodsStatus.notInstalled));
    });

143
    testWithoutContext('detects unknown version', () async {
144
      pretendPodIsInstalled();
145 146 147 148
      pretendPodVersionIs('Plugin loaded.\n1.5.3');
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.unknownVersion);
    });

149
    testWithoutContext('detects below minimum version', () async {
150
      pretendPodIsInstalled();
151
      pretendPodVersionIs('1.9.0');
152 153 154
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.belowMinimumVersion);
    });

155
    testWithoutContext('detects below recommended version', () async {
156
      pretendPodIsInstalled();
157
      pretendPodVersionIs('1.10.5');
158 159 160
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.belowRecommendedVersion);
    });

161
    testWithoutContext('detects at recommended version', () async {
162
      pretendPodIsInstalled();
163
      pretendPodVersionIs('1.11.0');
164 165 166
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.recommended);
    });

167
    testWithoutContext('detects above recommended version', () async {
168
      pretendPodIsInstalled();
169
      pretendPodVersionIs('1.11.1');
170 171
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.recommended);
    });
172 173
  });

174
  group('Setup Podfile', () {
175 176
    testUsingContext('creates objective-c Podfile when not present', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
177
      await cocoaPodsUnderTest.setupPodfile(projectUnderTest.ios);
178

179
      expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Objective-C iOS podfile template');
180
    });
181

182 183
    testUsingContext('creates swift Podfile if swift', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
184 185 186 187 188 189 190 191 192 193 194
      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,
      );
195

196
      final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project'));
197
      await cocoaPodsUnderTest.setupPodfile(project.ios);
198

199
      expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Swift iOS podfile template');
200
    });
201

202 203
    testUsingContext('creates macOS Podfile when not present', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
204
      projectUnderTest.macos.xcodeProject.createSync(recursive: true);
205
      await cocoaPodsUnderTest.setupPodfile(projectUnderTest.macos);
206 207 208 209

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

210 211
    testUsingContext('does not recreate Podfile when already present', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
212
      projectUnderTest.ios.podfile..createSync()..writeAsStringSync('Existing Podfile');
213

214
      final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project'));
215
      await cocoaPodsUnderTest.setupPodfile(project.ios);
216

217
      expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Existing Podfile');
218 219
    });

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

231
      final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project'));
232
      await cocoaPodsUnderTest.setupPodfile(project.ios);
233

234
      expect(projectUnderTest.ios.podfile.existsSync(), false);
235 236
    });

237 238
    testUsingContext('includes Pod config in xcconfig files, if not present', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
239 240 241 242 243 244 245
      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');
246

247
      final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project'));
248
      await cocoaPodsUnderTest.setupPodfile(project.ios);
249

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

260 261
    testUsingContext('does not include Pod config in xcconfig files, if legacy non-option include present', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
262 263 264 265 266 267 268 269 270 271 272
      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);

273
      final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project'));
274 275 276 277 278 279 280 281 282 283 284
      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));
    });
285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310

    testUsingContext('does not include Pod config in xcconfig files, if flavor include present', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
      projectUnderTest.ios.podfile..createSync()..writeAsStringSync('Existing Podfile');

      const String flavorDebugInclude = '#include? "Pods/Target Support Files/Pods-Free App/Pods-Free App.debug free.xcconfig"';
      projectUnderTest.ios.xcodeConfigFor('Debug')
        ..createSync(recursive: true)
        ..writeAsStringSync(flavorDebugInclude);
      const String flavorReleaseInclude = '#include? "Pods/Target Support Files/Pods-Free App/Pods-Free App.release free.xcconfig"';
      projectUnderTest.ios.xcodeConfigFor('Release')
        ..createSync(recursive: true)
        ..writeAsStringSync(flavorReleaseInclude);

      final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project'));
      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('Pods-Runner/Pods-Runner.debug')));
      expect(debugContents, equals(flavorDebugInclude));
      final String releaseContents = projectUnderTest.ios.xcodeConfigFor('Release').readAsStringSync();
      expect(releaseContents, isNot(contains('Pods-Runner/Pods-Runner.release')));
      expect(releaseContents, equals(flavorReleaseInclude));
    });
311 312 313 314
  });

  group('Update xcconfig', () {
    testUsingContext('includes Pod config in xcconfig files, if the user manually added Pod dependencies without using Flutter plugins', () async {
315
      final FlutterProject projectUnderTest = setupProjectUnderTest();
316
      fileSystem.file(fileSystem.path.join('project', 'foo', '.packages'))
317 318
        ..createSync(recursive: true)
        ..writeAsStringSync('\n');
319 320 321 322 323 324 325 326 327 328
      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');

329
      final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project'));
330
      await injectPlugins(project, iosPlatform: true);
331 332 333

      final String debugContents = projectUnderTest.ios.xcodeConfigFor('Debug').readAsStringSync();
      expect(debugContents, contains(
334
          '#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"\n'));
335 336
      expect(debugContents, contains('Existing debug config'));
      final String releaseContents = projectUnderTest.ios.xcodeConfigFor('Release').readAsStringSync();
337
      expect(releaseContents, contains(
338
          '#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"\n'));
339 340
      expect(releaseContents, contains('Existing release config'));
    }, overrides: <Type, Generator>{
341
      FileSystem: () => fileSystem,
342
      ProcessManager: () => FakeProcessManager.any(),
343 344 345 346
    });
  });

  group('Process pods', () {
347 348 349 350
    setUp(() {
      podsIsInHomeDir();
    });

351 352
    testUsingContext('throwsToolExit if CocoaPods is not installed', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
353
      pretendPodIsNotInstalled();
354
      projectUnderTest.ios.podfile.createSync();
355
      await expectLater(cocoaPodsUnderTest.processPods(
356
        xcodeProject: projectUnderTest.ios,
357
        buildMode: BuildMode.debug,
358
      ), throwsToolExit(message: 'CocoaPods not installed or not in valid state'));
359
      expect(fakeProcessManager, hasNoRemainingExpectations);
360
      expect(fakeProcessManager, hasNoRemainingExpectations);
361 362
    });

363 364
    testUsingContext('throwsToolExit if CocoaPods install is broken', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
365 366
      pretendPodIsBroken();
      projectUnderTest.ios.podfile.createSync();
367
      await expectLater(cocoaPodsUnderTest.processPods(
368
        xcodeProject: projectUnderTest.ios,
369
        buildMode: BuildMode.debug,
370
      ), throwsToolExit(message: 'CocoaPods not installed or not in valid state'));
371
      expect(fakeProcessManager, hasNoRemainingExpectations);
372
      expect(fakeProcessManager, hasNoRemainingExpectations);
373
    });
374

375 376
    testUsingContext('exits if Podfile creates the Flutter engine symlink', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
377
      fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile'))
378 379 380 381 382 383 384
        ..createSync()
        ..writeAsStringSync('Existing Podfile');

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

385
      await expectLater(cocoaPodsUnderTest.processPods(
386
        xcodeProject: projectUnderTest.ios,
387 388
        buildMode: BuildMode.debug,
      ), throwsToolExit(message: 'Podfile is out of date'));
389
      expect(fakeProcessManager, hasNoRemainingExpectations);
390 391
    });

392 393
    testUsingContext('exits if iOS Podfile parses .flutter-plugins', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
394
      fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile'))
395
        ..createSync()
396
        ..writeAsStringSync("plugin_pods = parse_KV_file('../.flutter-plugins')");
397

398
      await expectLater(cocoaPodsUnderTest.processPods(
399
        xcodeProject: projectUnderTest.ios,
400 401
        buildMode: BuildMode.debug,
      ), throwsToolExit(message: 'Podfile is out of date'));
402
      expect(fakeProcessManager, hasNoRemainingExpectations);
403 404
    });

405 406
    testUsingContext('prints warning if macOS Podfile parses .flutter-plugins', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
407
      pretendPodIsInstalled();
408
      pretendPodVersionIs('100.0.0');
409 410
      fakeProcessManager.addCommands(const <FakeCommand>[
        FakeCommand(
411 412
          command: <String>['pod', 'install', '--verbose'],
        ),
413 414 415 416
        FakeCommand(
          command: <String>['touch', 'project/macos/Podfile.lock'],
        ),
      ]);
417

418
      projectUnderTest.macos.podfile
419
        ..createSync()
420
        ..writeAsStringSync("plugin_pods = parse_KV_file('../.flutter-plugins')");
421 422 423
      projectUnderTest.macos.podfileLock
        ..createSync()
        ..writeAsStringSync('Existing lock file.');
424 425 426 427 428 429

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

430 431
      expect(logger.warningText, contains('Warning: Podfile is out of date'));
      expect(logger.warningText, contains('rm macos/Podfile'));
432
      expect(fakeProcessManager, hasNoRemainingExpectations);
433 434
    });

435 436
    testUsingContext('throws, if Podfile is missing.', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
437 438 439 440
      await expectLater(cocoaPodsUnderTest.processPods(
        xcodeProject: projectUnderTest.ios,
        buildMode: BuildMode.debug,
      ), throwsToolExit(message: 'Podfile missing'));
441
      expect(fakeProcessManager, hasNoRemainingExpectations);
442
    });
443

444 445
    testUsingContext('throws, if specs repo is outdated.', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
446
      pretendPodIsInstalled();
447
      pretendPodVersionIs('100.0.0');
448
      fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile'))
449
        ..createSync()
450
        ..writeAsStringSync('Existing Podfile');
451

452 453 454 455 456 457 458 459 460 461
      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: '''
462 463 464 465 466 467 468 469 470 471 472 473 474
[!] 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.''',
475 476 477 478 479 480 481 482 483 484 485 486
        ),
      );

      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"),
      );
487
    });
488

489 490 491
    final Map<String, String> possibleErrors = <String, String>{
      'symbol not found': 'LoadError - dlsym(0x7fbbeb6837d0, Init_ffi_c): symbol not found - /Library/Ruby/Gems/2.6.0/gems/ffi-1.13.1/lib/ffi_c.bundle',
      'incompatible architecture': "LoadError - (mach-o file, but is an incompatible architecture (have 'arm64', need 'x86_64')), '/usr/lib/ffi_c.bundle' (no such file) - /Library/Ruby/Gems/2.6.0/gems/ffi-1.15.4/lib/ffi_c.bundle",
492
      'bus error': '/Library/Ruby/Gems/2.6.0/gems/ffi-1.15.5/lib/ffi/library.rb:275: [BUG] Bus Error at 0x000000010072c000',
493 494 495 496 497
    };
    possibleErrors.forEach((String errorName, String cocoaPodsError) {
      testUsingContext('ffi $errorName failure on ARM macOS prompts gem install', () async {
        final FlutterProject projectUnderTest = setupProjectUnderTest();
        pretendPodIsInstalled();
498
        pretendPodVersionIs('100.0.0');
499 500 501 502 503 504 505 506 507 508 509 510 511
        fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile'))
          ..createSync()
          ..writeAsStringSync('Existing Podfile');

        fakeProcessManager.addCommands(<FakeCommand>[
          FakeCommand(
            command: const <String>['pod', 'install', '--verbose'],
            workingDirectory: 'project/ios',
            environment: const <String, String>{
              'COCOAPODS_DISABLE_STATS': 'true',
              'LANG': 'en_US.UTF-8',
            },
            exitCode: 1,
512
            stderr: cocoaPodsError,
513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533
          ),
          const FakeCommand(
            command: <String>['which', 'sysctl'],
          ),
          const FakeCommand(
            command: <String>['sysctl', 'hw.optional.arm64'],
            stdout: 'hw.optional.arm64: 1',
          ),
        ]);

        await expectToolExitLater(
          cocoaPodsUnderTest.processPods(
            xcodeProject: projectUnderTest.ios,
            buildMode: BuildMode.debug,
          ),
          equals('Error running pod install'),
        );
        expect(
          logger.errorText,
          contains('set up CocoaPods for ARM macOS'),
        );
534 535 536 537
        expect(
          logger.errorText,
          contains('enable-libffi-alloc'),
        );
538 539
        expect(usage.events, contains(const TestUsageEvent('pod-install-failure', 'arm-ffi')));
      });
540 541
    });

542 543
    testUsingContext('ffi failure on x86 macOS does not prompt gem install', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
544
      pretendPodIsInstalled();
545
      pretendPodVersionIs('100.0.0');
546 547 548 549
      fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile'))
        ..createSync()
        ..writeAsStringSync('Existing Podfile');

550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568
      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,
        ),
      ]);
569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585

      // 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);
    });

586 587
    testUsingContext('run pod install, if Podfile.lock is missing', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
588
      pretendPodIsInstalled();
589
      pretendPodVersionIs('100.0.0');
590
      projectUnderTest.ios.podfile
591
        ..createSync()
592
        ..writeAsStringSync('Existing Podfile');
593
      projectUnderTest.ios.podManifestLock
594
        ..createSync(recursive: true)
595
        ..writeAsStringSync('Existing lock file.');
596

597 598
      fakeProcessManager.addCommands(const <FakeCommand>[
        FakeCommand(
599 600 601 602
          command: <String>['pod', 'install', '--verbose'],
          workingDirectory: 'project/ios',
          environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'},
        ),
603
      ]);
604
      final bool didInstall = await cocoaPodsUnderTest.processPods(
605
        xcodeProject: projectUnderTest.ios,
606
        buildMode: BuildMode.debug,
607
        dependenciesChanged: false,
608
      );
609
      expect(didInstall, isTrue);
610
      expect(fakeProcessManager, hasNoRemainingExpectations);
611
    });
612

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

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

671 672
    testUsingContext('runs pod install, if Manifest.lock different from Podspec.lock', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
673
      pretendPodIsInstalled();
674
      pretendPodVersionIs('100.0.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('Different 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: false,
698
      );
699
      expect(didInstall, isTrue);
700
      expect(fakeProcessManager, hasNoRemainingExpectations);
701 702
    });

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

734 735
    testUsingContext('runs pod install, if Podfile.lock is older than Podfile', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
736
      pretendPodIsInstalled();
737
      pretendPodVersionIs('100.0.0');
738
      projectUnderTest.ios.podfile
739 740
        ..createSync()
        ..writeAsStringSync('Existing Podfile');
741
      projectUnderTest.ios.podfileLock
742 743
        ..createSync()
        ..writeAsStringSync('Existing lock file.');
744
      projectUnderTest.ios.podManifestLock
745 746
        ..createSync(recursive: true)
        ..writeAsStringSync('Existing lock file.');
747
      await Future<void>.delayed(const Duration(milliseconds: 10));
748
      projectUnderTest.ios.podfile
749
        .writeAsStringSync('Updated Podfile');
750 751
      fakeProcessManager.addCommands(const <FakeCommand>[
        FakeCommand(
752 753 754 755
          command: <String>['pod', 'install', '--verbose'],
          workingDirectory: 'project/ios',
          environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'},
        ),
756 757 758 759
        FakeCommand(
          command: <String>['touch', 'project/ios/Podfile.lock'],
        ),
      ]);
760
      await cocoaPodsUnderTest.processPods(
761
        xcodeProject: projectUnderTest.ios,
762
        buildMode: BuildMode.debug,
763 764
        dependenciesChanged: false,
      );
765
      expect(fakeProcessManager, hasNoRemainingExpectations);
766 767
    });

768 769
    testUsingContext('skips pod install, if nothing changed', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
770
      projectUnderTest.ios.podfile
771
        ..createSync()
772
        ..writeAsStringSync('Existing Podfile');
773
      projectUnderTest.ios.podfileLock
774
        ..createSync()
775
        ..writeAsStringSync('Existing lock file.');
776
      projectUnderTest.ios.podManifestLock
777
        ..createSync(recursive: true)
778
        ..writeAsStringSync('Existing lock file.');
779
      final bool didInstall = await cocoaPodsUnderTest.processPods(
780
        xcodeProject: projectUnderTest.ios,
781
        buildMode: BuildMode.debug,
782
        dependenciesChanged: false,
783
      );
784
      expect(didInstall, isFalse);
785
      expect(fakeProcessManager, hasNoRemainingExpectations);
786
    });
787

788 789
    testUsingContext('a failed pod install deletes Pods/Manifest.lock', () async {
      final FlutterProject projectUnderTest = setupProjectUnderTest();
790
      pretendPodIsInstalled();
791
      pretendPodVersionIs('100.0.0');
792
      projectUnderTest.ios.podfile
793
        ..createSync()
794
        ..writeAsStringSync('Existing Podfile');
795
      projectUnderTest.ios.podfileLock
796
        ..createSync()
797
        ..writeAsStringSync('Existing lock file.');
798
      projectUnderTest.ios.podManifestLock
799
        ..createSync(recursive: true)
800
        ..writeAsStringSync('Existing lock file.');
801 802 803 804 805 806 807
      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,
        ),
808
      );
809

810
      await expectLater(cocoaPodsUnderTest.processPods(
811
        xcodeProject: projectUnderTest.ios,
812
        buildMode: BuildMode.debug,
813 814
      ), throwsToolExit(message: 'Error running pod install'));
      expect(projectUnderTest.ios.podManifestLock.existsSync(), isFalse);
815 816
    });
  });
817 818
}

819 820 821 822 823 824 825 826 827
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, {
828
    XcodeProjectBuildContext? buildContext,
829 830 831 832 833
    Duration timeout = const Duration(minutes: 1),
  }) async => buildSettings;

  final Map<String, String> buildSettings;
}