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