blob: a28c7b4abcb4cb1fed2115d012ec5bd13e2875c8 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.compilation.cts;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.testtype.DeviceTestCase;
import com.android.tradefed.util.FileUtil;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Various integration tests for dex to oat compilation, with or without profiles.
* When changing this test, make sure it still passes in each of the following
* configurations:
* <ul>
* <li>On a 'user' build</li>
* <li>On a 'userdebug' build with system property 'dalvik.vm.usejitprofiles' set to false</li>
* <li>On a 'userdebug' build with system property 'dalvik.vm.usejitprofiles' set to true</li>
* </ul>
*/
public class AdbRootDependentCompilationTest extends DeviceTestCase {
private static final String APPLICATION_PACKAGE = "android.compilation.cts";
enum ProfileLocation {
CUR("/data/misc/profiles/cur/0/" + APPLICATION_PACKAGE),
REF("/data/misc/profiles/ref/" + APPLICATION_PACKAGE);
private String directory;
ProfileLocation(String directory) {
this.directory = directory;
}
public String getDirectory() {
return directory;
}
public String getPath() {
return directory + "/primary.prof";
}
}
private ITestDevice mDevice;
private File textProfileFile;
private byte[] initialOdexFileContents;
private File apkFile;
private boolean mCanEnableDeviceRootAccess;
private Matcher mAdbLineFilter;
@Override
protected void setUp() throws Exception {
super.setUp();
mDevice = getDevice();
String buildType = mDevice.getProperty("ro.build.type");
assertTrue("Unknown build type: " + buildType,
Arrays.asList("user", "userdebug", "eng").contains(buildType));
boolean wasRoot = mDevice.isAdbRoot();
// We can only enable root access on userdebug and eng builds.
mCanEnableDeviceRootAccess = buildType.equals("userdebug") || buildType.equals("eng");
apkFile = File.createTempFile("CtsCompilationApp", ".apk");
try (OutputStream outputStream = new FileOutputStream(apkFile)) {
InputStream inputStream = getClass().getResourceAsStream("/CtsCompilationApp.apk");
ByteStreams.copy(inputStream, outputStream);
}
mDevice.uninstallPackage(APPLICATION_PACKAGE); // in case it's still installed
String error = mDevice.installPackage(apkFile, false);
assertNull("Got install error: " + error, error);
// Write the text profile to a temporary file so that we can run profman on it to create a
// real profile.
byte[] profileBytes = ByteStreams.toByteArray(
getClass().getResourceAsStream("/primary.prof.txt"));
assertTrue("empty profile", profileBytes.length > 0); // sanity check
textProfileFile = File.createTempFile("compilationtest", "prof.txt");
Files.write(profileBytes, textProfileFile);
// Ignore issues in cmd.
mAdbLineFilter = Pattern.compile("FORTIFY: pthread_mutex_lock.*").matcher("");
}
@Override
protected void tearDown() throws Exception {
FileUtil.deleteFile(apkFile);
FileUtil.deleteFile(textProfileFile);
mDevice.uninstallPackage(APPLICATION_PACKAGE);
super.tearDown();
}
/**
* Tests compilation using {@code -r bg-dexopt -f}.
*/
public void testCompile_bgDexopt() throws Exception {
if (!canRunTest(EnumSet.noneOf(ProfileLocation.class))) {
return;
}
// Usually "interpret-only"
String expectedInstallFilter = checkNotNull(mDevice.getProperty("pm.dexopt.install"));
// Usually "speed-profile"
String expectedBgDexoptFilter = checkNotNull(mDevice.getProperty("pm.dexopt.bg-dexopt"));
String odexPath = getOdexFilePath();
assertEquals(expectedInstallFilter, getCompilerFilter(odexPath));
// Without -f, the compiler would only run if it judged the bg-dexopt filter to
// be "better" than the install filter. However manufacturers can change those
// values so we don't want to depend here on the resulting filter being better.
executeCompile("-r", "bg-dexopt", "-f");
assertEquals(expectedBgDexoptFilter, getCompilerFilter(odexPath));
}
/*
The tests below test the remaining combinations of the "ref" (reference) and
"cur" (current) profile being available. The "cur" profile gets moved/merged
into the "ref" profile when it differs enough; as of 2016-05-10, "differs
enough" is based on number of methods and classes in profile_assistant.cc.
No nonempty profile exists right after an app is installed.
Once the app runs, a profile will get collected in "cur" first but
may make it to "ref" later. While the profile is being processed by
profile_assistant, it may only be available in "ref".
*/
public void testCompile_noProfile() throws Exception {
compileWithProfilesAndCheckFilter(false /* expectOdexChange */,
EnumSet.noneOf(ProfileLocation.class));
}
public void testCompile_curProfile() throws Exception {
boolean didRun = compileWithProfilesAndCheckFilter(true /* expectOdexChange */,
EnumSet.of(ProfileLocation.CUR));
if (didRun) {
assertTrue("ref profile should have been created by the compiler",
doesFileExist(ProfileLocation.REF.getPath()));
}
}
public void testCompile_refProfile() throws Exception {
compileWithProfilesAndCheckFilter(false /* expectOdexChange */,
EnumSet.of(ProfileLocation.REF));
// We assume that the compiler isn't smart enough to realize that the
// previous odex was compiled before the ref profile was in place, even
// though theoretically it could be.
}
public void testCompile_curAndRefProfile() throws Exception {
compileWithProfilesAndCheckFilter(false /* expectOdexChange */,
EnumSet.of(ProfileLocation.CUR, ProfileLocation.REF));
}
private byte[] readFileOnClient(String clientPath) throws Exception {
assertTrue("File not found on client: " + clientPath,
doesFileExist(clientPath));
File copyOnHost = File.createTempFile("host", "copy");
try {
executePull(clientPath, copyOnHost.getPath());
return Files.toByteArray(copyOnHost);
} finally {
FileUtil.deleteFile(copyOnHost);
}
}
/**
* Places the profile in the specified locations, recompiles (without -f)
* and checks the compiler-filter in the odex file.
*
* @return whether the test ran (as opposed to early exit)
*/
private boolean compileWithProfilesAndCheckFilter(boolean expectOdexChange,
Set<ProfileLocation> profileLocations)
throws Exception {
if (!canRunTest(profileLocations)) {
return false;
}
// ensure no profiles initially present
for (ProfileLocation profileLocation : ProfileLocation.values()) {
String clientPath = profileLocation.getPath();
if (doesFileExist(clientPath)) {
executeSuShellAdbCommand(0, "rm", clientPath);
}
}
executeCompile("-m", "speed-profile", "-f");
String odexFilePath = getOdexFilePath();
byte[] initialOdexFileContents = readFileOnClient(odexFilePath);
assertTrue("empty odex file", initialOdexFileContents.length > 0); // sanity check
for (ProfileLocation profileLocation : profileLocations) {
writeProfile(profileLocation);
}
executeCompile("-m", "speed-profile");
// Confirm the compiler-filter used in creating the odex file
String compilerFilter = getCompilerFilter(odexFilePath);
assertEquals("compiler-filter", "speed-profile", compilerFilter);
byte[] odexFileContents = readFileOnClient(odexFilePath);
boolean odexChanged = !(Arrays.equals(initialOdexFileContents, odexFileContents));
if (odexChanged && !expectOdexChange) {
String msg = String.format(Locale.US, "Odex file without filters (%d bytes) "
+ "unexpectedly different from odex file (%d bytes) compiled with filters: %s",
initialOdexFileContents.length, odexFileContents.length, profileLocations);
fail(msg);
} else if (!odexChanged && expectOdexChange) {
fail("odex file should have changed when recompiling with " + profileLocations);
}
return true;
}
/**
* Invokes the dex2oat compiler on the client.
*
* @param compileOptions extra options to pass to the compiler on the command line
*/
private void executeCompile(String... compileOptions) throws Exception {
List<String> command = new ArrayList<>(Arrays.asList("cmd", "package", "compile"));
command.addAll(Arrays.asList(compileOptions));
command.add(APPLICATION_PACKAGE);
String[] commandArray = command.toArray(new String[0]);
assertEquals("Success", executeSuShellAdbCommand(1, commandArray)[0]);
}
/**
* Copies {@link #textProfileFile} to the device and convert it to a binary profile on the
* client device.
*/
private void writeProfile(ProfileLocation location) throws Exception {
String targetPath = location.getPath();
// Get the owner of the parent directory so we can set it on the file
String targetDir = location.getDirectory();
if (!doesFileExist(targetDir)) {
fail("Not found: " + targetPath);
}
// in format group:user so we can directly pass it to chown
String owner = executeSuShellAdbCommand(1, "stat", "-c", "%U:%g", targetDir)[0];
// for some reason, I've observed the output starting with a single space
while (owner.startsWith(" ")) {
owner = owner.substring(1);
}
String targetPathTemp = targetPath + ".tmp";
executePush(textProfileFile.getAbsolutePath(), targetPathTemp, targetDir);
assertTrue("Failed to push text profile", doesFileExist(targetPathTemp));
String targetPathApk = targetPath + ".apk";
executePush(apkFile.getAbsolutePath(), targetPathApk, targetDir);
assertTrue("Failed to push APK from ", doesFileExist(targetPathApk));
// Run profman to create the real profile on device.
String pathSpec = executeSuShellAdbCommand(1, "pm", "path", APPLICATION_PACKAGE)[0];
pathSpec = pathSpec.replace("package:", "");
assertTrue("Failed find APK " + pathSpec, doesFileExist(pathSpec));
executeSuShellAdbCommand(
"profman",
"--create-profile-from=" + targetPathTemp,
"--apk=" + pathSpec,
"--dex-location=" + pathSpec,
"--reference-profile-file=" + targetPath);
executeSuShellAdbCommand(0, "chown", owner, targetPath);
// Verify that the file was written successfully
assertTrue("failed to create profile file", doesFileExist(targetPath));
String[] result = executeSuShellAdbCommand(1, "stat", "-c", "%s", targetPath);
assertTrue("profile " + targetPath + " is " + Integer.parseInt(result[0]) + " bytes",
Integer.parseInt(result[0]) > 0);
}
/**
* Parses the value for the key "compiler-filter" out of the output from
* {@code oatdump --header-only}.
*/
private String getCompilerFilter(String odexFilePath) throws DeviceNotAvailableException {
String[] response = executeSuShellAdbCommand(
"oatdump", "--header-only", "--oat-file=" + odexFilePath);
String prefix = "compiler-filter =";
for (String line : response) {
line = line.trim();
if (line.startsWith(prefix)) {
return line.substring(prefix.length()).trim();
}
}
fail("No occurence of \"" + prefix + "\" in: " + Arrays.toString(response));
return null;
}
/**
* Returns the path to the application's base.odex file that should have
* been created by the compiler.
*/
private String getOdexFilePath() throws DeviceNotAvailableException {
// Something like "package:/data/app/android.compilation.cts-1/base.apk"
String pathSpec = executeSuShellAdbCommand(1, "pm", "path", APPLICATION_PACKAGE)[0];
Matcher matcher = Pattern.compile("^package:(.+/)base\\.apk$").matcher(pathSpec);
boolean found = matcher.find();
assertTrue("Malformed spec: " + pathSpec, found);
String apkDir = matcher.group(1);
// E.g. /data/app/android.compilation.cts-1/oat/arm64/base.odex
String result = executeSuShellAdbCommand(1, "find", apkDir, "-name", "base.odex")[0];
assertTrue("odex file not found: " + result, doesFileExist(result));
return result;
}
/**
* Returns whether a test that uses the given profileLocations can run
* in the current device configuration. This allows tests to exit early.
*
* <p>Ideally we'd like tests to be marked as skipped/ignored or similar
* rather than passing if they can't run on the current device, but that
* doesn't seem to be supported by CTS as of 2016-05-24.
* TODO: Use Assume.assumeTrue() if this test gets converted to JUnit 4.
*/
private boolean canRunTest(Set<ProfileLocation> profileLocations) throws Exception {
boolean result = mCanEnableDeviceRootAccess &&
(profileLocations.isEmpty() || isUseJitProfiles());
if (!result) {
System.err.printf("Skipping test [mCanEnableDeviceRootAccess=%s, %d profiles] on %s\n",
mCanEnableDeviceRootAccess, profileLocations.size(), mDevice);
}
return result;
}
private boolean isUseJitProfiles() throws Exception {
boolean propUseJitProfiles = Boolean.parseBoolean(
executeSuShellAdbCommand(1, "getprop", "dalvik.vm.usejitprofiles")[0]);
return propUseJitProfiles;
}
private String[] filterAdbLines(String[] lines) {
List<String> linesList = new ArrayList<String>(Arrays.asList(lines));
Iterator<String> it = linesList.iterator();
while (it.hasNext()) {
String line = it.next();
mAdbLineFilter.reset(line);
if (mAdbLineFilter.matches()) {
it.remove();
}
}
if (linesList.size() != lines.length) {
return linesList.toArray(new String[linesList.size()]);
}
return lines;
}
private String[] executeSuShellAdbCommand(int numLinesOutputExpected, String... command)
throws DeviceNotAvailableException {
String[] lines = filterAdbLines(executeSuShellAdbCommand(command));
assertEquals(
String.format(Locale.US, "Expected %d lines output, got %d running %s: %s",
numLinesOutputExpected, lines.length, Arrays.toString(command),
Arrays.toString(lines)),
numLinesOutputExpected, lines.length);
return lines;
}
private String[] executeSuShellAdbCommand(String... command)
throws DeviceNotAvailableException {
// Add `shell su root` to the adb command.
String cmdString = String.join(" ", command);
String output = mDevice.executeShellCommand("su root " + cmdString);
// "".split() returns { "" }, but we want an empty array
String[] lines = output.equals("") ? new String[0] : output.split("\n");
return filterAdbLines(lines);
}
private String getSelinuxLabel(String path) throws DeviceNotAvailableException {
// ls -aZ (-a so it sees directories, -Z so it prints the label).
String[] res = executeSuShellAdbCommand(String.format(
"ls -aZ '%s'", path));
if (res.length == 0) {
return null;
}
// For directories, it will print many outputs. Filter to first line which contains '.'
// The target line will look like
// "u:object_r:shell_data_file:s0 /data/local/tmp/android.compilation.cts.primary.prof"
// Remove the second word to only return "u:object_r:shell_data_file:s0".
return res[0].replaceAll("\\s+.*",""); // remove everything following the first whitespace
}
private void checkSelinuxLabelMatches(String a, String b) throws DeviceNotAvailableException {
String labelA = getSelinuxLabel(a);
String labelB = getSelinuxLabel(b);
assertEquals("expected the selinux labels to match", labelA, labelB);
}
private void executePush(String hostPath, String targetPath, String targetDirectory)
throws DeviceNotAvailableException {
// Cannot push to a privileged directory with one command.
// (i.e. there is no single-command equivalent of 'adb root; adb push src dst')
//
// Push to a tmp directory and then move it to the final destination
// after updating the selinux label.
String tmpPath = "/data/local/tmp/" + APPLICATION_PACKAGE + ".push.tmp";
assertTrue(mDevice.pushFile(new File(hostPath), tmpPath));
// Important: Use "cp" here because it newly copied files will inherit the security context
// of the targetDirectory according to the default policy.
//
// (Other approaches, such as moving the file retain the invalid security context
// of the tmp directory - b/37425296)
//
// This mimics the behavior of 'adb root; adb push $targetPath'.
executeSuShellAdbCommand("mv", tmpPath, targetPath);
// Important: Use "restorecon" here because the file in tmpPath retains the
// incompatible security context of /data/local/tmp.
//
// This mimics the behavior of 'adb root; adb push $targetPath'.
executeSuShellAdbCommand("restorecon", targetPath);
// Validate that the security context of the file matches the security context
// of the directory it was pushed to.
//
// This is a reasonable default behavior to check because most selinux policies
// are configured to behave like this.
checkSelinuxLabelMatches(targetDirectory, targetPath);
}
private void executePull(String targetPath, String hostPath)
throws DeviceNotAvailableException {
String tmpPath = "/data/local/tmp/" + APPLICATION_PACKAGE + ".pull.tmp";
executeSuShellAdbCommand("cp", targetPath, tmpPath);
try {
executeSuShellAdbCommand("chmod", "606", tmpPath);
assertTrue(mDevice.pullFile(tmpPath, new File(hostPath)));
} finally {
executeSuShellAdbCommand("rm", tmpPath);
}
}
private boolean doesFileExist(String path) throws DeviceNotAvailableException {
String[] result = executeSuShellAdbCommand("ls", path);
// Testing for empty directories will return an empty array.
return !(result.length > 0 && result[0].contains("No such file"));
}
}