1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tests.apex.host; 18 19 import static com.google.common.truth.Truth.assertThat; 20 import static com.google.common.truth.Truth.assertWithMessage; 21 22 import static org.junit.Assert.assertTrue; 23 import static org.junit.Assume.assumeTrue; 24 25 import android.cts.install.lib.host.InstallUtilsHost; 26 import android.platform.test.annotations.LargeTest; 27 28 import com.android.tradefed.device.DeviceNotAvailableException; 29 import com.android.tradefed.device.ITestDevice; 30 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; 31 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; 32 import com.android.tradefed.util.CommandResult; 33 import com.android.tradefed.util.CommandStatus; 34 35 import org.junit.After; 36 import org.junit.Before; 37 import org.junit.Test; 38 import org.junit.runner.RunWith; 39 40 import java.io.File; 41 import java.nio.file.Files; 42 import java.nio.file.Paths; 43 import java.time.Duration; 44 import java.util.List; 45 import java.util.Optional; 46 import java.util.stream.Collectors; 47 48 /** 49 * Test for platform support for Apex Compression feature 50 */ 51 @RunWith(DeviceJUnit4ClassRunner.class) 52 public class ApexCompressionTests extends BaseHostJUnit4Test { 53 private static final String COMPRESSED_APEX_PACKAGE_NAME = "com.android.apex.compressed"; 54 private static final String ORIGINAL_APEX_FILE_NAME = 55 COMPRESSED_APEX_PACKAGE_NAME + ".v1.apex"; 56 private static final String DECOMPRESSED_DIR_PATH = "/data/apex/decompressed/"; 57 private static final String APEX_ACTIVE_DIR = "/data/apex/active/"; 58 private static final String OTA_RESERVED_DIR = "/data/apex/ota_reserved/"; 59 private static final String DECOMPRESSED_APEX_SUFFIX = ".decompressed.apex"; 60 61 private final InstallUtilsHost mHostUtils = new InstallUtilsHost(this); 62 private boolean mWasAdbRoot = false; 63 64 @Before setUp()65 public void setUp() throws Exception { 66 mWasAdbRoot = getDevice().isAdbRoot(); 67 if (!mWasAdbRoot) { 68 assumeTrue("Requires root", getDevice().enableAdbRoot()); 69 } 70 deleteFiles("/system/apex/" + COMPRESSED_APEX_PACKAGE_NAME + "*apex", 71 APEX_ACTIVE_DIR + COMPRESSED_APEX_PACKAGE_NAME + "*apex", 72 DECOMPRESSED_DIR_PATH + COMPRESSED_APEX_PACKAGE_NAME + "*apex", 73 OTA_RESERVED_DIR + "*"); 74 } 75 76 @After tearDown()77 public void tearDown() throws Exception { 78 if (!mWasAdbRoot) { 79 getDevice().disableAdbRoot(); 80 } 81 deleteFiles("/system/apex/" + COMPRESSED_APEX_PACKAGE_NAME + "*apex", 82 APEX_ACTIVE_DIR + COMPRESSED_APEX_PACKAGE_NAME + "*apex", 83 DECOMPRESSED_DIR_PATH + COMPRESSED_APEX_PACKAGE_NAME + "*apex", 84 OTA_RESERVED_DIR + "*"); 85 } 86 87 /** 88 * Runs the given phase of a test by calling into the device. 89 * Throws an exception if the test phase fails. 90 * <p> 91 * For example, <code>runPhase("testApkOnlyEnableRollback");</code> 92 */ runPhase(String phase)93 private void runPhase(String phase) throws Exception { 94 assertTrue(runDeviceTests("com.android.tests.apex.compression.app", 95 "com.android.tests.apex.app.ApexCompressionTests", 96 phase)); 97 } 98 99 /** 100 * Deletes files and reboots the device if necessary. 101 * @param files the paths of files which might contain wildcards 102 */ deleteFiles(String... files)103 private void deleteFiles(String... files) throws Exception { 104 boolean found = false; 105 for (String file : files) { 106 CommandResult result = getDevice().executeShellV2Command("ls " + file); 107 if (result.getStatus() == CommandStatus.SUCCESS) { 108 found = true; 109 break; 110 } 111 } 112 113 if (found) { 114 getDevice().remountSystemWritable(); 115 for (String file : files) { 116 getDevice().executeShellCommand("rm -rf " + file); 117 } 118 getDevice().reboot(); 119 } 120 } 121 pushTestApex(final String fileName)122 private void pushTestApex(final String fileName) throws Exception { 123 final File apex = mHostUtils.getTestFile(fileName); 124 getDevice().remountSystemWritable(); 125 assertTrue(getDevice().pushFile(apex, "/system/apex/" + fileName)); 126 getDevice().reboot(); 127 } 128 getFilesInDir(String baseDir)129 private List<String> getFilesInDir(String baseDir) throws DeviceNotAvailableException { 130 return getDevice().getFileEntry(baseDir).getChildren(false) 131 .stream().map(entry -> entry.getName()) 132 .collect(Collectors.toList()); 133 } 134 135 /** 136 * Returns the active apex info as optional. 137 */ getActiveApexInfo(String packageName)138 private Optional<ITestDevice.ApexInfo> getActiveApexInfo(String packageName) 139 throws DeviceNotAvailableException { 140 return getDevice().getActiveApexes().stream().filter( 141 apex -> apex.name.equals(packageName)).findAny(); 142 } 143 144 @Test 145 @LargeTest testDecompressedApexIsConsideredFactory()146 public void testDecompressedApexIsConsideredFactory() throws Exception { 147 pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); 148 runPhase("testDecompressedApexIsConsideredFactory"); 149 } 150 151 @Test 152 @LargeTest testCompressedApexIsDecompressedAndActivated()153 public void testCompressedApexIsDecompressedAndActivated() throws Exception { 154 pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); 155 156 // Ensure that compressed APEX was decompressed in DECOMPRESSED_DIR_PATH 157 List<String> files = getFilesInDir(DECOMPRESSED_DIR_PATH); 158 assertThat(files).contains(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); 159 160 // Match the decompressed apex with original byte for byte 161 final File originalApex = mHostUtils.getTestFile(ORIGINAL_APEX_FILE_NAME); 162 final byte[] originalApexFileBytes = Files.readAllBytes(Paths.get(originalApex.toURI())); 163 final File decompressedFile = getDevice().pullFile( 164 DECOMPRESSED_DIR_PATH + COMPRESSED_APEX_PACKAGE_NAME + "@1" 165 + DECOMPRESSED_APEX_SUFFIX); 166 final byte[] decompressedFileBytes = 167 Files.readAllBytes(Paths.get(decompressedFile.toURI())); 168 assertThat(decompressedFileBytes).isEqualTo(originalApexFileBytes); 169 170 // The decompressed APEX should note be hard linked to APEX_ACTIVE_DIR 171 files = getFilesInDir(APEX_ACTIVE_DIR); 172 assertThat(files).doesNotContain( 173 COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); 174 } 175 176 @Test 177 @LargeTest testDecompressedApexSurvivesReboot()178 public void testDecompressedApexSurvivesReboot() throws Exception { 179 pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); 180 181 // Ensure that compressed APEX was activated from DECOMPRESSED_DIR_PATH 182 List<String> files = getFilesInDir(DECOMPRESSED_DIR_PATH); 183 assertThat(files).contains(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); 184 final File decompressedFile = getDevice().pullFile( 185 DECOMPRESSED_DIR_PATH + COMPRESSED_APEX_PACKAGE_NAME + "@1" 186 + DECOMPRESSED_APEX_SUFFIX); 187 final byte[] decompressedFileBytes = 188 Files.readAllBytes(Paths.get(decompressedFile.toURI())); 189 190 getDevice().reboot(); 191 192 // Ensure it gets activated again on reboot 193 files = getFilesInDir(DECOMPRESSED_DIR_PATH); 194 assertThat(files).contains(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); 195 final File decompressedFileAfterReboot = getDevice().pullFile( 196 DECOMPRESSED_DIR_PATH + COMPRESSED_APEX_PACKAGE_NAME + "@1" 197 + DECOMPRESSED_APEX_SUFFIX); 198 final byte[] decompressedFileBytesAfterReboot = 199 Files.readAllBytes(Paths.get(decompressedFileAfterReboot.toURI())); 200 assertThat(decompressedFileBytes).isEqualTo(decompressedFileBytesAfterReboot); 201 } 202 203 @Test 204 @LargeTest testDecompressionDoesNotHappenOnEveryReboot()205 public void testDecompressionDoesNotHappenOnEveryReboot() throws Exception { 206 pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); 207 208 final String decompressedApexFilePath = DECOMPRESSED_DIR_PATH 209 + COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX; 210 String lastModifiedTime1 = 211 getDevice().executeShellCommand("stat -c %Y " + decompressedApexFilePath); 212 213 getDevice().reboot(); 214 getDevice().waitForDeviceAvailable(); 215 216 String lastModifiedTime2 = 217 getDevice().executeShellCommand("stat -c %Y " + decompressedApexFilePath); 218 assertThat(lastModifiedTime1).isEqualTo(lastModifiedTime2); 219 } 220 221 @Test 222 @LargeTest testHigherVersionOnSystemTriggerDecompression()223 public void testHigherVersionOnSystemTriggerDecompression() throws Exception { 224 // Install v1 on /system partition 225 pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); 226 // On boot, /data partition will have decompressed v1 APEX in it 227 List<String> files = getFilesInDir(DECOMPRESSED_DIR_PATH); 228 assertThat(files).contains(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); 229 230 // Now replace /system APEX with v2 231 getDevice().remountSystemWritable(); 232 getDevice().executeShellCommand("rm -rf /system/apex/" 233 + COMPRESSED_APEX_PACKAGE_NAME + "*apex"); 234 pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v2.capex"); 235 236 // Ensure that v2 was decompressed 237 files = getFilesInDir(DECOMPRESSED_DIR_PATH); 238 assertThat(files).contains(COMPRESSED_APEX_PACKAGE_NAME + "@2" + DECOMPRESSED_APEX_SUFFIX); 239 } 240 241 242 @Test 243 @LargeTest testDifferentRootDigestTriggersDecompression()244 public void testDifferentRootDigestTriggersDecompression() throws Exception { 245 // Install v1 on /system partition 246 pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); 247 // On boot, /data partition will have decompressed v1 APEX in it 248 List<String> files = getFilesInDir(DECOMPRESSED_DIR_PATH); 249 assertThat(files).contains(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); 250 final File decompressedFile = getDevice().pullFile( 251 DECOMPRESSED_DIR_PATH + COMPRESSED_APEX_PACKAGE_NAME + "@1" 252 + DECOMPRESSED_APEX_SUFFIX); 253 final byte[] decompressedFileBytes = 254 Files.readAllBytes(Paths.get(decompressedFile.toURI())); 255 256 // Now replace /system APEX with same version but different root digest 257 getDevice().remountSystemWritable(); 258 getDevice().executeShellCommand("rm -rf /system/apex/" 259 + COMPRESSED_APEX_PACKAGE_NAME + "*apex"); 260 pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1_different_digest.capex"); 261 262 // Ensure that decompressed APEX is different than before 263 files = getFilesInDir(DECOMPRESSED_DIR_PATH); 264 assertThat(files).contains(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); 265 final File decompressedFileAfterReboot = getDevice().pullFile( 266 DECOMPRESSED_DIR_PATH + COMPRESSED_APEX_PACKAGE_NAME + "@1" 267 + DECOMPRESSED_APEX_SUFFIX); 268 final byte[] decompressedFileBytesAfterReboot = 269 Files.readAllBytes(Paths.get(decompressedFileAfterReboot.toURI())); 270 assertThat(decompressedFileBytes).isNotEqualTo(decompressedFileBytesAfterReboot); 271 } 272 273 @Test 274 @LargeTest testUnusedDecompressedApexIsCleanedUp_HigherVersion()275 public void testUnusedDecompressedApexIsCleanedUp_HigherVersion() throws Exception { 276 // Install v1 on /system partition 277 pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); 278 // Ensure that compressed APEX was decompressed in DECOMPRESSED_DIR_PATH 279 List<String> files = getFilesInDir(DECOMPRESSED_DIR_PATH); 280 assertThat(files).contains(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); 281 282 // Now install an update for that APEX so that decompressed APEX becomes redundant 283 runPhase("testUnusedDecompressedApexIsCleanedUp_HigherVersion"); 284 getDevice().reboot(); 285 286 // Verify that the decompressed APEX has been cleaned up 287 String filePath = Paths.get(DECOMPRESSED_DIR_PATH, 288 COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX).toString(); 289 mHostUtils.waitForFileDeleted(filePath, Duration.ofSeconds(15)); 290 } 291 292 @Test 293 @LargeTest testUnusedDecompressedApexIsCleanedUp_SameVersion()294 public void testUnusedDecompressedApexIsCleanedUp_SameVersion() throws Exception { 295 // Install v1 on /system partition 296 pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); 297 // Ensure that compressed APEX was decompressed in DECOMPRESSED_DIR_PATH 298 List<String> files = getFilesInDir(DECOMPRESSED_DIR_PATH); 299 assertThat(files).contains(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); 300 301 // Now install an update for that APEX so that decompressed APEX becomes redundant 302 runPhase("testUnusedDecompressedApexIsCleanedUp_SameVersion"); 303 getDevice().reboot(); 304 305 // Verify that the decompressed APEX has been cleaned up 306 String filePath = Paths.get(DECOMPRESSED_DIR_PATH, 307 COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX).toString(); 308 mHostUtils.waitForFileDeleted(filePath, Duration.ofSeconds(15)); 309 } 310 311 @Test 312 @LargeTest testReservedSpaceIsNotCleanedOnReboot()313 public void testReservedSpaceIsNotCleanedOnReboot() throws Exception { 314 getDevice().executeShellCommand("touch " + OTA_RESERVED_DIR + "random"); 315 316 getDevice().reboot(); 317 318 List<String> files = getFilesInDir(OTA_RESERVED_DIR); 319 assertThat(files).hasSize(1); 320 assertThat(files).contains("random"); 321 } 322 323 @Test 324 @LargeTest testReservedSpaceIsCleanedUpOnDecompression()325 public void testReservedSpaceIsCleanedUpOnDecompression() throws Exception { 326 getDevice().executeShellCommand("touch " + OTA_RESERVED_DIR + "random1"); 327 getDevice().executeShellCommand("touch " + OTA_RESERVED_DIR + "random2"); 328 329 pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); 330 331 assertThat(getFilesInDir(OTA_RESERVED_DIR)).isEmpty(); 332 } 333 334 @Test 335 @LargeTest testFailsToActivateApexOnDataFallbacksToPreInstalled()336 public void testFailsToActivateApexOnDataFallbacksToPreInstalled() throws Exception { 337 // Need to make /system writable before pushing an apex to /data/apex/active/. 338 // Otherwise, `pushTestApex()` below reboots the device to make /system writable 339 // to push an apex to /system/apex. The data apex is removed during that reboot 340 // because it doesn't have a pre-installed system apex yet. 341 getDevice().remountSystemWritable(); 342 343 // Push a data apex that will fail to activate 344 final File file = 345 mHostUtils.getTestFile("com.android.apex.compressed.v2_manifest_mismatch.apex"); 346 getDevice().pushFile(file, APEX_ACTIVE_DIR + COMPRESSED_APEX_PACKAGE_NAME + "@2.apex"); 347 // Push a CAPEX which should act as the fallback 348 // Note that this reboots the device. 349 pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v2.capex"); 350 assertWithMessage("Timed out waiting for device to boot").that( 351 getDevice().waitForBootComplete(Duration.ofMinutes(2).toMillis())).isTrue(); 352 353 // After reboot pre-installed version of shim apex should be activated, and corrupted 354 // version on /data should be deleted. 355 final ITestDevice.ApexInfo activeApex = 356 getActiveApexInfo(COMPRESSED_APEX_PACKAGE_NAME).get(); 357 assertThat(activeApex.versionCode).isEqualTo(2); 358 assertThat(getDevice().doesFileExist( 359 DECOMPRESSED_DIR_PATH + COMPRESSED_APEX_PACKAGE_NAME + "@2" 360 + DECOMPRESSED_APEX_SUFFIX)).isTrue(); 361 assertThat(getDevice().doesFileExist( 362 APEX_ACTIVE_DIR + COMPRESSED_APEX_PACKAGE_NAME + "@2" 363 + DECOMPRESSED_APEX_SUFFIX)).isFalse(); 364 assertThat(getDevice().doesFileExist( 365 APEX_ACTIVE_DIR + COMPRESSED_APEX_PACKAGE_NAME + "@2.apex")).isFalse(); 366 } 367 368 @Test 369 @LargeTest testCapexToApexSwitch()370 public void testCapexToApexSwitch() throws Exception { 371 pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); 372 assertThat(getFilesInDir(DECOMPRESSED_DIR_PATH)) 373 .contains(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); 374 375 // Now replace the CAPEX with an uncompressed APEX 376 getDevice().remountSystemWritable(); 377 getDevice().executeShellCommand("rm -rf /system/apex/" 378 + COMPRESSED_APEX_PACKAGE_NAME + "*apex"); 379 pushTestApex(ORIGINAL_APEX_FILE_NAME); 380 runPhase("testCapexToApexSwitch"); 381 382 // Ensure active apex is running from /system 383 final ITestDevice.ApexInfo activeApex = getActiveApexInfo(COMPRESSED_APEX_PACKAGE_NAME) 384 .orElseThrow(() -> new AssertionError( 385 "Can't find " + COMPRESSED_APEX_PACKAGE_NAME)); 386 assertThat(activeApex.sourceDir).startsWith("/system"); 387 // Ensure previous decompressed APEX has been cleaned up 388 String filePath = Paths.get(DECOMPRESSED_DIR_PATH, 389 COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX).toString(); 390 mHostUtils.waitForFileDeleted(filePath, Duration.ofSeconds(15)); 391 } 392 393 @Test 394 @LargeTest testDecompressedApexVersionAlwaysHasSameVersionAsCapex()395 public void testDecompressedApexVersionAlwaysHasSameVersionAsCapex() throws Exception { 396 pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v2.capex"); 397 // Now replace /system APEX with v1 398 getDevice().remountSystemWritable(); 399 getDevice().executeShellCommand("rm -rf /system/apex/" 400 + COMPRESSED_APEX_PACKAGE_NAME + "*apex"); 401 pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); 402 runPhase("testDecompressedApexVersionAlwaysHasSameVersionAsCapex"); 403 } 404 405 @Test 406 @LargeTest testCompressedApexCanBeRolledBack()407 public void testCompressedApexCanBeRolledBack() throws Exception { 408 pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); 409 410 // Now install update with rollback 411 runPhase("testCompressedApexCanBeRolledBack_Commit"); 412 getDevice().reboot(); 413 414 // Rollback the apex 415 runPhase("testCompressedApexCanBeRolledBack_Rollback"); 416 getDevice().reboot(); 417 418 runPhase("testCompressedApexCanBeRolledBack_Verify"); 419 } 420 421 @Test 422 @LargeTest testOrphanedDecompressedApexInActiveDirIsIgnored()423 public void testOrphanedDecompressedApexInActiveDirIsIgnored() throws Exception { 424 final File apex = mHostUtils.getTestFile( 425 COMPRESSED_APEX_PACKAGE_NAME + ".v1.apex"); 426 // Prepare an APEX in active directory with .decompressed.apex suffix. 427 // Place the same apex in system too. When booting, system APEX should 428 // be mounted while the decomrpessed APEX in active direcotyr should 429 // be ignored. 430 getDevice().remountSystemWritable(); 431 assertTrue(getDevice().pushFile(apex, 432 APEX_ACTIVE_DIR + COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX)); 433 assertTrue(getDevice().pushFile(apex, 434 "/system/apex/" + COMPRESSED_APEX_PACKAGE_NAME + ".v1.apex")); 435 getDevice().reboot(); 436 // Ensure active apex is running from /system 437 final ITestDevice.ApexInfo activeApex = getActiveApexInfo(COMPRESSED_APEX_PACKAGE_NAME) 438 .orElseThrow(() -> new AssertionError( 439 "Can't find " + COMPRESSED_APEX_PACKAGE_NAME)); 440 assertThat(activeApex.sourceDir).startsWith("/system"); 441 // Ensure orphaned decompressed APEX has been cleaned up 442 String filePath = Paths.get(DECOMPRESSED_DIR_PATH, 443 COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX).toString(); 444 mHostUtils.waitForFileDeleted(filePath, Duration.ofSeconds(15)); 445 } 446 } 447 448