[flutter_plugin_tests] Split analyze out of xctest (#4161)

To prep for making a combined command to run native tests across different platforms, rework `xctest`:
- Split analyze out into a new `xcode-analyze` command:
  - Since the analyze step runs a new build over everything with different flags, this is only a small amount slower than the combined version
  - This makes the logic easier to follow
  - This allows us to meaningfully report skips, to better notice missing tests.
- Add the ability to target specific test bundles (RunnerTests or RunnerUITests)

To share code between the commands, this extracts a new `Xcode` helper class.

Part of https://github.com/flutter/flutter/issues/84392 and https://github.com/flutter/flutter/issues/86489
This commit is contained in:
stuartmorgan
2021-07-20 18:17:20 -07:00
committed by GitHub
parent a206782759
commit ff8cb52f8e
8 changed files with 1440 additions and 296 deletions

View File

@ -1,3 +1,9 @@
## NEXT
- Added an `xctest` flag to select specific test targets, to allow running only
unit tests or integration tests.
- Split Xcode analysis out of `xctest` and into a new `xcode-analyze` command.
## 0.4.1
- Improved `license-check` output.

View File

@ -0,0 +1,159 @@
// 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 'dart:convert';
import 'dart:io' as io;
import 'package:file/file.dart';
import 'core.dart';
import 'process_runner.dart';
const String _xcodeBuildCommand = 'xcodebuild';
const String _xcRunCommand = 'xcrun';
/// A utility class for interacting with the installed version of Xcode.
class Xcode {
/// Creates an instance that runs commends with the given [processRunner].
///
/// If [log] is true, commands run by this instance will long various status
/// messages.
Xcode({
this.processRunner = const ProcessRunner(),
this.log = false,
});
/// The [ProcessRunner] used to run commands. Overridable for testing.
final ProcessRunner processRunner;
/// Whether or not to log when running commands.
final bool log;
/// Runs an `xcodebuild` in [directory] with the given parameters.
Future<int> runXcodeBuild(
Directory directory, {
List<String> actions = const <String>['build'],
required String workspace,
required String scheme,
String? configuration,
List<String> extraFlags = const <String>[],
}) {
final List<String> args = <String>[
_xcodeBuildCommand,
...actions,
if (workspace != null) ...<String>['-workspace', workspace],
if (scheme != null) ...<String>['-scheme', scheme],
if (configuration != null) ...<String>['-configuration', configuration],
...extraFlags,
];
final String completeTestCommand = '$_xcRunCommand ${args.join(' ')}';
if (log) {
print(completeTestCommand);
}
return processRunner.runAndStream(_xcRunCommand, args,
workingDir: directory);
}
/// Returns true if [project], which should be an .xcodeproj directory,
/// contains a target called [target], false if it does not, and null if the
/// check fails (e.g., if [project] is not an Xcode project).
Future<bool?> projectHasTarget(Directory project, String target) async {
final io.ProcessResult result =
await processRunner.run(_xcRunCommand, <String>[
_xcodeBuildCommand,
'-list',
'-json',
'-project',
project.path,
]);
if (result.exitCode != 0) {
return null;
}
Map<String, dynamic>? projectInfo;
try {
projectInfo = (jsonDecode(result.stdout as String)
as Map<String, dynamic>)['project'] as Map<String, dynamic>?;
} on FormatException {
return null;
}
if (projectInfo == null) {
return null;
}
final List<String>? targets =
(projectInfo['targets'] as List<dynamic>?)?.cast<String>();
return targets?.contains(target) ?? false;
}
/// Returns the newest available simulator (highest OS version, with ties
/// broken in favor of newest device), if any.
Future<String?> findBestAvailableIphoneSimulator() async {
final List<String> findSimulatorsArguments = <String>[
'simctl',
'list',
'devices',
'runtimes',
'available',
'--json',
];
final String findSimulatorCompleteCommand =
'$_xcRunCommand ${findSimulatorsArguments.join(' ')}';
if (log) {
print('Looking for available simulators...');
print(findSimulatorCompleteCommand);
}
final io.ProcessResult findSimulatorsResult =
await processRunner.run(_xcRunCommand, findSimulatorsArguments);
if (findSimulatorsResult.exitCode != 0) {
if (log) {
printError(
'Error occurred while running "$findSimulatorCompleteCommand":\n'
'${findSimulatorsResult.stderr}');
}
return null;
}
final Map<String, dynamic> simulatorListJson =
jsonDecode(findSimulatorsResult.stdout as String)
as Map<String, dynamic>;
final List<Map<String, dynamic>> runtimes =
(simulatorListJson['runtimes'] as List<dynamic>)
.cast<Map<String, dynamic>>();
final Map<String, Object> devices =
(simulatorListJson['devices'] as Map<String, dynamic>)
.cast<String, Object>();
if (runtimes.isEmpty || devices.isEmpty) {
return null;
}
String? id;
// Looking for runtimes, trying to find one with highest OS version.
for (final Map<String, dynamic> rawRuntimeMap in runtimes.reversed) {
final Map<String, Object> runtimeMap =
rawRuntimeMap.cast<String, Object>();
if ((runtimeMap['name'] as String?)?.contains('iOS') != true) {
continue;
}
final String? runtimeID = runtimeMap['identifier'] as String?;
if (runtimeID == null) {
continue;
}
final List<Map<String, dynamic>>? devicesForRuntime =
(devices[runtimeID] as List<dynamic>?)?.cast<Map<String, dynamic>>();
if (devicesForRuntime == null || devicesForRuntime.isEmpty) {
continue;
}
// Looking for runtimes, trying to find latest version of device.
for (final Map<String, dynamic> rawDevice in devicesForRuntime.reversed) {
final Map<String, Object> device = rawDevice.cast<String, Object>();
id = device['udid'] as String?;
if (id == null) {
continue;
}
if (log) {
print('device selected: $device');
}
return id;
}
}
return null;
}
}

View File

@ -24,6 +24,7 @@ import 'publish_plugin_command.dart';
import 'pubspec_check_command.dart';
import 'test_command.dart';
import 'version_check_command.dart';
import 'xcode_analyze_command.dart';
import 'xctest_command.dart';
void main(List<String> args) {
@ -59,6 +60,7 @@ void main(List<String> args) {
..addCommand(PubspecCheckCommand(packagesDir))
..addCommand(TestCommand(packagesDir))
..addCommand(VersionCheckCommand(packagesDir))
..addCommand(XcodeAnalyzeCommand(packagesDir))
..addCommand(XCTestCommand(packagesDir));
commandRunner.run(args).catchError((Object e) {

View File

@ -0,0 +1,111 @@
// 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:file/file.dart';
import 'package:platform/platform.dart';
import 'common/core.dart';
import 'common/package_looping_command.dart';
import 'common/plugin_utils.dart';
import 'common/process_runner.dart';
import 'common/xcode.dart';
/// The command to run Xcode's static analyzer on plugins.
class XcodeAnalyzeCommand extends PackageLoopingCommand {
/// Creates an instance of the test command.
XcodeAnalyzeCommand(
Directory packagesDir, {
ProcessRunner processRunner = const ProcessRunner(),
Platform platform = const LocalPlatform(),
}) : _xcode = Xcode(processRunner: processRunner, log: true),
super(packagesDir, processRunner: processRunner, platform: platform) {
argParser.addFlag(kPlatformIos, help: 'Analyze iOS');
argParser.addFlag(kPlatformMacos, help: 'Analyze macOS');
}
final Xcode _xcode;
@override
final String name = 'xcode-analyze';
@override
final String description =
'Runs Xcode analysis on the iOS and/or macOS example apps.';
@override
Future<void> initializeRun() async {
if (!(getBoolArg(kPlatformIos) || getBoolArg(kPlatformMacos))) {
printError('At least one platform flag must be provided.');
throw ToolExit(exitInvalidArguments);
}
}
@override
Future<PackageResult> runForPackage(Directory package) async {
final bool testIos = getBoolArg(kPlatformIos) &&
pluginSupportsPlatform(kPlatformIos, package,
requiredMode: PlatformSupport.inline);
final bool testMacos = getBoolArg(kPlatformMacos) &&
pluginSupportsPlatform(kPlatformMacos, package,
requiredMode: PlatformSupport.inline);
final bool multiplePlatformsRequested =
getBoolArg(kPlatformIos) && getBoolArg(kPlatformMacos);
if (!(testIos || testMacos)) {
return PackageResult.skip('Not implemented for target platform(s).');
}
final List<String> failures = <String>[];
if (testIos &&
!await _analyzePlugin(package, 'iOS', extraFlags: <String>[
'-destination',
'generic/platform=iOS Simulator'
])) {
failures.add('iOS');
}
if (testMacos && !await _analyzePlugin(package, 'macOS')) {
failures.add('macOS');
}
// Only provide the failing platform in the failure details if testing
// multiple platforms, otherwise it's just noise.
return failures.isEmpty
? PackageResult.success()
: PackageResult.fail(
multiplePlatformsRequested ? failures : <String>[]);
}
/// Analyzes [plugin] for [platform], returning true if it passed analysis.
Future<bool> _analyzePlugin(
Directory plugin,
String platform, {
List<String> extraFlags = const <String>[],
}) async {
bool passing = true;
for (final Directory example in getExamplesForPlugin(plugin)) {
// Running tests and static analyzer.
final String examplePath =
getRelativePosixPath(example, from: plugin.parent);
print('Running $platform tests and analyzer for $examplePath...');
final int exitCode = await _xcode.runXcodeBuild(
example,
actions: <String>['analyze'],
workspace: '${platform.toLowerCase()}/Runner.xcworkspace',
scheme: 'Runner',
configuration: 'Debug',
extraFlags: <String>[
...extraFlags,
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
],
);
if (exitCode == 0) {
printSuccess('$examplePath ($platform) passed analysis.');
} else {
printError('$examplePath ($platform) failed analysis.');
passing = false;
}
}
return passing;
}
}

View File

@ -2,9 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert';
import 'dart:io' as io;
import 'package:file/file.dart';
import 'package:platform/platform.dart';
@ -12,35 +9,39 @@ import 'common/core.dart';
import 'common/package_looping_command.dart';
import 'common/plugin_utils.dart';
import 'common/process_runner.dart';
import 'common/xcode.dart';
const String _kiOSDestination = 'ios-destination';
const String _kXcodeBuildCommand = 'xcodebuild';
const String _kXCRunCommand = 'xcrun';
const String _kFoundNoSimulatorsMessage =
'Cannot find any available simulators, tests failed';
const String _iosDestinationFlag = 'ios-destination';
const String _testTargetFlag = 'test-target';
const int _exitFindingSimulatorsFailed = 3;
const int _exitNoSimulators = 4;
// The exit code from 'xcodebuild test' when there are no tests.
const int _xcodebuildNoTestExitCode = 66;
const int _exitNoSimulators = 3;
/// 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 PackageLoopingCommand {
/// Creates an instance of the test command.
XCTestCommand(
Directory packagesDir, {
ProcessRunner processRunner = const ProcessRunner(),
Platform platform = const LocalPlatform(),
}) : super(packagesDir, processRunner: processRunner, platform: platform) {
}) : _xcode = Xcode(processRunner: processRunner, log: true),
super(packagesDir, processRunner: processRunner, platform: platform) {
argParser.addOption(
_kiOSDestination,
_iosDestinationFlag,
help:
'Specify the destination when running the test, used for -destination flag for xcodebuild command.\n'
'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.addOption(
_testTargetFlag,
help:
'Limits the tests to a specific target (e.g., RunnerTests or RunnerUITests)',
);
argParser.addFlag(kPlatformIos, help: 'Runs the iOS tests');
argParser.addFlag(kPlatformMacos, help: 'Runs the macOS tests');
}
@ -48,6 +49,8 @@ class XCTestCommand extends PackageLoopingCommand {
// The device destination flags for iOS tests.
List<String> _iosDestinationFlags = <String>[];
final Xcode _xcode;
@override
final String name = 'xctest';
@ -56,9 +59,6 @@ class XCTestCommand extends PackageLoopingCommand {
'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
String get failureListHeader => 'The following packages are failing XCTests:';
@override
Future<void> initializeRun() async {
final bool shouldTestIos = getBoolArg(kPlatformIos);
@ -70,11 +70,12 @@ class XCTestCommand extends PackageLoopingCommand {
}
if (shouldTestIos) {
String destination = getStringArg(_kiOSDestination);
String destination = getStringArg(_iosDestinationFlag);
if (destination.isEmpty) {
final String? simulatorId = await _findAvailableIphoneSimulator();
final String? simulatorId =
await _xcode.findBestAvailableIphoneSimulator();
if (simulatorId == null) {
printError(_kFoundNoSimulatorsMessage);
printError('Cannot find any available simulators, tests failed');
throw ToolExit(_exitNoSimulators);
}
destination = 'id=$simulatorId';
@ -115,15 +116,26 @@ class XCTestCommand extends PackageLoopingCommand {
}
final List<String> failures = <String>[];
if (testIos &&
!await _testPlugin(package, 'iOS',
extraXcrunFlags: _iosDestinationFlags)) {
failures.add('iOS');
bool ranTests = false;
if (testIos) {
final RunState result = await _testPlugin(package, 'iOS',
extraXcrunFlags: _iosDestinationFlags);
ranTests |= result != RunState.skipped;
if (result == RunState.failed) {
failures.add('iOS');
}
}
if (testMacos && !await _testPlugin(package, 'macOS')) {
failures.add('macOS');
if (testMacos) {
final RunState result = await _testPlugin(package, 'macOS');
ranTests |= result != RunState.skipped;
if (result == RunState.failed) {
failures.add('macOS');
}
}
if (!ranTests) {
return PackageResult.skip('No tests found.');
}
// Only provide the failing platform in the failure details if testing
// multiple platforms, otherwise it's just noise.
return failures.isEmpty
@ -133,124 +145,67 @@ class XCTestCommand extends PackageLoopingCommand {
}
/// Runs all applicable tests for [plugin], printing status and returning
/// success if the tests passed.
Future<bool> _testPlugin(
/// the test result.
Future<RunState> _testPlugin(
Directory plugin,
String platform, {
List<String> extraXcrunFlags = const <String>[],
}) async {
bool passing = true;
final String testTarget = getStringArg(_testTargetFlag);
// Assume skipped until at least one test has run.
RunState overallResult = RunState.skipped;
for (final Directory example in getExamplesForPlugin(plugin)) {
// Running tests and static analyzer.
final String examplePath =
getRelativePosixPath(example, from: plugin.parent);
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) {
printSuccess('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',
'${platform.toLowerCase()}/Runner.xcworkspace',
'-configuration',
'Debug',
'-scheme',
'Runner',
...extraFlags,
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
];
final String completeTestCommand =
'$_kXCRunCommand ${xctestArgs.join(' ')}';
print(completeTestCommand);
return processRunner.runAndStream(_kXCRunCommand, xctestArgs,
workingDir: example);
}
Future<String?> _findAvailableIphoneSimulator() async {
// Find the first available destination if not specified.
final List<String> findSimulatorsArguments = <String>[
'simctl',
'list',
'--json'
];
final String findSimulatorCompleteCommand =
'$_kXCRunCommand ${findSimulatorsArguments.join(' ')}';
print('Looking for available simulators...');
print(findSimulatorCompleteCommand);
final io.ProcessResult findSimulatorsResult =
await processRunner.run(_kXCRunCommand, findSimulatorsArguments);
if (findSimulatorsResult.exitCode != 0) {
printError(
'Error occurred while running "$findSimulatorCompleteCommand":\n'
'${findSimulatorsResult.stderr}');
throw ToolExit(_exitFindingSimulatorsFailed);
}
final Map<String, dynamic> simulatorListJson =
jsonDecode(findSimulatorsResult.stdout as String)
as Map<String, dynamic>;
final List<Map<String, dynamic>> runtimes =
(simulatorListJson['runtimes'] as List<dynamic>)
.cast<Map<String, dynamic>>();
final Map<String, Object> devices =
(simulatorListJson['devices'] as Map<String, dynamic>)
.cast<String, Object>();
if (runtimes.isEmpty || devices.isEmpty) {
return null;
}
String? id;
// Looking for runtimes, trying to find one with highest OS version.
for (final Map<String, dynamic> rawRuntimeMap in runtimes.reversed) {
final Map<String, Object> runtimeMap =
rawRuntimeMap.cast<String, Object>();
if ((runtimeMap['name'] as String?)?.contains('iOS') != true) {
continue;
}
final String? runtimeID = runtimeMap['identifier'] as String?;
if (runtimeID == null) {
continue;
}
final List<Map<String, dynamic>>? devicesForRuntime =
(devices[runtimeID] as List<dynamic>?)?.cast<Map<String, dynamic>>();
if (devicesForRuntime == null || devicesForRuntime.isEmpty) {
continue;
}
// Looking for runtimes, trying to find latest version of device.
for (final Map<String, dynamic> rawDevice in devicesForRuntime.reversed) {
final Map<String, Object> device = rawDevice.cast<String, Object>();
if (device['availabilityError'] != null ||
(device['isAvailable'] as bool?) == false) {
if (testTarget.isNotEmpty) {
final Directory project = example
.childDirectory(platform.toLowerCase())
.childDirectory('Runner.xcodeproj');
final bool? hasTarget =
await _xcode.projectHasTarget(project, testTarget);
if (hasTarget == null) {
printError('Unable to check targets for $examplePath.');
overallResult = RunState.failed;
continue;
} else if (!hasTarget) {
print('No "$testTarget" target in $examplePath; skipping.');
continue;
}
id = device['udid'] as String?;
if (id == null) {
}
print('Running $platform tests for $examplePath...');
final int exitCode = await _xcode.runXcodeBuild(
example,
actions: <String>['test'],
workspace: '${platform.toLowerCase()}/Runner.xcworkspace',
scheme: 'Runner',
configuration: 'Debug',
extraFlags: <String>[
if (testTarget.isNotEmpty) '-only-testing:$testTarget',
...extraXcrunFlags,
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
],
);
switch (exitCode) {
case _xcodebuildNoTestExitCode:
print('No tests found for $examplePath');
continue;
}
print('device selected: $device');
return id;
case 0:
printSuccess('Successfully ran $platform xctest for $examplePath');
// If this is the first test, assume success until something fails.
if (overallResult == RunState.skipped) {
overallResult = RunState.succeeded;
}
break;
default:
// Any failure means a failure overall.
overallResult = RunState.failed;
break;
}
}
return null;
return overallResult;
}
}

View File

@ -0,0 +1,396 @@
// 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 'dart:convert';
import 'dart:io' as io;
import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:flutter_plugin_tools/src/common/xcode.dart';
import 'package:test/test.dart';
import '../mocks.dart';
import '../util.dart';
void main() {
late RecordingProcessRunner processRunner;
late Xcode xcode;
setUp(() {
processRunner = RecordingProcessRunner();
xcode = Xcode(processRunner: processRunner);
});
group('findBestAvailableIphoneSimulator', () {
test('finds the newest device', () async {
const String expectedDeviceId = '1E76A0FD-38AC-4537-A989-EA639D7D012A';
// Note: This uses `dynamic` deliberately, and should not be updated to
// Object, in order to ensure that the code correctly handles this return
// type from JSON decoding.
final Map<String, dynamic> devices = <String, dynamic>{
'runtimes': <Map<String, dynamic>>[
<String, dynamic>{
'bundlePath':
'/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime',
'buildversion': '17A577',
'runtimeRoot':
'/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime/Contents/Resources/RuntimeRoot',
'identifier': 'com.apple.CoreSimulator.SimRuntime.iOS-13-0',
'version': '13.0',
'isAvailable': true,
'name': 'iOS 13.0'
},
<String, dynamic>{
'bundlePath':
'/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime',
'buildversion': '17L255',
'runtimeRoot':
'/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime/Contents/Resources/RuntimeRoot',
'identifier': 'com.apple.CoreSimulator.SimRuntime.iOS-13-4',
'version': '13.4',
'isAvailable': true,
'name': 'iOS 13.4'
},
<String, dynamic>{
'bundlePath':
'/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime',
'buildversion': '17T531',
'runtimeRoot':
'/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot',
'identifier': 'com.apple.CoreSimulator.SimRuntime.watchOS-6-2',
'version': '6.2.1',
'isAvailable': true,
'name': 'watchOS 6.2'
}
],
'devices': <String, dynamic>{
'com.apple.CoreSimulator.SimRuntime.iOS-13-4': <Map<String, dynamic>>[
<String, dynamic>{
'dataPath':
'/Users/xxx/Library/Developer/CoreSimulator/Devices/2706BBEB-1E01-403E-A8E9-70E8E5A24774/data',
'logPath':
'/Users/xxx/Library/Logs/CoreSimulator/2706BBEB-1E01-403E-A8E9-70E8E5A24774',
'udid': '2706BBEB-1E01-403E-A8E9-70E8E5A24774',
'isAvailable': true,
'deviceTypeIdentifier':
'com.apple.CoreSimulator.SimDeviceType.iPhone-8',
'state': 'Shutdown',
'name': 'iPhone 8'
},
<String, dynamic>{
'dataPath':
'/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data',
'logPath':
'/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A',
'udid': expectedDeviceId,
'isAvailable': true,
'deviceTypeIdentifier':
'com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus',
'state': 'Shutdown',
'name': 'iPhone 8 Plus'
}
]
}
};
processRunner.processToReturn = MockProcess.succeeding();
processRunner.resultStdout = jsonEncode(devices);
expect(await xcode.findBestAvailableIphoneSimulator(), expectedDeviceId);
});
test('ignores non-iOS runtimes', () async {
// Note: This uses `dynamic` deliberately, and should not be updated to
// Object, in order to ensure that the code correctly handles this return
// type from JSON decoding.
final Map<String, dynamic> devices = <String, dynamic>{
'runtimes': <Map<String, dynamic>>[
<String, dynamic>{
'bundlePath':
'/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime',
'buildversion': '17T531',
'runtimeRoot':
'/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot',
'identifier': 'com.apple.CoreSimulator.SimRuntime.watchOS-6-2',
'version': '6.2.1',
'isAvailable': true,
'name': 'watchOS 6.2'
}
],
'devices': <String, dynamic>{
'com.apple.CoreSimulator.SimRuntime.watchOS-6-2':
<Map<String, dynamic>>[
<String, dynamic>{
'dataPath':
'/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data',
'logPath':
'/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A',
'udid': '1E76A0FD-38AC-4537-A989-EA639D7D012A',
'isAvailable': true,
'deviceTypeIdentifier':
'com.apple.CoreSimulator.SimDeviceType.Apple-Watch-38mm',
'state': 'Shutdown',
'name': 'Apple Watch'
}
]
}
};
processRunner.processToReturn = MockProcess.succeeding();
processRunner.resultStdout = jsonEncode(devices);
expect(await xcode.findBestAvailableIphoneSimulator(), null);
});
test('returns null if simctl fails', () async {
processRunner.mockProcessesForExecutable['xcrun'] = <io.Process>[
MockProcess.failing(),
];
expect(await xcode.findBestAvailableIphoneSimulator(), null);
});
});
group('runXcodeBuild', () {
test('handles minimal arguments', () async {
final Directory directory = const LocalFileSystem().currentDirectory;
final int exitCode = await xcode.runXcodeBuild(
directory,
workspace: 'A.xcworkspace',
scheme: 'AScheme',
);
expect(exitCode, 0);
expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall(
'xcrun',
const <String>[
'xcodebuild',
'build',
'-workspace',
'A.xcworkspace',
'-scheme',
'AScheme',
],
directory.path),
]));
});
test('handles all arguments', () async {
final Directory directory = const LocalFileSystem().currentDirectory;
final int exitCode = await xcode.runXcodeBuild(directory,
actions: <String>['action1', 'action2'],
workspace: 'A.xcworkspace',
scheme: 'AScheme',
configuration: 'Debug',
extraFlags: <String>['-a', '-b', 'c=d']);
expect(exitCode, 0);
expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall(
'xcrun',
const <String>[
'xcodebuild',
'action1',
'action2',
'-workspace',
'A.xcworkspace',
'-scheme',
'AScheme',
'-configuration',
'Debug',
'-a',
'-b',
'c=d',
],
directory.path),
]));
});
test('returns error codes', () async {
processRunner.mockProcessesForExecutable['xcrun'] = <io.Process>[
MockProcess.failing(),
];
final Directory directory = const LocalFileSystem().currentDirectory;
final int exitCode = await xcode.runXcodeBuild(
directory,
workspace: 'A.xcworkspace',
scheme: 'AScheme',
);
expect(exitCode, 1);
expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall(
'xcrun',
const <String>[
'xcodebuild',
'build',
'-workspace',
'A.xcworkspace',
'-scheme',
'AScheme',
],
directory.path),
]));
});
});
group('projectHasTarget', () {
test('returns true when present', () async {
processRunner.processToReturn = MockProcess.succeeding();
processRunner.resultStdout = '''
{
"project" : {
"configurations" : [
"Debug",
"Release"
],
"name" : "Runner",
"schemes" : [
"Runner"
],
"targets" : [
"Runner",
"RunnerTests",
"RunnerUITests"
]
}
}''';
final Directory project =
const LocalFileSystem().directory('/foo.xcodeproj');
expect(await xcode.projectHasTarget(project, 'RunnerTests'), true);
expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall(
'xcrun',
<String>[
'xcodebuild',
'-list',
'-json',
'-project',
project.path,
],
null),
]));
});
test('returns false when not present', () async {
processRunner.processToReturn = MockProcess.succeeding();
processRunner.resultStdout = '''
{
"project" : {
"configurations" : [
"Debug",
"Release"
],
"name" : "Runner",
"schemes" : [
"Runner"
],
"targets" : [
"Runner",
"RunnerUITests"
]
}
}''';
final Directory project =
const LocalFileSystem().directory('/foo.xcodeproj');
expect(await xcode.projectHasTarget(project, 'RunnerTests'), false);
expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall(
'xcrun',
<String>[
'xcodebuild',
'-list',
'-json',
'-project',
project.path,
],
null),
]));
});
test('returns null for unexpected output', () async {
processRunner.processToReturn = MockProcess.succeeding();
processRunner.resultStdout = '{}';
final Directory project =
const LocalFileSystem().directory('/foo.xcodeproj');
expect(await xcode.projectHasTarget(project, 'RunnerTests'), null);
expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall(
'xcrun',
<String>[
'xcodebuild',
'-list',
'-json',
'-project',
project.path,
],
null),
]));
});
test('returns null for invalid output', () async {
processRunner.processToReturn = MockProcess.succeeding();
processRunner.resultStdout = ':)';
final Directory project =
const LocalFileSystem().directory('/foo.xcodeproj');
expect(await xcode.projectHasTarget(project, 'RunnerTests'), null);
expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall(
'xcrun',
<String>[
'xcodebuild',
'-list',
'-json',
'-project',
project.path,
],
null),
]));
});
test('returns null for failure', () async {
processRunner.processToReturn = MockProcess.failing();
final Directory project =
const LocalFileSystem().directory('/foo.xcodeproj');
expect(await xcode.projectHasTarget(project, 'RunnerTests'), null);
expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall(
'xcrun',
<String>[
'xcodebuild',
'-list',
'-json',
'-project',
project.path,
],
null),
]));
});
});
}

