1 /*
2  * Copyright (C) 2016 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 android.compilation.cts;
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.assertEquals;
23 import static org.junit.Assert.assertTrue;
24 import static org.junit.Assert.fail;
25 
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.testtype.junit4.DeviceTestRunOptions;
30 
31 import org.junit.After;
32 import org.junit.Before;
33 import org.junit.Test;
34 import org.junit.runner.RunWith;
35 
36 import java.util.ArrayList;
37 import java.util.Arrays;
38 import java.util.EnumSet;
39 import java.util.List;
40 import java.util.Locale;
41 import java.util.Objects;
42 import java.util.Set;
43 import java.util.regex.Matcher;
44 import java.util.regex.Pattern;
45 
46 /**
47  * Various integration tests for dex to oat compilation, with or without profiles.
48  */
49 @RunWith(DeviceJUnit4ClassRunner.class)
50 public class AdbRootDependentCompilationTest extends BaseHostJUnit4Test {
51     private static final String APPLICATION_PACKAGE = "android.compilation.cts";
52     private static final String APP_USED_BY_OTHER_APP_PACKAGE =
53             "android.compilation.cts.appusedbyotherapp";
54     private static final String APP_USING_OTHER_APP_PACKAGE =
55             "android.compilation.cts.appusingotherapp";
56     private static final String STATUS_CHECKER_PKG = "android.compilation.cts.statuscheckerapp";
57     private static final int PERMISSIONS_LENGTH = 10;
58     private static final int READ_OTHER = 7;
59     private static final String PACKAGE_DEX_USAGE_PATH = "/data/system/package-dex-usage.pb";
60     private static final String PACKAGE_DEX_USAGE_BACKUP_PATH =
61             "/data/local/tmp/package-dex-usage.pb.bak";
62 
63     enum ProfileLocation {
64         CUR("/data/misc/profiles/cur/0/"),
65         REF("/data/misc/profiles/ref/");
66 
67         private String directory;
68 
ProfileLocation(String directory)69         ProfileLocation(String directory) {
70             this.directory = directory;
71         }
72 
getDirectory(String packageName)73         public String getDirectory(String packageName) {
74             return directory + packageName;
75         }
76 
getPath(String packageName)77         public String getPath(String packageName) {
78             return directory + packageName + "/primary.prof";
79         }
80     }
81 
82     private ITestDevice mDevice;
83     private Utils mUtils;
84 
85     @Before
setUp()86     public void setUp() throws Exception {
87         mDevice = getDevice();
88         mUtils = new Utils(getTestInformation());
89 
90         mUtils.installFromResources(getAbi(), "/CtsCompilationApp.apk");
91     }
92 
93     @After
tearDown()94     public void tearDown() throws Exception {
95         mDevice.uninstallPackage(APPLICATION_PACKAGE);
96         mDevice.uninstallPackage(APP_USED_BY_OTHER_APP_PACKAGE);
97         mDevice.uninstallPackage(APP_USING_OTHER_APP_PACKAGE);
98     }
99 
100     /**
101      * Tests compilation using {@code -r bg-dexopt -f}.
102      */
103     @Test
testCompile_bgDexopt()104     public void testCompile_bgDexopt() throws Exception {
105         resetProfileState(APPLICATION_PACKAGE);
106 
107         // Copy the profile to the reference location so that the bg-dexopt
108         // can actually do work if it's configured to speed-profile.
109         for (ProfileLocation profileLocation : EnumSet.of(ProfileLocation.REF)) {
110             writeSystemManagedProfile(
111                     "/CtsCompilationApp.prof", profileLocation, APPLICATION_PACKAGE);
112         }
113 
114         // Usually "speed-profile"
115         String expectedInstallFilter =
116                 Objects.requireNonNull(mDevice.getProperty("pm.dexopt.install"));
117         if (expectedInstallFilter.equals("speed-profile")) {
118             // If the filter is speed-profile but no profile is present, the compiler
119             // will change it to verify.
120             expectedInstallFilter = "verify";
121         }
122         // Usually "speed-profile"
123         String expectedBgDexoptFilter =
124                 Objects.requireNonNull(mDevice.getProperty("pm.dexopt.bg-dexopt"));
125 
126         String odexPath = getOdexFilePath(APPLICATION_PACKAGE);
127         assertEquals(expectedInstallFilter, getCompilerFilter(odexPath));
128 
129         // Without -f, the compiler would only run if it judged the bg-dexopt filter to
130         // be "better" than the install filter. However manufacturers can change those
131         // values so we don't want to depend here on the resulting filter being better.
132         executeCompile(APPLICATION_PACKAGE, "-r", "bg-dexopt", "-f");
133 
134         assertEquals(expectedBgDexoptFilter, getCompilerFilter(odexPath));
135     }
136 
137     /*
138      The tests below test the remaining combinations of the "ref" (reference) and
139      "cur" (current) profile being available. The "cur" profile gets moved/merged
140      into the "ref" profile when it differs enough; as of 2016-05-10, "differs
141      enough" is based on number of methods and classes in profile_assistant.cc.
142 
143      No nonempty profile exists right after an app is installed.
144      Once the app runs, a profile will get collected in "cur" first but
145      may make it to "ref" later. While the profile is being processed by
146      profile_assistant, it may only be available in "ref".
147      */
148 
149     @Test
testCompile_noProfile()150     public void testCompile_noProfile() throws Exception {
151         compileWithProfilesAndCheckFilter(false /* expectOdexChange */,
152                 EnumSet.noneOf(ProfileLocation.class));
153     }
154 
155     @Test
testCompile_curProfile()156     public void testCompile_curProfile() throws Exception {
157         compileWithProfilesAndCheckFilter(true  /* expectOdexChange */,
158                 EnumSet.of(ProfileLocation.CUR));
159         assertTrue("ref profile should have been created by the compiler",
160                 mDevice.doesFileExist(ProfileLocation.REF.getPath(APPLICATION_PACKAGE)));
161     }
162 
163     @Test
testCompile_refProfile()164     public void testCompile_refProfile() throws Exception {
165         compileWithProfilesAndCheckFilter(true /* expectOdexChange */,
166                  EnumSet.of(ProfileLocation.REF));
167         // expect a change in odex because the of the change form
168         // verify -> speed-profile
169     }
170 
171     @Test
testCompile_curAndRefProfile()172     public void testCompile_curAndRefProfile() throws Exception {
173         compileWithProfilesAndCheckFilter(true /* expectOdexChange */,
174                 EnumSet.of(ProfileLocation.CUR, ProfileLocation.REF));
175         // expect a change in odex because the of the change form
176         // verify -> speed-profile
177     }
178 
179     /**
180      * Tests how compilation of an app used by other apps is handled.
181      */
182     @Test
testCompile_usedByOtherApps()183     public void testCompile_usedByOtherApps() throws Exception {
184         mUtils.installFromResources(getAbi(), "/AppUsedByOtherApp.apk", "/AppUsedByOtherApp_1.dm");
185         mUtils.installFromResources(getAbi(), "/AppUsingOtherApp.apk");
186 
187         String odexFilePath = getOdexFilePath(APP_USED_BY_OTHER_APP_PACKAGE);
188         // Initially, the app should be compiled with the cloud profile, and the odex file should be
189         // public.
190         assertThat(getCompilerFilter(odexFilePath)).isEqualTo("speed-profile");
191         assertFileIsPublic(odexFilePath);
192         assertThat(getCompiledMethods(odexFilePath))
193                 .containsExactly("android.compilation.cts.appusedbyotherapp.MyActivity.method2()");
194 
195         // Simulate that the app profile has changed.
196         resetProfileState(APP_USED_BY_OTHER_APP_PACKAGE);
197         writeSystemManagedProfile(
198                 "/AppUsedByOtherApp_2.prof", ProfileLocation.REF, APP_USED_BY_OTHER_APP_PACKAGE);
199 
200         executeCompile(APP_USED_BY_OTHER_APP_PACKAGE, "-m", "speed-profile", "-f");
201         // Right now, the app hasn't been used by any other app yet. It should be compiled with the
202         // new profile, and the odex file should be private.
203         assertThat(getCompilerFilter(odexFilePath)).isEqualTo("speed-profile");
204         assertFileIsPrivate(odexFilePath);
205         assertThat(getCompiledMethods(odexFilePath)).containsExactly(
206                 "android.compilation.cts.appusedbyotherapp.MyActivity.method1()",
207                 "android.compilation.cts.appusedbyotherapp.MyActivity.method2()");
208 
209         executeCompile(APP_USED_BY_OTHER_APP_PACKAGE, "-m", "verify");
210         // The app should not be re-compiled with a worse compiler filter even if the odex file can
211         // be public after then.
212         assertThat(getCompilerFilter(odexFilePath)).isEqualTo("speed-profile");
213 
214         DeviceTestRunOptions options = new DeviceTestRunOptions(APP_USING_OTHER_APP_PACKAGE);
215         options.setTestClassName(APP_USING_OTHER_APP_PACKAGE + ".UsingOtherAppTest");
216         options.setTestMethodName("useOtherApp");
217         runDeviceTests(options);
218 
219         executeCompile(APP_USED_BY_OTHER_APP_PACKAGE, "-m", "speed-profile");
220         // Now, the app has been used by any other app. It should be compiled with the cloud
221         // profile, and the odex file should be public.
222         assertThat(getCompilerFilter(odexFilePath)).isEqualTo("speed-profile");
223         assertFileIsPublic(odexFilePath);
224         assertThat(getCompiledMethods(odexFilePath))
225                 .containsExactly("android.compilation.cts.appusedbyotherapp.MyActivity.method2()");
226     }
227 
228     @Test
testSecondaryDexUseLoading()229     public void testSecondaryDexUseLoading() throws Exception {
230         mUtils.assertCommandSucceeds(
231                 String.format("cp %s %s", PACKAGE_DEX_USAGE_PATH, PACKAGE_DEX_USAGE_BACKUP_PATH));
232         try {
233             mUtils.pushFromResource("/package-dex-usage.pb", PACKAGE_DEX_USAGE_PATH);
234             applyPackageDexUsageChanges();
235 
236             String dump = mUtils.assertCommandSucceeds("pm art dump " + STATUS_CHECKER_PKG);
237             Utils.dumpDoesNotContainDexFile(dump, "bad_1.apk");
238             Utils.dumpDoesNotContainDexFile(dump, "bad_2.apk");
239             Utils.dumpDoesNotContainDexFile(dump, "bad_3.apk");
240             Utils.dumpDoesNotContainDexFile(dump, "bad_4.apk");
241             Utils.dumpContainsDexFile(dump, "good_1.apk");
242             Utils.dumpContainsDexFile(dump, "good_2.apk");
243             Utils.dumpContainsDexFile(dump, "good_3.apk");
244         } finally {
245             mUtils.assertCommandSucceeds(String.format(
246                     "cp %s %s", PACKAGE_DEX_USAGE_BACKUP_PATH, PACKAGE_DEX_USAGE_PATH));
247             applyPackageDexUsageChanges();
248         }
249     }
250 
251     /**
252      * Places the profile in the specified locations, recompiles (without -f)
253      * and checks the compiler-filter in the odex file.
254      */
compileWithProfilesAndCheckFilter(boolean expectOdexChange, Set<ProfileLocation> profileLocations)255     private void compileWithProfilesAndCheckFilter(boolean expectOdexChange,
256             Set<ProfileLocation> profileLocations) throws Exception {
257         resetProfileState(APPLICATION_PACKAGE);
258 
259         executeCompile(APPLICATION_PACKAGE, "-m", "speed-profile", "-f");
260         String odexFilePath = getOdexFilePath(APPLICATION_PACKAGE);
261         String initialOdexFileContents = mDevice.pullFileContents(odexFilePath);
262         // validity check
263         assertWithMessage("empty odex file").that(initialOdexFileContents.length())
264                 .isGreaterThan(0);
265 
266         for (ProfileLocation profileLocation : profileLocations) {
267             writeSystemManagedProfile(
268                     "/CtsCompilationApp.prof", profileLocation, APPLICATION_PACKAGE);
269         }
270         executeCompile(APPLICATION_PACKAGE, "-m", "speed-profile");
271 
272         // Confirm the compiler-filter used in creating the odex file
273         String compilerFilter = getCompilerFilter(odexFilePath);
274 
275         // Without profiles, the compiler filter should be verify.
276         String expectedCompilerFilter = profileLocations.isEmpty() ? "verify" : "speed-profile";
277         assertEquals("compiler-filter", expectedCompilerFilter, compilerFilter);
278 
279         String odexFileContents = mDevice.pullFileContents(odexFilePath);
280         boolean odexChanged = !initialOdexFileContents.equals(odexFileContents);
281         if (odexChanged && !expectOdexChange) {
282             String msg = String.format(Locale.US, "Odex file without filters (%d bytes) "
283                     + "unexpectedly different from odex file (%d bytes) compiled with filters: %s",
284                     initialOdexFileContents.length(), odexFileContents.length(), profileLocations);
285             fail(msg);
286         } else if (!odexChanged && expectOdexChange) {
287             fail("odex file should have changed when recompiling with " + profileLocations);
288         }
289     }
290 
resetProfileState(String packageName)291     private void resetProfileState(String packageName) throws Exception {
292         mDevice.executeShellV2Command("rm -f " + ProfileLocation.REF.getPath(packageName));
293         mDevice.executeShellV2Command("truncate -s 0 " + ProfileLocation.CUR.getPath(packageName));
294     }
295 
296     /**
297      * Invokes the dex2oat compiler on the client.
298      *
299      * @param compileOptions extra options to pass to the compiler on the command line
300      */
executeCompile(String packageName, String... compileOptions)301     private void executeCompile(String packageName, String... compileOptions) throws Exception {
302         List<String> command = new ArrayList<>(Arrays.asList("cmd", "package", "compile"));
303         command.addAll(Arrays.asList(compileOptions));
304         command.add(packageName);
305         String[] commandArray = command.toArray(new String[0]);
306         mUtils.assertCommandSucceeds(commandArray);
307     }
308 
309     /**
310      * Writes the given profile in binary format in a system-managed directory on the device, and
311      * sets appropriate owner.
312      */
writeSystemManagedProfile(String profileResourceName, ProfileLocation location, String packageName)313     private void writeSystemManagedProfile(String profileResourceName, ProfileLocation location,
314             String packageName) throws Exception {
315         String targetPath = location.getPath(packageName);
316         // Get the owner of the parent directory so we can set it on the file
317         String targetDir = location.getDirectory(packageName);
318         assertTrue("Directory " + targetDir + " not found", mDevice.doesFileExist(targetDir));
319         // In format group:user so we can directly pass it to chown.
320         String owner = assertCommandOutputsLines(1, "stat", "-c", "%U:%g", targetDir)[0];
321 
322         mUtils.pushFromResource(profileResourceName, targetPath);
323 
324         // System managed profiles are by default private, unless created from an external profile
325         // such as a cloud profile.
326         mUtils.assertCommandSucceeds("chmod", "640", targetPath);
327         mUtils.assertCommandSucceeds("chown", owner, targetPath);
328     }
329 
330     /**
331      * Parses the value for the key "compiler-filter" out of the output from
332      * {@code oatdump --header-only}.
333      */
getCompilerFilter(String odexFilePath)334     private String getCompilerFilter(String odexFilePath) throws Exception {
335         String[] response = mUtils.assertCommandSucceeds(
336                                           "oatdump", "--header-only", "--oat-file=" + odexFilePath)
337                                     .split("\n");
338         String prefix = "compiler-filter =";
339         for (String line : response) {
340             line = line.trim();
341             if (line.startsWith(prefix)) {
342                 return line.substring(prefix.length()).trim();
343             }
344         }
345         fail("No occurence of \"" + prefix + "\" in: " + Arrays.toString(response));
346         return null;
347     }
348 
349     /**
350      * Returns a list of methods that have native code in the odex file.
351      */
getCompiledMethods(String odexFilePath)352     private List<String> getCompiledMethods(String odexFilePath) throws Exception {
353         // Matches "    CODE: (code_offset=0x000010e0 size=198)...".
354         Pattern codePattern = Pattern.compile("^\\s*CODE:.*size=(\\d+)");
355 
356         // Matches
357         // "  0: void android.compilation.cts.appusedbyotherapp.R.<init>() (dex_method_idx=7)".
358         Pattern methodPattern =
359                 Pattern.compile("((?:\\w+\\.)+[<>\\w]+\\(.*?\\)).*dex_method_idx=\\d+");
360 
361         String[] response =
362                 mUtils.assertCommandSucceeds("oatdump", "--oat-file=" + odexFilePath).split("\n");
363         ArrayList<String> compiledMethods = new ArrayList<>();
364         String currentMethod = null;
365         int currentMethodIndent = -1;
366         for (int i = 0; i < response.length; i++) {
367             // While in a method block.
368             while (currentMethodIndent != -1 && i < response.length
369                     && getIndent(response[i]) > currentMethodIndent) {
370                 Matcher matcher = codePattern.matcher(response[i]);
371                 // The method has code whose size > 0.
372                 if (matcher.find() && Long.parseLong(matcher.group(1)) > 0) {
373                     compiledMethods.add(currentMethod);
374                 }
375                 i++;
376             }
377 
378             if (i >= response.length) {
379                 break;
380             }
381 
382             currentMethod = null;
383             currentMethodIndent = -1;
384 
385             Matcher matcher = methodPattern.matcher(response[i]);
386             if (matcher.find()) {
387                 currentMethod = matcher.group(1);
388                 currentMethodIndent = getIndent(response[i]);
389             }
390         }
391         return compiledMethods;
392     }
393 
394     /**
395      * Returns the number of leading spaces.
396      */
getIndent(String str)397     private int getIndent(String str) {
398         int indent = 0;
399         while (indent < str.length() && str.charAt(indent) == ' ') {
400             indent++;
401         }
402         return indent;
403     }
404 
405     /**
406      * Returns the path to the application's base.odex file that should have
407      * been created by the compiler.
408      */
getOdexFilePath(String packageName)409     private String getOdexFilePath(String packageName) throws Exception {
410         // Something like "package:/data/app/android.compilation.cts-1/base.apk"
411         String pathSpec = assertCommandOutputsLines(1, "pm", "path", packageName)[0];
412         Matcher matcher = Pattern.compile("^package:(.+/)base\\.apk$").matcher(pathSpec);
413         boolean found = matcher.find();
414         assertTrue("Malformed spec: " + pathSpec, found);
415         String apkDir = matcher.group(1);
416         // E.g. /data/app/android.compilation.cts-1/oat/arm64/base.odex
417         String result = assertCommandOutputsLines(1, "find", apkDir, "-name", "base.odex")[0];
418         assertTrue("odex file not found: " + result, mDevice.doesFileExist(result));
419         return result;
420     }
421 
assertCommandOutputsLines(int numLinesOutputExpected, String... command)422     private String[] assertCommandOutputsLines(int numLinesOutputExpected, String... command)
423             throws Exception {
424         String output = mUtils.assertCommandSucceeds(command);
425         // "".split() returns { "" }, but we want an empty array
426         String[] lines = output.equals("") ? new String[0] : output.split("\n");
427         assertEquals(
428                 String.format(Locale.US, "Expected %d lines output, got %d running %s: %s",
429                         numLinesOutputExpected, lines.length, Arrays.toString(command),
430                         Arrays.toString(lines)),
431                 numLinesOutputExpected, lines.length);
432         return lines;
433     }
434 
assertFileIsPublic(String path)435     private void assertFileIsPublic(String path) throws Exception {
436         String permissions = getPermissions(path);
437         assertWithMessage("Expected " + path + " to be public, got " + permissions)
438                 .that(permissions.charAt(READ_OTHER)).isEqualTo('r');
439     }
440 
assertFileIsPrivate(String path)441     private void assertFileIsPrivate(String path) throws Exception {
442         String permissions = getPermissions(path);
443         assertWithMessage("Expected " + path + " to be private, got " + permissions)
444                 .that(permissions.charAt(READ_OTHER)).isEqualTo('-');
445     }
446 
getPermissions(String path)447     private String getPermissions(String path) throws Exception {
448         String permissions = mDevice.getFileEntry(path).getPermissions();
449         assertWithMessage("Invalid permissions string " + permissions).that(permissions.length())
450                 .isEqualTo(PERMISSIONS_LENGTH);
451         return permissions;
452     }
453 
applyPackageDexUsageChanges()454     private void applyPackageDexUsageChanges() throws Exception {
455         mUtils.assertCommandSucceeds(
456                 String.format("chown system:system %s", PACKAGE_DEX_USAGE_PATH));
457         mUtils.assertCommandSucceeds(String.format("chmod 600 %s", PACKAGE_DEX_USAGE_PATH));
458         mUtils.assertCommandSucceeds(String.format("restorecon %s", PACKAGE_DEX_USAGE_PATH));
459         mUtils.softReboot();
460     }
461 }
462