flutter/packages/flutter_tools/test/ios/cocoapods_test.dart
Chris Bracken cdbdafa8f3
Rerun pod install on changed Xcode project, Podfile (#17274)
If the developer changes their Xcode build settings and their project
has plugins, pod install is required, (e.g. to pick up changes to the
target architecture).

Similarly, manual edits to the Podfile should trigger a pod install.
2018-05-03 19:40:16 -07:00

416 lines
15 KiB
Dart

// 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';
import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/ios/cocoapods.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:mockito/mockito.dart';
import 'package:process/process.dart';
import 'package:test/test.dart';
import '../src/context.dart';
void main() {
FileSystem fs;
ProcessManager mockProcessManager;
MockXcodeProjectInterpreter mockXcodeProjectInterpreter;
Directory projectUnderTest;
CocoaPods cocoaPodsUnderTest;
setUp(() {
Cache.flutterRoot = 'flutter';
fs = new MemoryFileSystem();
mockProcessManager = new MockProcessManager();
mockXcodeProjectInterpreter = new MockXcodeProjectInterpreter();
projectUnderTest = fs.directory(fs.path.join('project', 'ios'))..createSync(recursive: true);
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');
cocoaPodsUnderTest = const TestCocoaPods();
when(mockProcessManager.run(
<String>['pod', 'install', '--verbose'],
workingDirectory: 'project/ios',
environment: <String, String>{'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true'},
)).thenAnswer((_) => new Future<ProcessResult>.value(exitsHappy));
});
group('Setup Podfile', () {
File podfile;
File debugConfigFile;
File releaseConfigFile;
setUp(() {
debugConfigFile = fs.file(fs.path.join('project', 'ios', 'Flutter', 'Debug.xcconfig'));
releaseConfigFile = fs.file(fs.path.join('project', 'ios', 'Flutter', 'Release.xcconfig'));
podfile = fs.file(fs.path.join('project', 'ios', 'Podfile'));
});
testUsingContext('creates objective-c Podfile when not present', () {
cocoaPodsUnderTest.setupPodfile('project');
expect(podfile.readAsStringSync(), 'Objective-C podfile template');
}, overrides: <Type, Generator>{
FileSystem: () => fs,
});
testUsingContext('creates swift Podfile if swift', () {
when(mockXcodeProjectInterpreter.isInstalled).thenReturn(true);
when(mockXcodeProjectInterpreter.getBuildSettings(any, any)).thenReturn(<String, String>{
'SWIFT_VERSION': '4.0',
});
cocoaPodsUnderTest.setupPodfile('project');
expect(podfile.readAsStringSync(), 'Swift podfile template');
}, overrides: <Type, Generator>{
FileSystem: () => fs,
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
});
testUsingContext('does not recreate Podfile when already present', () {
podfile..createSync()..writeAsStringSync('Existing Podfile');
cocoaPodsUnderTest.setupPodfile('project');
expect(podfile.readAsStringSync(), 'Existing Podfile');
}, overrides: <Type, Generator>{
FileSystem: () => fs,
});
testUsingContext('does not create Podfile when we cannot interpret Xcode projects', () {
when(mockXcodeProjectInterpreter.isInstalled).thenReturn(false);
cocoaPodsUnderTest.setupPodfile('project');
expect(podfile.existsSync(), false);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
XcodeProjectInterpreter: () => mockXcodeProjectInterpreter,
});
testUsingContext('includes Pod config in xcconfig files, if not present', () {
podfile..createSync()..writeAsStringSync('Existing Podfile');
debugConfigFile..createSync(recursive: true)..writeAsStringSync('Existing debug config');
releaseConfigFile..createSync(recursive: true)..writeAsStringSync('Existing release config');
cocoaPodsUnderTest.setupPodfile('project');
final String debugContents = debugConfigFile.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 = releaseConfigFile.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: () => fs,
});
});
group('Process pods', () {
testUsingContext('prints error, if CocoaPods is not installed', () async {
projectUnderTest.childFile('Podfile').createSync();
cocoaPodsUnderTest = const TestCocoaPods(false);
final bool didInstall = await cocoaPodsUnderTest.processPods(
appIosDirectory: projectUnderTest,
iosEngineDir: 'engine/path',
);
verifyNever(mockProcessManager.run(
typed<List<String>>(any),
workingDirectory: any,
environment: typed<Map<String, String>>(any, named: 'environment'),
));
expect(testLogger.errorText, contains('not installed'));
expect(testLogger.errorText, contains('Skipping pod install'));
expect(didInstall, isFalse);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => mockProcessManager,
});
testUsingContext('throws, if Podfile is missing.', () async {
cocoaPodsUnderTest = const TestCocoaPods(true);
try {
await cocoaPodsUnderTest.processPods(
appIosDirectory: projectUnderTest,
iosEngineDir: 'engine/path',
);
fail('ToolExit expected');
} catch(e) {
expect(e, const isInstanceOf<ToolExit>());
verifyNever(mockProcessManager.run(
typed<List<String>>(any),
workingDirectory: any,
environment: typed<Map<String, String>>(any, named: 'environment'),
));
}
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => mockProcessManager,
});
testUsingContext('throws, if specs repo is outdated.', () async {
fs.file(fs.path.join('project', 'ios', 'Podfile'))
..createSync()
..writeAsString('Existing Podfile');
when(mockProcessManager.run(
<String>['pod', 'install', '--verbose'],
workingDirectory: 'project/ios',
environment: <String, String>{
'FLUTTER_FRAMEWORK_DIR': 'engine/path',
'COCOAPODS_DISABLE_STATS': 'true',
},
)).thenAnswer((_) => new Future<ProcessResult>.value(new ProcessResult(
1,
1,
'''
[!] 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.''',
'',
)));
try {
await cocoaPodsUnderTest.processPods(
appIosDirectory: projectUnderTest,
iosEngineDir: 'engine/path',
);
fail('ToolExit expected');
} catch (e) {
expect(e, const isInstanceOf<ToolExit>());
expect(
testLogger.errorText,
contains("CocoaPods's specs repository is too out-of-date to satisfy dependencies"),
);
}
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => mockProcessManager,
});
testUsingContext('run pod install, if Podfile.lock is missing', () async {
projectUnderTest.childFile('Podfile')
..createSync()
..writeAsString('Existing Podfile');
projectUnderTest.childFile('Pods/Manifest.lock')
..createSync(recursive: true)
..writeAsString('Existing lock file.');
final bool didInstall = await cocoaPodsUnderTest.processPods(
appIosDirectory: projectUnderTest,
iosEngineDir: 'engine/path',
dependenciesChanged: false,
);
expect(didInstall, isTrue);
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 is missing', () async {
projectUnderTest.childFile('Podfile')
..createSync()
..writeAsString('Existing Podfile');
projectUnderTest.childFile('Podfile.lock')
..createSync()
..writeAsString('Existing lock file.');
final bool didInstall = await cocoaPodsUnderTest.processPods(
appIosDirectory: projectUnderTest,
iosEngineDir: 'engine/path',
dependenciesChanged: false,
);
expect(didInstall, isTrue);
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 {
projectUnderTest.childFile('Podfile')
..createSync()
..writeAsString('Existing Podfile');
projectUnderTest.childFile('Podfile.lock')
..createSync()
..writeAsString('Existing lock file.');
projectUnderTest.childFile('Pods/Manifest.lock')
..createSync(recursive: true)
..writeAsString('Different lock file.');
final bool didInstall = await cocoaPodsUnderTest.processPods(
appIosDirectory: projectUnderTest,
iosEngineDir: 'engine/path',
dependenciesChanged: false,
);
expect(didInstall, isTrue);
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 flutter framework changed', () async {
projectUnderTest.childFile('Podfile')
..createSync()
..writeAsString('Existing Podfile');
projectUnderTest.childFile('Podfile.lock')
..createSync()
..writeAsString('Existing lock file.');
projectUnderTest.childFile('Pods/Manifest.lock')
..createSync(recursive: true)
..writeAsString('Existing lock file.');
final bool didInstall = await cocoaPodsUnderTest.processPods(
appIosDirectory: projectUnderTest,
iosEngineDir: 'engine/path',
dependenciesChanged: true,
);
expect(didInstall, isTrue);
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('skips pod install, if nothing changed', () async {
projectUnderTest.childFile('Podfile')
..createSync()
..writeAsString('Existing Podfile');
projectUnderTest.childFile('Podfile.lock')
..createSync()
..writeAsString('Existing lock file.');
projectUnderTest.childFile('Pods/Manifest.lock')
..createSync(recursive: true)
..writeAsString('Existing lock file.');
final bool didInstall = await cocoaPodsUnderTest.processPods(
appIosDirectory: projectUnderTest,
iosEngineDir: 'engine/path',
dependenciesChanged: false,
);
expect(didInstall, isFalse);
verifyNever(mockProcessManager.run(
typed<List<String>>(any),
workingDirectory: any,
environment: typed<Map<String, String>>(any, named: 'environment'),
));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => mockProcessManager,
});
testUsingContext('a failed pod install deletes Pods/Manifest.lock', () async {
projectUnderTest.childFile('Podfile')
..createSync()
..writeAsString('Existing Podfile');
projectUnderTest.childFile('Podfile.lock')
..createSync()
..writeAsString('Existing lock file.');
projectUnderTest.childFile('Pods/Manifest.lock')
..createSync(recursive: true)
..writeAsString('Existing lock file.');
when(mockProcessManager.run(
<String>['pod', 'install', '--verbose'],
workingDirectory: 'project/ios',
environment: <String, String>{
'FLUTTER_FRAMEWORK_DIR': 'engine/path',
'COCOAPODS_DISABLE_STATS': 'true',
},
)).thenAnswer(
(_) => new Future<ProcessResult>.value(
new ProcessResult(1, 1, 'fails for some reason', '')
)
);
try {
await cocoaPodsUnderTest.processPods(
appIosDirectory: projectUnderTest,
iosEngineDir: 'engine/path',
dependenciesChanged: true,
);
fail('Tool throw expected when pod install fails');
} on ToolExit {
expect(projectUnderTest.childFile('Pods/Manifest.lock').existsSync(), isFalse);
}
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => mockProcessManager,
});
});
}
class MockProcessManager extends Mock implements ProcessManager {}
class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {}
class TestCocoaPods extends CocoaPods {
const TestCocoaPods([this._hasCocoaPods = true]);
final bool _hasCocoaPods;
@override
Future<bool> get hasCocoaPods => new Future<bool>.value(_hasCocoaPods);
@override
Future<String> get cocoaPodsVersionText async => new Future<String>.value('1.5.0');
@override
Future<bool> get isCocoaPodsInitialized => new Future<bool>.value(true);
}
final ProcessResult exitsHappy = new ProcessResult(
1, // pid
0, // exitCode
'', // stdout
'', // stderr
);