diff --git a/.cirrus.yml b/.cirrus.yml index 8f69bd188c06..bf5675b6e3ae 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -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: @@ -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: diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 1e447721d13f..377e7860bd26 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,9 @@ +## NEXT + +- 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.1 - Improved `license-check` output. diff --git a/script/tool/lib/src/common/xcode.dart b/script/tool/lib/src/common/xcode.dart new file mode 100644 index 000000000000..d6bbae419eda --- /dev/null +++ b/script/tool/lib/src/common/xcode.dart @@ -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 runXcodeBuild( + Directory directory, { + List actions = const ['build'], + required String workspace, + required String scheme, + String? configuration, + List extraFlags = const [], + }) { + final List args = [ + _xcodeBuildCommand, + ...actions, + if (workspace != null) ...['-workspace', workspace], + if (scheme != null) ...['-scheme', scheme], + if (configuration != null) ...['-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 projectHasTarget(Directory project, String target) async { + final io.ProcessResult result = + await processRunner.run(_xcRunCommand, [ + _xcodeBuildCommand, + '-list', + '-json', + '-project', + project.path, + ]); + if (result.exitCode != 0) { + return null; + } + Map? projectInfo; + try { + projectInfo = (jsonDecode(result.stdout as String) + as Map)['project'] as Map?; + } on FormatException { + return null; + } + if (projectInfo == null) { + return null; + } + final List? targets = + (projectInfo['targets'] as List?)?.cast(); + return targets?.contains(target) ?? false; + } + + /// Returns the newest available simulator (highest OS version, with ties + /// broken in favor of newest device), if any. + Future findBestAvailableIphoneSimulator() async { + final List findSimulatorsArguments = [ + '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 simulatorListJson = + jsonDecode(findSimulatorsResult.stdout as String) + as Map; + final List> runtimes = + (simulatorListJson['runtimes'] as List) + .cast>(); + final Map devices = + (simulatorListJson['devices'] as Map) + .cast(); + if (runtimes.isEmpty || devices.isEmpty) { + return null; + } + String? id; + // Looking for runtimes, trying to find one with highest OS version. + for (final Map rawRuntimeMap in runtimes.reversed) { + final Map runtimeMap = + rawRuntimeMap.cast(); + if ((runtimeMap['name'] as String?)?.contains('iOS') != true) { + continue; + } + final String? runtimeID = runtimeMap['identifier'] as String?; + if (runtimeID == null) { + continue; + } + final List>? devicesForRuntime = + (devices[runtimeID] as List?)?.cast>(); + if (devicesForRuntime == null || devicesForRuntime.isEmpty) { + continue; + } + // Looking for runtimes, trying to find latest version of device. + for (final Map rawDevice in devicesForRuntime.reversed) { + final Map device = rawDevice.cast(); + id = device['udid'] as String?; + if (id == null) { + continue; + } + if (log) { + print('device selected: $device'); + } + return id; + } + } + return null; + } +} diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index f397a04aa663..ef1a18ab15b2 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -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 args) { @@ -59,6 +60,7 @@ void main(List args) { ..addCommand(PubspecCheckCommand(packagesDir)) ..addCommand(TestCommand(packagesDir)) ..addCommand(VersionCheckCommand(packagesDir)) + ..addCommand(XcodeAnalyzeCommand(packagesDir)) ..addCommand(XCTestCommand(packagesDir)); commandRunner.run(args).catchError((Object e) { diff --git a/script/tool/lib/src/xcode_analyze_command.dart b/script/tool/lib/src/xcode_analyze_command.dart new file mode 100644 index 000000000000..27cd8c435142 --- /dev/null +++ b/script/tool/lib/src/xcode_analyze_command.dart @@ -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 initializeRun() async { + if (!(getBoolArg(kPlatformIos) || getBoolArg(kPlatformMacos))) { + printError('At least one platform flag must be provided.'); + throw ToolExit(exitInvalidArguments); + } + } + + @override + Future 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 failures = []; + if (testIos && + !await _analyzePlugin(package, 'iOS', extraFlags: [ + '-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 : []); + } + + /// Analyzes [plugin] for [platform], returning true if it passed analysis. + Future _analyzePlugin( + Directory plugin, + String platform, { + List extraFlags = const [], + }) 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: ['analyze'], + workspace: '${platform.toLowerCase()}/Runner.xcworkspace', + scheme: 'Runner', + configuration: 'Debug', + extraFlags: [ + ...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; + } +} diff --git a/script/tool/lib/src/xctest_command.dart b/script/tool/lib/src/xctest_command.dart index 176adad39a09..44fc3a87d540 100644 --- a/script/tool/lib/src/xctest_command.dart +++ b/script/tool/lib/src/xctest_command.dart @@ -2,9 +2,6 @@ // 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'; @@ -12,35 +9,39 @@ 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 _testTargetFlag = 'test-target'; -const String _kiOSDestination = 'ios-destination'; -const String _kXcodeBuildCommand = 'xcodebuild'; -const String _kXCRunCommand = 'xcrun'; -const String _kFoundNoSimulatorsMessage = - 'Cannot find any available simulators, tests failed'; +// The exit code from 'xcodebuild test' when there are no tests. +const int _xcodebuildNoTestExitCode = 66; -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, /// usually at "example/{ios,macos}/Runner.xcworkspace". -/// -/// The static analyzer is also run. class XCTestCommand extends PackageLoopingCommand { /// Creates an instance of the test command. XCTestCommand( 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( - _kiOSDestination, + _iosDestinationFlag, 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( + _testTargetFlag, + help: + 'Limits the tests to a specific target (e.g., RunnerTests or RunnerUITests)', + ); argParser.addFlag(kPlatformIos, help: 'Runs the iOS tests'); argParser.addFlag(kPlatformMacos, help: 'Runs the macOS tests'); } @@ -48,6 +49,8 @@ class XCTestCommand extends PackageLoopingCommand { // The device destination flags for iOS tests. List _iosDestinationFlags = []; + final Xcode _xcode; + @override final String name = 'xctest'; @@ -56,9 +59,6 @@ class XCTestCommand extends PackageLoopingCommand { 'Runs the xctests in the iOS and/or macOS example apps.\n\n' 'This command requires "flutter" and "xcrun" to be in your path.'; - @override - String get failureListHeader => 'The following packages are failing XCTests:'; - @override Future initializeRun() async { final bool shouldTestIos = getBoolArg(kPlatformIos); @@ -70,11 +70,12 @@ class XCTestCommand extends PackageLoopingCommand { } if (shouldTestIos) { - String destination = getStringArg(_kiOSDestination); + String destination = getStringArg(_iosDestinationFlag); if (destination.isEmpty) { - final String? simulatorId = await _findAvailableIphoneSimulator(); + final String? simulatorId = + await _xcode.findBestAvailableIphoneSimulator(); if (simulatorId == null) { - printError(_kFoundNoSimulatorsMessage); + printError('Cannot find any available simulators, tests failed'); throw ToolExit(_exitNoSimulators); } destination = 'id=$simulatorId'; @@ -115,15 +116,26 @@ class XCTestCommand extends PackageLoopingCommand { } final List failures = []; - if (testIos && - !await _testPlugin(package, 'iOS', - extraXcrunFlags: _iosDestinationFlags)) { - failures.add('iOS'); + bool ranTests = false; + if (testIos) { + final RunState result = await _testPlugin(package, 'iOS', + extraXcrunFlags: _iosDestinationFlags); + ranTests |= result != RunState.skipped; + if (result == RunState.failed) { + failures.add('iOS'); + } } - if (testMacos && !await _testPlugin(package, 'macOS')) { - failures.add('macOS'); + if (testMacos) { + final RunState result = await _testPlugin(package, 'macOS'); + ranTests |= result != RunState.skipped; + if (result == RunState.failed) { + failures.add('macOS'); + } } + if (!ranTests) { + return PackageResult.skip('No tests found.'); + } // Only provide the failing platform in the failure details if testing // multiple platforms, otherwise it's just noise. return failures.isEmpty @@ -133,124 +145,67 @@ class XCTestCommand extends PackageLoopingCommand { } /// Runs all applicable tests for [plugin], printing status and returning - /// success if the tests passed. - Future _testPlugin( + /// the test result. + Future _testPlugin( Directory plugin, String platform, { List extraXcrunFlags = const [], }) async { - bool passing = true; + final String testTarget = getStringArg(_testTargetFlag); + + // Assume skipped until at least one test has run. + RunState overallResult = RunState.skipped; 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...'); - int exitCode = - await _runTests(true, example, platform, extraFlags: extraXcrunFlags); - // 66 = there is no test target (this fails fast). Try again with just the analyzer. - if (exitCode == 66) { - print('Tests not found for $examplePath, running analyzer only...'); - exitCode = await _runTests(false, example, platform, - extraFlags: extraXcrunFlags); - } - if (exitCode == 0) { - printSuccess('Successfully ran $platform xctest for $examplePath'); - } else { - passing = false; - } - } - return passing; - } - Future _runTests( - bool runTests, - Directory example, - String platform, { - List extraFlags = const [], - }) { - final List xctestArgs = [ - _kXcodeBuildCommand, - if (runTests) 'test', - 'analyze', - '-workspace', - '${platform.toLowerCase()}/Runner.xcworkspace', - '-configuration', - 'Debug', - '-scheme', - 'Runner', - ...extraFlags, - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ]; - final String completeTestCommand = - '$_kXCRunCommand ${xctestArgs.join(' ')}'; - print(completeTestCommand); - return processRunner.runAndStream(_kXCRunCommand, xctestArgs, - workingDir: example); - } - - Future _findAvailableIphoneSimulator() async { - // Find the first available destination if not specified. - final List findSimulatorsArguments = [ - '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) { - printError( - 'Error occurred while running "$findSimulatorCompleteCommand":\n' - '${findSimulatorsResult.stderr}'); - throw ToolExit(_exitFindingSimulatorsFailed); - } - final Map simulatorListJson = - jsonDecode(findSimulatorsResult.stdout as String) - as Map; - final List> runtimes = - (simulatorListJson['runtimes'] as List) - .cast>(); - final Map devices = - (simulatorListJson['devices'] as Map) - .cast(); - if (runtimes.isEmpty || devices.isEmpty) { - return null; - } - String? id; - // Looking for runtimes, trying to find one with highest OS version. - for (final Map rawRuntimeMap in runtimes.reversed) { - final Map runtimeMap = - rawRuntimeMap.cast(); - if ((runtimeMap['name'] as String?)?.contains('iOS') != true) { - continue; - } - final String? runtimeID = runtimeMap['identifier'] as String?; - if (runtimeID == null) { - continue; - } - final List>? devicesForRuntime = - (devices[runtimeID] as List?)?.cast>(); - if (devicesForRuntime == null || devicesForRuntime.isEmpty) { - continue; - } - // Looking for runtimes, trying to find latest version of device. - for (final Map rawDevice in devicesForRuntime.reversed) { - final Map device = rawDevice.cast(); - if (device['availabilityError'] != null || - (device['isAvailable'] as bool?) == false) { + if (testTarget.isNotEmpty) { + final Directory project = example + .childDirectory(platform.toLowerCase()) + .childDirectory('Runner.xcodeproj'); + final bool? hasTarget = + await _xcode.projectHasTarget(project, testTarget); + if (hasTarget == null) { + printError('Unable to check targets for $examplePath.'); + overallResult = RunState.failed; continue; - } - id = device['udid'] as String?; - if (id == null) { + } else if (!hasTarget) { + print('No "$testTarget" target in $examplePath; skipping.'); continue; } - print('device selected: $device'); - return id; + } + + print('Running $platform tests for $examplePath...'); + final int exitCode = await _xcode.runXcodeBuild( + example, + actions: ['test'], + workspace: '${platform.toLowerCase()}/Runner.xcworkspace', + scheme: 'Runner', + configuration: 'Debug', + extraFlags: [ + if (testTarget.isNotEmpty) '-only-testing:$testTarget', + ...extraXcrunFlags, + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + ); + + switch (exitCode) { + case _xcodebuildNoTestExitCode: + print('No tests found for $examplePath'); + continue; + case 0: + printSuccess('Successfully ran $platform xctest for $examplePath'); + // If this is the first test, assume success until something fails. + if (overallResult == RunState.skipped) { + overallResult = RunState.succeeded; + } + break; + default: + // Any failure means a failure overall. + overallResult = RunState.failed; + break; } } - return null; + return overallResult; } } diff --git a/script/tool/test/common/xcode_test.dart b/script/tool/test/common/xcode_test.dart new file mode 100644 index 000000000000..7e046a2446c2 --- /dev/null +++ b/script/tool/test/common/xcode_test.dart @@ -0,0 +1,396 @@ +// 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 'package:file/local.dart'; +import 'package:flutter_plugin_tools/src/common/xcode.dart'; +import 'package:test/test.dart'; + +import '../mocks.dart'; +import '../util.dart'; + +void main() { + late RecordingProcessRunner processRunner; + late Xcode xcode; + + setUp(() { + processRunner = RecordingProcessRunner(); + xcode = Xcode(processRunner: processRunner); + }); + + group('findBestAvailableIphoneSimulator', () { + test('finds the newest device', () async { + const String expectedDeviceId = '1E76A0FD-38AC-4537-A989-EA639D7D012A'; + // 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 devices = { + 'runtimes': >[ + { + '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' + }, + { + 'bundlePath': + '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime', + 'buildversion': '17L255', + 'runtimeRoot': + '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime/Contents/Resources/RuntimeRoot', + 'identifier': 'com.apple.CoreSimulator.SimRuntime.iOS-13-4', + 'version': '13.4', + 'isAvailable': true, + 'name': 'iOS 13.4' + }, + { + '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': { + 'com.apple.CoreSimulator.SimRuntime.iOS-13-4': >[ + { + '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' + }, + { + 'dataPath': + '/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data', + 'logPath': + '/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'udid': expectedDeviceId, + 'isAvailable': true, + 'deviceTypeIdentifier': + 'com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus', + 'state': 'Shutdown', + 'name': 'iPhone 8 Plus' + } + ] + } + }; + + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = jsonEncode(devices); + + expect(await xcode.findBestAvailableIphoneSimulator(), expectedDeviceId); + }); + + test('ignores non-iOS runtimes', () async { + // 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 devices = { + 'runtimes': >[ + { + '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': { + 'com.apple.CoreSimulator.SimRuntime.watchOS-6-2': + >[ + { + 'dataPath': + '/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data', + 'logPath': + '/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'udid': '1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'isAvailable': true, + 'deviceTypeIdentifier': + 'com.apple.CoreSimulator.SimDeviceType.Apple-Watch-38mm', + 'state': 'Shutdown', + 'name': 'Apple Watch' + } + ] + } + }; + + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = jsonEncode(devices); + + expect(await xcode.findBestAvailableIphoneSimulator(), null); + }); + + test('returns null if simctl fails', () async { + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess.failing(), + ]; + + expect(await xcode.findBestAvailableIphoneSimulator(), null); + }); + }); + + group('runXcodeBuild', () { + test('handles minimal arguments', () async { + final Directory directory = const LocalFileSystem().currentDirectory; + + final int exitCode = await xcode.runXcodeBuild( + directory, + workspace: 'A.xcworkspace', + scheme: 'AScheme', + ); + + expect(exitCode, 0); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'build', + '-workspace', + 'A.xcworkspace', + '-scheme', + 'AScheme', + ], + directory.path), + ])); + }); + + test('handles all arguments', () async { + final Directory directory = const LocalFileSystem().currentDirectory; + + final int exitCode = await xcode.runXcodeBuild(directory, + actions: ['action1', 'action2'], + workspace: 'A.xcworkspace', + scheme: 'AScheme', + configuration: 'Debug', + extraFlags: ['-a', '-b', 'c=d']); + + expect(exitCode, 0); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'action1', + 'action2', + '-workspace', + 'A.xcworkspace', + '-scheme', + 'AScheme', + '-configuration', + 'Debug', + '-a', + '-b', + 'c=d', + ], + directory.path), + ])); + }); + + test('returns error codes', () async { + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess.failing(), + ]; + final Directory directory = const LocalFileSystem().currentDirectory; + + final int exitCode = await xcode.runXcodeBuild( + directory, + workspace: 'A.xcworkspace', + scheme: 'AScheme', + ); + + expect(exitCode, 1); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'build', + '-workspace', + 'A.xcworkspace', + '-scheme', + 'AScheme', + ], + directory.path), + ])); + }); + }); + + group('projectHasTarget', () { + test('returns true when present', () async { + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = ''' +{ + "project" : { + "configurations" : [ + "Debug", + "Release" + ], + "name" : "Runner", + "schemes" : [ + "Runner" + ], + "targets" : [ + "Runner", + "RunnerTests", + "RunnerUITests" + ] + } +}'''; + + final Directory project = + const LocalFileSystem().directory('/foo.xcodeproj'); + expect(await xcode.projectHasTarget(project, 'RunnerTests'), true); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + project.path, + ], + null), + ])); + }); + + test('returns false when not present', () async { + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = ''' +{ + "project" : { + "configurations" : [ + "Debug", + "Release" + ], + "name" : "Runner", + "schemes" : [ + "Runner" + ], + "targets" : [ + "Runner", + "RunnerUITests" + ] + } +}'''; + + final Directory project = + const LocalFileSystem().directory('/foo.xcodeproj'); + expect(await xcode.projectHasTarget(project, 'RunnerTests'), false); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + project.path, + ], + null), + ])); + }); + + test('returns null for unexpected output', () async { + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = '{}'; + + final Directory project = + const LocalFileSystem().directory('/foo.xcodeproj'); + expect(await xcode.projectHasTarget(project, 'RunnerTests'), null); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + project.path, + ], + null), + ])); + }); + + test('returns null for invalid output', () async { + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = ':)'; + + final Directory project = + const LocalFileSystem().directory('/foo.xcodeproj'); + expect(await xcode.projectHasTarget(project, 'RunnerTests'), null); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + project.path, + ], + null), + ])); + }); + + test('returns null for failure', () async { + processRunner.processToReturn = MockProcess.failing(); + + final Directory project = + const LocalFileSystem().directory('/foo.xcodeproj'); + expect(await xcode.projectHasTarget(project, 'RunnerTests'), null); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + project.path, + ], + null), + ])); + }); + }); +} diff --git a/script/tool/test/xcode_analyze_command_test.dart b/script/tool/test/xcode_analyze_command_test.dart new file mode 100644 index 000000000000..b715ac531f50 --- /dev/null +++ b/script/tool/test/xcode_analyze_command_test.dart @@ -0,0 +1,416 @@ +// 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:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; +import 'package:flutter_plugin_tools/src/xcode_analyze_command.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +// TODO(stuartmorgan): Rework these tests to use a mock Xcode instead of +// doing all the process mocking and validation. +void main() { + group('test xcode_analyze_command', () { + late FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + late CommandRunner runner; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(isMacOS: true); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); + final XcodeAnalyzeCommand command = XcodeAnalyzeCommand(packagesDir, + processRunner: processRunner, platform: mockPlatform); + + runner = CommandRunner( + 'xcode_analyze_command', 'Test for xcode_analyze_command'); + runner.addCommand(command); + }); + + test('Fails if no platforms are provided', () async { + Error? commandError; + final List output = await runCapturingPrint( + runner, ['xcode-analyze'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('At least one platform flag must be provided'), + ]), + ); + }); + + group('iOS', () { + test('skip if iOS is not supported', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final List output = + await runCapturingPrint(runner, ['xcode-analyze', '--ios']); + expect(output, + contains(contains('Not implemented for target platform(s).'))); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('skip if iOS is implemented in a federated package', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformIos: PlatformSupport.federated + }); + + final List output = + await runCapturingPrint(runner, ['xcode-analyze', '--ios']); + expect(output, + contains(contains('Not implemented for target platform(s).'))); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('runs for iOS plugin', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', packagesDir, platformSupport: { + kPlatformIos: PlatformSupport.inline + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'xcode-analyze', + '--ios', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('plugin/example (iOS) passed analysis.') + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'analyze', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + '-destination', + 'generic/platform=iOS Simulator', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('fails if xcrun fails', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformIos: PlatformSupport.inline + }); + + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess.failing() + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, + [ + 'xcode-analyze', + '--ios', + ], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains(' plugin'), + ])); + }); + }); + + group('macOS', () { + test('skip if macOS is not supported', () async { + createFakePlugin( + 'plugin', + packagesDir, + ); + + final List output = await runCapturingPrint( + runner, ['xcode-analyze', '--macos']); + expect(output, + contains(contains('Not implemented for target platform(s).'))); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('skip if macOS is implemented in a federated package', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.federated, + }); + + final List output = await runCapturingPrint( + runner, ['xcode-analyze', '--macos']); + expect(output, + contains(contains('Not implemented for target platform(s).'))); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('runs for macOS plugin', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'xcode-analyze', + '--macos', + ]); + + expect(output, + contains(contains('plugin/example (macOS) passed analysis.'))); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'analyze', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('fails if xcrun fails', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess.failing() + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['xcode-analyze', '--macos'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains(' plugin'), + ]), + ); + }); + }); + + group('combined', () { + test('runs both iOS and macOS when supported', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformIos: PlatformSupport.inline, + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'xcode-analyze', + '--ios', + '--macos', + ]); + + expect( + output, + containsAll([ + contains('plugin/example (iOS) passed analysis.'), + contains('plugin/example (macOS) passed analysis.'), + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'analyze', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + '-destination', + 'generic/platform=iOS Simulator', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'analyze', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('runs only macOS for a macOS plugin', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'xcode-analyze', + '--ios', + '--macos', + ]); + + expect( + output, + containsAllInOrder([ + contains('plugin/example (macOS) passed analysis.'), + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'analyze', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('runs only iOS for a iOS plugin', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', packagesDir, platformSupport: { + kPlatformIos: PlatformSupport.inline + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'xcode-analyze', + '--ios', + '--macos', + ]); + + expect( + output, + containsAllInOrder( + [contains('plugin/example (iOS) passed analysis.')])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'analyze', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + '-destination', + 'generic/platform=iOS Simulator', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('skips when neither are supported', () async { + createFakePlugin('plugin', packagesDir); + + final List output = await runCapturingPrint(runner, [ + 'xcode-analyze', + '--ios', + '--macos', + ]); + + expect( + output, + containsAllInOrder([ + contains('SKIPPING: Not implemented for target platform(s).'), + ])); + + expect(processRunner.recordedCalls, orderedEquals([])); + }); + }); + }); +} diff --git a/script/tool/test/xctest_command_test.dart b/script/tool/test/xctest_command_test.dart index aa6d23fb56f5..324dea0e71ef 100644 --- a/script/tool/test/xctest_command_test.dart +++ b/script/tool/test/xctest_command_test.dart @@ -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 _kDeviceListMap = { 'runtimes': >[ - { - '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' - }, { 'bundlePath': '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime', @@ -43,32 +29,9 @@ final Map _kDeviceListMap = { 'isAvailable': true, 'name': 'iOS 13.4' }, - { - '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': { 'com.apple.CoreSimulator.SimRuntime.iOS-13-4': >[ - { - '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' - }, { 'dataPath': '/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data', @@ -85,6 +48,8 @@ final Map _kDeviceListMap = { } }; +// 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'; @@ -123,13 +88,198 @@ void main() { ); }); + test('allows target filtering', () async { + final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = '{"project":{"targets":["RunnerTests"]}}'; + + final List output = await runCapturingPrint(runner, [ + 'xctest', + '--macos', + '--test-target=RunnerTests', + ]); + + expect( + output, + contains( + contains('Successfully ran macOS xctest for plugin/example'))); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + pluginExampleDirectory + .childDirectory('macos') + .childDirectory('Runner.xcodeproj') + .path, + ], + null), + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + '-only-testing:RunnerTests', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('skips when the requested target is not present', () async { + final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + processRunner.processToReturn = MockProcess.succeeding(); + processRunner.resultStdout = '{"project":{"targets":["Runner"]}}'; + final List output = await runCapturingPrint(runner, [ + 'xctest', + '--macos', + '--test-target=RunnerTests', + ]); + + expect( + output, + containsAllInOrder([ + contains('No "RunnerTests" target in plugin/example; skipping.'), + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + pluginExampleDirectory + .childDirectory('macos') + .childDirectory('Runner.xcodeproj') + .path, + ], + null), + ])); + }); + + test('fails if unable to check for requested target', () async { + final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + processRunner.processToReturn = MockProcess.failing(); + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'xctest', + '--macos', + '--test-target=RunnerTests', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Unable to check targets for plugin/example.'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + pluginExampleDirectory + .childDirectory('macos') + .childDirectory('Runner.xcodeproj') + .path, + ], + null), + ])); + }); + + test('reports skips with no tests', () async { + final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + // Exit code 66 from testing indicates no tests. + final MockProcess noTestsProcessResult = MockProcess(); + noTestsProcessResult.exitCodeCompleter.complete(66); + processRunner.mockProcessesForExecutable['xcrun'] = [ + noTestsProcessResult, + ]; + final List output = + await runCapturingPrint(runner, ['xctest', '--macos']); + + expect(output, contains(contains('No tests found.'))); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + group('iOS', () { test('skip if iOS is not supported', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); final List output = await runCapturingPrint(runner, ['xctest', '--ios', _kDestination, 'foo_destination']); @@ -141,11 +291,10 @@ void main() { }); test('skip if iOS is implemented in a federated package', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - kPlatformIos: PlatformSupport.federated - }); + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformIos: PlatformSupport.federated + }); final List output = await runCapturingPrint(runner, ['xctest', '--ios', _kDestination, 'foo_destination']); @@ -157,19 +306,14 @@ void main() { }); test('running with correct destination', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { + final Directory pluginDirectory = createFakePlugin( + 'plugin', packagesDir, platformSupport: { kPlatformIos: PlatformSupport.inline }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; final List output = await runCapturingPrint(runner, [ 'xctest', '--ios', @@ -192,13 +336,12 @@ void main() { const [ 'xcodebuild', 'test', - 'analyze', '-workspace', 'ios/Runner.xcworkspace', - '-configuration', - 'Debug', '-scheme', 'Runner', + '-configuration', + 'Debug', '-destination', 'foo_destination', 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', @@ -209,45 +352,43 @@ void main() { test('Not specifying --ios-destination assigns an available simulator', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { + final Directory pluginDirectory = createFakePlugin( + 'plugin', packagesDir, platformSupport: { kPlatformIos: PlatformSupport.inline }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - final Map schemeCommandResult = { - 'project': { - 'targets': ['bar_scheme', 'foo_scheme'] - } - }; processRunner.processToReturn = MockProcess.succeeding(); - // For simplicity of the test, we combine all the mock results into a single mock result, each internal command - // will get this result and they should still be able to parse them correctly. - processRunner.resultStdout = - jsonEncode(schemeCommandResult..addAll(_kDeviceListMap)); + processRunner.resultStdout = jsonEncode(_kDeviceListMap); await runCapturingPrint(runner, ['xctest', '--ios']); expect( processRunner.recordedCalls, orderedEquals([ const ProcessCall( - 'xcrun', ['simctl', 'list', '--json'], null), + 'xcrun', + [ + 'simctl', + 'list', + 'devices', + 'runtimes', + 'available', + '--json', + ], + null), ProcessCall( 'xcrun', const [ 'xcodebuild', 'test', - 'analyze', '-workspace', 'ios/Runner.xcworkspace', - '-configuration', - 'Debug', '-scheme', 'Runner', + '-configuration', + 'Debug', '-destination', 'id=1E76A0FD-38AC-4537-A989-EA639D7D012A', 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', @@ -257,15 +398,11 @@ void main() { }); test('fails if xcrun fails', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - kPlatformIos: PlatformSupport.inline - }); + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformIos: PlatformSupport.inline + }); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; processRunner.mockProcessesForExecutable['xcrun'] = [ MockProcess.failing() ]; @@ -288,7 +425,7 @@ void main() { expect( output, containsAllInOrder([ - contains('The following packages are failing XCTests:'), + contains('The following packages had errors:'), contains(' plugin'), ])); }); @@ -296,16 +433,10 @@ void main() { group('macOS', () { test('skip if macOS is not supported', () async { - createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test', - ], - ); + createFakePlugin('plugin', packagesDir); - final List output = await runCapturingPrint(runner, - ['xctest', '--macos', _kDestination, 'foo_destination']); + final List output = + await runCapturingPrint(runner, ['xctest', '--macos']); expect( output, contains( @@ -314,14 +445,13 @@ void main() { }); test('skip if macOS is implemented in a federated package', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - kPlatformMacos: PlatformSupport.federated, - }); + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.federated, + }); - final List output = await runCapturingPrint(runner, - ['xctest', '--macos', _kDestination, 'foo_destination']); + final List output = + await runCapturingPrint(runner, ['xctest', '--macos']); expect( output, contains( @@ -330,19 +460,15 @@ void main() { }); test('runs for macOS plugin', () async { - final Directory pluginDirectory1 = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; final List output = await runCapturingPrint(runner, [ 'xctest', '--macos', @@ -361,13 +487,12 @@ void main() { const [ 'xcodebuild', 'test', - 'analyze', '-workspace', 'macos/Runner.xcworkspace', - '-configuration', - 'Debug', '-scheme', 'Runner', + '-configuration', + 'Debug', 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', ], pluginExampleDirectory.path), @@ -375,15 +500,11 @@ void main() { }); test('fails if xcrun fails', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; processRunner.mockProcessesForExecutable['xcrun'] = [ MockProcess.failing() ]; @@ -398,7 +519,7 @@ void main() { expect( output, containsAllInOrder([ - contains('The following packages are failing XCTests:'), + contains('The following packages had errors:'), contains(' plugin'), ]), ); @@ -407,20 +528,16 @@ void main() { group('combined', () { test('runs both iOS and macOS when supported', () async { - final Directory pluginDirectory1 = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - kPlatformIos: PlatformSupport.inline, - kPlatformMacos: PlatformSupport.inline, - }); + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformIos: PlatformSupport.inline, + kPlatformMacos: PlatformSupport.inline, + }); final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; final List output = await runCapturingPrint(runner, [ 'xctest', '--ios', @@ -444,13 +561,12 @@ void main() { const [ 'xcodebuild', 'test', - 'analyze', '-workspace', 'ios/Runner.xcworkspace', - '-configuration', - 'Debug', '-scheme', 'Runner', + '-configuration', + 'Debug', '-destination', 'foo_destination', 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', @@ -461,13 +577,12 @@ void main() { const [ 'xcodebuild', 'test', - 'analyze', '-workspace', 'macos/Runner.xcworkspace', - '-configuration', - 'Debug', '-scheme', 'Runner', + '-configuration', + 'Debug', 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', ], pluginExampleDirectory.path), @@ -475,19 +590,15 @@ void main() { }); test('runs only macOS for a macOS plugin', () async { - final Directory pluginDirectory1 = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { - kPlatformMacos: PlatformSupport.inline, - }); + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }); final Directory pluginExampleDirectory = pluginDirectory1.childDirectory('example'); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; final List output = await runCapturingPrint(runner, [ 'xctest', '--ios', @@ -511,13 +622,12 @@ void main() { const [ 'xcodebuild', 'test', - 'analyze', '-workspace', 'macos/Runner.xcworkspace', - '-configuration', - 'Debug', '-scheme', 'Runner', + '-configuration', + 'Debug', 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', ], pluginExampleDirectory.path), @@ -525,19 +635,14 @@ void main() { }); test('runs only iOS for a iOS plugin', () async { - final Directory pluginDirectory = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ], platformSupport: { + final Directory pluginDirectory = createFakePlugin( + 'plugin', packagesDir, platformSupport: { kPlatformIos: PlatformSupport.inline }); final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; final List output = await runCapturingPrint(runner, [ 'xctest', '--ios', @@ -561,13 +666,12 @@ void main() { const [ 'xcodebuild', 'test', - 'analyze', '-workspace', 'ios/Runner.xcworkspace', - '-configuration', - 'Debug', '-scheme', 'Runner', + '-configuration', + 'Debug', '-destination', 'foo_destination', 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', @@ -577,13 +681,8 @@ void main() { }); test('skips when neither are supported', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test', - ]); + createFakePlugin('plugin', packagesDir); - processRunner.processToReturn = MockProcess.succeeding(); - processRunner.resultStdout = - '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; final List output = await runCapturingPrint(runner, [ 'xctest', '--ios',