diff --git a/.ci.yaml b/.ci.yaml index 9672d7b07ef26..cf27cb82731e6 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -5481,6 +5481,16 @@ targets: ["devicelab", "hostonly", "mac"] task_name: hot_mode_dev_cycle_ios_simulator + - name: Mac_ios hot_mode_dev_cycle_ios_xcode_debug + recipe: devicelab/devicelab_drone + bringup: true + presubmit: false + timeout: 60 + properties: + tags: > + ["devicelab", "ios", "mac"] + task_name: hot_mode_dev_cycle_ios_xcode_debug + - name: Mac_ios fullscreen_textfield_perf_ios__e2e_summary recipe: devicelab/devicelab_drone presubmit: false diff --git a/TESTOWNERS b/TESTOWNERS index 07827f1e9be41..63beb5f2af2cb 100644 --- a/TESTOWNERS +++ b/TESTOWNERS @@ -267,6 +267,7 @@ /dev/devicelab/bin/tasks/hello_world_macos__compile.dart @cbracken @flutter/desktop /dev/devicelab/bin/tasks/hello_world_win_desktop__compile.dart @yaakovschectman @flutter/desktop /dev/devicelab/bin/tasks/hot_mode_dev_cycle_ios_simulator.dart @louisehsu @flutter/tool +/dev/devicelab/bin/tasks/hot_mode_dev_cycle_ios_xcode_debug.dart @vashworth @flutter/tool /dev/devicelab/bin/tasks/hot_mode_dev_cycle_macos_target__benchmark.dart @cbracken @flutter/tool /dev/devicelab/bin/tasks/hot_mode_dev_cycle_win_target__benchmark.dart @cbracken @flutter/desktop /dev/devicelab/bin/tasks/integration_ui_test_test_macos.dart @cbracken @flutter/desktop diff --git a/bin/internal/engine.version b/bin/internal/engine.version new file mode 100644 index 0000000000000..21fab17782acc --- /dev/null +++ b/bin/internal/engine.version @@ -0,0 +1 @@ +877970cbc98718dbee8f0a34374fa2b1a1db2248 diff --git a/bin/internal/release-candidate-branch.version b/bin/internal/release-candidate-branch.version new file mode 100644 index 0000000000000..ef3df3a8b7408 --- /dev/null +++ b/bin/internal/release-candidate-branch.version @@ -0,0 +1 @@ +flutter-3.36-candidate.0 diff --git a/dev/devicelab/bin/tasks/hot_mode_dev_cycle_ios_xcode_debug.dart b/dev/devicelab/bin/tasks/hot_mode_dev_cycle_ios_xcode_debug.dart new file mode 100644 index 0000000000000..0beaa1d5d8815 --- /dev/null +++ b/dev/devicelab/bin/tasks/hot_mode_dev_cycle_ios_xcode_debug.dart @@ -0,0 +1,45 @@ +// Copyright 2014 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:flutter_devicelab/framework/devices.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/framework/task_result.dart'; +import 'package:flutter_devicelab/framework/utils.dart'; +import 'package:flutter_devicelab/tasks/hot_mode_tests.dart'; +import 'package:path/path.dart' as path; + +/// This is a test to validate that Xcode debugging still works now that LLDB is the default. +Future main() async { + await task(() async { + deviceOperatingSystem = DeviceOperatingSystem.ios; + try { + await disableLLDBDebugging(); + // This isn't actually a benchmark test, so do not use the returned `benchmarkScoreKeys` result. + await createHotModeTest()(); + return TaskResult.success(null); + } finally { + await enableLLDBDebugging(); + } + }); +} + +Future disableLLDBDebugging() async { + final int configResult = await exec(path.join(flutterDirectory.path, 'bin', 'flutter'), [ + 'config', + '--no-enable-lldb-debugging', + ]); + if (configResult != 0) { + print('Failed to disable configuration, tasks may not run.'); + } +} + +Future enableLLDBDebugging() async { + final int configResult = await exec(path.join(flutterDirectory.path, 'bin', 'flutter'), [ + 'config', + '--enable-lldb-debugging', + ], canFail: true); + if (configResult != 0) { + print('Failed to enable configuration.'); + } +} diff --git a/engine/src/flutter/BUILD.gn b/engine/src/flutter/BUILD.gn index 1258d615fd6a4..f5ace7c353167 100644 --- a/engine/src/flutter/BUILD.gn +++ b/engine/src/flutter/BUILD.gn @@ -78,10 +78,12 @@ group("flutter") { if (!is_qnx) { public_deps = [ - ":unittests", "//flutter/shell/platform/embedder:flutter_engine", "//flutter/sky", ] + if (enable_unittests) { + public_deps += [ ":unittests" ] + } } # Ensure the example for a sample embedder compiles. diff --git a/engine/src/flutter/shell/platform/linux/fl_view.cc b/engine/src/flutter/shell/platform/linux/fl_view.cc index 6d7443449a9ba..f770b93e1cb65 100644 --- a/engine/src/flutter/shell/platform/linux/fl_view.cc +++ b/engine/src/flutter/shell/platform/linux/fl_view.cc @@ -99,11 +99,16 @@ G_DEFINE_TYPE_WITH_CODE( G_IMPLEMENT_INTERFACE(fl_plugin_registry_get_type(), fl_view_plugin_registry_iface_init)) -// Emit the first frame signal in the main thread. -static gboolean first_frame_idle_cb(gpointer user_data) { +// Redraw the view from the GTK thread. +static gboolean redraw_cb(gpointer user_data) { FlView* self = FL_VIEW(user_data); - g_signal_emit(self, fl_view_signals[SIGNAL_FIRST_FRAME], 0); + gtk_widget_queue_draw(GTK_WIDGET(self->render_area)); + + if (!self->have_first_frame) { + self->have_first_frame = TRUE; + g_signal_emit(self, fl_view_signals[SIGNAL_FIRST_FRAME], 0); + } return FALSE; } @@ -247,14 +252,8 @@ static void fl_view_present_layers(FlRenderable* renderable, fl_compositor_present_layers(self->compositor, layers, layers_count); - gtk_widget_queue_draw(GTK_WIDGET(self->render_area)); - - if (!self->have_first_frame) { - self->have_first_frame = TRUE; - // This is not the main thread, so the signal needs to be done via an idle - // callback. - g_idle_add(first_frame_idle_cb, self); - } + // Perform the redraw in the GTK thead. + g_idle_add(redraw_cb, self); } // Implements FlPluginRegistry::get_registrar_for_plugin. diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index 19e925a3f03bf..57030638fa05d 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -1198,7 +1198,6 @@ class DebuggingOptions { String? route, Map platformArgs, { DeviceConnectionInterface interfaceType = DeviceConnectionInterface.attached, - bool isCoreDevice = false, }) { return [ if (enableDartProfiling) '--enable-dart-profiling', @@ -1212,13 +1211,7 @@ class DebuggingOptions { if (environmentType == EnvironmentType.simulator && dartFlags.isNotEmpty) '--dart-flags=$dartFlags', if (useTestFonts) '--use-test-fonts', - // Core Devices (iOS 17 devices) are debugged through Xcode so don't - // include these flags, which are used to check if the app was launched - // via Flutter CLI and `ios-deploy`. - if (debuggingEnabled && !isCoreDevice) ...[ - '--enable-checked-mode', - '--verify-entry-points', - ], + if (debuggingEnabled) ...['--enable-checked-mode', '--verify-entry-points'], if (enableSoftwareRendering) '--enable-software-rendering', if (traceSystrace) '--trace-systrace', if (traceToFile != null) '--trace-to-file="$traceToFile"', diff --git a/packages/flutter_tools/lib/src/features.dart b/packages/flutter_tools/lib/src/features.dart index 21733b69e3f16..31d02666a59b0 100644 --- a/packages/flutter_tools/lib/src/features.dart +++ b/packages/flutter_tools/lib/src/features.dart @@ -67,6 +67,9 @@ abstract class FeatureFlags { /// Whether desktop windowing is enabled. bool get isWindowingEnabled; + /// Whether physical iOS devices are debugging with LLDB. + bool get isLLDBDebuggingEnabled; + /// Whether a particular feature is enabled for the current channel. /// /// Prefer using one of the specific getters above instead of this API. @@ -87,6 +90,7 @@ abstract class FeatureFlags { swiftPackageManager, omitLegacyVersionFile, windowingFeature, + lldbDebugging, ]; /// All current Flutter feature flags that can be configured. @@ -218,6 +222,23 @@ const windowingFeature = Feature( master: FeatureChannelSetting(available: true), ); +/// Enable LLDB debugging for physical iOS devices. When LLDB debugging is off, +/// Xcode debugging is used instead. +/// +/// Requires iOS 17+ and Xcode 26+. If those requirements are not met, the previous +/// default debugging method is used instead. +const lldbDebugging = Feature( + name: 'support for debugging with LLDB for physical iOS devices', + extraHelpText: + 'If LLDB debugging is off, Xcode debugging is used instead. ' + 'Only available for iOS 17 or newer devices. Requires Xcode 26 or greater.', + configSetting: 'enable-lldb-debugging', + environmentOverride: 'FLUTTER_LLDB_DEBUGGING', + master: FeatureChannelSetting(available: true, enabledByDefault: true), + beta: FeatureChannelSetting(available: true, enabledByDefault: true), + stable: FeatureChannelSetting(available: true, enabledByDefault: true), +); + /// A [Feature] is a process for conditionally enabling tool features. /// /// All settings are optional, and if not provided will generally default to diff --git a/packages/flutter_tools/lib/src/flutter_features.dart b/packages/flutter_tools/lib/src/flutter_features.dart index fdec727aa5d0e..386fcbc625e4b 100644 --- a/packages/flutter_tools/lib/src/flutter_features.dart +++ b/packages/flutter_tools/lib/src/flutter_features.dart @@ -57,6 +57,9 @@ mixin FlutterFeatureFlagsIsEnabled implements FeatureFlags { @override bool get isWindowingEnabled => isEnabled(windowingFeature); + + @override + bool get isLLDBDebuggingEnabled => isEnabled(lldbDebugging); } interface class FlutterFeatureFlags extends FeatureFlags with FlutterFeatureFlagsIsEnabled { diff --git a/packages/flutter_tools/lib/src/ios/core_devices.dart b/packages/flutter_tools/lib/src/ios/core_devices.dart index 418f0559a5951..bab9e5d27ce1b 100644 --- a/packages/flutter_tools/lib/src/ios/core_devices.dart +++ b/packages/flutter_tools/lib/src/ios/core_devices.dart @@ -66,7 +66,7 @@ class IOSCoreDeviceLauncher { } // Launch app to device - final IOSCoreDeviceLaunchResult? launchResult = await _coreDeviceControl.launchAppInternal( + final IOSCoreDeviceLaunchResult? launchResult = await _coreDeviceControl.launchApp( deviceId: deviceId, bundleId: bundleId, launchArguments: launchArguments, @@ -99,7 +99,7 @@ class IOSCoreDeviceLauncher { } // Launch app on device, but start it stopped so it will wait until the debugger is attached before starting. - final IOSCoreDeviceLaunchResult? launchResult = await _coreDeviceControl.launchAppInternal( + final IOSCoreDeviceLaunchResult? launchResult = await _coreDeviceControl.launchApp( deviceId: deviceId, bundleId: bundleId, launchArguments: launchArguments, @@ -532,66 +532,11 @@ class IOSCoreDeviceControl { } } - Future launchApp({ - required String deviceId, - required String bundleId, - List launchArguments = const [], - }) async { - if (!_xcode.isDevicectlInstalled) { - _logger.printError('devicectl is not installed.'); - return false; - } - - final Directory tempDirectory = _fileSystem.systemTempDirectory.createTempSync('core_devices.'); - final File output = tempDirectory.childFile('launch_results.json'); - output.createSync(); - - final command = [ - ..._xcode.xcrunCommand(), - 'devicectl', - 'device', - 'process', - 'launch', - '--device', - deviceId, - bundleId, - if (launchArguments.isNotEmpty) ...launchArguments, - '--json-output', - output.path, - ]; - - try { - await _processUtils.run(command, throwOnError: true); - final String stringOutput = output.readAsStringSync(); - - try { - final Object? decodeResult = (json.decode(stringOutput) as Map)['info']; - if (decodeResult is Map && decodeResult['outcome'] == 'success') { - return true; - } - _logger.printError('devicectl returned unexpected JSON response: $stringOutput'); - return false; - } on FormatException { - // We failed to parse the devicectl output, or it returned junk. - _logger.printError('devicectl returned non-JSON response: $stringOutput'); - return false; - } - } on ProcessException catch (err) { - _logger.printError('Error executing devicectl: $err'); - return false; - } finally { - tempDirectory.deleteSync(recursive: true); - } - } - /// Launches the app on the device. /// /// If [startStopped] is true, the app will be launched and paused, waiting /// for a debugger to attach. - // TODO(vashworth): Rename this method to launchApp and replace old version. - // See https://github.com/flutter/flutter/issues/173416. - @visibleForTesting - Future launchAppInternal({ + Future launchApp({ required String deviceId, required String bundleId, List launchArguments = const [], diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index 98b4e87d6e645..a6dd75ee5a90d 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:meta/meta.dart'; import 'package:process/process.dart'; +import 'package:unified_analytics/unified_analytics.dart'; import 'package:vm_service/vm_service.dart' as vm_service; import '../application_package.dart'; @@ -24,6 +25,7 @@ import '../darwin/darwin.dart'; import '../device.dart'; import '../device_port_forwarder.dart'; import '../device_vm_service_discovery_for_attach.dart'; +import '../features.dart'; import '../globals.dart' as globals; import '../macos/xcdevice.dart'; import '../mdns_discovery.dart'; @@ -59,6 +61,15 @@ In the meantime, we recommend these temporary workarounds: profile mode via --release or --profile flags. ════════════════════════════════════════════════════════════════════════════════'''; +enum IOSDeploymentMethod { + iosDeployLaunch, + iosDeployLaunchAndAttach, + coreDeviceWithoutDebugger, + coreDeviceWithLLDB, + coreDeviceWithXcode, + coreDeviceWithXcodeFallback, +} + class IOSDevices extends PollingDeviceDiscovery { IOSDevices({ required Platform platform, @@ -284,6 +295,7 @@ class IOSDevice extends Device { required XcodeDebug xcodeDebug, required IProxy iProxy, required super.logger, + required Analytics analytics, }) : _sdkVersion = sdkVersion, _iosDeploy = iosDeploy, _iMobileDevice = iMobileDevice, @@ -293,6 +305,7 @@ class IOSDevice extends Device { _iproxy = iProxy, _fileSystem = fileSystem, _logger = logger, + _analytics = analytics, _platform = platform, super(category: Category.mobile, platformType: PlatformType.ios, ephemeral: true) { if (!_platform.isMacOS) { @@ -303,14 +316,12 @@ class IOSDevice extends Device { final String? _sdkVersion; final IOSDeploy _iosDeploy; + final Analytics _analytics; final FileSystem _fileSystem; final Logger _logger; final Platform _platform; final IMobileDevice _iMobileDevice; final IOSCoreDeviceControl _coreDeviceControl; - - // TODO(vashworth): See https://github.com/flutter/flutter/issues/173416. - // ignore: unused_field final IOSCoreDeviceLauncher _coreDeviceLauncher; final XcodeDebug _xcodeDebug; final IProxy _iproxy; @@ -492,7 +503,7 @@ class IOSDevice extends Device { _logger.printError('Could not build the precompiled application for the device.'); await diagnoseXcodeBuildFailure( buildResult, - analytics: globals.analytics, + analytics: _analytics, fileSystem: globals.fs, logger: globals.logger, platform: FlutterDarwinPlatform.ios, @@ -519,9 +530,10 @@ class IOSDevice extends Device { route, platformArgs, interfaceType: connectionInterface, - isCoreDevice: isCoreDevice, ); Status startAppStatus = _logger.startProgress('Installing and launching...'); + + IOSDeploymentMethod? deploymentMethod; try { ProtocolDiscovery? vmServiceDiscovery; var installationResult = 1; @@ -537,18 +549,21 @@ class IOSDevice extends Device { } if (isCoreDevice) { - installationResult = - await _startAppOnCoreDevice( - debuggingOptions: debuggingOptions, - package: package, - launchArguments: launchArguments, - mainPath: mainPath, - discoveryTimeout: discoveryTimeout, - shutdownHooks: shutdownHooks ?? globals.shutdownHooks, - ) - ? 0 - : 1; + final ( + bool result, + IOSDeploymentMethod coreDeviceDeploymentMethod, + ) = await _startAppOnCoreDevice( + debuggingOptions: debuggingOptions, + package: package, + launchArguments: launchArguments, + mainPath: mainPath, + discoveryTimeout: discoveryTimeout, + shutdownHooks: shutdownHooks ?? globals.shutdownHooks, + ); + installationResult = result ? 0 : 1; + deploymentMethod = coreDeviceDeploymentMethod; } else if (iosDeployDebugger == null) { + deploymentMethod = IOSDeploymentMethod.iosDeployLaunch; installationResult = await _iosDeploy.launchApp( deviceId: id, bundlePath: bundle.path, @@ -558,15 +573,30 @@ class IOSDevice extends Device { uninstallFirst: debuggingOptions.uninstallFirst, ); } else { + deploymentMethod = IOSDeploymentMethod.iosDeployLaunchAndAttach; installationResult = await iosDeployDebugger!.launchAndAttach() ? 0 : 1; } if (installationResult != 0) { + _analytics.send( + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: deploymentMethod.name, + result: 'launch failed', + ), + ); _printInstallError(bundle); await dispose(); return LaunchResult.failed(); } if (!debuggingOptions.debuggingEnabled) { + _analytics.send( + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: deploymentMethod.name, + result: 'release success', + ), + ); return LaunchResult.succeeded(); } @@ -587,13 +617,6 @@ class IOSDevice extends Device { _logger.printError( 'The Dart VM Service was not discovered after $defaultTimeout seconds. This is taking much longer than expected...', ); - if (isCoreDevice && debuggingOptions.debuggingEnabled) { - _logger.printError( - 'Open the Xcode window the project is opened in to ensure the app ' - 'is running. If the app is not running, try selecting "Product > Run" ' - 'to fix the problem.', - ); - } // If debugging with a wireless device and the timeout is reached, remind the // user to allow local network permissions. if (isWirelesslyConnected) { @@ -632,6 +655,13 @@ class IOSDevice extends Device { if (serviceURL == null) { await iosDeployDebugger?.stopAndDumpBacktrace(); await dispose(); + _analytics.send( + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: deploymentMethod.name, + result: 'wireless debugging failed', + ), + ); return LaunchResult.failed(); } @@ -688,13 +718,36 @@ class IOSDevice extends Device { if (localUri == null) { await iosDeployDebugger?.stopAndDumpBacktrace(); await dispose(); + _analytics.send( + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: deploymentMethod.name, + result: 'debugging failed', + ), + ); return LaunchResult.failed(); } + _analytics.send( + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: deploymentMethod.name, + result: 'debugging success', + ), + ); return LaunchResult.succeeded(vmServiceUri: localUri); } on ProcessException catch (e) { await iosDeployDebugger?.stopAndDumpBacktrace(); _logger.printError(e.message); await dispose(); + if (deploymentMethod != null) { + _analytics.send( + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: deploymentMethod.name, + result: 'process exception', + ), + ); + } return LaunchResult.failed(); } finally { startAppStatus.stop(); @@ -881,17 +934,25 @@ class IOSDevice extends Device { ); } + /// Uses either `devicectl` or Xcode automation to install, launch, and debug + /// apps on physical iOS devices. + /// /// Starting with Xcode 15 and iOS 17, `ios-deploy` stopped working due to /// the new CoreDevice connectivity stack. Previously, `ios-deploy` was used /// to install the app, launch the app, and start `debugserver`. + /// /// Xcode 15 introduced a new command line tool called `devicectl` that /// includes much of the functionality supplied by `ios-deploy`. However, - /// `devicectl` lacks the ability to start a `debugserver` and therefore `ptrace`, which are needed - /// for debug mode due to using a JIT Dart VM. + /// `devicectl` lacked the ability to start a `debugserver` and therefore `ptrace`, + /// which are needed for debug mode due to using a JIT Dart VM. + /// + /// Xcode 16 introduced a command to lldb that allows you to start a debugserver, which + /// can be used in unison with `devicectl`. /// /// Therefore, when starting an app on a CoreDevice, use `devicectl` when - /// debugging is not enabled. Otherwise, use Xcode automation. - Future _startAppOnCoreDevice({ + /// debugging is not enabled. If using Xcode 16, use `devicectl` and `lldb`. + /// Otherwise use Xcode automation. + Future<(bool, IOSDeploymentMethod)> _startAppOnCoreDevice({ required DebuggingOptions debuggingOptions, required IOSApp package, required List launchArguments, @@ -908,98 +969,140 @@ class IOSDevice extends Device { bundlePath: package.deviceBundlePath, ); if (!installSuccess) { - return installSuccess; + return (installSuccess, IOSDeploymentMethod.coreDeviceWithoutDebugger); } // Launch app to device - final bool launchSuccess = await _coreDeviceControl.launchApp( + final IOSCoreDeviceLaunchResult? launchResult = await _coreDeviceControl.launchApp( deviceId: id, bundleId: package.id, launchArguments: launchArguments, ); + final bool launchSuccess = launchResult != null && launchResult.outcome == 'success'; - return launchSuccess; - } else { - _logger.printStatus( - 'You may be prompted to give access to control Xcode. Flutter uses Xcode ' - 'to run your app. If access is not allowed, you can change this through ' - 'your Settings > Privacy & Security > Automation.', + return (launchSuccess, IOSDeploymentMethod.coreDeviceWithoutDebugger); + } + + IOSDeploymentMethod? deploymentMethod; + + // Xcode 16 introduced a way to start and attach to a debugserver through LLDB. + // However, it doesn't work reliably until Xcode 26. + // Use LLDB if Xcode version is greater than 26 and the feature is enabled. + final Version? xcodeVersion = globals.xcode?.currentVersion; + final bool lldbFeatureEnabled = featureFlags.isLLDBDebuggingEnabled; + if (xcodeVersion != null && xcodeVersion.major >= 26 && lldbFeatureEnabled) { + final bool launchSuccess = await _coreDeviceLauncher.launchAppWithLLDBDebugger( + deviceId: id, + bundlePath: package.deviceBundlePath, + bundleId: package.id, + launchArguments: launchArguments, ); - final launchTimeout = isWirelesslyConnected ? 45 : 30; - final timer = Timer(discoveryTimeout ?? Duration(seconds: launchTimeout), () { - _logger.printError( - 'Xcode is taking longer than expected to start debugging the app. ' - 'Ensure the project is opened in Xcode.', + + // If it succeeds to launch with LLDB, return, otherwise continue on to + // try launching with Xcode. + if (launchSuccess) { + return (launchSuccess, IOSDeploymentMethod.coreDeviceWithLLDB); + } else { + deploymentMethod = IOSDeploymentMethod.coreDeviceWithXcodeFallback; + _analytics.send( + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.coreDeviceWithLLDB.name, + result: 'launch failed', + ), ); - }); + } + } - XcodeDebugProject debugProject; - final FlutterProject flutterProject = FlutterProject.current(); + deploymentMethod ??= IOSDeploymentMethod.coreDeviceWithXcode; - if (package is PrebuiltIOSApp) { - debugProject = await _xcodeDebug.createXcodeProjectWithCustomBundle( - package.deviceBundlePath, - templateRenderer: globals.templateRenderer, - verboseLogging: _logger.isVerbose, - ); - } else if (package is BuildableIOSApp) { - // Before installing/launching/debugging with Xcode, update the build - // settings to use a custom configuration build directory so Xcode - // knows where to find the app bundle to launch. - final Directory bundle = _fileSystem.directory(package.deviceBundlePath); - await updateGeneratedXcodeProperties( - project: flutterProject, - buildInfo: debuggingOptions.buildInfo, - targetOverride: mainPath, - configurationBuildDir: bundle.parent.absolute.path, - ); + // If LLDB is not available or fails, fallback to using Xcode. + _logger.printStatus( + 'You may be prompted to give access to control Xcode. Flutter uses Xcode ' + 'to run your app. If access is not allowed, you can change this through ' + 'your Settings > Privacy & Security > Automation.', + ); + final launchTimeout = isWirelesslyConnected ? 45 : 30; + final timer = Timer(discoveryTimeout ?? Duration(seconds: launchTimeout), () { + _logger.printError( + 'Xcode is taking longer than expected to start debugging the app. ' + 'If the issue persists, try closing Xcode and re-running your Flutter command.', + ); + }); - final IosProject project = package.project; - final XcodeProjectInfo? projectInfo = await project.projectInfo(); - if (projectInfo == null) { - globals.printError('Xcode project not found.'); - return false; - } - if (project.xcodeWorkspace == null) { - globals.printError('Unable to get Xcode workspace.'); - return false; - } - final String? scheme = projectInfo.schemeFor(debuggingOptions.buildInfo); - if (scheme == null) { - projectInfo.reportFlavorNotFoundAndExit(); - } + XcodeDebugProject debugProject; + final FlutterProject flutterProject = FlutterProject.current(); - _xcodeDebug.ensureXcodeDebuggerLaunchAction(project.xcodeProjectSchemeFile(scheme: scheme)); + if (package is PrebuiltIOSApp) { + debugProject = await _xcodeDebug.createXcodeProjectWithCustomBundle( + package.deviceBundlePath, + templateRenderer: globals.templateRenderer, + verboseLogging: _logger.isVerbose, + ); + } else if (package is BuildableIOSApp) { + // Before installing/launching/debugging with Xcode, update the build + // settings to use a custom configuration build directory so Xcode + // knows where to find the app bundle to launch. + final Directory bundle = _fileSystem.directory(package.deviceBundlePath); + await updateGeneratedXcodeProperties( + project: flutterProject, + buildInfo: debuggingOptions.buildInfo, + targetOverride: mainPath, + configurationBuildDir: bundle.parent.absolute.path, + ); - debugProject = XcodeDebugProject( - scheme: scheme, - xcodeProject: project.xcodeProject, - xcodeWorkspace: project.xcodeWorkspace!, - hostAppProjectName: project.hostAppProjectName, - expectedConfigurationBuildDir: bundle.parent.absolute.path, - verboseLogging: _logger.isVerbose, - ); - } else { - // This should not happen. Currently, only PrebuiltIOSApp and - // BuildableIOSApp extend from IOSApp. - _logger.printError('IOSApp type ${package.runtimeType} is not recognized.'); - return false; + final IosProject project = package.project; + final XcodeProjectInfo? projectInfo = await project.projectInfo(); + if (projectInfo == null) { + globals.printError('Xcode project not found.'); + return (false, deploymentMethod); + } + if (project.xcodeWorkspace == null) { + globals.printError('Unable to get Xcode workspace.'); + return (false, deploymentMethod); + } + final String? scheme = projectInfo.schemeFor(debuggingOptions.buildInfo); + if (scheme == null) { + projectInfo.reportFlavorNotFoundAndExit(); } - final bool debugSuccess = await _xcodeDebug.debugApp( - project: debugProject, - deviceId: id, - launchArguments: launchArguments, + _xcodeDebug.ensureXcodeDebuggerLaunchAction(project.xcodeProjectSchemeFile(scheme: scheme)); + + debugProject = XcodeDebugProject( + scheme: scheme, + xcodeProject: project.xcodeProject, + xcodeWorkspace: project.xcodeWorkspace!, + hostAppProjectName: project.hostAppProjectName, + expectedConfigurationBuildDir: bundle.parent.absolute.path, + verboseLogging: _logger.isVerbose, ); - timer.cancel(); + } else { + // This should not happen. Currently, only PrebuiltIOSApp and + // BuildableIOSApp extend from IOSApp. + _logger.printError('IOSApp type ${package.runtimeType} is not recognized.'); + return (false, deploymentMethod); + } - // Kill Xcode on shutdown when running from CI - if (debuggingOptions.usingCISystem) { - shutdownHooks.addShutdownHook(() => _xcodeDebug.exit(force: true)); - } + // Core Devices (iOS 17 devices) are debugged through Xcode so don't + // include these flags, which are used to check if the app was launched + // via Flutter CLI and `ios-deploy`. + final List filteredLaunchArguments = launchArguments + .where((String arg) => arg != '--enable-checked-mode' && arg != '--verify-entry-points') + .toList(); + + final bool debugSuccess = await _xcodeDebug.debugApp( + project: debugProject, + deviceId: id, + launchArguments: filteredLaunchArguments, + ); + timer.cancel(); - return debugSuccess; + // Kill Xcode on shutdown when running from CI + if (debuggingOptions.usingCISystem) { + shutdownHooks.addShutdownHook(() => _xcodeDebug.exit(force: true)); } + + return (debugSuccess, deploymentMethod); } @override @@ -1012,7 +1115,7 @@ class IOSDevice extends Device { if (_xcodeDebug.debugStarted) { return _xcodeDebug.exit(); } - return false; + return _coreDeviceLauncher.stopApp(deviceId: id); } @override diff --git a/packages/flutter_tools/lib/src/ios/lldb.dart b/packages/flutter_tools/lib/src/ios/lldb.dart index 76912f3d9a1c9..025876ecec4f6 100644 --- a/packages/flutter_tools/lib/src/ios/lldb.dart +++ b/packages/flutter_tools/lib/src/ios/lldb.dart @@ -99,7 +99,7 @@ return False await _attachToAppProcess(appProcessId); await _resumeProcess(); } on _LLDBError catch (e) { - _logger.printError('lldb failed with error: ${e.message}'); + _logger.printTrace('lldb failed with error: ${e.message}'); exit(); return false; } finally { @@ -115,7 +115,7 @@ return False /// to `stderr`, complete with an error and stop the process. Future _startLLDB(int appProcessId) async { if (_lldbProcess != null) { - _logger.printError( + _logger.printTrace( 'An LLDB process is already running. It must be stopped before starting a new one.', ); return false; @@ -139,7 +139,7 @@ return False .transform(utf8.decoder) .transform(const LineSplitter()) .listen((String line) { - _logger.printError('[lldb]: $line'); + _logger.printTrace('[lldb]: $line'); _monitorError(line); }); @@ -155,7 +155,7 @@ return False }), ); } on ProcessException catch (exception) { - _logger.printError('Process exception running lldb:\n$exception'); + _logger.printTrace('Process exception running lldb:\n$exception'); return false; } return true; diff --git a/packages/flutter_tools/lib/src/macos/xcdevice.dart b/packages/flutter_tools/lib/src/macos/xcdevice.dart index 687784cb37f19..9534531b1eb7d 100644 --- a/packages/flutter_tools/lib/src/macos/xcdevice.dart +++ b/packages/flutter_tools/lib/src/macos/xcdevice.dart @@ -602,6 +602,7 @@ class XCDevice { iProxy: _iProxy, fileSystem: globals.fs, logger: _logger, + analytics: globals.analytics, iosDeploy: _iosDeploy, iMobileDevice: _iMobileDevice, coreDeviceControl: _coreDeviceControl, diff --git a/packages/flutter_tools/test/general.shard/device_test.dart b/packages/flutter_tools/test/general.shard/device_test.dart index e18b510f4022d..ad86a756eeef9 100644 --- a/packages/flutter_tools/test/general.shard/device_test.dart +++ b/packages/flutter_tools/test/general.shard/device_test.dart @@ -791,22 +791,6 @@ void main() { }, ); - testWithoutContext( - 'Get launch arguments for physical CoreDevice with debugging enabled with no launch arguments', - () { - final original = DebuggingOptions.enabled(BuildInfo.debug); - - final List launchArguments = original.getIOSLaunchArguments( - EnvironmentType.physical, - null, - {}, - isCoreDevice: true, - ); - - expect(launchArguments.join(' '), ['--enable-dart-profiling'].join(' ')); - }, - ); - testWithoutContext('Get launch arguments for physical device with iPv4 network connection', () { final original = DebuggingOptions.enabled(BuildInfo.debug); diff --git a/packages/flutter_tools/test/general.shard/flutter_validator_test.dart b/packages/flutter_tools/test/general.shard/flutter_validator_test.dart index b9b147c0a7db6..2f96ce4e71fd9 100644 --- a/packages/flutter_tools/test/general.shard/flutter_validator_test.dart +++ b/packages/flutter_tools/test/general.shard/flutter_validator_test.dart @@ -818,6 +818,9 @@ class FakeFlutterFeatures extends FeatureFlags { @override bool get isWindowingEnabled => _enabled; + @override + bool get isLLDBDebuggingEnabled => _enabled; + @override final List allFeatures; diff --git a/packages/flutter_tools/test/general.shard/ios/core_devices_test.dart b/packages/flutter_tools/test/general.shard/ios/core_devices_test.dart index eedfa39685475..80434cac57e6f 100644 --- a/packages/flutter_tools/test/general.shard/ios/core_devices_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/core_devices_test.dart @@ -681,13 +681,13 @@ void main() { }); testWithoutContext('fails to launch app', () async { - final bool status = await deviceControl.launchApp( + final IOSCoreDeviceLaunchResult? result = await deviceControl.launchApp( deviceId: 'device-id', bundleId: 'com.example.flutterApp', ); expect(fakeProcessManager, hasNoRemainingExpectations); - expect(logger.errorText, contains('devicectl is not installed.')); - expect(status, isFalse); + expect(logger.traceText, contains('devicectl is not installed.')); + expect(result, isNull); }); testWithoutContext('fails to check if app is installed', () async { @@ -1287,403 +1287,7 @@ invalid JSON ), ); - final bool status = await deviceControl.launchApp(deviceId: deviceId, bundleId: bundleId); - - expect(fakeProcessManager, hasNoRemainingExpectations); - expect(logger.errorText, isEmpty); - expect(tempFile, isNot(exists)); - expect(status, true); - }); - - testWithoutContext('Successful launch with launch args', () async { - const deviceControlOutput = ''' -{ - "info" : { - "arguments" : [ - "devicectl", - "device", - "process", - "launch", - "--device", - "00001234-0001234A3C03401E", - "com.example.flutterApp", - "--arg1", - "--arg2", - "--json-output", - "/var/folders/wq/randompath/T/flutter_tools.rand0/core_devices.rand0/install_results.json" - ], - "commandType" : "devicectl.device.process.launch", - "environment" : { - - }, - "outcome" : "success", - "version" : "341" - }, - "result" : { - "deviceIdentifier" : "123456BB5-AEDE-7A22-B890-1234567890DD", - "launchOptions" : { - "activatedWhenStarted" : true, - "arguments" : [ - - ], - "environmentVariables" : { - "TERM" : "vt100" - }, - "platformSpecificOptions" : { - - }, - "startStopped" : false, - "terminateExistingInstances" : false, - "user" : { - "active" : true - } - }, - "process" : { - "auditToken" : [ - 12345, - 678 - ], - "executable" : "file:///private/var/containers/Bundle/Application/12345E6A-7F89-0C12-345E-F6A7E890CFF1/Runner.app/Runner", - "processIdentifier" : 1234 - } - } -} -'''; - final File tempFile = fileSystem.systemTempDirectory - .childDirectory('core_devices.rand0') - .childFile('launch_results.json'); - fakeProcessManager.addCommand( - FakeCommand( - command: [ - 'xcrun', - 'devicectl', - 'device', - 'process', - 'launch', - '--device', - deviceId, - bundleId, - '--arg1', - '--arg2', - '--json-output', - tempFile.path, - ], - onRun: (_) { - expect(tempFile, exists); - tempFile.writeAsStringSync(deviceControlOutput); - }, - ), - ); - - final bool status = await deviceControl.launchApp( - deviceId: deviceId, - bundleId: bundleId, - launchArguments: ['--arg1', '--arg2'], - ); - - expect(fakeProcessManager, hasNoRemainingExpectations); - expect(logger.errorText, isEmpty); - expect(tempFile, isNot(exists)); - expect(status, true); - }); - - testWithoutContext('devicectl fails install with an error', () async { - const deviceControlOutput = ''' -{ - "error" : { - "code" : -10814, - "domain" : "NSOSStatusErrorDomain", - "userInfo" : { - "_LSFunction" : { - "string" : "runEvaluator" - }, - "_LSLine" : { - "int" : 1608 - } - } - }, - "info" : { - "arguments" : [ - "devicectl", - "device", - "process", - "launch", - "--device", - "00001234-0001234A3C03401E", - "com.example.flutterApp", - "--json-output", - "/var/folders/wq/randompath/T/flutter_tools.rand0/core_devices.rand0/install_results.json" - ], - "commandType" : "devicectl.device.process.launch", - "environment" : { - - }, - "outcome" : "failed", - "version" : "341" - } -} -'''; - final File tempFile = fileSystem.systemTempDirectory - .childDirectory('core_devices.rand0') - .childFile('launch_results.json'); - fakeProcessManager.addCommand( - FakeCommand( - command: [ - 'xcrun', - 'devicectl', - 'device', - 'process', - 'launch', - '--device', - deviceId, - bundleId, - '--json-output', - tempFile.path, - ], - onRun: (_) { - expect(tempFile, exists); - tempFile.writeAsStringSync(deviceControlOutput); - }, - exitCode: 1, - stderr: ''' -ERROR: The operation couldn?t be completed. (OSStatus error -10814.) (NSOSStatusErrorDomain error -10814.) - _LSFunction = runEvaluator - _LSLine = 1608 -''', - ), - ); - - final bool status = await deviceControl.launchApp(deviceId: deviceId, bundleId: bundleId); - - expect(fakeProcessManager, hasNoRemainingExpectations); - expect(logger.errorText, contains('ERROR: The operation couldn?t be completed.')); - expect(tempFile, isNot(exists)); - expect(status, false); - }); - - testWithoutContext('devicectl fails install without an error', () async { - const deviceControlOutput = ''' -{ - "error" : { - "code" : -10814, - "domain" : "NSOSStatusErrorDomain", - "userInfo" : { - "_LSFunction" : { - "string" : "runEvaluator" - }, - "_LSLine" : { - "int" : 1608 - } - } - }, - "info" : { - "arguments" : [ - "devicectl", - "device", - "process", - "launch", - "--device", - "00001234-0001234A3C03401E", - "com.example.flutterApp", - "--json-output", - "/var/folders/wq/randompath/T/flutter_tools.rand0/core_devices.rand0/install_results.json" - ], - "commandType" : "devicectl.device.process.launch", - "environment" : { - - }, - "outcome" : "failed", - "version" : "341" - } -} -'''; - final File tempFile = fileSystem.systemTempDirectory - .childDirectory('core_devices.rand0') - .childFile('launch_results.json'); - fakeProcessManager.addCommand( - FakeCommand( - command: [ - 'xcrun', - 'devicectl', - 'device', - 'process', - 'launch', - '--device', - deviceId, - bundleId, - '--json-output', - tempFile.path, - ], - onRun: (_) { - expect(tempFile, exists); - tempFile.writeAsStringSync(deviceControlOutput); - }, - ), - ); - - final bool status = await deviceControl.launchApp(deviceId: deviceId, bundleId: bundleId); - - expect(fakeProcessManager, hasNoRemainingExpectations); - expect(tempFile, isNot(exists)); - expect(status, false); - }); - - testWithoutContext('fails launch because of unexpected JSON', () async { - const deviceControlOutput = ''' -{ - "valid_unexpected_json": true -} -'''; - final File tempFile = fileSystem.systemTempDirectory - .childDirectory('core_devices.rand0') - .childFile('launch_results.json'); - fakeProcessManager.addCommand( - FakeCommand( - command: [ - 'xcrun', - 'devicectl', - 'device', - 'process', - 'launch', - '--device', - deviceId, - bundleId, - '--json-output', - tempFile.path, - ], - onRun: (_) { - expect(tempFile, exists); - tempFile.writeAsStringSync(deviceControlOutput); - }, - ), - ); - - final bool status = await deviceControl.launchApp(deviceId: deviceId, bundleId: bundleId); - - expect(fakeProcessManager, hasNoRemainingExpectations); - expect(logger.errorText, contains('devicectl returned unexpected JSON response')); - expect(tempFile, isNot(exists)); - expect(status, false); - }); - - testWithoutContext('fails launch because of invalid JSON', () async { - const deviceControlOutput = ''' -invalid JSON -'''; - final File tempFile = fileSystem.systemTempDirectory - .childDirectory('core_devices.rand0') - .childFile('launch_results.json'); - fakeProcessManager.addCommand( - FakeCommand( - command: [ - 'xcrun', - 'devicectl', - 'device', - 'process', - 'launch', - '--device', - deviceId, - bundleId, - '--json-output', - tempFile.path, - ], - onRun: (_) { - expect(tempFile, exists); - tempFile.writeAsStringSync(deviceControlOutput); - }, - ), - ); - - final bool status = await deviceControl.launchApp(deviceId: deviceId, bundleId: bundleId); - - expect(fakeProcessManager, hasNoRemainingExpectations); - expect(logger.errorText, contains('devicectl returned non-JSON response')); - expect(tempFile, isNot(exists)); - expect(status, false); - }); - }); - - group('launchAppInternal', () { - const deviceId = 'device-id'; - const bundleId = 'com.example.flutterApp'; - - testWithoutContext('Successful launch without launch args', () async { - const deviceControlOutput = ''' -{ - "info" : { - "arguments" : [ - "devicectl", - "device", - "process", - "launch", - "--device", - "00001234-0001234A3C03401E", - "com.example.flutterApp", - "--json-output", - "/var/folders/wq/randompath/T/flutter_tools.rand0/core_devices.rand0/install_results.json" - ], - "commandType" : "devicectl.device.process.launch", - "environment" : { - - }, - "outcome" : "success", - "version" : "341" - }, - "result" : { - "deviceIdentifier" : "123456BB5-AEDE-7A22-B890-1234567890DD", - "launchOptions" : { - "activatedWhenStarted" : true, - "arguments" : [ - - ], - "environmentVariables" : { - "TERM" : "vt100" - }, - "platformSpecificOptions" : { - - }, - "startStopped" : false, - "terminateExistingInstances" : false, - "user" : { - "active" : true - } - }, - "process" : { - "auditToken" : [ - 12345, - 678 - ], - "executable" : "file:///private/var/containers/Bundle/Application/12345E6A-7F89-0C12-345E-F6A7E890CFF1/Runner.app/Runner", - "processIdentifier" : 1234 - } - } -} -'''; - final File tempFile = fileSystem.systemTempDirectory - .childDirectory('core_devices.rand0') - .childFile('launch_results.json'); - fakeProcessManager.addCommand( - FakeCommand( - command: [ - 'xcrun', - 'devicectl', - 'device', - 'process', - 'launch', - '--device', - deviceId, - bundleId, - '--json-output', - tempFile.path, - ], - onRun: (_) { - expect(tempFile, exists); - tempFile.writeAsStringSync(deviceControlOutput); - }, - ), - ); - - final IOSCoreDeviceLaunchResult? result = await deviceControl.launchAppInternal( + final IOSCoreDeviceLaunchResult? result = await deviceControl.launchApp( deviceId: deviceId, bundleId: bundleId, ); @@ -1775,7 +1379,7 @@ invalid JSON ), ); - final IOSCoreDeviceLaunchResult? result = await deviceControl.launchAppInternal( + final IOSCoreDeviceLaunchResult? result = await deviceControl.launchApp( deviceId: deviceId, bundleId: bundleId, launchArguments: ['--arg1', '--arg2'], @@ -1854,7 +1458,7 @@ ERROR: The operation couldn?t be completed. (OSStatus error -10814.) (NSOSStatus ), ); - final IOSCoreDeviceLaunchResult? result = await deviceControl.launchAppInternal( + final IOSCoreDeviceLaunchResult? result = await deviceControl.launchApp( deviceId: deviceId, bundleId: bundleId, ); @@ -1925,7 +1529,7 @@ ERROR: The operation couldn?t be completed. (OSStatus error -10814.) (NSOSStatus ), ); - final IOSCoreDeviceLaunchResult? result = await deviceControl.launchAppInternal( + final IOSCoreDeviceLaunchResult? result = await deviceControl.launchApp( deviceId: deviceId, bundleId: bundleId, ); @@ -1966,7 +1570,7 @@ ERROR: The operation couldn?t be completed. (OSStatus error -10814.) (NSOSStatus ), ); - final IOSCoreDeviceLaunchResult? result = await deviceControl.launchAppInternal( + final IOSCoreDeviceLaunchResult? result = await deviceControl.launchApp( deviceId: deviceId, bundleId: bundleId, ); @@ -2005,7 +1609,7 @@ invalid JSON ), ); - final IOSCoreDeviceLaunchResult? result = await deviceControl.launchAppInternal( + final IOSCoreDeviceLaunchResult? result = await deviceControl.launchApp( deviceId: deviceId, bundleId: bundleId, ); @@ -3584,7 +3188,7 @@ class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl { } @override - Future launchAppInternal({ + Future launchApp({ required String deviceId, required String bundleId, List launchArguments = const [], diff --git a/packages/flutter_tools/test/general.shard/ios/devices_test.dart b/packages/flutter_tools/test/general.shard/ios/devices_test.dart index 9b43d4c117663..f09b38040af65 100644 --- a/packages/flutter_tools/test/general.shard/ios/devices_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/devices_test.dart @@ -28,6 +28,7 @@ import 'package:flutter_tools/src/ios/mac.dart'; import 'package:flutter_tools/src/ios/xcode_debug.dart'; import 'package:flutter_tools/src/macos/xcdevice.dart'; import 'package:test/fake.dart'; +import 'package:unified_analytics/unified_analytics.dart'; import '../../src/common.dart'; import '../../src/fake_process_manager.dart'; @@ -79,6 +80,7 @@ void main() { logger: logger, platform: macPlatform, iosDeploy: iosDeploy, + analytics: FakeAnalytics(), iMobileDevice: iMobileDevice, coreDeviceControl: coreDeviceControl, coreDeviceLauncher: coreDeviceLauncher, @@ -101,6 +103,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: macPlatform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -125,6 +128,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: macPlatform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -148,6 +152,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: macPlatform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -171,6 +176,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: macPlatform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -194,6 +200,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: macPlatform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -217,6 +224,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: macPlatform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -242,6 +250,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: macPlatform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -267,6 +276,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: macPlatform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -292,6 +302,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: macPlatform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -317,6 +328,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: macPlatform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -342,6 +354,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: macPlatform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -363,6 +376,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: macPlatform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -387,6 +401,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: macPlatform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -412,6 +427,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: macPlatform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -444,6 +460,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: platform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -533,6 +550,7 @@ void main() { iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), fileSystem: fileSystem, logger: logger, + analytics: FakeAnalytics(), platform: macPlatform, iosDeploy: iosDeploy, iMobileDevice: iMobileDevice, @@ -609,6 +627,7 @@ void main() { cpuArchitecture: DarwinArch.arm64, iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), iosDeploy: iosDeploy, + analytics: FakeAnalytics(), iMobileDevice: iMobileDevice, coreDeviceControl: coreDeviceControl, coreDeviceLauncher: coreDeviceLauncher, @@ -630,6 +649,7 @@ void main() { cpuArchitecture: DarwinArch.arm64, iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), iosDeploy: iosDeploy, + analytics: FakeAnalytics(), iMobileDevice: iMobileDevice, coreDeviceControl: coreDeviceControl, coreDeviceLauncher: coreDeviceLauncher, @@ -939,6 +959,7 @@ void main() { cpuArchitecture: DarwinArch.arm64, iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()), iosDeploy: iosDeploy, + analytics: FakeAnalytics(), iMobileDevice: iMobileDevice, coreDeviceControl: coreDeviceControl, coreDeviceLauncher: coreDeviceLauncher, @@ -1108,3 +1129,5 @@ class FakeXcodeDebug extends Fake implements XcodeDebug { class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl {} class FakeIOSCoreDeviceLauncher extends Fake implements IOSCoreDeviceLauncher {} + +class FakeAnalytics extends Fake implements Analytics {} diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_install_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_install_test.dart index 539c14f009773..9d62ad5307eff 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_install_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_install_test.dart @@ -19,6 +19,7 @@ import 'package:flutter_tools/src/ios/iproxy.dart'; import 'package:flutter_tools/src/ios/mac.dart'; import 'package:flutter_tools/src/ios/xcode_debug.dart'; import 'package:test/fake.dart'; +import 'package:unified_analytics/unified_analytics.dart'; import '../../src/common.dart'; import '../../src/fake_process_manager.dart'; @@ -380,6 +381,7 @@ IOSDevice setUpIOSDevice({ artifacts: artifacts, cache: cache, ), + analytics: FakeAnalytics(), coreDeviceControl: FakeIOSCoreDeviceControl(), coreDeviceLauncher: FakeIOSCoreDeviceLauncher(), xcodeDebug: FakeXcodeDebug(), @@ -412,3 +414,5 @@ class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl { } class FakeIOSCoreDeviceLauncher extends Fake implements IOSCoreDeviceLauncher {} + +class FakeAnalytics extends Fake implements Analytics {} diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_project_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_project_test.dart index 5fcb5a71fdde4..d42e12c1e1b80 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_project_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_project_test.dart @@ -18,6 +18,7 @@ import 'package:flutter_tools/src/ios/mac.dart'; import 'package:flutter_tools/src/ios/xcode_debug.dart'; import 'package:flutter_tools/src/project.dart'; import 'package:test/fake.dart'; +import 'package:unified_analytics/unified_analytics.dart'; import '../../src/common.dart'; import '../../src/context.dart'; @@ -107,6 +108,7 @@ IOSDevice setUpIOSDevice(FileSystem fileSystem) { artifacts: Artifacts.test(), cache: Cache.test(processManager: processManager), ), + analytics: FakeAnalytics(), iMobileDevice: IMobileDevice.test(processManager: processManager), coreDeviceControl: FakeIOSCoreDeviceControl(), coreDeviceLauncher: FakeIOSCoreDeviceLauncher(), @@ -129,3 +131,5 @@ class FakeXcodeDebug extends Fake implements XcodeDebug {} class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl {} class FakeIOSCoreDeviceLauncher extends Fake implements IOSCoreDeviceLauncher {} + +class FakeAnalytics extends Fake implements Analytics {} diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart index a681ad09225b2..9e7a498510c1f 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart @@ -219,11 +219,13 @@ void main() { testUsingContext( 'with buildable app', () async { + final fakeExactAnalytics = FakeExactAnalytics(); final IOSDevice iosDevice = setUpIOSDevice( fileSystem: fileSystem, processManager: processManager, logger: logger, artifacts: artifacts, + analytics: fakeExactAnalytics, ); setUpIOSProject(fileSystem); final FlutterProject flutterProject = FlutterProject.fromDirectory( @@ -279,6 +281,13 @@ void main() { expect(fileSystem.directory('build/ios/iphoneos'), exists); expect(launchResult.started, true); expect(processManager, hasNoRemainingExpectations); + expect(fakeExactAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.iosDeployLaunch.name, + result: 'release success', + ), + ]); }, overrides: { ProcessManager: () => processManager, @@ -296,11 +305,13 @@ void main() { 'ONLY_ACTIVE_ARCH is NO if different host and target architectures', () async { // Host architecture is x64, target architecture is arm64. + final fakeExactAnalytics = FakeExactAnalytics(); final IOSDevice iosDevice = setUpIOSDevice( fileSystem: fileSystem, processManager: processManager, logger: logger, artifacts: artifacts, + analytics: fakeExactAnalytics, ); setUpIOSProject(fileSystem); final FlutterProject flutterProject = FlutterProject.fromDirectory( @@ -385,6 +396,13 @@ void main() { expect(fileSystem.directory('build/ios/iphoneos'), exists); expect(launchResult.started, true); expect(processManager, hasNoRemainingExpectations); + expect(fakeExactAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.iosDeployLaunch.name, + result: 'release success', + ), + ]); }, overrides: { ProcessManager: () => processManager, @@ -401,11 +419,13 @@ void main() { testUsingContext( 'with concurrent build failures', () async { + final fakeExactAnalytics = FakeExactAnalytics(); final IOSDevice iosDevice = setUpIOSDevice( fileSystem: fileSystem, processManager: processManager, logger: logger, artifacts: artifacts, + analytics: fakeExactAnalytics, ); setUpIOSProject(fileSystem); final FlutterProject flutterProject = FlutterProject.fromDirectory( @@ -464,6 +484,13 @@ void main() { ); expect(launchResult.started, true); expect(processManager, hasNoRemainingExpectations); + expect(fakeExactAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.iosDeployLaunch.name, + result: 'release success', + ), + ]); }), ), ); @@ -502,7 +529,10 @@ void main() { projectInfo = XcodeProjectInfo(['Runner'], ['Debug', 'Release'], [ 'Runner', ], logger); - fakeXcodeProjectInterpreter = FakeXcodeProjectInterpreter(projectInfo: projectInfo); + fakeXcodeProjectInterpreter = FakeXcodeProjectInterpreter( + projectInfo: projectInfo, + xcodeVersion: Version(15, 0, 0), + ); xcode = Xcode.test( processManager: FakeProcessManager.any(), xcodeProjectInterpreter: fakeXcodeProjectInterpreter, @@ -513,6 +543,7 @@ void main() { testUsingContext( 'succeeds when install and launch succeed', () async { + final fakeExactAnalytics = FakeExactAnalytics(); final IOSDevice iosDevice = setUpIOSDevice( fileSystem: fileSystem, processManager: FakeProcessManager.any(), @@ -520,6 +551,7 @@ void main() { artifacts: artifacts, isCoreDevice: true, coreDeviceControl: FakeIOSCoreDeviceControl(), + analytics: fakeExactAnalytics, ); setUpIOSProject(fileSystem); final FlutterProject flutterProject = FlutterProject.fromDirectory( @@ -543,6 +575,13 @@ void main() { expect(fileSystem.directory('build/ios/iphoneos'), exists); expect(launchResult.started, true); expect(processManager, hasNoRemainingExpectations); + expect(fakeExactAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.coreDeviceWithoutDebugger.name, + result: 'release success', + ), + ]); }, overrides: { ProcessManager: () => FakeProcessManager.any(), @@ -559,6 +598,7 @@ void main() { testUsingContext( 'fails when install fails', () async { + final fakeExactAnalytics = FakeExactAnalytics(); final IOSDevice iosDevice = setUpIOSDevice( fileSystem: fileSystem, processManager: FakeProcessManager.any(), @@ -566,6 +606,8 @@ void main() { artifacts: artifacts, isCoreDevice: true, coreDeviceControl: FakeIOSCoreDeviceControl(installSuccess: false), + coreDeviceLauncher: FakeIOSCoreDeviceLauncher(launchResult: false), + analytics: fakeExactAnalytics, ); setUpIOSProject(fileSystem); final FlutterProject flutterProject = FlutterProject.fromDirectory( @@ -589,6 +631,13 @@ void main() { expect(fileSystem.directory('build/ios/iphoneos'), exists); expect(launchResult.started, false); expect(processManager, hasNoRemainingExpectations); + expect(fakeExactAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.coreDeviceWithoutDebugger.name, + result: 'launch failed', + ), + ]); }, overrides: { ProcessManager: () => FakeProcessManager.any(), @@ -652,6 +701,7 @@ void main() { 'ensure arguments passed to launch', () async { final coreDeviceControl = FakeIOSCoreDeviceControl(); + final fakeExactAnalytics = FakeExactAnalytics(); final IOSDevice iosDevice = setUpIOSDevice( fileSystem: fileSystem, processManager: FakeProcessManager.any(), @@ -659,6 +709,7 @@ void main() { artifacts: artifacts, isCoreDevice: true, coreDeviceControl: coreDeviceControl, + analytics: fakeExactAnalytics, ); setUpIOSProject(fileSystem); final FlutterProject flutterProject = FlutterProject.fromDirectory( @@ -684,6 +735,13 @@ void main() { expect(processManager, hasNoRemainingExpectations); expect(coreDeviceControl.argumentsUsedForLaunch, isNotNull); expect(coreDeviceControl.argumentsUsedForLaunch, contains('--enable-dart-profiling')); + expect(fakeExactAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.coreDeviceWithoutDebugger.name, + result: 'release success', + ), + ]); }, overrides: { ProcessManager: () => FakeProcessManager.any(), @@ -1177,6 +1235,7 @@ IOSDevice setUpIOSDevice({ IOSCoreDeviceLauncher? coreDeviceLauncher, FakeXcodeDebug? xcodeDebug, DarwinArch cpuArchitecture = DarwinArch.arm64, + FakeExactAnalytics? analytics, }) { artifacts ??= Artifacts.test(); final cache = Cache.test( @@ -1200,6 +1259,7 @@ IOSDevice setUpIOSDevice({ artifacts: artifacts, cache: cache, ), + analytics: analytics ?? FakeExactAnalytics(), iMobileDevice: IMobileDevice( logger: logger, processManager: processManager ?? FakeProcessManager.any(), @@ -1226,7 +1286,8 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete 'WRAPPER_NAME': 'My Super Awesome App.app', 'DEVELOPMENT_TEAM': '3333CCCC33', }, - }); + Version? xcodeVersion, + }) : version = xcodeVersion ?? Version(1000, 0, 0); final Map buildSettings; final XcodeProjectInfo? projectInfo; @@ -1235,7 +1296,7 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete final isInstalled = true; @override - final version = Version(1000, 0, 0); + Version? version; @override String get versionText => version.toString(); @@ -1319,13 +1380,17 @@ class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl { } @override - Future launchApp({ + Future launchApp({ required String deviceId, required String bundleId, List launchArguments = const [], + bool startStopped = false, }) async { _launchArguments = launchArguments; - return launchSuccess; + final outcome = launchSuccess ? 'success' : 'failed'; + return IOSCoreDeviceLaunchResult.fromJson({ + 'info': {'outcome': outcome}, + }); } } @@ -1395,4 +1460,36 @@ const _validScheme = ''' '''; -class FakeIOSCoreDeviceLauncher extends Fake implements IOSCoreDeviceLauncher {} +class FakeIOSCoreDeviceLauncher extends Fake implements IOSCoreDeviceLauncher { + FakeIOSCoreDeviceLauncher({this.launchResult = true}); + bool launchResult; + + @override + Future launchAppWithoutDebugger({ + required String deviceId, + required String bundlePath, + required String bundleId, + required List launchArguments, + }) async { + return launchResult; + } + + @override + Future launchAppWithLLDBDebugger({ + required String deviceId, + required String bundlePath, + required String bundleId, + required List launchArguments, + }) async { + return true; + } +} + +class FakeExactAnalytics extends Fake implements Analytics { + final sentEvents = []; + + @override + void send(Event event) { + sentEvents.add(event); + } +} diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart index 05ed61ae0100a..e294e9441e914 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart @@ -13,6 +13,7 @@ import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/process.dart'; import 'package:flutter_tools/src/base/template.dart'; +import 'package:flutter_tools/src/base/version.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/device.dart'; @@ -24,8 +25,10 @@ import 'package:flutter_tools/src/ios/ios_deploy.dart'; import 'package:flutter_tools/src/ios/iproxy.dart'; import 'package:flutter_tools/src/ios/mac.dart'; import 'package:flutter_tools/src/ios/xcode_debug.dart'; +import 'package:flutter_tools/src/macos/xcode.dart'; import 'package:flutter_tools/src/mdns_discovery.dart'; import 'package:test/fake.dart'; +import 'package:unified_analytics/unified_analytics.dart'; import '../../src/common.dart'; import '../../src/context.dart'; @@ -131,9 +134,11 @@ void main() { () async { final FileSystem fileSystem = MemoryFileSystem.test(); final processManager = FakeProcessManager.list([attachDebuggerCommand()]); + final fakeAnalytics = FakeAnalytics(); final IOSDevice device = setUpIOSDevice( processManager: processManager, fileSystem: fileSystem, + analytics: fakeAnalytics, ); final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', @@ -162,6 +167,13 @@ void main() { expect(launchResult.started, true); expect(launchResult.hasVmService, true); expect(await device.stopApp(iosApp), false); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.iosDeployLaunchAndAttach.name, + result: 'debugging success', + ), + ]); }, ); @@ -171,6 +183,7 @@ void main() { final logger = BufferLogger.test(); final FileSystem fileSystem = MemoryFileSystem.test(); final completer = Completer(); + final fakeAnalytics = FakeAnalytics(); final processManager = FakeProcessManager.list([ attachDebuggerCommand(stdout: 'PROCESS_EXITED'), attachDebuggerCommand( @@ -183,6 +196,7 @@ void main() { processManager: processManager, fileSystem: fileSystem, logger: logger, + analytics: fakeAnalytics, ); final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', @@ -214,6 +228,18 @@ void main() { expect(secondLaunchResult.started, true); expect(secondLaunchResult.hasVmService, true); expect(await device.stopApp(iosApp), true); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.iosDeployLaunchAndAttach.name, + result: 'launch failed', + ), + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.iosDeployLaunchAndAttach.name, + result: 'debugging success', + ), + ]); }, ); @@ -222,10 +248,12 @@ void main() { () async { final FileSystem fileSystem = MemoryFileSystem.test(); final processManager = FakeProcessManager.list([kLaunchDebugCommand]); + final fakeAnalytics = FakeAnalytics(); final IOSDevice device = setUpIOSDevice( sdkVersion: '12.4.4', processManager: processManager, fileSystem: fileSystem, + analytics: fakeAnalytics, ); final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', @@ -254,6 +282,13 @@ void main() { expect(launchResult.started, true); expect(launchResult.hasVmService, true); expect(await device.stopApp(iosApp), false); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.iosDeployLaunch.name, + result: 'debugging success', + ), + ]); }, ); @@ -264,12 +299,14 @@ void main() { final logger = BufferLogger.test(); final stdin = CompleterIOSink(); final completer = Completer(); + final fakeAnalytics = FakeAnalytics(); final processManager = FakeProcessManager.list([ attachDebuggerCommand(stdin: stdin, completer: completer), ]); final IOSDevice device = setUpIOSDevice( processManager: processManager, fileSystem: fileSystem, + analytics: fakeAnalytics, logger: logger, ); final IOSApp iosApp = PrebuiltIOSApp( @@ -307,6 +344,13 @@ void main() { expect(utf8.decoder.convert(stdin.writes.first), contains('process interrupt')); completer.complete(); expect(processManager, hasNoRemainingExpectations); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.iosDeployLaunchAndAttach.name, + result: 'debugging success', + ), + ]); }, ); @@ -317,12 +361,14 @@ void main() { final logger = BufferLogger.test(); final stdin = CompleterIOSink(); final completer = Completer(); + final fakeAnalytics = FakeAnalytics(); final processManager = FakeProcessManager.list([ attachDebuggerCommand(stdin: stdin, completer: completer, isWirelessDevice: true), ]); final IOSDevice device = setUpIOSDevice( processManager: processManager, fileSystem: fileSystem, + analytics: fakeAnalytics, logger: logger, interfaceType: DeviceConnectionInterface.wireless, ); @@ -371,6 +417,13 @@ void main() { ), ); completer.complete(); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.iosDeployLaunchAndAttach.name, + result: 'debugging success', + ), + ]); expect(processManager, hasNoRemainingExpectations); }, overrides: {MDnsVmServiceDiscovery: () => FakeMDnsVmServiceDiscovery()}, @@ -382,6 +435,7 @@ void main() { final logger = BufferLogger.test(); final FileSystem fileSystem = MemoryFileSystem.test(); final completer = Completer(); + final fakeAnalytics = FakeAnalytics(); final processManager = FakeProcessManager.list([ attachDebuggerCommand( stdout: @@ -399,6 +453,7 @@ void main() { processManager: processManager, fileSystem: fileSystem, logger: logger, + analytics: fakeAnalytics, ); final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', @@ -425,6 +480,13 @@ void main() { expect(launchResult.started, true); expect(launchResult.hasVmService, true); expect(await device.stopApp(iosApp), true); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.iosDeployLaunchAndAttach.name, + result: 'debugging success', + ), + ]); }, ); @@ -433,6 +495,7 @@ void main() { () async { final logger = BufferLogger.test(); final FileSystem fileSystem = MemoryFileSystem.test(); + final fakeAnalytics = FakeAnalytics(); final processManager = FakeProcessManager.list([ attachDebuggerCommand( stdout: @@ -442,6 +505,7 @@ void main() { final IOSDevice device = setUpIOSDevice( processManager: processManager, fileSystem: fileSystem, + analytics: fakeAnalytics, logger: logger, ); final IOSApp iosApp = PrebuiltIOSApp( @@ -464,13 +528,25 @@ void main() { expect(launchResult.started, false); expect(launchResult.hasVmService, false); expect(await device.stopApp(iosApp), false); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.iosDeployLaunchAndAttach.name, + result: 'debugging failed', + ), + ]); }, ); testWithoutContext('IOSDevice.startApp succeeds in release mode', () async { final FileSystem fileSystem = MemoryFileSystem.test(); final processManager = FakeProcessManager.list([kLaunchReleaseCommand]); - final IOSDevice device = setUpIOSDevice(processManager: processManager, fileSystem: fileSystem); + final fakeAnalytics = FakeAnalytics(); + final IOSDevice device = setUpIOSDevice( + processManager: processManager, + fileSystem: fileSystem, + analytics: fakeAnalytics, + ); final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', bundleName: 'Runner', @@ -489,10 +565,18 @@ void main() { expect(launchResult.hasVmService, false); expect(await device.stopApp(iosApp), false); expect(processManager, hasNoRemainingExpectations); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.iosDeployLaunch.name, + result: 'release success', + ), + ]); }); testWithoutContext('IOSDevice.startApp forwards all supported debugging options', () async { final FileSystem fileSystem = MemoryFileSystem.test(); + final fakeAnalytics = FakeAnalytics(); final processManager = FakeProcessManager.list([ FakeCommand( command: [ @@ -546,6 +630,7 @@ void main() { sdkVersion: '13.3', processManager: processManager, fileSystem: fileSystem, + analytics: fakeAnalytics, ); final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', @@ -594,10 +679,18 @@ void main() { expect(launchResult.started, true); expect(await device.stopApp(iosApp), false); expect(processManager, hasNoRemainingExpectations); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.iosDeployLaunchAndAttach.name, + result: 'debugging success', + ), + ]); }); testWithoutContext('startApp using route', () async { final FileSystem fileSystem = MemoryFileSystem.test(); + final fakeAnalytics = FakeAnalytics(); final processManager = FakeProcessManager.list([ FakeCommand( command: [ @@ -633,6 +726,7 @@ void main() { sdkVersion: '13.3', processManager: processManager, fileSystem: fileSystem, + analytics: fakeAnalytics, ); final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', @@ -661,10 +755,18 @@ void main() { expect(launchResult.started, true); expect(await device.stopApp(iosApp), false); expect(processManager, hasNoRemainingExpectations); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.iosDeployLaunchAndAttach.name, + result: 'debugging success', + ), + ]); }); testWithoutContext('startApp using trace-startup', () async { final FileSystem fileSystem = MemoryFileSystem.test(); + final fakeAnalytics = FakeAnalytics(); final processManager = FakeProcessManager.list([ FakeCommand( command: [ @@ -700,6 +802,7 @@ void main() { sdkVersion: '13.3', processManager: processManager, fileSystem: fileSystem, + analytics: fakeAnalytics, ); final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', @@ -727,14 +830,210 @@ void main() { expect(launchResult.started, true); expect(await device.stopApp(iosApp), false); expect(processManager, hasNoRemainingExpectations); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.iosDeployLaunchAndAttach.name, + result: 'debugging success', + ), + ]); }); group('IOSDevice.startApp for CoreDevice', () { group('in debug mode', () { - testUsingContext('succeeds', () async { + testUsingContext( + 'uses LLDB with Xcode 26+', + () async { + final FileSystem fileSystem = MemoryFileSystem.test(); + final processManager = FakeProcessManager.empty(); + final Directory bundleLocation = fileSystem.currentDirectory; + final fakeAnalytics = FakeAnalytics(); + final fakeLauncher = FakeIOSCoreDeviceLauncher(); + final IOSDevice device = setUpIOSDevice( + processManager: processManager, + fileSystem: fileSystem, + isCoreDevice: true, + coreDeviceLauncher: fakeLauncher, + analytics: fakeAnalytics, + ); + final IOSApp iosApp = PrebuiltIOSApp( + projectBundleId: 'app', + bundleName: 'Runner', + uncompressedBundle: bundleLocation, + applicationPackage: bundleLocation, + ); + final deviceLogReader = FakeDeviceLogReader(); + + device.portForwarder = const NoOpDevicePortForwarder(); + device.setLogReader(iosApp, deviceLogReader); + + // Start writing messages to the log reader. + Timer.run(() { + deviceLogReader.addLine('Foo'); + deviceLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:456'); + }); + + final LaunchResult launchResult = await device.startApp( + iosApp, + prebuiltApplication: true, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + platformArgs: {}, + ); + + expect(launchResult.started, true); + expect(fakeLauncher.launchedWithLLDB, true); + expect(fakeLauncher.launchedWithXcode, false); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.coreDeviceWithLLDB.name, + result: 'debugging success', + ), + ]); + }, + overrides: { + Xcode: () => FakeXcode(currentVersion: Version(26, 0, 0)), + Analytics: () => FakeAnalytics(), + }, + ); + + testUsingContext('uses Xcode if LLDB fails', () async { final FileSystem fileSystem = MemoryFileSystem.test(); final processManager = FakeProcessManager.empty(); + final Directory temporaryXcodeProjectDirectory = fileSystem.systemTempDirectory + .childDirectory('flutter_empty_xcode.rand0'); + final Directory bundleLocation = fileSystem.currentDirectory; + final fakeAnalytics = FakeAnalytics(); + final fakeLauncher = FakeIOSCoreDeviceLauncher(lldbLaunchResult: false); + final IOSDevice device = setUpIOSDevice( + processManager: processManager, + fileSystem: fileSystem, + isCoreDevice: true, + coreDeviceControl: FakeIOSCoreDeviceControl(), + xcodeDebug: FakeXcodeDebug( + expectedProject: XcodeDebugProject( + scheme: 'Runner', + xcodeWorkspace: temporaryXcodeProjectDirectory.childDirectory('Runner.xcworkspace'), + xcodeProject: temporaryXcodeProjectDirectory.childDirectory('Runner.xcodeproj'), + hostAppProjectName: 'Runner', + ), + expectedDeviceId: '123', + expectedLaunchArguments: ['--enable-dart-profiling'], + expectedBundlePath: bundleLocation.path, + ), + coreDeviceLauncher: fakeLauncher, + analytics: fakeAnalytics, + ); + final IOSApp iosApp = PrebuiltIOSApp( + projectBundleId: 'app', + bundleName: 'Runner', + uncompressedBundle: bundleLocation, + applicationPackage: bundleLocation, + ); + final deviceLogReader = FakeDeviceLogReader(); + + device.portForwarder = const NoOpDevicePortForwarder(); + device.setLogReader(iosApp, deviceLogReader); + + // Start writing messages to the log reader. + Timer.run(() { + deviceLogReader.addLine('Foo'); + deviceLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:456'); + }); + + final LaunchResult launchResult = await device.startApp( + iosApp, + prebuiltApplication: true, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + platformArgs: {}, + ); + + expect(launchResult.started, true); + expect(fakeLauncher.launchedWithLLDB, true); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.coreDeviceWithLLDB.name, + result: 'launch failed', + ), + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.coreDeviceWithXcodeFallback.name, + result: 'debugging success', + ), + ]); + }, overrides: {Xcode: () => FakeXcode(currentVersion: Version(26, 0, 0))}); + + testUsingContext( + 'uses Xcode if less than Xcode 26', + () async { + final FileSystem fileSystem = MemoryFileSystem.test(); + final processManager = FakeProcessManager.empty(); + final Directory temporaryXcodeProjectDirectory = fileSystem.systemTempDirectory + .childDirectory('flutter_empty_xcode.rand0'); + final Directory bundleLocation = fileSystem.currentDirectory; + final fakeAnalytics = FakeAnalytics(); + final fakeLauncher = FakeIOSCoreDeviceLauncher(); + final IOSDevice device = setUpIOSDevice( + processManager: processManager, + fileSystem: fileSystem, + isCoreDevice: true, + coreDeviceControl: FakeIOSCoreDeviceControl(), + xcodeDebug: FakeXcodeDebug( + expectedProject: XcodeDebugProject( + scheme: 'Runner', + xcodeWorkspace: temporaryXcodeProjectDirectory.childDirectory('Runner.xcworkspace'), + xcodeProject: temporaryXcodeProjectDirectory.childDirectory('Runner.xcodeproj'), + hostAppProjectName: 'Runner', + ), + expectedDeviceId: '123', + expectedLaunchArguments: ['--enable-dart-profiling'], + expectedBundlePath: bundleLocation.path, + ), + coreDeviceLauncher: fakeLauncher, + analytics: fakeAnalytics, + ); + final IOSApp iosApp = PrebuiltIOSApp( + projectBundleId: 'app', + bundleName: 'Runner', + uncompressedBundle: bundleLocation, + applicationPackage: bundleLocation, + ); + final deviceLogReader = FakeDeviceLogReader(); + + device.portForwarder = const NoOpDevicePortForwarder(); + device.setLogReader(iosApp, deviceLogReader); + + // Start writing messages to the log reader. + Timer.run(() { + deviceLogReader.addLine('Foo'); + deviceLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:456'); + }); + final LaunchResult launchResult = await device.startApp( + iosApp, + prebuiltApplication: true, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + platformArgs: {}, + ); + + expect(launchResult.started, true); + expect(fakeLauncher.launchedWithLLDB, false); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.coreDeviceWithXcode.name, + result: 'debugging success', + ), + ]); + }, + overrides: {Xcode: () => FakeXcode(currentVersion: Version(16, 0, 0))}, + ); + + testUsingContext('succeeds', () async { + final FileSystem fileSystem = MemoryFileSystem.test(); + final processManager = FakeProcessManager.empty(); + final fakeAnalytics = FakeAnalytics(); final Directory temporaryXcodeProjectDirectory = fileSystem.systemTempDirectory .childDirectory('flutter_empty_xcode.rand0'); final Directory bundleLocation = fileSystem.currentDirectory; @@ -754,6 +1053,7 @@ void main() { expectedLaunchArguments: ['--enable-dart-profiling'], expectedBundlePath: bundleLocation.path, ), + analytics: fakeAnalytics, ); final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', @@ -780,6 +1080,13 @@ void main() { ); expect(launchResult.started, true); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.coreDeviceWithXcode.name, + result: 'debugging success', + ), + ]); }); testUsingContext('prints warning message if it takes too long to start debugging', () async { @@ -839,7 +1146,8 @@ void main() { expect( logger.errorText, contains( - 'Xcode is taking longer than expected to start debugging the app. Ensure the project is opened in Xcode.', + 'Xcode is taking longer than expected to start debugging the app. ' + 'If the issue persists, try closing Xcode and re-running your Flutter command.', ), ); completer.complete(); @@ -853,6 +1161,7 @@ void main() { final Directory temporaryXcodeProjectDirectory = fileSystem.systemTempDirectory .childDirectory('flutter_empty_xcode.rand0'); final Directory bundleLocation = fileSystem.currentDirectory; + final fakeAnalytics = FakeAnalytics(); final IOSDevice device = setUpIOSDevice( processManager: processManager, fileSystem: fileSystem, @@ -869,6 +1178,7 @@ void main() { expectedLaunchArguments: ['--enable-dart-profiling'], expectedBundlePath: bundleLocation.path, ), + analytics: fakeAnalytics, ); final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', @@ -899,6 +1209,13 @@ void main() { expect(launchResult.started, true); expect(shutDownHooks.hooks.length, 1); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.coreDeviceWithXcode.name, + result: 'debugging success', + ), + ]); }); testUsingContext( @@ -910,10 +1227,12 @@ void main() { final Directory temporaryXcodeProjectDirectory = fileSystem.systemTempDirectory .childDirectory('flutter_empty_xcode.rand0'); final Directory bundleLocation = fileSystem.currentDirectory; + final fakeAnalytics = FakeAnalytics(); final IOSDevice device = setUpIOSDevice( processManager: processManager, fileSystem: fileSystem, isCoreDevice: true, + analytics: fakeAnalytics, coreDeviceControl: FakeIOSCoreDeviceControl(), xcodeDebug: FakeXcodeDebug( expectedProject: XcodeDebugProject( @@ -948,6 +1267,13 @@ void main() { expect(launchResult.started, true); expect(launchResult.hasVmService, true); expect(await device.stopApp(iosApp), true); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.coreDeviceWithXcode.name, + result: 'debugging success', + ), + ]); }, // If mDNS is not the only method of discovery, it shouldn't throw on error. overrides: { @@ -965,7 +1291,7 @@ void main() { testUsingContext('when mDNS fails', () async { final FileSystem fileSystem = MemoryFileSystem.test(); final processManager = FakeProcessManager.empty(); - + final fakeAnalytics = FakeAnalytics(); final Directory temporaryXcodeProjectDirectory = fileSystem.systemTempDirectory .childDirectory('flutter_empty_xcode.rand0'); final Directory bundleLocation = fileSystem.currentDirectory; @@ -985,6 +1311,7 @@ void main() { expectedLaunchArguments: ['--enable-dart-profiling'], expectedBundlePath: bundleLocation.path, ), + analytics: fakeAnalytics, ); final IOSApp iosApp = PrebuiltIOSApp( projectBundleId: 'app', @@ -1017,6 +1344,13 @@ void main() { expect(launchResult.started, true); expect(launchResult.hasVmService, true); expect(await device.stopApp(iosApp), true); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.coreDeviceWithXcode.name, + result: 'debugging success', + ), + ]); }, overrides: {MDnsVmServiceDiscovery: () => mdnsDiscovery}); }); @@ -1032,6 +1366,7 @@ void main() { final Directory temporaryXcodeProjectDirectory = fileSystem.systemTempDirectory .childDirectory('flutter_empty_xcode.rand0'); final Directory bundleLocation = fileSystem.currentDirectory; + final fakeAnalytics = FakeAnalytics(); final IOSDevice device = setUpIOSDevice( processManager: processManager, fileSystem: fileSystem, @@ -1052,6 +1387,7 @@ void main() { operatingSystem: 'macos', environment: {'HOME': pathToHome}, ), + analytics: fakeAnalytics, ); final IOSApp iosApp = PrebuiltIOSApp( @@ -1091,6 +1427,13 @@ void main() { .existsSync(), true, ); + expect(fakeAnalytics.sentEvents, [ + Event.appleUsageEvent( + workflow: 'ios-physical-deployment', + parameter: IOSDeploymentMethod.coreDeviceWithXcode.name, + result: 'debugging failed', + ), + ]); completer.complete(); }); time.elapse(const Duration(minutes: 15)); @@ -1184,6 +1527,8 @@ IOSDevice setUpIOSDevice({ DeviceConnectionInterface interfaceType = DeviceConnectionInterface.attached, bool isCoreDevice = false, IOSCoreDeviceControl? coreDeviceControl, + IOSCoreDeviceLauncher? coreDeviceLauncher, + Analytics? analytics, FakeXcodeDebug? xcodeDebug, FakePlatform? platform, }) { @@ -1214,6 +1559,7 @@ IOSDevice setUpIOSDevice({ artifacts: artifacts, cache: cache, ), + analytics: analytics ?? FakeAnalytics(), iMobileDevice: IMobileDevice( logger: logger, processManager: processManager ?? FakeProcessManager.any(), @@ -1221,7 +1567,7 @@ IOSDevice setUpIOSDevice({ cache: cache, ), coreDeviceControl: coreDeviceControl ?? FakeIOSCoreDeviceControl(), - coreDeviceLauncher: FakeIOSCoreDeviceLauncher(), + coreDeviceLauncher: coreDeviceLauncher ?? FakeIOSCoreDeviceLauncher(), xcodeDebug: xcodeDebug ?? FakeXcodeDebug(), cpuArchitecture: DarwinArch.arm64, connectionInterface: interfaceType, @@ -1349,4 +1695,61 @@ class FakeShutDownHooks extends Fake implements ShutdownHooks { } } -class FakeIOSCoreDeviceLauncher extends Fake implements IOSCoreDeviceLauncher {} +class FakeXcode extends Fake implements Xcode { + FakeXcode({this.currentVersion}); + + @override + Version? currentVersion; +} + +class FakeIOSCoreDeviceLauncher extends Fake implements IOSCoreDeviceLauncher { + FakeIOSCoreDeviceLauncher({this.lldbLaunchResult = true, this.xcodeLaunchResult = true}); + bool lldbLaunchResult; + bool xcodeLaunchResult; + var launchedWithLLDB = false; + var launchedWithXcode = false; + + Completer? xcodeCompleter; + + @override + Future launchAppWithLLDBDebugger({ + required String deviceId, + required String bundlePath, + required String bundleId, + required List launchArguments, + }) async { + launchedWithLLDB = true; + return lldbLaunchResult; + } + + @override + Future launchAppWithXcodeDebugger({ + required String deviceId, + required DebuggingOptions debuggingOptions, + required IOSApp package, + required List launchArguments, + required TemplateRenderer templateRenderer, + String? mainPath, + Duration? discoveryTimeout, + }) async { + if (xcodeCompleter != null) { + await xcodeCompleter!.future; + } + launchedWithXcode = true; + return xcodeLaunchResult; + } + + @override + Future stopApp({required String deviceId, int? processId}) async { + return false; + } +} + +class FakeAnalytics extends Fake implements Analytics { + final sentEvents = []; + + @override + void send(Event event) { + sentEvents.add(event); + } +} diff --git a/packages/flutter_tools/test/general.shard/ios/lldb_test.dart b/packages/flutter_tools/test/general.shard/ios/lldb_test.dart index e94da55b1b8f4..3756f58d6ded6 100644 --- a/packages/flutter_tools/test/general.shard/ios/lldb_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/lldb_test.dart @@ -43,7 +43,7 @@ void main() { expect(lldb.isRunning, isFalse); expect(lldb.appProcessId, isNull); expect(processManager.hasRemainingExpectations, isFalse); - expect(logger.errorText, contains('Process exception running lldb')); + expect(logger.traceText, contains('Process exception running lldb')); }); testWithoutContext('attachAndStart returns true on success', () async { @@ -174,7 +174,7 @@ Target 0: (Runner) stopped. expect(lldb.appProcessId, isNull); expect(expectedInputs, isEmpty); expect(processManager.hasRemainingExpectations, isFalse); - expect(logger.errorText, contains(errorText)); + expect(logger.traceText, contains(errorText)); }); testWithoutContext('attachAndStart returns false when stderr not during log waiter', () async { @@ -220,7 +220,7 @@ Target 0: (Runner) stopped. expect(lldb.appProcessId, isNull); expect(expectedInputs, isEmpty); expect(processManager.hasRemainingExpectations, isFalse); - expect(logger.errorText, contains(errorText)); + expect(logger.traceText, contains(errorText)); }); testWithoutContext('attachAndStart prints warning if takes too long', () async { diff --git a/packages/flutter_tools/test/src/fakes.dart b/packages/flutter_tools/test/src/fakes.dart index 56af2b901a0d1..ba3198b7da87c 100644 --- a/packages/flutter_tools/test/src/fakes.dart +++ b/packages/flutter_tools/test/src/fakes.dart @@ -503,6 +503,7 @@ class TestFeatureFlags implements FeatureFlags { this.isSwiftPackageManagerEnabled = false, this.isOmitLegacyVersionFileEnabled = false, this.isWindowingEnabled = false, + this.isLLDBDebuggingEnabled = false, }); @override @@ -544,6 +545,9 @@ class TestFeatureFlags implements FeatureFlags { @override final bool isWindowingEnabled; + @override + final bool isLLDBDebuggingEnabled; + @override bool isEnabled(Feature feature) { return switch (feature) { @@ -557,8 +561,10 @@ class TestFeatureFlags implements FeatureFlags { flutterCustomDevicesFeature => areCustomDevicesEnabled, cliAnimation => isCliAnimationEnabled, nativeAssets => isNativeAssetsEnabled, + swiftPackageManager => isSwiftPackageManagerEnabled, omitLegacyVersionFile => isOmitLegacyVersionFileEnabled, windowingFeature => isWindowingEnabled, + lldbDebugging => isLLDBDebuggingEnabled, _ => false, }; } @@ -578,6 +584,7 @@ class TestFeatureFlags implements FeatureFlags { swiftPackageManager, omitLegacyVersionFile, windowingFeature, + lldbDebugging, ]; @override