blob: 4988f86bab5a062fb9c432d10b1b78f56293430f [file]
// Copyright 2013 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:args/command_runner.dart';
import 'package:file/file.dart';
import 'package:flutter_plugin_tools/src/common/core.dart';
import 'package:flutter_plugin_tools/src/common/plugin_utils.dart';
import 'package:flutter_plugin_tools/src/podspec_check_command.dart';
import 'package:git/git.dart';
import 'package:platform/platform.dart';
import 'package:test/test.dart';
import 'mocks.dart';
import 'util.dart';
/// Adds a fake podspec to [plugin]'s [subdirectory] directory.
///
/// If [includeSwiftWorkaround] is set, the xcconfig additions to make Swift
/// libraries work in apps that have no Swift will be included. If
/// [scopeSwiftWorkaround] is set, it will be specific to the iOS configuration.
void _writeFakePodspec(
RepositoryPackage plugin,
String subdirectory, {
bool includeSwiftWorkaround = false,
bool scopeSwiftWorkaround = false,
bool includePrivacyManifest = false,
bool includeSwiftVersion = true,
}) {
final String pluginName = plugin.directory.basename;
final File file = plugin.directory.childDirectory(subdirectory).childFile('$pluginName.podspec');
final swiftWorkaround = includeSwiftWorkaround
? '''
s.${scopeSwiftWorkaround ? 'ios.' : ''}xcconfig = {
'LIBRARY_SEARCH_PATHS' => '\$(TOOLCHAIN_DIR)/usr/lib/swift/\$(PLATFORM_NAME)/ \$(SDKROOT)/usr/lib/swift',
'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift',
}
'''
: '';
final privacyManifest = includePrivacyManifest
? '''
s.resource_bundles = {'$pluginName' => ['Resources/PrivacyInfo.xcprivacy']}
'''
: '';
final swiftVersion = includeSwiftVersion ? "s.swift_version = '5.0'" : '';
file.createSync(recursive: true);
file.writeAsStringSync('''
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
#
Pod::Spec.new do |s|
s.name = 'shared_preferences_foundation'
s.version = '0.0.1'
s.summary = 'iOS and macOS implementation of the shared_preferences plugin.'
s.description = <<-DESC
Wraps NSUserDefaults, providing a persistent store for simple key-value pairs.
DESC
s.homepage = 'https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences_foundation'
s.license = { :type => 'BSD', :file => '../LICENSE' }
s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' }
s.source = { :http => 'https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences_foundation' }
s.source_files = 'Classes/**/*'
s.ios.dependency 'Flutter'
s.osx.dependency 'FlutterMacOS'
s.ios.deployment_target = '9.0'
s.osx.deployment_target = '10.11'
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
$swiftWorkaround
$swiftVersion
$privacyManifest
end
''');
}
void main() {
group('PodspecCheckCommand', () {
late Directory packagesDir;
late CommandRunner<void> runner;
late MockPlatform mockPlatform;
late RecordingProcessRunner processRunner;
setUp(() {
mockPlatform = MockPlatform(isMacOS: true);
final GitDir gitDir;
(:packagesDir, :processRunner, gitProcessRunner: _, :gitDir) = configureBaseCommandMocks(
platform: mockPlatform,
);
final command = PodspecCheckCommand(
packagesDir,
processRunner: processRunner,
platform: mockPlatform,
gitDir: gitDir,
);
runner = CommandRunner<void>('podspec_test', 'Test for $PodspecCheckCommand');
runner.addCommand(command);
});
test('only runs on macOS', () async {
createFakePlugin('plugin1', packagesDir, extraFiles: <String>['plugin1.podspec']);
mockPlatform.isMacOS = false;
Error? commandError;
final List<String> output = await runCapturingPrint(
runner,
<String>['podspec-check'],
errorHandler: (Error e) {
commandError = e;
},
);
expect(commandError, isA<ToolExit>());
expect(processRunner.recordedCalls, equals(<ProcessCall>[]));
expect(output, containsAllInOrder(<Matcher>[contains('only supported on macOS')]));
});
test('runs pod lib lint on a podspec', () async {
final RepositoryPackage plugin = createFakePlugin(
'plugin1',
packagesDir,
extraFiles: <String>[
'bogus.dart', // Ignore non-podspecs.
],
);
_writeFakePodspec(plugin, 'ios');
processRunner.mockProcessesForExecutable['pod'] = <FakeProcessInfo>[
FakeProcessInfo(MockProcess(stdout: 'Foo', stderr: 'Bar')),
FakeProcessInfo(MockProcess()),
];
final List<String> output = await runCapturingPrint(runner, <String>['podspec-check']);
expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall('which', const <String>['pod'], packagesDir.path),
ProcessCall('pod', <String>[
'lib',
'lint',
plugin.platformDirectory(FlutterPlatform.ios).childFile('plugin1.podspec').path,
'--quick',
], packagesDir.path),
]),
);
expect(output, contains('Linting plugin1.podspec'));
expect(output, contains('Foo'));
expect(output, contains('Bar'));
});
test('skips shim podspecs for the Flutter framework', () async {
final RepositoryPackage plugin = createFakePlugin(
'plugin1',
packagesDir,
extraFiles: <String>[
'example/ios/Flutter/Flutter.podspec',
'example/macos/Flutter/ephemeral/FlutterMacOS.podspec',
],
);
_writeFakePodspec(plugin, 'macos');
final List<String> output = await runCapturingPrint(runner, <String>['podspec-check']);
expect(output, isNot(contains('FlutterMacOS.podspec')));
expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall('which', const <String>['pod'], packagesDir.path),
ProcessCall('pod', <String>[
'lib',
'lint',
plugin.platformDirectory(FlutterPlatform.macos).childFile('plugin1.podspec').path,
'--quick',
], packagesDir.path),
]),
);
});
test('fails if pod is missing', () async {
final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir);
_writeFakePodspec(plugin, 'ios');
// Simulate failure from `which pod`.
processRunner.mockProcessesForExecutable['which'] = <FakeProcessInfo>[
FakeProcessInfo(MockProcess(exitCode: 1), <String>['pod']),
];
Error? commandError;
final List<String> output = await runCapturingPrint(
runner,
<String>['podspec-check'],
errorHandler: (Error e) {
commandError = e;
},
);
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('Unable to find "pod". Make sure it is in your path.'),
]),
);
});
test('fails if linting as a framework fails', () async {
final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir);
_writeFakePodspec(plugin, 'ios');
// Simulate failure from `pod`.
processRunner.mockProcessesForExecutable['pod'] = <FakeProcessInfo>[
FakeProcessInfo(MockProcess(exitCode: 1)),
];
Error? commandError;
final List<String> output = await runCapturingPrint(
runner,
<String>['podspec-check'],
errorHandler: (Error e) {
commandError = e;
},
);
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('The following packages had errors:'),
contains(
'plugin1:\n'
' plugin1.podspec',
),
]),
);
});
test('fails if an iOS Swift plugin is missing the search paths workaround', () async {
final RepositoryPackage plugin = createFakePlugin(
'plugin1',
packagesDir,
extraFiles: <String>['ios/Classes/SomeSwift.swift', 'ios/plugin1/Package.swift'],
);
_writeFakePodspec(plugin, 'ios');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner,
<String>['podspec-check'],
errorHandler: (Error e) {
commandError = e;
},
);
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains(r'''
s.xcconfig = {
'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift',
'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift',
}'''),
contains('The following packages had errors:'),
contains(
'plugin1:\n'
' plugin1.podspec',
),
]),
);
});
test('fails if a shared-source Swift plugin is missing the search paths workaround', () async {
final RepositoryPackage plugin = createFakePlugin(
'plugin1',
packagesDir,
extraFiles: <String>['darwin/Classes/SomeSwift.swift'],
);
_writeFakePodspec(plugin, 'darwin');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner,
<String>['podspec-check'],
errorHandler: (Error e) {
commandError = e;
},
);
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains(r'''
s.xcconfig = {
'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift',
'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift',
}'''),
contains('The following packages had errors:'),
contains(
'plugin1:\n'
' plugin1.podspec',
),
]),
);
});
test('does not require the search paths workaround for iOS Package.swift', () async {
final RepositoryPackage plugin = createFakePlugin(
'plugin1',
packagesDir,
extraFiles: <String>['ios/plugin1/Package.swift'],
);
_writeFakePodspec(plugin, 'ios');
final List<String> output = await runCapturingPrint(runner, <String>['podspec-check']);
expect(output, containsAllInOrder(<Matcher>[contains('Ran for 1 package(s)')]));
});
test('does not require the search paths workaround for Swift tests', () async {
final RepositoryPackage plugin = createFakePlugin(
'plugin1',
packagesDir,
extraFiles: <String>[
'darwin/Tests/SharedTest.swift',
'example/ios/RunnerTests/UnitTest.swift',
'example/ios/RunnerUITests/UITest.swift',
],
);
_writeFakePodspec(plugin, 'ios');
final List<String> output = await runCapturingPrint(runner, <String>['podspec-check']);
expect(output, containsAllInOrder(<Matcher>[contains('Ran for 1 package(s)')]));
});
test('does not require the search paths workaround for darwin Package.swift', () async {
final RepositoryPackage plugin = createFakePlugin(
'plugin1',
packagesDir,
extraFiles: <String>['darwin/plugin1/Package.swift'],
);
_writeFakePodspec(plugin, 'darwin');
final List<String> output = await runCapturingPrint(runner, <String>['podspec-check']);
expect(output, containsAllInOrder(<Matcher>[contains('Ran for 1 package(s)')]));
});
test('does not require the search paths workaround for macOS plugins', () async {
final RepositoryPackage plugin = createFakePlugin(
'plugin1',
packagesDir,
extraFiles: <String>['macos/Classes/SomeSwift.swift'],
);
_writeFakePodspec(plugin, 'macos');
final List<String> output = await runCapturingPrint(runner, <String>['podspec-check']);
expect(output, containsAllInOrder(<Matcher>[contains('Ran for 1 package(s)')]));
});
test('does not require the search paths workaround for ObjC iOS plugins', () async {
final RepositoryPackage plugin = createFakePlugin(
'plugin1',
packagesDir,
extraFiles: <String>['ios/Classes/SomeObjC.h', 'ios/Classes/SomeObjC.m'],
);
_writeFakePodspec(plugin, 'ios');
final List<String> output = await runCapturingPrint(runner, <String>['podspec-check']);
expect(output, containsAllInOrder(<Matcher>[contains('Ran for 1 package(s)')]));
});
test(
'ignores Swift files in the build directory when determining whether the plugin uses Swift',
() async {
final RepositoryPackage plugin = createFakePlugin(
'plugin1',
packagesDir,
extraFiles: <String>[
'ios/Classes/SomeObjC.h',
'ios/Classes/SomeObjC.m',
'build/SomeSwiftFile.swift',
'ios/build/AnotherSwiftFile.swift',
],
);
_writeFakePodspec(plugin, 'ios');
final List<String> output = await runCapturingPrint(runner, <String>['podspec-check']);
expect(output, containsAllInOrder(<Matcher>[contains('Ran for 1 package(s)')]));
},
);
test('does not check for search paths in build output', () async {
final RepositoryPackage plugin = createFakePlugin(
'plugin1',
packagesDir,
extraFiles: <String>['ios/Classes/SomeSwift.swift', 'ios/plugin1/Package.swift'],
);
_writeFakePodspec(plugin, 'ios', includeSwiftWorkaround: true);
_writeFakePodspec(plugin, 'ios/build/some_pod');
final List<String> output = await runCapturingPrint(runner, <String>['podspec-check']);
expect(output, containsAllInOrder(<Matcher>[contains('Ran for 1 package(s)')]));
});
test('passes if the search paths workaround is present', () async {
final RepositoryPackage plugin = createFakePlugin(
'plugin1',
packagesDir,
extraFiles: <String>['ios/Classes/SomeSwift.swift'],
);
_writeFakePodspec(plugin, 'ios', includeSwiftWorkaround: true);
final List<String> output = await runCapturingPrint(runner, <String>['podspec-check']);
expect(output, containsAllInOrder(<Matcher>[contains('Ran for 1 package(s)')]));
});
test('passes if the search paths workaround is present for iOS only', () async {
final RepositoryPackage plugin = createFakePlugin(
'plugin1',
packagesDir,
extraFiles: <String>['ios/Classes/SomeSwift.swift'],
);
_writeFakePodspec(plugin, 'ios', includeSwiftWorkaround: true, scopeSwiftWorkaround: true);
final List<String> output = await runCapturingPrint(runner, <String>['podspec-check']);
expect(output, containsAllInOrder(<Matcher>[contains('Ran for 1 package(s)')]));
});
test('does not require the search paths workaround for Swift example code', () async {
final RepositoryPackage plugin = createFakePlugin(
'plugin1',
packagesDir,
extraFiles: <String>[
'ios/Classes/SomeObjC.h',
'ios/Classes/SomeObjC.m',
'example/ios/Runner/AppDelegate.swift',
],
);
_writeFakePodspec(plugin, 'ios');
final List<String> output = await runCapturingPrint(runner, <String>['podspec-check']);
expect(output, containsAllInOrder(<Matcher>[contains('Ran for 1 package(s)')]));
});
test('skips when there are no podspecs', () async {
createFakePlugin('plugin1', packagesDir);
final List<String> output = await runCapturingPrint(runner, <String>['podspec-check']);
expect(output, containsAllInOrder(<Matcher>[contains('SKIPPING: No podspecs.')]));
});
test('fails when an iOS plugin is missing a privacy manifest', () async {
final RepositoryPackage plugin = createFakePlugin(
'plugin1',
packagesDir,
platformSupport: <String, PlatformDetails>{
Platform.iOS: const PlatformDetails(PlatformSupport.inline),
},
);
_writeFakePodspec(plugin, 'ios');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner,
<String>['podspec-check'],
errorHandler: (Error e) {
commandError = e;
},
);
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[contains('No PrivacyInfo.xcprivacy file specified.')]),
);
});
test('passes when an iOS plugin has a privacy manifest', () async {
final RepositoryPackage plugin = createFakePlugin(
'plugin1',
packagesDir,
platformSupport: <String, PlatformDetails>{
Platform.iOS: const PlatformDetails(PlatformSupport.inline),
},
);
_writeFakePodspec(plugin, 'ios', includePrivacyManifest: true);
final List<String> output = await runCapturingPrint(runner, <String>['podspec-check']);
expect(output, containsAllInOrder(<Matcher>[contains('Ran for 1 package(s)')]));
});
test('fails when a macOS plugin is missing a privacy manifest', () async {
final RepositoryPackage plugin = createFakePlugin(
'plugin1',
packagesDir,
platformSupport: <String, PlatformDetails>{
Platform.macOS: const PlatformDetails(PlatformSupport.inline),
},
);
_writeFakePodspec(plugin, 'macos');
Error? commandError;
final List<String> output = await runCapturingPrint(
runner,
<String>['podspec-check'],
errorHandler: (Error e) {
commandError = e;
},
);
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[contains('No PrivacyInfo.xcprivacy file specified.')]),
);
});
test('passes when a macOS plugin has a privacy manifest', () async {
final RepositoryPackage plugin = createFakePlugin(
'plugin1',
packagesDir,
platformSupport: <String, PlatformDetails>{
Platform.macOS: const PlatformDetails(PlatformSupport.inline),
},
);
_writeFakePodspec(plugin, 'macos', includePrivacyManifest: true);
final List<String> output = await runCapturingPrint(runner, <String>['podspec-check']);
expect(output, containsAllInOrder(<Matcher>[contains('Ran for 1 package(s)')]));
});
test('fails when a Swift plugin is missing a Swift version', () async {
final RepositoryPackage plugin = createFakePlugin(
'plugin1',
packagesDir,
platformSupport: <String, PlatformDetails>{
Platform.iOS: const PlatformDetails(PlatformSupport.inline),
},
extraFiles: <String>['ios/Classes/SomeSwift.swift'],
);
_writeFakePodspec(
plugin,
'ios',
includeSwiftVersion: false,
includeSwiftWorkaround: true,
includePrivacyManifest: true,
);
Error? commandError;
final List<String> output = await runCapturingPrint(
runner,
<String>['podspec-check'],
errorHandler: (Error e) {
commandError = e;
},
);
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[contains('missing Swift version configuration')]),
);
});
});
}