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_original.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.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 DECOMPRESSED_DIR_PATH does not contain the decompressed APEX 287 files = getFilesInDir(DECOMPRESSED_DIR_PATH); 288 assertThat(files).doesNotContain( 289 COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); 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 DECOMPRESSED_DIR_PATH does not contain the decompressed APEX 306 files = getFilesInDir(DECOMPRESSED_DIR_PATH); 307 assertThat(files).doesNotContain( 308 COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); 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 // Push a data apex that will fail to activate 338 final File file = 339 mHostUtils.getTestFile("com.android.apex.compressed.v2_manifest_mismatch.apex"); 340 getDevice().pushFile(file, APEX_ACTIVE_DIR + COMPRESSED_APEX_PACKAGE_NAME + "@2.apex"); 341 // Push a CAPEX which should act as the fallback 342 pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v2.capex"); 343 assertWithMessage("Timed out waiting for device to boot").that( 344 getDevice().waitForBootComplete(Duration.ofMinutes(2).toMillis())).isTrue(); 345 346 // After reboot pre-installed version of shim apex should be activated, and corrupted 347 // version on /data should be deleted. 348 final ITestDevice.ApexInfo activeApex = 349 getActiveApexInfo(COMPRESSED_APEX_PACKAGE_NAME).get(); 350 assertThat(activeApex.versionCode).isEqualTo(2); 351 assertThat(getDevice().doesFileExist( 352 DECOMPRESSED_DIR_PATH + COMPRESSED_APEX_PACKAGE_NAME + "@2" 353 + DECOMPRESSED_APEX_SUFFIX)).isTrue(); 354 assertThat(getDevice().doesFileExist( 355 APEX_ACTIVE_DIR + COMPRESSED_APEX_PACKAGE_NAME + "@2" 356 + DECOMPRESSED_APEX_SUFFIX)).isFalse(); 357 assertThat(getDevice().doesFileExist( 358 APEX_ACTIVE_DIR + COMPRESSED_APEX_PACKAGE_NAME + "@2.apex")).isFalse(); 359 } 360 361 @Test 362 @LargeTest testCapexToApexSwitch()363 public void testCapexToApexSwitch() throws Exception { 364 pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); 365 assertThat(getFilesInDir(DECOMPRESSED_DIR_PATH)) 366 .contains(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); 367 368 // Now replace the CAPEX with an uncompressed APEX 369 getDevice().remountSystemWritable(); 370 getDevice().executeShellCommand("rm -rf /system/apex/" 371 + COMPRESSED_APEX_PACKAGE_NAME + "*apex"); 372 pushTestApex(ORIGINAL_APEX_FILE_NAME); 373 runPhase("testCapexToApexSwitch"); 374 375 // Ensure active apex is running from /system 376 final ITestDevice.ApexInfo activeApex = getActiveApexInfo(COMPRESSED_APEX_PACKAGE_NAME) 377 .orElseThrow(() -> new AssertionError( 378 "Can't find " + COMPRESSED_APEX_PACKAGE_NAME)); 379 assertThat(activeApex.sourceDir).startsWith("/system"); 380 // Ensure previous decompressed APEX has been cleaned up 381 assertThat(getFilesInDir(DECOMPRESSED_DIR_PATH)) 382 .doesNotContain(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); 383 } 384 385 @Test 386 @LargeTest testDecompressedApexVersionAlwaysHasSameVersionAsCapex()387 public void testDecompressedApexVersionAlwaysHasSameVersionAsCapex() throws Exception { 388 pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v2.capex"); 389 // Now replace /system APEX with v1 390 getDevice().remountSystemWritable(); 391 getDevice().executeShellCommand("rm -rf /system/apex/" 392 + COMPRESSED_APEX_PACKAGE_NAME + "*apex"); 393 pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); 394 runPhase("testDecompressedApexVersionAlwaysHasSameVersionAsCapex"); 395 } 396 397 @Test 398 @LargeTest testCompressedApexCanBeRolledBack()399 public void testCompressedApexCanBeRolledBack() throws Exception { 400 pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); 401 402 // Now install update with rollback 403 runPhase("testCompressedApexCanBeRolledBack_Commit"); 404 getDevice().reboot(); 405 406 // Rollback the apex 407 runPhase("testCompressedApexCanBeRolledBack_Rollback"); 408 getDevice().reboot(); 409 410 runPhase("testCompressedApexCanBeRolledBack_Verify"); 411 } 412 413 @Test 414 @LargeTest testOrphanedDecompressedApexInActiveDirIsIgnored()415 public void testOrphanedDecompressedApexInActiveDirIsIgnored() throws Exception { 416 final File apex = mHostUtils.getTestFile( 417 COMPRESSED_APEX_PACKAGE_NAME + ".v1_original.apex"); 418 // Prepare an APEX in active directory with .decompressed.apex suffix. 419 // Place the same apex in system too. When booting, system APEX should 420 // be mounted while the decomrpessed APEX in active direcotyr should 421 // be ignored. 422 getDevice().remountSystemWritable(); 423 assertTrue(getDevice().pushFile(apex, 424 APEX_ACTIVE_DIR + COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX)); 425 assertTrue(getDevice().pushFile(apex, 426 "/system/apex/" + COMPRESSED_APEX_PACKAGE_NAME + ".v1.apex")); 427 getDevice().reboot(); 428 // Ensure active apex is running from /system 429 final ITestDevice.ApexInfo activeApex = getActiveApexInfo(COMPRESSED_APEX_PACKAGE_NAME) 430 .orElseThrow(() -> new AssertionError( 431 "Can't find " + COMPRESSED_APEX_PACKAGE_NAME)); 432 assertThat(activeApex.sourceDir).startsWith("/system"); 433 // Ensure orphaned decompressed APEX has been cleaned up 434 assertThat(getFilesInDir(APEX_ACTIVE_DIR)) 435 .doesNotContain(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); 436 } 437 } 438 439