| /* |
| * 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.tests.apex.host; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.common.truth.Truth.assertWithMessage; |
| |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assume.assumeTrue; |
| |
| import android.cts.install.lib.host.InstallUtilsHost; |
| import android.platform.test.annotations.LargeTest; |
| |
| import com.android.tradefed.device.DeviceNotAvailableException; |
| import com.android.tradefed.device.ITestDevice; |
| import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; |
| import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; |
| import com.android.tradefed.util.CommandResult; |
| import com.android.tradefed.util.CommandStatus; |
| |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import java.io.File; |
| import java.nio.file.Files; |
| import java.nio.file.Paths; |
| import java.time.Duration; |
| import java.util.List; |
| import java.util.Optional; |
| import java.util.stream.Collectors; |
| |
| /** |
| * Test for platform support for Apex Compression feature |
| */ |
| @RunWith(DeviceJUnit4ClassRunner.class) |
| public class ApexCompressionTests extends BaseHostJUnit4Test { |
| private static final String COMPRESSED_APEX_PACKAGE_NAME = "com.android.apex.compressed"; |
| private static final String ORIGINAL_APEX_FILE_NAME = |
| COMPRESSED_APEX_PACKAGE_NAME + ".v1.apex"; |
| private static final String DECOMPRESSED_DIR_PATH = "/data/apex/decompressed/"; |
| private static final String APEX_ACTIVE_DIR = "/data/apex/active/"; |
| private static final String OTA_RESERVED_DIR = "/data/apex/ota_reserved/"; |
| private static final String DECOMPRESSED_APEX_SUFFIX = ".decompressed.apex"; |
| |
| private final InstallUtilsHost mHostUtils = new InstallUtilsHost(this); |
| private boolean mWasAdbRoot = false; |
| |
| @Before |
| public void setUp() throws Exception { |
| mWasAdbRoot = getDevice().isAdbRoot(); |
| if (!mWasAdbRoot) { |
| assumeTrue("Requires root", getDevice().enableAdbRoot()); |
| } |
| deleteFiles("/system/apex/" + COMPRESSED_APEX_PACKAGE_NAME + "*apex", |
| APEX_ACTIVE_DIR + COMPRESSED_APEX_PACKAGE_NAME + "*apex", |
| DECOMPRESSED_DIR_PATH + COMPRESSED_APEX_PACKAGE_NAME + "*apex", |
| OTA_RESERVED_DIR + "*"); |
| } |
| |
| @After |
| public void tearDown() throws Exception { |
| if (!mWasAdbRoot) { |
| getDevice().disableAdbRoot(); |
| } |
| deleteFiles("/system/apex/" + COMPRESSED_APEX_PACKAGE_NAME + "*apex", |
| APEX_ACTIVE_DIR + COMPRESSED_APEX_PACKAGE_NAME + "*apex", |
| DECOMPRESSED_DIR_PATH + COMPRESSED_APEX_PACKAGE_NAME + "*apex", |
| OTA_RESERVED_DIR + "*"); |
| } |
| |
| /** |
| * Runs the given phase of a test by calling into the device. |
| * Throws an exception if the test phase fails. |
| * <p> |
| * For example, <code>runPhase("testApkOnlyEnableRollback");</code> |
| */ |
| private void runPhase(String phase) throws Exception { |
| assertTrue(runDeviceTests("com.android.tests.apex.compression.app", |
| "com.android.tests.apex.app.ApexCompressionTests", |
| phase)); |
| } |
| |
| /** |
| * Deletes files and reboots the device if necessary. |
| * @param files the paths of files which might contain wildcards |
| */ |
| private void deleteFiles(String... files) throws Exception { |
| boolean found = false; |
| for (String file : files) { |
| CommandResult result = getDevice().executeShellV2Command("ls " + file); |
| if (result.getStatus() == CommandStatus.SUCCESS) { |
| found = true; |
| break; |
| } |
| } |
| |
| if (found) { |
| getDevice().remountSystemWritable(); |
| for (String file : files) { |
| getDevice().executeShellCommand("rm -rf " + file); |
| } |
| getDevice().reboot(); |
| } |
| } |
| |
| private void pushTestApex(final String fileName) throws Exception { |
| final File apex = mHostUtils.getTestFile(fileName); |
| getDevice().remountSystemWritable(); |
| assertTrue(getDevice().pushFile(apex, "/system/apex/" + fileName)); |
| getDevice().reboot(); |
| } |
| |
| private List<String> getFilesInDir(String baseDir) throws DeviceNotAvailableException { |
| return getDevice().getFileEntry(baseDir).getChildren(false) |
| .stream().map(entry -> entry.getName()) |
| .collect(Collectors.toList()); |
| } |
| |
| /** |
| * Returns the active apex info as optional. |
| */ |
| private Optional<ITestDevice.ApexInfo> getActiveApexInfo(String packageName) |
| throws DeviceNotAvailableException { |
| return getDevice().getActiveApexes().stream().filter( |
| apex -> apex.name.equals(packageName)).findAny(); |
| } |
| |
| @Test |
| @LargeTest |
| public void testDecompressedApexIsConsideredFactory() throws Exception { |
| pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); |
| runPhase("testDecompressedApexIsConsideredFactory"); |
| } |
| |
| @Test |
| @LargeTest |
| public void testCompressedApexIsDecompressedAndActivated() throws Exception { |
| pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); |
| |
| // Ensure that compressed APEX was decompressed in DECOMPRESSED_DIR_PATH |
| List<String> files = getFilesInDir(DECOMPRESSED_DIR_PATH); |
| assertThat(files).contains(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); |
| |
| // Match the decompressed apex with original byte for byte |
| final File originalApex = mHostUtils.getTestFile(ORIGINAL_APEX_FILE_NAME); |
| final byte[] originalApexFileBytes = Files.readAllBytes(Paths.get(originalApex.toURI())); |
| final File decompressedFile = getDevice().pullFile( |
| DECOMPRESSED_DIR_PATH + COMPRESSED_APEX_PACKAGE_NAME + "@1" |
| + DECOMPRESSED_APEX_SUFFIX); |
| final byte[] decompressedFileBytes = |
| Files.readAllBytes(Paths.get(decompressedFile.toURI())); |
| assertThat(decompressedFileBytes).isEqualTo(originalApexFileBytes); |
| |
| // The decompressed APEX should note be hard linked to APEX_ACTIVE_DIR |
| files = getFilesInDir(APEX_ACTIVE_DIR); |
| assertThat(files).doesNotContain( |
| COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); |
| } |
| |
| @Test |
| @LargeTest |
| public void testDecompressedApexSurvivesReboot() throws Exception { |
| pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); |
| |
| // Ensure that compressed APEX was activated from DECOMPRESSED_DIR_PATH |
| List<String> files = getFilesInDir(DECOMPRESSED_DIR_PATH); |
| assertThat(files).contains(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); |
| final File decompressedFile = getDevice().pullFile( |
| DECOMPRESSED_DIR_PATH + COMPRESSED_APEX_PACKAGE_NAME + "@1" |
| + DECOMPRESSED_APEX_SUFFIX); |
| final byte[] decompressedFileBytes = |
| Files.readAllBytes(Paths.get(decompressedFile.toURI())); |
| |
| getDevice().reboot(); |
| |
| // Ensure it gets activated again on reboot |
| files = getFilesInDir(DECOMPRESSED_DIR_PATH); |
| assertThat(files).contains(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); |
| final File decompressedFileAfterReboot = getDevice().pullFile( |
| DECOMPRESSED_DIR_PATH + COMPRESSED_APEX_PACKAGE_NAME + "@1" |
| + DECOMPRESSED_APEX_SUFFIX); |
| final byte[] decompressedFileBytesAfterReboot = |
| Files.readAllBytes(Paths.get(decompressedFileAfterReboot.toURI())); |
| assertThat(decompressedFileBytes).isEqualTo(decompressedFileBytesAfterReboot); |
| } |
| |
| @Test |
| @LargeTest |
| public void testDecompressionDoesNotHappenOnEveryReboot() throws Exception { |
| pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); |
| |
| final String decompressedApexFilePath = DECOMPRESSED_DIR_PATH |
| + COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX; |
| String lastModifiedTime1 = |
| getDevice().executeShellCommand("stat -c %Y " + decompressedApexFilePath); |
| |
| getDevice().reboot(); |
| getDevice().waitForDeviceAvailable(); |
| |
| String lastModifiedTime2 = |
| getDevice().executeShellCommand("stat -c %Y " + decompressedApexFilePath); |
| assertThat(lastModifiedTime1).isEqualTo(lastModifiedTime2); |
| } |
| |
| @Test |
| @LargeTest |
| public void testHigherVersionOnSystemTriggerDecompression() throws Exception { |
| // Install v1 on /system partition |
| pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); |
| // On boot, /data partition will have decompressed v1 APEX in it |
| List<String> files = getFilesInDir(DECOMPRESSED_DIR_PATH); |
| assertThat(files).contains(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); |
| |
| // Now replace /system APEX with v2 |
| getDevice().remountSystemWritable(); |
| getDevice().executeShellCommand("rm -rf /system/apex/" |
| + COMPRESSED_APEX_PACKAGE_NAME + "*apex"); |
| pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v2.capex"); |
| |
| // Ensure that v2 was decompressed |
| files = getFilesInDir(DECOMPRESSED_DIR_PATH); |
| assertThat(files).contains(COMPRESSED_APEX_PACKAGE_NAME + "@2" + DECOMPRESSED_APEX_SUFFIX); |
| } |
| |
| |
| @Test |
| @LargeTest |
| public void testDifferentRootDigestTriggersDecompression() throws Exception { |
| // Install v1 on /system partition |
| pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); |
| // On boot, /data partition will have decompressed v1 APEX in it |
| List<String> files = getFilesInDir(DECOMPRESSED_DIR_PATH); |
| assertThat(files).contains(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); |
| final File decompressedFile = getDevice().pullFile( |
| DECOMPRESSED_DIR_PATH + COMPRESSED_APEX_PACKAGE_NAME + "@1" |
| + DECOMPRESSED_APEX_SUFFIX); |
| final byte[] decompressedFileBytes = |
| Files.readAllBytes(Paths.get(decompressedFile.toURI())); |
| |
| // Now replace /system APEX with same version but different root digest |
| getDevice().remountSystemWritable(); |
| getDevice().executeShellCommand("rm -rf /system/apex/" |
| + COMPRESSED_APEX_PACKAGE_NAME + "*apex"); |
| pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1_different_digest.capex"); |
| |
| // Ensure that decompressed APEX is different than before |
| files = getFilesInDir(DECOMPRESSED_DIR_PATH); |
| assertThat(files).contains(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); |
| final File decompressedFileAfterReboot = getDevice().pullFile( |
| DECOMPRESSED_DIR_PATH + COMPRESSED_APEX_PACKAGE_NAME + "@1" |
| + DECOMPRESSED_APEX_SUFFIX); |
| final byte[] decompressedFileBytesAfterReboot = |
| Files.readAllBytes(Paths.get(decompressedFileAfterReboot.toURI())); |
| assertThat(decompressedFileBytes).isNotEqualTo(decompressedFileBytesAfterReboot); |
| } |
| |
| @Test |
| @LargeTest |
| public void testUnusedDecompressedApexIsCleanedUp_HigherVersion() throws Exception { |
| // Install v1 on /system partition |
| pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); |
| // Ensure that compressed APEX was decompressed in DECOMPRESSED_DIR_PATH |
| List<String> files = getFilesInDir(DECOMPRESSED_DIR_PATH); |
| assertThat(files).contains(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); |
| |
| // Now install an update for that APEX so that decompressed APEX becomes redundant |
| runPhase("testUnusedDecompressedApexIsCleanedUp_HigherVersion"); |
| getDevice().reboot(); |
| |
| // Verify that the decompressed APEX has been cleaned up |
| String filePath = Paths.get(DECOMPRESSED_DIR_PATH, |
| COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX).toString(); |
| mHostUtils.waitForFileDeleted(filePath, Duration.ofSeconds(15)); |
| } |
| |
| @Test |
| @LargeTest |
| public void testUnusedDecompressedApexIsCleanedUp_SameVersion() throws Exception { |
| // Install v1 on /system partition |
| pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); |
| // Ensure that compressed APEX was decompressed in DECOMPRESSED_DIR_PATH |
| List<String> files = getFilesInDir(DECOMPRESSED_DIR_PATH); |
| assertThat(files).contains(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); |
| |
| // Now install an update for that APEX so that decompressed APEX becomes redundant |
| runPhase("testUnusedDecompressedApexIsCleanedUp_SameVersion"); |
| getDevice().reboot(); |
| |
| // Verify that the decompressed APEX has been cleaned up |
| String filePath = Paths.get(DECOMPRESSED_DIR_PATH, |
| COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX).toString(); |
| mHostUtils.waitForFileDeleted(filePath, Duration.ofSeconds(15)); |
| } |
| |
| @Test |
| @LargeTest |
| public void testReservedSpaceIsNotCleanedOnReboot() throws Exception { |
| getDevice().executeShellCommand("touch " + OTA_RESERVED_DIR + "random"); |
| |
| getDevice().reboot(); |
| |
| List<String> files = getFilesInDir(OTA_RESERVED_DIR); |
| assertThat(files).hasSize(1); |
| assertThat(files).contains("random"); |
| } |
| |
| @Test |
| @LargeTest |
| public void testReservedSpaceIsCleanedUpOnDecompression() throws Exception { |
| getDevice().executeShellCommand("touch " + OTA_RESERVED_DIR + "random1"); |
| getDevice().executeShellCommand("touch " + OTA_RESERVED_DIR + "random2"); |
| |
| pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); |
| |
| assertThat(getFilesInDir(OTA_RESERVED_DIR)).isEmpty(); |
| } |
| |
| @Test |
| @LargeTest |
| public void testFailsToActivateApexOnDataFallbacksToPreInstalled() throws Exception { |
| // Push a data apex that will fail to activate |
| final File file = |
| mHostUtils.getTestFile("com.android.apex.compressed.v2_manifest_mismatch.apex"); |
| getDevice().pushFile(file, APEX_ACTIVE_DIR + COMPRESSED_APEX_PACKAGE_NAME + "@2.apex"); |
| // Push a CAPEX which should act as the fallback |
| pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v2.capex"); |
| assertWithMessage("Timed out waiting for device to boot").that( |
| getDevice().waitForBootComplete(Duration.ofMinutes(2).toMillis())).isTrue(); |
| |
| // After reboot pre-installed version of shim apex should be activated, and corrupted |
| // version on /data should be deleted. |
| final ITestDevice.ApexInfo activeApex = |
| getActiveApexInfo(COMPRESSED_APEX_PACKAGE_NAME).get(); |
| assertThat(activeApex.versionCode).isEqualTo(2); |
| assertThat(getDevice().doesFileExist( |
| DECOMPRESSED_DIR_PATH + COMPRESSED_APEX_PACKAGE_NAME + "@2" |
| + DECOMPRESSED_APEX_SUFFIX)).isTrue(); |
| assertThat(getDevice().doesFileExist( |
| APEX_ACTIVE_DIR + COMPRESSED_APEX_PACKAGE_NAME + "@2" |
| + DECOMPRESSED_APEX_SUFFIX)).isFalse(); |
| assertThat(getDevice().doesFileExist( |
| APEX_ACTIVE_DIR + COMPRESSED_APEX_PACKAGE_NAME + "@2.apex")).isFalse(); |
| } |
| |
| @Test |
| @LargeTest |
| public void testCapexToApexSwitch() throws Exception { |
| pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); |
| assertThat(getFilesInDir(DECOMPRESSED_DIR_PATH)) |
| .contains(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); |
| |
| // Now replace the CAPEX with an uncompressed APEX |
| getDevice().remountSystemWritable(); |
| getDevice().executeShellCommand("rm -rf /system/apex/" |
| + COMPRESSED_APEX_PACKAGE_NAME + "*apex"); |
| pushTestApex(ORIGINAL_APEX_FILE_NAME); |
| runPhase("testCapexToApexSwitch"); |
| |
| // Ensure active apex is running from /system |
| final ITestDevice.ApexInfo activeApex = getActiveApexInfo(COMPRESSED_APEX_PACKAGE_NAME) |
| .orElseThrow(() -> new AssertionError( |
| "Can't find " + COMPRESSED_APEX_PACKAGE_NAME)); |
| assertThat(activeApex.sourceDir).startsWith("/system"); |
| // Ensure previous decompressed APEX has been cleaned up |
| String filePath = Paths.get(DECOMPRESSED_DIR_PATH, |
| COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX).toString(); |
| mHostUtils.waitForFileDeleted(filePath, Duration.ofSeconds(15)); |
| } |
| |
| @Test |
| @LargeTest |
| public void testDecompressedApexVersionAlwaysHasSameVersionAsCapex() throws Exception { |
| pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v2.capex"); |
| // Now replace /system APEX with v1 |
| getDevice().remountSystemWritable(); |
| getDevice().executeShellCommand("rm -rf /system/apex/" |
| + COMPRESSED_APEX_PACKAGE_NAME + "*apex"); |
| pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); |
| runPhase("testDecompressedApexVersionAlwaysHasSameVersionAsCapex"); |
| } |
| |
| @Test |
| @LargeTest |
| public void testCompressedApexCanBeRolledBack() throws Exception { |
| pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); |
| |
| // Now install update with rollback |
| runPhase("testCompressedApexCanBeRolledBack_Commit"); |
| getDevice().reboot(); |
| |
| // Rollback the apex |
| runPhase("testCompressedApexCanBeRolledBack_Rollback"); |
| getDevice().reboot(); |
| |
| runPhase("testCompressedApexCanBeRolledBack_Verify"); |
| } |
| |
| @Test |
| @LargeTest |
| public void testOrphanedDecompressedApexInActiveDirIsIgnored() throws Exception { |
| final File apex = mHostUtils.getTestFile( |
| COMPRESSED_APEX_PACKAGE_NAME + ".v1.apex"); |
| // Prepare an APEX in active directory with .decompressed.apex suffix. |
| // Place the same apex in system too. When booting, system APEX should |
| // be mounted while the decomrpessed APEX in active direcotyr should |
| // be ignored. |
| getDevice().remountSystemWritable(); |
| assertTrue(getDevice().pushFile(apex, |
| APEX_ACTIVE_DIR + COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX)); |
| assertTrue(getDevice().pushFile(apex, |
| "/system/apex/" + COMPRESSED_APEX_PACKAGE_NAME + ".v1.apex")); |
| getDevice().reboot(); |
| // Ensure active apex is running from /system |
| final ITestDevice.ApexInfo activeApex = getActiveApexInfo(COMPRESSED_APEX_PACKAGE_NAME) |
| .orElseThrow(() -> new AssertionError( |
| "Can't find " + COMPRESSED_APEX_PACKAGE_NAME)); |
| assertThat(activeApex.sourceDir).startsWith("/system"); |
| // Ensure orphaned decompressed APEX has been cleaned up |
| String filePath = Paths.get(DECOMPRESSED_DIR_PATH, |
| COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX).toString(); |
| mHostUtils.waitForFileDeleted(filePath, Duration.ofSeconds(15)); |
| } |
| } |
| |