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