cocoapods_test.dart 18.7 KB
Newer Older
1 2 3 4 5 6 7 8
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:file/file.dart';
import 'package:file/memory.dart';
9
import 'package:flutter_tools/src/base/common.dart';
10
import 'package:flutter_tools/src/base/io.dart';
11
import 'package:flutter_tools/src/cache.dart';
12
import 'package:flutter_tools/src/project.dart';
13 14
import 'package:flutter_tools/src/ios/cocoapods.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart';
15 16 17
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';

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

21
typedef InvokeProcess = Future<ProcessResult> Function();
22

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

  void pretendPodIsNotInstalled() {
    resultOfPodVersion = () async => throw 'Executable does not exist';
  }

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

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

43
  setUp(() async {
44
    Cache.flutterRoot = 'flutter';
45 46 47
    fs = MemoryFileSystem();
    mockProcessManager = MockProcessManager();
    mockXcodeProjectInterpreter = MockXcodeProjectInterpreter();
48
    projectUnderTest = await FlutterProject.fromDirectory(fs.directory('project'));
49
    projectUnderTest.ios.xcodeProject.createSync(recursive: true);
50
    cocoaPodsUnderTest = CocoaPods();
51
    pretendPodVersionIs('1.5.0');
52 53 54 55 56 57 58 59 60 61
    fs.file(fs.path.join(
      Cache.flutterRoot, 'packages', 'flutter_tools', 'templates', 'cocoapods', 'Podfile-objc'
    ))
        ..createSync(recursive: true)
        ..writeAsStringSync('Objective-C podfile template');
    fs.file(fs.path.join(
      Cache.flutterRoot, 'packages', 'flutter_tools', 'templates', 'cocoapods', 'Podfile-swift'
    ))
        ..createSync(recursive: true)
        ..writeAsStringSync('Swift podfile template');
62 63 64
    fs.directory(fs.path.join(homeDirPath, '.cocoapods', 'repos', 'master')).createSync(recursive: true);
    when(mockProcessManager.run(
      <String>['pod', '--version'],
65 66
      workingDirectory: anyNamed('workingDirectory'),
      environment: anyNamed('environment'),
67
    )).thenAnswer((_) => resultOfPodVersion());
68 69 70
    when(mockProcessManager.run(
      <String>['pod', 'install', '--verbose'],
      workingDirectory: 'project/ios',
71
      environment: <String, String>{'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true'},
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
    )).thenAnswer((_) async => exitsHappy());
  });

  group('Evaluate installation', () {
    testUsingContext('detects not installed, if pod exec does not exist', () async {
      pretendPodIsNotInstalled();
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.notInstalled);
    }, overrides: <Type, Generator>{
      ProcessManager: () => mockProcessManager,
    });

    testUsingContext('detects not installed, if pod version fails', () async {
      pretendPodVersionFails();
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.notInstalled);
    }, overrides: <Type, Generator>{
      ProcessManager: () => mockProcessManager,
    });

    testUsingContext('detects installed', () async {
      pretendPodVersionIs('0.0.1');
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, isNot(CocoaPodsStatus.notInstalled));
    }, overrides: <Type, Generator>{
      ProcessManager: () => mockProcessManager,
    });

    testUsingContext('detects below minimum version', () async {
      pretendPodVersionIs('0.39.8');
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.belowMinimumVersion);
    }, overrides: <Type, Generator>{
      ProcessManager: () => mockProcessManager,
    });

    testUsingContext('detects below recommended version', () async {
      pretendPodVersionIs('1.4.99');
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.belowRecommendedVersion);
    }, overrides: <Type, Generator>{
      ProcessManager: () => mockProcessManager,
    });

    testUsingContext('detects at recommended version', () async {
      pretendPodVersionIs('1.5.0');
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.recommended);
    }, overrides: <Type, Generator>{
      ProcessManager: () => mockProcessManager,
    });

    testUsingContext('detects above recommended version', () async {
      pretendPodVersionIs('1.5.1');
      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.recommended);
    }, overrides: <Type, Generator>{
      ProcessManager: () => mockProcessManager,
    });
124 125
  });

