aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/RemoteAdtTestRunner.java
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/RemoteAdtTestRunner.java')
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/RemoteAdtTestRunner.java524
1 files changed, 524 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/RemoteAdtTestRunner.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/RemoteAdtTestRunner.java
new file mode 100755
index 000000000..a48acd324
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/RemoteAdtTestRunner.java
@@ -0,0 +1,524 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.android.ide.eclipse.adt.internal.launch.junit.runtime;
+
+import com.android.ddmlib.AdbCommandRejectedException;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.ShellCommandUnresponsiveException;
+import com.android.ddmlib.TimeoutException;
+import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner.TestSize;
+import com.android.ddmlib.testrunner.ITestRunListener;
+import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
+import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.launch.LaunchMessages;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jdt.internal.junit.runner.IListensToTestExecutions;
+import org.eclipse.jdt.internal.junit.runner.ITestReference;
+import org.eclipse.jdt.internal.junit.runner.MessageIds;
+import org.eclipse.jdt.internal.junit.runner.RemoteTestRunner;
+import org.eclipse.jdt.internal.junit.runner.TestExecution;
+import org.eclipse.jdt.internal.junit.runner.TestReferenceFailure;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Supports Eclipse JUnit execution of Android tests.
+ * <p/>
+ * Communicates back to a Eclipse JDT JUnit client via a socket connection.
+ *
+ * @see org.eclipse.jdt.internal.junit.runner.RemoteTestRunner for more details on the protocol
+ */
+@SuppressWarnings("restriction")
+public class RemoteAdtTestRunner extends RemoteTestRunner {
+
+ private static final String DELAY_MSEC_KEY = "delay_msec";
+ /** the delay between each test execution when in collecting test info */
+ private static final String COLLECT_TEST_DELAY_MS = "15";
+
+ private AndroidJUnitLaunchInfo mLaunchInfo;
+ private TestExecution mExecution;
+
+ /**
+ * Initialize the JDT JUnit test runner parameters from the {@code args}.
+ *
+ * @param args name-value pair of arguments to pass to parent JUnit runner.
+ * @param launchInfo the Android specific test launch info
+ */
+ protected void init(String[] args, AndroidJUnitLaunchInfo launchInfo) {
+ defaultInit(args);
+ mLaunchInfo = launchInfo;
+ }
+
+ /**
+ * Runs a set of tests, and reports back results using parent class.
+ * <p/>
+ * JDT Unit expects to be sent data in the following sequence:
+ * <ol>
+ * <li>The total number of tests to be executed.</li>
+ * <li>The test 'tree' data about the tests to be executed, which is composed of the set of
+ * test class names, the number of tests in each class, and the names of each test in the
+ * class.</li>
+ * <li>The test execution result for each test method. Expects individual notifications of
+ * the test execution start, any failures, and the end of the test execution.</li>
+ * <li>The end of the test run, with its elapsed time.</li>
+ * </ol>
+ * <p/>
+ * In order to satisfy this, this method performs two actual Android instrumentation runs.
+ * The first is a 'log only' run that will collect the test tree data, without actually
+ * executing the tests, and send it back to JDT JUnit. The second is the actual test execution,
+ * whose results will be communicated back in real-time to JDT JUnit.
+ *
+ * The tests are run concurrently on all devices. The overall structure is as follows:
+ * <ol>
+ * <li> First, a separate job per device is run to collect test tree data. A per device
+ * {@link TestCollector} records information regarding the tests run on the device.
+ * </li>
+ * <li> Once all the devices have finished collecting the test tree data, the tree info is
+ * collected from all of them and passed to the Junit UI </li>
+ * <li> A job per device is again launched to do the actual test run. A per device
+ * {@link TestRunListener} notifies the shared {@link TestResultsNotifier} of test
+ * status. </li>
+ * <li> As tests complete, the test run listener updates the Junit UI </li>
+ * </ol>
+ *
+ * @param testClassNames ignored - the AndroidJUnitLaunchInfo will be used to determine which
+ * tests to run.
+ * @param testName ignored
+ * @param execution used to report test progress
+ */
+ @Override
+ public void runTests(String[] testClassNames, String testName, TestExecution execution) {
+ // hold onto this execution reference so it can be used to report test progress
+ mExecution = execution;
+
+ List<IDevice> devices = new ArrayList<IDevice>(mLaunchInfo.getDevices());
+ List<RemoteAndroidTestRunner> runners =
+ new ArrayList<RemoteAndroidTestRunner>(devices.size());
+
+ for (IDevice device : devices) {
+ RemoteAndroidTestRunner runner = new RemoteAndroidTestRunner(
+ mLaunchInfo.getAppPackage(), mLaunchInfo.getRunner(), device);
+
+ if (mLaunchInfo.getTestClass() != null) {
+ if (mLaunchInfo.getTestMethod() != null) {
+ runner.setMethodName(mLaunchInfo.getTestClass(), mLaunchInfo.getTestMethod());
+ } else {
+ runner.setClassName(mLaunchInfo.getTestClass());
+ }
+ }
+
+ if (mLaunchInfo.getTestPackage() != null) {
+ runner.setTestPackageName(mLaunchInfo.getTestPackage());
+ }
+
+ TestSize size = mLaunchInfo.getTestSize();
+ if (size != null) {
+ runner.setTestSize(size);
+ }
+
+ runners.add(runner);
+ }
+
+ // Launch all test info collector jobs
+ List<TestTreeCollectorJob> collectorJobs =
+ new ArrayList<TestTreeCollectorJob>(devices.size());
+ List<TestCollector> perDeviceCollectors = new ArrayList<TestCollector>(devices.size());
+ for (int i = 0; i < devices.size(); i++) {
+ RemoteAndroidTestRunner runner = runners.get(i);
+ String deviceName = devices.get(i).getName();
+ TestCollector collector = new TestCollector(deviceName);
+ perDeviceCollectors.add(collector);
+
+ TestTreeCollectorJob job = new TestTreeCollectorJob(
+ "Test Tree Collector for " + deviceName,
+ runner, mLaunchInfo.isDebugMode(), collector);
+ job.setPriority(Job.INTERACTIVE);
+ job.schedule();
+
+ collectorJobs.add(job);
+ }
+
+ // wait for all test info collector jobs to complete
+ int totalTests = 0;
+ for (TestTreeCollectorJob job : collectorJobs) {
+ try {
+ job.join();
+ } catch (InterruptedException e) {
+ endTestRunWithError(e.getMessage());
+ return;
+ }
+
+ if (!job.getResult().isOK()) {
+ endTestRunWithError(job.getResult().getMessage());
+ return;
+ }
+
+ TestCollector collector = job.getCollector();
+ String err = collector.getErrorMessage();
+ if (err != null) {
+ endTestRunWithError(err);
+ return;
+ }
+
+ totalTests += collector.getTestCaseCount();
+ }
+
+ AdtPlugin.printToConsole(mLaunchInfo.getProject(), "Sending test information to Eclipse");
+ notifyTestRunStarted(totalTests);
+ sendTestTrees(perDeviceCollectors);
+
+ List<TestRunnerJob> instrumentationRunnerJobs =
+ new ArrayList<TestRunnerJob>(devices.size());
+
+ TestResultsNotifier notifier = new TestResultsNotifier(mExecution.getListener(),
+ devices.size());
+
+ // Spawn all instrumentation runner jobs
+ for (int i = 0; i < devices.size(); i++) {
+ RemoteAndroidTestRunner runner = runners.get(i);
+ String deviceName = devices.get(i).getName();
+ TestRunListener testRunListener = new TestRunListener(deviceName, notifier);
+ InstrumentationRunJob job = new InstrumentationRunJob(
+ "Test Tree Collector for " + deviceName,
+ runner, mLaunchInfo.isDebugMode(), testRunListener);
+ job.setPriority(Job.INTERACTIVE);
+ job.schedule();
+
+ instrumentationRunnerJobs.add(job);
+ }
+
+ // Wait for all jobs to complete
+ for (TestRunnerJob job : instrumentationRunnerJobs) {
+ try {
+ job.join();
+ } catch (InterruptedException e) {
+ endTestRunWithError(e.getMessage());
+ return;
+ }
+
+ if (!job.getResult().isOK()) {
+ endTestRunWithError(job.getResult().getMessage());
+ return;
+ }
+ }
+ }
+
+ /** Sends info about the test tree to be executed (ie the suites and their enclosed tests) */
+ private void sendTestTrees(List<TestCollector> perDeviceCollectors) {
+ for (TestCollector c : perDeviceCollectors) {
+ ITestReference ref = c.getDeviceSuite();
+ ref.sendTree(this);
+ }
+ }
+
+ private static abstract class TestRunnerJob extends Job {
+ private ITestRunListener mListener;
+ private RemoteAndroidTestRunner mRunner;
+ private boolean mIsDebug;
+
+ public TestRunnerJob(String name, RemoteAndroidTestRunner runner,
+ boolean isDebug, ITestRunListener listener) {
+ super(name);
+
+ mRunner = runner;
+ mIsDebug = isDebug;
+ mListener = listener;
+ }
+
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ try {
+ setupRunner();
+ mRunner.run(mListener);
+ } catch (TimeoutException e) {
+ return new Status(Status.ERROR, AdtPlugin.PLUGIN_ID,
+ LaunchMessages.RemoteAdtTestRunner_RunTimeoutException,
+ e);
+ } catch (IOException e) {
+ return new Status(Status.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format(LaunchMessages.RemoteAdtTestRunner_RunIOException_s,
+ e.getMessage()),
+ e);
+ } catch (AdbCommandRejectedException e) {
+ return new Status(Status.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format(
+ LaunchMessages.RemoteAdtTestRunner_RunAdbCommandRejectedException_s,
+ e.getMessage()),
+ e);
+ } catch (ShellCommandUnresponsiveException e) {
+ return new Status(Status.ERROR, AdtPlugin.PLUGIN_ID,
+ LaunchMessages.RemoteAdtTestRunner_RunTimeoutException,
+ e);
+ }
+
+ return Status.OK_STATUS;
+ }
+
+ public RemoteAndroidTestRunner getRunner() {
+ return mRunner;
+ }
+
+ public boolean isDebug() {
+ return mIsDebug;
+ }
+
+ public ITestRunListener getListener() {
+ return mListener;
+ }
+
+ protected abstract void setupRunner();
+ }
+
+ private static class TestTreeCollectorJob extends TestRunnerJob {
+ public TestTreeCollectorJob(String name, RemoteAndroidTestRunner runner, boolean isDebug,
+ TestCollector listener) {
+ super(name, runner, isDebug, listener);
+ }
+
+ @Override
+ protected void setupRunner() {
+ RemoteAndroidTestRunner runner = getRunner();
+
+ // set log only to just collect test case info,
+ // so Eclipse has correct test case count/tree info
+ runner.setLogOnly(true);
+
+ // add a small delay between each test. Otherwise for large test suites framework may
+ // report Binder transaction failures
+ runner.addInstrumentationArg(DELAY_MSEC_KEY, COLLECT_TEST_DELAY_MS);
+ }
+
+ public TestCollector getCollector() {
+ return (TestCollector) getListener();
+ }
+ }
+
+ private static class InstrumentationRunJob extends TestRunnerJob {
+ public InstrumentationRunJob(String name, RemoteAndroidTestRunner runner, boolean isDebug,
+ ITestRunListener listener) {
+ super(name, runner, isDebug, listener);
+ }
+
+ @Override
+ protected void setupRunner() {
+ RemoteAndroidTestRunner runner = getRunner();
+ runner.setLogOnly(false);
+ runner.removeInstrumentationArg(DELAY_MSEC_KEY);
+ if (isDebug()) {
+ runner.setDebug(true);
+ }
+ }
+ }
+
+ /**
+ * Main entry method to run tests
+ *
+ * @param programArgs JDT JUnit program arguments to be processed by parent
+ * @param junitInfo the {@link AndroidJUnitLaunchInfo} containing info about this test ru
+ */
+ public void runTests(String[] programArgs, AndroidJUnitLaunchInfo junitInfo) {
+ init(programArgs, junitInfo);
+ run();
+ }
+
+ /**
+ * Stop the current test run.
+ */
+ public void terminate() {
+ stop();
+ }
+
+ @Override
+ protected void stop() {
+ if (mExecution != null) {
+ mExecution.stop();
+ }
+ }
+
+ private void notifyTestRunEnded(long elapsedTime) {
+ // copy from parent - not ideal, but method is private
+ sendMessage(MessageIds.TEST_RUN_END + elapsedTime);
+ flush();
+ //shutDown();
+ }
+
+ /**
+ * @param errorMessage
+ */
+ private void reportError(String errorMessage) {
+ AdtPlugin.printErrorToConsole(mLaunchInfo.getProject(),
+ String.format(LaunchMessages.RemoteAdtTestRunner_RunFailedMsg_s, errorMessage));
+ // is this needed?
+ //notifyTestRunStopped(-1);
+ }
+
+ private void endTestRunWithError(String message) {
+ reportError(message);
+ notifyTestRunEnded(0);
+ }
+
+ /**
+ * This class provides the interface to notify the JDT UI regarding the status of tests.
+ * When running tests on multiple devices, there is a {@link TestRunListener} that listens
+ * to results from each device. Rather than all such listeners directly notifying JDT
+ * from different threads, they all notify this class which notifies JDT. In addition,
+ * the {@link #testRunEnded(String, long)} method make sure that JDT is notified that the
+ * test run has completed only when tests on all devices have completed.
+ * */
+ private class TestResultsNotifier {
+ private final IListensToTestExecutions mListener;
+ private final int mDeviceCount;
+
+ private int mCompletedRuns;
+ private long mMaxElapsedTime;
+
+ public TestResultsNotifier(IListensToTestExecutions listener, int nDevices) {
+ mListener = listener;
+ mDeviceCount = nDevices;
+ }
+
+ public synchronized void testEnded(TestCaseReference ref) {
+ mListener.notifyTestEnded(ref);
+ }
+
+ public synchronized void testFailed(TestReferenceFailure ref) {
+ mListener.notifyTestFailed(ref);
+ }
+
+ public synchronized void testRunEnded(String mDeviceName, long elapsedTime) {
+ mCompletedRuns++;
+
+ if (elapsedTime > mMaxElapsedTime) {
+ mMaxElapsedTime = elapsedTime;
+ }
+
+ if (mCompletedRuns == mDeviceCount) {
+ notifyTestRunEnded(mMaxElapsedTime);
+ }
+ }
+
+ public synchronized void testStarted(TestCaseReference testId) {
+ mListener.notifyTestStarted(testId);
+ }
+ }
+
+ /**
+ * TestRunListener that communicates results in real-time back to JDT JUnit via the
+ * {@link TestResultsNotifier}.
+ * */
+ private class TestRunListener implements ITestRunListener {
+ private final String mDeviceName;
+ private TestResultsNotifier mNotifier;
+
+ /**
+ * Constructs a {@link ITestRunListener} that listens for test results on given device.
+ * @param deviceName device on which the tests are being run
+ * @param notifier notifier to inform of test status
+ */
+ public TestRunListener(String deviceName, TestResultsNotifier notifier) {
+ mDeviceName = deviceName;
+ mNotifier = notifier;
+ }
+
+ @Override
+ public void testEnded(TestIdentifier test, Map<String, String> ignoredTestMetrics) {
+ mNotifier.testEnded(new TestCaseReference(mDeviceName, test));
+ }
+
+ @Override
+ public void testFailed(TestIdentifier test, String trace) {
+ TestReferenceFailure failure =
+ new TestReferenceFailure(new TestCaseReference(mDeviceName, test),
+ MessageIds.TEST_FAILED, trace, null);
+ mNotifier.testFailed(failure);
+ }
+
+ @Override
+ public void testAssumptionFailure(TestIdentifier test, String trace) {
+ TestReferenceFailure failure =
+ new TestReferenceFailure(new TestCaseReference(mDeviceName, test),
+ MessageIds.TEST_FAILED, trace, null);
+ mNotifier.testFailed(failure);
+ }
+
+ @Override
+ public void testIgnored(TestIdentifier test) {
+ // TODO: implement me?
+ }
+
+ @Override
+ public synchronized void testRunEnded(long elapsedTime, Map<String, String> runMetrics) {
+ mNotifier.testRunEnded(mDeviceName, elapsedTime);
+ AdtPlugin.printToConsole(mLaunchInfo.getProject(),
+ LaunchMessages.RemoteAdtTestRunner_RunCompleteMsg);
+ }
+
+ @Override
+ public synchronized void testRunFailed(String errorMessage) {
+ reportError(errorMessage);
+ }
+
+ @Override
+ public synchronized void testRunStarted(String runName, int testCount) {
+ // ignore
+ }
+
+ @Override
+ public synchronized void testRunStopped(long elapsedTime) {
+ notifyTestRunStopped(elapsedTime);
+ AdtPlugin.printToConsole(mLaunchInfo.getProject(),
+ LaunchMessages.RemoteAdtTestRunner_RunStoppedMsg);
+ }
+
+ @Override
+ public synchronized void testStarted(TestIdentifier test) {
+ TestCaseReference testId = new TestCaseReference(mDeviceName, test);
+ mNotifier.testStarted(testId);
+ }
+ }
+
+ /** Override parent to get extra logs. */
+ @Override
+ protected boolean connect() {
+ boolean result = super.connect();
+ if (!result) {
+ AdtPlugin.printErrorToConsole(mLaunchInfo.getProject(),
+ "Connect to Eclipse test result listener failed");
+ }
+ return result;
+ }
+
+ /** Override parent to dump error message to console. */
+ @Override
+ public void runFailed(String message, Exception exception) {
+ if (exception != null) {
+ AdtPlugin.logAndPrintError(exception, mLaunchInfo.getProject().getName(),
+ "Test launch failed: %s", message);
+ } else {
+ AdtPlugin.printErrorToConsole(mLaunchInfo.getProject(), "Test launch failed: %s",
+ message);
+ }
+ }
+}