blob: e9c69750a84fef75d6ee0e317b654a97c364f530 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.deployer;
import static com.android.tools.deployer.DeployerException.Error.CANNOT_SWAP_BEFORE_API_26;
import static com.android.tools.deployer.DeployerException.Error.CANNOT_SWAP_RESOURCE;
import static com.android.tools.deployer.DeployerException.Error.DUMP_FAILED;
import static com.android.tools.deployer.DeployerException.Error.DUMP_UNKNOWN_PROCESS;
import static com.android.tools.deployer.DeployerException.Error.NO_ERROR;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import com.android.ddmlib.AdbInitOptions;
import com.android.ddmlib.AndroidDebugBridge;
import com.android.ddmlib.IDevice;
import com.android.testutils.AssumeUtil;
import com.android.testutils.TestUtils;
import com.android.tools.deploy.proto.Deploy;
import com.android.tools.deployer.devices.FakeDevice;
import com.android.tools.deployer.devices.shell.FailingMkdir;
import com.android.tools.deployer.rules.ApiLevel;
import com.android.tools.deployer.rules.FakeDeviceConnection;
import com.android.tools.perflogger.Benchmark;
import com.android.tools.tracer.Trace;
import com.android.utils.FileUtils;
import com.android.utils.ILogger;
import com.google.common.base.Charsets;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
/*
How these tests work:
====================
These tests use the FakeAdbServer infrastructure but none of the default Handler.
Instead, we install our own FakeDeviceHandler.
DeployerRunner -> DDMLIB -> FakeAdbServer -> FakeDeviceHandler | Fake sync (for install command)
| Fake shell/exec ->| Fake ls
| Fake mkdir
| ...
| installer (external command)
The installer executable is built and runs on the local machine. To work in a non-Android environment,
two mechanisms are used:
1- For filesystem operations, an IO system configured via FAKE_DEVICE_ROOT environment variable redirects all
open/read/write/close/state to a test directory.
2- For exec(3) operations, the workspace Executor is substituted to a RedirectExecutor which forward requests
to a shell based on FAKE_DEVICE_SHELL environment variable.
The DeployerRunner runs as is and the DeviceHandler records all sync/exec/shell commands received by the device.
As the end of each test, the list of commands received is compared against the list of commands expected.
Concurrency: These tests are NEVER sharded on the same machine. Therefore, having one FakeAdbServer is not a problem.
A single FakeAdbServer can also be used in other tests.
*/
@RunWith(ApiLevel.class)
public class DeployerRunnerTest {
@Rule public TestName name = new TestName();
@Rule @ApiLevel.Init public FakeDeviceConnection connection;
private static final String BASE = "tools/base/deploy/deployer/src/test/resource/";
private static File dexDbFile;
private DeploymentCacheDatabase cacheDb;
private SqlApkFileDatabase dexDB;
private UIService service;
private FakeDevice device;
private ILogger logger;
private Benchmark benchmark;
private long startTime;
private static final String INSTALLER_INVOCATION =
AdbInstaller.INSTALLER_PATH + " -version=$VERSION";
@BeforeClass
public static void prepare() throws Exception {
dexDbFile = File.createTempFile("cached_db", ".bin");
dexDbFile.delete();
// Fill in the database file by calling dump() at least once.
// From then on, we will just keep copying this file and reusing it
// for every test.
new SqlApkFileDatabase(dexDbFile, null).dump();
dexDbFile.deleteOnExit();
}
@Before
public void setUp() throws Exception {
this.device = connection.getDevice();
this.service = Mockito.mock(UIService.class);
logger = new TestLogger();
File dbFile = File.createTempFile("test_db", ".bin");
dbFile.deleteOnExit();
FileUtils.copyFile(dexDbFile, dbFile);
dexDB = new SqlApkFileDatabase(dbFile, null);
cacheDb = new DeploymentCacheDatabase(2);
if ("true".equals(System.getProperty("dashboards.enabled"))) {
// Put all APIs (parameters) of a particular test into one benchmark.
String benchmarkName = name.getMethodName();
benchmark =
new Benchmark.Builder(benchmarkName)
.setProject("Android Studio Deployment")
.build();
startTime = System.currentTimeMillis();
}
Trace.begin(name.getMethodName());
}
@After
public void tearDown() throws Exception {
long currentTime = System.currentTimeMillis();
Trace.end();
if (benchmark != null) {
long timeTaken = currentTime - startTime;
// Benchmark names can only include [a-zA-Z0-9_-] characters in them.
String metricName = String.format("%s-%s_time", name.getMethodName(), connection.getDeviceId());
benchmark.log(metricName, timeTaken);
}
System.out.print(getLogcatContent(device));
Mockito.verifyNoMoreInteractions(service);
}
@Test
public void testFullInstallSuccessful() throws Exception {
assertTrue(device.getApps().isEmpty());
DeployerRunner runner = new DeployerRunner(cacheDb, dexDB, service);
Path file = TestUtils.resolveWorkspacePath(BASE + "sample.apk");
String[] args = {
"install", "com.example.helloworld", file.toString(), "--force-full-install"
};
int retcode = runner.run(args, logger);
assertEquals(0, retcode);
assertEquals(1, device.getApps().size());
assertInstalled("com.example.helloworld", file);
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:DISABLED",
"INSTALL:OK",
"DDMLIB_UPLOAD",
"DDMLIB_INSTALL");
assertFalse(device.hasFile("/data/local/tmp/sample.apk"));
}
@Test
public void testAttemptDeltaInstallWithoutPreviousInstallation() throws Exception {
AssumeUtil.assumeNotWindows(); // This test runs the installer on the host
assertTrue(device.getApps().isEmpty());
DeployerRunner runner = new DeployerRunner(cacheDb, dexDB, service);
Path file = TestUtils.resolveWorkspacePath(BASE + "sample.apk");
Path installersPath = DeployerTestUtils.prepareInstaller().toPath();
String[] args = {
"install",
"com.example.helloworld",
file.toString(),
"--installers-path=" + installersPath.toString()
};
int retcode = runner.run(args, logger);
assertEquals(0, retcode);
assertEquals(1, device.getApps().size());
assertInstalled("com.example.helloworld", file);
if (device.getApi() < 21) {
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:API_NOT_SUPPORTED",
"INSTALL:OK",
"DDMLIB_UPLOAD",
"DDMLIB_INSTALL");
assertHistory(
device,
"getprop",
"pm install -r -t \"/data/local/tmp/sample.apk\"",
"rm \"/data/local/tmp/sample.apk\"");
} else if (device.getApi() < 24) {
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:API_NOT_SUPPORTED",
"INSTALL:OK",
"DDMLIB_UPLOAD",
"DDMLIB_INSTALL");
assertHistory(
device,
"getprop",
"pm install-create -r -t -S ${size:com.example.helloworld}",
"pm install-write -S ${size:com.example.helloworld} 1 0_sample -",
"pm install-commit 1");
} else {
String packageCommand = device.getApi() < 28 ? "dump" : "path";
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:DUMP_UNKNOWN_PACKAGE",
"INSTALL:OK",
"DDMLIB_UPLOAD",
"DDMLIB_INSTALL");
assertHistory(
device,
"getprop",
INSTALLER_INVOCATION, // dump com.example.helloworld
AdbInstallerTest.RM_DIR,
AdbInstallerTest.MK_DIR,
AdbInstallerTest.CHMOD,
INSTALLER_INVOCATION, // dump com.example.helloworld
"/system/bin/run-as com.example.helloworld id -u",
String.format(
"/system/bin/cmd package %s com.example.helloworld", packageCommand),
"cmd package install-create -r -t -S ${size:com.example.helloworld}",
"cmd package install-write -S ${size:com.example.helloworld} 1 0_sample -",
"cmd package install-commit 1");
}
}
@Test
@ApiLevel.InRange(max = 29)
public void testSkipInstall() throws Exception {
AssumeUtil.assumeNotWindows(); // This test runs the installer on the host
assertTrue(device.getApps().isEmpty());
DeployerRunner runner = new DeployerRunner(cacheDb, dexDB, service);
Path file = TestUtils.resolveWorkspacePath(BASE + "apks/simple.apk");
Path installersPath = DeployerTestUtils.prepareInstaller().toPath();
String[] args = {
"install", "com.example.simpleapp", file.toString(), "--force-full-install"
};
assertEquals(0, runner.run(args, logger));
assertInstalled("com.example.simpleapp", file);
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:DISABLED",
"INSTALL:OK",
"DDMLIB_UPLOAD",
"DDMLIB_INSTALL");
device.getShell().clearHistory();
args =
new String[] {
"install",
"com.example.simpleapp",
file.toString(),
"--installers-path=" + installersPath.toString()
};
int retcode = runner.run(args, logger);
assertEquals(0, retcode);
assertEquals(1, device.getApps().size());
assertInstalled("com.example.simpleapp", file);
if (device.getApi() < 24) {
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:API_NOT_SUPPORTED",
"INSTALL:OK",
"DDMLIB_UPLOAD",
"DDMLIB_INSTALL");
} else {
String packageCommand = device.getApi() < 28 ? "dump" : "path";
assertHistory(
device,
"getprop",
INSTALLER_INVOCATION,
AdbInstallerTest.RM_DIR,
AdbInstallerTest.MK_DIR,
AdbInstallerTest.CHMOD,
INSTALLER_INVOCATION,
"/system/bin/run-as com.example.simpleapp id -u",
"id -u",
String.format(
"/system/bin/cmd package %s com.example.simpleapp", packageCommand),
"am force-stop com.example.simpleapp");
assertMetrics(runner.getMetrics(), "INSTALL:SKIPPED_INSTALL");
}
}
@Test
public void testDeltaInstall() throws Exception {
AssumeUtil.assumeNotWindows(); // This test runs the installer on the host
assertTrue(device.getApps().isEmpty());
DeployerRunner runner = new DeployerRunner(cacheDb, dexDB, service);
Path file = TestUtils.resolveWorkspacePath(BASE + "apks/simple.apk");
Path installersPath = DeployerTestUtils.prepareInstaller().toPath();
String[] args = {
"install", "com.example.simpleapp", file.toString(), "--force-full-install"
};
assertEquals(0, runner.run(args, logger));
assertInstalled("com.example.simpleapp", file);
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:DISABLED",
"INSTALL:OK",
"DDMLIB_UPLOAD",
"DDMLIB_INSTALL");
device.getShell().clearHistory();
file = TestUtils.resolveWorkspacePath(BASE + "apks/simple+code.apk");
args =
new String[] {
"install",
"com.example.simpleapp",
file.toString(),
"--installers-path=" + installersPath.toString()
};
int retcode = runner.run(args, logger);
assertEquals(0, retcode);
assertEquals(1, device.getApps().size());
assertInstalled("com.example.simpleapp", file);
if (device.getApi() < 24) {
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:API_NOT_SUPPORTED",
"INSTALL:OK",
"DDMLIB_UPLOAD",
"DDMLIB_INSTALL");
} else {
String packageCommand = device.getApi() < 28 ? "dump" : "path";
assertHistory(
device,
"getprop",
INSTALLER_INVOCATION, // dump
AdbInstallerTest.RM_DIR,
AdbInstallerTest.MK_DIR,
AdbInstallerTest.CHMOD,
INSTALLER_INVOCATION, // dump
"/system/bin/run-as com.example.simpleapp id -u",
"id -u",
String.format(
"/system/bin/cmd package %s com.example.simpleapp", packageCommand),
INSTALLER_INVOCATION, // deltainstall
"/system/bin/cmd package install-create -t -r",
"cmd package install-write -S ${size:com.example.simpleapp} 2 base.apk",
"/system/bin/cmd package install-commit 2");
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL_UPLOAD",
"DELTAINSTALL_INSTALL",
"DELTAINSTALL:SUCCESS");
}
}
@Test
public void testInstallOldVersion() throws Exception {
AssumeUtil.assumeNotWindows(); // This test runs the installer on the host
assertTrue(device.getApps().isEmpty());
DeployerRunner runner = new DeployerRunner(cacheDb, dexDB, service);
Path v2 = TestUtils.resolveWorkspacePath(BASE + "apks/simple+ver.apk");
Path installersPath = DeployerTestUtils.prepareInstaller().toPath();
String[] args = {"install", "com.example.simpleapp", v2.toString(), "--force-full-install"};
assertEquals(0, runner.run(args, logger));
assertInstalled("com.example.simpleapp", v2);
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:DISABLED",
"INSTALL:OK",
"DDMLIB_UPLOAD",
"DDMLIB_INSTALL");
device.getShell().clearHistory();
Path v1 = TestUtils.resolveWorkspacePath(BASE + "apks/simple.apk");
args =
new String[] {
"install",
"com.example.simpleapp",
v1.toString(),
"--installers-path=" + installersPath.toString()
};
Mockito.when(service.prompt(ArgumentMatchers.anyString())).thenReturn(false);
int retcode = runner.run(args, logger);
assertEquals(DeployerException.Error.INSTALL_FAILED.ordinal(), retcode);
assertEquals(1, device.getApps().size());
// Check old app still installed
assertInstalled("com.example.simpleapp", v2);
if (device.getApi() == 19) {
assertHistory(
device, "getprop", "pm install -r -t \"/data/local/tmp/simple.apk\""
// ,"rm \"/data/local/tmp/simple.apk\"" TODO: ddmlib doesn't remove when installation fails
);
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:API_NOT_SUPPORTED",
"INSTALL:INSTALL_FAILED_VERSION_DOWNGRADE");
} else if (device.getApi() < 24) {
assertHistory(
device,
"getprop",
"pm install-create -r -t -S ${size:com.example.simpleapp}", // TODO: passing size on create?
"pm install-write -S ${size:com.example.simpleapp} 2 0_simple -",
"pm install-commit 2");
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:API_NOT_SUPPORTED",
"INSTALL:INSTALL_FAILED_VERSION_DOWNGRADE");
} else {
String packageCommand = device.getApi() < 28 ? "dump" : "path";
assertHistory(
device,
"getprop",
INSTALLER_INVOCATION, // dump com.example.simpleapp
AdbInstallerTest.RM_DIR,
AdbInstallerTest.MK_DIR,
AdbInstallerTest.CHMOD,
INSTALLER_INVOCATION, // dump com.example.simpleapp
"/system/bin/run-as com.example.simpleapp id -u",
"id -u",
String.format(
"/system/bin/cmd package %s com.example.simpleapp", packageCommand),
INSTALLER_INVOCATION, // deltainstall
"/system/bin/cmd package install-create -t -r",
"cmd package install-write -S ${size:com.example.simpleapp} 2 base.apk",
"/system/bin/cmd package install-commit 2");
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL_UPLOAD",
"DELTAINSTALL_INSTALL",
"DELTAINSTALL:ERROR.INSTALL_FAILED_VERSION_DOWNGRADE");
}
Mockito.verify(service, Mockito.times(1)).prompt(ArgumentMatchers.anyString());
}
@Test
public void testInstallSplit() throws Exception {
AssumeUtil.assumeNotWindows(); // This test runs the installer on the host
assertTrue(device.getApps().isEmpty());
DeployerRunner runner = new DeployerRunner(cacheDb, dexDB, service);
Path base = TestUtils.resolveWorkspacePath(BASE + "apks/simple.apk");
Path split = TestUtils.resolveWorkspacePath(BASE + "apks/split.apk");
String[] args = {
"install",
"com.example.simpleapp",
base.toString(),
split.toString(),
"--force-full-install"
};
int code = runner.run(args, logger);
if (device.getApi() < 21) {
assertEquals(DeployerException.Error.INSTALL_FAILED.ordinal(), code);
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:DISABLED",
"INSTALL:MULTI_APKS_NO_SUPPORTED_BELOW21");
} else {
assertInstalled("com.example.simpleapp", base, split);
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:DISABLED",
"INSTALL:OK",
"DDMLIB_UPLOAD",
"DDMLIB_INSTALL");
}
}
@Test
public void testInstallVersionMismatchSplit() throws Exception {
AssumeUtil.assumeNotWindows(); // This test runs the installer on the host
assertTrue(device.getApps().isEmpty());
DeployerRunner runner = new DeployerRunner(cacheDb, dexDB, service);
Path base = TestUtils.resolveWorkspacePath(BASE + "apks/simple.apk");
Path split = TestUtils.resolveWorkspacePath(BASE + "apks/split+ver.apk");
String[] args = {
"install",
"com.example.simpleapp",
base.toString(),
split.toString(),
"--force-full-install"
};
int code = runner.run(args, logger);
assertEquals(DeployerException.Error.INSTALL_FAILED.ordinal(), code);
if (device.getApi() < 21) {
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:DISABLED",
"INSTALL:MULTI_APKS_NO_SUPPORTED_BELOW21");
} else {
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:DISABLED",
"INSTALL:INSTALL_FAILED_INVALID_APK");
}
}
@Test
public void testBadDeltaOnSplit() throws Exception {
AssumeUtil.assumeNotWindows(); // This test runs the installer on the host
assertTrue(device.getApps().isEmpty());
DeployerRunner runner = new DeployerRunner(cacheDb, dexDB, service);
Path base = TestUtils.resolveWorkspacePath(BASE + "apks/simple.apk");
Path split = TestUtils.resolveWorkspacePath(BASE + "apks/split.apk");
Path installersPath = DeployerTestUtils.prepareInstaller().toPath();
String[] args = {
"install",
"com.example.simpleapp",
base.toString(),
split.toString(),
"--force-full-install"
};
int code = runner.run(args, logger);
if (device.getApi() < 21) {
assertEquals(DeployerException.Error.INSTALL_FAILED.ordinal(), code);
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:DISABLED",
"INSTALL:MULTI_APKS_NO_SUPPORTED_BELOW21");
} else {
assertInstalled("com.example.simpleapp", base, split);
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:DISABLED",
"INSTALL:OK",
"DDMLIB_UPLOAD",
"DDMLIB_INSTALL");
}
device.getShell().clearHistory();
Path update = TestUtils.resolveWorkspacePath(BASE + "apks/split+ver.apk");
args =
new String[] {
"install",
"com.example.simpleapp",
base.toString(),
update.toString(),
"--installers-path=" + installersPath.toString()
};
code = runner.run(args, logger);
if (device.getApi() < 21) {
assertEquals(DeployerException.Error.INSTALL_FAILED.ordinal(), code);
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:API_NOT_SUPPORTED",
"INSTALL:MULTI_APKS_NO_SUPPORTED_BELOW21");
} else {
assertEquals(DeployerException.Error.INSTALL_FAILED.ordinal(), code);
assertEquals(1, device.getApps().size());
// Check old app still installed
assertInstalled("com.example.simpleapp", base, split);
if (device.getApi() < 24) {
assertHistory(
device,
"getprop",
"pm install-create -r -t -S ${size:com.example.simpleapp}",
"pm install-write -S ${size:com.example.simpleapp:base.apk} 2 0_simple -",
"pm install-write -S ${size:com.example.simpleapp:split_split_01.apk} 2 1_split_ver -",
"pm install-commit 2");
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:API_NOT_SUPPORTED",
"INSTALL:INSTALL_FAILED_INVALID_APK");
} else {
String packageCommand = device.getApi() < 28 ? "dump" : "path";
assertHistory(
device,
"getprop",
INSTALLER_INVOCATION, // dump com.example.simpleapp
AdbInstallerTest.RM_DIR,
AdbInstallerTest.MK_DIR,
AdbInstallerTest.CHMOD,
INSTALLER_INVOCATION, // dump com.example.simpleapp
"/system/bin/run-as com.example.simpleapp id -u",
"id -u",
String.format(
"/system/bin/cmd package %s com.example.simpleapp", packageCommand),
INSTALLER_INVOCATION, // deltainstall
"/system/bin/cmd package install-create -t -r",
"cmd package install-write -S ${size:com.example.simpleapp:split_split_01.apk} 2 split_split_01.apk",
"cmd package install-write -S ${size:com.example.simpleapp:base.apk} 2 base.apk",
"/system/bin/cmd package install-commit 2");
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL_UPLOAD",
"DELTAINSTALL_INSTALL",
"DELTAINSTALL:ERROR.INSTALL_FAILED_INVALID_APK");
}
}
}
@Test
public void testDeltaOnSplit() throws Exception {
AssumeUtil.assumeNotWindows(); // This test runs the installer on the host
assertTrue(device.getApps().isEmpty());
DeployerRunner runner = new DeployerRunner(cacheDb, dexDB, service);
Path base = TestUtils.resolveWorkspacePath(BASE + "apks/simple.apk");
Path split = TestUtils.resolveWorkspacePath(BASE + "apks/split.apk");
Path installersPath = DeployerTestUtils.prepareInstaller().toPath();
String[] args = {
"install",
"com.example.simpleapp",
base.toString(),
split.toString(),
"--force-full-install"
};
int code = runner.run(args, logger);
if (device.getApi() < 21) {
assertEquals(DeployerException.Error.INSTALL_FAILED.ordinal(), code);
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:DISABLED",
"INSTALL:MULTI_APKS_NO_SUPPORTED_BELOW21");
} else {
assertInstalled("com.example.simpleapp", base, split);
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:DISABLED",
"INSTALL:OK",
"DDMLIB_UPLOAD",
"DDMLIB_INSTALL");
}
device.getShell().clearHistory();
Path update = TestUtils.resolveWorkspacePath(BASE + "apks/split+code.apk");
args =
new String[] {
"install",
"com.example.simpleapp",
base.toString(),
update.toString(),
"--installers-path=" + installersPath.toString()
};
code = runner.run(args, logger);
if (device.getApi() < 21) {
assertEquals(DeployerException.Error.INSTALL_FAILED.ordinal(), code);
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:API_NOT_SUPPORTED",
"INSTALL:MULTI_APKS_NO_SUPPORTED_BELOW21");
} else {
assertEquals(0, code);
assertEquals(1, device.getApps().size());
// Check new app installed
assertInstalled("com.example.simpleapp", base, update);
if (device.getApi() < 24) {
assertHistory(
device,
"getprop",
"pm install-create -r -t -S ${size:com.example.simpleapp}",
"pm install-write -S ${size:com.example.simpleapp:base.apk} 2 0_simple -",
"pm install-write -S ${size:com.example.simpleapp:split_split_01.apk} 2 1_split_code -",
"pm install-commit 2");
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:API_NOT_SUPPORTED",
"INSTALL:OK",
"DDMLIB_UPLOAD",
"DDMLIB_INSTALL");
} else {
String packageCommand = device.getApi() < 28 ? "dump" : "path";
assertHistory(
device,
"getprop",
INSTALLER_INVOCATION, // dump com.example.simpleapp
AdbInstallerTest.RM_DIR,
AdbInstallerTest.MK_DIR,
AdbInstallerTest.CHMOD,
INSTALLER_INVOCATION, // dump com.example.simpleapp
"/system/bin/run-as com.example.simpleapp id -u",
"id -u",
String.format(
"/system/bin/cmd package %s com.example.simpleapp", packageCommand),
INSTALLER_INVOCATION, // detalinstall
"/system/bin/cmd package install-create -t -r -p com.example.simpleapp",
"cmd package install-write -S ${size:com.example.simpleapp:split_split_01.apk} 2 split_split_01.apk",
"/system/bin/cmd package install-commit 2");
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL_UPLOAD",
"DELTAINSTALL_INSTALL",
"DELTAINSTALL:SUCCESS");
}
}
}
@Test
public void testAddSplit() throws Exception {
AssumeUtil.assumeNotWindows(); // This test runs the installer on the host
assertTrue(device.getApps().isEmpty());
DeployerRunner runner = new DeployerRunner(cacheDb, dexDB, service);
Path base = TestUtils.resolveWorkspacePath(BASE + "apks/simple.apk");
Path split = TestUtils.resolveWorkspacePath(BASE + "apks/split.apk");
Path installersPath = DeployerTestUtils.prepareInstaller().toPath();
String[] args = {
"install",
"com.example.simpleapp",
base.toString(),
split.toString(),
"--force-full-install"
};
int code = runner.run(args, logger);
if (device.getApi() < 21) {
assertEquals(DeployerException.Error.INSTALL_FAILED.ordinal(), code);
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:DISABLED",
"INSTALL:MULTI_APKS_NO_SUPPORTED_BELOW21");
} else {
assertInstalled("com.example.simpleapp", base, split);
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:DISABLED",
"INSTALL:OK",
"DDMLIB_UPLOAD",
"DDMLIB_INSTALL");
}
device.getShell().clearHistory();
Path added = TestUtils.resolveWorkspacePath(BASE + "apks/split2.apk");
args =
new String[] {
"install",
"com.example.simpleapp",
base.toString(),
split.toString(),
added.toString(),
"--installers-path=" + installersPath.toString()
};
code = runner.run(args, logger);
if (device.getApi() < 21) {
assertEquals(DeployerException.Error.INSTALL_FAILED.ordinal(), code);
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:API_NOT_SUPPORTED",
"INSTALL:MULTI_APKS_NO_SUPPORTED_BELOW21");
} else {
assertEquals(0, code);
assertEquals(1, device.getApps().size());
// Check new app installed
assertInstalled("com.example.simpleapp", base, split, added);
if (device.getApi() < 24) {
assertHistory(
device,
"getprop",
"pm install-create -r -t -S ${size:com.example.simpleapp}",
"pm install-write -S ${size:com.example.simpleapp:base.apk} 2 0_simple -",
"pm install-write -S ${size:com.example.simpleapp:split_split_01.apk} 2 1_split -",
"pm install-write -S ${size:com.example.simpleapp:split_split_02.apk} 2 2_split_ -",
"pm install-commit 2");
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:API_NOT_SUPPORTED",
"INSTALL:OK",
"DDMLIB_UPLOAD",
"DDMLIB_INSTALL");
} else {
String packageCommand = device.getApi() < 28 ? "dump" : "path";
assertHistory(
device,
"getprop",
INSTALLER_INVOCATION, // dump com.example.simpleapp
AdbInstallerTest.RM_DIR,
AdbInstallerTest.MK_DIR,
AdbInstallerTest.CHMOD,
INSTALLER_INVOCATION, // dump com.example.simpleapp
"/system/bin/run-as com.example.simpleapp id -u",
"id -u",
String.format(
"/system/bin/cmd package %s com.example.simpleapp", packageCommand),
"cmd package install-create -r -t -S ${size:com.example.simpleapp}",
"cmd package install-write -S ${size:com.example.simpleapp:base.apk} 2 0_simple -",
"cmd package install-write -S ${size:com.example.simpleapp:split_split_01.apk} 2 1_split -",
"cmd package install-write -S ${size:com.example.simpleapp:split_split_02.apk} 2 2_split_ -",
"cmd package install-commit 2");
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:CANNOT_GENERATE_DELTA",
"INSTALL:OK",
"DDMLIB_UPLOAD",
"DDMLIB_INSTALL");
}
}
}
@Test
public void testRemoveSplit() throws Exception {
AssumeUtil.assumeNotWindows(); // This test runs the installer on the host
assertTrue(device.getApps().isEmpty());
DeployerRunner runner = new DeployerRunner(cacheDb, dexDB, service);
Path base = TestUtils.resolveWorkspacePath(BASE + "apks/simple.apk");
Path split1 = TestUtils.resolveWorkspacePath(BASE + "apks/split.apk");
Path split2 = TestUtils.resolveWorkspacePath(BASE + "apks/split2.apk");
Path installersPath = DeployerTestUtils.prepareInstaller().toPath();
String[] args = {
"install",
"com.example.simpleapp",
base.toString(),
split1.toString(),
split2.toString(),
"--force-full-install"
};
int code = runner.run(args, logger);
if (device.getApi() < 21) {
assertEquals(DeployerException.Error.INSTALL_FAILED.ordinal(), code);
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:DISABLED",
"INSTALL:MULTI_APKS_NO_SUPPORTED_BELOW21");
} else {
assertInstalled("com.example.simpleapp", base, split1, split2);
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:DISABLED",
"INSTALL:OK",
"DDMLIB_UPLOAD",
"DDMLIB_INSTALL");
}
device.getShell().clearHistory();
args =
new String[] {
"install",
"com.example.simpleapp",
base.toString(),
split1.toString(),
"--installers-path=" + installersPath.toString()
};
code = runner.run(args, logger);
if (device.getApi() < 21) {
assertEquals(DeployerException.Error.INSTALL_FAILED.ordinal(), code);
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:API_NOT_SUPPORTED",
"INSTALL:MULTI_APKS_NO_SUPPORTED_BELOW21");
} else {
assertEquals(0, code);
assertEquals(1, device.getApps().size());
// Check new app installed
assertInstalled("com.example.simpleapp", base, split1);
if (device.getApi() < 24) {
assertHistory(
device,
"getprop",
"pm install-create -r -t -S ${size:com.example.simpleapp}",
"pm install-write -S ${size:com.example.simpleapp:base.apk} 2 0_simple -",
"pm install-write -S ${size:com.example.simpleapp:split_split_01.apk} 2 1_split -",
"pm install-commit 2");
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:API_NOT_SUPPORTED",
"INSTALL:OK",
"DDMLIB_UPLOAD",
"DDMLIB_INSTALL");
} else {
String packageCommand = device.getApi() < 28 ? "dump" : "path";
assertHistory(
device,
"getprop",
INSTALLER_INVOCATION, // dump com.example.simpleapp
AdbInstallerTest.RM_DIR,
AdbInstallerTest.MK_DIR,
AdbInstallerTest.CHMOD,
INSTALLER_INVOCATION, // dump com.example.simpleapp
"/system/bin/run-as com.example.simpleapp id -u",
"id -u",
String.format(
"/system/bin/cmd package %s com.example.simpleapp", packageCommand),
"cmd package install-create -r -t -S ${size:com.example.simpleapp}",
"cmd package install-write -S ${size:com.example.simpleapp:base.apk} 2 0_simple -",
"cmd package install-write -S ${size:com.example.simpleapp:split_split_01.apk} 2 1_split -",
"cmd package install-commit 2");
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:CANNOT_GENERATE_DELTA",
"INSTALL:OK",
"DDMLIB_UPLOAD",
"DDMLIB_INSTALL");
}
}
}
@Test
public void testAddAsset() throws Exception {
AssumeUtil.assumeNotWindows(); // This test runs the installer on the host
assertTrue(device.getApps().isEmpty());
DeployerRunner runner = new DeployerRunner(cacheDb, dexDB, service);
Path file = TestUtils.resolveWorkspacePath(BASE + "apks/simple.apk");
Path installersPath = DeployerTestUtils.prepareInstaller().toPath();
String[] args = {
"install", "com.example.simpleapp", file.toString(), "--force-full-install"
};
assertEquals(0, runner.run(args, logger));
assertInstalled("com.example.simpleapp", file);
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:DISABLED",
"INSTALL:OK",
"DDMLIB_UPLOAD",
"DDMLIB_INSTALL");
device.getShell().clearHistory();
file = TestUtils.resolveWorkspacePath(BASE + "apks/simple+new_asset.apk");
args =
new String[] {
"install",
"com.example.simpleapp",
file.toString(),
"--installers-path=" + installersPath.toString()
};
int retcode = runner.run(args, logger);
assertEquals(0, retcode);
assertEquals(1, device.getApps().size());
assertInstalled("com.example.simpleapp", file);
if (device.getApi() < 24) {
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:API_NOT_SUPPORTED",
"INSTALL:OK",
"DDMLIB_UPLOAD",
"DDMLIB_INSTALL");
} else {
String packageCommand = device.getApi() < 28 ? "dump" : "path";
assertHistory(
device,
"getprop",
INSTALLER_INVOCATION, // dump com.example.simpleapp
AdbInstallerTest.RM_DIR,
AdbInstallerTest.MK_DIR,
AdbInstallerTest.CHMOD,
INSTALLER_INVOCATION, // dump com.example.simpleapp
"/system/bin/run-as com.example.simpleapp id -u",
"id -u",
String.format(
"/system/bin/cmd package %s com.example.simpleapp", packageCommand),
INSTALLER_INVOCATION, // deltainstall
"/system/bin/cmd package install-create -t -r",
"cmd package install-write -S ${size:com.example.simpleapp} 2 base.apk",
"/system/bin/cmd package install-commit 2");
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL_UPLOAD",
"DELTAINSTALL_INSTALL",
"DELTAINSTALL:SUCCESS");
}
}
@Test
public void testAddAssetWithSplits() throws Exception {
AssumeUtil.assumeNotWindows(); // This test runs the installer on the host
assertTrue(device.getApps().isEmpty());
DeployerRunner runner = new DeployerRunner(cacheDb, dexDB, service);
Path base = TestUtils.resolveWorkspacePath(BASE + "apks/simple.apk");
Path split = TestUtils.resolveWorkspacePath(BASE + "apks/split.apk");
Path installersPath = DeployerTestUtils.prepareInstaller().toPath();
String[] args = {
"install",
"com.example.simpleapp",
base.toString(),
split.toString(),
"--force-full-install"
};
int code = runner.run(args, logger);
if (device.getApi() < 21) {
assertEquals(DeployerException.Error.INSTALL_FAILED.ordinal(), code);
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:DISABLED",
"INSTALL:MULTI_APKS_NO_SUPPORTED_BELOW21");
} else {
assertInstalled("com.example.simpleapp", base, split);
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:DISABLED",
"INSTALL:OK",
"DDMLIB_UPLOAD",
"DDMLIB_INSTALL");
}
device.getShell().clearHistory();
Path newBase = TestUtils.resolveWorkspacePath(BASE + "apks/simple+new_asset.apk");
args =
new String[] {
"install",
"com.example.simpleapp",
newBase.toString(),
split.toString(),
"--installers-path=" + installersPath.toString()
};
code = runner.run(args, logger);
if (device.getApi() < 21) {
assertEquals(DeployerException.Error.INSTALL_FAILED.ordinal(), code);
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:API_NOT_SUPPORTED",
"INSTALL:MULTI_APKS_NO_SUPPORTED_BELOW21");
} else {
assertEquals(0, code);
assertEquals(1, device.getApps().size());
// Check new app installed
assertInstalled("com.example.simpleapp", newBase, split);
if (device.getApi() < 24) {
assertHistory(
device,
"getprop",
"pm install-create -r -t -S ${size:com.example.simpleapp}",
"pm install-write -S ${size:com.example.simpleapp:base.apk} 2 0_simple_new_asset -",
"pm install-write -S ${size:com.example.simpleapp:split_split_01.apk} 2 1_split -",
"pm install-commit 2");
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:API_NOT_SUPPORTED",
"INSTALL:OK",
"DDMLIB_UPLOAD",
"DDMLIB_INSTALL");
} else {
String packageCommand = device.getApi() < 28 ? "dump" : "path";
assertHistory(
device,
"getprop",
INSTALLER_INVOCATION, // dump com.example.simpleapp
AdbInstallerTest.RM_DIR,
AdbInstallerTest.MK_DIR,
AdbInstallerTest.CHMOD,
INSTALLER_INVOCATION, // dump com.example.simpleapp
"/system/bin/run-as com.example.simpleapp id -u",
"id -u",
String.format(
"/system/bin/cmd package %s com.example.simpleapp", packageCommand),
INSTALLER_INVOCATION, // deltainstal
"/system/bin/cmd package install-create -t -r -p com.example.simpleapp",
"cmd package install-write -S ${size:com.example.simpleapp:base.apk} 2 base.apk",
"/system/bin/cmd package install-commit 2");
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL_UPLOAD",
"DELTAINSTALL_INSTALL",
"DELTAINSTALL:SUCCESS");
}
}
}
@Test
public void testStartApp() throws Exception {
AssumeUtil.assumeNotWindows(); // This test runs the installer on the host
assertTrue(device.getApps().isEmpty());
Path installersPath = DeployerTestUtils.prepareInstaller().toPath();
// Install the base apk:
DeployerRunner runner = new DeployerRunner(cacheDb, dexDB, service);
Path file = TestUtils.resolveWorkspacePath(BASE + "apks/simple.apk");
String[] args = {
"install",
"com.example.simpleapp",
file.toString(),
"--installers-path=" + installersPath.toString()
};
int retcode = runner.run(args, logger);
assertEquals(0, retcode);
assertEquals(1, device.getApps().size());
assertInstalled("com.example.simpleapp", file);
String cmd = "am start -n com.example.simpleapp/.MainActivity -a android.intent.action.MAIN";
assertEquals(0, device.executeScript(cmd, new byte[] {}).value);
List<FakeDevice.AndroidProcess> processes = device.getProcesses();
assertEquals(1, processes.size());
assertEquals("com.example.simpleapp", processes.get(0).application.packageName);
assertEquals(0, device.executeScript("am force-stop com.foo", new byte[] {}).value);
processes = device.getProcesses();
assertEquals(1, processes.size());
assertEquals("com.example.simpleapp", processes.get(0).application.packageName);
assertEquals(
0,
device.executeScript("am force-stop com.example.simpleapp.bar", new byte[] {})
.value);
processes = device.getProcesses();
assertEquals(1, processes.size());
assertEquals("com.example.simpleapp", processes.get(0).application.packageName);
assertNotEquals(0, device.executeScript("am force-stop", new byte[] {}).value);
processes = device.getProcesses();
assertEquals(1, processes.size());
assertEquals("com.example.simpleapp", processes.get(0).application.packageName);
assertEquals(
0,
device.executeScript("am force-stop com.example.simpleapp", new byte[] {}).value);
processes = device.getProcesses();
assertEquals(0, processes.size());
}
@Test
public void testApkNotRecognized() throws Exception {
AssumeUtil.assumeNotWindows(); // This test runs the installer on the host
// Install the base apk:
assertTrue(device.getApps().isEmpty());
Path installersPath = DeployerTestUtils.prepareInstaller().toPath();
DeployerRunner runner = new DeployerRunner(cacheDb, dexDB, service);
Path file = TestUtils.resolveWorkspacePath(BASE + "apks/simple.apk");
String[] args = {
"install",
"com.example.simpleapp",
file.toString(),
"--installers-path=" + installersPath.toString()
};
int retcode = runner.run(args, logger);
assertEquals(0, retcode);
assertEquals(1, device.getApps().size());
assertInstalled("com.example.simpleapp", file);
if (device.getApi() < 24) {
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:API_NOT_SUPPORTED",
"INSTALL:OK",
"DDMLIB_UPLOAD",
"DDMLIB_INSTALL");
} else {
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:DUMP_UNKNOWN_PACKAGE",
"INSTALL:OK",
"DDMLIB_UPLOAD",
"DDMLIB_INSTALL");
}
file = TestUtils.resolveWorkspacePath(BASE + "apks/simple+code.apk");
args =
new String[] {
"codeswap",
"com.example.simpleapp",
file.toString(),
"--installers-path=" + installersPath.toString()
};
// We create a empty database. This simulate an installed APK not found in the database.
dexDB = new SqlApkFileDatabase(File.createTempFile("test_db_empty", ".bin"), null);
device.getShell().clearHistory();
runner = new DeployerRunner(cacheDb, dexDB, service);
retcode = runner.run(args, logger);
if (device.supportsJvmti()) {
// TODO WIP. This is WRONG, this is where optimistic swap should fail because of
// OverlayID mismatch.
if (device.getApi() < 30) {
assertEquals(DeployerException.Error.REMOTE_APK_NOT_FOUND_IN_DB.ordinal(), retcode);
}
} else {
assertEquals(DeployerException.Error.CANNOT_SWAP_BEFORE_API_26.ordinal(), retcode);
}
if (device.getApi() < 26) {
assertTrue(runner.getMetrics().isEmpty());
assertHistory(device, "getprop");
} else if (device.getApi() < 30) {
String packageCommand = device.getApi() < 28 ? "dump" : "path";
assertMetrics(
runner.getMetrics(),
"DELTAPREINSTALL_WRITE",
":Success",
":Success",
":Success",
"PARSE_PATHS:Success",
"DUMP:Success",
"DIFF:Success",
"PREINSTALL:Success",
"VERIFY:Success",
"COMPARE:Failed");
assertHistory(
device,
"getprop",
INSTALLER_INVOCATION, // dump com.example.simpleapp
"/system/bin/run-as com.example.simpleapp id -u",
"id -u",
String.format(
"/system/bin/cmd package %s com.example.simpleapp", packageCommand),
INSTALLER_INVOCATION, // deltapreinstall
"/system/bin/cmd package install-create -t -r --dont-kill",
"cmd package install-write -S ${size:com.example.simpleapp} 2 base.apk",
"cmd package install-abandon 2");
}
}
@Test
public void testDump() throws Exception {
AssumeUtil.assumeNotWindows(); // This test runs the installer on the host
String packageName = "com.example.simpleapp";
assertTrue(device.getApps().isEmpty());
Path installersPath = DeployerTestUtils.prepareInstaller().toPath();
DeployerRunner runner = new DeployerRunner(cacheDb, dexDB, service);
AndroidDebugBridge.init(AdbInitOptions.DEFAULT);
AndroidDebugBridge bridge = AndroidDebugBridge.createBridge();
while (!bridge.hasInitialDeviceList()) {
Thread.sleep(100);
}
IDevice iDevice = bridge.getDevices()[0];
AdbClient adb = new AdbClient(iDevice, logger);
ArrayList<DeployMetric> metrics = new ArrayList<>();
Installer installer = new AdbInstaller(installersPath.toString(), adb, metrics, logger);
// Make sure we have true negative.
Deploy.DumpResponse response = installer.dump(Collections.singletonList(packageName));
assertEquals(Deploy.DumpResponse.Status.ERROR_PACKAGE_NOT_FOUND, response.getStatus());
AndroidDebugBridge.terminate();
// Install our target APK.
{
Path file = TestUtils.resolveWorkspacePath(BASE + "apks/simple.apk");
String[] args = {
"install",
packageName,
file.toString(),
"--installers-path=" + installersPath.toString()
};
int retcode = runner.run(args, logger);
assertEquals(0, retcode);
assertEquals(1, device.getApps().size());
assertInstalled(packageName, file);
}
// Make sure we have true positive and no false negative.
response = installer.dump(Collections.singletonList(packageName));
if (device.getApi() < 24) {
// No "cmd" on APIs < 24.
assertEquals(Deploy.DumpResponse.Status.ERROR_PACKAGE_NOT_FOUND, response.getStatus());
} else {
assertEquals(Deploy.DumpResponse.Status.OK, response.getStatus());
assertEquals(1, response.getPackagesCount());
assertEquals(1, response.getPackages(0).getApksCount());
assertEquals(
device.getAppPaths(packageName).get(0),
response.getPackages(0).getApks(0).getAbsolutePath());
}
// Make sure we don't have false positive.
response = installer.dump(Collections.singletonList("foo.bar"));
assertEquals(Deploy.DumpResponse.Status.ERROR_PACKAGE_NOT_FOUND, response.getStatus());
}
@Test
public void testSwapWithAppNotRunning() throws Exception {
AssumeUtil.assumeNotWindows(); // This test runs the installer on the host
assertTrue(device.getApps().isEmpty());
DeployerRunner runner = new DeployerRunner(cacheDb, dexDB, service);
Path file = TestUtils.resolveWorkspacePath(BASE + "apks/simple.apk");
Path installersPath = DeployerTestUtils.prepareInstaller().toPath();
String[] args = {
"install", "com.example.simpleapp", file.toString(), "--force-full-install"
};
assertEquals(0, runner.run(args, logger));
assertInstalled("com.example.simpleapp", file);
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:DISABLED",
"INSTALL:OK",
"DDMLIB_UPLOAD",
"DDMLIB_INSTALL");
device.getShell().clearHistory();
file = TestUtils.resolveWorkspacePath(BASE + "apks/simple+code.apk");
args =
new String[] {
"codeswap",
"com.example.simpleapp",
file.toString(),
"--installers-path=" + installersPath.toString()
};
int retcode = runner.run(args, logger);
if (device.getApi() < 26) {
assertEquals(CANNOT_SWAP_BEFORE_API_26.ordinal(), retcode);
assertMetrics(runner.getMetrics()); // No metrics
} else if (device.getApi() < 30) {
assertEquals(DUMP_UNKNOWN_PROCESS.ordinal(), retcode);
assertHistory(
device,
"getprop",
INSTALLER_INVOCATION, // dump com.example.simpleapp
AdbInstallerTest.RM_DIR,
AdbInstallerTest.MK_DIR,
AdbInstallerTest.CHMOD,
INSTALLER_INVOCATION, // dump com.example.simpleapp
"/system/bin/run-as com.example.simpleapp id -u",
"id -u",
"/system/bin/cmd package "
+ (device.getApi() < 28 ? "dump" : "path")
+ " com.example.simpleapp",
INSTALLER_INVOCATION, // deltapreinstall
"/system/bin/cmd package install-create -t -r --dont-kill",
"cmd package install-write -S ${size:com.example.simpleapp} 2 base.apk",
"cmd package install-abandon 2");
assertMetrics(
runner.getMetrics(),
"DELTAPREINSTALL_WRITE",
":Success",
":Success",
":Success",
"PARSE_PATHS:Success",
"DUMP:Success",
"DIFF:Success",
"PREINSTALL:Success",
"VERIFY:Success",
"COMPARE:Success",
"SWAP:Failed");
}
// TODO: API 30 tests.
}
@Test
@ApiLevel.InRange(max = 29)
public void testBasicSwap() throws Exception {
AssumeUtil.assumeNotWindows(); // This test runs the installer on the host
assertTrue(device.getApps().isEmpty());
DeployerRunner runner = new DeployerRunner(cacheDb, dexDB, service);
Path file = TestUtils.resolveWorkspacePath(BASE + "apks/simple.apk");
Path installersPath = DeployerTestUtils.prepareInstaller().toPath();
String[] args = {
"install", "com.example.simpleapp", file.toString(), "--force-full-install"
};
assertEquals(0, runner.run(args, logger));
assertInstalled("com.example.simpleapp", file);
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:DISABLED",
"INSTALL:OK",
"DDMLIB_UPLOAD",
"DDMLIB_INSTALL");
String cmd =
"am start -n com.example.simpleapp/.MainActivity -a android.intent.action.MAIN";
assertEquals(0, device.executeScript(cmd, new byte[] {}).value);
device.getShell().clearHistory();
file = TestUtils.resolveWorkspacePath(BASE + "apks/simple+code.apk");
args =
new String[] {
"codeswap",
"com.example.simpleapp",
file.toString(),
"--installers-path=" + installersPath.toString()
};
int retcode = runner.run(args, logger);
String logcat = getLogcatContent(device);
if (device.getApi() < 26) {
assertEquals(CANNOT_SWAP_BEFORE_API_26.ordinal(), retcode);
assertMetrics(runner.getMetrics()); // No metrics
} else if (device.getApi() < 30) {
assertEquals(NO_ERROR.ordinal(), retcode);
assertHistory(
device,
"getprop",
INSTALLER_INVOCATION, // dump com.example.simpleapp
AdbInstallerTest.RM_DIR,
AdbInstallerTest.MK_DIR,
AdbInstallerTest.CHMOD,
INSTALLER_INVOCATION, // dump com.example.simpleapp
"/system/bin/run-as com.example.simpleapp id -u",
"id -u",
"/system/bin/cmd package "
+ (device.getApi() < 28 ? "dump" : "path")
+ " com.example.simpleapp",
INSTALLER_INVOCATION, // deltapreinstall
"/system/bin/cmd package install-create -t -r --dont-kill",
"cmd package install-write -S ${size:com.example.simpleapp} 2 base.apk",
INSTALLER_INVOCATION, // swap
"/system/bin/run-as com.example.simpleapp cp -rF /data/local/tmp/.studio/tmp/$VERSION/ "
+ Sites.appStudio("com.example.simpleapp"),
"cp -rF /data/local/tmp/.studio/tmp/$VERSION/ "
+ Sites.appStudio("com.example.simpleapp"),
"/system/bin/run-as com.example.simpleapp /data/data/com.example.simpleapp/code_cache/install_server-$VERSION com.example.simpleapp",
"/data/data/com.example.simpleapp/code_cache/install_server-$VERSION com.example.simpleapp",
"/system/bin/run-as com.example.simpleapp cp -n /data/local/tmp/.studio/tmp/$VERSION/install_server /data/data/com.example.simpleapp/code_cache/install_server-$VERSION",
"cp -n /data/local/tmp/.studio/tmp/$VERSION/install_server /data/data/com.example.simpleapp/code_cache/install_server-$VERSION",
"/system/bin/run-as com.example.simpleapp /data/data/com.example.simpleapp/code_cache/install_server-$VERSION com.example.simpleapp",
"/data/data/com.example.simpleapp/code_cache/install_server-$VERSION com.example.simpleapp",
"/system/bin/cmd activity attach-agent 10001 /data/data/com.example.simpleapp/code_cache/.studio/agent.so=irsocket-0",
"/system/bin/cmd package install-commit 2");
assertMetrics(
runner.getMetrics(),
"DELTAPREINSTALL_WRITE",
":Success",
":Success",
":Success",
"PARSE_PATHS:Success",
"DUMP:Success",
"DIFF:Success",
"PREINSTALL:Success",
"VERIFY:Success",
"COMPARE:Success",
"SWAP:Success");
assertRetransformed(
logcat, "android.app.ActivityThread", "dalvik.system.DexPathList$Element");
} else {
assertRetransformed(
logcat,
"android.app.ActivityThread",
"dalvik.system.DexPathList$Element",
"dalvik.system.DexPathList",
"android.app.ResourcesManager");
}
TestUtils.eventually(
() -> {
try {
if (dexDB.dump().isEmpty()) {
Assert.fail();
}
} catch (DeployerException e) {
Assert.fail();
}
},
Duration.ofSeconds(5));
assertFalse(dexDB.hasDuplicates());
}
@Test
@ApiLevel.InRange(min = 30)
public void testAgentTransformCache() throws Exception {
AssumeUtil.assumeNotWindows(); // This test runs the installer on the host
Path oldApk = TestUtils.resolveWorkspacePath(BASE + "apks/simple.apk");
Path newApk = TestUtils.resolveWorkspacePath(BASE + "apks/simple+code.apk");
assertTrue(device.getApps().isEmpty());
DeployerRunner runner = new DeployerRunner(cacheDb, dexDB, service);
Path installersPath = DeployerTestUtils.prepareInstaller().toPath();
String[] args = {
"install", "com.example.simpleapp", oldApk.toString(), "--force-full-install"
};
assertEquals(0, runner.run(args, logger));
assertInstalled("com.example.simpleapp", oldApk);
String cmd =
"am start -n com.example.simpleapp/.MainActivity -a android.intent.action.MAIN";
assertEquals(0, device.executeScript(cmd, new byte[] {}).value);
device.getShell().clearHistory();
// This swap is just to put the agent in place.
args =
new String[] {
"codeswap",
"com.example.simpleapp",
newApk.toString(),
"--installers-path=" + installersPath.toString()
};
assertEquals(0, runner.run(args, logger));
// Stop the app so we can restart it and attach a startup agent.
device.stopApp("com.example.simpleapp");
assertEquals(0, device.executeScript(cmd, new byte[] {}).value);
// The second time we restart it, it should use cached instrumentation.
device.stopApp("com.example.simpleapp");
assertEquals(0, device.executeScript(cmd, new byte[] {}).value);
// The third time we restart it, it should use cached instrumentation.
device.stopApp("com.example.simpleapp");
assertEquals(0, device.executeScript(cmd, new byte[] {}).value);
String logcat = getLogcatContent(device);
// We currently determine if the agent is using transform caching by inspecting JVMTI
// invocations. The agent uses RetransformClasses when cached classes are not available, and
// uses RedefineClasses when cached classes are present.
// Should only have one retransform of each of these classes.
assertRetransformed(
logcat,
"java.lang.Thread",
"dalvik.system.DexPathList",
"android.app.LoadedApk",
"android.app.ResourcesManager");
// Should have redefined each of these classes twice, once per restart.
assertRedefined(
logcat,
"java.lang.Thread",
"dalvik.system.DexPathList",
"android.app.LoadedApk",
"android.app.ResourcesManager",
"java.lang.Thread",
"dalvik.system.DexPathList",
"android.app.LoadedApk",
"android.app.ResourcesManager");
}
@Test
public void testCodeSwapThatFails() throws Exception {
AssumeUtil.assumeNotWindows(); // This test runs the installer on the host
assertTrue(device.getApps().isEmpty());
DeployerRunner runner = new DeployerRunner(cacheDb, dexDB, service);
Path file = TestUtils.resolveWorkspacePath(BASE + "apks/simple.apk");
Path installersPath = DeployerTestUtils.prepareInstaller().toPath();
String[] args = {
"install", "com.example.simpleapp", file.toString(), "--force-full-install"
};
assertEquals(0, runner.run(args, logger));
assertInstalled("com.example.simpleapp", file);
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:DISABLED",
"INSTALL:OK",
"DDMLIB_UPLOAD",
"DDMLIB_INSTALL");
String cmd =
"am start -n com.example.simpleapp/.MainActivity -a android.intent.action.MAIN";
assertEquals(0, device.executeScript(cmd, new byte[] {}).value);
device.getShell().clearHistory();
file = TestUtils.resolveWorkspacePath(BASE + "apks/simple+code+res.apk");
args =
new String[] {
"codeswap",
"com.example.simpleapp",
file.toString(),
"--installers-path=" + installersPath.toString()
};
int retcode = runner.run(args, logger);
if (device.getApi() < 26) {
assertEquals(CANNOT_SWAP_BEFORE_API_26.ordinal(), retcode);
assertMetrics(runner.getMetrics()); // No metrics
} else if (device.getApi() < 30) {
assertEquals(CANNOT_SWAP_RESOURCE.ordinal(), retcode);
assertHistory(
device,
"getprop",
INSTALLER_INVOCATION, // dump com.example.simpleapp
AdbInstallerTest.RM_DIR,
AdbInstallerTest.MK_DIR,
AdbInstallerTest.CHMOD,
INSTALLER_INVOCATION, // dump com.example.simpleapp
"/system/bin/run-as com.example.simpleapp id -u",
"id -u",
"/system/bin/cmd package "
+ (device.getApi() < 28 ? "dump" : "path")
+ " com.example.simpleapp",
INSTALLER_INVOCATION, // deltainstall
"/system/bin/cmd package install-create -t -r --dont-kill",
"cmd package install-write -S ${size:com.example.simpleapp} 2 base.apk",
"cmd package install-abandon 2");
assertMetrics(
runner.getMetrics(),
"DELTAPREINSTALL_WRITE",
":Success",
":Success",
":Success",
"PARSE_PATHS:Success",
"DUMP:Success",
"DIFF:Success",
"PREINSTALL:Success",
"VERIFY:Failed");
} else {
// TODO: Pipeline 2.0 tests.
}
}
@Test
@ApiLevel.InRange(max = 29)
public void testResourceAndCodeSwap() throws Exception {
AssumeUtil.assumeNotWindows(); // This test runs the installer on the host
assertTrue(device.getApps().isEmpty());
DeployerRunner runner = new DeployerRunner(cacheDb, dexDB, service);
Path file = TestUtils.resolveWorkspacePath(BASE + "apks/simple.apk");
Path installersPath = DeployerTestUtils.prepareInstaller().toPath();
String[] args = {
"install", "com.example.simpleapp", file.toString(), "--force-full-install"
};
assertEquals(0, runner.run(args, logger));
assertInstalled("com.example.simpleapp", file);
assertMetrics(
runner.getMetrics(),
"DELTAINSTALL:DISABLED",
"INSTALL:OK",
"DDMLIB_UPLOAD",
"DDMLIB_INSTALL");
String cmd =
"am start -n com.example.simpleapp/.MainActivity -a android.intent.action.MAIN";
assertEquals(0, device.executeScript(cmd, new byte[] {}).value);
device.getShell().clearHistory();
file = TestUtils.resolveWorkspacePath(BASE + "apks/simple+code+res.apk");
args =
new String[] {
"fullswap",
"com.example.simpleapp",
file.toString(),
"--installers-path=" + installersPath.toString()
};
int retcode = runner.run(args, logger);
String logcat = getLogcatContent(device);
if (device.getApi() < 26) {
assertEquals(CANNOT_SWAP_BEFORE_API_26.ordinal(), retcode);
assertMetrics(runner.getMetrics()); // No metrics
} else if (device.getApi() < 30) {
assertEquals(NO_ERROR.ordinal(), retcode);
assertHistory(
device,
"getprop",
INSTALLER_INVOCATION, // dump com.example.simpleapp
AdbInstallerTest.RM_DIR,
AdbInstallerTest.MK_DIR,
AdbInstallerTest.CHMOD,
INSTALLER_INVOCATION, // dump com.example.simpleapp
"/system/bin/run-as com.example.simpleapp id -u",
"id -u",
"/system/bin/cmd package "
+ (device.getApi() < 28 ? "dump" : "path")
+ " com.example.simpleapp",
INSTALLER_INVOCATION, // deltapreinstall
"/system/bin/cmd package install-create -t -r --dont-kill",
"cmd package install-write -S ${size:com.example.simpleapp} 2 base.apk",
INSTALLER_INVOCATION, // swap
"/system/bin/run-as com.example.simpleapp cp -rF /data/local/tmp/.studio/tmp/$VERSION/ "
+ Sites.appStudio("com.example.simpleapp"),
"cp -rF /data/local/tmp/.studio/tmp/$VERSION/ "
+ Sites.appStudio("com.example.simpleapp"),
"/system/bin/run-as com.example.simpleapp /data/data/com.example.simpleapp/code_cache/install_server-$VERSION com.example.simpleapp",
"/data/data/com.example.simpleapp/code_cache/install_server-$VERSION com.example.simpleapp",
"/system/bin/run-as com.example.simpleapp cp -n /data/local/tmp/.studio/tmp/$VERSION/install_server /data/data/com.example.simpleapp/code_cache/install_server-$VERSION",
"cp -n /data/local/tmp/.studio/tmp/$VERSION/install_server /data/data/com.example.simpleapp/code_cache/install_server-$VERSION",
"/system/bin/run-as com.example.simpleapp /data/data/com.example.simpleapp/code_cache/install_server-$VERSION com.example.simpleapp",
"/data/data/com.example.simpleapp/code_cache/install_server-$VERSION com.example.simpleapp",
"/system/bin/cmd activity attach-agent 10001 /data/data/com.example.simpleapp/code_cache/.studio/agent.so=irsocket-0",
"/system/bin/cmd package install-commit 2");
assertMetrics(
runner.getMetrics(),
"DELTAPREINSTALL_WRITE",
":Success",
":Success",
":Success",
"PARSE_PATHS:Success",
"DUMP:Success",
"DIFF:Success",
"PREINSTALL:Success",
"VERIFY:Success",
"COMPARE:Success",
"SWAP:Success");
assertRetransformed(
logcat, "android.app.ActivityThread", "dalvik.system.DexPathList$Element");
} else {
assertRetransformed(
logcat,
"android.app.ActivityThread",
"dalvik.system.DexPathList$Element",
"dalvik.system.DexPathList",
"android.app.ResourcesManager");
}
}
@Test
public void checkFailingMkDir() throws IOException {
AssumeUtil.assumeNotWindows(); // This test runs the installer on the host
assertTrue(device.getApps().isEmpty());
DeployerRunner runner = new DeployerRunner(cacheDb, dexDB, service);
Path installersPath = DeployerTestUtils.prepareInstaller().toPath();
Path file = TestUtils.resolveWorkspacePath(BASE + "apks/simple+code.apk");
// Set device to fail on any attempt to mkdir
device.getShell().addCommand(new FailingMkdir());
String[] args =
new String[] {
"codeswap",
"com.example.simpleapp",
file.toString(),
"--installers-path=" + installersPath.toString()
};
int retcode = runner.run(args, logger);
if (device.getApi() < 26) {
assertEquals(CANNOT_SWAP_BEFORE_API_26.ordinal(), retcode);
assertMetrics(runner.getMetrics()); // No metrics
} else if (device.getApi() < 30) {
assertEquals(DUMP_FAILED.ordinal(), retcode);
assertHistory(
device,
"getprop",
INSTALLER_INVOCATION,
AdbInstallerTest.RM_DIR,
AdbInstallerTest.MK_DIR);
} else {
// TODO: Add R+ tests.
}
}
private static void assertHistory(FakeDevice device, String... expectedHistory)
throws IOException {
List<String> actualHistory = device.getShell().getHistory();
String actual = String.join("\n", actualHistory);
String expected = String.join("\n", expectedHistory);
// Apply the right version
expected = expected.replaceAll("\\$VERSION", Version.hash());
// Find the right sizes:
Pattern pattern = Pattern.compile("\\$\\{size:([^:}]*)(:([^:}]*))?}");
Matcher matcher = pattern.matcher(expected);
StringBuffer buffer = new StringBuffer();
while (matcher.find()) {
String pkg = matcher.group(1);
String file = matcher.group(3);
List<String> paths = device.getAppPaths(pkg);
int size = 0;
for (String path : paths) {
if (file == null || path.endsWith("/" + file)) {
size += device.readFile(path).length;
}
}
matcher.appendReplacement(buffer, Integer.toString(size));
}
matcher.appendTail(buffer);
expected = buffer.toString();
assertEquals(expected, actual);
}
public void assertInstalled(String packageName, Path... files) throws IOException {
assertArrayEquals(new String[] {packageName}, device.getApps().toArray());
List<String> paths = device.getAppPaths(packageName);
assertEquals(files.length, paths.size());
for (int i = 0; i < paths.size(); i++) {
byte[] expected = Files.readAllBytes(files[i]);
assertArrayEquals(expected, device.readFile(paths.get(i)));
}
}
private void assertMetrics(List<DeployMetric> metrics, String... expected) {
String[] actual =
metrics.stream()
.map(m -> m.getName() + (m.hasStatus() ? ":" + m.getStatus() : ""))
.toArray(String[]::new);
assertArrayEquals(expected, actual);
}
private static String getLogcatContent(FakeDevice device) {
try {
return new String(Files.readAllBytes(device.getLogcatFile().toPath()), Charsets.UTF_8);
} catch (IOException io) {
return "";
}
}
private static void assertRedefined(String logcat, String... classes) {
assertPrefixedInLogcat(logcat, "JVMTI::RedefineClasses:", classes);
}
private static void assertRetransformed(String logcat, String... classes) {
assertPrefixedInLogcat(logcat, "JVMTI::RetransformClasses:", classes);
}
private static void assertPrefixedInLogcat(String logcat, String prefix, String... expected) {
String[] actual = logcat.split("\n");
int expectedIndex = 0;
for (int i = 0; i < actual.length; ++i) {
int idx = actual[i].indexOf(prefix);
if (idx == -1) {
continue;
}
if (expectedIndex == expected.length) {
fail("Unexpected logcat line: " + actual[i]);
}
String trimmed = actual[i].substring(idx);
assertEquals("Unexpected logcat line", prefix + expected[expectedIndex], trimmed);
++expectedIndex;
}
if (expectedIndex != expected.length) {
fail("Missing logcat line: " + prefix + expected[expectedIndex]);
}
}
}