// 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 [platform] 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 platform, { bool includeSwiftWorkaround = false, bool scopeSwiftWorkaround = false, bool includePrivacyManifest = false, }) { final String pluginName = plugin.directory.basename; final File file = plugin.directory .childDirectory(platform) .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']} ''' : ''; 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 s.swift_version = '5.0' $privacyManifest end '''); } void main() { group('PodspecCheckCommand', () { late Directory packagesDir; late CommandRunner 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( 'podspec_test', 'Test for $PodspecCheckCommand', ); runner.addCommand(command); }); test('only runs on macOS', () async { createFakePlugin( 'plugin1', packagesDir, extraFiles: ['plugin1.podspec'], ); mockPlatform.isMacOS = false; Error? commandError; final List output = await runCapturingPrint( runner, ['podspec-check'], errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect(processRunner.recordedCalls, equals([])); expect( output, containsAllInOrder([contains('only supported on macOS')]), ); }); test('runs pod lib lint on a podspec', () async { final RepositoryPackage plugin = createFakePlugin( 'plugin1', packagesDir, extraFiles: [ 'bogus.dart', // Ignore non-podspecs. ], ); _writeFakePodspec(plugin, 'ios'); processRunner.mockProcessesForExecutable['pod'] = [ FakeProcessInfo(MockProcess(stdout: 'Foo', stderr: 'Bar')), FakeProcessInfo(MockProcess()), ]; final List output = await runCapturingPrint(runner, [ 'podspec-check', ]); expect( processRunner.recordedCalls, orderedEquals([ ProcessCall('which', const ['pod'], packagesDir.path), ProcessCall('pod', [ '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: [ 'example/ios/Flutter/Flutter.podspec', 'example/macos/Flutter/ephemeral/FlutterMacOS.podspec', ], ); _writeFakePodspec(plugin, 'macos'); final List output = await runCapturingPrint(runner, [ 'podspec-check', ]); expect(output, isNot(contains('FlutterMacOS.podspec'))); expect( processRunner.recordedCalls, orderedEquals([ ProcessCall('which', const ['pod'], packagesDir.path), ProcessCall('pod', [ '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(MockProcess(exitCode: 1), ['pod']), ]; Error? commandError; final List output = await runCapturingPrint( runner, ['podspec-check'], errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([ 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(MockProcess(exitCode: 1)), ]; Error? commandError; final List output = await runCapturingPrint( runner, ['podspec-check'], errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([ 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: [ 'ios/Classes/SomeSwift.swift', 'ios/plugin1/Package.swift', ], ); _writeFakePodspec(plugin, 'ios'); Error? commandError; final List output = await runCapturingPrint( runner, ['podspec-check'], errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([ 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: ['darwin/Classes/SomeSwift.swift'], ); _writeFakePodspec(plugin, 'darwin'); Error? commandError; final List output = await runCapturingPrint( runner, ['podspec-check'], errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([ 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: ['ios/plugin1/Package.swift'], ); _writeFakePodspec(plugin, 'ios'); final List output = await runCapturingPrint(runner, [ 'podspec-check', ]); expect( output, containsAllInOrder([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: [ 'darwin/Tests/SharedTest.swift', 'example/ios/RunnerTests/UnitTest.swift', 'example/ios/RunnerUITests/UITest.swift', ], ); _writeFakePodspec(plugin, 'ios'); final List output = await runCapturingPrint(runner, [ 'podspec-check', ]); expect( output, containsAllInOrder([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: ['darwin/plugin1/Package.swift'], ); _writeFakePodspec(plugin, 'darwin'); final List output = await runCapturingPrint(runner, [ 'podspec-check', ]); expect( output, containsAllInOrder([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: ['macos/Classes/SomeSwift.swift'], ); _writeFakePodspec(plugin, 'macos'); final List output = await runCapturingPrint(runner, [ 'podspec-check', ]); expect( output, containsAllInOrder([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: [ 'ios/Classes/SomeObjC.h', 'ios/Classes/SomeObjC.m', ], ); _writeFakePodspec(plugin, 'ios'); final List output = await runCapturingPrint(runner, [ 'podspec-check', ]); expect( output, containsAllInOrder([contains('Ran for 1 package(s)')]), ); }, ); test('passes if the search paths workaround is present', () async { final RepositoryPackage plugin = createFakePlugin( 'plugin1', packagesDir, extraFiles: ['ios/Classes/SomeSwift.swift'], ); _writeFakePodspec(plugin, 'ios', includeSwiftWorkaround: true); final List output = await runCapturingPrint(runner, [ 'podspec-check', ]); expect( output, containsAllInOrder([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: ['ios/Classes/SomeSwift.swift'], ); _writeFakePodspec( plugin, 'ios', includeSwiftWorkaround: true, scopeSwiftWorkaround: true, ); final List output = await runCapturingPrint(runner, [ 'podspec-check', ]); expect( output, containsAllInOrder([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: [ 'ios/Classes/SomeObjC.h', 'ios/Classes/SomeObjC.m', 'example/ios/Runner/AppDelegate.swift', ], ); _writeFakePodspec(plugin, 'ios'); final List output = await runCapturingPrint(runner, [ 'podspec-check', ]); expect( output, containsAllInOrder([contains('Ran for 1 package(s)')]), ); }, ); test('skips when there are no podspecs', () async { createFakePlugin('plugin1', packagesDir); final List output = await runCapturingPrint(runner, [ 'podspec-check', ]); expect( output, containsAllInOrder([contains('SKIPPING: No podspecs.')]), ); }); test('fails when an iOS plugin is missing a privacy manifest', () async { final RepositoryPackage plugin = createFakePlugin( 'plugin1', packagesDir, platformSupport: { Platform.iOS: const PlatformDetails(PlatformSupport.inline), }, ); _writeFakePodspec(plugin, 'ios'); Error? commandError; final List output = await runCapturingPrint( runner, ['podspec-check'], errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('No PrivacyInfo.xcprivacy file specified.'), ]), ); }); test('passes when an iOS plugin has a privacy manifest', () async { final RepositoryPackage plugin = createFakePlugin( 'plugin1', packagesDir, platformSupport: { Platform.iOS: const PlatformDetails(PlatformSupport.inline), }, ); _writeFakePodspec(plugin, 'ios', includePrivacyManifest: true); final List output = await runCapturingPrint(runner, [ 'podspec-check', ]); expect( output, containsAllInOrder([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: { Platform.macOS: const PlatformDetails(PlatformSupport.inline), }, ); _writeFakePodspec(plugin, 'macos'); Error? commandError; final List output = await runCapturingPrint( runner, ['podspec-check'], errorHandler: (Error e) { commandError = e; }, ); expect(commandError, isA()); expect( output, containsAllInOrder([ contains('No PrivacyInfo.xcprivacy file specified.'), ]), ); }); test('passes when a macOS plugin has a privacy manifest', () async { final RepositoryPackage plugin = createFakePlugin( 'plugin1', packagesDir, platformSupport: { Platform.macOS: const PlatformDetails(PlatformSupport.inline), }, ); _writeFakePodspec(plugin, 'macos', includePrivacyManifest: true); final List output = await runCapturingPrint(runner, [ 'podspec-check', ]); expect( output, containsAllInOrder([contains('Ran for 1 package(s)')]), ); }); }); }