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         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