cocoapods_test.dart 26.1 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
import 'package:flutter_tools/src/base/common.dart';
8
import 'package:flutter_tools/src/base/io.dart';
9 10
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
11
import 'package:flutter_tools/src/cache.dart';
12
import 'package:flutter_tools/src/globals.dart' as globals;
13 14
import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:flutter_tools/src/macos/cocoapods.dart';
15
import 'package:flutter_tools/src/plugins.dart';
16
import 'package:flutter_tools/src/project.dart';
17 18
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
19

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

23
typedef InvokeProcess = Future<ProcessResult> Function();
24

25
void main() {
26
  FileSystem fileSystem;
27
  ProcessManager mockProcessManager;
28
  MockXcodeProjectInterpreter mockXcodeProjectInterpreter;
29
  FlutterProject projectUnderTest;
30
  CocoaPods cocoaPodsUnderTest;
31
  InvokeProcess resultOfPodVersion;
32
  BufferLogger logger;
33 34 35 36 37 38 39 40

  void pretendPodVersionFails() {
    resultOfPodVersion = () async => exitsWithError();
  }

  void pretendPodVersionIs(String versionText) {
    resultOfPodVersion = () async => exitsHappy(versionText);
  }
41

42
  void podsIsInHomeDir() {
43
    fileSystem.directory(fileSystem.path.join(
44 45 46 47
      '.cocoapods',
      'repos',
      'master',
    )).createSync(recursive: true);
48 49 50
  }

  String podsIsInCustomDir({String cocoapodsReposDir}) {
51
    cocoapodsReposDir ??= fileSystem.path.join(
52 53 54 55
      'cache',
      'cocoapods',
      'repos',
    );
56
    fileSystem.directory(fileSystem.path.join(cocoapodsReposDir, 'master')).createSync(recursive: true);
57 58 59
    return cocoapodsReposDir;
  }

60
  setUp(() async {
61
    Cache.flutterRoot = 'flutter';
62
    fileSystem = MemoryFileSystem.test();
63
    mockProcessManager = MockProcessManager();
64
    logger = BufferLogger.test();
65
    mockXcodeProjectInterpreter = MockXcodeProjectInterpreter();
66
    projectUnderTest = FlutterProject.fromDirectory(fileSystem.directory('project'));
67
    projectUnderTest.ios.xcodeProject.createSync(recursive: true);
68
    cocoaPodsUnderTest = CocoaPods(
69
      fileSystem: fileSystem,
70 71 72 73 74 75
      processManager: mockProcessManager,
      logger: logger,
      platform: FakePlatform(),
      xcodeProjectInterpreter: mockXcodeProjectInterpreter,
      timeoutConfiguration: const TimeoutConfiguration(),
    );
76
    pretendPodVersionIs('1.8.0');
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 89 90 91
      Cache.flutterRoot, 'packages', 'flutter_tools', 'templates', 'cocoapods', 'Podfile-macos',
    ))
        ..createSync(recursive: true)
        ..writeAsStringSync('macOS podfile template');
92 93
    when(mockProcessManager.run(
      <String>['pod', '--version'],
94 95
      workingDirectory: anyNamed('workingDirectory'),
      environment: anyNamed('environment'),
96
    )).thenAnswer((_) => resultOfPodVersion());
97 98 99
    when(mockProcessManager.run(
      <String>['pod', 'install', '--verbose'],
      workingDirectory: 'project/ios',
100
      environment: <String, String>{'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'},
101
    )).thenAnswer((_) async => exitsHappy());
102 103 104
    when(mockProcessManager.run(
      <String>['pod', 'install', '--verbose'],
      workingDirectory: 'project/macos',
105
      environment: <String, String>{'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'},
106
    )).thenAnswer((_) async => exitsHappy());
107
    fileSystem.file('.packages').writeAsStringSync('\n');
108 109
  });

110 111 112 113 114 115 116 117
  void pretendPodIsNotInstalled() {
    when(mockProcessManager.run(
      <String>['which', 'pod'],
      workingDirectory: anyNamed('workingDirectory'),
      environment: anyNamed('environment'),
    )).thenAnswer((_) async => exitsWithError());
  }

