1 /* 2 * Copyright (C) 2018 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.cts.dexmetadata; 18 19 import static org.junit.Assert.assertArrayEquals; 20 import static org.junit.Assert.assertEquals; 21 import static org.junit.Assert.assertFalse; 22 import static org.junit.Assert.assertNotNull; 23 import static org.junit.Assert.assertTrue; 24 25 import com.android.compatibility.common.util.ApiLevelUtil; 26 import com.android.tradefed.device.ITestDevice; 27 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; 28 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; 29 import com.android.tradefed.util.CommandResult; 30 import com.android.tradefed.util.FileUtil; 31 32 import org.junit.After; 33 import org.junit.Assume; 34 import org.junit.Before; 35 import org.junit.Test; 36 import org.junit.runner.RunWith; 37 38 import java.io.BufferedOutputStream; 39 import java.io.ByteArrayOutputStream; 40 import java.io.File; 41 import java.io.FileInputStream; 42 import java.io.FileOutputStream; 43 import java.io.InputStream; 44 import java.io.OutputStream; 45 import java.nio.ByteBuffer; 46 import java.nio.ByteOrder; 47 import java.nio.file.Files; 48 import java.util.zip.Inflater; 49 import java.util.zip.ZipEntry; 50 import java.util.zip.ZipInputStream; 51 52 /** 53 * Verifies that dex metadata files are installed and updated successfully. 54 */ 55 @RunWith(DeviceJUnit4ClassRunner.class) 56 public class InstallDexMetadataHostTest extends BaseHostJUnit4Test { 57 58 private static final String TEST_PACKAGE = "com.android.cts.dexmetadata"; 59 private static final String TEST_CLASS = TEST_PACKAGE + ".InstallDexMetadataTest"; 60 private static final String INSTALL_PACKAGE = "com.android.cts.dexmetadata.splitapp"; 61 62 private static final String APK_BASE = "CtsDexMetadataSplitApp.apk"; 63 private static final String APK_FEATURE_A = "CtsDexMetadataSplitAppFeatureA.apk"; 64 private static final String APK_BASE_WITH_VDEX = "CtsDexMetadataSplitAppWithVdex.apk"; 65 private static final String APK_FEATURE_A_WITH_VDEX 66 = "CtsDexMetadataSplitAppFeatureAWithVdex.apk"; 67 68 private static final String DM_BASE = "CtsDexMetadataSplitApp.dm"; 69 private static final String DM_S_BASE = "CtsDexMetadataSplitApp-S.dm"; 70 private static final String DM_FEATURE_A = "CtsDexMetadataSplitAppFeatureA.dm"; 71 private static final String DM_BASE_WITH_VDEX = "CtsDexMetadataSplitAppWithVdex.dm"; 72 private static final String DM_FEATURE_A_WITH_VDEX 73 = "CtsDexMetadataSplitAppFeatureAWithVdex.dm"; 74 75 private File mTmpDir; 76 private File mApkBaseFile = null; 77 private File mApkFeatureAFile = null; 78 private File mApkBaseFileWithVdex = null; 79 private File mApkFeatureAFileWithVdex = null; 80 private File mDmBaseFile = null; 81 private File mDmBaseFileForS = null; 82 private File mDmFeatureAFile = null; 83 private File mDmBaseFileWithVdex = null; 84 private File mDmFeatureAFileWithVdex = null; 85 private boolean mShouldRunTests; 86 87 /** 88 * Setup the test. 89 */ 90 @Before setUp()91 public void setUp() throws Exception { 92 ITestDevice device = getDevice(); 93 device.uninstallPackage(INSTALL_PACKAGE); 94 mShouldRunTests = ApiLevelUtil.isAtLeast(getDevice(), 28) 95 || ApiLevelUtil.isAtLeast(getDevice(), "P") 96 || ApiLevelUtil.codenameEquals(getDevice(), "P"); 97 98 Assume.assumeTrue("Skip DexMetadata tests on releases before P.", mShouldRunTests); 99 100 if (mShouldRunTests) { 101 mTmpDir = FileUtil.createTempDir("InstallDexMetadataHostTest"); 102 mApkBaseFile = extractResource(APK_BASE, mTmpDir); 103 mApkFeatureAFile = extractResource(APK_FEATURE_A, mTmpDir); 104 mApkBaseFileWithVdex = extractResource(APK_BASE_WITH_VDEX, mTmpDir); 105 mApkFeatureAFileWithVdex = extractResource(APK_FEATURE_A_WITH_VDEX, mTmpDir); 106 mDmBaseFile = extractResource(DM_BASE, mTmpDir); 107 mDmBaseFileForS = extractResource(DM_S_BASE, mTmpDir); 108 mDmFeatureAFile = extractResource(DM_FEATURE_A, mTmpDir); 109 mDmBaseFileWithVdex = extractResource(DM_BASE_WITH_VDEX, mTmpDir); 110 mDmFeatureAFileWithVdex = extractResource(DM_FEATURE_A_WITH_VDEX, mTmpDir); 111 } 112 } 113 114 /** 115 * Tear down the test. 116 */ 117 @After tearDown()118 public void tearDown() throws Exception { 119 getDevice().uninstallPackage(INSTALL_PACKAGE); 120 FileUtil.recursiveDelete(mTmpDir); 121 } 122 123 /** 124 * Verify .dm installation for stand-alone base (no splits) 125 */ 126 @Test testInstallDmForBase()127 public void testInstallDmForBase() throws Exception { 128 new InstallMultiple().addApk(mApkBaseFile).addDm(mDmBaseFile).run(); 129 assertNotNull(getDevice().getAppPackageInfo(INSTALL_PACKAGE)); 130 131 assertTrue(runDeviceTests(TEST_PACKAGE, TEST_CLASS, "testDmForBase")); 132 } 133 134 /** 135 * Verify .dm installation for base and splits 136 */ 137 @Test testInstallDmForBaseAndSplit()138 public void testInstallDmForBaseAndSplit() throws Exception { 139 new InstallMultiple() 140 .addApk(mApkBaseFile) 141 .addDm(mDmBaseFile) 142 .addApk(mApkFeatureAFile) 143 .addDm(mDmFeatureAFile) 144 .run(); 145 assertNotNull(getDevice().getAppPackageInfo(INSTALL_PACKAGE)); 146 147 assertTrue(runDeviceTests(TEST_PACKAGE, TEST_CLASS, "testDmForBaseAndSplit")); 148 } 149 150 /** 151 * Verify .dm installation for base but not for splits. 152 */ 153 @Test testInstallDmForBaseButNoSplit()154 public void testInstallDmForBaseButNoSplit() throws Exception { 155 new InstallMultiple() 156 .addApk(mApkBaseFile) 157 .addDm(mDmBaseFile) 158 .addApk(mApkFeatureAFile) 159 .run(); 160 assertNotNull(getDevice().getAppPackageInfo(INSTALL_PACKAGE)); 161 162 assertTrue(runDeviceTests(TEST_PACKAGE, TEST_CLASS, "testDmForBaseButNoSplit")); 163 } 164 165 /** 166 * Verify .dm installation for splits but not for base. 167 */ 168 @Test testInstallDmForSplitButNoBase()169 public void testInstallDmForSplitButNoBase() throws Exception { 170 new InstallMultiple() 171 .addApk(mApkBaseFile) 172 .addApk(mApkFeatureAFile) 173 .addDm(mDmFeatureAFile) 174 .run(); 175 assertNotNull(getDevice().getAppPackageInfo(INSTALL_PACKAGE)); 176 177 assertTrue(runDeviceTests(TEST_PACKAGE, TEST_CLASS, "testDmForSplitButNoBase")); 178 } 179 180 /** 181 * Verify that updating .dm files works as expected. 182 */ 183 @Test testUpdateDm()184 public void testUpdateDm() throws Exception { 185 new InstallMultiple() 186 .addApk(mApkBaseFile) 187 .addDm(mDmBaseFile) 188 .addApk(mApkFeatureAFile) 189 .addDm(mDmFeatureAFile) 190 .run(); 191 assertNotNull(getDevice().getAppPackageInfo(INSTALL_PACKAGE)); 192 193 assertTrue(runDeviceTests(TEST_PACKAGE, TEST_CLASS, "testDmForBaseAndSplit")); 194 195 // Remove .dm files during update. 196 new InstallMultiple().addArg("-r").addApk(mApkBaseFile) 197 .addApk(mApkFeatureAFile).run(); 198 assertNotNull(getDevice().getAppPackageInfo(INSTALL_PACKAGE)); 199 200 assertTrue(runDeviceTests(TEST_PACKAGE, TEST_CLASS, "testNoDm")); 201 202 // Add only a split .dm file during update. 203 new InstallMultiple() 204 .addArg("-r") 205 .addApk(mApkBaseFile) 206 .addApk(mApkFeatureAFile) 207 .addDm(mDmFeatureAFile) 208 .run(); 209 assertNotNull(getDevice().getAppPackageInfo(INSTALL_PACKAGE)); 210 211 assertTrue(runDeviceTests(TEST_PACKAGE, TEST_CLASS, "testDmForSplitButNoBase")); 212 } 213 214 /** 215 * Verify .dm installation for base but not for splits and with a .dm name 216 * that doesn't match the apk name. 217 */ 218 @Test testInstallDmForBaseButNoSplitWithNoMatchingDm()219 public void testInstallDmForBaseButNoSplitWithNoMatchingDm() throws Exception { 220 String nonMatchingDmName = mDmFeatureAFile.getName().replace(".dm", ".not.there.dm"); 221 new InstallMultiple() 222 .addApk(mApkBaseFile) 223 .addDm(mDmBaseFile) 224 .addApk(mApkFeatureAFile) 225 .addDm(mDmFeatureAFile, nonMatchingDmName) 226 .run(); 227 assertNotNull(getDevice().getAppPackageInfo(INSTALL_PACKAGE)); 228 229 assertTrue(runDeviceTests(TEST_PACKAGE, TEST_CLASS, "testDmForBaseButNoSplit")); 230 } 231 232 static class ProfileReaderV10 { 233 byte[] data; 234 ProfileReaderV10(byte[] bytes)235 ProfileReaderV10(byte[] bytes) throws Exception { 236 ByteBuffer bb = ByteBuffer.wrap(bytes); 237 238 // Read header. 239 bb.order(ByteOrder.LITTLE_ENDIAN); 240 assertEquals(0x006f7270 /* LE "pro\0" */, bb.getInt()); 241 assertEquals(0x00303130 /* LE "010\0" */, bb.getInt()); 242 bb.get(); // Skip dex file count. 243 int uncompressed_size = bb.getInt(); 244 int compressed_size = bb.getInt(); 245 246 // Decompress profile. 247 Inflater inflater = new Inflater(); 248 inflater.setInput(bb.array(), bb.arrayOffset() + bb.position(), bb.remaining()); 249 data = new byte[uncompressed_size]; 250 assertEquals(uncompressed_size, inflater.inflate(data)); 251 } 252 } 253 254 static class ProfileReaderV15 { 255 byte[] dexFilesData; 256 byte[] extraDescriptorsData; 257 byte[] classesData; 258 byte[] methodsData; 259 ProfileReaderV15(byte[] bytes)260 ProfileReaderV15(byte[] bytes) throws Exception { 261 ByteBuffer bb = ByteBuffer.wrap(bytes); 262 263 // Read header. 264 bb.order(ByteOrder.LITTLE_ENDIAN); 265 assertEquals(0x006f7270 /* LE "pro\0" */, bb.getInt()); 266 assertEquals(0x00353130 /* LE "015\0" */, bb.getInt()); 267 int section_count = bb.getInt(); 268 assertFalse(section_count == 0); 269 270 // Mandatory dex files section. 271 assertEquals(/*kDexFiles*/ 0, bb.getInt()); 272 dexFilesData = readSection(bb); 273 274 // Read optional sections. Assume no more than one occurrence of each known section. 275 for (int i = 1; i != section_count; ++i) { 276 int sectionType = bb.getInt(); 277 switch (sectionType) { 278 case 1: // kExtraDescriptors 279 assertTrue(extraDescriptorsData == null); 280 extraDescriptorsData = readSection(bb); 281 break; 282 case 2: // kClasses 283 assertTrue(classesData == null); 284 classesData = readSection(bb); 285 break; 286 case 3: // kMethods 287 assertTrue(methodsData == null); 288 methodsData = readSection(bb); 289 break; 290 default: 291 // Unknown section. Skip it. New versions of ART are allowed 292 // to add sections that shall be ignored by old versions. 293 skipSection(bb); 294 break; 295 } 296 } 297 } 298 readSection(ByteBuffer bb)299 private byte[] readSection(ByteBuffer bb) throws Exception { 300 int fileOffset = bb.getInt(); 301 int fileSize = bb.getInt(); 302 int inflatedSize = bb.getInt(); 303 if (inflatedSize != 0) { 304 // Decompress section. 305 byte[] data = new byte[inflatedSize]; 306 Inflater inflater = new Inflater(); 307 inflater.setInput(bb.array(), fileOffset, fileSize); 308 assertEquals(inflatedSize, inflater.inflate(data)); 309 return data; 310 } else { 311 // Copy uncompressed data. 312 byte[] data = new byte[fileSize]; 313 System.arraycopy(bb.array(), fileOffset, data, 0, fileSize); 314 return data; 315 } 316 } 317 skipSection(ByteBuffer bb)318 private void skipSection(ByteBuffer bb) { 319 bb.getInt(); // fileOffset 320 bb.getInt(); // fileSize 321 bb.getInt(); // inflatedSize 322 } 323 } 324 325 // This test is questionable because it assumes that ART can understand the format of the 326 // profiles in the DM file passed by this test. 327 // 328 // As ART is updatable, this assumption can be broken by a future change. 329 // 330 // TODO(jiakaiz): Re-evaluate the necessity of having this test in CTS. Maybe move it to MTS. 331 @Test testProfileSnapshotAfterInstall()332 public void testProfileSnapshotAfterInstall() throws Exception { 333 // Determine which profile to use. 334 boolean useProfileForS = ApiLevelUtil.isAtLeast(getDevice(), "S"); 335 336 // Install the app. 337 File dmBaseFile = useProfileForS ? mDmBaseFileForS : mDmBaseFile; 338 String dmName = mDmBaseFile.getName(); // APK name with ".apk" replaced by ".dm". 339 new InstallMultiple().addApk(mApkBaseFile).addDm(dmBaseFile, dmName).run(); 340 341 // Take a snapshot of the installed profile. 342 String snapshotCmd = "cmd package snapshot-profile " + INSTALL_PACKAGE; 343 CommandResult result = getDevice().executeShellV2Command(snapshotCmd); 344 assertEquals(result.getStdout().trim() /* message */, 0L, (long) result.getExitCode()); 345 346 // Extract the profile bytes from the dex metadata and from the profile snapshot. 347 byte[] rawDeviceProfile = extractProfileSnapshotFromDevice(); 348 byte[] rawMetadataProfile = extractProfileFromDexMetadata(dmBaseFile); 349 if (useProfileForS) { 350 ProfileReaderV15 snapshotReader = new ProfileReaderV15(rawDeviceProfile); 351 ProfileReaderV15 expectedReader = new ProfileReaderV15(rawMetadataProfile); 352 353 assertArrayEquals(expectedReader.dexFilesData, snapshotReader.dexFilesData); 354 assertArrayEquals( 355 expectedReader.extraDescriptorsData, snapshotReader.extraDescriptorsData); 356 assertArrayEquals(expectedReader.classesData, snapshotReader.classesData); 357 assertArrayEquals(expectedReader.methodsData, snapshotReader.methodsData); 358 } else { 359 byte[] snapshotProfileBytes = new ProfileReaderV10(rawDeviceProfile).data; 360 byte[] expectedProfileBytes = new ProfileReaderV10(rawMetadataProfile).data; 361 362 assertArrayEquals(expectedProfileBytes, snapshotProfileBytes); 363 } 364 } 365 366 /** 367 * Verify .dm installation for stand-alone base (no splits) with vdex file. 368 */ 369 @Test testInstallDmForBaseWithVdex()370 public void testInstallDmForBaseWithVdex() throws Exception { 371 new InstallMultiple().addApk(mApkBaseFileWithVdex).addDm(mDmBaseFileWithVdex).run(); 372 assertNotNull(getDevice().getAppPackageInfo(INSTALL_PACKAGE)); 373 374 assertTrue(runDeviceTests(TEST_PACKAGE, TEST_CLASS, "testDmForBase")); 375 } 376 377 /** 378 * Verify .dm installation for base and splits with vdex files. 379 */ 380 @Test testInstallDmForBaseAndSplitWithVdex()381 public void testInstallDmForBaseAndSplitWithVdex() throws Exception { 382 new InstallMultiple() 383 .addApk(mApkBaseFileWithVdex) 384 .addDm(mDmBaseFileWithVdex) 385 .addApk(mApkFeatureAFileWithVdex) 386 .addDm(mDmFeatureAFileWithVdex) 387 .run(); 388 assertNotNull(getDevice().getAppPackageInfo(INSTALL_PACKAGE)); 389 390 assertTrue(runDeviceTests(TEST_PACKAGE, TEST_CLASS, "testDmForBaseAndSplit")); 391 } 392 393 /** Verify .dm installation for split-only install. */ 394 @Test testInstallDmForSplitOnlyInstall()395 public void testInstallDmForSplitOnlyInstall() throws Exception { 396 new InstallMultiple().addApk(mApkBaseFile).addDm(mDmBaseFile).run(); 397 assertNotNull(getDevice().getAppPackageInfo(INSTALL_PACKAGE)); 398 399 new InstallMultiple() 400 .inheritFrom(TEST_PACKAGE) 401 .addApk(mApkFeatureAFile) 402 .addDm(mDmFeatureAFile) 403 .runExpectingFailure(); 404 assertNotNull(getDevice().getAppPackageInfo(INSTALL_PACKAGE)); 405 } 406 407 /** Extracts the profile bytes for the snapshot captured with 'cmd package snapshot-profile' */ extractProfileSnapshotFromDevice()408 private byte[] extractProfileSnapshotFromDevice() throws Exception { 409 File snapshotFile = File.createTempFile(INSTALL_PACKAGE, "primary.prof"); 410 snapshotFile.deleteOnExit(); 411 getDevice().pullFile(getSnapshotLocation(INSTALL_PACKAGE), snapshotFile); 412 return Files.readAllBytes(snapshotFile.toPath()); 413 } 414 getSnapshotLocation(String pkg)415 static private String getSnapshotLocation(String pkg) { 416 return "/data/misc/profman/" + pkg + ".prof"; 417 } 418 419 /** Extracts the profile bytes from the dex metadata profile. */ extractProfileFromDexMetadata(File dmFile)420 static private byte[] extractProfileFromDexMetadata(File dmFile) throws Exception { 421 try (ZipInputStream in = new ZipInputStream(new FileInputStream(dmFile))) { 422 for (ZipEntry ze; (ze = in.getNextEntry()) != null; ) { 423 if (!"primary.prof".equals(ze.getName())) { 424 continue; 425 } 426 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 427 428 final byte[] buffer = new byte[128]; 429 for (int count; (count = in.read(buffer)) != -1; ) { 430 bos.write(buffer, 0, count); 431 } 432 return bos.toByteArray(); 433 } 434 } 435 throw new IllegalArgumentException("primary.prof not found in the .dm file"); 436 } 437 438 /** 439 * Extract a resource into the given directory and return a reference to its file. 440 */ extractResource(String fullResourceName, File outputDir)441 private File extractResource(String fullResourceName, File outputDir) 442 throws Exception { 443 File outputFile = new File(outputDir, fullResourceName); 444 try (InputStream in = getClass().getResourceAsStream("/" + fullResourceName); 445 OutputStream out = new BufferedOutputStream(new FileOutputStream(outputFile))) { 446 if (in == null) { 447 throw new IllegalArgumentException("Resource not found: " + fullResourceName); 448 } 449 byte[] buf = new byte[65536]; 450 int chunkSize; 451 while ((chunkSize = in.read(buf)) != -1) { 452 out.write(buf, 0, chunkSize); 453 } 454 } 455 return outputFile; 456 } 457 458 private class InstallMultiple extends BaseInstallMultiple<InstallMultiple> { InstallMultiple()459 InstallMultiple() { 460 super(getDevice(), getBuild()); 461 } 462 } 463 } 464