blog/content/posts/2015/10/25/android-integration-test-timeouts.md

99 lines
3.9 KiB
Markdown
Raw Permalink Normal View History

2022-06-25 08:47:06 +01:00
---
title: Fixing timeouts running Android integration tests
date: 2015-10-25 09:55:54
tags: [android, testing]
---
I run Android tests on CI and after having switched to Lollipop recently the integration tests wouldn't run. Invoking
`androidConnectedTest` gradle target always resulted in crashing with
`ShellCommandUnresponsiveException`. Internet says that in such a case
ou just need to set `ADB_INSTALL_TIMEOUT`. I tried to no avail.
Sourcediving it is then !
A long while after that I got to this file:
[Device.java](https://android.googlesource.com/platform/tools/base/+/master/ddmlib/src/main/java/com/android/ddmlib/Device.java) [Linking
to master, here's the commit
hash:`1cb1a4c2976b99ae53d28d7f01d975232c85f990`, as I don't seem to be
able to find how to link to that hash directly] 
What do we see there ? That indeed `ADB_INSTALL_TIMEOUT` is being used:
```java
static {
String installTimeout = System.getenv("ADB_INSTALL_TIMEOUT");
long time = 4;
if (installTimeout != null) {
try {
time = Long.parseLong(installTimeout);
} catch (NumberFormatException e) {
// use default value
}
}
INSTALL_TIMEOUT_MINUTES = time;
}
```
So far so good,
`ADB_INSTALL_TIMEOUT` system variable seems to be respected when
invoking package installation tools. Are the above the only methods that
can install a package though ? Going further on that hunch we see that
in addition to installing single packages there is a possibility of
having a multi-package installation session.
```java
public void installPackages(List<String> apkFilePaths, int timeOutInMs, boolean reinstall, String... extraArgs) throws InstallException {
assert(!apkFilePaths.isEmpty());
if (getApiLevel() < 21) {
Log.w("Internal error : installPackages invoked with device < 21 for %s",Joiner.on(",").join(apkFilePaths));
if (apkFilePaths.size() == 1) {
installPackage(apkFilePaths.get(0), reinstall, extraArgs);
return;
}
Log.e("Internal error : installPackages invoked with device < 21 for multiple APK : %s", Joiner.on(",").join(apkFilePaths));
throw new InstallException("Internal error : installPackages invoked with device < 21 for multiple APK : " + Joiner.on(",").join(apkFilePaths));
}
[...]
String sessionId = createMultiInstallSession(apkFilePaths, extraArgsList, reinstall);
```
Aha ! Non-Lollipop check here, with a
fallback to the old method - we may be onto something ! Some lines pass
and we can see an invocation of `createMultiInstallSession`. What's
there ?
```java
private String createMultiInstallSession(List<String> apkFileNames, @NonNull Collection<String> extraArgs, boolean reinstall) throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException {
[...]
String cmd = String.format("pm install-create %1$s -S %2$d", parameters.toString(), totalFileSize);
executeShellCommand(cmd, receiver, DdmPreferences.getTimeOut());
[...]
```
A different invocation of
`executeShellCommand`, now using `DdmPreferences.getTimeOut()` as a
timeout value source.
Summarizing - this only happens if you install
multiple applications for your `androidConnectedTest` and you are using
android device to test on that has api version that is equal or greater
to 21. That is all cool that we had this little Computer Science
Investigation, but how to fix that - i.e. how to have proper timeouts
for your installations ? Ideally from somewhere you configure and/or
invoke your builds. It turns out that gradle supports invoking just
enough Java for us to use there. In your `gradle.build` as the very
first lines:
```groovy
println "setting global timeout for apk installation to 10 minutes"
com.android.ddmlib.DdmPreferences.setTimeOut(600000)
android {
compileSdkVersion compileSdk
buildToolsVersion buildTools
[...]
```
That's it. Invoke your android tests with
`ADB_INSTALL_TIMEOUT` env variable set **AND** have the
`DddPreference` set in your `gradle.build` as in the example above and
you should be golden. Happy droiding !