Home | History | Annotate | Download | only in cts
      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