/* * Copyright 2015-present Facebook, Inc. * * 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 com.facebook.buck.testrunner; import com.android.ddmlib.AndroidDebugBridge; import com.android.ddmlib.DdmPreferences; import com.android.ddmlib.IDevice; import com.android.ddmlib.MultiLineReceiver; import com.android.ddmlib.testrunner.ITestRunListener; import com.android.ddmlib.testrunner.RemoteAndroidTestRunner; import com.android.ddmlib.testrunner.TestIdentifier; import java.io.File; import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map; public class InstrumentationTestRunner { private static final long ADB_CONNECT_TIMEOUT_MS = 5000; private static final long ADB_CONNECT_TIME_STEP_MS = ADB_CONNECT_TIMEOUT_MS / 10; private final String adbExecutablePath; private final String deviceSerial; private final String packageName; private final String testRunner; private final File outputDirectory; private final boolean attemptUninstall; private final Map<String, String> extraInstrumentationArguments; @Nullable private final String instrumentationApkPath; @Nullable private final String apkUnderTestPath; public InstrumentationTestRunner( String adbExecutablePath, String deviceSerial, String packageName, String testRunner, File outputDirectory, String instrumentationApkPath, String apkUnderTestPath, boolean attemptUninstall, Map<String, String> extraInstrumentationArguments) { this.adbExecutablePath = adbExecutablePath; this.deviceSerial = deviceSerial; this.packageName = packageName; this.testRunner = testRunner; this.outputDirectory = outputDirectory; this.instrumentationApkPath = instrumentationApkPath; this.apkUnderTestPath = apkUnderTestPath; this.attemptUninstall = attemptUninstall; this.extraInstrumentationArguments = extraInstrumentationArguments; } public static InstrumentationTestRunner fromArgs(String... args) { File outputDirectory = null; String adbExecutablePath = null; String apkUnderTestPath = null; String packageName = null; String testRunner = null; String instrumentationApkPath = null; boolean attemptUninstall = false; Map<String, String> extraInstrumentationArguments = new HashMap<String, String>(); for (int i = 0; i < args.length; i++) { switch (args[i]) { case "--test-package-name": packageName = args[++i]; break; case "--test-runner": testRunner = args[++i]; break; case "--output": outputDirectory = new File(args[++i]); if (!outputDirectory.exists()) { System.err.printf("The output directory did not exist: %s\n", outputDirectory); System.exit(1); } break; case "--adb-executable-path": adbExecutablePath = args[++i]; break; case "--apk-under-test-path": apkUnderTestPath = args[++i]; break; case "--instrumentation-apk-path": instrumentationApkPath = args[++i]; break; case "--attempt-uninstall": attemptUninstall = true; break; case "--extra-instrumentation-argument": String rawArg = args[++i]; String[] extraArguments = rawArg.split("=", 2); if (extraArguments.length != 2) { System.err.printf("Not a valid extra arguments argument: %s\n", rawArg); System.exit(1); } extraInstrumentationArguments.put(extraArguments[0], extraArguments[1]); break; } } if (packageName == null) { System.err.println("Must pass --test-package-name argument."); System.exit(1); } if (testRunner == null) { System.err.println("Must pass --test-runner argument."); System.exit(1); } if (outputDirectory == null) { System.err.println("Must pass --output argument."); System.exit(1); } if (adbExecutablePath == null) { System.err.println("Must pass --adb-executable-path argument."); System.exit(1); } String deviceSerial = System.getProperty("buck.device.id"); if (deviceSerial == null) { System.err.println("Must pass buck.device.id system property."); System.exit(1); } return new InstrumentationTestRunner( adbExecutablePath, deviceSerial, packageName, testRunner, outputDirectory, instrumentationApkPath, apkUnderTestPath, attemptUninstall, extraInstrumentationArguments); } public void run() throws Throwable { IDevice device = getDevice(this.deviceSerial); if (device == null) { System.err.printf("Unable to get device/emulator with serial %s", this.deviceSerial); System.exit(1); } if (this.instrumentationApkPath != null) { DdmPreferences.setTimeOut(60000); device.installPackage(this.instrumentationApkPath, true); if (this.apkUnderTestPath != null) { device.installPackage(this.apkUnderTestPath, true); } } try { final RemoteAndroidTestRunner runner = new RemoteAndroidTestRunner(this.packageName, this.testRunner, getDevice(deviceSerial)); for (Map.Entry<String, String> entry : this.extraInstrumentationArguments.entrySet()) { runner.addInstrumentationArg(entry.getKey(), entry.getValue()); } BuckXmlTestRunListener listener = new BuckXmlTestRunListener(); ITestRunListener trimLineListener = new ITestRunListener() { /** * Before the actual run starts (and after the InstrumentationResultsParser is created), * we need to do some reflection magic to make RemoteAndroidTestRunner not trim * indentation from lines. */ @Override public void testRunStarted(String runName, int testCount) { setTrimLine(runner, false); } @Override public void testRunEnded(long elapsedTime, Map<String, String> runMetrics) {} @Override public void testRunFailed(String errorMessage) {} @Override public void testStarted(TestIdentifier test) {} @Override public void testFailed(TestIdentifier test, String trace) {} @Override public void testAssumptionFailure(TestIdentifier test, String trace) {} @Override public void testIgnored(TestIdentifier test) {} @Override public void testEnded(TestIdentifier test, Map<String, String> testMetrics) {} @Override public void testRunStopped(long elapsedTime) {} }; listener.setReportDir(this.outputDirectory); runner.run(trimLineListener, listener); } finally { if (this.attemptUninstall) { // Best effort uninstall from the emulator/device. device.uninstallPackage(this.packageName); } } } @Nullable private IDevice getDevice(String serial) throws InterruptedException { AndroidDebugBridge adb = createAdb(); if (adb == null) { System.err.println("Unable to set up adb."); System.exit(1); } IDevice[] allDevices = adb.getDevices(); for (IDevice device : allDevices) { if (device.getSerialNumber().equals(serial)) { return device; } } return null; } private boolean isAdbInitialized(AndroidDebugBridge adb) { return adb.isConnected() && adb.hasInitialDeviceList(); } /** * Creates connection to adb and waits for this connection to be initialized and receive initial * list of devices. */ @Nullable @SuppressWarnings("PMD.EmptyCatchBlock") private AndroidDebugBridge createAdb() throws InterruptedException { AndroidDebugBridge.initIfNeeded(/* clientSupport */ false); AndroidDebugBridge adb = AndroidDebugBridge.createBridge(this.adbExecutablePath, false); if (adb == null) { System.err.println("Failed to connect to adb. Make sure adb server is running."); return null; } long start = System.currentTimeMillis(); while (!isAdbInitialized(adb)) { long timeLeft = start + ADB_CONNECT_TIMEOUT_MS - System.currentTimeMillis(); if (timeLeft <= 0) { break; } Thread.sleep(ADB_CONNECT_TIME_STEP_MS); } return isAdbInitialized(adb) ? adb : null; } // VisibleForTesting static void setTrimLine(RemoteAndroidTestRunner runner, boolean value) { try { Field mParserField = RemoteAndroidTestRunner.class.getDeclaredField("mParser"); mParserField.setAccessible(true); MultiLineReceiver multiLineReceiver = (MultiLineReceiver) mParserField.get(runner); multiLineReceiver.setTrimLine(value); } catch (NoSuchFieldException | IllegalAccessException e) { throw new RuntimeException(e); } } /** We minimize external dependencies, but we'd like to have {@link javax.annotation.Nullable}. */ @interface Nullable {} }