Home | History | Annotate | Download | only in compilation
      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         String pathSpec = executeSuShellAdbCommand(1, "pm", "path", APPLICATION_PACKAGE)[0];
    278         pathSpec = pathSpec.replace("package:", "");
    279         assertTrue("Failed find APK " + pathSpec, doesFileExist(pathSpec));
    280         executeSuShellAdbCommand(
    281                 "profman",
    282                 "--create-profile-from=" + targetPathTemp,
    283                 "--apk=" + pathSpec,
    284                 "--dex-location=" + pathSpec,
    285                 "--reference-profile-file=" + targetPath);
    286         executeSuShellAdbCommand(0, "chown", owner, targetPath);
    287         // Verify that the file was written successfully
    288         assertTrue("failed to create profile file", doesFileExist(targetPath));
    289         String[] result = executeSuShellAdbCommand(1, "stat", "-c", "%s", targetPath);
    290         assertTrue("profile " + targetPath + " is " + Integer.parseInt(result[0]) + " bytes",
    291                    Integer.parseInt(result[0]) > 0);
    292     }
    293 
    294     /**
    295      * Parses the value for the key "compiler-filter" out of the output from
    296      * {@code oatdump --header-only}.
    297      */
    298     private String getCompilerFilter(String odexFilePath) throws DeviceNotAvailableException {
    299         String[] response = executeSuShellAdbCommand(
    300                 "oatdump", "--header-only", "--oat-file=" + odexFilePath);
    301         String prefix = "compiler-filter =";
    302         for (String line : response) {
    303             line = line.trim();
    304             if (line.startsWith(prefix)) {
    305                 return line.substring(prefix.length()).trim();
    306             }
    307         }
    308         fail("No occurence of \"" + prefix + "\" in: " + Arrays.toString(response));
    309         return null;
    310     }
    311 
    312     /**
    313      * Returns the path to the application's base.odex file that should have
    314      * been created by the compiler.
    315      */
    316     private String getOdexFilePath() throws DeviceNotAvailableException {
    317         // Something like "package:/data/app/android.cts.compilation-1/base.apk"
    318         String pathSpec = executeSuShellAdbCommand(1, "pm", "path", APPLICATION_PACKAGE)[0];
    319         Matcher matcher = Pattern.compile("^package:(.+/)base\\.apk$").matcher(pathSpec);
    320         boolean found = matcher.find();
    321         assertTrue("Malformed spec: " + pathSpec, found);
    322         String apkDir = matcher.group(1);
    323         // E.g. /data/app/android.cts.compilation-1/oat/arm64/base.odex
    324         String result = executeSuShellAdbCommand(1, "find", apkDir, "-name", "base.odex")[0];
    325         assertTrue("odex file not found: " + result, doesFileExist(result));
    326         return result;
    327     }
    328 
    329     /**
    330      * Returns whether a test that uses the given profileLocations can run
    331      * in the current device configuration. This allows tests to exit early.
    332      *
    333      * <p>Ideally we'd like tests to be marked as skipped/ignored or similar
    334      * rather than passing if they can't run on the current device, but that
    335      * doesn't seem to be supported by CTS as of 2016-05-24.
    336      * TODO: Use Assume.assumeTrue() if this test gets converted to JUnit 4.
    337      */
    338     private boolean canRunTest(Set<ProfileLocation> profileLocations) throws Exception {
    339         boolean result = mCanEnableDeviceRootAccess &&
    340                 (profileLocations.isEmpty() || isUseJitProfiles());
    341         if (!result) {
    342             System.err.printf("Skipping test [mCanEnableDeviceRootAccess=%s, %d profiles] on %s\n",
    343                     mCanEnableDeviceRootAccess, profileLocations.size(), mDevice);
    344         }
    345         return result;
    346     }
    347 
    348     private boolean isUseJitProfiles() throws Exception {
    349         boolean propUseJitProfiles = Boolean.parseBoolean(
    350                 executeSuShellAdbCommand(1, "getprop", "dalvik.vm.usejitprofiles")[0]);
    351         return propUseJitProfiles;
    352     }
    353 
    354     private String[] executeSuShellAdbCommand(int numLinesOutputExpected, String... command)
    355             throws DeviceNotAvailableException {
    356         String[] lines = executeSuShellAdbCommand(command);
    357         assertEquals(
    358                 String.format(Locale.US, "Expected %d lines output, got %d running %s: %s",
    359                         numLinesOutputExpected, lines.length, Arrays.toString(command),
    360                         Arrays.toString(lines)),
    361                 numLinesOutputExpected, lines.length);
    362         return lines;
    363     }
    364 
    365     private String[] executeSuShellAdbCommand(String... command)
    366             throws DeviceNotAvailableException {
    367         // Add `shell su root` to the adb command.
    368         String cmdString = String.join(" ", command);
    369         String output = mDevice.executeShellCommand("su root " + cmdString);
    370         // "".split() returns { "" }, but we want an empty array
    371         String[] lines = output.equals("") ? new String[0] : output.split("\n");
    372         return lines;
    373     }
    374 
    375     private String getSelinuxLabel(String path) throws DeviceNotAvailableException {
    376         // ls -aZ (-a so it sees directories, -Z so it prints the label).
    377         String[] res = executeSuShellAdbCommand(String.format(
    378             "ls -aZ '%s'", path));
    379 
    380         if (res.length == 0) {
    381           return null;
    382         }
    383 
    384         // For directories, it will print many outputs. Filter to first line which contains '.'
    385         // The target line will look like
    386         //      "u:object_r:shell_data_file:s0 /data/local/tmp/android.cts.compilation.primary.prof"
    387         // Remove the second word to only return "u:object_r:shell_data_file:s0".
    388 
    389         return res[0].replaceAll("\\s+.*","");  // remove everything following the first whitespace
    390     }
    391 
    392     private void checkSelinuxLabelMatches(String a, String b) throws DeviceNotAvailableException {
    393       String labelA = getSelinuxLabel(a);
    394       String labelB = getSelinuxLabel(b);
    395 
    396       assertEquals("expected the selinux labels to match", labelA, labelB);
    397     }
    398 
    399     private void executePush(String hostPath, String targetPath, String targetDirectory)
    400             throws DeviceNotAvailableException {
    401         // Cannot push to a privileged directory with one command.
    402         // (i.e. there is no single-command equivalent of 'adb root; adb push src dst')
    403         //
    404         // Push to a tmp directory and then move it to the final destination
    405         // after updating the selinux label.
    406         String tmpPath = "/data/local/tmp/" + APPLICATION_PACKAGE + ".push.tmp";
    407         assertTrue(mDevice.pushFile(new File(hostPath), tmpPath));
    408 
    409         // Important: Use "cp" here because it newly copied files will inherit the security context
    410         // of the targetDirectory according to the default policy.
    411         //
    412         // (Other approaches, such as moving the file retain the invalid security context
    413         // of the tmp directory - b/37425296)
    414         //
    415         // This mimics the behavior of 'adb root; adb push $targetPath'.
    416         executeSuShellAdbCommand("mv", tmpPath, targetPath);
    417 
    418         // Important: Use "restorecon" here because the file in tmpPath retains the
    419         // incompatible security context of /data/local/tmp.
    420         //
    421         // This mimics the behavior of 'adb root; adb push $targetPath'.
    422         executeSuShellAdbCommand("restorecon", targetPath);
    423 
    424         // Validate that the security context of the file matches the security context
    425         // of the directory it was pushed to.
    426         //
    427         // This is a reasonable default behavior to check because most selinux policies
    428         // are configured to behave like this.
    429         checkSelinuxLabelMatches(targetDirectory, targetPath);
    430     }
    431 
    432     private void executePull(String targetPath, String hostPath)
    433             throws DeviceNotAvailableException {
    434         String tmpPath = "/data/local/tmp/" + APPLICATION_PACKAGE + ".pull.tmp";
    435         executeSuShellAdbCommand("cp", targetPath, tmpPath);
    436         try {
    437             executeSuShellAdbCommand("chmod", "606", tmpPath);
    438             assertTrue(mDevice.pullFile(tmpPath, new File(hostPath)));
    439         } finally {
    440             executeSuShellAdbCommand("rm", tmpPath);
    441         }
    442     }
    443 
    444     private boolean doesFileExist(String path) throws DeviceNotAvailableException {
    445         String[] result = executeSuShellAdbCommand("ls", path);
    446         // Testing for empty directories will return an empty array.
    447         return !(result.length > 0 && result[0].contains("No such file"));
    448     }
    449 }
    450