Skip to content

Commit f8f4f92

Browse files
stuartmorgan-gamantoux
authored andcommitted
[flutter_plugin_tests] Split analyze out of xctest (flutter#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 flutter/flutter#84392 and flutter/flutter#86489
1 parent 8c3d95f commit f8f4f92

9 files changed

+1444
-296
lines changed

.cirrus.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,8 @@ task:
221221
- xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-11 com.apple.CoreSimulator.SimRuntime.iOS-14-5 | xargs xcrun simctl boot
222222
build_script:
223223
- ./script/tool_runner.sh build-examples --ios
224+
xcode_analyze_script:
225+
- ./script/tool_runner.sh xcode-analyze --ios
224226
xctest_script:
225227
- ./script/tool_runner.sh xctest --ios --ios-destination "platform=iOS Simulator,name=iPhone 11,OS=latest"
226228
drive_script:
@@ -249,6 +251,8 @@ task:
249251
build_script:
250252
- flutter config --enable-macos-desktop
251253
- ./script/tool_runner.sh build-examples --macos
254+
xcode_analyze_script:
255+
- ./script/tool_runner.sh xcode-analyze --macos
252256
xctest_script:
253257
- ./script/tool_runner.sh xctest --macos --exclude $PLUGINS_TO_EXCLUDE_MACOS_XCTESTS
254258
drive_script:

script/tool/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## NEXT
2+
3+
- Added an `xctest` flag to select specific test targets, to allow running only
4+
unit tests or integration tests.
5+
- Split Xcode analysis out of `xctest` and into a new `xcode-analyze` command.
6+
17
## 0.4.1
28

39
- Improved `license-check` output.

script/tool/lib/src/common/xcode.dart

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:convert';
6+
import 'dart:io' as io;
7+
8+
import 'package:file/file.dart';
9+
10+
import 'core.dart';
11+
import 'process_runner.dart';
12+
13+
const String _xcodeBuildCommand = 'xcodebuild';
14+
const String _xcRunCommand = 'xcrun';
15+
16+
/// A utility class for interacting with the installed version of Xcode.
17+
class Xcode {
18+
/// Creates an instance that runs commends with the given [processRunner].
19+
///
20+
/// If [log] is true, commands run by this instance will long various status
21+
/// messages.
22+
Xcode({
23+
this.processRunner = const ProcessRunner(),
24+
this.log = false,
25+
});
26+
27+
/// The [ProcessRunner] used to run commands. Overridable for testing.
28+
final ProcessRunner processRunner;
29+
30+
/// Whether or not to log when running commands.
31+
final bool log;
32+
33+
/// Runs an `xcodebuild` in [directory] with the given parameters.
34+
Future<int> runXcodeBuild(
35+
Directory directory, {
36+
List<String> actions = const <String>['build'],
37+
required String workspace,
38+
required String scheme,
39+
String? configuration,
40+
List<String> extraFlags = const <String>[],
41+
}) {
42+
final List<String> args = <String>[
43+
_xcodeBuildCommand,
44+
...actions,
45+
if (workspace != null) ...<String>['-workspace', workspace],
46+
if (scheme != null) ...<String>['-scheme', scheme],
47+
if (configuration != null) ...<String>['-configuration', configuration],
48+
...extraFlags,
49+
];
50+
final String completeTestCommand = '$_xcRunCommand ${args.join(' ')}';
51+
if (log) {
52+
print(completeTestCommand);
53+
}
54+
return processRunner.runAndStream(_xcRunCommand, args,
55+
workingDir: directory);
56+
}
57+
58+
/// Returns true if [project], which should be an .xcodeproj directory,
59+
/// contains a target called [target], false if it does not, and null if the
60+
/// check fails (e.g., if [project] is not an Xcode project).
61+
Future<bool?> projectHasTarget(Directory project, String target) async {
62+
final io.ProcessResult result =
63+
await processRunner.run(_xcRunCommand, <String>[
64+
_xcodeBuildCommand,
65+
'-list',
66+
'-json',
67+
'-project',
68+
project.path,
69+
]);
70+
if (result.exitCode != 0) {
71+
return null;
72+
}
73+
Map<String, dynamic>? projectInfo;
74+
try {
75+
projectInfo = (jsonDecode(result.stdout as String)
76+
as Map<String, dynamic>)['project'] as Map<String, dynamic>?;
77+
} on FormatException {
78+
return null;
79+
}
80+
if (projectInfo == null) {
81+
return null;
82+
}
83+
final List<String>? targets =
84+
(projectInfo['targets'] as List<dynamic>?)?.cast<String>();
85+
return targets?.contains(target) ?? false;
86+
}
87+
88+
/// Returns the newest available simulator (highest OS version, with ties
89+
/// broken in favor of newest device), if any.
90+
Future<String?> findBestAvailableIphoneSimulator() async {
91+
final List<String> findSimulatorsArguments = <String>[
92+
'simctl',
93+
'list',
94+
'devices',
95+
'runtimes',
96+
'available',
97+
'--json',
98+
];
99+
final String findSimulatorCompleteCommand =
100+
'$_xcRunCommand ${findSimulatorsArguments.join(' ')}';
101+
if (log) {
102+
print('Looking for available simulators...');
103+
print(findSimulatorCompleteCommand);
104+
}
105+
final io.ProcessResult findSimulatorsResult =
106+
await processRunner.run(_xcRunCommand, findSimulatorsArguments);
107+
if (findSimulatorsResult.exitCode != 0) {
108+
if (log) {
109+
printError(
110+
'Error occurred while running "$findSimulatorCompleteCommand":\n'
111+
'${findSimulatorsResult.stderr}');
112+
}
113+
return null;
114+
}
115+
final Map<String, dynamic> simulatorListJson =
116+
jsonDecode(findSimulatorsResult.stdout as String)
117+
as Map<String, dynamic>;
118+
final List<Map<String, dynamic>> runtimes =
119+
(simulatorListJson['runtimes'] as List<dynamic>)
120+
.cast<Map<String, dynamic>>();
121+
final Map<String, Object> devices =
122+
(simulatorListJson['devices'] as Map<String, dynamic>)
123+
.cast<String, Object>();
124+
if (runtimes.isEmpty || devices.isEmpty) {
125+
return null;
126+
}
127+
String? id;
128+
// Looking for runtimes, trying to find one with highest OS version.
129+
for (final Map<String, dynamic> rawRuntimeMap in runtimes.reversed) {
130+
final Map<String, Object> runtimeMap =
131+
rawRuntimeMap.cast<String, Object>();
132+
if ((runtimeMap['name'] as String?)?.contains('iOS') != true) {
133+
continue;
134+
}
135+
final String? runtimeID = runtimeMap['identifier'] as String?;
136+
if (runtimeID == null) {
137+
continue;
138+
}
139+
final List<Map<String, dynamic>>? devicesForRuntime =
140+
(devices[runtimeID] as List<dynamic>?)?.cast<Map<String, dynamic>>();
141+
if (devicesForRuntime == null || devicesForRuntime.isEmpty) {
142+
continue;
143+
}
144+
// Looking for runtimes, trying to find latest version of device.
145+
for (final Map<String, dynamic> rawDevice in devicesForRuntime.reversed) {
146+
final Map<String, Object> device = rawDevice.cast<String, Object>();
147+
id = device['udid'] as String?;
148+
if (id == null) {
149+
continue;
150+
}
151+
if (log) {
152+
print('device selected: $device');
153+
}
154+
return id;
155+
}
156+
}
157+
return null;
158+
}
159+
}

script/tool/lib/src/main.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import 'publish_plugin_command.dart';
2424
import 'pubspec_check_command.dart';
2525
import 'test_command.dart';
2626
import 'version_check_command.dart';
27+
import 'xcode_analyze_command.dart';
2728
import 'xctest_command.dart';
2829

2930
void main(List<String> args) {
@@ -59,6 +60,7 @@ void main(List<String> args) {
5960
..addCommand(PubspecCheckCommand(packagesDir))
6061
..addCommand(TestCommand(packagesDir))
6162
..addCommand(VersionCheckCommand(packagesDir))
63+
..addCommand(XcodeAnalyzeCommand(packagesDir))
6264
..addCommand(XCTestCommand(packagesDir));
6365

6466
commandRunner.run(args).catchError((Object e) {
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:file/file.dart';
6+
import 'package:platform/platform.dart';
7+
8+
import 'common/core.dart';
9+
import 'common/package_looping_command.dart';
10+
import 'common/plugin_utils.dart';
11+
import 'common/process_runner.dart';
12+
import 'common/xcode.dart';
13+
14+
/// The command to run Xcode's static analyzer on plugins.
15+
class XcodeAnalyzeCommand extends PackageLoopingCommand {
16+
/// Creates an instance of the test command.
17+
XcodeAnalyzeCommand(
18+
Directory packagesDir, {
19+
ProcessRunner processRunner = const ProcessRunner(),
20+
Platform platform = const LocalPlatform(),
21+
}) : _xcode = Xcode(processRunner: processRunner, log: true),
22+
super(packagesDir, processRunner: processRunner, platform: platform) {
23+
argParser.addFlag(kPlatformIos, help: 'Analyze iOS');
24+
argParser.addFlag(kPlatformMacos, help: 'Analyze macOS');
25+
}
26+
27+
final Xcode _xcode;
28+
29+
@override
30+
final String name = 'xcode-analyze';
31+
32+
@override
33+
final String description =
34+
'Runs Xcode analysis on the iOS and/or macOS example apps.';
35+
36+
@override
37+
Future<void> initializeRun() async {
38+
if (!(getBoolArg(kPlatformIos) || getBoolArg(kPlatformMacos))) {
39+
printError('At least one platform flag must be provided.');
40+
throw ToolExit(exitInvalidArguments);
41+
}
42+
}
43+
44+
@override
45+
Future<PackageResult> runForPackage(Directory package) async {
46+
final bool testIos = getBoolArg(kPlatformIos) &&
47+
pluginSupportsPlatform(kPlatformIos, package,
48+
requiredMode: PlatformSupport.inline);
49+
final bool testMacos = getBoolArg(kPlatformMacos) &&
50+
pluginSupportsPlatform(kPlatformMacos, package,
51+
requiredMode: PlatformSupport.inline);
52+
53+
final bool multiplePlatformsRequested =
54+
getBoolArg(kPlatformIos) && getBoolArg(kPlatformMacos);
55+
if (!(testIos || testMacos)) {
56+
return PackageResult.skip('Not implemented for target platform(s).');
57+
}
58+
59+
final List<String> failures = <String>[];
60+
if (testIos &&
61+
!await _analyzePlugin(package, 'iOS', extraFlags: <String>[
62+
'-destination',
63+
'generic/platform=iOS Simulator'
64+
])) {
65+
failures.add('iOS');
66+
}
67+
if (testMacos && !await _analyzePlugin(package, 'macOS')) {
68+
failures.add('macOS');
69+
}
70+
71+
// Only provide the failing platform in the failure details if testing
72+
// multiple platforms, otherwise it's just noise.
73+
return failures.isEmpty
74+
? PackageResult.success()
75+
: PackageResult.fail(
76+
multiplePlatformsRequested ? failures : <String>[]);
77+
}
78+
79+
/// Analyzes [plugin] for [platform], returning true if it passed analysis.
80+
Future<bool> _analyzePlugin(
81+
Directory plugin,
82+
String platform, {
83+
List<String> extraFlags = const <String>[],
84+
}) async {
85+
bool passing = true;
86+
for (final Directory example in getExamplesForPlugin(plugin)) {
87+
// Running tests and static analyzer.
88+
final String examplePath =
89+
getRelativePosixPath(example, from: plugin.parent);
90+
print('Running $platform tests and analyzer for $examplePath...');
91+
final int exitCode = await _xcode.runXcodeBuild(
92+
example,
93+
actions: <String>['analyze'],
94+
workspace: '${platform.toLowerCase()}/Runner.xcworkspace',
95+
scheme: 'Runner',
96+
configuration: 'Debug',
97+
extraFlags: <String>[
98+
...extraFlags,
99+
'GCC_TREAT_WARNINGS_AS_ERRORS=YES',
100+
],
101+
);
102+
if (exitCode == 0) {
103+
printSuccess('$examplePath ($platform) passed analysis.');
104+
} else {
105+
printError('$examplePath ($platform) failed analysis.');
106+
passing = false;
107+
}
108+
}
109+
return passing;
110+
}
111+
}

0 commit comments

Comments
 (0)