Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

[flutter_plugin_tests] Split analyze out of xctest #4161

Merged
merged 10 commits into from
Jul 21, 2021
4 changes: 4 additions & 0 deletions .cirrus.yml
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ task:
- xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-11 com.apple.CoreSimulator.SimRuntime.iOS-14-5 | xargs xcrun simctl boot
build_script:
- ./script/tool_runner.sh build-examples --ios
xcode_analyze_script:
- ./script/tool_runner.sh xcode-analyze --ios
xctest_script:
- ./script/tool_runner.sh xctest --ios --ios-destination "platform=iOS Simulator,name=iPhone 11,OS=latest"
drive_script:
Expand Down Expand Up @@ -249,6 +251,8 @@ task:
build_script:
- flutter config --enable-macos-desktop
- ./script/tool_runner.sh build-examples --macos
xcode_analyze_script:
- ./script/tool_runner.sh xcode-analyze --macos
xctest_script:
- ./script/tool_runner.sh xctest --macos --exclude $PLUGINS_TO_EXCLUDE_MACOS_XCTESTS
drive_script:
Expand Down
3 changes: 3 additions & 0 deletions script/tool/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
## NEXT

- Improved `license-check` output.
- 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.0

Expand Down
130 changes: 130 additions & 0 deletions script/tool/lib/src/common/xcode.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// 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> xctestArgs = <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 ${xctestArgs.join(' ')}';
if (log) {
print(completeTestCommand);
}
return processRunner.runAndStream(_xcRunCommand, xctestArgs,
workingDir: directory);
}

/// 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',
'--json'
];
Copy link
Member

Choose a reason for hiding this comment

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

I know you just copied this, but if you add devices available

Suggested change
final List<String> findSimulatorsArguments = <String>[
'simctl',
'list',
'--json'
];
final List<String> findSimulatorsArguments = <String>[
'simctl',
'list',
'devices',
'available',
'--json'
];

The availabilityError/isAvailable check below can be removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, done! (I had to add 'runtimes' as well since it's parsing those too.)

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>();
if (device['availabilityError'] != null ||
(device['isAvailable'] as bool?) == false) {
continue;
}
id = device['udid'] as String?;
if (id == null) {
continue;
}
if (log) {
print('device selected: $device');
}
return id;
}
}
return null;
}
}
2 changes: 2 additions & 0 deletions script/tool/lib/src/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
111 changes: 111 additions & 0 deletions script/tool/lib/src/xcode_analyze_command.dart
Original file line number Diff line number Diff line change
@@ -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')) {
Copy link
Member

Choose a reason for hiding this comment

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

You don't need to worry about this now, but when CI starts using Xcode 13 next year we may need a macOS -destination option.
flutter/flutter#86590

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;
}
}
Loading