|  | // 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); | 
|  | } | 
|  | } | 
|  | } |