Skip to content

Commit 04db01f

Browse files
authored
feat: Add --no-uninstall flag to flutter test for integration tests (#182714)
## Description Add `--no-uninstall` flag to `flutter test` to skip app uninstallation after integration tests complete. When running integration tests on Android (`flutter test integration_test/`), the `IntegrationTestTestDevice.kill()` method always calls `device.uninstallApp()` after tests finish. For Device Policy Manager (DPM) apps, Android prevents uninstallation, causing the error: ```bash adb uninstall failed: Failure [DELETE_FAILED_DEVICE_POLICY_MANAGER] ``` This PR adds a `--uninstall` flag (defaulting to true) to `flutter test`, so users can pass `--no-uninstall` to skip the post-test uninstallation step: ```bash flutter test integration_test/ --no-uninstall ``` Fixes #166709 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing.
1 parent 692b517 commit 04db01f

5 files changed

Lines changed: 145 additions & 2 deletions

File tree

‎packages/flutter_tools/lib/src/commands/test.dart‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,13 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
295295
'and this flag can be used to override the default. To disable this for the '
296296
'skwasm renderer, use "--no-cross-origin-isolation".',
297297
hide: !verboseHelp,
298+
)
299+
..addFlag(
300+
'uninstall',
301+
defaultsTo: true,
302+
help:
303+
'Whether to uninstall the app after running integration tests. '
304+
'Set "--no-uninstall" to keep the app installed on the device.',
298305
);
299306

300307
addDdsOptions(verboseHelp: verboseHelp);
@@ -477,6 +484,7 @@ class TestCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
477484
: null,
478485
printDtd: boolArg(FlutterGlobalOptions.kPrintDtd, global: true),
479486
webUseWasm: useWasm,
487+
uninstallApp: boolArg('uninstall'),
480488
);
481489

482490
final Uri? nativeAssetsJson = _isIntegrationTest