118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
  void pretendPodIsBroken() {
    // it is present
    when(mockProcessManager.run(
      <String>['which', 'pod'],
      workingDirectory: anyNamed('workingDirectory'),
      environment: anyNamed('environment'),
    )).thenAnswer((_) async => exitsHappy());

    // but is not working
    when(mockProcessManager.run(
      <String>['pod', '--version'],
      workingDirectory: anyNamed('workingDirectory'),
      environment: anyNamed('environment'),
    )).thenAnswer((_) async => exitsWithError());
  }

134 135 136 137 138 139 140 141
  void pretendPodIsInstalled() {
    when(mockProcessManager.run(
      <String>['which', 'pod'],
      workingDirectory: anyNamed('workingDirectory'),
      environment: anyNamed('environment'),
    )).thenAnswer((_) async => exitsHappy());
  }

142
  group('Evaluate installation', () {
143 144 145 146 147
    setUp(() {
      // Assume all binaries can run
      when(mockProcessManager.canRun(any)).thenReturn(true);
    });

148
    testWithoutContext('detects not installed, if pod exec does not exist', () async {
149 150 151 152
      pretendPodIsNotInstalled();
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.notInstalled);
    });

153
    testWithoutContext('detects not installed, if pod is installed but version fails', () async {
154
      pretendPodIsInstalled();
155
      pretendPodVersionFails();
156
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.brokenInstall);
157 158
    });

159
    testWithoutContext('detects installed', () async {
160
      pretendPodIsInstalled();
161 162 163 164
      pretendPodVersionIs('0.0.1');
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, isNot(CocoaPodsStatus.notInstalled));
    });

165
    testWithoutContext('detects unknown version', () async {
166
      pretendPodIsInstalled();
167 168 169 170
      pretendPodVersionIs('Plugin loaded.\n1.5.3');
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.unknownVersion);
    });

171
    testWithoutContext('detects below minimum version', () async {
172
      pretendPodIsInstalled();
173
      pretendPodVersionIs('1.5.0');
174 175 176
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.belowMinimumVersion);
    });

177
    testWithoutContext('detects below recommended version', () async {
178
      pretendPodIsInstalled();
179
      pretendPodVersionIs('1.6.0');
180 181 182
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.belowRecommendedVersion);
    });

183
    testWithoutContext('detects at recommended version', () async {
184 185
      pretendPodIsInstalled();
      pretendPodVersionIs('1.8.0');
186 187 188
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.recommended);
    });

189
    testWithoutContext('detects above recommended version', () async {
190
      pretendPodIsInstalled();
191
      pretendPodVersionIs('1.8.1');
192 193
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.recommended);
    });
194

195
    testWithoutContext('detects initialized over 1.8.0', () async {
196 197 198 199
      pretendPodIsInstalled();
      pretendPodVersionIs('1.8.0');
      expect(await cocoaPodsUnderTest.isCocoaPodsInitialized, isTrue);
    });
200 201
  });

