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 com.google.common.io.ByteStreams; 20 import com.google.common.io.Files; 21 22 import com.android.tradefed.device.DeviceNotAvailableException; 23 import com.android.tradefed.device.ITestDevice; 24 import com.android.tradefed.testtype.DeviceTestCase; 25 import com.android.tradefed.util.FileUtil; 26 27 import java.io.File; 28 import java.io.FileOutputStream; 29 import java.io.InputStream; 30 import java.io.OutputStream; 31 import java.util.ArrayList; 32 import java.util.Arrays; 33 import java.util.EnumSet; 34 import java.util.Iterator; 35 import java.util.List; 36 import java.util.Locale; 37 import java.util.Objects; 38 import java.util.Set; 39 import java.util.regex.Matcher; 40 import java.util.regex.Pattern; 41 42 /** 43 * Various integration tests for dex to oat compilation, with or without profiles. 44 * When changing this test, make sure it still passes in each of the following 45 * configurations: 46 * <ul> 47 * <li>On a 'user' build</li> 48 * <li>On a 'userdebug' build with system property 'dalvik.vm.usejitprofiles' set to false</li> 49 * <li>On a 'userdebug' build with system property 'dalvik.vm.usejitprofiles' set to true</li> 50 * </ul> 51 */ 52 public class AdbRootDependentCompilationTest extends DeviceTestCase { 53 private static final String APPLICATION_PACKAGE = "android.compilation.cts"; 54 55 enum ProfileLocation { 56 CUR("/data/misc/profiles/cur/0/" + APPLICATION_PACKAGE), 57 REF("/data/misc/profiles/ref/" + APPLICATION_PACKAGE); 58 59 private String directory; 60 ProfileLocation(String directory)61 ProfileLocation(String directory) { 62 this.directory = directory; 63 } 64 getDirectory()65 public String getDirectory() { 66 return directory; 67 } 68 getPath()69 public String getPath() { 70 return directory + "/primary.prof"; 71 } 72 } 73 74 private ITestDevice mDevice; 75 private File textProfileFile; 76 private byte[] initialOdexFileContents; 77 private File apkFile; 78 private boolean mCanEnableDeviceRootAccess; 79 80 private Matcher mAdbLineFilter; 81 82 @Override setUp()83 protected void setUp() throws Exception { 84 super.setUp(); 85 mDevice = getDevice(); 86 87 String buildType = mDevice.getProperty("ro.build.type"); 88 assertTrue("Unknown build type: " + buildType, 89 Arrays.asList("user", "userdebug", "eng").contains(buildType)); 90 boolean wasRoot = mDevice.isAdbRoot(); 91 // We can only enable root access on userdebug and eng builds. 92 mCanEnableDeviceRootAccess = buildType.equals("userdebug") || buildType.equals("eng"); 93 94 apkFile = File.createTempFile("CtsCompilationApp", ".apk"); 95 try (OutputStream outputStream = new FileOutputStream(apkFile)) { 96 InputStream inputStream = getClass().getResourceAsStream("/CtsCompilationApp.apk"); 97 ByteStreams.copy(inputStream, outputStream); 98 } 99 mDevice.uninstallPackage(APPLICATION_PACKAGE); // in case it's still installed 100 String error = mDevice.installPackage(apkFile, false); 101 assertNull("Got install error: " + error, error); 102 103 // Write the text profile to a temporary file so that we can run profman on it to create a 104 // real profile. 105 byte[] profileBytes = ByteStreams.toByteArray( 106 getClass().getResourceAsStream("/primary.prof.txt")); 107 assertTrue("empty profile", profileBytes.length > 0); // validity check 108 textProfileFile = File.createTempFile("compilationtest", "prof.txt"); 109 Files.write(profileBytes, textProfileFile); 110 111 // Ignore issues in cmd. 112 mAdbLineFilter = Pattern.compile("FORTIFY: pthread_mutex_lock.*").matcher(""); 113 } 114 115 @Override tearDown()116 protected void tearDown() throws Exception { 117 FileUtil.deleteFile(apkFile); 118 FileUtil.deleteFile(textProfileFile); 119 mDevice.uninstallPackage(APPLICATION_PACKAGE); 120 super.tearDown(); 121 } 122 123 /** 124 * Tests compilation using {@code -r bg-dexopt -f}. 125 */ testCompile_bgDexopt()126 public void testCompile_bgDexopt() throws Exception { 127 if (!canRunTest(EnumSet.noneOf(ProfileLocation.class))) { 128 return; 129 } 130 131 resetProfileState(); 132 133 // Copy the profile to the reference location so that the bg-dexopt 134 // can actually do work if it's configured to speed-profile. 135 for (ProfileLocation profileLocation : EnumSet.of(ProfileLocation.REF)) { 136 writeProfile(profileLocation); 137 } 138 139 // Usually "interpret-only" 140 String expectedInstallFilter = Objects.requireNonNull(mDevice.getProperty("pm.dexopt.install")); 141 if (expectedInstallFilter.equals("speed-profile")) { 142 // If the filter is speed-profile but no profile is present, the compiler 143 // will change it to verify. 144 expectedInstallFilter = "verify"; 145 } 146 // Usually "speed-profile" 147 String expectedBgDexoptFilter = Objects.requireNonNull(mDevice.getProperty("pm.dexopt.bg-dexopt")); 148 149 String odexPath = getOdexFilePath(); 150 assertEquals(expectedInstallFilter, getCompilerFilter(odexPath)); 151 152 // Without -f, the compiler would only run if it judged the bg-dexopt filter to 153 // be "better" than the install filter. However manufacturers can change those 154 // values so we don't want to depend here on the resulting filter being better. 155 executeCompile("-r", "bg-dexopt", "-f"); 156 157 assertEquals(expectedBgDexoptFilter, getCompilerFilter(odexPath)); 158 } 159 160 /* 161 The tests below test the remaining combinations of the "ref" (reference) and 162 "cur" (current) profile being available. The "cur" profile gets moved/merged 163 into the "ref" profile when it differs enough; as of 2016-05-10, "differs 164 enough" is based on number of methods and classes in profile_assistant.cc. 165 166 No nonempty profile exists right after an app is installed. 167 Once the app runs, a profile will get collected in "cur" first but 168 may make it to "ref" later. While the profile is being processed by 169 profile_assistant, it may only be available in "ref". 170 */ 171 testCompile_noProfile()172 public void testCompile_noProfile() throws Exception { 173 compileWithProfilesAndCheckFilter(false /* expectOdexChange */, 174 EnumSet.noneOf(ProfileLocation.class)); 175 } 176 testCompile_curProfile()177 public void testCompile_curProfile() throws Exception { 178 boolean didRun = compileWithProfilesAndCheckFilter(true /* expectOdexChange */, 179 EnumSet.of(ProfileLocation.CUR)); 180 if (didRun) { 181 assertTrue("ref profile should have been created by the compiler", 182 doesFileExist(ProfileLocation.REF.getPath())); 183 } 184 } 185 testCompile_refProfile()186 public void testCompile_refProfile() throws Exception { 187 compileWithProfilesAndCheckFilter(true /* expectOdexChange */, 188 EnumSet.of(ProfileLocation.REF)); 189 // expect a change in odex because the of the change form 190 // verify -> speed-profile 191 } 192 testCompile_curAndRefProfile()193 public void testCompile_curAndRefProfile() throws Exception { 194 compileWithProfilesAndCheckFilter(true /* expectOdexChange */, 195 EnumSet.of(ProfileLocation.CUR, ProfileLocation.REF)); 196 // expect a change in odex because the of the change form 197 // verify -> speed-profile 198 } 199 readFileOnClient(String clientPath)200 private byte[] readFileOnClient(String clientPath) throws Exception { 201 assertTrue("File not found on client: " + clientPath, 202 doesFileExist(clientPath)); 203 File copyOnHost = File.createTempFile("host", "copy"); 204 try { 205 executePull(clientPath, copyOnHost.getPath()); 206 return Files.toByteArray(copyOnHost); 207 } finally { 208 FileUtil.deleteFile(copyOnHost); 209 } 210 } 211 212 /** 213 * Places the profile in the specified locations, recompiles (without -f) 214 * and checks the compiler-filter in the odex file. 215 * 216 * @return whether the test ran (as opposed to early exit) 217 */ compileWithProfilesAndCheckFilter(boolean expectOdexChange, Set<ProfileLocation> profileLocations)218 private boolean compileWithProfilesAndCheckFilter(boolean expectOdexChange, 219 Set<ProfileLocation> profileLocations) 220 throws Exception { 221 if (!canRunTest(profileLocations)) { 222 return false; 223 } 224 225 resetProfileState(); 226 227 executeCompile("-m", "speed-profile", "-f"); 228 String odexFilePath = getOdexFilePath(); 229 byte[] initialOdexFileContents = readFileOnClient(odexFilePath); 230 assertTrue("empty odex file", initialOdexFileContents.length > 0); // validity check 231 232 for (ProfileLocation profileLocation : profileLocations) { 233 writeProfile(profileLocation); 234 } 235 executeCompile("-m", "speed-profile"); 236 237 // Confirm the compiler-filter used in creating the odex file 238 String compilerFilter = getCompilerFilter(odexFilePath); 239 240 // Without profiles, the compiler filter should be verify. 241 String expectedCompilerFilter = profileLocations.isEmpty() ? "verify" : "speed-profile"; 242 assertEquals("compiler-filter", expectedCompilerFilter, compilerFilter); 243 244 byte[] odexFileContents = readFileOnClient(odexFilePath); 245 boolean odexChanged = !(Arrays.equals(initialOdexFileContents, odexFileContents)); 246 if (odexChanged && !expectOdexChange) { 247 String msg = String.format(Locale.US, "Odex file without filters (%d bytes) " 248 + "unexpectedly different from odex file (%d bytes) compiled with filters: %s", 249 initialOdexFileContents.length, odexFileContents.length, profileLocations); 250 fail(msg); 251 } else if (!odexChanged && expectOdexChange) { 252 fail("odex file should have changed when recompiling with " + profileLocations); 253 } 254 return true; 255 } 256 resetProfileState()257 public void resetProfileState() throws Exception { 258 executeSuShellAdbCommand(0, "rm", "-f", ProfileLocation.REF.getPath()); 259 executeSuShellAdbCommand(0, "truncate", "-s", "0", ProfileLocation.CUR.getPath()); 260 } 261 262 /** 263 * Invokes the dex2oat compiler on the client. 264 * 265 * @param compileOptions extra options to pass to the compiler on the command line 266 */ executeCompile(String... compileOptions)267 private void executeCompile(String... compileOptions) throws Exception { 268 List<String> command = new ArrayList<>(Arrays.asList("cmd", "package", "compile")); 269 command.addAll(Arrays.asList(compileOptions)); 270 command.add(APPLICATION_PACKAGE); 271 String[] commandArray = command.toArray(new String[0]); 272 assertEquals("Success", executeSuShellAdbCommand(1, commandArray)[0]); 273 } 274 275 /** 276 * Copies {@link #textProfileFile} to the device and convert it to a binary profile on the 277 * client device. 278 */ writeProfile(ProfileLocation location)279 private void writeProfile(ProfileLocation location) throws Exception { 280 String targetPath = location.getPath(); 281 // Get the owner of the parent directory so we can set it on the file 282 String targetDir = location.getDirectory(); 283 if (!doesFileExist(targetDir)) { 284 fail("Not found: " + targetPath); 285 } 286 // in format group:user so we can directly pass it to chown 287 String owner = executeSuShellAdbCommand(1, "stat", "-c", "%U:%g", targetDir)[0]; 288 // for some reason, I've observed the output starting with a single space 289 while (owner.startsWith(" ")) { 290 owner = owner.substring(1); 291 } 292 293 String targetPathTemp = targetPath + ".tmp"; 294 executePush(textProfileFile.getAbsolutePath(), targetPathTemp, targetDir); 295 assertTrue("Failed to push text profile", doesFileExist(targetPathTemp)); 296 297 String targetPathApk = targetPath + ".apk"; 298 executePush(apkFile.getAbsolutePath(), targetPathApk, targetDir); 299 assertTrue("Failed to push APK from ", doesFileExist(targetPathApk)); 300 // Run profman to create the real profile on device. 301 String pathSpec = executeSuShellAdbCommand(1, "pm", "path", APPLICATION_PACKAGE)[0]; 302 pathSpec = pathSpec.replace("package:", ""); 303 assertTrue("Failed find APK " + pathSpec, doesFileExist(pathSpec)); 304 executeSuShellAdbCommand( 305 "profman", 306 "--create-profile-from=" + targetPathTemp, 307 "--apk=" + pathSpec, 308 "--dex-location=" + pathSpec, 309 "--reference-profile-file=" + targetPath); 310 executeSuShellAdbCommand(0, "chown", owner, targetPath); 311 // Verify that the file was written successfully 312 assertTrue("failed to create profile file", doesFileExist(targetPath)); 313 String[] result = executeSuShellAdbCommand(1, "stat", "-c", "%s", targetPath); 314 assertTrue("profile " + targetPath + " is " + Integer.parseInt(result[0]) + " bytes", 315 Integer.parseInt(result[0]) > 0); 316 } 317 318 /** 319 * Parses the value for the key "compiler-filter" out of the output from 320 * {@code oatdump --header-only}. 321 */ getCompilerFilter(String odexFilePath)322 private String getCompilerFilter(String odexFilePath) throws DeviceNotAvailableException { 323 String[] response = executeSuShellAdbCommand( 324 "oatdump", "--header-only", "--oat-file=" + odexFilePath); 325 String prefix = "compiler-filter ="; 326 for (String line : response) { 327 line = line.trim(); 328 if (line.startsWith(prefix)) { 329 return line.substring(prefix.length()).trim(); 330 } 331 } 332 fail("No occurence of \"" + prefix + "\" in: " + Arrays.toString(response)); 333 return null; 334 } 335 336 /** 337 * Returns the path to the application's base.odex file that should have 338 * been created by the compiler. 339 */ getOdexFilePath()340 private String getOdexFilePath() throws DeviceNotAvailableException { 341 // Something like "package:/data/app/android.compilation.cts-1/base.apk" 342 String pathSpec = executeSuShellAdbCommand(1, "pm", "path", APPLICATION_PACKAGE)[0]; 343 Matcher matcher = Pattern.compile("^package:(.+/)base\\.apk$").matcher(pathSpec); 344 boolean found = matcher.find(); 345 assertTrue("Malformed spec: " + pathSpec, found); 346 String apkDir = matcher.group(1); 347 // E.g. /data/app/android.compilation.cts-1/oat/arm64/base.odex 348 String result = executeSuShellAdbCommand(1, "find", apkDir, "-name", "base.odex")[0]; 349 assertTrue("odex file not found: " + result, doesFileExist(result)); 350 return result; 351 } 352 353 /** 354 * Returns whether a test that uses the given profileLocations can run 355 * in the current device configuration. This allows tests to exit early. 356 * 357 * <p>Ideally we'd like tests to be marked as skipped/ignored or similar 358 * rather than passing if they can't run on the current device, but that 359 * doesn't seem to be supported by CTS as of 2016-05-24. 360 * TODO: Use Assume.assumeTrue() if this test gets converted to JUnit 4. 361 */ canRunTest(Set<ProfileLocation> profileLocations)362 private boolean canRunTest(Set<ProfileLocation> profileLocations) throws Exception { 363 boolean result = mCanEnableDeviceRootAccess && 364 (profileLocations.isEmpty() || isUseJitProfiles()); 365 if (!result) { 366 System.err.printf("Skipping test [mCanEnableDeviceRootAccess=%s, %d profiles] on %s\n", 367 mCanEnableDeviceRootAccess, profileLocations.size(), mDevice); 368 } 369 return result; 370 } 371 isUseJitProfiles()372 private boolean isUseJitProfiles() throws Exception { 373 boolean propUseJitProfiles = Boolean.parseBoolean( 374 executeSuShellAdbCommand(1, "getprop", "dalvik.vm.usejitprofiles")[0]); 375 return propUseJitProfiles; 376 } 377 filterAdbLines(String[] lines)378 private String[] filterAdbLines(String[] lines) { 379 List<String> linesList = new ArrayList<String>(Arrays.asList(lines)); 380 Iterator<String> it = linesList.iterator(); 381 while (it.hasNext()) { 382 String line = it.next(); 383 mAdbLineFilter.reset(line); 384 if (mAdbLineFilter.matches()) { 385 it.remove(); 386 } 387 } 388 if (linesList.size() != lines.length) { 389 return linesList.toArray(new String[linesList.size()]); 390 } 391 return lines; 392 } 393 executeSuShellAdbCommand(int numLinesOutputExpected, String... command)394 private String[] executeSuShellAdbCommand(int numLinesOutputExpected, String... command) 395 throws DeviceNotAvailableException { 396 String[] lines = filterAdbLines(executeSuShellAdbCommand(command)); 397 assertEquals( 398 String.format(Locale.US, "Expected %d lines output, got %d running %s: %s", 399 numLinesOutputExpected, lines.length, Arrays.toString(command), 400 Arrays.toString(lines)), 401 numLinesOutputExpected, lines.length); 402 return lines; 403 } 404 executeSuShellAdbCommand(String... command)405 private String[] executeSuShellAdbCommand(String... command) 406 throws DeviceNotAvailableException { 407 // Add `shell su root` to the adb command. 408 String cmdString = String.join(" ", command); 409 String output = mDevice.executeShellCommand("su root " + cmdString); 410 // "".split() returns { "" }, but we want an empty array 411 String[] lines = output.equals("") ? new String[0] : output.split("\n"); 412 return filterAdbLines(lines); 413 } 414 getSelinuxLabel(String path)415 private String getSelinuxLabel(String path) throws DeviceNotAvailableException { 416 // ls -aZ (-a so it sees directories, -Z so it prints the label). 417 String[] res = executeSuShellAdbCommand(String.format( 418 "ls -aZ '%s'", path)); 419 420 if (res.length == 0) { 421 return null; 422 } 423 424 // For directories, it will print many outputs. Filter to first line which contains '.' 425 // The target line will look like 426 // "u:object_r:shell_data_file:s0 /data/local/tmp/android.compilation.cts.primary.prof" 427 // Remove the second word to only return "u:object_r:shell_data_file:s0". 428 429 return res[0].replaceAll("\\s+.*",""); // remove everything following the first whitespace 430 } 431 checkSelinuxLabelMatches(String a, String b)432 private void checkSelinuxLabelMatches(String a, String b) throws DeviceNotAvailableException { 433 String labelA = getSelinuxLabel(a); 434 String labelB = getSelinuxLabel(b); 435 436 assertEquals("expected the selinux labels to match", labelA, labelB); 437 } 438 executePush(String hostPath, String targetPath, String targetDirectory)439 private void executePush(String hostPath, String targetPath, String targetDirectory) 440 throws DeviceNotAvailableException { 441 // Cannot push to a privileged directory with one command. 442 // (i.e. there is no single-command equivalent of 'adb root; adb push src dst') 443 // 444 // Push to a tmp directory and then move it to the final destination 445 // after updating the selinux label. 446 String tmpPath = "/data/local/tmp/" + APPLICATION_PACKAGE + ".push.tmp"; 447 assertTrue(mDevice.pushFile(new File(hostPath), tmpPath)); 448 449 // Important: Use "cp" here because it newly copied files will inherit the security context 450 // of the targetDirectory according to the default policy. 451 // 452 // (Other approaches, such as moving the file retain the invalid security context 453 // of the tmp directory - b/37425296) 454 // 455 // This mimics the behavior of 'adb root; adb push $targetPath'. 456 executeSuShellAdbCommand("mv", tmpPath, targetPath); 457 458 // Important: Use "restorecon" here because the file in tmpPath retains the 459 // incompatible security context of /data/local/tmp. 460 // 461 // This mimics the behavior of 'adb root; adb push $targetPath'. 462 executeSuShellAdbCommand("restorecon", targetPath); 463 464 // Validate that the security context of the file matches the security context 465 // of the directory it was pushed to. 466 // 467 // This is a reasonable default behavior to check because most selinux policies 468 // are configured to behave like this. 469 checkSelinuxLabelMatches(targetDirectory, targetPath); 470 } 471 executePull(String targetPath, String hostPath)472 private void executePull(String targetPath, String hostPath) 473 throws DeviceNotAvailableException { 474 String tmpPath = "/data/local/tmp/" + APPLICATION_PACKAGE + ".pull.tmp"; 475 executeSuShellAdbCommand("cp", targetPath, tmpPath); 476 try { 477 executeSuShellAdbCommand("chmod", "606", tmpPath); 478 assertTrue(mDevice.pullFile(tmpPath, new File(hostPath))); 479 } finally { 480 executeSuShellAdbCommand("rm", tmpPath); 481 } 482 } 483 doesFileExist(String path)484 private boolean doesFileExist(String path) throws DeviceNotAvailableException { 485 String[] result = executeSuShellAdbCommand("ls", path); 486 // Testing for empty directories will return an empty array. 487 return !(result.length > 0 && result[0].contains("No such file")); 488 } 489 } 490