‎packages/flutter_tools/lib/src/device.dart‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -982,6 +982,7 @@ class DebuggingOptions {
982982
this.enableFlutterGpu = false,
983983
this.enableVulkanValidation = false,
984984
this.uninstallFirst = false,
985+
this.uninstallApp = true,
985986
this.enableDartProfiling = true,
986987
this.profileStartup = false,
987988
this.enableEmbedderApi = false,
@@ -1016,6 +1017,7 @@ class DebuggingOptions {
10161017
this.enableFlutterGpu = false,
10171018
this.enableVulkanValidation = false,
10181019
this.uninstallFirst = false,
1020+
this.uninstallApp = true,
10191021
this.enableDartProfiling = true,
10201022
this.profileStartup = false,
10211023
this.enableEmbedderApi = false,
@@ -1099,6 +1101,7 @@ class DebuggingOptions {
10991101
required this.enableFlutterGpu,
11001102
required this.enableVulkanValidation,
11011103
required this.uninstallFirst,
1104+
required this.uninstallApp,
11021105
required this.enableDartProfiling,
11031106
required this.profileStartup,
11041107
required this.enableEmbedderApi,
@@ -1162,6 +1165,12 @@ class DebuggingOptions {
11621165
/// This is not implemented for every platform.
11631166
final bool uninstallFirst;
11641167

1168+
/// Whether the tool should uninstall the app after running.
1169+
///
1170+
/// This is currently only implemented for integration tests.
1171+
/// Defaults to true.
1172+
final bool uninstallApp;
1173+
11651174
/// Whether to run the browser in headless mode.
11661175
///
11671176
/// Some CI environments do not provide a display and fail to launch the
@@ -1295,6 +1304,7 @@ class DebuggingOptions {
12951304
'enableImpeller': enableImpeller.asBool,
12961305
'enableFlutterGpu': enableFlutterGpu,
12971306
'enableVulkanValidation': enableVulkanValidation,
1307+
'uninstallApp': uninstallApp,
12981308
'enableDartProfiling': enableDartProfiling,
12991309
'profileStartup': profileStartup,
13001310
'enableEmbedderApi': enableEmbedderApi,
@@ -1364,6 +1374,7 @@ class DebuggingOptions {
13641374
enableFlutterGpu: json['enableFlutterGpu']! as bool,
13651375
enableVulkanValidation: (json['enableVulkanValidation'] as bool?) ?? false,
13661376
uninstallFirst: (json['uninstallFirst'] as bool?) ?? false,
1377+
uninstallApp: (json['uninstallApp'] as bool?) ?? true,
13671378
enableDartProfiling: (json['enableDartProfiling'] as bool?) ?? true,
13681379
profileStartup: (json['profileStartup'] as bool?) ?? false,
13691380
enableEmbedderApi: (json['enableEmbedderApi'] as bool?) ?? false,

‎packages/flutter_tools/lib/src/test/integration_test_device.dart‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,10 @@ class IntegrationTestTestDevice implements TestDevice {
141141
if (!await device.stopApp(applicationPackage, userIdentifier: userIdentifier)) {
142142
globals.printTrace('Could not stop the Integration Test app.');
143143
}
144-
if (!await device.uninstallApp(applicationPackage, userIdentifier: userIdentifier)) {
145-
globals.printTrace('Could not uninstall the Integration Test app.');
144+
if (debuggingOptions.uninstallApp) {
145+
if (!await device.uninstallApp(applicationPackage, userIdentifier: userIdentifier)) {
146+
globals.printTrace('Could not uninstall the Integration Test app.');
147+
}
146148
}
147149
}
148150

‎packages/flutter_tools/test/commands.shard/hermetic/test_test.dart‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1522,6 +1522,40 @@ dev_dependencies:
15221522
},
15231523
);
15241524

1525+
testUsingContext(
1526+
'uninstallApp defaults to true',
1527+
() async {
1528+
final testRunner = FakeFlutterTestRunner(0);
1529+
1530+
final testCommand = TestCommand(testRunner: testRunner);
1531+
final CommandRunner<void> commandRunner = createTestCommandRunner(testCommand);
1532+
1533+
await commandRunner.run(const <String>['test', '--no-pub']);
1534+
expect(testRunner.lastDebuggingOptionsValue.uninstallApp, true);
1535+
},
1536+
overrides: <Type, Generator>{
1537+
FileSystem: () => fs,
1538+
ProcessManager: () => FakeProcessManager.any(),
1539+
},
1540+
);
1541+
1542+
testUsingContext(
1543+
'--no-uninstall sets uninstallApp to false',
1544+
() async {
1545+
final testRunner = FakeFlutterTestRunner(0);
1546+
1547+
final testCommand = TestCommand(testRunner: testRunner);
1548+
final CommandRunner<void> commandRunner = createTestCommandRunner(testCommand);
1549+
1550+
await commandRunner.run(const <String>['test', '--no-pub', '--no-uninstall']);
1551+
expect(testRunner.lastDebuggingOptionsValue.uninstallApp, false);
1552+
},
1553+
overrides: <Type, Generator>{
1554+
FileSystem: () => fs,
1555+
ProcessManager: () => FakeProcessManager.any(),
1556+
},
1557+
);
1558+
15251559
testUsingContext(
15261560
'Passes web renderer into debugging options',
15271561
() async {

‎packages/flutter_tools/test/general.shard/integration_test_device_test.dart‎

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,76 @@ void main() {
239239
},
240240
);
241241

242+
testUsingContext(
243+
'kill() calls uninstallApp when uninstallApp is true',
244+
() async {
245+
final trackingDevice = FakeDeviceTrackingUninstall();
246+
final testDeviceWithUninstall = IntegrationTestTestDevice(
247+
id: 1,
248+
device: trackingDevice,
249+
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
250+
userIdentifier: '',
251+
compileExpression: null,
252+
);
253+
254+
await testDeviceWithUninstall.start('entrypointPath');
255+
await testDeviceWithUninstall.kill();
256+
257+
expect(trackingDevice.uninstallAppCalled, isTrue);
258+
expect(testDeviceWithUninstall.finished, completes);
259+
},
260+
overrides: <Type, Generator>{
261+
ApplicationPackageFactory: () => FakeApplicationPackageFactory(),
262+
VMServiceConnector: () =>
263+
(
264+
Uri httpUri, {
265+
ReloadSources? reloadSources,
266+
Restart? restart,
267+
CompileExpression? compileExpression,
268+
FlutterProject? flutterProject,
269+
PrintStructuredErrorLogMethod? printStructuredErrorLogMethod,
270+
io.CompressionOptions? compression,
271+
Device? device,
272+
Logger? logger,
273+
}) async => fakeVmServiceHost.vmService,
274+
},
275+
);
276+
277+
testUsingContext(
278+
'kill() does not call uninstallApp when uninstallApp is false',
279+
() async {
280+
final trackingDevice = FakeDeviceTrackingUninstall();
281+
final testDeviceWithoutUninstall = IntegrationTestTestDevice(
282+
id: 1,
283+
device: trackingDevice,
284+
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug, uninstallApp: false),
285+
userIdentifier: '',
286+
compileExpression: null,
287+
);
288+
289+
await testDeviceWithoutUninstall.start('entrypointPath');
290+
await testDeviceWithoutUninstall.kill();
291+
292+
expect(trackingDevice.uninstallAppCalled, isFalse);
293+
expect(testDeviceWithoutUninstall.finished, completes);
294+
},
295+
overrides: <Type, Generator>{
296+
ApplicationPackageFactory: () => FakeApplicationPackageFactory(),
297+
VMServiceConnector: () =>
298+
(
299+
Uri httpUri, {
300+
ReloadSources? reloadSources,
301+
Restart? restart,
302+
CompileExpression? compileExpression,
303+
FlutterProject? flutterProject,
304+
PrintStructuredErrorLogMethod? printStructuredErrorLogMethod,
305+
io.CompressionOptions? compression,
306+
Device? device,
307+
Logger? logger,
308+
}) async => fakeVmServiceHost.vmService,
309+
},
310+
);
311+
242312
testUsingContext(
243313
'Can handle closing of the VM service',
244314
() async {
@@ -274,3 +344,21 @@ class FakeApplicationPackageFactory extends Fake implements ApplicationPackageFa
274344
}
275345

276346
class FakeApplicationPackage extends Fake implements ApplicationPackage {}
347+
348+
class FakeDeviceTrackingUninstall extends FakeDevice {
349+
FakeDeviceTrackingUninstall()
350+
: super(
351+
'ephemeral',
352+
'ephemeral',
353+
type: PlatformType.android,
354+
launchResult: LaunchResult.succeeded(vmServiceUri: vmServiceUri),
355+
);
356+
357+
bool uninstallAppCalled = false;
358+
359+
@override
360+
Future<bool> uninstallApp(ApplicationPackage app, {String? userIdentifier}) async {
361+
uninstallAppCalled = true;
362+
return true;
363+
}
364+
}

0 commit comments

Comments
 (0)