// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // @dart = 2.8 import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/flutter_plugins.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart'; import 'package:flutter_tools/src/macos/cocoapods.dart'; import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:test/fake.dart'; import '../../src/common.dart'; import '../../src/context.dart'; import '../../src/fake_process_manager.dart'; typedef InvokeProcess = Future<ProcessResult> Function(); void main() { FileSystem fileSystem; FakeProcessManager fakeProcessManager; CocoaPods cocoaPodsUnderTest; BufferLogger logger; TestUsage usage; void pretendPodVersionFails() { fakeProcessManager.addCommand( const FakeCommand( command: <String>['pod', '--version'], exitCode: 1, ), ); } void pretendPodVersionIs(String versionText) { fakeProcessManager.addCommand( FakeCommand( command: const <String>['pod', '--version'], stdout: versionText, ), ); } void podsIsInHomeDir() { fileSystem.directory(fileSystem.path.join( '.cocoapods', 'repos', 'master', )).createSync(recursive: true); } 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; } setUp(() async { Cache.flutterRoot = 'flutter'; fileSystem = MemoryFileSystem.test(); fakeProcessManager = FakeProcessManager.empty(); logger = BufferLogger.test(); usage = TestUsage(); cocoaPodsUnderTest = CocoaPods( fileSystem: fileSystem, processManager: fakeProcessManager, logger: logger, platform: FakePlatform(operatingSystem: 'macos'), xcodeProjectInterpreter: FakeXcodeProjectInterpreter(), usage: usage, ); fileSystem.file(fileSystem.path.join( Cache.flutterRoot, 'packages', 'flutter_tools', 'templates', 'cocoapods', 'Podfile-ios-objc', )) ..createSync(recursive: true) ..writeAsStringSync('Objective-C iOS podfile template'); fileSystem.file(fileSystem.path.join( Cache.flutterRoot, 'packages', 'flutter_tools', 'templates', 'cocoapods', 'Podfile-ios-swift', )) ..createSync(recursive: true) ..writeAsStringSync('Swift iOS podfile template'); fileSystem.file(fileSystem.path.join( Cache.flutterRoot, 'packages', 'flutter_tools', 'templates', 'cocoapods', 'Podfile-macos', )) ..createSync(recursive: true) ..writeAsStringSync('macOS podfile template'); }); void pretendPodIsNotInstalled() { fakeProcessManager.addCommand( const FakeCommand( command: <String>['which', 'pod'], exitCode: 1, ), ); } void pretendPodIsBroken() { fakeProcessManager.addCommands(<FakeCommand>[ // it is present const FakeCommand( command: <String>['which', 'pod'], ), // but is not working const FakeCommand( command: <String>['pod', '--version'], exitCode: 1, ), ]); } void pretendPodIsInstalled() { fakeProcessManager.addCommands(<FakeCommand>[ const FakeCommand( command: <String>['which', 'pod'], ), ]); } group('Evaluate installation', () { testWithoutContext('detects not installed, if pod exec does not exist', () async { pretendPodIsNotInstalled(); expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.notInstalled); }); testWithoutContext('detects not installed, if pod is installed but version fails', () async { pretendPodIsInstalled(); pretendPodVersionFails(); expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.brokenInstall); }); testWithoutContext('detects installed', () async { pretendPodIsInstalled(); pretendPodVersionIs('0.0.1'); expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, isNot(CocoaPodsStatus.notInstalled)); }); testWithoutContext('detects unknown version', () async { pretendPodIsInstalled(); pretendPodVersionIs('Plugin loaded.\n1.5.3'); expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.unknownVersion); }); testWithoutContext('detects below minimum version', () async { pretendPodIsInstalled(); pretendPodVersionIs('1.6.0'); expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.belowMinimumVersion); }); testWithoutContext('detects below recommended version', () async { pretendPodIsInstalled(); pretendPodVersionIs('1.9.0'); expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.belowRecommendedVersion); }); testWithoutContext('detects at recommended version', () async { pretendPodIsInstalled(); pretendPodVersionIs('1.11.0'); expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.recommended); }); testWithoutContext('detects above recommended version', () async { pretendPodIsInstalled(); pretendPodVersionIs('1.11.1'); expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.recommended); }); }); group('Setup Podfile', () { testUsingContext('creates objective-c Podfile when not present', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); await cocoaPodsUnderTest.setupPodfile(projectUnderTest.ios); expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Objective-C iOS podfile template'); }); testUsingContext('creates swift Podfile if swift', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); 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, ); final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project')); await cocoaPodsUnderTest.setupPodfile(project.ios); expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Swift iOS podfile template'); }); testUsingContext('creates macOS Podfile when not present', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); projectUnderTest.macos.xcodeProject.createSync(recursive: true); await cocoaPodsUnderTest.setupPodfile(projectUnderTest.macos); expect(projectUnderTest.macos.podfile.readAsStringSync(), 'macOS podfile template'); }); testUsingContext('does not recreate Podfile when already present', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); projectUnderTest.ios.podfile..createSync()..writeAsStringSync('Existing Podfile'); final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project')); await cocoaPodsUnderTest.setupPodfile(project.ios); expect(projectUnderTest.ios.podfile.readAsStringSync(), 'Existing Podfile'); }); testUsingContext('does not create Podfile when we cannot interpret Xcode projects', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); final CocoaPods cocoaPodsUnderTest = CocoaPods( fileSystem: fileSystem, processManager: fakeProcessManager, logger: logger, platform: FakePlatform(operatingSystem: 'macos'), xcodeProjectInterpreter: FakeXcodeProjectInterpreter(isInstalled: false), usage: usage, ); final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project')); await cocoaPodsUnderTest.setupPodfile(project.ios); expect(projectUnderTest.ios.podfile.existsSync(), false); }); testUsingContext('includes Pod config in xcconfig files, if not present', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); 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'); final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project')); await cocoaPodsUnderTest.setupPodfile(project.ios); 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(); expect(releaseContents, contains( '#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"\n')); expect(releaseContents, contains('Existing release config')); }); testUsingContext('does not include Pod config in xcconfig files, if legacy non-option include present', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); 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); 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('#include?'))); expect(debugContents, equals(legacyDebugInclude)); final String releaseContents = projectUnderTest.ios.xcodeConfigFor('Release').readAsStringSync(); expect(releaseContents, isNot(contains('#include?'))); expect(releaseContents, equals(legacyReleaseInclude)); }); 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)); }); }); group('Update xcconfig', () { testUsingContext('includes Pod config in xcconfig files, if the user manually added Pod dependencies without using Flutter plugins', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); fileSystem.file(fileSystem.path.join('project', 'foo', '.packages')) ..createSync(recursive: true) ..writeAsStringSync('\n'); 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'); final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.directory('project')); await injectPlugins(project, iosPlatform: true); 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(); 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: () => fileSystem, ProcessManager: () => FakeProcessManager.any(), }); }); group('Process pods', () { setUp(() { podsIsInHomeDir(); }); testUsingContext('throwsToolExit if CocoaPods is not installed', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); pretendPodIsNotInstalled(); projectUnderTest.ios.podfile.createSync(); await expectLater(cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.ios, buildMode: BuildMode.debug, ), throwsToolExit(message: 'CocoaPods not installed or not in valid state')); expect(fakeProcessManager, hasNoRemainingExpectations); expect(fakeProcessManager, hasNoRemainingExpectations); }); testUsingContext('throwsToolExit if CocoaPods install is broken', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); pretendPodIsBroken(); projectUnderTest.ios.podfile.createSync(); await expectLater(cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.ios, buildMode: BuildMode.debug, ), throwsToolExit(message: 'CocoaPods not installed or not in valid state')); expect(fakeProcessManager, hasNoRemainingExpectations); expect(fakeProcessManager, hasNoRemainingExpectations); }); testUsingContext('exits if Podfile creates the Flutter engine symlink', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile')) ..createSync() ..writeAsStringSync('Existing Podfile'); final Directory symlinks = projectUnderTest.ios.symlinks ..createSync(recursive: true); symlinks.childLink('flutter').createSync('cache'); await expectLater(cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.ios, buildMode: BuildMode.debug, ), throwsToolExit(message: 'Podfile is out of date')); expect(fakeProcessManager, hasNoRemainingExpectations); }); testUsingContext('exits if iOS Podfile parses .flutter-plugins', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile')) ..createSync() ..writeAsStringSync("plugin_pods = parse_KV_file('../.flutter-plugins')"); await expectLater(cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.ios, buildMode: BuildMode.debug, ), throwsToolExit(message: 'Podfile is out of date')); expect(fakeProcessManager, hasNoRemainingExpectations); }); testUsingContext('prints warning if macOS Podfile parses .flutter-plugins', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); pretendPodIsInstalled(); pretendPodVersionIs('100.0.0'); fakeProcessManager.addCommands(const <FakeCommand>[ FakeCommand( command: <String>['pod', 'install', '--verbose'], ), FakeCommand( command: <String>['touch', 'project/macos/Podfile.lock'], ), ]); projectUnderTest.macos.podfile ..createSync() ..writeAsStringSync("plugin_pods = parse_KV_file('../.flutter-plugins')"); projectUnderTest.macos.podfileLock ..createSync() ..writeAsStringSync('Existing lock file.'); await cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.macos, buildMode: BuildMode.debug, ); expect(logger.warningText, contains('Warning: Podfile is out of date')); expect(logger.warningText, contains('rm macos/Podfile')); expect(fakeProcessManager, hasNoRemainingExpectations); }); testUsingContext('throws, if Podfile is missing.', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); await expectLater(cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.ios, buildMode: BuildMode.debug, ), throwsToolExit(message: 'Podfile missing')); expect(fakeProcessManager, hasNoRemainingExpectations); }); testUsingContext('throws, if specs repo is outdated.', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); pretendPodIsInstalled(); pretendPodVersionIs('100.0.0'); fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile')) ..createSync() ..writeAsStringSync('Existing Podfile'); 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: ''' [!] 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.''', ), ); 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"), ); }); 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", 'bus error': '/Library/Ruby/Gems/2.6.0/gems/ffi-1.15.5/lib/ffi/library.rb:275: [BUG] Bus Error at 0x000000010072c000', }; possibleErrors.forEach((String errorName, String cocoaPodsError) { testUsingContext('ffi $errorName failure on ARM macOS prompts gem install', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); pretendPodIsInstalled(); pretendPodVersionIs('100.0.0'); 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, stderr: cocoaPodsError, ), 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'), ); expect(usage.events, contains(const TestUsageEvent('pod-install-failure', 'arm-ffi'))); }); }); testUsingContext('ffi failure on x86 macOS does not prompt gem install', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); pretendPodIsInstalled(); pretendPodVersionIs('100.0.0'); fileSystem.file(fileSystem.path.join('project', 'ios', 'Podfile')) ..createSync() ..writeAsStringSync('Existing Podfile'); 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, ), ]); // 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); }); testUsingContext('run pod install, if Podfile.lock is missing', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); pretendPodIsInstalled(); pretendPodVersionIs('100.0.0'); projectUnderTest.ios.podfile ..createSync() ..writeAsStringSync('Existing Podfile'); projectUnderTest.ios.podManifestLock ..createSync(recursive: true) ..writeAsStringSync('Existing lock file.'); fakeProcessManager.addCommands(const <FakeCommand>[ FakeCommand( command: <String>['pod', 'install', '--verbose'], workingDirectory: 'project/ios', environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'}, ), ]); final bool didInstall = await cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.ios, buildMode: BuildMode.debug, dependenciesChanged: false, ); expect(didInstall, isTrue); expect(fakeProcessManager, hasNoRemainingExpectations); }); testUsingContext('runs iOS pod install, if Manifest.lock is missing', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); pretendPodIsInstalled(); pretendPodVersionIs('100.0.0'); projectUnderTest.ios.podfile ..createSync() ..writeAsStringSync('Existing Podfile'); projectUnderTest.ios.podfileLock ..createSync() ..writeAsStringSync('Existing lock file.'); fakeProcessManager.addCommands(const <FakeCommand>[ FakeCommand( command: <String>['pod', 'install', '--verbose'], workingDirectory: 'project/ios', environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'}, ), FakeCommand( command: <String>['touch', 'project/ios/Podfile.lock'], ), ]); final bool didInstall = await cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.ios, buildMode: BuildMode.debug, dependenciesChanged: false, ); expect(didInstall, isTrue); expect(fakeProcessManager, hasNoRemainingExpectations); }); testUsingContext('runs macOS pod install, if Manifest.lock is missing', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); pretendPodIsInstalled(); pretendPodVersionIs('100.0.0'); projectUnderTest.macos.podfile ..createSync() ..writeAsStringSync('Existing Podfile'); projectUnderTest.macos.podfileLock ..createSync() ..writeAsStringSync('Existing lock file.'); fakeProcessManager.addCommands(const <FakeCommand>[ FakeCommand( command: <String>['pod', 'install', '--verbose'], workingDirectory: 'project/macos', environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'}, ), FakeCommand( command: <String>['touch', 'project/macos/Podfile.lock'], ), ]); final bool didInstall = await cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.macos, buildMode: BuildMode.debug, dependenciesChanged: false, ); expect(didInstall, isTrue); expect(fakeProcessManager, hasNoRemainingExpectations); }); testUsingContext('runs pod install, if Manifest.lock different from Podspec.lock', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); pretendPodIsInstalled(); pretendPodVersionIs('100.0.0'); projectUnderTest.ios.podfile ..createSync() ..writeAsStringSync('Existing Podfile'); projectUnderTest.ios.podfileLock ..createSync() ..writeAsStringSync('Existing lock file.'); projectUnderTest.ios.podManifestLock ..createSync(recursive: true) ..writeAsStringSync('Different lock file.'); fakeProcessManager.addCommands(const <FakeCommand>[ FakeCommand( command: <String>['pod', 'install', '--verbose'], workingDirectory: 'project/ios', environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'}, ), FakeCommand( command: <String>['touch', 'project/ios/Podfile.lock'], ), ]); final bool didInstall = await cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.ios, buildMode: BuildMode.debug, dependenciesChanged: false, ); expect(didInstall, isTrue); expect(fakeProcessManager, hasNoRemainingExpectations); }); testUsingContext('runs pod install, if flutter framework changed', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); pretendPodIsInstalled(); pretendPodVersionIs('100.0.0'); projectUnderTest.ios.podfile ..createSync() ..writeAsStringSync('Existing Podfile'); projectUnderTest.ios.podfileLock ..createSync() ..writeAsStringSync('Existing lock file.'); projectUnderTest.ios.podManifestLock ..createSync(recursive: true) ..writeAsStringSync('Existing lock file.'); fakeProcessManager.addCommands(const <FakeCommand>[ FakeCommand( command: <String>['pod', 'install', '--verbose'], workingDirectory: 'project/ios', environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'}, ), FakeCommand( command: <String>['touch', 'project/ios/Podfile.lock'], ), ]); final bool didInstall = await cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.ios, buildMode: BuildMode.debug, dependenciesChanged: true, ); expect(didInstall, isTrue); expect(fakeProcessManager, hasNoRemainingExpectations); }); testUsingContext('runs pod install, if Podfile.lock is older than Podfile', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); pretendPodIsInstalled(); pretendPodVersionIs('100.0.0'); projectUnderTest.ios.podfile ..createSync() ..writeAsStringSync('Existing Podfile'); projectUnderTest.ios.podfileLock ..createSync() ..writeAsStringSync('Existing lock file.'); projectUnderTest.ios.podManifestLock ..createSync(recursive: true) ..writeAsStringSync('Existing lock file.'); await Future<void>.delayed(const Duration(milliseconds: 10)); projectUnderTest.ios.podfile .writeAsStringSync('Updated Podfile'); fakeProcessManager.addCommands(const <FakeCommand>[ FakeCommand( command: <String>['pod', 'install', '--verbose'], workingDirectory: 'project/ios', environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true', 'LANG': 'en_US.UTF-8'}, ), FakeCommand( command: <String>['touch', 'project/ios/Podfile.lock'], ), ]); await cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.ios, buildMode: BuildMode.debug, dependenciesChanged: false, ); expect(fakeProcessManager, hasNoRemainingExpectations); }); testUsingContext('skips pod install, if nothing changed', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); projectUnderTest.ios.podfile ..createSync() ..writeAsStringSync('Existing Podfile'); projectUnderTest.ios.podfileLock ..createSync() ..writeAsStringSync('Existing lock file.'); projectUnderTest.ios.podManifestLock ..createSync(recursive: true) ..writeAsStringSync('Existing lock file.'); final bool didInstall = await cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.ios, buildMode: BuildMode.debug, dependenciesChanged: false, ); expect(didInstall, isFalse); expect(fakeProcessManager, hasNoRemainingExpectations); }); testUsingContext('a failed pod install deletes Pods/Manifest.lock', () async { final FlutterProject projectUnderTest = setupProjectUnderTest(); pretendPodIsInstalled(); pretendPodVersionIs('100.0.0'); projectUnderTest.ios.podfile ..createSync() ..writeAsStringSync('Existing Podfile'); projectUnderTest.ios.podfileLock ..createSync() ..writeAsStringSync('Existing lock file.'); projectUnderTest.ios.podManifestLock ..createSync(recursive: true) ..writeAsStringSync('Existing lock file.'); 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, ), ); await expectLater(cocoaPodsUnderTest.processPods( xcodeProject: projectUnderTest.ios, buildMode: BuildMode.debug, ), throwsToolExit(message: 'Error running pod install')); expect(projectUnderTest.ios.podManifestLock.existsSync(), isFalse); }); }); } 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, { XcodeProjectBuildContext buildContext, Duration timeout = const Duration(minutes: 1), }) async => buildSettings; final Map<String, String> buildSettings; }