View File

@ -0,0 +1,416 @@
// 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 'dart:io' as io;
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/xcode_analyze_command.dart';
import 'package:test/test.dart';
import 'mocks.dart';
import 'util.dart';
// TODO(stuartmorgan): Rework these tests to use a mock Xcode instead of
// doing all the process mocking and validation.
void main() {
group('test xcode_analyze_command', () {
late FileSystem fileSystem;
late MockPlatform mockPlatform;
late Directory packagesDir;
late CommandRunner<void> runner;
late RecordingProcessRunner processRunner;
setUp(() {
fileSystem = MemoryFileSystem();
mockPlatform = MockPlatform(isMacOS: true);
packagesDir = createPackagesDirectory(fileSystem: fileSystem);
processRunner = RecordingProcessRunner();
final XcodeAnalyzeCommand command = XcodeAnalyzeCommand(packagesDir,
processRunner: processRunner, platform: mockPlatform);
runner = CommandRunner<void>(
'xcode_analyze_command', 'Test for xcode_analyze_command');
runner.addCommand(command);
});
test('Fails if no platforms are provided', () async {
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['xcode-analyze'], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('At least one platform flag must be provided'),
]),
);
});
group('iOS', () {
test('skip if iOS is not supported', () async {
createFakePlugin('plugin', packagesDir,
platformSupport: <String, PlatformSupport>{
kPlatformMacos: PlatformSupport.inline,
});
final List<String> output =
await runCapturingPrint(runner, <String>['xcode-analyze', '--ios']);
expect(output,
contains(contains('Not implemented for target platform(s).')));
expect(processRunner.recordedCalls, orderedEquals(<ProcessCall>[]));
});
test('skip if iOS is implemented in a federated package', () async {
createFakePlugin('plugin', packagesDir,
platformSupport: <String, PlatformSupport>{
kPlatformIos: PlatformSupport.federated
});
final List<String> output =
await runCapturingPrint(runner, <String>['xcode-analyze', '--ios']);
expect(output,
contains(contains('Not implemented for target platform(s).')));
expect(processRunner.recordedCalls, orderedEquals(<ProcessCall>[]));
});
test('runs for iOS plugin', () async {
final Directory pluginDirectory = createFakePlugin(
'plugin', packagesDir, platformSupport: <String, PlatformSupport>{
kPlatformIos: PlatformSupport.inline
});
final Directory pluginExampleDirectory =
pluginDirectory.childDirectory('example');
final List<String> output = await runCapturingPrint(runner, <String>[
'xcode-analyze',
'--ios',
]);
expect(
output,
containsAllInOrder(<Matcher>[
contains('Running for plugin'),
contains('plugin/example (iOS) passed analysis.')
]));
expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall(
'xcrun',
const <String>[
'xcodebuild',
'analyze',
'-workspace',
'ios/Runner.xcworkspace',
'-scheme',
'Runner',
'-configuration',
'Debug',
'-destination',
'generic/platform=iOS Simulator',
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
],
pluginExampleDirectory.path),
]));
});
test('fails if xcrun fails', () async {
createFakePlugin('plugin', packagesDir,
platformSupport: <String, PlatformSupport>{
kPlatformIos: PlatformSupport.inline
});
processRunner.mockProcessesForExecutable['xcrun'] = <io.Process>[
MockProcess.failing()
];
Error? commandError;
final List<String> output = await runCapturingPrint(
runner,
<String>[
'xcode-analyze',
'--ios',
],
errorHandler: (Error e) {
commandError = e;
},
);
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('The following packages had errors:'),
contains(' plugin'),
]));
});
});
group('macOS', () {
test('skip if macOS is not supported', () async {
createFakePlugin(
'plugin',
packagesDir,
);
final List<String> output = await runCapturingPrint(
runner, <String>['xcode-analyze', '--macos']);
expect(output,
contains(contains('Not implemented for target platform(s).')));
expect(processRunner.recordedCalls, orderedEquals(<ProcessCall>[]));
});
test('skip if macOS is implemented in a federated package', () async {
createFakePlugin('plugin', packagesDir,
platformSupport: <String, PlatformSupport>{
kPlatformMacos: PlatformSupport.federated,
});
final List<String> output = await runCapturingPrint(
runner, <String>['xcode-analyze', '--macos']);
expect(output,
contains(contains('Not implemented for target platform(s).')));
expect(processRunner.recordedCalls, orderedEquals(<ProcessCall>[]));
});
test('runs for macOS plugin', () async {
final Directory pluginDirectory1 = createFakePlugin(
'plugin', packagesDir,
platformSupport: <String, PlatformSupport>{
kPlatformMacos: PlatformSupport.inline,
});
final Directory pluginExampleDirectory =
pluginDirectory1.childDirectory('example');
final List<String> output = await runCapturingPrint(runner, <String>[
'xcode-analyze',
'--macos',
]);
expect(output,
contains(contains('plugin/example (macOS) passed analysis.')));
expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall(
'xcrun',
const <String>[
'xcodebuild',
'analyze',
'-workspace',
'macos/Runner.xcworkspace',
'-scheme',
'Runner',
'-configuration',
'Debug',
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
],
pluginExampleDirectory.path),
]));
});
test('fails if xcrun fails', () async {
createFakePlugin('plugin', packagesDir,
platformSupport: <String, PlatformSupport>{
kPlatformMacos: PlatformSupport.inline,
});
processRunner.mockProcessesForExecutable['xcrun'] = <io.Process>[
MockProcess.failing()
];
Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['xcode-analyze', '--macos'],
errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('The following packages had errors:'),
contains(' plugin'),
]),
);
});
});
group('combined', () {
test('runs both iOS and macOS when supported', () async {
final Directory pluginDirectory1 = createFakePlugin(
'plugin', packagesDir,
platformSupport: <String, PlatformSupport>{
kPlatformIos: PlatformSupport.inline,
kPlatformMacos: PlatformSupport.inline,
});
final Directory pluginExampleDirectory =
pluginDirectory1.childDirectory('example');
final List<String> output = await runCapturingPrint(runner, <String>[
'xcode-analyze',
'--ios',
'--macos',
]);
expect(
output,
containsAll(<Matcher>[
contains('plugin/example (iOS) passed analysis.'),
contains('plugin/example (macOS) passed analysis.'),
]));
expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall(
'xcrun',
const <String>[
'xcodebuild',
'analyze',
'-workspace',
'ios/Runner.xcworkspace',
'-scheme',
'Runner',
'-configuration',
'Debug',
'-destination',
'generic/platform=iOS Simulator',
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
],
pluginExampleDirectory.path),
ProcessCall(
'xcrun',
const <String>[
'xcodebuild',
'analyze',
'-workspace',
'macos/Runner.xcworkspace',
'-scheme',
'Runner',
'-configuration',
'Debug',
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
],
pluginExampleDirectory.path),
]));
});
test('runs only macOS for a macOS plugin', () async {
final Directory pluginDirectory1 = createFakePlugin(
'plugin', packagesDir,
platformSupport: <String, PlatformSupport>{
kPlatformMacos: PlatformSupport.inline,
});
final Directory pluginExampleDirectory =
pluginDirectory1.childDirectory('example');
final List<String> output = await runCapturingPrint(runner, <String>[
'xcode-analyze',
'--ios',
'--macos',
]);
expect(
output,
containsAllInOrder(<Matcher>[
contains('plugin/example (macOS) passed analysis.'),
]));
expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall(
'xcrun',
const <String>[
'xcodebuild',
'analyze',
'-workspace',
'macos/Runner.xcworkspace',
'-scheme',
'Runner',
'-configuration',
'Debug',
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
],
pluginExampleDirectory.path),
]));
});
test('runs only iOS for a iOS plugin', () async {
final Directory pluginDirectory = createFakePlugin(
'plugin', packagesDir, platformSupport: <String, PlatformSupport>{
kPlatformIos: PlatformSupport.inline
});
final Directory pluginExampleDirectory =
pluginDirectory.childDirectory('example');
final List<String> output = await runCapturingPrint(runner, <String>[
'xcode-analyze',
'--ios',
'--macos',
]);
expect(
output,
containsAllInOrder(
<Matcher>[contains('plugin/example (iOS) passed analysis.')]));
expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall(
'xcrun',
const <String>[
'xcodebuild',
'analyze',
'-workspace',
'ios/Runner.xcworkspace',
'-scheme',
'Runner',
'-configuration',
'Debug',
'-destination',
'generic/platform=iOS Simulator',
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
],
pluginExampleDirectory.path),
]));
});
test('skips when neither are supported', () async {
createFakePlugin('plugin', packagesDir);
final List<String> output = await runCapturingPrint(runner, <String>[
'xcode-analyze',
'--ios',
'--macos',
]);
expect(
output,
containsAllInOrder(<Matcher>[
contains('SKIPPING: Not implemented for target platform(s).'),
]));
expect(processRunner.recordedCalls, orderedEquals(<ProcessCall>[]));
});
});
});
}

