blob: d90b7a8fbfea2be21c32b8a24a1c24d442e914ec [file] [log] [blame]
// Copyright 2017 The Chromium 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:async';
import 'dart:convert';
import 'dart:io' as io;
import 'package:file/file.dart';
import 'package:path/path.dart' as p;
import 'common.dart';
const String _kiOSDestination = 'ios-destination';
const String _kTarget = 'target';
const String _kSkip = 'skip';
const String _kXcodeBuildCommand = 'xcodebuild';
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.xcodeproj".
/// The command takes a "-target" argument which has to match the target of the test target.
/// For information on how to add test target in an xcode project, see https://developer.apple.com/library/archive/documentation/ToolsLanguages/Conceptual/Xcode_Overview/UnitTesting.html
class XCTestCommand extends PluginCommand {
XCTestCommand(
Directory packagesDir,
FileSystem fileSystem, {
ProcessRunner processRunner = const ProcessRunner(),
}) : super(packagesDir, fileSystem, processRunner: processRunner) {
argParser.addOption(
_kiOSDestination,
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(_kTarget,
help: 'The test target.\n'
'This is the xcode project test target. This is passed to the `-scheme` argument in the 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 scheme');
argParser.addMultiOption(_kSkip,
help: 'Plugins to skip while running this command. \n');
}
@override
final String name = 'xctest';
@override
final String description = 'Runs the xctests in the iOS example apps.\n\n'
'This command requires "flutter" to be in your path.';
@override
Future<Null> run() async {
if (argResults[_kTarget] == null) {
// TODO(cyanglaz): Automatically find all the available testing schemes if this argument is not specified.
// https://github.com/flutter/flutter/issues/68419
print('--$_kTarget must be specified');
throw ToolExit(1);
}
String destination = argResults[_kiOSDestination];
if (destination == null) {
String simulatorId = await _findAvailableIphoneSimulator();
if (simulatorId == null) {
print(_kFoundNoSimulatorsMessage);
throw ToolExit(1);
}
destination = 'id=$simulatorId';
}
checkSharding();
final String target = argResults[_kTarget];
final List<String> skipped = argResults[_kSkip];
List<String> failingPackages = <String>[];
await for (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, fileSystem)) {
print('iOS is not supported by this plugin.');
print('\n\n');
continue;
}
if (skipped.contains(packageName)) {
print('$packageName was skipped with the --skip flag.');
print('\n\n');
continue;
}
for (Directory example in getExamplesForPlugin(plugin)) {
// Look for the test scheme in the example app.
print('Look for target named: $_kTarget ...');
final List<String> findSchemeArgs = <String>[
'-project',
'ios/Runner.xcodeproj',
'-list',
'-json'
];
final String completeFindSchemeCommand =
'$_kXcodeBuildCommand ${findSchemeArgs.join(' ')}';
print(completeFindSchemeCommand);
final io.ProcessResult xcodeprojListResult = await processRunner
.run(_kXcodeBuildCommand, findSchemeArgs, workingDir: example);
if (xcodeprojListResult.exitCode != 0) {
print('Error occurred while running "$completeFindSchemeCommand":\n'
'${xcodeprojListResult.stderr}');
failingPackages.add(packageName);
print('\n\n');
continue;
}
final String xcodeprojListOutput = xcodeprojListResult.stdout;
Map<String, dynamic> xcodeprojListOutputJson =
jsonDecode(xcodeprojListOutput);
if (!xcodeprojListOutputJson['project']['targets'].contains(target)) {
failingPackages.add(packageName);
print('$target not configured for $packageName, test failed.');
print(
'Please check the scheme for the test target if it matches the name $target.\n'
'If this plugin does not have an XCTest target, use the $_kSkip flag in the $name command to skip the plugin.');
print('\n\n');
continue;
}
// Found the scheme, running tests
print('Running XCTests:$target for $packageName ...');
final List<String> xctestArgs = <String>[
'test',
'-workspace',
'ios/Runner.xcworkspace',
'-scheme',
target,
'-destination',
destination,
'CODE_SIGN_IDENTITY=""',
'CODE_SIGNING_REQUIRED=NO'
];
final String completeTestCommand =
'$_kXcodeBuildCommand ${xctestArgs.join(' ')}';
print(completeTestCommand);
final int exitCode = await processRunner
.runAndStream(_kXcodeBuildCommand, xctestArgs, workingDir: example);
if (exitCode == 0) {
print('Successfully ran xctest for $packageName');
} else {
failingPackages.add(packageName);
}
}
}
// Command end, print reports.
if (failingPackages.isEmpty) {
print("All XCTests have passed!");
} else {
print(
'The following packages are failing XCTests (see above for details):');
for (String package in failingPackages) {
print(' * $package');
}
throw ToolExit(1);
}
}
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) {
print('Error occurred while running "$findSimulatorCompleteCommand":\n'
'${findSimulatorsResult.stderr}');
throw ToolExit(1);
}
final Map<String, dynamic> simulatorListJson =
jsonDecode(findSimulatorsResult.stdout);
final List<dynamic> runtimes = simulatorListJson['runtimes'];
final Map<String, dynamic> devices = simulatorListJson['devices'];
if (runtimes.isEmpty || devices.isEmpty) {
return null;
}
String id;
// Looking for runtimes, trying to find one with highest OS version.
for (Map<String, dynamic> runtimeMap in runtimes.reversed) {
if (!runtimeMap['name'].contains('iOS')) {
continue;
}
final String runtimeID = runtimeMap['identifier'];
final List<dynamic> devicesForRuntime = devices[runtimeID];
if (devicesForRuntime.isEmpty) {
continue;
}
// Looking for runtimes, trying to find latest version of device.
for (Map<String, dynamic> device in devicesForRuntime.reversed) {
if (device['availabilityError'] != null ||
(device['isAvailable'] as bool == false)) {
continue;
}
id = device['udid'];
print('device selected: $device');
return id;
}
}
return null;
}
}