blob: ce8f7602497986364a9d9a2a447c244010fcf13b [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.cts.compilation;
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.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.cts.compilation";
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 byte[] profileBytes;
private File localProfileFile;
private File apkFile;
private boolean mCanEnableDeviceRootAccess;
@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
mDevice.installPackage(apkFile, false);
// Load snapshot of file contents from {@link ProfileLocation#CUR} after
// manually running the target application manually for a few minutes.
profileBytes = ByteStreams.toByteArray(getClass().getResourceAsStream("/primary.prof"));
localProfileFile = File.createTempFile("compilationtest", "prof");
Files.write(profileBytes, localProfileFile);
assertTrue("empty profile", profileBytes.length > 0); // sanity check
}
@Override
protected void tearDown() throws Exception {
FileUtil.deleteFile(apkFile);
FileUtil.deleteFile(localProfileFile);
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 {@link #profileBytes} 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 #localProfileFile} to the specified location 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);
}
executePush(localProfileFile.getAbsolutePath(), targetPath);
executeSuShellAdbCommand(0, "chown", owner, targetPath);
// Verify that the file was written successfully
assertTrue("failed to create profile file", doesFileExist(targetPath));
assertEquals(Integer.toString(profileBytes.length),
executeSuShellAdbCommand(1, "stat", "-c", "%s", targetPath)[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.cts.compilation-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.cts.compilation-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[] executeSuShellAdbCommand(int numLinesOutputExpected, String... command)
throws DeviceNotAvailableException {
String[] lines = 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 lines;
}
private void executePush(String hostPath, String targetPath)
throws DeviceNotAvailableException {
String tmpPath = "/data/local/tmp/" + APPLICATION_PACKAGE + ".push.tmp";
assertTrue(mDevice.pushFile(new File(hostPath), tmpPath));
executeSuShellAdbCommand("mv", tmpPath, 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"));
}
}