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