202
  group('Setup Podfile', () {
203 204 205 206 207
    setUp(() {
      when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
    });

    testWithoutContext('creates objective-c Podfile when not present', () async {
208
      when(mockXcodeProjectInterpreter.getBuildSettings(any, scheme: null, timeout: anyNamed('timeout')))
209
        .thenAnswer((_) async => <String, String>{});
210
      await cocoaPodsUnderTest.setupPodfile(projectUnderTest.ios);
211

212
      expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Objective-C iOS podfile template');
213
    });
214

215
    testUsingContext('creates swift Podfile if swift', () async {
216
      when(mockXcodeProjectInterpreter.getBuildSettings(any, scheme: null, timeout: anyNamed('timeout')))
217
        .thenAnswer((_) async => <String, String>{
218
          'SWIFT_VERSION': '5.0',
219
        });
220

221
      final FlutterProject project = FlutterProject.fromPath('project');
222
      await cocoaPodsUnderTest.setupPodfile(project.ios);
223

224
      expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Swift iOS podfile template');
225
    }, overrides: <Type, Generator>{
226
      FileSystem: () => fileSystem,
227
      ProcessManager: () => FakeProcessManager.any(),
228 229
      XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
    });
230

231
    testWithoutContext('creates macOS Podfile when not present', () async {
232
      projectUnderTest.macos.xcodeProject.createSync(recursive: true);
233
      await cocoaPodsUnderTest.setupPodfile(projectUnderTest.macos);
234 235 236 237

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

238
    testUsingContext('does not recreate Podfile when already present', () async {
239
      projectUnderTest.ios.podfile..createSync()..writeAsStringSync('Existing Podfile');
240

241
      final FlutterProject project = FlutterProject.fromPath('project');
242
      await cocoaPodsUnderTest.setupPodfile(project.ios);
243

244
      expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Existing Podfile');
245
    }, overrides: <Type, Generator>{
246
      FileSystem: () => fileSystem,
247
      ProcessManager: () => FakeProcessManager.any(),
248 249
    });

250
    testUsingContext('does not create Podfile when we cannot interpret Xcode projects', () async {
251
      when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false);
252

253
      final FlutterProject project = FlutterProject.fromPath('project');
254
      await cocoaPodsUnderTest.setupPodfile(project.ios);
255

256
      expect(projectUnderTest.ios.podfile.existsSync(), false);
257
    }, overrides: <Type, Generator>{
258
      FileSystem: () => fileSystem,
259
      ProcessManager: () => FakeProcessManager.any(),
260 261 262
      XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
    });

263
    testUsingContext('includes Pod config in xcconfig files, if not present', () async {
264 265 266 267 268 269 270
      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');
271

272
      final FlutterProject project = FlutterProject.fromPath('project');
273
      await cocoaPodsUnderTest.setupPodfile(project.ios);
274

275
      final String debugContents = projectUnderTest.ios.xcodeConfigFor('Debug').readAsStringSync();
276 277 278
      expect(debugContents, contains(
          '#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"\n'));
      expect(debugContents, contains('Existing debug config'));
279
      final String releaseContents = projectUnderTest.ios.xcodeConfigFor('Release').readAsStringSync();
280 281 282 283
      expect(releaseContents, contains(
          '#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"\n'));
      expect(releaseContents, contains('Existing release config'));
    }, overrides: <Type, Generator>{
284
      FileSystem: () => fileSystem,
285
      ProcessManager: () => FakeProcessManager.any(),
286 287 288 289 290
    });
  });

  group('Update xcconfig', () {
    testUsingContext('includes Pod config in xcconfig files, if the user manually added Pod dependencies without using Flutter plugins', () async {
291 292 293
      globals.fs.file(globals.fs.path.join('project', 'foo', '.packages'))
        ..createSync(recursive: true)
        ..writeAsStringSync('\n');
294 295 296 297 298 299 300 301 302 303
      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');

304
      final FlutterProject project = FlutterProject.fromPath('project');
305
      await injectPlugins(project, checkProjects: true);
306 307 308 309 310 311

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

  group('Process pods', () {
322 323
    setUp(() {
      podsIsInHomeDir();
324 325
      // Assume all binaries can run
      when(mockProcessManager.canRun(any)).thenReturn(true);
326 327
    });

328
    testWithoutContext('throwsToolExit if CocoaPods is not installed', () async {
329
      pretendPodIsNotInstalled();
330
      projectUnderTest.ios.podfile.createSync();
331 332 333 334 335 336 337 338 339 340 341 342
      final Function invokeProcessPods = () async => await cocoaPodsUnderTest.processPods(
        xcodeProject: projectUnderTest.ios,
        engineDir: 'engine/path',
      );
      expect(invokeProcessPods, throwsToolExit());
      verifyNever(mockProcessManager.run(
      argThat(containsAllInOrder(<String>['pod', 'install'])),
        workingDirectory: anyNamed('workingDirectory'),
        environment: anyNamed('environment'),
      ));
    });

343
    testWithoutContext('throwsToolExit if CocoaPods install is broken', () async {
344 345 346
      pretendPodIsBroken();
      projectUnderTest.ios.podfile.createSync();
      final Function invokeProcessPods = () async => await cocoaPodsUnderTest.processPods(
347 348
        xcodeProject: projectUnderTest.ios,
        engineDir: 'engine/path',
349
      );
350
      expect(invokeProcessPods, throwsToolExit());
351
      verifyNever(mockProcessManager.run(
352
      argThat(containsAllInOrder(<String>['pod', 'install'])),
353
        workingDirectory: anyNamed('workingDirectory'),
354
        environment: anyNamed('environment'),
355
      ));
356
    });
357

358
    testWithoutContext('prints warning, if Podfile creates the Flutter engine symlink', () async {
359 360
      pretendPodIsInstalled();

361
      fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile'))
362 363 364 365 366 367 368 369 370 371 372
        ..createSync()
        ..writeAsStringSync('Existing Podfile');

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

      await cocoaPodsUnderTest.processPods(
        xcodeProject: projectUnderTest.ios,
        engineDir: 'engine/path',
      );
373
      expect(logger.errorText, contains('Warning: Podfile is out of date'));
374 375
    });

376 377 378
    testWithoutContext('prints warning, if Podfile parses .flutter-plugins', () async {
      pretendPodIsInstalled();

379
      fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile'))
380 381 382 383 384 385 386 387 388 389
        ..createSync()
        ..writeAsStringSync('plugin_pods = parse_KV_file(\'../.flutter-plugins\')');

      await cocoaPodsUnderTest.processPods(
        xcodeProject: projectUnderTest.ios,
        engineDir: 'engine/path',
      );
      expect(logger.errorText, contains('Warning: Podfile is out of date'));
    });

390
    testWithoutContext('throws, if Podfile is missing.', () async {
391
      pretendPodIsInstalled();
392 393
      try {
        await cocoaPodsUnderTest.processPods(
394 395
          xcodeProject: projectUnderTest.ios,
          engineDir: 'engine/path',
396
        );
397
        fail('ToolExit expected');
398
      } on Exception catch (e) {
Dan Field's avatar
Dan Field committed
399
        expect(e, isA<ToolExit>());
400
        verifyNever(mockProcessManager.run(
401
        argThat(containsAllInOrder(<String>['pod', 'install'])),
402
          workingDirectory: anyNamed('workingDirectory'),
403
          environment: anyNamed('environment'),
404 405
        ));
      }
406
    });
407

408
    testWithoutContext('throws, if specs repo is outdated.', () async {
409
      pretendPodIsInstalled();
410
      fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile'))
411
        ..createSync()
412
        ..writeAsStringSync('Existing Podfile');
413 414 415 416

      when(mockProcessManager.run(
        <String>['pod', 'install', '--verbose'],
        workingDirectory: 'project/ios',
417 418 419
        environment: <String, String>{
          'FLUTTER_FRAMEWORK_DIR': 'engine/path',
          'COCOAPODS_DISABLE_STATS': 'true',
420
          'LANG': 'en_US.UTF-8',
421
        },
422
      )).thenAnswer((_) async => exitsWithError(
423 424 425 426 427 428 429 430 431 432 433 434 435 436
        '''
[!] 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.''',
437
      ));
438 439
      try {
        await cocoaPodsUnderTest.processPods(
440 441
          xcodeProject: projectUnderTest.ios,
          engineDir: 'engine/path',
442 443
        );
        fail('ToolExit expected');
444
      } on Exception catch (e) {
Dan Field's avatar
Dan Field committed
445
        expect(e, isA<ToolExit>());
446
        expect(
447
          logger.errorText,
448 449
          contains("CocoaPods's specs repository is too out-of-date to satisfy dependencies"),
        );
450
      }
451
    });
452

453
    testWithoutContext('run pod install, if Podfile.lock is missing', () async {
454
      pretendPodIsInstalled();
455
      projectUnderTest.ios.podfile
456
        ..createSync()
457
        ..writeAsStringSync('Existing Podfile');
458
      projectUnderTest.ios.podManifestLock
459
        ..createSync(recursive: true)
460
        ..writeAsStringSync('Existing lock file.');
461
      final bool didInstall = await cocoaPodsUnderTest.processPods(
462 463
        xcodeProject: projectUnderTest.ios,
        engineDir: 'engine/path',
464
        dependenciesChanged: false,
465
      );
466
      expect(didInstall, isTrue);
467
      verify(mockProcessManager.run(
468 469
        <String>['pod', 'install', '--verbose'],
        workingDirectory: 'project/ios',
470
        environment: <String, String>{'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'},
471
      ));
472
    });
473

474
    testWithoutContext('runs pod install, if Manifest.lock is missing', () async {
475
      pretendPodIsInstalled();
476
      projectUnderTest.ios.podfile
477
        ..createSync()
478
        ..writeAsStringSync('Existing Podfile');
479
      projectUnderTest.ios.podfileLock
480
        ..createSync()
481
        ..writeAsStringSync('Existing lock file.');
482
      final bool didInstall = await cocoaPodsUnderTest.processPods(
483 484
        xcodeProject: projectUnderTest.ios,
        engineDir: 'engine/path',
485
        dependenciesChanged: false,
486
      );
487
      expect(didInstall, isTrue);
488 489 490 491 492 493
      verify(mockProcessManager.run(
        <String>['pod', 'install', '--verbose'],
        workingDirectory: 'project/ios',
        environment: <String, String>{
          'FLUTTER_FRAMEWORK_DIR': 'engine/path',
          'COCOAPODS_DISABLE_STATS': 'true',
494
          'LANG': 'en_US.UTF-8',
495 496 497 498
        },
      ));
    });

499
    testWithoutContext('runs pod install, if Manifest.lock different from Podspec.lock', () async {
500
      pretendPodIsInstalled();
501
      projectUnderTest.ios.podfile
502
        ..createSync()
503
        ..writeAsStringSync('Existing Podfile');
504
      projectUnderTest.ios.podfileLock
505
        ..createSync()
506
        ..writeAsStringSync('Existing lock file.');
507
      projectUnderTest.ios.podManifestLock
508
        ..createSync(recursive: true)
509
        ..writeAsStringSync('Different lock file.');
510
      final bool didInstall = await cocoaPodsUnderTest.processPods(
511 512
        xcodeProject: projectUnderTest.ios,
        engineDir: 'engine/path',
513
        dependenciesChanged: false,
514
      );
515
      expect(didInstall, isTrue);
516
      verify(mockProcessManager.run(
517 518
        <String>['pod', 'install', '--verbose'],
        workingDirectory: 'project/ios',
519 520 521
        environment: <String, String>{
          'FLUTTER_FRAMEWORK_DIR': 'engine/path',
          'COCOAPODS_DISABLE_STATS': 'true',
522
          'LANG': 'en_US.UTF-8',
523
        },
524
      ));
525 526
    });

527
    testWithoutContext('runs pod install, if flutter framework changed', () async {
528
      pretendPodIsInstalled();
529
      projectUnderTest.ios.podfile
530
        ..createSync()
531
        ..writeAsStringSync('Existing Podfile');
532
      projectUnderTest.ios.podfileLock
533
        ..createSync()
534
        ..writeAsStringSync('Existing lock file.');
535
      projectUnderTest.ios.podManifestLock
536
        ..createSync(recursive: true)
537
        ..writeAsStringSync('Existing lock file.');
538
      final bool didInstall = await cocoaPodsUnderTest.processPods(
539 540
        xcodeProject: projectUnderTest.ios,
        engineDir: 'engine/path',
541
        dependenciesChanged: true,
542
      );
543
      expect(didInstall, isTrue);
544 545 546 547 548 549
      verify(mockProcessManager.run(
        <String>['pod', 'install', '--verbose'],
        workingDirectory: 'project/ios',
        environment: <String, String>{
          'FLUTTER_FRAMEWORK_DIR': 'engine/path',
          'COCOAPODS_DISABLE_STATS': 'true',
550
          'LANG': 'en_US.UTF-8',
551 552 553 554
        },
      ));
    });

555
    testWithoutContext('runs pod install, if Podfile.lock is older than Podfile', () async {
556
      pretendPodIsInstalled();
557
      projectUnderTest.ios.podfile
558 559
        ..createSync()
        ..writeAsStringSync('Existing Podfile');
560
      projectUnderTest.ios.podfileLock
561 562
        ..createSync()
        ..writeAsStringSync('Existing lock file.');
563
      projectUnderTest.ios.podManifestLock
564 565
        ..createSync(recursive: true)
        ..writeAsStringSync('Existing lock file.');
566
      await Future<void>.delayed(const Duration(milliseconds: 10));
567
      projectUnderTest.ios.podfile
568
        .writeAsStringSync('Updated Podfile');
569
      await cocoaPodsUnderTest.processPods(
570 571
        xcodeProject: projectUnderTest.ios,
        engineDir: 'engine/path',
572 573 574 575 576 577 578 579
        dependenciesChanged: false,
      );
      verify(mockProcessManager.run(
        <String>['pod', 'install', '--verbose'],
        workingDirectory: 'project/ios',
        environment: <String, String>{
          'FLUTTER_FRAMEWORK_DIR': 'engine/path',
          'COCOAPODS_DISABLE_STATS': 'true',
580
          'LANG': 'en_US.UTF-8',
581 582 583 584
        },
      ));
    });

585
    testWithoutContext('skips pod install, if nothing changed', () async {
586
      pretendPodIsInstalled();
587
      projectUnderTest.ios.podfile
588
        ..createSync()
589
        ..writeAsStringSync('Existing Podfile');
590
      projectUnderTest.ios.podfileLock
591
        ..createSync()
592
        ..writeAsStringSync('Existing lock file.');
593
      projectUnderTest.ios.podManifestLock
594
        ..createSync(recursive: true)
595
        ..writeAsStringSync('Existing lock file.');
596
      final bool didInstall = await cocoaPodsUnderTest.processPods(
597 598
        xcodeProject: projectUnderTest.ios,
        engineDir: 'engine/path',
599
        dependenciesChanged: false,
600
      );
601
      expect(didInstall, isFalse);
602
      verifyNever(mockProcessManager.run(
603
      argThat(containsAllInOrder(<String>['pod', 'install'])),
604
        workingDirectory: anyNamed('workingDirectory'),
605
        environment: anyNamed('environment'),
606 607
      ));
    });
608

609
    testWithoutContext('a failed pod install deletes Pods/Manifest.lock', () async {
610
      pretendPodIsInstalled();
611
      projectUnderTest.ios.podfile
612
        ..createSync()
613
        ..writeAsStringSync('Existing Podfile');
614
      projectUnderTest.ios.podfileLock
615
        ..createSync()
616
        ..writeAsStringSync('Existing lock file.');
617
      projectUnderTest.ios.podManifestLock
618
        ..createSync(recursive: true)
619
        ..writeAsStringSync('Existing lock file.');
620 621 622 623 624 625 626

      when(mockProcessManager.run(
        <String>['pod', 'install', '--verbose'],
        workingDirectory: 'project/ios',
        environment: <String, String>{
          'FLUTTER_FRAMEWORK_DIR': 'engine/path',
          'COCOAPODS_DISABLE_STATS': 'true',
627
          'LANG': 'en_US.UTF-8',
628
        },
629
      )).thenAnswer(
630
        (_) async => exitsWithError()
631
      );
632 633 634

      try {
        await cocoaPodsUnderTest.processPods(
635 636
          xcodeProject: projectUnderTest.ios,
          engineDir: 'engine/path',
637
          dependenciesChanged: true,
638 639 640
        );
        fail('Tool throw expected when pod install fails');
      } on ToolExit {
641
        expect(projectUnderTest.ios.podManifestLock.existsSync(), isFalse);
642 643
      }
    });
644
  });
645 646 647 648 649

  group('Pods repos dir is custom', () {
    String cocoapodsRepoDir;
    Map<String, String> environment;
    setUp(() {
650 651
      // Assume binaries exist and can run
      when(mockProcessManager.canRun(any)).thenReturn(true);
652 653 654 655 656
      cocoapodsRepoDir = podsIsInCustomDir();
      environment = <String, String>{
        'FLUTTER_FRAMEWORK_DIR': 'engine/path',
        'COCOAPODS_DISABLE_STATS': 'true',
        'CP_REPOS_DIR': cocoapodsRepoDir,
657
        'LANG': 'en_US.UTF8',
658 659 660
      };
    });

661
    testWithoutContext('succeeds, if specs repo is in CP_REPOS_DIR.', () async {
662
      pretendPodIsInstalled();
663
      fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile'))
664 665 666 667 668 669 670 671
        ..createSync()
        ..writeAsStringSync('Existing Podfile');
      when(mockProcessManager.run(
        <String>['pod', 'install', '--verbose'],
        workingDirectory: 'project/ios',
        environment: environment,
      )).thenAnswer((_) async => exitsHappy());
      final bool success = await cocoaPodsUnderTest.processPods(
672 673
        xcodeProject: projectUnderTest.ios,
        engineDir: 'engine/path',
674 675 676 677
      );
      expect(success, true);
    });
  });
678 679 680
}

class MockProcessManager extends Mock implements ProcessManager {}
681
class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {}
682

683 684
ProcessResult exitsWithError([ String stdout = '' ]) => ProcessResult(1, 1, stdout, '');
ProcessResult exitsHappy([ String stdout = '' ]) => ProcessResult(1, 0, stdout, '');