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:
stuartmorgan
2021-06-10 14:50:20 -07:00
committed by GitHub
parent b98034dd76
commit cb92e5d416
12 changed files with 956 additions and 549 deletions

View File

@ -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',
],

View File

@ -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.

View File

@ -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);
}

View File

@ -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 =