diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart index eaac1ade1a39f..cbb40ea451b26 100644 --- a/packages/flutter_tools/lib/src/commands/test.dart +++ b/packages/flutter_tools/lib/src/commands/test.dart @@ -295,6 +295,13 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { 'and this flag can be used to override the default. To disable this for the ' 'skwasm renderer, use "--no-cross-origin-isolation".', hide: !verboseHelp, + ) + ..addFlag( + 'uninstall', + defaultsTo: true, + help: + 'Whether to uninstall the app after running integration tests. ' + 'Set "--no-uninstall" to keep the app installed on the device.', ); addDdsOptions(verboseHelp: verboseHelp); @@ -477,6 +484,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts { : null, printDtd: boolArg(FlutterGlobalOptions.kPrintDtd, global: true), webUseWasm: useWasm, + uninstallApp: boolArg('uninstall'), ); final Uri? nativeAssetsJson = _isIntegrationTest diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index f67e69b67c86d..bd93029e3229d 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -982,6 +982,7 @@ class DebuggingOptions { this.enableFlutterGpu = false, this.enableVulkanValidation = false, this.uninstallFirst = false, + this.uninstallApp = true, this.enableDartProfiling = true, this.profileStartup = false, this.enableEmbedderApi = false, @@ -1016,6 +1017,7 @@ class DebuggingOptions { this.enableFlutterGpu = false, this.enableVulkanValidation = false, this.uninstallFirst = false, + this.uninstallApp = true, this.enableDartProfiling = true, this.profileStartup = false, this.enableEmbedderApi = false, @@ -1099,6 +1101,7 @@ class DebuggingOptions { required this.enableFlutterGpu, required this.enableVulkanValidation, required this.uninstallFirst, + required this.uninstallApp, required this.enableDartProfiling, required this.profileStartup, required this.enableEmbedderApi, @@ -1162,6 +1165,12 @@ class DebuggingOptions { /// This is not implemented for every platform. final bool uninstallFirst; + /// Whether the tool should uninstall the app after running. + /// + /// This is currently only implemented for integration tests. + /// Defaults to true. + final bool uninstallApp; + /// Whether to run the browser in headless mode. /// /// Some CI environments do not provide a display and fail to launch the @@ -1295,6 +1304,7 @@ class DebuggingOptions { 'enableImpeller': enableImpeller.asBool, 'enableFlutterGpu': enableFlutterGpu, 'enableVulkanValidation': enableVulkanValidation, + 'uninstallApp': uninstallApp, 'enableDartProfiling': enableDartProfiling, 'profileStartup': profileStartup, 'enableEmbedderApi': enableEmbedderApi, @@ -1364,6 +1374,7 @@ class DebuggingOptions { enableFlutterGpu: json['enableFlutterGpu']! as bool, enableVulkanValidation: (json['enableVulkanValidation'] as bool?) ?? false, uninstallFirst: (json['uninstallFirst'] as bool?) ?? false, + uninstallApp: (json['uninstallApp'] as bool?) ?? true, enableDartProfiling: (json['enableDartProfiling'] as bool?) ?? true, profileStartup: (json['profileStartup'] as bool?) ?? false, enableEmbedderApi: (json['enableEmbedderApi'] as bool?) ?? false, diff --git a/packages/flutter_tools/lib/src/test/integration_test_device.dart b/packages/flutter_tools/lib/src/test/integration_test_device.dart index 0c6d0b6722075..d4e6ba2cc763b 100644 --- a/packages/flutter_tools/lib/src/test/integration_test_device.dart +++ b/packages/flutter_tools/lib/src/test/integration_test_device.dart @@ -141,8 +141,10 @@ class IntegrationTestTestDevice implements TestDevice { if (!await device.stopApp(applicationPackage, userIdentifier: userIdentifier)) { globals.printTrace('Could not stop the Integration Test app.'); } - if (!await device.uninstallApp(applicationPackage, userIdentifier: userIdentifier)) { - globals.printTrace('Could not uninstall the Integration Test app.'); + if (debuggingOptions.uninstallApp) { + if (!await device.uninstallApp(applicationPackage, userIdentifier: userIdentifier)) { + globals.printTrace('Could not uninstall the Integration Test app.'); + } } } diff --git a/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart index b9a77ed8d30ae..fd347f51c171a 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart @@ -1522,6 +1522,40 @@ dev_dependencies: }, ); + testUsingContext( + 'uninstallApp defaults to true', + () async { + final testRunner = FakeFlutterTestRunner(0); + + final testCommand = TestCommand(testRunner: testRunner); + final CommandRunner commandRunner = createTestCommandRunner(testCommand); + + await commandRunner.run(const ['test', '--no-pub']); + expect(testRunner.lastDebuggingOptionsValue.uninstallApp, true); + }, + overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + }, + ); + + testUsingContext( + '--no-uninstall sets uninstallApp to false', + () async { + final testRunner = FakeFlutterTestRunner(0); + + final testCommand = TestCommand(testRunner: testRunner); + final CommandRunner commandRunner = createTestCommandRunner(testCommand); + + await commandRunner.run(const ['test', '--no-pub', '--no-uninstall']); + expect(testRunner.lastDebuggingOptionsValue.uninstallApp, false); + }, + overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + }, + ); + testUsingContext( 'Passes web renderer into debugging options', () async { diff --git a/packages/flutter_tools/test/general.shard/integration_test_device_test.dart b/packages/flutter_tools/test/general.shard/integration_test_device_test.dart index 422d1e6db82f2..86ed35649a211 100644 --- a/packages/flutter_tools/test/general.shard/integration_test_device_test.dart +++ b/packages/flutter_tools/test/general.shard/integration_test_device_test.dart @@ -239,6 +239,76 @@ void main() { }, ); + testUsingContext( + 'kill() calls uninstallApp when uninstallApp is true', + () async { + final trackingDevice = FakeDeviceTrackingUninstall(); + final testDeviceWithUninstall = IntegrationTestTestDevice( + id: 1, + device: trackingDevice, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + userIdentifier: '', + compileExpression: null, + ); + + await testDeviceWithUninstall.start('entrypointPath'); + await testDeviceWithUninstall.kill(); + + expect(trackingDevice.uninstallAppCalled, isTrue); + expect(testDeviceWithUninstall.finished, completes); + }, + overrides: { + ApplicationPackageFactory: () => FakeApplicationPackageFactory(), + VMServiceConnector: () => + ( + Uri httpUri, { + ReloadSources? reloadSources, + Restart? restart, + CompileExpression? compileExpression, + FlutterProject? flutterProject, + PrintStructuredErrorLogMethod? printStructuredErrorLogMethod, + io.CompressionOptions? compression, + Device? device, + Logger? logger, + }) async => fakeVmServiceHost.vmService, + }, + ); + + testUsingContext( + 'kill() does not call uninstallApp when uninstallApp is false', + () async { + final trackingDevice = FakeDeviceTrackingUninstall(); + final testDeviceWithoutUninstall = IntegrationTestTestDevice( + id: 1, + device: trackingDevice, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug, uninstallApp: false), + userIdentifier: '', + compileExpression: null, + ); + + await testDeviceWithoutUninstall.start('entrypointPath'); + await testDeviceWithoutUninstall.kill(); + + expect(trackingDevice.uninstallAppCalled, isFalse); + expect(testDeviceWithoutUninstall.finished, completes); + }, + overrides: { + ApplicationPackageFactory: () => FakeApplicationPackageFactory(), + VMServiceConnector: () => + ( + Uri httpUri, { + ReloadSources? reloadSources, + Restart? restart, + CompileExpression? compileExpression, + FlutterProject? flutterProject, + PrintStructuredErrorLogMethod? printStructuredErrorLogMethod, + io.CompressionOptions? compression, + Device? device, + Logger? logger, + }) async => fakeVmServiceHost.vmService, + }, + ); + testUsingContext( 'Can handle closing of the VM service', () async { @@ -274,3 +344,21 @@ class FakeApplicationPackageFactory extends Fake implements ApplicationPackageFa } class FakeApplicationPackage extends Fake implements ApplicationPackage {} + +class FakeDeviceTrackingUninstall extends FakeDevice { + FakeDeviceTrackingUninstall() + : super( + 'ephemeral', + 'ephemeral', + type: PlatformType.android, + launchResult: LaunchResult.succeeded(vmServiceUri: vmServiceUri), + ); + + bool uninstallAppCalled = false; + + @override + Future uninstallApp(ApplicationPackage app, {String? userIdentifier}) async { + uninstallAppCalled = true; + return true; + } +}