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