Files
packages/script/tool/test/podspec_check_command_test.dart
stuartmorgan 8c287e99d6 [tool] Move changed file detection to base command class (#8730)
Consolidates the code to find all changed file paths into the `PackageLoopingCommand` class that is the base of almost all of the repo tooling commands. This in a preparatory PR for a future change to allow each command to define a list of files or file patterns that definitively *don't* affect that test, so that CI can be smarter about what tests to run (e.g., not running expensive integration tests for README changes).

A side effect of this change is that tests of almost all commands now need a mock `GitDir` instance. This would add a lot of copy/pasted boilerplate to the test setup, and there is already too much of that, so instead this refactors common test setup:
- Creating a memory file system
- Populating it with a packages directory
- Creating a RecordingProcessRunner to mock out process calls
- Creating a mock GitDir that forwards to a RecordingProcessRunner

into a helper method (using records and destructuring to easily return multiple values). While some tests don't need all of these steps, those that don't can easily ignore parts of it, and it will make it much easier to update tests in the future if they need them, and it makes the setup much more consistent which makes it easier to reason about test setup in general.

Prep for https://github.com/flutter/flutter/issues/136394
2025-03-25 21:29:17 +00:00

649 lines
20 KiB
Dart

// 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: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 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', () {
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 PodspecCheckCommand 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,
'--configuration=Debug',
'--skip-tests',
'--use-libraries'
],
packagesDir.path),
ProcessCall(
'pod',
<String>[
'lib',
'lint',
plugin
.platformDirectory(FlutterPlatform.ios)
.childFile('plugin1.podspec')
.path,
'--configuration=Debug',
'--skip-tests',
],
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,
'--configuration=Debug',
'--skip-tests',
'--use-libraries'
],
packagesDir.path),
ProcessCall(
'pod',
<String>[
'lib',
'lint',
plugin
.platformDirectory(FlutterPlatform.macos)
.childFile('plugin1.podspec')
.path,
'--configuration=Debug',
'--skip-tests',
],
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 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>[
FakeProcessInfo(MockProcess()),
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('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)')],
));
});
});
}