| // Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| package com.android.tools.r8; |
| |
| import com.android.ddmlib.AdbCommandRejectedException; |
| import com.android.ddmlib.AndroidDebugBridge; |
| import com.android.ddmlib.FileListingService; |
| import com.android.ddmlib.IDevice; |
| import com.android.ddmlib.IShellOutputReceiver; |
| import com.android.ddmlib.ShellCommandUnresponsiveException; |
| import com.android.ddmlib.SyncException; |
| import com.android.ddmlib.TimeoutException; |
| import com.android.tools.r8.utils.StringUtils; |
| import com.google.common.base.Joiner; |
| import java.io.BufferedReader; |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.FileReader; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.io.PrintStream; |
| import java.nio.file.Path; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.stream.Collectors; |
| |
| /** |
| * This class is used to run tests on devices (or emulators) |
| * using ddmlib. |
| */ |
| public class DeviceRunner { |
| |
| private static final boolean VERBOSE = false; |
| private static final long ADB_CONNECTION_TIMEOUT = 5000; |
| private static final long ADB_WAIT_STEP = ADB_CONNECTION_TIMEOUT / 10; |
| private static final String TEST_SCRIPT_NAME = "test-exit-status.sh"; |
| private static final File TEST_SCRIPT_FILE = |
| new File(System.getProperty("user.dir"), "scripts/" + TEST_SCRIPT_NAME); |
| private static final char PATH_SEPARATOR_CHAR = ':'; |
| private static final String RUNTIME_NAME = "ART"; |
| |
| private List<String> vmOptions = Collections.emptyList(); |
| private Map<String, String> systemProperties = Collections.emptyMap(); |
| private List<File> classpath = Collections.emptyList(); |
| private List<File> bootClasspath = Collections.emptyList(); |
| private String mainClass; |
| private List<String> programArguments = Collections.emptyList(); |
| private OutputStream outRedirectStream = new ByteArrayOutputStream(); |
| |
| private ShellOutputReceiver hostOutput = new ShellOutputReceiver(); |
| |
| public static class DeviceRunnerConfigurationException extends Exception { |
| |
| public DeviceRunnerConfigurationException(String message) { |
| super(message); |
| } |
| } |
| |
| private static class ShellOutputReceiver implements IShellOutputReceiver { |
| |
| private final PrintStream out; |
| |
| public ShellOutputReceiver() { |
| this.out = System.out; |
| } |
| |
| public ShellOutputReceiver(OutputStream out) { |
| this.out = new PrintStream(out); |
| } |
| |
| @Override |
| public void addOutput(byte[] data, int offset, int length) { |
| out.print(new String(Arrays.copyOfRange(data, offset, offset + length))); |
| } |
| |
| @Override |
| public void flush() { |
| } |
| |
| @Override |
| public boolean isCancelled() { |
| return false; |
| } |
| } |
| |
| private static class ShellOutputToStringReceiver implements IShellOutputReceiver { |
| |
| StringBuffer outBuffer = new StringBuffer(); |
| |
| @Override |
| public void addOutput(byte[] data, int offset, int length) { |
| outBuffer.append(new String(Arrays.copyOfRange(data, offset, offset + length))); |
| } |
| |
| @Override |
| public void flush() { |
| } |
| |
| @Override |
| public boolean isCancelled() { |
| return false; |
| } |
| |
| public String getOutput() { |
| return outBuffer.toString(); |
| } |
| } |
| |
| public DeviceRunner() { |
| try { |
| AndroidDebugBridge.init(/* clientSupport */ false); |
| } catch (IllegalStateException ex) { |
| // ADB was already initialized, we're fine, so just ignore. |
| } |
| } |
| |
| public DeviceRunner setVmOptions(List<String> vmOptions) { |
| this.vmOptions = vmOptions; |
| return this; |
| } |
| |
| public DeviceRunner setSystemProperties(Map<String, String> systemProperties) { |
| this.systemProperties = systemProperties; |
| return this; |
| } |
| |
| public DeviceRunner setClasspath(List<File> classpath) { |
| this.classpath = classpath; |
| return this; |
| } |
| |
| public DeviceRunner setBootClasspath(List<File> bootClasspath) { |
| this.bootClasspath = bootClasspath; |
| return this; |
| } |
| |
| public DeviceRunner setMainClass(String mainClass) { |
| this.mainClass = mainClass; |
| return this; |
| } |
| |
| public DeviceRunner setProgramArguments(List<String> programArguments) { |
| this.programArguments = programArguments; |
| return this; |
| } |
| |
| public DeviceRunner setOutputStream(OutputStream outputStream) { |
| outRedirectStream = outputStream; |
| return this; |
| } |
| |
| public ToolHelper.ProcessResult run() throws DeviceRunnerConfigurationException, IOException { |
| |
| AndroidDebugBridge adb = initializeAdb(); |
| |
| IDevice[] connectedDevices = adb.getDevices(); |
| if (connectedDevices.length == 0) { |
| throw new DeviceRunnerConfigurationException("No device found"); |
| } |
| |
| if (connectedDevices.length != 1) { |
| throw new DeviceRunnerConfigurationException( |
| "Running tests on more than one device is not yet supported. " |
| + "Currently connected devices: [" |
| + StringUtils.join(",", Arrays.asList(connectedDevices)) |
| + "]"); |
| } |
| |
| int exitStatus = -1; |
| String uuid = java.util.UUID.randomUUID().toString(); |
| IDevice device = connectedDevices[0]; |
| try { |
| checkDeviceRuntime(device); |
| |
| log("Running on device: " + device.getName()); |
| |
| ensureAdbRoot(device); |
| |
| // Remove trailing '\n' returned by emulator |
| File testsRootDirFile = new File(device.getMountPoint(IDevice.MNT_DATA).replace("\n", ""), |
| "r8-tests-" + uuid); |
| String testsRootDir = convertToTargetPath(testsRootDirFile); |
| |
| String testScriptPathOnTarget = convertToTargetPath( |
| new File(testsRootDirFile, TEST_SCRIPT_NAME)); |
| |
| ClasspathPair destFilePaths = installTestFiles(device, testsRootDirFile, |
| testScriptPathOnTarget); |
| |
| File rootDir = new File(device.getMountPoint(IDevice.MNT_ROOT).replace("\n", "")); |
| |
| // Bug : exit code return by adb shell is wrong (always 0) |
| // https://code.google.com/p/android/issues/detail?id=3254 |
| // Use go team hack to work this around |
| // https://code.google.com/p/go/source/browse/misc/arm/a |
| executeShellCommand(testScriptPathOnTarget + ' ' + uuid + ' ' + Joiner.on(' ').join( |
| buildCommandLine(rootDir, testsRootDir, destFilePaths.bootclasspath, |
| destFilePaths.classpath)), device, new ShellOutputReceiver(outRedirectStream), |
| /* maxTimeToOutputResponse = */10000); |
| exitStatus = getExitStatus(device, testsRootDir); |
| log("Exit status: " + exitStatus); |
| |
| if (exitStatus != 0) { |
| System.err.println("Execution failed on device '" + device.getName() + "'"); |
| } |
| |
| } catch (IOException t) { |
| System.err.println("Error with device '" + device.getName() + "': " + t.getMessage()); |
| t.printStackTrace(); |
| throw t; |
| } finally { |
| deleteTestFiles(device, uuid); |
| } |
| |
| // Convert target line separator to whatever host is for output comparison-based tests |
| String outputAsString = outRedirectStream.toString() |
| .replace("\n", ToolHelper.LINE_SEPARATOR); |
| |
| return new ToolHelper.ProcessResult(exitStatus, outputAsString, ""); |
| } |
| |
| private AndroidDebugBridge initializeAdb() throws DeviceRunnerConfigurationException { |
| AndroidDebugBridge adb = AndroidDebugBridge.createBridge(getAdbLocation(), false); |
| |
| long start = System.currentTimeMillis(); |
| |
| log("Initializing adb..."); |
| |
| while (!isAdbInitialized(adb)) { |
| long timeLeft = start + ADB_CONNECTION_TIMEOUT - System.currentTimeMillis(); |
| if (timeLeft <= 0) { |
| break; |
| } |
| try { |
| Thread.sleep(ADB_WAIT_STEP); |
| } catch (InterruptedException e) { |
| Thread.currentThread().interrupt(); |
| throw new AssertionError(e); |
| } |
| } |
| |
| if (!isAdbInitialized(adb)) { |
| String userDefinedPathToSdk = System.getProperty("ANDROID_SDK_HOME"); |
| if (userDefinedPathToSdk != null) { |
| throw new DeviceRunnerConfigurationException( |
| "Adb not found. Check SDK location '" |
| + userDefinedPathToSdk |
| + "'"); |
| } else { |
| throw new DeviceRunnerConfigurationException( |
| "Adb not found. Set either PATH or ANDROID_SDK_HOME environment variable"); |
| } |
| } |
| |
| log("Done"); |
| return adb; |
| } |
| |
| private static class ClasspathPair { |
| public String[] bootclasspath; |
| public String[] classpath; |
| |
| ClasspathPair(String[] bootclasspath, String[] classpath) { |
| this.bootclasspath = bootclasspath; |
| this.classpath = classpath; |
| } |
| } |
| |
| /** |
| * @return path of classpath and bootclasspath files on device |
| */ |
| private ClasspathPair installTestFiles(IDevice device, File testsRootDirFile, |
| String testScriptPathOnTarget) throws IOException { |
| |
| String testsRootDir = convertToTargetPath(testsRootDirFile); |
| |
| String[] desFilePaths = new String[classpath.size()]; |
| String[] destFileBootCpPaths = new String[bootClasspath.size()]; |
| |
| executeShellCommand("mkdir " + testsRootDir, device); |
| executeShellCommand( |
| "rm " + testsRootDir + FileListingService.FILE_SEPARATOR + "*", device); |
| executePushCommand(TEST_SCRIPT_FILE.getAbsolutePath(), testScriptPathOnTarget, device); |
| executeShellCommand("chmod 777 " + testScriptPathOnTarget, device); |
| |
| int i = 0; |
| for (File f : bootClasspath) { |
| destFileBootCpPaths[i] = convertToTargetPath( |
| new File(testsRootDirFile, "f" + i + "_" + f.getName())); |
| executePushCommand(f.getAbsolutePath(), destFileBootCpPaths[i], device); |
| i++; |
| } |
| i = 0; |
| for (File f : classpath) { |
| desFilePaths[i] = convertToTargetPath( |
| new File(testsRootDirFile, "f" + i + "_" + f.getName())); |
| executePushCommand(f.getAbsolutePath(), desFilePaths[i], device); |
| i++; |
| } |
| return new ClasspathPair(destFileBootCpPaths, desFilePaths); |
| } |
| |
| private int getExitStatus(IDevice device, String testsRootDir) throws IOException { |
| File exitStatusFile = createTempFile("exitStatus", ""); |
| executePullCommand( |
| testsRootDir + "/exitStatus", exitStatusFile.getAbsolutePath(), device); |
| |
| try (BufferedReader br = new BufferedReader(new FileReader(exitStatusFile))) { |
| String readLine = br.readLine(); |
| if (readLine == null) { |
| throw new FileNotFoundException("Exit status not found at " + exitStatusFile.getPath()); |
| } |
| return Integer.parseInt(readLine); |
| } |
| } |
| |
| @SuppressWarnings("deprecation") |
| private void executeShellCommand( |
| String command, |
| IDevice device, |
| ShellOutputReceiver hostOutput, |
| int maxTimeToOutputResponse) throws IOException { |
| log("adb -s " + device.getSerialNumber() + " shell " + command); |
| try { |
| if (maxTimeToOutputResponse != -1) { |
| device.executeShellCommand(command, hostOutput, maxTimeToOutputResponse); |
| } else { |
| device.executeShellCommand(command, hostOutput); |
| } |
| } catch (IOException |
| | ShellCommandUnresponsiveException |
| | TimeoutException |
| | AdbCommandRejectedException e) { |
| throw new IOException( |
| "Failed to execute shell command: '" + command + "'", e); |
| } |
| } |
| |
| private void executeShellCommand(String command, IDevice device) |
| throws IOException { |
| executeShellCommand(command, device, hostOutput, -1); |
| } |
| |
| private void executePushCommand( |
| String srcFile, String destFile, IDevice device) throws IOException { |
| log("adb -s " + device.getSerialNumber() + " push " + srcFile + " " + destFile); |
| try { |
| device.pushFile(srcFile, destFile); |
| } catch (IOException | AdbCommandRejectedException | TimeoutException | SyncException e) { |
| throw new IOException( |
| "Unable to push file '" + srcFile + "' on device into '" + destFile + "'", e); |
| } |
| } |
| |
| private void executePullCommand( |
| String srcFile, String destFile, IDevice device) throws IOException { |
| log("adb -s " + device.getSerialNumber() + " pull " + srcFile + " " + destFile); |
| try { |
| device.pullFile(srcFile, destFile); |
| } catch (IOException | AdbCommandRejectedException | TimeoutException | SyncException e) { |
| throw new IOException( |
| "Unable to pull file '" + srcFile + "' from device into '" + destFile + "'", e); |
| } |
| } |
| |
| private boolean isAdbInitialized(AndroidDebugBridge adb) { |
| return adb.isConnected() && adb.hasInitialDeviceList(); |
| } |
| |
| private String getAdbLocation() { |
| String adbLocation = "adb"; |
| String userSpecifiedSdkLocation = System.getenv("ANDROID_SDK_HOME"); |
| if (userSpecifiedSdkLocation != null) { |
| adbLocation = |
| userSpecifiedSdkLocation |
| + File.separatorChar |
| + "platform-tools" |
| + File.separatorChar |
| + "adb"; |
| } |
| return adbLocation; |
| } |
| |
| |
| private void ensureAdbRoot(IDevice device) throws IOException { |
| boolean isRoot; |
| try { |
| isRoot = device.isRoot(); |
| } catch (TimeoutException |
| | AdbCommandRejectedException |
| | IOException |
| | ShellCommandUnresponsiveException e) { |
| throw new IOException( |
| "Cannot fetch root status for device '" |
| + device.getName() |
| + "(" |
| + device.getSerialNumber() |
| + ")" |
| + "': " |
| + e.getMessage(), |
| e); |
| } |
| |
| int numberOfTries = 0; |
| while (!isRoot && numberOfTries < 5) { |
| try { |
| isRoot = device.root(); |
| } catch (TimeoutException |
| | AdbCommandRejectedException |
| | IOException |
| | ShellCommandUnresponsiveException e1) { |
| // root() seems to throw an IOException: EOF, and it tends |
| // to make the subsequent call to isRoot() fail with |
| // AdbCommandRejectedException: device offline, until adbd is |
| // restarted as root. |
| } finally { |
| try { |
| Thread.sleep(1000); |
| } catch (InterruptedException e1) { |
| Thread.currentThread().interrupt(); |
| } |
| } |
| numberOfTries++; |
| } |
| |
| if (!isRoot) { |
| throw new IOException( |
| "Cannot switch to root on device '" |
| + device.getName() |
| + "(" |
| + device.getSerialNumber() |
| + ")" |
| + "'"); |
| } |
| } |
| |
| private void checkDeviceRuntime(IDevice device) |
| throws DeviceRunnerConfigurationException, IOException { |
| ShellOutputToStringReceiver outputToString = new ShellOutputToStringReceiver(); |
| try { |
| device.executeShellCommand("dalvikvm -showversion", outputToString); |
| if (!outputToString.getOutput().contains(RUNTIME_NAME)) { |
| throw new DeviceRunnerConfigurationException( |
| "The plugged device does not run the required runtime: '" + RUNTIME_NAME + "'"); |
| } |
| } catch (TimeoutException |
| | AdbCommandRejectedException |
| | ShellCommandUnresponsiveException |
| | IOException e) { |
| throw new IOException("Could not check device runtime", e); |
| } |
| } |
| |
| private String convertToTargetPath(File file) { |
| String path = file.getPath(); |
| Path root = file.toPath().getRoot(); |
| if (root != null) { |
| path = path.replace(root.toString(), FileListingService.FILE_SEPARATOR); |
| } |
| return path.replace(File.separator, FileListingService.FILE_SEPARATOR); |
| } |
| |
| private static File createTempFile(String prefix, String suffix) throws IOException { |
| File tmp = File.createTempFile("r8-tests-" + prefix, suffix); |
| tmp.deleteOnExit(); |
| return tmp; |
| } |
| |
| private void deleteTestFiles(IDevice device, String uuid) throws IOException { |
| String deleteCommand = "find data -name '*r8-tests-" + uuid + "*' -exec rm -rf {} +"; |
| log("adb -s " + device.getSerialNumber() + " shell " + deleteCommand); |
| try { |
| device.executeShellCommand(deleteCommand, hostOutput); |
| } catch (TimeoutException |
| | AdbCommandRejectedException |
| | ShellCommandUnresponsiveException |
| | IOException e) { |
| throw new IOException("Error while deleting test file on device", e); |
| } |
| } |
| |
| private List<String> buildCommandLine(File rootDir, String testRootDir, |
| String[] bootClasspathFiles, String[] classpathFiles) { |
| List<String> result = new ArrayList<>(); |
| |
| result.add(convertToTargetPath(new File(rootDir, "/bin/dalvikvm"))); |
| |
| for (String option : vmOptions) { |
| result.add(option); |
| } |
| for (Map.Entry<String, String> entry : systemProperties.entrySet()) { |
| StringBuilder builder = new StringBuilder("-D"); |
| builder.append(entry.getKey()); |
| builder.append("="); |
| builder.append(entry.getValue()); |
| result.add(builder.toString()); |
| } |
| |
| if (classpathFiles.length > 0) { |
| result.add("-cp"); |
| result.add(Joiner.on(PATH_SEPARATOR_CHAR).join( |
| Arrays.asList(classpathFiles).stream() |
| .map(filePath -> convertToTargetPath(new File(filePath))) |
| .collect(Collectors.toList()))); |
| } |
| if (bootClasspathFiles.length > 0) { |
| result.add("-Xbootclasspath:" + Joiner.on(PATH_SEPARATOR_CHAR).join( |
| Arrays.asList(bootClasspathFiles).stream() |
| .map(filePath -> convertToTargetPath(new File(filePath))) |
| .collect(Collectors.toList()))); |
| } |
| |
| if (mainClass != null) { |
| result.add(mainClass); |
| } |
| for (String argument : programArguments) { |
| result.add(argument); |
| } |
| return result; |
| } |
| |
| private void log(String message) { |
| if (VERBOSE) { |
| System.out.println(message); |
| } |
| } |
| } |