126
  group('Setup Podfile', () {
127
    testUsingContext('creates objective-c Podfile when not present', () async {
128
      cocoaPodsUnderTest.setupPodfile(projectUnderTest.ios);
129

130
      expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Objective-C podfile template');
131
    }, overrides: <Type, Generator>{
132
      FileSystem: () => fs,
133
    });
134

135
    testUsingContext('creates swift Podfile if swift', () async {
136
      when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
137
      when(mockXcodeProjectInterpreter.getBuildSettings(any, any)).thenReturn(<String, String>{
138 139 140
        'SWIFT_VERSION': '4.0',
      });

141
      final FlutterProject project = await FlutterProject.fromPath('project');
142
      cocoaPodsUnderTest.setupPodfile(project.ios);
143

144
      expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Swift podfile template');
145
    }, overrides: <Type, Generator>{
146
      FileSystem: () => fs,
147 148
      XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
    });
149

150
    testUsingContext('does not recreate Podfile when already present', () async {
151
      projectUnderTest.ios.podfile..createSync()..writeAsStringSync('Existing Podfile');
152

153
      final FlutterProject project = await FlutterProject.fromPath('project');
154
      cocoaPodsUnderTest.setupPodfile(project.ios);
155

156
      expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Existing Podfile');
157 158 159 160
    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
    });

161
    testUsingContext('does not create Podfile when we cannot interpret Xcode projects', () async {
162
      when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false);
163

164
      final FlutterProject project = await FlutterProject.fromPath('project');
165
      cocoaPodsUnderTest.setupPodfile(project.ios);
166

167
      expect(projectUnderTest.ios.podfile.existsSync(), false);
168 169 170 171 172
    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
      XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
    });

173
    testUsingContext('includes Pod config in xcconfig files, if not present', () async {
174 175 176 177 178 179 180
      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');
181

182
      final FlutterProject project = await FlutterProject.fromPath('project');
183
      cocoaPodsUnderTest.setupPodfile(project.ios);
184

185
      final String debugContents = projectUnderTest.ios.xcodeConfigFor('Debug').readAsStringSync();
186 187 188
      expect(debugContents, contains(
          '#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"\n'));
      expect(debugContents, contains('Existing debug config'));
189
      final String releaseContents = projectUnderTest.ios.xcodeConfigFor('Release').readAsStringSync();
190 191 192 193 194 195 196 197 198 199
      expect(releaseContents, contains(
          '#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"\n'));
      expect(releaseContents, contains('Existing release config'));
    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
    });
  });

  group('Process pods', () {
    testUsingContext('prints error, if CocoaPods is not installed', () async {
200
      pretendPodIsNotInstalled();
201
      projectUnderTest.ios.podfile.createSync();
202
      final bool didInstall = await cocoaPodsUnderTest.processPods(
203
        iosProject: projectUnderTest.ios,
204
        iosEngineDir: 'engine/path',
205
      );
206
      verifyNever(mockProcessManager.run(
207
      argThat(containsAllInOrder(<String>['pod', 'install'])),
208
        workingDirectory: anyNamed('workingDirectory'),
209
        environment: anyNamed('environment'),
210
      ));
211 212
      expect(testLogger.errorText, contains('not installed'));
      expect(testLogger.errorText, contains('Skipping pod install'));
213
      expect(didInstall, isFalse);
214
    }, overrides: <Type, Generator>{
215 216
      FileSystem: () => fs,
      ProcessManager: () => mockProcessManager,
217
    });
218

219
    testUsingContext('throws, if Podfile is missing.', () async {
220 221
      try {
        await cocoaPodsUnderTest.processPods(
222
          iosProject: projectUnderTest.ios,
223
          iosEngineDir: 'engine/path',
224
        );
225 226
        fail('ToolExit expected');
      } catch(e) {
227
        expect(e, isInstanceOf<ToolExit>());
228
        verifyNever(mockProcessManager.run(
229
        argThat(containsAllInOrder(<String>['pod', 'install'])),
230
          workingDirectory: anyNamed('workingDirectory'),
231
          environment: anyNamed('environment'),
232 233
        ));
      }
234
    }, overrides: <Type, Generator>{
235 236
      FileSystem: () => fs,
      ProcessManager: () => mockProcessManager,
237
    });
238

239
    testUsingContext('throws, if specs repo is outdated.', () async {
240 241
      fs.file(fs.path.join('project', 'ios', 'Podfile'))
        ..createSync()
242
        ..writeAsStringSync('Existing Podfile');
243 244 245 246

      when(mockProcessManager.run(
        <String>['pod', 'install', '--verbose'],
        workingDirectory: 'project/ios',
247 248 249 250
        environment: <String, String>{
          'FLUTTER_FRAMEWORK_DIR': 'engine/path',
          'COCOAPODS_DISABLE_STATS': 'true',
        },
251
      )).thenAnswer((_) async => exitsWithError(
252 253 254 255 256 257 258 259 260 261 262 263 264 265
        '''
[!] 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.''',
266
      ));
267 268
      try {
        await cocoaPodsUnderTest.processPods(
269
          iosProject: projectUnderTest.ios,
270
          iosEngineDir: 'engine/path',
271 272 273
        );
        fail('ToolExit expected');
      } catch (e) {
274
        expect(e, isInstanceOf<ToolExit>());
275 276 277 278
        expect(
          testLogger.errorText,
          contains("CocoaPods's specs repository is too out-of-date to satisfy dependencies"),
        );
279
      }
280
    }, overrides: <Type, Generator>{
281 282
      FileSystem: () => fs,
      ProcessManager: () => mockProcessManager,
283
    });
284

285
    testUsingContext('run pod install, if Podfile.lock is missing', () async {
286
      projectUnderTest.ios.podfile
287
        ..createSync()
288
        ..writeAsStringSync('Existing Podfile');
289
      projectUnderTest.ios.podManifestLock
290
        ..createSync(recursive: true)
291
        ..writeAsStringSync('Existing lock file.');
292
      final bool didInstall = await cocoaPodsUnderTest.processPods(
293
        iosProject: projectUnderTest.ios,
294
        iosEngineDir: 'engine/path',
295
        dependenciesChanged: false,
296
      );
297
      expect(didInstall, isTrue);
298
      verify(mockProcessManager.run(
299 300
        <String>['pod', 'install', '--verbose'],
        workingDirectory: 'project/ios',
301
        environment: <String, String>{'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true'},
302
      ));
303
    }, overrides: <Type, Generator>{
304 305
      FileSystem: () => fs,
      ProcessManager: () => mockProcessManager,
306
    });
307

308
    testUsingContext('runs pod install, if Manifest.lock is missing', () async {
309
      projectUnderTest.ios.podfile
310
        ..createSync()
311
        ..writeAsStringSync('Existing Podfile');
312
      projectUnderTest.ios.podfileLock
313
        ..createSync()
314
        ..writeAsStringSync('Existing lock file.');
315
      final bool didInstall = await cocoaPodsUnderTest.processPods(
316
        iosProject: projectUnderTest.ios,
317
        iosEngineDir: 'engine/path',
318
        dependenciesChanged: false,
319
      );
320
      expect(didInstall, isTrue);
321 322 323 324 325 326 327 328 329 330 331 332 333 334
      verify(mockProcessManager.run(
        <String>['pod', 'install', '--verbose'],
        workingDirectory: 'project/ios',
        environment: <String, String>{
          'FLUTTER_FRAMEWORK_DIR': 'engine/path',
          'COCOAPODS_DISABLE_STATS': 'true',
        },
      ));
    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
      ProcessManager: () => mockProcessManager,
    });

    testUsingContext('runs pod install, if Manifest.lock different from Podspec.lock', () async {
335
      projectUnderTest.ios.podfile
336
        ..createSync()
337
        ..writeAsStringSync('Existing Podfile');
338
      projectUnderTest.ios.podfileLock
339
        ..createSync()
340
        ..writeAsStringSync('Existing lock file.');
341
      projectUnderTest.ios.podManifestLock
342
        ..createSync(recursive: true)
343
        ..writeAsStringSync('Different lock file.');
344
      final bool didInstall = await cocoaPodsUnderTest.processPods(
345
        iosProject: projectUnderTest.ios,
346
        iosEngineDir: 'engine/path',
347
        dependenciesChanged: false,
348
      );
349
      expect(didInstall, isTrue);
350
      verify(mockProcessManager.run(
351 352
        <String>['pod', 'install', '--verbose'],
        workingDirectory: 'project/ios',
353 354 355 356
        environment: <String, String>{
          'FLUTTER_FRAMEWORK_DIR': 'engine/path',
          'COCOAPODS_DISABLE_STATS': 'true',
        },
357
      ));
358
    }, overrides: <Type, Generator>{
359 360
      FileSystem: () => fs,
      ProcessManager: () => mockProcessManager,
361 362 363
    });

    testUsingContext('runs pod install, if flutter framework changed', () async {
364
      projectUnderTest.ios.podfile
365
        ..createSync()
366
        ..writeAsStringSync('Existing Podfile');
367
      projectUnderTest.ios.podfileLock
368
        ..createSync()
369
        ..writeAsStringSync('Existing lock file.');
370
      projectUnderTest.ios.podManifestLock
371
        ..createSync(recursive: true)
372
        ..writeAsStringSync('Existing lock file.');
373
      final bool didInstall = await cocoaPodsUnderTest.processPods(
374
        iosProject: projectUnderTest.ios,
375
        iosEngineDir: 'engine/path',
376
        dependenciesChanged: true,
377
      );
378
      expect(didInstall, isTrue);
379 380 381 382 383 384 385 386 387 388 389 390 391
      verify(mockProcessManager.run(
        <String>['pod', 'install', '--verbose'],
        workingDirectory: 'project/ios',
        environment: <String, String>{
          'FLUTTER_FRAMEWORK_DIR': 'engine/path',
          'COCOAPODS_DISABLE_STATS': 'true',
        },
      ));
    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
      ProcessManager: () => mockProcessManager,
    });

392
    testUsingContext('runs pod install, if Podfile.lock is older than Podfile', () async {
393
      projectUnderTest.ios.podfile
394 395
        ..createSync()
        ..writeAsStringSync('Existing Podfile');
396
      projectUnderTest.ios.podfileLock
397 398
        ..createSync()
        ..writeAsStringSync('Existing lock file.');
399
      projectUnderTest.ios.podManifestLock
400 401
        ..createSync(recursive: true)
        ..writeAsStringSync('Existing lock file.');
402
      await Future<void>.delayed(const Duration(milliseconds: 10));
403
      projectUnderTest.ios.podfile
404 405
        ..writeAsStringSync('Updated Podfile');
      await cocoaPodsUnderTest.processPods(
406
        iosProject: projectUnderTest.ios,
407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422
        iosEngineDir: 'engine/path',
        dependenciesChanged: false,
      );
      verify(mockProcessManager.run(
        <String>['pod', 'install', '--verbose'],
        workingDirectory: 'project/ios',
        environment: <String, String>{
          'FLUTTER_FRAMEWORK_DIR': 'engine/path',
          'COCOAPODS_DISABLE_STATS': 'true',
        },
      ));
    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
      ProcessManager: () => mockProcessManager,
    });

423
    testUsingContext('skips pod install, if nothing changed', () async {
424
      projectUnderTest.ios.podfile
425
        ..createSync()
426
        ..writeAsStringSync('Existing Podfile');
427
      projectUnderTest.ios.podfileLock
428
        ..createSync()
429
        ..writeAsStringSync('Existing lock file.');
430
      projectUnderTest.ios.podManifestLock
431
        ..createSync(recursive: true)
432
        ..writeAsStringSync('Existing lock file.');
433
      final bool didInstall = await cocoaPodsUnderTest.processPods(
434
        iosProject: projectUnderTest.ios,
435
        iosEngineDir: 'engine/path',
436
        dependenciesChanged: false,
437
      );
438
      expect(didInstall, isFalse);
439
      verifyNever(mockProcessManager.run(
440
      argThat(containsAllInOrder(<String>['pod', 'install'])),
441
        workingDirectory: anyNamed('workingDirectory'),
442
        environment: anyNamed('environment'),
443 444 445 446 447
      ));
    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
      ProcessManager: () => mockProcessManager,
    });
448 449

    testUsingContext('a failed pod install deletes Pods/Manifest.lock', () async {
450
      projectUnderTest.ios.podfile
451
        ..createSync()
452
        ..writeAsStringSync('Existing Podfile');
453
      projectUnderTest.ios.podfileLock
454
        ..createSync()
455
        ..writeAsStringSync('Existing lock file.');
456
      projectUnderTest.ios.podManifestLock
457
        ..createSync(recursive: true)
458
        ..writeAsStringSync('Existing lock file.');
459 460 461 462 463 464 465 466

      when(mockProcessManager.run(
        <String>['pod', 'install', '--verbose'],
        workingDirectory: 'project/ios',
        environment: <String, String>{
          'FLUTTER_FRAMEWORK_DIR': 'engine/path',
          'COCOAPODS_DISABLE_STATS': 'true',
        },
467
      )).thenAnswer(
468
        (_) async => exitsWithError()
469
      );
470 471 472

      try {
        await cocoaPodsUnderTest.processPods(
473
          iosProject: projectUnderTest.ios,
474
          iosEngineDir: 'engine/path',
475
          dependenciesChanged: true,
476 477 478
        );
        fail('Tool throw expected when pod install fails');
      } on ToolExit {
479
        expect(projectUnderTest.ios.podManifestLock.existsSync(), isFalse);
480 481 482 483 484
      }
    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
      ProcessManager: () => mockProcessManager,
    });
485
  });
486 487 488
}

class MockProcessManager extends Mock implements ProcessManager {}
489
class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {}
490

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