View File

@ -16,22 +16,8 @@ import 'package:test/test.dart';
import 'mocks.dart';
import 'util.dart';
// Note: This uses `dynamic` deliberately, and should not be updated to Object,
// in order to ensure that the code correctly handles this return type from
// JSON decoding.
final Map<String, dynamic> _kDeviceListMap = <String, dynamic>{
'runtimes': <Map<String, dynamic>>[
<String, dynamic>{
'bundlePath':
'/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime',
'buildversion': '17A577',
'runtimeRoot':
'/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime/Contents/Resources/RuntimeRoot',
'identifier': 'com.apple.CoreSimulator.SimRuntime.iOS-13-0',
'version': '13.0',
'isAvailable': true,
'name': 'iOS 13.0'
},
<String, dynamic>{
'bundlePath':
'/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime',
@ -43,32 +29,9 @@ final Map<String, dynamic> _kDeviceListMap = <String, dynamic>{
'isAvailable': true,
'name': 'iOS 13.4'
},
<String, dynamic>{
'bundlePath':
'/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime',
'buildversion': '17T531',
'runtimeRoot':
'/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot',
'identifier': 'com.apple.CoreSimulator.SimRuntime.watchOS-6-2',
'version': '6.2.1',
'isAvailable': true,
'name': 'watchOS 6.2'
}
],
'devices': <String, dynamic>{
'com.apple.CoreSimulator.SimRuntime.iOS-13-4': <Map<String, dynamic>>[
<String, dynamic>{
'dataPath':
'/Users/xxx/Library/Developer/CoreSimulator/Devices/2706BBEB-1E01-403E-A8E9-70E8E5A24774/data',
'logPath':
'/Users/xxx/Library/Logs/CoreSimulator/2706BBEB-1E01-403E-A8E9-70E8E5A24774',
'udid': '2706BBEB-1E01-403E-A8E9-70E8E5A24774',
'isAvailable': true,
'deviceTypeIdentifier':
'com.apple.CoreSimulator.SimDeviceType.iPhone-8',
'state': 'Shutdown',
'name': 'iPhone 8'
},
<String, dynamic>{
'dataPath':
'/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data',
@ -85,6 +48,8 @@ final Map<String, dynamic> _kDeviceListMap = <String, dynamic>{
}
};
// TODO(stuartmorgan): Rework these tests to use a mock Xcode instead of
// doing all the process mocking and validation.
void main() {
const String _kDestination = '--ios-destination';
@ -123,13 +88,198 @@ void main() {
);
});
test('allows target filtering', () async {
final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir,
platformSupport: <String, PlatformSupport>{
kPlatformMacos: PlatformSupport.inline,
});
final Directory pluginExampleDirectory =
pluginDirectory1.childDirectory('example');
processRunner.processToReturn = MockProcess.succeeding();
processRunner.resultStdout = '{"project":{"targets":["RunnerTests"]}}';
final List<String> output = await runCapturingPrint(runner, <String>[
'xctest',
'--macos',
'--test-target=RunnerTests',
]);
expect(
output,
contains(
contains('Successfully ran macOS xctest for plugin/example')));
expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall(
'xcrun',
<String>[
'xcodebuild',
'-list',
'-json',
'-project',
pluginExampleDirectory
.childDirectory('macos')
.childDirectory('Runner.xcodeproj')
.path,
],
null),
ProcessCall(
'xcrun',
const <String>[
'xcodebuild',
'test',
'-workspace',
'macos/Runner.xcworkspace',
'-scheme',
'Runner',
'-configuration',
'Debug',
'-only-testing:RunnerTests',
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
],
pluginExampleDirectory.path),
]));
});
test('skips when the requested target is not present', () async {
final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir,
platformSupport: <String, PlatformSupport>{
kPlatformMacos: PlatformSupport.inline,
});
final Directory pluginExampleDirectory =
pluginDirectory1.childDirectory('example');
processRunner.processToReturn = MockProcess.succeeding();
processRunner.resultStdout = '{"project":{"targets":["Runner"]}}';
final List<String> output = await runCapturingPrint(runner, <String>[
'xctest',
'--macos',
'--test-target=RunnerTests',
]);
expect(
output,
containsAllInOrder(<Matcher>[
contains('No "RunnerTests" target in plugin/example; skipping.'),
]));
expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall(
'xcrun',
<String>[
'xcodebuild',
'-list',
'-json',
'-project',
pluginExampleDirectory
.childDirectory('macos')
.childDirectory('Runner.xcodeproj')
.path,
],
null),
]));
});
test('fails if unable to check for requested target', () async {
final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir,
platformSupport: <String, PlatformSupport>{
kPlatformMacos: PlatformSupport.inline,
});
final Directory pluginExampleDirectory =
pluginDirectory1.childDirectory('example');
processRunner.processToReturn = MockProcess.failing();
Error? commandError;
final List<String> output = await runCapturingPrint(runner, <String>[
'xctest',
'--macos',
'--test-target=RunnerTests',
], errorHandler: (Error e) {
commandError = e;
});
expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('Unable to check targets for plugin/example.'),
]),
);
expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall(
'xcrun',
<String>[
'xcodebuild',
'-list',
'-json',
'-project',
pluginExampleDirectory
.childDirectory('macos')
.childDirectory('Runner.xcodeproj')
.path,
],
null),
]));
});
test('reports skips with no tests', () async {
final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir,
platformSupport: <String, PlatformSupport>{
kPlatformMacos: PlatformSupport.inline,
});
final Directory pluginExampleDirectory =
pluginDirectory1.childDirectory('example');
// Exit code 66 from testing indicates no tests.
final MockProcess noTestsProcessResult = MockProcess();
noTestsProcessResult.exitCodeCompleter.complete(66);
processRunner.mockProcessesForExecutable['xcrun'] = <io.Process>[
noTestsProcessResult,
];
final List<String> output =
await runCapturingPrint(runner, <String>['xctest', '--macos']);
expect(output, contains(contains('No tests found.')));
expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall(
'xcrun',
const <String>[
'xcodebuild',
'test',
'-workspace',
'macos/Runner.xcworkspace',
'-scheme',
'Runner',
'-configuration',
'Debug',
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
],
pluginExampleDirectory.path),
]));
});
group('iOS', () {
test('skip if iOS is not supported', () async {
createFakePlugin('plugin', packagesDir, extraFiles: <String>[
'example/test',
], platformSupport: <String, PlatformSupport>{
kPlatformMacos: PlatformSupport.inline,
});
createFakePlugin('plugin', packagesDir,
platformSupport: <String, PlatformSupport>{
kPlatformMacos: PlatformSupport.inline,
});
final List<String> output = await runCapturingPrint(runner,
<String>['xctest', '--ios', _kDestination, 'foo_destination']);
@ -141,11 +291,10 @@ void main() {
});
test('skip if iOS is implemented in a federated package', () async {
createFakePlugin('plugin', packagesDir, extraFiles: <String>[
'example/test',
], platformSupport: <String, PlatformSupport>{
kPlatformIos: PlatformSupport.federated
});
createFakePlugin('plugin', packagesDir,
platformSupport: <String, PlatformSupport>{
kPlatformIos: PlatformSupport.federated
});
final List<String> output = await runCapturingPrint(runner,
<String>['xctest', '--ios', _kDestination, 'foo_destination']);
@ -157,19 +306,14 @@ void main() {
});
test('running with correct destination', () async {
final Directory pluginDirectory =
createFakePlugin('plugin', packagesDir, extraFiles: <String>[
'example/test',
], platformSupport: <String, PlatformSupport>{
final Directory pluginDirectory = createFakePlugin(
'plugin', packagesDir, platformSupport: <String, PlatformSupport>{
kPlatformIos: PlatformSupport.inline
});
final Directory pluginExampleDirectory =
pluginDirectory.childDirectory('example');
processRunner.processToReturn = MockProcess.succeeding();
processRunner.resultStdout =
'{"project":{"targets":["bar_scheme", "foo_scheme"]}}';
final List<String> output = await runCapturingPrint(runner, <String>[
'xctest',
'--ios',
@ -192,13 +336,12 @@ void main() {
const <String>[
'xcodebuild',
'test',
'analyze',
'-workspace',
'ios/Runner.xcworkspace',
'-configuration',
'Debug',
'-scheme',
'Runner',
'-configuration',
'Debug',
'-destination',
'foo_destination',
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
@ -209,45 +352,43 @@ void main() {
test('Not specifying --ios-destination assigns an available simulator',
() async {
final Directory pluginDirectory =
createFakePlugin('plugin', packagesDir, extraFiles: <String>[
'example/test',
], platformSupport: <String, PlatformSupport>{
final Directory pluginDirectory = createFakePlugin(
'plugin', packagesDir, platformSupport: <String, PlatformSupport>{
kPlatformIos: PlatformSupport.inline
});
final Directory pluginExampleDirectory =
pluginDirectory.childDirectory('example');
final Map<String, dynamic> schemeCommandResult = <String, dynamic>{
'project': <String, dynamic>{
'targets': <String>['bar_scheme', 'foo_scheme']
}
};
processRunner.processToReturn = MockProcess.succeeding();
// For simplicity of the test, we combine all the mock results into a single mock result, each internal command
// will get this result and they should still be able to parse them correctly.
processRunner.resultStdout =
jsonEncode(schemeCommandResult..addAll(_kDeviceListMap));
processRunner.resultStdout = jsonEncode(_kDeviceListMap);
await runCapturingPrint(runner, <String>['xctest', '--ios']);
expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
const ProcessCall(
'xcrun', <String>['simctl', 'list', '--json'], null),
'xcrun',
<String>[
'simctl',
'list',
'devices',
'runtimes',
'available',
'--json',
],
null),
ProcessCall(
'xcrun',
const <String>[
'xcodebuild',
'test',
'analyze',
'-workspace',
'ios/Runner.xcworkspace',
'-configuration',
'Debug',
'-scheme',
'Runner',
'-configuration',
'Debug',
'-destination',
'id=1E76A0FD-38AC-4537-A989-EA639D7D012A',
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
@ -257,15 +398,11 @@ void main() {
});
test('fails if xcrun fails', () async {
createFakePlugin('plugin', packagesDir, extraFiles: <String>[
'example/test',
], platformSupport: <String, PlatformSupport>{
kPlatformIos: PlatformSupport.inline
});
createFakePlugin('plugin', packagesDir,
platformSupport: <String, PlatformSupport>{
kPlatformIos: PlatformSupport.inline
});
processRunner.processToReturn = MockProcess.succeeding();
processRunner.resultStdout =
'{"project":{"targets":["bar_scheme", "foo_scheme"]}}';
processRunner.mockProcessesForExecutable['xcrun'] = <io.Process>[
MockProcess.failing()
];
@ -288,7 +425,7 @@ void main() {
expect(
output,
containsAllInOrder(<Matcher>[
contains('The following packages are failing XCTests:'),
contains('The following packages had errors:'),
contains(' plugin'),
]));
});
@ -296,16 +433,10 @@ void main() {
group('macOS', () {
test('skip if macOS is not supported', () async {
createFakePlugin(
'plugin',
packagesDir,
extraFiles: <String>[
'example/test',
],
);
createFakePlugin('plugin', packagesDir);
final List<String> output = await runCapturingPrint(runner,
<String>['xctest', '--macos', _kDestination, 'foo_destination']);
final List<String> output =
await runCapturingPrint(runner, <String>['xctest', '--macos']);
expect(
output,
contains(
@ -314,14 +445,13 @@ void main() {
});
test('skip if macOS is implemented in a federated package', () async {
createFakePlugin('plugin', packagesDir, extraFiles: <String>[
'example/test',
], platformSupport: <String, PlatformSupport>{
kPlatformMacos: PlatformSupport.federated,
});
createFakePlugin('plugin', packagesDir,
platformSupport: <String, PlatformSupport>{
kPlatformMacos: PlatformSupport.federated,
});
final List<String> output = await runCapturingPrint(runner,
<String>['xctest', '--macos', _kDestination, 'foo_destination']);
final List<String> output =
await runCapturingPrint(runner, <String>['xctest', '--macos']);
expect(
output,
contains(
@ -330,19 +460,15 @@ void main() {
});
test('runs for macOS plugin', () async {
final Directory pluginDirectory1 =
createFakePlugin('plugin', packagesDir, extraFiles: <String>[
'example/test',
], platformSupport: <String, PlatformSupport>{
kPlatformMacos: PlatformSupport.inline,
});
final Directory pluginDirectory1 = createFakePlugin(
'plugin', packagesDir,
platformSupport: <String, PlatformSupport>{
kPlatformMacos: PlatformSupport.inline,
});
final Directory pluginExampleDirectory =
pluginDirectory1.childDirectory('example');
processRunner.processToReturn = MockProcess.succeeding();
processRunner.resultStdout =
'{"project":{"targets":["bar_scheme", "foo_scheme"]}}';
final List<String> output = await runCapturingPrint(runner, <String>[
'xctest',
'--macos',
@ -361,13 +487,12 @@ void main() {
const <String>[
'xcodebuild',
'test',
'analyze',
'-workspace',
'macos/Runner.xcworkspace',
'-configuration',
'Debug',
'-scheme',
'Runner',
'-configuration',
'Debug',
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
],
pluginExampleDirectory.path),
@ -375,15 +500,11 @@ void main() {
});
test('fails if xcrun fails', () async {
createFakePlugin('plugin', packagesDir, extraFiles: <String>[
'example/test',
], platformSupport: <String, PlatformSupport>{
kPlatformMacos: PlatformSupport.inline,
});
createFakePlugin('plugin', packagesDir,
platformSupport: <String, PlatformSupport>{
kPlatformMacos: PlatformSupport.inline,
});
processRunner.processToReturn = MockProcess.succeeding();
processRunner.resultStdout =
'{"project":{"targets":["bar_scheme", "foo_scheme"]}}';
processRunner.mockProcessesForExecutable['xcrun'] = <io.Process>[
MockProcess.failing()
];
@ -398,7 +519,7 @@ void main() {
expect(
output,
containsAllInOrder(<Matcher>[
contains('The following packages are failing XCTests:'),
contains('The following packages had errors:'),
contains(' plugin'),
]),
);
@ -407,20 +528,16 @@ void main() {
group('combined', () {
test('runs both iOS and macOS when supported', () async {
final Directory pluginDirectory1 =
createFakePlugin('plugin', packagesDir, extraFiles: <String>[
'example/test',
], platformSupport: <String, PlatformSupport>{
kPlatformIos: PlatformSupport.inline,
kPlatformMacos: PlatformSupport.inline,
});
final Directory pluginDirectory1 = createFakePlugin(
'plugin', packagesDir,
platformSupport: <String, PlatformSupport>{
kPlatformIos: PlatformSupport.inline,
kPlatformMacos: PlatformSupport.inline,
});
final Directory pluginExampleDirectory =
pluginDirectory1.childDirectory('example');
processRunner.processToReturn = MockProcess.succeeding();
processRunner.resultStdout =
'{"project":{"targets":["bar_scheme", "foo_scheme"]}}';
final List<String> output = await runCapturingPrint(runner, <String>[
'xctest',
'--ios',
@ -444,13 +561,12 @@ void main() {
const <String>[
'xcodebuild',
'test',
'analyze',
'-workspace',
'ios/Runner.xcworkspace',
'-configuration',
'Debug',
'-scheme',
'Runner',
'-configuration',
'Debug',
'-destination',
'foo_destination',
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
@ -461,13 +577,12 @@ void main() {
const <String>[
'xcodebuild',
'test',
'analyze',
'-workspace',
'macos/Runner.xcworkspace',
'-configuration',
'Debug',
'-scheme',
'Runner',
'-configuration',
'Debug',
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
],
pluginExampleDirectory.path),
@ -475,19 +590,15 @@ void main() {
});
test('runs only macOS for a macOS plugin', () async {
final Directory pluginDirectory1 =
createFakePlugin('plugin', packagesDir, extraFiles: <String>[
'example/test',
], platformSupport: <String, PlatformSupport>{
kPlatformMacos: PlatformSupport.inline,
});
final Directory pluginDirectory1 = createFakePlugin(
'plugin', packagesDir,
platformSupport: <String, PlatformSupport>{
kPlatformMacos: PlatformSupport.inline,
});
final Directory pluginExampleDirectory =
pluginDirectory1.childDirectory('example');
processRunner.processToReturn = MockProcess.succeeding();
processRunner.resultStdout =
'{"project":{"targets":["bar_scheme", "foo_scheme"]}}';
final List<String> output = await runCapturingPrint(runner, <String>[
'xctest',
'--ios',
@ -511,13 +622,12 @@ void main() {
const <String>[
'xcodebuild',
'test',
'analyze',
'-workspace',
'macos/Runner.xcworkspace',
'-configuration',
'Debug',
'-scheme',
'Runner',
'-configuration',
'Debug',
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
],
pluginExampleDirectory.path),
@ -525,19 +635,14 @@ void main() {
});
test('runs only iOS for a iOS plugin', () async {
final Directory pluginDirectory =
createFakePlugin('plugin', packagesDir, extraFiles: <String>[
'example/test',
], platformSupport: <String, PlatformSupport>{
final Directory pluginDirectory = createFakePlugin(
'plugin', packagesDir, platformSupport: <String, PlatformSupport>{
kPlatformIos: PlatformSupport.inline
});
final Directory pluginExampleDirectory =
pluginDirectory.childDirectory('example');
processRunner.processToReturn = MockProcess.succeeding();
processRunner.resultStdout =
'{"project":{"targets":["bar_scheme", "foo_scheme"]}}';
final List<String> output = await runCapturingPrint(runner, <String>[
'xctest',
'--ios',
@ -561,13 +666,12 @@ void main() {
const <String>[
'xcodebuild',
'test',
'analyze',
'-workspace',
'ios/Runner.xcworkspace',
'-configuration',
'Debug',
'-scheme',
'Runner',
'-configuration',
'Debug',
'-destination',
'foo_destination',
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
@ -577,13 +681,8 @@ void main() {
});
test('skips when neither are supported', () async {
createFakePlugin('plugin', packagesDir, extraFiles: <String>[
'example/test',
]);
createFakePlugin('plugin', packagesDir);
processRunner.processToReturn = MockProcess.succeeding();
processRunner.resultStdout =
'{"project":{"targets":["bar_scheme", "foo_scheme"]}}';
final List<String> output = await runCapturingPrint(runner, <String>[
'xctest',
'--ios',