Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Prev Previous commit
Next Next commit
Migrate xctest to the Xcode class
  • Loading branch information
stuartmorgan-g committed Jul 15, 2021
commit aba3199b0bfcb514992e14610dab25d2896ba065
117 changes: 24 additions & 93 deletions script/tool/lib/src/xctest_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,20 @@
// 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';

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 _iosDestinationFlag = 'ios-destination';
const String _analyzeFlag = 'analyze';
const String _testTargetFlag = 'test-target';

const String _xcodeBuildCommand = 'xcodebuild';
const String _xcRunCommand = 'xcrun';

const int _exitFindingSimulatorsFailed = 3;
const int _exitNoSimulators = 4;
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,
Expand All @@ -34,7 +28,8 @@ class XCTestCommand extends PackageLoopingCommand {
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(
_iosDestinationFlag,
help:
Expand All @@ -56,6 +51,8 @@ class XCTestCommand extends PackageLoopingCommand {
// The device destination flags for iOS tests.
List<String> _iosDestinationFlags = <String>[];

final Xcode _xcode;

@override
final String name = 'xctest';

Expand All @@ -80,7 +77,8 @@ class XCTestCommand extends PackageLoopingCommand {
if (shouldTestIos) {
String destination = getStringArg(_iosDestinationFlag);
if (destination.isEmpty) {
final String? simulatorId = await _findAvailableIphoneSimulator();
final String? simulatorId =
await _xcode.findBestAvailableIphoneSimulator();
if (simulatorId == null) {
printError('Cannot find any available simulators, tests failed');
throw ToolExit(_exitNoSimulators);
Expand Down Expand Up @@ -200,90 +198,23 @@ class XCTestCommand extends PackageLoopingCommand {
required bool analyze,
List<String> extraFlags = const <String>[],
}) {
assert(runTests || analyze);
final String testTarget = getStringArg(_testTargetFlag);
final List<String> xctestArgs = <String>[
_xcodeBuildCommand,
if (runTests) 'test',
if (analyze) 'analyze',
'-workspace',
'${platform.toLowerCase()}/Runner.xcworkspace',
'-configuration',
'Debug',
'-scheme',
'Runner',
if (runTests && testTarget.isNotEmpty) '-only-testing:$testTarget',
...extraFlags,
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
];
final String completeTestCommand = '$_xcRunCommand ${xctestArgs.join(' ')}';
print(completeTestCommand);
return processRunner.runAndStream(_xcRunCommand, 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 =
'$_xcRunCommand ${findSimulatorsArguments.join(' ')}';
print('Looking for available simulators...');
print(findSimulatorCompleteCommand);
final io.ProcessResult findSimulatorsResult =
await processRunner.run(_xcRunCommand, 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) {
continue;
}
id = device['udid'] as String?;
if (id == null) {
continue;
}
print('device selected: $device');
return id;
}
}
return null;
return _xcode.runXcodeBuild(
example,
actions: <String>[
if (runTests) 'test',
if (analyze) 'analyze',
],
workspace: '${platform.toLowerCase()}/Runner.xcworkspace',
scheme: 'Runner',
configuration: 'Debug',
extraFlags: <String>[
if (runTests && testTarget.isNotEmpty) '-only-testing:$testTarget',
...extraFlags,
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
],
);
}
}
87 changes: 26 additions & 61 deletions script/tool/test/xctest_command_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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'
},
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This testing of finding the best device among many is now part of xcode_test.dart, so what's left here is just one valid device for the test to find.

<String, dynamic>{
'bundlePath':
'/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime',
Expand All @@ -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',
Expand All @@ -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';

Expand Down Expand Up @@ -159,10 +124,10 @@ void main() {
'analyze',
'-workspace',
'macos/Runner.xcworkspace',
'-configuration',
'Debug',
'-scheme',
'Runner',
'-configuration',
'Debug',
'-only-testing:RunnerTests',
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
],
Expand Down Expand Up @@ -205,10 +170,10 @@ void main() {
'test',
'-workspace',
'macos/Runner.xcworkspace',
'-configuration',
'Debug',
'-scheme',
'Runner',
'-configuration',
'Debug',
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
],
pluginExampleDirectory.path),
Expand Down Expand Up @@ -252,10 +217,10 @@ void main() {
'test',
'-workspace',
'macos/Runner.xcworkspace',
'-configuration',
'Debug',
'-scheme',
'Runner',
'-configuration',
'Debug',
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
],
pluginExampleDirectory.path),
Expand Down Expand Up @@ -305,10 +270,10 @@ void main() {
'analyze',
'-workspace',
'macos/Runner.xcworkspace',
'-configuration',
'Debug',
'-scheme',
'Runner',
'-configuration',
'Debug',
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
],
pluginExampleDirectory.path),
Expand All @@ -319,10 +284,10 @@ void main() {
'analyze',
'-workspace',
'macos/Runner.xcworkspace',
'-configuration',
'Debug',
'-scheme',
'Runner',
'-configuration',
'Debug',
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
],
pluginExampleDirectory.path),
Expand Down Expand Up @@ -401,10 +366,10 @@ void main() {
'analyze',
'-workspace',
'ios/Runner.xcworkspace',
'-configuration',
'Debug',
'-scheme',
'Runner',
'-configuration',
'Debug',
'-destination',
'foo_destination',
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
Expand Down Expand Up @@ -450,10 +415,10 @@ void main() {
'analyze',
'-workspace',
'ios/Runner.xcworkspace',
'-configuration',
'Debug',
'-scheme',
'Runner',
'-configuration',
'Debug',
'-destination',
'id=1E76A0FD-38AC-4537-A989-EA639D7D012A',
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
Expand Down Expand Up @@ -570,10 +535,10 @@ void main() {
'analyze',
'-workspace',
'macos/Runner.xcworkspace',
'-configuration',
'Debug',
'-scheme',
'Runner',
'-configuration',
'Debug',
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
],
pluginExampleDirectory.path),
Expand Down Expand Up @@ -653,10 +618,10 @@ void main() {
'analyze',
'-workspace',
'ios/Runner.xcworkspace',
'-configuration',
'Debug',
'-scheme',
'Runner',
'-configuration',
'Debug',
'-destination',
'foo_destination',
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
Expand All @@ -670,10 +635,10 @@ void main() {
'analyze',
'-workspace',
'macos/Runner.xcworkspace',
'-configuration',
'Debug',
'-scheme',
'Runner',
'-configuration',
'Debug',
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
],
pluginExampleDirectory.path),
Expand Down Expand Up @@ -720,10 +685,10 @@ void main() {
'analyze',
'-workspace',
'macos/Runner.xcworkspace',
'-configuration',
'Debug',
'-scheme',
'Runner',
'-configuration',
'Debug',
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
],
pluginExampleDirectory.path),
Expand Down Expand Up @@ -770,10 +735,10 @@ void main() {
'analyze',
'-workspace',
'ios/Runner.xcworkspace',
'-configuration',
'Debug',
'-scheme',
'Runner',
'-configuration',
'Debug',
'-destination',
'foo_destination',
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
Expand Down