mirror of
https://github.com/flutter/packages.git
synced 2025-06-17 02:48:43 +08:00
Enable macOS XCTest support (#4043)
- Adds macOS support to the `xctest` tool command - Adds logic to the tool to check for packages that delegate their implementations to another package, so they can be skipped when running native unit tests - Updates the tool's unit test utility for writing pubspecs to be able to make delegated federated implementation references to test it - Adds initial unit tests to the non-deprecated macOS plugins - Enables macOS XCTesting in CI macOS portion of https://github.com/flutter/flutter/issues/82445
This commit is contained in:
@ -11,6 +11,12 @@ import 'package:platform/platform.dart';
|
||||
|
||||
import 'common.dart';
|
||||
|
||||
/// Key for IPA.
|
||||
const String kIpa = 'ipa';
|
||||
|
||||
/// Key for APK.
|
||||
const String kApk = 'apk';
|
||||
|
||||
/// A command to build the example applications for packages.
|
||||
class BuildExamplesCommand extends PluginCommand {
|
||||
/// Creates an instance of the build command.
|
||||
@ -18,10 +24,10 @@ class BuildExamplesCommand extends PluginCommand {
|
||||
Directory packagesDir, {
|
||||
ProcessRunner processRunner = const ProcessRunner(),
|
||||
}) : super(packagesDir, processRunner: processRunner) {
|
||||
argParser.addFlag(kLinux, defaultsTo: false);
|
||||
argParser.addFlag(kMacos, defaultsTo: false);
|
||||
argParser.addFlag(kWeb, defaultsTo: false);
|
||||
argParser.addFlag(kWindows, defaultsTo: false);
|
||||
argParser.addFlag(kPlatformFlagLinux, defaultsTo: false);
|
||||
argParser.addFlag(kPlatformFlagMacos, defaultsTo: false);
|
||||
argParser.addFlag(kPlatformFlagWeb, defaultsTo: false);
|
||||
argParser.addFlag(kPlatformFlagWindows, defaultsTo: false);
|
||||
argParser.addFlag(kIpa, defaultsTo: io.Platform.isMacOS);
|
||||
argParser.addFlag(kApk);
|
||||
argParser.addOption(
|
||||
@ -44,10 +50,10 @@ class BuildExamplesCommand extends PluginCommand {
|
||||
final List<String> platformSwitches = <String>[
|
||||
kApk,
|
||||
kIpa,
|
||||
kLinux,
|
||||
kMacos,
|
||||
kWeb,
|
||||
kWindows,
|
||||
kPlatformFlagLinux,
|
||||
kPlatformFlagMacos,
|
||||
kPlatformFlagWeb,
|
||||
kPlatformFlagWindows,
|
||||
];
|
||||
if (!platformSwitches.any((String platform) => getBoolArg(platform))) {
|
||||
print(
|
||||
@ -66,14 +72,14 @@ class BuildExamplesCommand extends PluginCommand {
|
||||
final String packageName =
|
||||
p.relative(example.path, from: packagesDir.path);
|
||||
|
||||
if (getBoolArg(kLinux)) {
|
||||
if (getBoolArg(kPlatformFlagLinux)) {
|
||||
print('\nBUILDING Linux for $packageName');
|
||||
if (isLinuxPlugin(plugin)) {
|
||||
final int buildExitCode = await processRunner.runAndStream(
|
||||
flutterCommand,
|
||||
<String>[
|
||||
'build',
|
||||
kLinux,
|
||||
kPlatformFlagLinux,
|
||||
if (enableExperiment.isNotEmpty)
|
||||
'--enable-experiment=$enableExperiment',
|
||||
],
|
||||
@ -86,14 +92,14 @@ class BuildExamplesCommand extends PluginCommand {
|
||||
}
|
||||
}
|
||||
|
||||
if (getBoolArg(kMacos)) {
|
||||
if (getBoolArg(kPlatformFlagMacos)) {
|
||||
print('\nBUILDING macOS for $packageName');
|
||||
if (isMacOsPlugin(plugin)) {
|
||||
final int exitCode = await processRunner.runAndStream(
|
||||
flutterCommand,
|
||||
<String>[
|
||||
'build',
|
||||
kMacos,
|
||||
kPlatformFlagMacos,
|
||||
if (enableExperiment.isNotEmpty)
|
||||
'--enable-experiment=$enableExperiment',
|
||||
],
|
||||
@ -106,14 +112,14 @@ class BuildExamplesCommand extends PluginCommand {
|
||||
}
|
||||
}
|
||||
|
||||
if (getBoolArg(kWeb)) {
|
||||
if (getBoolArg(kPlatformFlagWeb)) {
|
||||
print('\nBUILDING web for $packageName');
|
||||
if (isWebPlugin(plugin)) {
|
||||
final int buildExitCode = await processRunner.runAndStream(
|
||||
flutterCommand,
|
||||
<String>[
|
||||
'build',
|
||||
kWeb,
|
||||
kPlatformFlagWeb,
|
||||
if (enableExperiment.isNotEmpty)
|
||||
'--enable-experiment=$enableExperiment',
|
||||
],
|
||||
@ -126,14 +132,14 @@ class BuildExamplesCommand extends PluginCommand {
|
||||
}
|
||||
}
|
||||
|
||||
if (getBoolArg(kWindows)) {
|
||||
if (getBoolArg(kPlatformFlagWindows)) {
|
||||
print('\nBUILDING Windows for $packageName');
|
||||
if (isWindowsPlugin(plugin)) {
|
||||
final int buildExitCode = await processRunner.runAndStream(
|
||||
flutterCommand,
|
||||
<String>[
|
||||
'build',
|
||||
kWindows,
|
||||
kPlatformFlagWindows,
|
||||
if (enableExperiment.isNotEmpty)
|
||||
'--enable-experiment=$enableExperiment',
|
||||
],
|
||||
|
@ -21,28 +21,22 @@ import 'package:yaml/yaml.dart';
|
||||
typedef Print = void Function(Object? object);
|
||||
|
||||
/// Key for windows platform.
|
||||
const String kWindows = 'windows';
|
||||
const String kPlatformFlagWindows = 'windows';
|
||||
|
||||
/// Key for macos platform.
|
||||
const String kMacos = 'macos';
|
||||
const String kPlatformFlagMacos = 'macos';
|
||||
|
||||
/// Key for linux platform.
|
||||
const String kLinux = 'linux';
|
||||
const String kPlatformFlagLinux = 'linux';
|
||||
|
||||
/// Key for IPA (iOS) platform.
|
||||
const String kIos = 'ios';
|
||||
const String kPlatformFlagIos = 'ios';
|
||||
|
||||
/// Key for APK (Android) platform.
|
||||
const String kAndroid = 'android';
|
||||
const String kPlatformFlagAndroid = 'android';
|
||||
|
||||
/// Key for Web platform.
|
||||
const String kWeb = 'web';
|
||||
|
||||
/// Key for IPA.
|
||||
const String kIpa = 'ipa';
|
||||
|
||||
/// Key for APK.
|
||||
const String kApk = 'apk';
|
||||
const String kPlatformFlagWeb = 'web';
|
||||
|
||||
/// Key for enable experiment.
|
||||
const String kEnableExperiment = 'enable-experiment';
|
||||
@ -69,6 +63,15 @@ bool isFlutterPackage(FileSystemEntity entity) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Possible plugin support options for a platform.
|
||||
enum PlatformSupport {
|
||||
/// The platform has an implementation in the package.
|
||||
inline,
|
||||
|
||||
/// The platform has an endorsed federated implementation in another package.
|
||||
federated,
|
||||
}
|
||||
|
||||
/// Returns whether the given directory contains a Flutter [platform] plugin.
|
||||
///
|
||||
/// It checks this by looking for the following pattern in the pubspec:
|
||||
@ -77,13 +80,17 @@ bool isFlutterPackage(FileSystemEntity entity) {
|
||||
/// plugin:
|
||||
/// platforms:
|
||||
/// [platform]:
|
||||
bool pluginSupportsPlatform(String platform, FileSystemEntity entity) {
|
||||
assert(platform == kIos ||
|
||||
platform == kAndroid ||
|
||||
platform == kWeb ||
|
||||
platform == kMacos ||
|
||||
platform == kWindows ||
|
||||
platform == kLinux);
|
||||
///
|
||||
/// If [requiredMode] is provided, the plugin must have the given type of
|
||||
/// implementation in order to return true.
|
||||
bool pluginSupportsPlatform(String platform, FileSystemEntity entity,
|
||||
{PlatformSupport? requiredMode}) {
|
||||
assert(platform == kPlatformFlagIos ||
|
||||
platform == kPlatformFlagAndroid ||
|
||||
platform == kPlatformFlagWeb ||
|
||||
platform == kPlatformFlagMacos ||
|
||||
platform == kPlatformFlagWindows ||
|
||||
platform == kPlatformFlagLinux);
|
||||
if (entity is! Directory) {
|
||||
return false;
|
||||
}
|
||||
@ -102,13 +109,25 @@ bool pluginSupportsPlatform(String platform, FileSystemEntity entity) {
|
||||
}
|
||||
final YamlMap? platforms = pluginSection['platforms'] as YamlMap?;
|
||||
if (platforms == null) {
|
||||
// Legacy plugin specs are assumed to support iOS and Android.
|
||||
// Legacy plugin specs are assumed to support iOS and Android. They are
|
||||
// never federated.
|
||||
if (requiredMode == PlatformSupport.federated) {
|
||||
return false;
|
||||
}
|
||||
if (!pluginSection.containsKey('platforms')) {
|
||||
return platform == kIos || platform == kAndroid;
|
||||
return platform == kPlatformFlagIos || platform == kPlatformFlagAndroid;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return platforms.containsKey(platform);
|
||||
final YamlMap? platformEntry = platforms[platform] as YamlMap?;
|
||||
if (platformEntry == null) {
|
||||
return false;
|
||||
}
|
||||
// If the platform entry is present, then it supports the platform. Check
|
||||
// for required mode if specified.
|
||||
final bool federated = platformEntry.containsKey('default_package');
|
||||
return requiredMode == null ||
|
||||
federated == (requiredMode == PlatformSupport.federated);
|
||||
} on FileSystemException {
|
||||
return false;
|
||||
} on YamlException {
|
||||
@ -118,32 +137,32 @@ bool pluginSupportsPlatform(String platform, FileSystemEntity entity) {
|
||||
|
||||
/// Returns whether the given directory contains a Flutter Android plugin.
|
||||
bool isAndroidPlugin(FileSystemEntity entity) {
|
||||
return pluginSupportsPlatform(kAndroid, entity);
|
||||
return pluginSupportsPlatform(kPlatformFlagAndroid, entity);
|
||||
}
|
||||
|
||||
/// Returns whether the given directory contains a Flutter iOS plugin.
|
||||
bool isIosPlugin(FileSystemEntity entity) {
|
||||
return pluginSupportsPlatform(kIos, entity);
|
||||
return pluginSupportsPlatform(kPlatformFlagIos, entity);
|
||||
}
|
||||
|
||||
/// Returns whether the given directory contains a Flutter web plugin.
|
||||
bool isWebPlugin(FileSystemEntity entity) {
|
||||
return pluginSupportsPlatform(kWeb, entity);
|
||||
return pluginSupportsPlatform(kPlatformFlagWeb, entity);
|
||||
}
|
||||
|
||||
/// Returns whether the given directory contains a Flutter Windows plugin.
|
||||
bool isWindowsPlugin(FileSystemEntity entity) {
|
||||
return pluginSupportsPlatform(kWindows, entity);
|
||||
return pluginSupportsPlatform(kPlatformFlagWindows, entity);
|
||||
}
|
||||
|
||||
/// Returns whether the given directory contains a Flutter macOS plugin.
|
||||
bool isMacOsPlugin(FileSystemEntity entity) {
|
||||
return pluginSupportsPlatform(kMacos, entity);
|
||||
return pluginSupportsPlatform(kPlatformFlagMacos, entity);
|
||||
}
|
||||
|
||||
/// Returns whether the given directory contains a Flutter linux plugin.
|
||||
bool isLinuxPlugin(FileSystemEntity entity) {
|
||||
return pluginSupportsPlatform(kLinux, entity);
|
||||
return pluginSupportsPlatform(kPlatformFlagLinux, entity);
|
||||
}
|
||||
|
||||
/// Throws a [ToolExit] with `exitCode` and log the `errorMessage` in red.
|
||||
|
@ -15,17 +15,17 @@ class DriveExamplesCommand extends PluginCommand {
|
||||
Directory packagesDir, {
|
||||
ProcessRunner processRunner = const ProcessRunner(),
|
||||
}) : super(packagesDir, processRunner: processRunner) {
|
||||
argParser.addFlag(kAndroid,
|
||||
argParser.addFlag(kPlatformFlagAndroid,
|
||||
help: 'Runs the Android implementation of the examples');
|
||||
argParser.addFlag(kIos,
|
||||
argParser.addFlag(kPlatformFlagIos,
|
||||
help: 'Runs the iOS implementation of the examples');
|
||||
argParser.addFlag(kLinux,
|
||||
argParser.addFlag(kPlatformFlagLinux,
|
||||
help: 'Runs the Linux implementation of the examples');
|
||||
argParser.addFlag(kMacos,
|
||||
argParser.addFlag(kPlatformFlagMacos,
|
||||
help: 'Runs the macOS implementation of the examples');
|
||||
argParser.addFlag(kWeb,
|
||||
argParser.addFlag(kPlatformFlagWeb,
|
||||
help: 'Runs the web implementation of the examples');
|
||||
argParser.addFlag(kWindows,
|
||||
argParser.addFlag(kPlatformFlagWindows,
|
||||
help: 'Runs the Windows implementation of the examples');
|
||||
argParser.addOption(
|
||||
kEnableExperiment,
|
||||
@ -52,10 +52,10 @@ class DriveExamplesCommand extends PluginCommand {
|
||||
Future<void> run() async {
|
||||
final List<String> failingTests = <String>[];
|
||||
final List<String> pluginsWithoutTests = <String>[];
|
||||
final bool isLinux = getBoolArg(kLinux);
|
||||
final bool isMacos = getBoolArg(kMacos);
|
||||
final bool isWeb = getBoolArg(kWeb);
|
||||
final bool isWindows = getBoolArg(kWindows);
|
||||
final bool isLinux = getBoolArg(kPlatformFlagLinux);
|
||||
final bool isMacos = getBoolArg(kPlatformFlagMacos);
|
||||
final bool isWeb = getBoolArg(kPlatformFlagWeb);
|
||||
final bool isWindows = getBoolArg(kPlatformFlagWindows);
|
||||
await for (final Directory plugin in getPlugins()) {
|
||||
final String pluginName = plugin.basename;
|
||||
if (pluginName.endsWith('_platform_interface') &&
|
||||
@ -219,12 +219,12 @@ Tried searching for the following:
|
||||
|
||||
Future<bool> _pluginSupportedOnCurrentPlatform(
|
||||
FileSystemEntity plugin) async {
|
||||
final bool isAndroid = getBoolArg(kAndroid);
|
||||
final bool isIOS = getBoolArg(kIos);
|
||||
final bool isLinux = getBoolArg(kLinux);
|
||||
final bool isMacos = getBoolArg(kMacos);
|
||||
final bool isWeb = getBoolArg(kWeb);
|
||||
final bool isWindows = getBoolArg(kWindows);
|
||||
final bool isAndroid = getBoolArg(kPlatformFlagAndroid);
|
||||
final bool isIOS = getBoolArg(kPlatformFlagIos);
|
||||
final bool isLinux = getBoolArg(kPlatformFlagLinux);
|
||||
final bool isMacos = getBoolArg(kPlatformFlagMacos);
|
||||
final bool isWeb = getBoolArg(kPlatformFlagWeb);
|
||||
final bool isWindows = getBoolArg(kPlatformFlagWindows);
|
||||
if (isAndroid) {
|
||||
return isAndroidPlugin(plugin);
|
||||
}
|
||||
|
@ -17,8 +17,10 @@ const String _kXCRunCommand = 'xcrun';
|
||||
const String _kFoundNoSimulatorsMessage =
|
||||
'Cannot find any available simulators, tests failed';
|
||||
|
||||
/// The command to run iOS XCTests in plugins, this should work for both XCUnitTest and XCUITest targets.
|
||||
/// The tests target have to be added to the xcode project of the example app. Usually at "example/ios/Runner.xcworkspace".
|
||||
/// The command to run XCTests (XCUnitTest and XCUITest) in plugins.
|
||||
/// The tests target have to be added to the Xcode project of the example app,
|
||||
/// usually at "example/{ios,macos}/Runner.xcworkspace".
|
||||
///
|
||||
/// The static analyzer is also run.
|
||||
class XCTestCommand extends PluginCommand {
|
||||
/// Creates an instance of the test command.
|
||||
@ -33,52 +35,61 @@ class XCTestCommand extends PluginCommand {
|
||||
'this is passed to the `-destination` argument in xcodebuild command.\n'
|
||||
'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT for details on how to specify the destination.',
|
||||
);
|
||||
argParser.addFlag(kPlatformFlagIos, help: 'Runs the iOS tests');
|
||||
argParser.addFlag(kPlatformFlagMacos, help: 'Runs the macOS tests');
|
||||
}
|
||||
|
||||
@override
|
||||
final String name = 'xctest';
|
||||
|
||||
@override
|
||||
final String description = 'Runs the xctests in the iOS example apps.\n\n'
|
||||
final String description =
|
||||
'Runs the xctests in the iOS and/or macOS example apps.\n\n'
|
||||
'This command requires "flutter" and "xcrun" to be in your path.';
|
||||
|
||||
@override
|
||||
Future<void> run() async {
|
||||
String destination = getStringArg(_kiOSDestination);
|
||||
if (destination.isEmpty) {
|
||||
final String? simulatorId = await _findAvailableIphoneSimulator();
|
||||
if (simulatorId == null) {
|
||||
print(_kFoundNoSimulatorsMessage);
|
||||
throw ToolExit(1);
|
||||
final bool testIos = getBoolArg(kPlatformFlagIos);
|
||||
final bool testMacos = getBoolArg(kPlatformFlagMacos);
|
||||
|
||||
if (!(testIos || testMacos)) {
|
||||
print('At least one platform flag must be provided.');
|
||||
throw ToolExit(2);
|
||||
}
|
||||
|
||||
List<String> iosDestinationFlags = <String>[];
|
||||
if (testIos) {
|
||||
String destination = getStringArg(_kiOSDestination);
|
||||
if (destination.isEmpty) {
|
||||
final String? simulatorId = await _findAvailableIphoneSimulator();
|
||||
if (simulatorId == null) {
|
||||
print(_kFoundNoSimulatorsMessage);
|
||||
throw ToolExit(1);
|
||||
}
|
||||
destination = 'id=$simulatorId';
|
||||
}
|
||||
destination = 'id=$simulatorId';
|
||||
iosDestinationFlags = <String>[
|
||||
'-destination',
|
||||
destination,
|
||||
];
|
||||
}
|
||||
|
||||
final List<String> failingPackages = <String>[];
|
||||
await for (final Directory plugin in getPlugins()) {
|
||||
// Start running for package.
|
||||
final String packageName =
|
||||
p.relative(plugin.path, from: packagesDir.path);
|
||||
print('Start running for $packageName ...');
|
||||
if (!isIosPlugin(plugin)) {
|
||||
print('iOS is not supported by this plugin.');
|
||||
print('\n\n');
|
||||
continue;
|
||||
print('============================================================');
|
||||
print('Start running for $packageName...');
|
||||
bool passed = true;
|
||||
if (testIos) {
|
||||
passed &= await _testPlugin(plugin, 'iOS',
|
||||
extraXcrunFlags: iosDestinationFlags);
|
||||
}
|
||||
for (final Directory example in getExamplesForPlugin(plugin)) {
|
||||
// Running tests and static analyzer.
|
||||
print('Running tests and analyzer for $packageName ...');
|
||||
int exitCode = await _runTests(true, destination, example);
|
||||
// 66 = there is no test target (this fails fast). Try again with just the analyzer.
|
||||
if (exitCode == 66) {
|
||||
print('Tests not found for $packageName, running analyzer only...');
|
||||
exitCode = await _runTests(false, destination, example);
|
||||
}
|
||||
if (exitCode == 0) {
|
||||
print('Successfully ran xctest for $packageName');
|
||||
} else {
|
||||
failingPackages.add(packageName);
|
||||
}
|
||||
if (testMacos) {
|
||||
passed &= await _testPlugin(plugin, 'macOS');
|
||||
}
|
||||
if (!passed) {
|
||||
failingPackages.add(packageName);
|
||||
}
|
||||
}
|
||||
|
||||
@ -95,19 +106,59 @@ class XCTestCommand extends PluginCommand {
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> _runTests(bool runTests, String destination, Directory example) {
|
||||
/// Runs all applicable tests for [plugin], printing status and returning
|
||||
/// success if the tests passed (or did not exist).
|
||||
Future<bool> _testPlugin(
|
||||
Directory plugin,
|
||||
String platform, {
|
||||
List<String> extraXcrunFlags = const <String>[],
|
||||
}) async {
|
||||
if (!pluginSupportsPlatform(platform.toLowerCase(), plugin,
|
||||
requiredMode: PlatformSupport.inline)) {
|
||||
print('$platform is not implemented by this plugin package.');
|
||||
print('\n');
|
||||
return true;
|
||||
}
|
||||
bool passing = true;
|
||||
for (final Directory example in getExamplesForPlugin(plugin)) {
|
||||
// Running tests and static analyzer.
|
||||
final String examplePath =
|
||||
p.relative(example.path, from: plugin.parent.path);
|
||||
print('Running $platform tests and analyzer for $examplePath...');
|
||||
int exitCode =
|
||||
await _runTests(true, example, platform, extraFlags: extraXcrunFlags);
|
||||
// 66 = there is no test target (this fails fast). Try again with just the analyzer.
|
||||
if (exitCode == 66) {
|
||||
print('Tests not found for $examplePath, running analyzer only...');
|
||||
exitCode = await _runTests(false, example, platform,
|
||||
extraFlags: extraXcrunFlags);
|
||||
}
|
||||
if (exitCode == 0) {
|
||||
print('Successfully ran $platform xctest for $examplePath');
|
||||
} else {
|
||||
passing = false;
|
||||
}
|
||||
}
|
||||
return passing;
|
||||
}
|
||||
|
||||
Future<int> _runTests(
|
||||
bool runTests,
|
||||
Directory example,
|
||||
String platform, {
|
||||
List<String> extraFlags = const <String>[],
|
||||
}) {
|
||||
final List<String> xctestArgs = <String>[
|
||||
_kXcodeBuildCommand,
|
||||
if (runTests) 'test',
|
||||
'analyze',
|
||||
'-workspace',
|
||||
'ios/Runner.xcworkspace',
|
||||
'${platform.toLowerCase()}/Runner.xcworkspace',
|
||||
'-configuration',
|
||||
'Debug',
|
||||
'-scheme',
|
||||
'Runner',
|
||||
'-destination',
|
||||
destination,
|
||||
...extraFlags,
|
||||
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
|
||||
];
|
||||
final String completeTestCommand =
|
||||
|
Reference in New Issue
Block a user