// Copyright 2013 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. import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.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: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 String 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 String 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', () { FileSystem fileSystem; late Directory packagesDir; late CommandRunner runner; late MockPlatform mockPlatform; late RecordingProcessRunner processRunner; setUp(() { fileSystem = MemoryFileSystem(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); mockPlatform = MockPlatform(isMacOS: true); processRunner = RecordingProcessRunner(); final PodspecCheckCommand command = PodspecCheckCommand( packagesDir, processRunner: processRunner, platform: mockPlatform, ); 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, '--configuration=Debug', '--skip-tests', '--use-modular-headers', '--use-libraries' ], packagesDir.path), ProcessCall( 'pod', [ 'lib', 'lint', plugin .platformDirectory(FlutterPlatform.ios) .childFile('plugin1.podspec') .path, '--configuration=Debug', '--skip-tests', '--use-modular-headers', ], 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, '--configuration=Debug', '--skip-tests', '--use-modular-headers', '--use-libraries' ], packagesDir.path), ProcessCall( 'pod', [ 'lib', 'lint', plugin .platformDirectory(FlutterPlatform.macos) .childFile('plugin1.podspec') .path, '--configuration=Debug', '--skip-tests', '--use-modular-headers', ], 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 linting as a static library fails', () async { final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir); _writeFakePodspec(plugin, 'ios'); // Simulate failure from the second call to `pod`. processRunner.mockProcessesForExecutable['pod'] = [ FakeProcessInfo(MockProcess()), 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)')], )); }); }); }