forked from flutter/plugins
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[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
- Loading branch information
1 parent
4773739
commit b8b8680
Showing
9 changed files
with
1,444 additions
and
296 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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')) { | ||
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; | ||
} | ||
} |
Oops, something went wrong.