blob: 4823f707d42a4abd58a6c8ac6378371da8aaf3b9 [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 org.hamcrest.Matchers.everyItem;
import static org.hamcrest.Matchers.isIn;
import static org.mockito.Mockito.when;
import com.android.ddmlib.IDevice;
import com.android.tools.deploy.proto.Deploy;
import com.android.tools.deployer.model.Apk;
import com.android.utils.ILogger;
import com.android.utils.NullLogger;
import com.android.zipflinger.BytesSource;
import com.android.zipflinger.ZipArchive;
import com.android.zipflinger.ZipInfo;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
public class OptimisticApkInstallerTest {
private static final String TEST_ABI = "x86_64";
private static final String TEST_PACKAGE = "test-package";
private static final String TEST_SERIAL = "test-serial";
private static final DeployerOption IWI_ON =
new DeployerOption.Builder()
.setUseOptimisticSwap(true)
.setOptimisticInstallSupport(EnumSet.allOf(ChangeType.class))
.build();
private static final DeployerOption IWI_OFF =
new DeployerOption.Builder()
.setUseOptimisticSwap(true)
.setOptimisticInstallSupport(EnumSet.noneOf(ChangeType.class))
.build();
private AdbClient adb;
private Installer installer;
private DeploymentCacheDatabase cache;
private MetricsRecorder metrics;
private ILogger logger;
@Rule public TemporaryFolder folder = new TemporaryFolder();
@Rule public ExpectedException thrown = ExpectedException.none();
@Before
public void beforeTest() throws IOException {
IDevice device = Mockito.mock(IDevice.class);
when(device.getSerialNumber()).thenReturn(TEST_SERIAL);
when(device.getAbis()).thenReturn(ImmutableList.of(TEST_ABI));
installer = Mockito.mock(Installer.class);
when(installer.overlayInstall(ArgumentMatchers.any()))
.thenReturn(
Deploy.OverlayInstallResponse.newBuilder()
.setStatus(Deploy.OverlayInstallResponse.Status.OK)
.build());
adb = new AdbClient(device, logger);
cache = new DeploymentCacheDatabase(DeploymentCacheDatabase.DEFAULT_SIZE);
metrics = new MetricsRecorder();
logger = new NullLogger();
}
@Test
public void addRemoveOverlayFile() throws IOException, DeployerException {
OptimisticApkInstaller apkInstaller =
new OptimisticApkInstaller(installer, adb, cache, metrics, IWI_ON, logger);
// Populate the cache. To prevent us from having to mock dump, we create a cache entry with
// an empty overlay, which prevents the cache entry from being treated as a base install.
Apk installedApk =
buildApk(
"base",
"0",
ImmutableMap.of(
"file1", "0",
"file2", "1"));
OverlayId baseId = OverlayId.builder(new OverlayId(ImmutableList.of(installedApk))).build();
cache.store(TEST_SERIAL, TEST_PACKAGE, ImmutableList.of(installedApk), baseId);
// Test adding two new files and modifying an existing one.
Apk nextApk =
buildApk(
"base",
"1",
ImmutableMap.of(
"file1", "0",
"file2", "99",
"file3", "2",
"file4", "2"));
OverlayId nextId = apkInstaller.install(TEST_PACKAGE, ImmutableList.of(nextApk));
assertOverlay(nextId, "base/file2", "base/file3", "base/file4");
cache.store(TEST_SERIAL, TEST_PACKAGE, ImmutableList.of(nextApk), nextId);
// Test modifying installed files and files that only exist in the overlay.
nextApk =
buildApk(
"base",
"2",
ImmutableMap.of(
"file1", "99",
"file2", "99",
"file3", "99",
"file4", "99"));
nextId = apkInstaller.install(TEST_PACKAGE, ImmutableList.of(nextApk));
assertOverlay(nextId, "base/file1", "base/file2", "base/file3", "base/file4");
cache.store(TEST_SERIAL, TEST_PACKAGE, ImmutableList.of(nextApk), nextId);
// Test undoing changes to installed files and overlay files, and removing overlay files.
nextApk =
buildApk(
"base",
"3",
ImmutableMap.of(
"file1", "0",
"file2", "1",
"file4", "2"));
nextId = apkInstaller.install(TEST_PACKAGE, ImmutableList.of(nextApk));
assertOverlay(nextId, "base/file1", "base/file2", "base/file4");
}
@Test
public void deleteInstalledFile() throws IOException, DeployerException {
OptimisticApkInstaller apkInstaller =
new OptimisticApkInstaller(installer, adb, cache, metrics, IWI_ON, logger);
// Populate the cache. To prevent us from having to mock dump, we create a cache entry with
// an empty overlay, which prevents the cache entry from being treated as a base install.
Apk installedApk =
buildApk(
"base",
"0",
ImmutableMap.of(
"file1", "0",
"file2", "1"));
OverlayId baseId = OverlayId.builder(new OverlayId(ImmutableList.of(installedApk))).build();
cache.store(TEST_SERIAL, TEST_PACKAGE, ImmutableList.of(installedApk), baseId);
// Test adding two new files and modifying an existing one.
Apk nextApk =
buildApk(
"base",
"1",
ImmutableMap.of(
"file1", "0",
"file2", "99",
"file3", "2",
"file4", "2"));
OverlayId nextId = apkInstaller.install(TEST_PACKAGE, ImmutableList.of(nextApk));
assertOverlay(nextId, "base/file2", "base/file3", "base/file4");
cache.store(TEST_SERIAL, TEST_PACKAGE, ImmutableList.of(nextApk), nextId);
// Test deleting an installed file.
nextApk =
buildApk(
"base",
"2",
ImmutableMap.of(
"file1", "0",
"file4", "2"));
thrown.expect(DeployerException.class);
apkInstaller.install(TEST_PACKAGE, ImmutableList.of(nextApk));
}
@Test
public void iwiDisabled() throws IOException, DeployerException {
OptimisticApkInstaller apkInstaller =
new OptimisticApkInstaller(installer, adb, cache, metrics, IWI_OFF, logger);
// Populate the cache. To prevent us from having to mock dump, we create a cache entry with
// an empty overlay, which prevents the cache entry from being treated as a base install.
Apk installedApk =
buildApk(
"base",
"0",
ImmutableMap.of(
"file1", "0",
"file2", "1"));
OverlayId baseId = OverlayId.builder(new OverlayId(ImmutableList.of(installedApk))).build();
cache.store(TEST_SERIAL, TEST_PACKAGE, ImmutableList.of(installedApk), baseId);
// Test that we throw when IWI is off.
Apk nextApk =
buildApk(
"base",
"1",
ImmutableMap.of(
"file1", "0",
"file2", "99"));
thrown.expect(DeployerException.class);
apkInstaller.install(TEST_PACKAGE, ImmutableList.of(nextApk));
}
private static void assertOverlay(OverlayId id, String... files) {
Assert.assertThat(id.getOverlayContents().allFiles(), everyItem(isIn(files)));
Assert.assertThat(
ImmutableList.copyOf(files), everyItem(isIn(id.getOverlayContents().allFiles())));
}
private Apk buildApk(String name, String checksum, Map<String, String> files)
throws IOException {
Path apkFile = folder.getRoot().toPath().resolve(name + "-" + checksum);
ZipArchive zip = new ZipArchive(apkFile);
for (Map.Entry<String, String> entry : files.entrySet()) {
zip.add(
new BytesSource(
entry.getValue().getBytes(StandardCharsets.UTF_8), entry.getKey(), 0));
}
ZipInfo info = zip.closeWithInfo();
Apk.Builder builder =
Apk.builder()
.setName(name)
.setChecksum(checksum)
.setPath(apkFile.toAbsolutePath().toString())
.addLibraryAbi(TEST_ABI)
.setPackageName(TEST_PACKAGE)
.setTargetPackages(ImmutableList.of())
.setIsolatedServices(ImmutableList.of());
ByteBuffer buffer = ByteBuffer.wrap(Files.readAllBytes(apkFile));
buffer.position((int) info.cd.first);
List<ZipUtils.ZipEntry> entries = ZipUtils.readZipEntries(buffer);
for (ZipUtils.ZipEntry entry : entries) {
builder.addApkEntry(entry);
}
return builder.build();
}
}