diff --git a/.ci.yaml b/.ci.yaml index dcd1a44a7c..7c6659eaf2 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -3690,6 +3690,15 @@ targets: ["devicelab", "ios", "mac"] task_name: hot_mode_dev_cycle_ios__benchmark + - name: Mac_ios hot_mode_dev_cycle_ios_simulator + recipe: devicelab/devicelab_drone + bringup: true + timeout: 60 + properties: + tags: > + ["devicelab", "ios", "mac"] + task_name: hot_mode_dev_cycle_ios_simulator + - name: Mac_ios fullscreen_textfield_perf_ios__e2e_summary recipe: devicelab/devicelab_drone presubmit: false diff --git a/TESTOWNERS b/TESTOWNERS index 9ae50083fc..9bc17667ca 100644 --- a/TESTOWNERS +++ b/TESTOWNERS @@ -168,6 +168,7 @@ /dev/devicelab/bin/tasks/fullscreen_textfield_perf_impeller_ios__e2e_summary.dart @zanderso @flutter/engine /dev/devicelab/bin/tasks/fullscreen_textfield_perf_ios__e2e_summary.dart @zanderso @flutter/engine /dev/devicelab/bin/tasks/hello_world_ios__compile.dart @zanderso @flutter/engine +/dev/devicelab/bin/tasks/hot_mode_dev_cycle_ios_simulator.dart @jmagman @flutter/tool /dev/devicelab/bin/tasks/hot_mode_dev_cycle_ios__benchmark.dart @zanderso @flutter/tool /dev/devicelab/bin/tasks/hot_mode_dev_cycle_macos_target__benchmark.dart @zanderso @flutter/tool /dev/devicelab/bin/tasks/imagefiltered_transform_animation_perf_impeller_ios__timeline_summary.dart @zanderso @flutter/engine diff --git a/dev/devicelab/bin/tasks/hot_mode_dev_cycle_ios_simulator.dart b/dev/devicelab/bin/tasks/hot_mode_dev_cycle_ios_simulator.dart new file mode 100644 index 0000000000..d41e414f62 --- /dev/null +++ b/dev/devicelab/bin/tasks/hot_mode_dev_cycle_ios_simulator.dart @@ -0,0 +1,27 @@ +// 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/ios.dart'; +import 'package:flutter_devicelab/framework/task_result.dart'; +import 'package:flutter_devicelab/tasks/hot_mode_tests.dart'; + +Future main() async { + await task(() async { + deviceOperatingSystem = DeviceOperatingSystem.ios; + String? simulatorDeviceId; + try { + await testWithNewIOSSimulator('TestHotReloadSim', (String deviceId) async { + simulatorDeviceId = deviceId; + // This isn't actually a benchmark test, so do not use the returned `benchmarkScoreKeys` result. + await createHotModeTest(deviceIdOverride: deviceId, checkAppRunningOnLocalDevice: true)(); + }); + } finally { + await removeIOSimulator(simulatorDeviceId); + } + + return TaskResult.success(null); + }); +} diff --git a/dev/devicelab/bin/tasks/hot_mode_dev_cycle_linux_target__benchmark.dart b/dev/devicelab/bin/tasks/hot_mode_dev_cycle_linux_target__benchmark.dart index 8c49bea1c6..41c0d4217b 100644 --- a/dev/devicelab/bin/tasks/hot_mode_dev_cycle_linux_target__benchmark.dart +++ b/dev/devicelab/bin/tasks/hot_mode_dev_cycle_linux_target__benchmark.dart @@ -6,5 +6,5 @@ import 'package:flutter_devicelab/framework/framework.dart'; import 'package:flutter_devicelab/tasks/hot_mode_tests.dart'; Future main() async { - await task(createHotModeTest(deviceIdOverride: 'linux')); + await task(createHotModeTest(deviceIdOverride: 'linux', checkAppRunningOnLocalDevice: true)); } diff --git a/dev/devicelab/lib/tasks/hot_mode_tests.dart b/dev/devicelab/lib/tasks/hot_mode_tests.dart index 65c2f8e324..2123f71588 100644 --- a/dev/devicelab/lib/tasks/hot_mode_tests.dart +++ b/dev/devicelab/lib/tasks/hot_mode_tests.dart @@ -7,9 +7,11 @@ import 'dart:convert'; import 'dart:io'; import 'package:path/path.dart' as path; +import 'package:process/process.dart'; import '../framework/devices.dart'; import '../framework/framework.dart'; +import '../framework/running_processes.dart'; import '../framework/task_result.dart'; import '../framework/utils.dart'; @@ -18,7 +20,11 @@ final Directory flutterGalleryDir = dir(path.join(flutterDirectory.path, 'dev/in const String kSourceLine = 'fontSize: (orientation == Orientation.portrait) ? 32.0 : 24.0'; const String kReplacementLine = 'fontSize: (orientation == Orientation.portrait) ? 34.0 : 24.0'; -TaskFunction createHotModeTest({String? deviceIdOverride, Map? environment}) { +TaskFunction createHotModeTest({ + String? deviceIdOverride, + Map? environment, + bool checkAppRunningOnLocalDevice = false, +}) { // This file is modified during the test and needs to be restored at the end. final File flutterFrameworkSource = file(path.join( flutterDirectory.path, 'packages/flutter/lib/src/widgets/framework.dart', @@ -96,7 +102,7 @@ TaskFunction createHotModeTest({String? deviceIdOverride, Map? e } }); - largeReloadData = await captureReloadData(options, environment, benchmarkFile, (String line, Process process) { + largeReloadData = await captureReloadData(options, environment, benchmarkFile, (String line, Process process) async { if (!line.contains('Reloaded ')) { return; } @@ -108,6 +114,9 @@ TaskFunction createHotModeTest({String? deviceIdOverride, Map? e process.stdin.writeln('r'); hotReloadCount += 1; } else { + if (checkAppRunningOnLocalDevice) { + await _checkAppRunning(true); + } process.stdin.writeln('q'); } }); @@ -150,6 +159,9 @@ TaskFunction createHotModeTest({String? deviceIdOverride, Map? e json.decode(benchmarkFile.readAsStringSync()) as Map; } }); + if (checkAppRunningOnLocalDevice) { + await _checkAppRunning(false); + } } finally { flutterFrameworkSource.writeAsStringSync(oldContents); } @@ -257,3 +269,23 @@ Future> captureReloadData( benchmarkFile.deleteSync(); return result; } + +Future _checkAppRunning(bool shouldBeRunning) async { + late Set galleryProcesses; + for (int i = 0; i < 10; i++) { + final String exe = Platform.isWindows ? '.exe' : ''; + galleryProcesses = await getRunningProcesses( + processName: 'Flutter Gallery$exe', + processManager: const LocalProcessManager(), + ); + + if (galleryProcesses.isNotEmpty == shouldBeRunning) { + return; + } + + // Give the app time to shut down. + sleep(const Duration(seconds: 1)); + } + print(galleryProcesses.join('\n')); + throw TaskResult.failure('Flutter Gallery app is ${shouldBeRunning ? 'not' : 'still'} running'); +} diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart index f789f5a02e..4cdb2c18a3 100644 --- a/packages/flutter_tools/lib/src/ios/simulators.dart +++ b/packages/flutter_tools/lib/src/ios/simulators.dart @@ -257,6 +257,25 @@ class SimControl { return result; } + Future stopApp(String deviceId, String appIdentifier) async { + RunResult result; + try { + result = await _processUtils.run( + [ + ..._xcode.xcrunCommand(), + 'simctl', + 'terminate', + deviceId, + appIdentifier, + ], + throwOnError: true, + ); + } on ProcessException catch (exception) { + throwToolExit('Unable to terminate $appIdentifier on $deviceId:\n$exception'); + } + return result; + } + Future takeScreenshot(String deviceId, String outputPath) async { try { await _processUtils.run( @@ -536,8 +555,7 @@ class IOSSimulator extends Device { ApplicationPackage app, { String? userIdentifier, }) async { - // Currently we don't have a way to stop an app running on iOS. - return false; + return (await _simControl.stopApp(id, app.id)).exitCode == 0; } String get logFilePath { diff --git a/packages/flutter_tools/test/general.shard/ios/simulators_test.dart b/packages/flutter_tools/test/general.shard/ios/simulators_test.dart index 45f42816d8..ab58347ad3 100644 --- a/packages/flutter_tools/test/general.shard/ios/simulators_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/simulators_test.dart @@ -901,6 +901,24 @@ Dec 20 17:04:32 md32-11-vm1 Another App[88374]: Ignore this text''' throwsToolExit(message: r'Unable to launch'), ); }); + + testWithoutContext('.stopApp() handles exceptions', () async { + fakeProcessManager.addCommand(const FakeCommand( + command: [ + 'xcrun', + 'simctl', + 'terminate', + deviceId, + appId, + ], + exception: ProcessException('xcrun', []), + )); + + expect( + () async => simControl.stopApp(deviceId, appId), + throwsToolExit(message: r'Unable to terminate'), + ); + }); }); group('startApp', () {