aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchAction.java293
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchConfigDelegate.java286
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchConfigurationTab.java1061
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchShortcut.java56
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitPropertyTester.java131
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitTabGroup.java44
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/InstrumentationRunnerValidator.java159
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/AndroidJUnitLaunchInfo.java148
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/AndroidTestReference.java65
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/RemoteAdtTestRunner.java524
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/TestCaseReference.java67
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/TestCollector.java136
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/TestSuiteReference.java92
13 files changed, 3062 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchAction.java
new file mode 100644
index 000000000..c773ab9ba
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchAction.java
@@ -0,0 +1,293 @@
+/*
+ * 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;
+
+import com.android.ddmlib.IDevice;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.launch.DelayedLaunchInfo;
+import com.android.ide.eclipse.adt.internal.launch.IAndroidLaunchAction;
+import com.android.ide.eclipse.adt.internal.launch.LaunchMessages;
+import com.android.ide.eclipse.adt.internal.launch.junit.runtime.AndroidJUnitLaunchInfo;
+import com.android.ide.eclipse.adt.internal.launch.junit.runtime.RemoteAdtTestRunner;
+import com.google.common.base.Joiner;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.debug.core.ILaunch;
+import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.debug.core.ILaunchManager;
+import org.eclipse.debug.core.model.IProcess;
+import org.eclipse.debug.core.model.IStreamsProxy;
+import org.eclipse.jdt.junit.launcher.JUnitLaunchConfigurationDelegate;
+import org.eclipse.jdt.launching.IVMRunner;
+import org.eclipse.jdt.launching.VMRunnerConfiguration;
+import org.eclipse.swt.widgets.Display;
+
+import java.util.Collection;
+
+/**
+ * A launch action that executes a instrumentation test run on an Android device.
+ */
+class AndroidJUnitLaunchAction implements IAndroidLaunchAction {
+ private static final Joiner JOINER = Joiner.on(',').skipNulls();
+ private final AndroidJUnitLaunchInfo mLaunchInfo;
+
+ /**
+ * Creates a AndroidJUnitLaunchAction.
+ *
+ * @param launchInfo the {@link AndroidJUnitLaunchInfo} for the JUnit run
+ */
+ public AndroidJUnitLaunchAction(AndroidJUnitLaunchInfo launchInfo) {
+ mLaunchInfo = launchInfo;
+ }
+
+ /**
+ * Launch a instrumentation test run on given Android devices.
+ * Reuses JDT JUnit launch delegate so results can be communicated back to JDT JUnit UI.
+ * <p/>
+ * Note: Must be executed on non-UI thread.
+ *
+ * @see IAndroidLaunchAction#doLaunchActions(DelayedLaunchInfo, IDevice)
+ */
+ @Override
+ public boolean doLaunchAction(DelayedLaunchInfo info, Collection<IDevice> devices) {
+ String msg = String.format(LaunchMessages.AndroidJUnitLaunchAction_LaunchInstr_2s,
+ mLaunchInfo.getRunner(), JOINER.join(devices));
+ AdtPlugin.printToConsole(info.getProject(), msg);
+
+ try {
+ mLaunchInfo.setDebugMode(info.isDebugMode());
+ mLaunchInfo.setDevices(devices);
+ JUnitLaunchDelegate junitDelegate = new JUnitLaunchDelegate(mLaunchInfo);
+ final String mode = info.isDebugMode() ? ILaunchManager.DEBUG_MODE :
+ ILaunchManager.RUN_MODE;
+
+ junitDelegate.launch(info.getLaunch().getLaunchConfiguration(), mode, info.getLaunch(),
+ info.getMonitor());
+
+ // TODO: need to add AMReceiver-type functionality somewhere
+ } catch (CoreException e) {
+ AdtPlugin.printErrorToConsole(info.getProject(),
+ LaunchMessages.AndroidJUnitLaunchAction_LaunchFail);
+ }
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getLaunchDescription() {
+ return String.format(LaunchMessages.AndroidJUnitLaunchAction_LaunchDesc_s,
+ mLaunchInfo.getRunner());
+ }
+
+ /**
+ * Extends the JDT JUnit launch delegate to allow for JUnit UI reuse.
+ */
+ private static class JUnitLaunchDelegate extends JUnitLaunchConfigurationDelegate {
+
+ private AndroidJUnitLaunchInfo mLaunchInfo;
+
+ public JUnitLaunchDelegate(AndroidJUnitLaunchInfo launchInfo) {
+ mLaunchInfo = launchInfo;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jdt.junit.launcher.JUnitLaunchConfigurationDelegate#launch(org.eclipse.debug.core.ILaunchConfiguration, java.lang.String, org.eclipse.debug.core.ILaunch, org.eclipse.core.runtime.IProgressMonitor)
+ */
+ @Override
+ public synchronized void launch(ILaunchConfiguration configuration, String mode,
+ ILaunch launch, IProgressMonitor monitor) throws CoreException {
+ // TODO: is progress monitor adjustment needed here?
+ super.launch(configuration, mode, launch, monitor);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @see org.eclipse.jdt.junit.launcher.JUnitLaunchConfigurationDelegate#verifyMainTypeName(org.eclipse.debug.core.ILaunchConfiguration)
+ */
+ @Override
+ public String verifyMainTypeName(ILaunchConfiguration configuration) {
+ return "com.android.ide.eclipse.adt.junit.internal.runner.RemoteAndroidTestRunner"; //$NON-NLS-1$
+ }
+
+ /**
+ * Overrides parent to return a VM Runner implementation which launches a thread, rather
+ * than a separate VM process
+ */
+ @Override
+ public IVMRunner getVMRunner(ILaunchConfiguration configuration, String mode) {
+ return new VMTestRunner(mLaunchInfo);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @see org.eclipse.debug.core.model.LaunchConfigurationDelegate#getLaunch(org.eclipse.debug.core.ILaunchConfiguration, java.lang.String)
+ */
+ @Override
+ public ILaunch getLaunch(ILaunchConfiguration configuration, String mode) {
+ return mLaunchInfo.getLaunch();
+ }
+ }
+
+ /**
+ * Provides a VM runner implementation which starts a inline implementation of a launch process
+ */
+ private static class VMTestRunner implements IVMRunner {
+
+ private final AndroidJUnitLaunchInfo mJUnitInfo;
+
+ VMTestRunner(AndroidJUnitLaunchInfo info) {
+ mJUnitInfo = info;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws CoreException
+ */
+ @Override
+ public void run(final VMRunnerConfiguration config, ILaunch launch,
+ IProgressMonitor monitor) throws CoreException {
+
+ TestRunnerProcess runnerProcess =
+ new TestRunnerProcess(config, mJUnitInfo);
+ launch.addProcess(runnerProcess);
+ runnerProcess.run();
+ }
+ }
+
+ /**
+ * Launch process that executes the tests.
+ */
+ private static class TestRunnerProcess implements IProcess {
+
+ private final VMRunnerConfiguration mRunConfig;
+ private final AndroidJUnitLaunchInfo mJUnitInfo;
+ private RemoteAdtTestRunner mTestRunner = null;
+ private boolean mIsTerminated = false;
+
+ TestRunnerProcess(VMRunnerConfiguration runConfig, AndroidJUnitLaunchInfo info) {
+ mRunConfig = runConfig;
+ mJUnitInfo = info;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.core.model.IProcess#getAttribute(java.lang.String)
+ */
+ @Override
+ public String getAttribute(String key) {
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @see org.eclipse.debug.core.model.IProcess#getExitValue()
+ */
+ @Override
+ public int getExitValue() {
+ return 0;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.core.model.IProcess#getLabel()
+ */
+ @Override
+ public String getLabel() {
+ return mJUnitInfo.getLaunch().getLaunchMode();
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.core.model.IProcess#getLaunch()
+ */
+ @Override
+ public ILaunch getLaunch() {
+ return mJUnitInfo.getLaunch();
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.core.model.IProcess#getStreamsProxy()
+ */
+ @Override
+ public IStreamsProxy getStreamsProxy() {
+ return null;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.core.model.IProcess#setAttribute(java.lang.String,
+ * java.lang.String)
+ */
+ @Override
+ public void setAttribute(String key, String value) {
+ // ignore
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.core.runtime.IAdaptable#getAdapter(java.lang.Class)
+ */
+ @Override
+ public Object getAdapter(Class adapter) {
+ return null;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.core.model.ITerminate#canTerminate()
+ */
+ @Override
+ public boolean canTerminate() {
+ return true;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.core.model.ITerminate#isTerminated()
+ */
+ @Override
+ public boolean isTerminated() {
+ return mIsTerminated;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @see org.eclipse.debug.core.model.ITerminate#terminate()
+ */
+ @Override
+ public void terminate() {
+ if (mTestRunner != null) {
+ mTestRunner.terminate();
+ }
+ mIsTerminated = true;
+ }
+
+ /**
+ * Launches a test runner that will communicate results back to JDT JUnit UI.
+ * <p/>
+ * Must be executed on a non-UI thread.
+ */
+ public void run() {
+ if (Display.getCurrent() != null) {
+ AdtPlugin.log(IStatus.ERROR, "Adt test runner executed on UI thread");
+ AdtPlugin.printErrorToConsole(mJUnitInfo.getProject(),
+ "Test launch failed due to internal error: Running tests on UI thread");
+ terminate();
+ return;
+ }
+ mTestRunner = new RemoteAdtTestRunner();
+ mTestRunner.runTests(mRunConfig.getProgramArguments(), mJUnitInfo);
+ }
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchConfigDelegate.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchConfigDelegate.java
new file mode 100755
index 000000000..6e47ce91c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchConfigDelegate.java
@@ -0,0 +1,286 @@
+/*
+ * 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;
+
+import com.android.SdkConstants;
+import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner.TestSize;
+import com.android.ide.common.xml.ManifestData;
+import com.android.ide.common.xml.ManifestData.Instrumentation;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.launch.AndroidLaunch;
+import com.android.ide.eclipse.adt.internal.launch.AndroidLaunchConfiguration;
+import com.android.ide.eclipse.adt.internal.launch.AndroidLaunchController;
+import com.android.ide.eclipse.adt.internal.launch.IAndroidLaunchAction;
+import com.android.ide.eclipse.adt.internal.launch.LaunchConfigDelegate;
+import com.android.ide.eclipse.adt.internal.launch.LaunchMessages;
+import com.android.ide.eclipse.adt.internal.launch.junit.runtime.AndroidJUnitLaunchInfo;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+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.debug.core.ILaunchConfiguration;
+import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.internal.junit.launcher.JUnitLaunchConfigurationConstants;
+import org.eclipse.jdt.internal.junit.launcher.TestKindRegistry;
+import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
+import org.eclipse.swt.widgets.Display;
+
+/**
+ * Run configuration that can execute JUnit tests on an Android platform.
+ * <p/>
+ * Will deploy apps on target Android platform by reusing functionality from ADT
+ * LaunchConfigDelegate, and then run JUnits tests by reusing functionality from JDT
+ * JUnitLaunchConfigDelegate.
+ */
+@SuppressWarnings("restriction")
+public class AndroidJUnitLaunchConfigDelegate extends LaunchConfigDelegate {
+
+ /** Launch config attribute that stores instrumentation runner. */
+ static final String ATTR_INSTR_NAME = AdtPlugin.PLUGIN_ID + ".instrumentation"; //$NON-NLS-1$
+
+ /** Launch config attribute that stores the test size annotation to run. */
+ static final String ATTR_TEST_SIZE = AdtPlugin.PLUGIN_ID + ".testSize"; //$NON-NLS-1$
+
+ private static final String EMPTY_STRING = ""; //$NON-NLS-1$
+
+ @Override
+ protected void doLaunch(final ILaunchConfiguration configuration, final String mode,
+ final IProgressMonitor monitor, final IProject project,
+ final AndroidLaunch androidLaunch, final AndroidLaunchConfiguration config,
+ final AndroidLaunchController controller, final IFile applicationPackage,
+ final ManifestData manifestData) {
+
+ String runner = getRunner(project, configuration, manifestData);
+ if (runner == null) {
+ AdtPlugin.displayError(LaunchMessages.LaunchDialogTitle,
+ String.format(LaunchMessages.AndroidJUnitDelegate_NoRunnerMsg_s,
+ project.getName()));
+ androidLaunch.stopLaunch();
+ return;
+ }
+ // get the target app's package
+ final String targetAppPackage = getTargetPackage(manifestData, runner);
+ if (targetAppPackage == null) {
+ AdtPlugin.displayError(LaunchMessages.LaunchDialogTitle,
+ String.format(LaunchMessages.AndroidJUnitDelegate_NoTargetMsg_3s,
+ project.getName(), runner, SdkConstants.FN_ANDROID_MANIFEST_XML));
+ androidLaunch.stopLaunch();
+ return;
+ }
+ final String testAppPackage = manifestData.getPackage();
+ AndroidJUnitLaunchInfo junitLaunchInfo = new AndroidJUnitLaunchInfo(project,
+ testAppPackage, runner);
+ junitLaunchInfo.setTestClass(getTestClass(configuration));
+ junitLaunchInfo.setTestPackage(getTestPackage(configuration));
+ junitLaunchInfo.setTestMethod(getTestMethod(configuration));
+ junitLaunchInfo.setLaunch(androidLaunch);
+ junitLaunchInfo.setTestSize(getTestSize(configuration));
+ final IAndroidLaunchAction junitLaunch = new AndroidJUnitLaunchAction(junitLaunchInfo);
+
+ // launch on a separate thread if currently on the display thread
+ if (Display.getCurrent() != null) {
+ Job job = new Job("Junit Launch") { //$NON-NLS-1$
+ @Override
+ protected IStatus run(IProgressMonitor m) {
+ controller.launch(project, mode, applicationPackage, testAppPackage,
+ targetAppPackage, manifestData.getDebuggable(),
+ manifestData.getMinSdkVersionString(),
+ junitLaunch, config, androidLaunch, monitor);
+ return Status.OK_STATUS;
+ }
+ };
+ job.setPriority(Job.INTERACTIVE);
+ job.schedule();
+ } else {
+ controller.launch(project, mode, applicationPackage, testAppPackage, targetAppPackage,
+ manifestData.getDebuggable(), manifestData.getMinSdkVersionString(),
+ junitLaunch, config, androidLaunch, monitor);
+ }
+ }
+
+ /**
+ * Get the target Android application's package for the given instrumentation runner, or
+ * <code>null</code> if it could not be found.
+ *
+ * @param manifestParser the {@link ManifestData} for the test project
+ * @param runner the instrumentation runner class name
+ * @return the target package or <code>null</code>
+ */
+ private String getTargetPackage(ManifestData manifestParser, String runner) {
+ for (Instrumentation instr : manifestParser.getInstrumentations()) {
+ if (instr.getName().equals(runner)) {
+ return instr.getTargetPackage();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the test package stored in the launch configuration, or <code>null</code> if not
+ * specified.
+ *
+ * @param configuration the {@link ILaunchConfiguration} to retrieve the test package info from
+ * @return the test package or <code>null</code>.
+ */
+ private String getTestPackage(ILaunchConfiguration configuration) {
+ // try to retrieve a package name from the JUnit container attribute
+ String containerHandle = getStringLaunchAttribute(
+ JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER, configuration);
+ if (containerHandle != null && containerHandle.length() > 0) {
+ IJavaElement element = JavaCore.create(containerHandle);
+ // containerHandle could be a IProject, check if its a java package
+ if (element.getElementType() == IJavaElement.PACKAGE_FRAGMENT) {
+ return element.getElementName();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the test class stored in the launch configuration.
+ *
+ * @param configuration the {@link ILaunchConfiguration} to retrieve the test class info from
+ * @return the test class. <code>null</code> if not specified.
+ */
+ private String getTestClass(ILaunchConfiguration configuration) {
+ return getStringLaunchAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME,
+ configuration);
+ }
+
+ /**
+ * Returns the test method stored in the launch configuration.
+ *
+ * @param configuration the {@link ILaunchConfiguration} to retrieve the test method info from
+ * @return the test method. <code>null</code> if not specified.
+ */
+ private String getTestMethod(ILaunchConfiguration configuration) {
+ return getStringLaunchAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_METHOD_NAME,
+ configuration);
+ }
+
+ /**
+ * Returns the test sizes to run as saved in the launch configuration.
+ * @return {@link TestSize} if only tests of specific sizes should be run,
+ * null if all tests should be run
+ */
+ private TestSize getTestSize(ILaunchConfiguration configuration) {
+ String testSizeAnnotation = getStringLaunchAttribute(
+ AndroidJUnitLaunchConfigDelegate.ATTR_TEST_SIZE,
+ configuration);
+ if (AndroidJUnitLaunchConfigurationTab.SMALL_TEST_ANNOTATION.equals(
+ testSizeAnnotation)){
+ return TestSize.SMALL;
+ } else if (AndroidJUnitLaunchConfigurationTab.MEDIUM_TEST_ANNOTATION.equals(
+ testSizeAnnotation)) {
+ return TestSize.MEDIUM;
+ } else if (AndroidJUnitLaunchConfigurationTab.LARGE_TEST_ANNOTATION.equals(
+ testSizeAnnotation)) {
+ return TestSize.LARGE;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Gets a instrumentation runner for the launch.
+ * <p/>
+ * If a runner is stored in the given <code>configuration</code>, will return that.
+ * Otherwise, will try to find the first valid runner for the project.
+ * If a runner can still not be found, will return <code>null</code>, and will log an error
+ * to the console.
+ *
+ * @param project the {@link IProject} for the app
+ * @param configuration the {@link ILaunchConfiguration} for the launch
+ * @param manifestData the {@link ManifestData} for the project
+ *
+ * @return <code>null</code> if no instrumentation runner can be found, otherwise return
+ * the fully qualified runner name.
+ */
+ private String getRunner(IProject project, ILaunchConfiguration configuration,
+ ManifestData manifestData) {
+ try {
+ String runner = getRunnerFromConfig(configuration);
+ if (runner != null) {
+ return runner;
+ }
+ final InstrumentationRunnerValidator instrFinder = new InstrumentationRunnerValidator(
+ BaseProjectHelper.getJavaProject(project), manifestData);
+ runner = instrFinder.getValidInstrumentationTestRunner();
+ if (runner != null) {
+ AdtPlugin.printErrorToConsole(project, String.format(
+ LaunchMessages.AndroidJUnitDelegate_NoRunnerConfigMsg_s, runner));
+ return runner;
+ }
+ AdtPlugin.printErrorToConsole(project, String.format(
+ LaunchMessages.AndroidJUnitDelegate_NoRunnerConsoleMsg_4s,
+ project.getName(),
+ SdkConstants.CLASS_INSTRUMENTATION_RUNNER,
+ AdtConstants.LIBRARY_TEST_RUNNER,
+ SdkConstants.FN_ANDROID_MANIFEST_XML));
+ return null;
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Error when retrieving instrumentation info"); //$NON-NLS-1$
+ }
+
+ return null;
+ }
+
+ private String getRunnerFromConfig(ILaunchConfiguration configuration) {
+ return getStringLaunchAttribute(ATTR_INSTR_NAME, configuration);
+ }
+
+ /**
+ * Helper method to retrieve a string attribute from the launch configuration
+ *
+ * @param attributeName name of the launch attribute
+ * @param configuration the {@link ILaunchConfiguration} to retrieve the attribute from
+ * @return the attribute's value. <code>null</code> if not found.
+ */
+ private String getStringLaunchAttribute(String attributeName,
+ ILaunchConfiguration configuration) {
+ try {
+ String attrValue = configuration.getAttribute(attributeName, EMPTY_STRING);
+ if (attrValue.length() < 1) {
+ return null;
+ }
+ return attrValue;
+ } catch (CoreException e) {
+ AdtPlugin.log(e, String.format("Error when retrieving launch info %1$s", //$NON-NLS-1$
+ attributeName));
+ }
+ return null;
+ }
+
+ /**
+ * Helper method to set JUnit-related attributes expected by JDT JUnit runner
+ *
+ * @param config the launch configuration to modify
+ */
+ static void setJUnitDefaults(ILaunchConfigurationWorkingCopy config) {
+ // set the test runner to JUnit3 to placate JDT JUnit runner logic
+ config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_RUNNER_KIND,
+ TestKindRegistry.JUNIT3_TEST_KIND_ID);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchConfigurationTab.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchConfigurationTab.java
new file mode 100644
index 000000000..038f0b91f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchConfigurationTab.java
@@ -0,0 +1,1061 @@
+/*
+ * 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;
+
+import com.android.SdkConstants;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.launch.LaunchMessages;
+import com.android.ide.eclipse.adt.internal.launch.MainLaunchConfigTab;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.project.ProjectChooserHelper;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
+import org.eclipse.debug.ui.AbstractLaunchConfigurationTab;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IJavaModel;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IPackageFragment;
+import org.eclipse.jdt.core.IPackageFragmentRoot;
+import org.eclipse.jdt.core.ISourceReference;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.internal.junit.Messages;
+import org.eclipse.jdt.internal.junit.launcher.ITestKind;
+import org.eclipse.jdt.internal.junit.launcher.JUnitLaunchConfigurationConstants;
+import org.eclipse.jdt.internal.junit.launcher.JUnitMigrationDelegate;
+import org.eclipse.jdt.internal.junit.launcher.TestKindRegistry;
+import org.eclipse.jdt.internal.junit.launcher.TestSelectionDialog;
+import org.eclipse.jdt.internal.junit.ui.JUnitMessages;
+import org.eclipse.jdt.internal.junit.util.LayoutUtil;
+import org.eclipse.jdt.internal.junit.util.TestSearchEngine;
+import org.eclipse.jdt.internal.ui.wizards.TypedElementSelectionValidator;
+import org.eclipse.jdt.internal.ui.wizards.TypedViewerFilter;
+import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
+import org.eclipse.jdt.ui.JavaElementComparator;
+import org.eclipse.jdt.ui.JavaElementLabelProvider;
+import org.eclipse.jdt.ui.StandardJavaElementContentProvider;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.layout.GridDataFactory;
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerFilter;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.dialogs.ElementTreeSelectionDialog;
+import org.eclipse.ui.dialogs.SelectionDialog;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * The launch config UI tab for Android JUnit
+ * <p/>
+ * Based on org.eclipse.jdt.junit.launcher.JUnitLaunchConfigurationTab
+ */
+@SuppressWarnings("restriction")
+public class AndroidJUnitLaunchConfigurationTab extends AbstractLaunchConfigurationTab {
+
+ // Project UI widgets
+ private Label mProjLabel;
+ private Text mProjText;
+ private Button mProjButton;
+
+ // Test class UI widgets
+ private Text mTestText;
+ private Button mSearchButton;
+ private String mOriginalTestMethodName;
+ private Label mTestMethodLabel;
+ private Text mContainerText;
+ private IJavaElement mContainerElement;
+ private final ILabelProvider mJavaElementLabelProvider = new JavaElementLabelProvider();
+
+ private Button mContainerSearchButton;
+ private Button mTestContainerRadioButton;
+ private Button mTestRadioButton;
+ private Label mTestLabel;
+
+ // Android specific members
+ private Image mTabIcon = null;
+ private Combo mInstrumentationCombo;
+ private Combo mTestSizeCombo;
+ private static final String EMPTY_STRING = ""; //$NON-NLS-1$
+ private static final String TAG = "AndroidJUnitLaunchConfigurationTab"; //$NON-NLS-1$
+ private String[] mInstrumentations = null;
+ private InstrumentationRunnerValidator mInstrValidator = null;
+ private ProjectChooserHelper mProjectChooserHelper;
+
+ public static final String SMALL_TEST_ANNOTATION = "@SmallTest"; //$NON-NLS-1$
+ public static final String MEDIUM_TEST_ANNOTATION = "@MediumTest"; //$NON-NLS-1$
+ public static final String LARGE_TEST_ANNOTATION = "@LargeTest"; //$NON-NLS-1$
+ private static final List<String> TEST_SIZE_OPTIONS = Arrays.asList(
+ "All Tests",
+ SMALL_TEST_ANNOTATION,
+ MEDIUM_TEST_ANNOTATION,
+ LARGE_TEST_ANNOTATION
+ );
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.ILaunchConfigurationTab#createControl(org.eclipse.swt.widgets.Composite)
+ */
+ @Override
+ public void createControl(Composite parent) {
+ mProjectChooserHelper = new ProjectChooserHelper(parent.getShell(), null /*filter*/);
+
+ Composite comp = new Composite(parent, SWT.NONE);
+ setControl(comp);
+
+ GridLayout topLayout = new GridLayout();
+ topLayout.numColumns = 3;
+ comp.setLayout(topLayout);
+
+ createSingleTestSection(comp);
+ createTestContainerSelectionGroup(comp);
+
+ createSpacer(comp);
+
+ createInstrumentationGroup(comp);
+ createSizeSelector(comp);
+
+ Dialog.applyDialogFont(comp);
+ // TODO: add help link here when available
+ //PlatformUI.getWorkbench().getHelpSystem().setHelp(getControl(),
+ // IJUnitHelpContextIds.LAUNCH_CONFIGURATION_DIALOG_JUNIT_MAIN_TAB);
+ validatePage();
+ }
+
+
+ private void createSpacer(Composite comp) {
+ Label label = new Label(comp, SWT.NONE);
+ GridData gd = new GridData();
+ gd.horizontalSpan = 3;
+ label.setLayoutData(gd);
+ }
+
+ private void createSingleTestSection(Composite comp) {
+ mTestRadioButton = new Button(comp, SWT.RADIO);
+ mTestRadioButton.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_oneTest);
+ GridData gd = new GridData();
+ gd.horizontalSpan = 3;
+ mTestRadioButton.setLayoutData(gd);
+ mTestRadioButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mTestRadioButton.getSelection()) {
+ testModeChanged();
+ }
+ }
+ });
+
+ mProjLabel = new Label(comp, SWT.NONE);
+ mProjLabel.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_project);
+ gd = new GridData();
+ gd.horizontalIndent = 25;
+ mProjLabel.setLayoutData(gd);
+
+ mProjText = new Text(comp, SWT.SINGLE | SWT.BORDER);
+ mProjText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mProjText.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent evt) {
+ validatePage();
+ updateLaunchConfigurationDialog();
+ mSearchButton.setEnabled(mTestRadioButton.getSelection() &&
+ mProjText.getText().length() > 0);
+ }
+ });
+
+ mProjButton = new Button(comp, SWT.PUSH);
+ mProjButton.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_browse);
+ mProjButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent evt) {
+ handleProjectButtonSelected();
+ }
+ });
+ setButtonGridData(mProjButton);
+
+ mTestLabel = new Label(comp, SWT.NONE);
+ gd = new GridData();
+ gd.horizontalIndent = 25;
+ mTestLabel.setLayoutData(gd);
+ mTestLabel.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_test);
+
+
+ mTestText = new Text(comp, SWT.SINGLE | SWT.BORDER);
+ mTestText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mTestText.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent evt) {
+ validatePage();
+ updateLaunchConfigurationDialog();
+ }
+ });
+
+ mSearchButton = new Button(comp, SWT.PUSH);
+ mSearchButton.setEnabled(mProjText.getText().length() > 0);
+ mSearchButton.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_search);
+ mSearchButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent evt) {
+ handleSearchButtonSelected();
+ }
+ });
+ setButtonGridData(mSearchButton);
+
+ new Label(comp, SWT.NONE);
+
+ mTestMethodLabel = new Label(comp, SWT.NONE);
+ mTestMethodLabel.setText(""); //$NON-NLS-1$
+ gd = new GridData();
+ gd.horizontalSpan = 2;
+ mTestMethodLabel.setLayoutData(gd);
+ }
+
+ private void createTestContainerSelectionGroup(Composite comp) {
+ mTestContainerRadioButton = new Button(comp, SWT.RADIO);
+ mTestContainerRadioButton.setText(
+ LaunchMessages.AndroidJUnitTab_TestContainerText);
+ GridData gd = new GridData();
+ gd.horizontalSpan = 3;
+ mTestContainerRadioButton.setLayoutData(gd);
+ mTestContainerRadioButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mTestContainerRadioButton.getSelection()) {
+ testModeChanged();
+ }
+ }
+ });
+
+ mContainerText = new Text(comp, SWT.SINGLE | SWT.BORDER | SWT.READ_ONLY);
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalIndent = 25;
+ gd.horizontalSpan = 2;
+ mContainerText.setLayoutData(gd);
+ mContainerText.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent evt) {
+ updateLaunchConfigurationDialog();
+ }
+ });
+
+ mContainerSearchButton = new Button(comp, SWT.PUSH);
+ mContainerSearchButton.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_search);
+ mContainerSearchButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent evt) {
+ handleContainerSearchButtonSelected();
+ }
+ });
+ setButtonGridData(mContainerSearchButton);
+ }
+
+ private void createInstrumentationGroup(Composite comp) {
+ Label loaderLabel = new Label(comp, SWT.NONE);
+ loaderLabel.setText(LaunchMessages.AndroidJUnitTab_LoaderLabel);
+ GridData gd = new GridData();
+ gd.horizontalIndent = 0;
+ loaderLabel.setLayoutData(gd);
+
+ mInstrumentationCombo = new Combo(comp, SWT.DROP_DOWN | SWT.READ_ONLY);
+ GridDataFactory.defaultsFor(mInstrumentationCombo)
+ .span(2, 1)
+ .applyTo(mInstrumentationCombo);
+ mInstrumentationCombo.clearSelection();
+ mInstrumentationCombo.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ validatePage();
+ updateLaunchConfigurationDialog();
+ }
+ });
+ }
+
+ private void createSizeSelector(Composite comp) {
+ Label l = new Label(comp, SWT.NONE);
+ l.setText(LaunchMessages.AndroidJUnitTab_SizeLabel);
+ GridData gd = new GridData();
+ gd.horizontalIndent = 0;
+ l.setLayoutData(gd);
+
+ mTestSizeCombo = new Combo(comp, SWT.DROP_DOWN | SWT.READ_ONLY);
+ mTestSizeCombo.setItems(TEST_SIZE_OPTIONS.toArray(new String[TEST_SIZE_OPTIONS.size()]));
+ mTestSizeCombo.select(0);
+ mTestSizeCombo.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ updateLaunchConfigurationDialog();
+ }
+ });
+ }
+
+ private void handleContainerSearchButtonSelected() {
+ IJavaElement javaElement = chooseContainer(mContainerElement);
+ if (javaElement != null) {
+ setContainerElement(javaElement);
+ }
+ }
+
+ private void setContainerElement(IJavaElement javaElement) {
+ mContainerElement = javaElement;
+ mContainerText.setText(getPresentationName(javaElement));
+ validatePage();
+ updateLaunchConfigurationDialog();
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.ILaunchConfigurationTab#initializeFrom(org.eclipse.debug.core.ILaunchConfiguration)
+ */
+ @Override
+ public void initializeFrom(ILaunchConfiguration config) {
+ String projectName = updateProjectFromConfig(config);
+ String containerHandle = EMPTY_STRING;
+ try {
+ containerHandle = config.getAttribute(
+ JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER, EMPTY_STRING);
+ } catch (CoreException ce) {
+ // ignore
+ }
+
+ if (containerHandle.length() > 0) {
+ updateTestContainerFromConfig(config);
+ } else {
+ updateTestTypeFromConfig(config);
+ }
+
+ IProject proj = mProjectChooserHelper.getAndroidProject(projectName);
+ loadInstrumentations(proj);
+ updateInstrumentationFromConfig(config);
+ updateTestSizeFromConfig(config);
+
+ validatePage();
+ }
+
+ private void updateInstrumentationFromConfig(ILaunchConfiguration config) {
+ boolean found = false;
+ try {
+ String currentInstrumentation = config.getAttribute(
+ AndroidJUnitLaunchConfigDelegate.ATTR_INSTR_NAME, EMPTY_STRING);
+ if (mInstrumentations != null) {
+ // look for the name of the instrumentation in the combo.
+ for (int i = 0; i < mInstrumentations.length; i++) {
+ if (currentInstrumentation.equals(mInstrumentations[i])) {
+ found = true;
+ mInstrumentationCombo.select(i);
+ break;
+ }
+ }
+ }
+ } catch (CoreException ce) {
+ // ignore
+ }
+ if (!found) {
+ mInstrumentationCombo.clearSelection();
+ }
+ }
+
+ private void updateTestSizeFromConfig(ILaunchConfiguration config) {
+ try {
+ String testSize = config.getAttribute(
+ AndroidJUnitLaunchConfigDelegate.ATTR_TEST_SIZE, EMPTY_STRING);
+ int index = TEST_SIZE_OPTIONS.indexOf(testSize);
+ if (index >= 0 && mTestSizeCombo != null) {
+ mTestSizeCombo.select(index);
+ }
+ } catch (CoreException e) {
+ // ignore
+ }
+ }
+
+ private String updateProjectFromConfig(ILaunchConfiguration config) {
+ String projectName = EMPTY_STRING;
+ try {
+ projectName = config.getAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME,
+ EMPTY_STRING);
+ } catch (CoreException ce) {
+ // ignore
+ }
+ mProjText.setText(projectName);
+ return projectName;
+ }
+
+ private void updateTestTypeFromConfig(ILaunchConfiguration config) {
+ String testTypeName = EMPTY_STRING;
+ mOriginalTestMethodName = EMPTY_STRING;
+ try {
+ testTypeName = config.getAttribute(
+ IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, ""); //$NON-NLS-1$
+ mOriginalTestMethodName = config.getAttribute(
+ JUnitLaunchConfigurationConstants.ATTR_TEST_METHOD_NAME, ""); //$NON-NLS-1$
+ } catch (CoreException ce) {
+ // ignore
+ }
+ mTestRadioButton.setSelection(true);
+ setEnableSingleTestGroup(true);
+ setEnableContainerTestGroup(false);
+ mTestContainerRadioButton.setSelection(false);
+ mTestText.setText(testTypeName);
+ mContainerText.setText(EMPTY_STRING);
+ setTestMethodLabel(mOriginalTestMethodName);
+ }
+
+ private void setTestMethodLabel(String testMethodName) {
+ if (!EMPTY_STRING.equals(testMethodName)) {
+ mTestMethodLabel.setText(
+ JUnitMessages.JUnitLaunchConfigurationTab_label_method +
+ mOriginalTestMethodName);
+ } else {
+ mTestMethodLabel.setText(EMPTY_STRING);
+ }
+ }
+
+ private void updateTestContainerFromConfig(ILaunchConfiguration config) {
+ String containerHandle = EMPTY_STRING;
+ IJavaElement containerElement = null;
+ try {
+ containerHandle = config.getAttribute(
+ JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER, EMPTY_STRING);
+ if (containerHandle.length() > 0) {
+ containerElement = JavaCore.create(containerHandle);
+ }
+ } catch (CoreException ce) {
+ // ignore
+ }
+ if (containerElement != null) {
+ mContainerElement = containerElement;
+ }
+ mTestContainerRadioButton.setSelection(true);
+ setEnableSingleTestGroup(false);
+ setEnableContainerTestGroup(true);
+ mTestRadioButton.setSelection(false);
+ if (mContainerElement != null) {
+ mContainerText.setText(getPresentationName(mContainerElement));
+ }
+ mTestText.setText(EMPTY_STRING);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.eclipse.debug.ui.ILaunchConfigurationTab#performApply(org.eclipse.debug.core.ILaunchConfigurationWorkingCopy)
+ */
+ @Override
+ public void performApply(ILaunchConfigurationWorkingCopy config) {
+ if (mTestContainerRadioButton.getSelection() && mContainerElement != null) {
+ config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME,
+ mContainerElement.getJavaProject().getElementName());
+ config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER,
+ mContainerElement.getHandleIdentifier());
+ config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME,
+ EMPTY_STRING);
+ //workaround for Eclipse bug 65399
+ config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_METHOD_NAME,
+ EMPTY_STRING);
+ } else {
+ config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME,
+ mProjText.getText());
+ config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME,
+ mTestText.getText());
+ config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER,
+ EMPTY_STRING);
+ config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_METHOD_NAME,
+ mOriginalTestMethodName);
+ }
+ try {
+ mapResources(config);
+ } catch (CoreException e) {
+ // TODO: does the real error need to be extracted out of CoreException
+ AdtPlugin.log(e, "Error occurred saving configuration"); //$NON-NLS-1$
+ }
+ AndroidJUnitLaunchConfigDelegate.setJUnitDefaults(config);
+
+ config.setAttribute(AndroidJUnitLaunchConfigDelegate.ATTR_INSTR_NAME,
+ getSelectedInstrumentation());
+ config.setAttribute(AndroidJUnitLaunchConfigDelegate.ATTR_TEST_SIZE,
+ getSelectedTestSize());
+ }
+
+ private void mapResources(ILaunchConfigurationWorkingCopy config) throws CoreException {
+ JUnitMigrationDelegate.mapResources(config);
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.AbstractLaunchConfigurationTab#dispose()
+ */
+ @Override
+ public void dispose() {
+ super.dispose();
+ mTabIcon = null;
+ mJavaElementLabelProvider.dispose();
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.AbstractLaunchConfigurationTab#getImage()
+ */
+ @Override
+ public Image getImage() {
+ // reuse icon from the Android App Launch config tab
+ if (mTabIcon == null) {
+ mTabIcon = IconFactory.getInstance().getIcon(MainLaunchConfigTab.LAUNCH_TAB_IMAGE);
+ }
+ return mTabIcon;
+ }
+
+ /**
+ * Show a dialog that lists all main types
+ */
+ private void handleSearchButtonSelected() {
+ Shell shell = getShell();
+
+ IJavaProject javaProject = getJavaProject();
+
+ IType[] types = new IType[0];
+ boolean[] radioSetting = new boolean[2];
+ try {
+ // fix for Eclipse bug 66922 Wrong radio behaviour when switching
+ // remember the selected radio button
+ radioSetting[0] = mTestRadioButton.getSelection();
+ radioSetting[1] = mTestContainerRadioButton.getSelection();
+
+ types = TestSearchEngine.findTests(getLaunchConfigurationDialog(), javaProject,
+ getTestKind());
+ } catch (InterruptedException e) {
+ setErrorMessage(e.getMessage());
+ return;
+ } catch (InvocationTargetException e) {
+ AdtPlugin.log(e.getTargetException(), "Error finding test types"); //$NON-NLS-1$
+ return;
+ } finally {
+ mTestRadioButton.setSelection(radioSetting[0]);
+ mTestContainerRadioButton.setSelection(radioSetting[1]);
+ }
+
+ SelectionDialog dialog = new TestSelectionDialog(shell, types);
+ dialog.setTitle(JUnitMessages.JUnitLaunchConfigurationTab_testdialog_title);
+ dialog.setMessage(JUnitMessages.JUnitLaunchConfigurationTab_testdialog_message);
+ if (dialog.open() == Window.CANCEL) {
+ return;
+ }
+
+ Object[] results = dialog.getResult();
+ if ((results == null) || (results.length < 1)) {
+ return;
+ }
+ IType type = (IType) results[0];
+
+ if (type != null) {
+ mTestText.setText(type.getFullyQualifiedName('.'));
+ javaProject = type.getJavaProject();
+ mProjText.setText(javaProject.getElementName());
+ }
+ }
+
+ private ITestKind getTestKind() {
+ // harddcode this to JUnit 3
+ return TestKindRegistry.getDefault().getKind(TestKindRegistry.JUNIT3_TEST_KIND_ID);
+ }
+
+ /**
+ * Show a dialog that lets the user select a Android project. This in turn provides
+ * context for the main type, allowing the user to key a main type name, or
+ * constraining the search for main types to the specified project.
+ */
+ private void handleProjectButtonSelected() {
+ IJavaProject project = mProjectChooserHelper.chooseJavaProject(getProjectName(),
+ "Please select a project to launch");
+ if (project == null) {
+ return;
+ }
+
+ String projectName = project.getElementName();
+ mProjText.setText(projectName);
+ loadInstrumentations(project.getProject());
+ }
+
+ /**
+ * Return the IJavaProject corresponding to the project name in the project name
+ * text field, or null if the text does not match a Android project name.
+ */
+ private IJavaProject getJavaProject() {
+ String projectName = getProjectName();
+ return getJavaModel().getJavaProject(projectName);
+ }
+
+ /**
+ * Returns the name of the currently specified project. Null if no project is selected.
+ */
+ private String getProjectName() {
+ String projectName = mProjText.getText().trim();
+ if (projectName.length() < 1) {
+ return null;
+ }
+ return projectName;
+ }
+
+ /**
+ * Convenience method to get the workspace root.
+ */
+ private IWorkspaceRoot getWorkspaceRoot() {
+ return ResourcesPlugin.getWorkspace().getRoot();
+ }
+
+ /**
+ * Convenience method to get access to the java model.
+ */
+ private IJavaModel getJavaModel() {
+ return JavaCore.create(getWorkspaceRoot());
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.AbstractLaunchConfigurationTab#isValid(org.eclipse.debug.core.ILaunchConfiguration)
+ */
+ @Override
+ public boolean isValid(ILaunchConfiguration config) {
+ validatePage();
+ return getErrorMessage() == null;
+ }
+
+ private void testModeChanged() {
+ boolean isSingleTestMode = mTestRadioButton.getSelection();
+ setEnableSingleTestGroup(isSingleTestMode);
+ setEnableContainerTestGroup(!isSingleTestMode);
+ if (!isSingleTestMode && mContainerText.getText().length() == 0) {
+ String projText = mProjText.getText();
+ if (Path.EMPTY.isValidSegment(projText)) {
+ IJavaProject javaProject = getJavaModel().getJavaProject(projText);
+ if (javaProject != null && javaProject.exists()) {
+ setContainerElement(javaProject);
+ }
+ }
+ }
+ validatePage();
+ updateLaunchConfigurationDialog();
+ }
+
+ private void validatePage() {
+ setErrorMessage(null);
+ setMessage(null);
+
+ if (mTestContainerRadioButton.getSelection()) {
+ if (mContainerElement == null) {
+ setErrorMessage(JUnitMessages.JUnitLaunchConfigurationTab_error_noContainer);
+ return;
+ }
+ validateJavaProject(mContainerElement.getJavaProject());
+ return;
+ }
+
+ String projectName = mProjText.getText().trim();
+ if (projectName.length() == 0) {
+ setErrorMessage(JUnitMessages.JUnitLaunchConfigurationTab_error_projectnotdefined);
+ return;
+ }
+
+ IStatus status = ResourcesPlugin.getWorkspace().validatePath(IPath.SEPARATOR + projectName,
+ IResource.PROJECT);
+ if (!status.isOK() || !Path.ROOT.isValidSegment(projectName)) {
+ setErrorMessage(Messages.format(
+ JUnitMessages.JUnitLaunchConfigurationTab_error_invalidProjectName,
+ projectName));
+ return;
+ }
+
+ IProject project = getWorkspaceRoot().getProject(projectName);
+ if (!project.exists()) {
+ setErrorMessage(JUnitMessages.JUnitLaunchConfigurationTab_error_projectnotexists);
+ return;
+ }
+ IJavaProject javaProject = JavaCore.create(project);
+ validateJavaProject(javaProject);
+
+ try {
+ if (!project.hasNature(AdtConstants.NATURE_DEFAULT)) {
+ setErrorMessage(
+ LaunchMessages.NonAndroidProjectError);
+ return;
+ }
+ String className = mTestText.getText().trim();
+ if (className.length() == 0) {
+ setErrorMessage(JUnitMessages.JUnitLaunchConfigurationTab_error_testnotdefined);
+ return;
+ }
+ if (javaProject.findType(className) == null) {
+ setErrorMessage(Messages.format(
+ JUnitMessages.JUnitLaunchConfigurationTab_error_test_class_not_found,
+ new String[] { className, projectName }));
+ return;
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "validatePage failed"); //$NON-NLS-1$
+ }
+
+ validateInstrumentation();
+ }
+
+ private void validateJavaProject(IJavaProject javaProject) {
+ if (!TestSearchEngine.hasTestCaseType(javaProject)) {
+ setErrorMessage(JUnitMessages.JUnitLaunchConfigurationTab_error_testcasenotonpath);
+ return;
+ }
+ }
+
+ private void validateInstrumentation() {
+ String instrumentation = getSelectedInstrumentation();
+ if (instrumentation == null) {
+ setErrorMessage(LaunchMessages.AndroidJUnitTab_NoRunnerError);
+ return;
+ }
+ String result = mInstrValidator.validateInstrumentationRunner(instrumentation);
+ if (result != InstrumentationRunnerValidator.INSTRUMENTATION_OK) {
+ setErrorMessage(result);
+ return;
+ }
+ }
+
+ private String getSelectedInstrumentation() {
+ int selectionIndex = mInstrumentationCombo.getSelectionIndex();
+ if (mInstrumentations != null && selectionIndex >= 0 &&
+ selectionIndex < mInstrumentations.length) {
+ return mInstrumentations[selectionIndex];
+ }
+ return null;
+ }
+
+ private String getSelectedTestSize() {
+ if (mTestSizeCombo != null) {
+ int index = mTestSizeCombo.getSelectionIndex();
+ return TEST_SIZE_OPTIONS.get(index);
+ } else {
+ return null;
+ }
+ }
+
+ private void setEnableContainerTestGroup(boolean enabled) {
+ mContainerSearchButton.setEnabled(enabled);
+ mContainerText.setEnabled(enabled);
+ }
+
+ private void setEnableSingleTestGroup(boolean enabled) {
+ mProjLabel.setEnabled(enabled);
+ mProjText.setEnabled(enabled);
+ mProjButton.setEnabled(enabled);
+ mTestLabel.setEnabled(enabled);
+ mTestText.setEnabled(enabled);
+ mSearchButton.setEnabled(enabled && mProjText.getText().length() > 0);
+ mTestMethodLabel.setEnabled(enabled);
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.ILaunchConfigurationTab#setDefaults(org.eclipse.debug.core.ILaunchConfigurationWorkingCopy)
+ */
+ @Override
+ public void setDefaults(ILaunchConfigurationWorkingCopy config) {
+ IJavaElement javaElement = getContext();
+ if (javaElement != null) {
+ initializeJavaProject(javaElement, config);
+ } else {
+ // We set empty attributes for project & main type so that when one config is
+ // compared to another, the existence of empty attributes doesn't cause an
+ // incorrect result (the performApply() method can result in empty values
+ // for these attributes being set on a config if there is nothing in the
+ // corresponding text boxes)
+ config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, EMPTY_STRING);
+ config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER,
+ EMPTY_STRING);
+ }
+ initializeTestAttributes(javaElement, config);
+ }
+
+ private void initializeTestAttributes(IJavaElement javaElement,
+ ILaunchConfigurationWorkingCopy config) {
+ if (javaElement != null && javaElement.getElementType() < IJavaElement.COMPILATION_UNIT) {
+ initializeTestContainer(javaElement, config);
+ } else {
+ initializeTestType(javaElement, config);
+ }
+ }
+
+ private void initializeTestContainer(IJavaElement javaElement,
+ ILaunchConfigurationWorkingCopy config) {
+ config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER,
+ javaElement.getHandleIdentifier());
+ initializeName(config, javaElement.getElementName());
+ }
+
+ private void initializeName(ILaunchConfigurationWorkingCopy config, String name) {
+ if (name == null) {
+ name = EMPTY_STRING;
+ }
+ if (name.length() > 0) {
+ int index = name.lastIndexOf('.');
+ if (index > 0) {
+ name = name.substring(index + 1);
+ }
+ name = getLaunchConfigurationDialog().generateName(name);
+ config.rename(name);
+ }
+ }
+
+ /**
+ * Sets the main type & name attributes on the working copy based on the IJavaElement
+ */
+ private void initializeTestType(IJavaElement javaElement,
+ ILaunchConfigurationWorkingCopy config) {
+ String name = EMPTY_STRING;
+ String testKindId = null;
+ try {
+ // only do a search for compilation units or class files or source references
+ if (javaElement instanceof ISourceReference) {
+ ITestKind testKind = TestKindRegistry.getContainerTestKind(javaElement);
+ testKindId = testKind.getId();
+
+ IType[] types = TestSearchEngine.findTests(getLaunchConfigurationDialog(),
+ javaElement, testKind);
+ if ((types == null) || (types.length < 1)) {
+ return;
+ }
+ // Simply grab the first main type found in the searched element
+ name = types[0].getFullyQualifiedName('.');
+
+ }
+ } catch (InterruptedException ie) {
+ // ignore
+ } catch (InvocationTargetException ite) {
+ // ignore
+ }
+ config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, name);
+ if (testKindId != null) {
+ config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_RUNNER_KIND,
+ testKindId);
+ }
+ initializeName(config, name);
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.ILaunchConfigurationTab#getName()
+ */
+ @Override
+ public String getName() {
+ return JUnitMessages.JUnitLaunchConfigurationTab_tab_label;
+ }
+
+ private IJavaElement chooseContainer(IJavaElement initElement) {
+ @SuppressWarnings("rawtypes")
+ Class[] acceptedClasses = new Class[] { IJavaProject.class,
+ IPackageFragment.class };
+ TypedElementSelectionValidator validator = new TypedElementSelectionValidator(
+ acceptedClasses, false) {
+ @Override
+ public boolean isSelectedValid(Object element) {
+ return true;
+ }
+ };
+
+ acceptedClasses = new Class[] { IJavaModel.class, IPackageFragmentRoot.class,
+ IJavaProject.class, IPackageFragment.class };
+ ViewerFilter filter = new TypedViewerFilter(acceptedClasses) {
+ @Override
+ public boolean select(Viewer viewer, Object parent, Object element) {
+ if (element instanceof IPackageFragmentRoot &&
+ ((IPackageFragmentRoot) element).isArchive()) {
+ return false;
+ }
+ try {
+ if (element instanceof IPackageFragment &&
+ !((IPackageFragment) element).hasChildren()) {
+ return false;
+ }
+ } catch (JavaModelException e) {
+ return false;
+ }
+ return super.select(viewer, parent, element);
+ }
+ };
+
+ AndroidJavaElementContentProvider provider = new AndroidJavaElementContentProvider();
+ ILabelProvider labelProvider = new JavaElementLabelProvider(
+ JavaElementLabelProvider.SHOW_DEFAULT);
+ ElementTreeSelectionDialog dialog = new ElementTreeSelectionDialog(getShell(),
+ labelProvider, provider);
+ dialog.setValidator(validator);
+ dialog.setComparator(new JavaElementComparator());
+ dialog.setTitle(JUnitMessages.JUnitLaunchConfigurationTab_folderdialog_title);
+ dialog.setMessage(JUnitMessages.JUnitLaunchConfigurationTab_folderdialog_message);
+ dialog.addFilter(filter);
+ dialog.setInput(JavaCore.create(getWorkspaceRoot()));
+ dialog.setInitialSelection(initElement);
+ dialog.setAllowMultiple(false);
+
+ if (dialog.open() == Window.OK) {
+ Object element = dialog.getFirstResult();
+ return (IJavaElement) element;
+ }
+ return null;
+ }
+
+ private String getPresentationName(IJavaElement element) {
+ return mJavaElementLabelProvider.getText(element);
+ }
+
+ /**
+ * Returns the current Java element context from which to initialize
+ * default settings, or <code>null</code> if none.
+ *
+ * @return Java element context.
+ */
+ private IJavaElement getContext() {
+ IWorkbenchWindow activeWorkbenchWindow =
+ PlatformUI.getWorkbench().getActiveWorkbenchWindow();
+ if (activeWorkbenchWindow == null) {
+ return null;
+ }
+ IWorkbenchPage page = activeWorkbenchWindow.getActivePage();
+ if (page != null) {
+ ISelection selection = page.getSelection();
+ if (selection instanceof IStructuredSelection) {
+ IStructuredSelection ss = (IStructuredSelection) selection;
+ if (!ss.isEmpty()) {
+ Object obj = ss.getFirstElement();
+ if (obj instanceof IJavaElement) {
+ return (IJavaElement) obj;
+ }
+ if (obj instanceof IResource) {
+ IJavaElement je = JavaCore.create((IResource) obj);
+ if (je == null) {
+ IProject pro = ((IResource) obj).getProject();
+ je = JavaCore.create(pro);
+ }
+ if (je != null) {
+ return je;
+ }
+ }
+ }
+ }
+ IEditorPart part = page.getActiveEditor();
+ if (part != null) {
+ IEditorInput input = part.getEditorInput();
+ return (IJavaElement) input.getAdapter(IJavaElement.class);
+ }
+ }
+ return null;
+ }
+
+ private void initializeJavaProject(IJavaElement javaElement,
+ ILaunchConfigurationWorkingCopy config) {
+ IJavaProject javaProject = javaElement.getJavaProject();
+ String name = null;
+ if (javaProject != null && javaProject.exists()) {
+ name = javaProject.getElementName();
+ }
+ config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, name);
+ }
+
+ private void setButtonGridData(Button button) {
+ GridData gridData = new GridData();
+ button.setLayoutData(gridData);
+ LayoutUtil.setButtonDimensionHint(button);
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.AbstractLaunchConfigurationTab#getId()
+ */
+ @Override
+ public String getId() {
+ return "com.android.ide.eclipse.adt.launch.AndroidJUnitLaunchConfigurationTab"; //$NON-NLS-1$
+ }
+
+ /**
+ * Loads the UI with the instrumentations of the specified project, and stores the
+ * instrumentations in <code>mInstrumentations</code>.
+ *
+ * @param project the {@link IProject} to load the instrumentations from.
+ */
+ private void loadInstrumentations(IProject project) {
+ try {
+ mInstrValidator = new InstrumentationRunnerValidator(project);
+ mInstrumentations = mInstrValidator.getInstrumentationNames();
+ if (mInstrumentations.length > 0) {
+ mInstrumentationCombo.removeAll();
+ for (String instrumentation : mInstrumentations) {
+ mInstrumentationCombo.add(instrumentation);
+ }
+ // the selection will be set when we update the ui from the current
+ // config object.
+ return;
+ }
+ } catch (CoreException e) {
+ AdtPlugin.logAndPrintError(e, project.getName(),
+ LaunchMessages.AndroidJUnitTab_LoadInstrError_s,
+ SdkConstants.FN_ANDROID_MANIFEST_XML);
+ }
+ // if we reach this point, either project is null, or we got an exception during
+ // the parsing. In either case, we empty the instrumentation list.
+ mInstrValidator = null;
+ mInstrumentations = null;
+ mInstrumentationCombo.removeAll();
+ }
+
+ /**
+ * Overrides the {@link StandardJavaElementContentProvider} to only display Android projects
+ */
+ private static class AndroidJavaElementContentProvider
+ extends StandardJavaElementContentProvider {
+
+ /**
+ * Override parent to return only Android projects if at the root. Otherwise, use parent
+ * functionality.
+ */
+ @Override
+ public Object[] getChildren(Object element) {
+ if (element instanceof IJavaModel) {
+ return BaseProjectHelper.getAndroidProjects((IJavaModel) element, null /*filter*/);
+ }
+ return super.getChildren(element);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchShortcut.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchShortcut.java
new file mode 100755
index 000000000..b94ced891
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchShortcut.java
@@ -0,0 +1,56 @@
+/*
+ * 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;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.junit.launcher.JUnitLaunchShortcut;
+
+/**
+ * Launch shortcut to launch debug/run Android JUnit configuration directly.
+ */
+public class AndroidJUnitLaunchShortcut extends JUnitLaunchShortcut {
+
+ @Override
+ protected String getLaunchConfigurationTypeId() {
+ return "com.android.ide.eclipse.adt.junit.launchConfigurationType"; //$NON-NLS-1$
+ }
+
+ /**
+ * Creates a default Android JUnit launch configuration. Sets the instrumentation runner to the
+ * first instrumentation found in the AndroidManifest.
+ */
+ @Override
+ protected ILaunchConfigurationWorkingCopy createLaunchConfiguration(IJavaElement element)
+ throws CoreException {
+ ILaunchConfigurationWorkingCopy config = super.createLaunchConfiguration(element);
+ // just get first valid instrumentation runner
+ String instrumentation = new InstrumentationRunnerValidator(element.getJavaProject()).
+ getValidInstrumentationTestRunner();
+ if (instrumentation != null) {
+ config.setAttribute(AndroidJUnitLaunchConfigDelegate.ATTR_INSTR_NAME,
+ instrumentation);
+ }
+ // if a valid runner is not found, rely on launch delegate to log error.
+ // This method is called without explicit user action to launch Android JUnit, so avoid
+ // logging an error here.
+
+ AndroidJUnitLaunchConfigDelegate.setJUnitDefaults(config);
+ return config;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitPropertyTester.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitPropertyTester.java
new file mode 100644
index 000000000..5172e09a7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitPropertyTester.java
@@ -0,0 +1,131 @@
+/*
+ * 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;
+
+import org.eclipse.core.expressions.PropertyTester;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IAdaptable;
+import org.eclipse.jdt.core.IClassFile;
+import org.eclipse.jdt.core.ICompilationUnit;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IMember;
+import org.eclipse.jdt.core.IPackageFragment;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.internal.junit.util.TestSearchEngine;
+
+/**
+ * A {@link PropertyTester} that checks if selected elements can be run as Android
+ * JUnit tests.
+ * <p/>
+ * Based on org.eclipse.jdt.internal.junit.JUnitPropertyTester. The only substantial difference in
+ * this implementation is source folders cannot be run as Android JUnit.
+ */
+@SuppressWarnings("restriction")
+public class AndroidJUnitPropertyTester extends PropertyTester {
+ private static final String PROPERTY_IS_TEST = "isTest"; //$NON-NLS-1$
+
+ private static final String PROPERTY_CAN_LAUNCH_AS_JUNIT_TEST = "canLaunchAsJUnit"; //$NON-NLS-1$
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jdt.internal.corext.refactoring.participants.properties.IPropertyEvaluator#test(java.lang.Object, java.lang.String, java.lang.String)
+ */
+ @Override
+ public boolean test(Object receiver, String property, Object[] args, Object expectedValue) {
+ if (!(receiver instanceof IAdaptable)) {
+ final String elementName = (receiver == null ? "null" : //$NON-NLS-1$
+ receiver.getClass().getName());
+ throw new IllegalArgumentException(
+ String.format("Element must be of type IAdaptable, is %s", //$NON-NLS-1$
+ elementName));
+ }
+
+ IJavaElement element;
+ if (receiver instanceof IJavaElement) {
+ element = (IJavaElement) receiver;
+ } else if (receiver instanceof IResource) {
+ element = JavaCore.create((IResource) receiver);
+ if (element == null) {
+ return false;
+ }
+ } else { // is IAdaptable
+ element= (IJavaElement) ((IAdaptable) receiver).getAdapter(IJavaElement.class);
+ if (element == null) {
+ IResource resource = (IResource) ((IAdaptable) receiver).getAdapter(
+ IResource.class);
+ element = JavaCore.create(resource);
+ if (element == null) {
+ return false;
+ }
+ }
+ }
+ if (PROPERTY_IS_TEST.equals(property)) {
+ return isJUnitTest(element);
+ } else if (PROPERTY_CAN_LAUNCH_AS_JUNIT_TEST.equals(property)) {
+ return canLaunchAsJUnitTest(element);
+ }
+ throw new IllegalArgumentException(
+ String.format("Unknown test property '%s'", property)); //$NON-NLS-1$
+ }
+
+ private boolean canLaunchAsJUnitTest(IJavaElement element) {
+ try {
+ switch (element.getElementType()) {
+ case IJavaElement.JAVA_PROJECT:
+ return true; // can run, let JDT detect if there are tests
+ case IJavaElement.PACKAGE_FRAGMENT_ROOT:
+ return false; // not supported by Android test runner
+ case IJavaElement.PACKAGE_FRAGMENT:
+ return ((IPackageFragment) element).hasChildren();
+ case IJavaElement.COMPILATION_UNIT:
+ case IJavaElement.CLASS_FILE:
+ case IJavaElement.TYPE:
+ case IJavaElement.METHOD:
+ return isJUnitTest(element);
+ default:
+ return false;
+ }
+ } catch (JavaModelException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Return whether the target resource is a JUnit test.
+ */
+ private boolean isJUnitTest(IJavaElement element) {
+ try {
+ IType testType = null;
+ if (element instanceof ICompilationUnit) {
+ testType = (((ICompilationUnit) element)).findPrimaryType();
+ } else if (element instanceof IClassFile) {
+ testType = (((IClassFile) element)).getType();
+ } else if (element instanceof IType) {
+ testType = (IType) element;
+ } else if (element instanceof IMember) {
+ testType = ((IMember) element).getDeclaringType();
+ }
+ if (testType != null && testType.exists()) {
+ return TestSearchEngine.isTestOrTestSuite(testType);
+ }
+ } catch (CoreException e) {
+ // ignore, return false
+ }
+ return false;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitTabGroup.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitTabGroup.java
new file mode 100644
index 000000000..deb30de01
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitTabGroup.java
@@ -0,0 +1,44 @@
+/*
+ * 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;
+
+import com.android.ide.eclipse.adt.internal.launch.EmulatorConfigTab;
+
+import org.eclipse.debug.core.ILaunchManager;
+import org.eclipse.debug.ui.AbstractLaunchConfigurationTabGroup;
+import org.eclipse.debug.ui.CommonTab;
+import org.eclipse.debug.ui.ILaunchConfigurationDialog;
+import org.eclipse.debug.ui.ILaunchConfigurationTab;
+
+/**
+ * Tab group object for Android JUnit launch configuration type.
+ */
+public class AndroidJUnitTabGroup extends AbstractLaunchConfigurationTabGroup {
+
+ /**
+ * Creates the UI tabs for the Android JUnit configuration
+ */
+ @Override
+ public void createTabs(ILaunchConfigurationDialog dialog, String mode) {
+ ILaunchConfigurationTab[] tabs = new ILaunchConfigurationTab[] {
+ new AndroidJUnitLaunchConfigurationTab(),
+ new EmulatorConfigTab(ILaunchManager.RUN_MODE.equals(mode)),
+ new CommonTab()
+ };
+ setTabs(tabs);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/InstrumentationRunnerValidator.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/InstrumentationRunnerValidator.java
new file mode 100644
index 000000000..820fed7d3
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/InstrumentationRunnerValidator.java
@@ -0,0 +1,159 @@
+/*
+ * 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;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.xml.ManifestData;
+import com.android.ide.common.xml.ManifestData.Instrumentation;
+import com.android.ide.common.xml.ManifestData.UsesLibrary;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.internal.launch.LaunchMessages;
+import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jdt.core.IJavaProject;
+
+/**
+ * Provides validation for Android instrumentation test runner
+ */
+class InstrumentationRunnerValidator {
+ private final IJavaProject mJavaProject;
+ private String[] mInstrumentationNames = null;
+ private boolean mHasRunnerLibrary = false;
+
+ static final String INSTRUMENTATION_OK = null;
+
+ /**
+ * Initializes the InstrumentationRunnerValidator.
+ *
+ * @param javaProject the {@link IJavaProject} for the Android project to validate
+ */
+ InstrumentationRunnerValidator(IJavaProject javaProject) {
+ mJavaProject = javaProject;
+ ManifestData manifestData = AndroidManifestHelper.parseForData(javaProject.getProject());
+ init(manifestData);
+ }
+
+ /**
+ * Initializes the InstrumentationRunnerValidator.
+ *
+ * @param project the {@link IProject} for the Android project to validate
+ * @throws CoreException if a fatal error occurred in initialization
+ */
+ InstrumentationRunnerValidator(IProject project) throws CoreException {
+ this(BaseProjectHelper.getJavaProject(project));
+ }
+
+ /**
+ * Initializes the InstrumentationRunnerValidator with an existing {@link AndroidManifestHelper}
+ *
+ * @param javaProject the {@link IJavaProject} for the Android project to validate
+ * @param manifestData the {@link ManifestData} for the Android project
+ */
+ InstrumentationRunnerValidator(IJavaProject javaProject, ManifestData manifestData) {
+ mJavaProject = javaProject;
+ init(manifestData);
+ }
+
+ private void init(ManifestData manifestData) {
+ if (manifestData == null) {
+ mInstrumentationNames = new String[0];
+ mHasRunnerLibrary = false;
+ return;
+ }
+
+ Instrumentation[] instrumentations = manifestData.getInstrumentations();
+ mInstrumentationNames = new String[instrumentations.length];
+ for (int i = 0; i < instrumentations.length; i++) {
+ mInstrumentationNames[i] = instrumentations[i].getName();
+ }
+ mHasRunnerLibrary = hasTestRunnerLibrary(manifestData);
+ }
+
+ /**
+ * Helper method to determine if given manifest has a <code>SdkConstants.LIBRARY_TEST_RUNNER
+ * </code> library reference
+ *
+ * @param manifestParser the {@link ManifestData} to search
+ * @return true if test runner library found, false otherwise
+ */
+ private boolean hasTestRunnerLibrary(ManifestData manifestData) {
+ for (UsesLibrary lib : manifestData.getUsesLibraries()) {
+ if (AdtConstants.LIBRARY_TEST_RUNNER.equals(lib.getName())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Return the set of instrumentation names for the Android project.
+ *
+ * @return array of instrumentation class names, possibly empty
+ */
+ @NonNull
+ String[] getInstrumentationNames() {
+ return mInstrumentationNames;
+ }
+
+ /**
+ * Helper method to get the first instrumentation that can be used as a test runner.
+ *
+ * @return fully qualified instrumentation class name. <code>null</code> if no valid
+ * instrumentation can be found.
+ */
+ @Nullable
+ String getValidInstrumentationTestRunner() {
+ for (String instrumentation : getInstrumentationNames()) {
+ if (validateInstrumentationRunner(instrumentation) == INSTRUMENTATION_OK) {
+ return instrumentation;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Helper method to determine if specified instrumentation can be used as a test runner
+ *
+ * @param instrumentation the instrumentation class name to validate. Assumes this
+ * instrumentation is one of {@link #getInstrumentationNames()}
+ * @return <code>INSTRUMENTATION_OK</code> if valid, otherwise returns error message
+ */
+ String validateInstrumentationRunner(String instrumentation) {
+ if (!mHasRunnerLibrary) {
+ return String.format(LaunchMessages.InstrValidator_NoTestLibMsg_s,
+ AdtConstants.LIBRARY_TEST_RUNNER);
+ }
+ // check if this instrumentation is the standard test runner
+ if (!instrumentation.equals(SdkConstants.CLASS_INSTRUMENTATION_RUNNER)) {
+ // Ideally, we'd check if the class extends instrumentation test runner.
+ // However, the Google Instrumentation Test Runner extends Google Instrumentation, and not a test runner,
+ // so we just check that the super class is Instrumentation.
+ String result = BaseProjectHelper.testClassForManifest(mJavaProject,
+ instrumentation, SdkConstants.CLASS_INSTRUMENTATION, true);
+ if (result != BaseProjectHelper.TEST_CLASS_OK) {
+ return String.format(
+ LaunchMessages.InstrValidator_WrongRunnerTypeMsg_s,
+ SdkConstants.CLASS_INSTRUMENTATION);
+ }
+ }
+ return INSTRUMENTATION_OK;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/AndroidJUnitLaunchInfo.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/AndroidJUnitLaunchInfo.java
new file mode 100644
index 000000000..08702f4e2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/AndroidJUnitLaunchInfo.java
@@ -0,0 +1,148 @@
+/*
+ * 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.IDevice;
+import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner.TestSize;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.debug.core.ILaunch;
+
+import java.util.Collection;
+import java.util.Collections;
+
+/**
+ * Contains info about Android JUnit launch
+ */
+public class AndroidJUnitLaunchInfo {
+ private final IProject mProject;
+ private final String mAppPackage;
+ private final String mRunner;
+
+ private boolean mDebugMode = false;
+ private Collection<IDevice> mDevices = Collections.EMPTY_LIST;
+ private String mTestPackage = null;
+ private String mTestClass = null;
+ private String mTestMethod = null;
+ private ILaunch mLaunch = null;
+ private TestSize mTestSize = null;
+
+ public AndroidJUnitLaunchInfo(IProject project, String appPackage, String runner) {
+ mProject = project;
+ mAppPackage = appPackage;
+ mRunner = runner;
+ }
+
+ public IProject getProject() {
+ return mProject;
+ }
+
+ public String getAppPackage() {
+ return mAppPackage;
+ }
+
+ public String getRunner() {
+ return mRunner;
+ }
+
+ public boolean isDebugMode() {
+ return mDebugMode;
+ }
+
+ public void setDebugMode(boolean debugMode) {
+ mDebugMode = debugMode;
+ }
+
+ public TestSize getTestSize() {
+ return mTestSize;
+ }
+
+ public void setTestSize(TestSize size) {
+ mTestSize = size;
+ }
+
+ public Collection<IDevice> getDevices() {
+ return mDevices;
+ }
+
+ public void setDevices(Collection<IDevice> devices) {
+ mDevices = devices;
+ }
+
+ /**
+ * Specify to run all tests within given package.
+ *
+ * @param testPackage fully qualified java package
+ */
+ public void setTestPackage(String testPackage) {
+ mTestPackage = testPackage;
+ }
+
+ /**
+ * Return the package of tests to run.
+ *
+ * @return fully qualified java package. <code>null</code> if not specified.
+ */
+ public String getTestPackage() {
+ return mTestPackage;
+ }
+
+ /**
+ * Sets the test class to run.
+ *
+ * @param testClass fully qualfied test class to run
+ * Expected format: x.y.x.testclass
+ */
+ public void setTestClass(String testClass) {
+ mTestClass = testClass;
+ }
+
+ /**
+ * Returns the test class to run.
+ *
+ * @return fully qualfied test class to run.
+ * <code>null</code> if not specified.
+ */
+ public String getTestClass() {
+ return mTestClass;
+ }
+
+ /**
+ * Sets the test method to run. testClass must also be set.
+ *
+ * @param testMethod test method to run
+ */
+ public void setTestMethod(String testMethod) {
+ mTestMethod = testMethod;
+ }
+
+ /**
+ * Returns the test method to run.
+ *
+ * @return test method to run. <code>null</code> if not specified.
+ */
+ public String getTestMethod() {
+ return mTestMethod;
+ }
+
+ public ILaunch getLaunch() {
+ return mLaunch;
+ }
+
+ public void setLaunch(ILaunch launch) {
+ mLaunch = launch;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/AndroidTestReference.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/AndroidTestReference.java
new file mode 100644
index 000000000..ec3104d91
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/AndroidTestReference.java
@@ -0,0 +1,65 @@
+/*
+ * 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 org.eclipse.jdt.internal.junit.runner.ITestIdentifier;
+import org.eclipse.jdt.internal.junit.runner.ITestReference;
+import org.eclipse.jdt.internal.junit.runner.TestExecution;
+
+/**
+ * Base implementation of the Eclipse {@link ITestReference} and {@link ITestIdentifier} interfaces
+ * for Android tests.
+ * <p/>
+ * Provides generic equality/hashcode services
+ */
+@SuppressWarnings("restriction")
+abstract class AndroidTestReference implements ITestReference, ITestIdentifier {
+
+ /**
+ * Gets the {@link ITestIdentifier} for this test reference.
+ */
+ @Override
+ public ITestIdentifier getIdentifier() {
+ // this class serves as its own test identifier
+ return this;
+ }
+
+ /**
+ * Not supported.
+ */
+ @Override
+ public void run(TestExecution execution) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Compares {@link ITestIdentifier} using names
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof ITestIdentifier) {
+ ITestIdentifier testid = (ITestIdentifier) obj;
+ return getName().equals(testid.getName());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return getName().hashCode();
+ }
+}
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);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/TestCaseReference.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/TestCaseReference.java
new file mode 100644
index 000000000..e05e9b857
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/TestCaseReference.java
@@ -0,0 +1,67 @@
+/*
+ * 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.testrunner.TestIdentifier;
+
+import org.eclipse.jdt.internal.junit.runner.IVisitsTestTrees;
+
+/**
+ * Reference for a single Android test method.
+ */
+@SuppressWarnings("restriction")
+class TestCaseReference extends AndroidTestReference {
+ private final String mClassName;
+ private final String mTestName;
+ private final String mDeviceName;
+
+ /**
+ * Creates a TestCaseReference from a {@link TestIdentifier}
+ * @param test
+ */
+ TestCaseReference(String deviceName, TestIdentifier test) {
+ mDeviceName = deviceName;
+ mClassName = test.getClassName();
+ mTestName = test.getTestName();
+ }
+
+ /**
+ * Returns a count of the number of test cases referenced. Is always one for this class.
+ */
+ @Override
+ public int countTestCases() {
+ return 1;
+ }
+
+ /**
+ * Sends test identifier and test count information for this test
+ *
+ * @param notified the {@link IVisitsTestTrees} to send test info to
+ */
+ @Override
+ public void sendTree(IVisitsTestTrees notified) {
+ notified.visitTreeEntry(getIdentifier(), false, countTestCases());
+ }
+
+ /**
+ * Returns the identifier of this test, in a format expected by JDT JUnit
+ */
+ @Override
+ public String getName() {
+ return String.format("%s (%s) [%s]", mTestName, mClassName, mDeviceName);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/TestCollector.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/TestCollector.java
new file mode 100644
index 000000000..32c5ef81e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/TestCollector.java
@@ -0,0 +1,136 @@
+/*
+ * 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.testrunner.ITestRunListener;
+import com.android.ddmlib.testrunner.TestIdentifier;
+
+import java.util.Map;
+
+/**
+ * Collects info about tests to be executed by listening to the results of an Android test run.
+ */
+class TestCollector implements ITestRunListener {
+ private final String mDeviceName;
+ private final TestSuiteReference mDeviceSuiteRef;
+
+ private int mTotalTestCount;
+ /** test name to test suite reference map. */
+
+ private String mErrorMessage = null;
+
+ TestCollector(String deviceName) {
+ mDeviceName = deviceName;
+ mDeviceSuiteRef = new TestSuiteReference(deviceName);
+
+ mTotalTestCount = 0;
+ }
+
+ @Override
+ public synchronized void testEnded(TestIdentifier test, Map<String, String> testMetrics) {
+ // ignore
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.testrunner.ITestRunListener#testFailed(com.android.ddmlib.testrunner.TestIdentifier, java.lang.String)
+ */
+ @Override
+ public synchronized void testFailed(TestIdentifier test, String trace) {
+ // ignore - should be impossible since this is only collecting test information
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.testrunner.ITestRunListener#testIgnored(com.android.ddmlib.testrunner.TestIdentifier)
+ */
+ @Override
+ public synchronized void testIgnored(TestIdentifier test) {
+ // ignore - should be impossible since this is only collecting test information
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.testrunner.ITestRunListener#testAssumptionFailure(com.android.ddmlib.testrunner.TestIdentifier, java.lang.String)
+ */
+ @Override
+ public synchronized void testAssumptionFailure(TestIdentifier test, String trace) {
+ // ignore - should be impossible since this is only collecting test information
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.testrunner.ITestRunListener#testRunEnded(long, Map<String, String>)
+ */
+ @Override
+ public synchronized void testRunEnded(long elapsedTime, Map<String, String> runMetrics) {
+ // ignore
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.testrunner.ITestRunListener#testRunFailed(java.lang.String)
+ */
+ @Override
+ public synchronized void testRunFailed(String errorMessage) {
+ mErrorMessage = errorMessage;
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.testrunner.ITestRunListener#testRunStarted(int)
+ */
+ @Override
+ public synchronized void testRunStarted(String ignoredRunName, int testCount) {
+ mTotalTestCount = testCount;
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.testrunner.ITestRunListener#testRunStopped(long)
+ */
+ @Override
+ public synchronized void testRunStopped(long elapsedTime) {
+ // ignore
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.testrunner.ITestRunListener#testStarted(com.android.ddmlib.testrunner.TestIdentifier)
+ */
+ @Override
+ public synchronized void testStarted(TestIdentifier test) {
+ TestSuiteReference suiteRef = mDeviceSuiteRef.getTestSuite(test.getClassName());
+ if (suiteRef == null) {
+ suiteRef = new TestSuiteReference(test.getClassName());
+ mDeviceSuiteRef.addTest(suiteRef);
+ }
+
+ suiteRef.addTest(new TestCaseReference(mDeviceName, test));
+ }
+
+ /**
+ * Returns the total test count in the test run.
+ */
+ public synchronized int getTestCaseCount() {
+ return mTotalTestCount;
+ }
+
+ /**
+ * Returns the error message that was reported when collecting test info.
+ * Returns <code>null</code> if no error occurred.
+ */
+ public synchronized String getErrorMessage() {
+ return mErrorMessage;
+ }
+
+ public TestSuiteReference getDeviceSuite() {
+ return mDeviceSuiteRef;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/TestSuiteReference.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/TestSuiteReference.java
new file mode 100644
index 000000000..dcc9f10ec
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/TestSuiteReference.java
@@ -0,0 +1,92 @@
+/*
+ * 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 org.eclipse.jdt.internal.junit.runner.IVisitsTestTrees;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Reference for an Android test suite aka class.
+ */
+@SuppressWarnings("restriction")
+class TestSuiteReference extends AndroidTestReference {
+
+ private final String mClassName;
+ private List<AndroidTestReference> mTests;
+
+ /**
+ * Creates a TestSuiteReference
+ *
+ * @param className the fully qualified name of the test class
+ */
+ TestSuiteReference(String className) {
+ mClassName = className;
+ mTests = new ArrayList<AndroidTestReference>();
+ }
+
+ /**
+ * Returns a count of the number of test cases included in this suite.
+ */
+ @Override
+ public int countTestCases() {
+ return mTests.size();
+ }
+
+ /**
+ * Sends test identifier and test count information for this test class, and all its included
+ * test methods.
+ *
+ * @param notified the {@link IVisitsTestTrees} to send test info too
+ */
+ @Override
+ public void sendTree(IVisitsTestTrees notified) {
+ notified.visitTreeEntry(getIdentifier(), true, countTestCases());
+ for (AndroidTestReference ref: mTests) {
+ ref.sendTree(notified);
+ }
+ }
+
+ /**
+ * Return the name of this test class.
+ */
+ @Override
+ public String getName() {
+ return mClassName;
+ }
+
+ /**
+ * Adds a test method to this suite.
+ *
+ * @param testRef the {@link TestCaseReference} to add
+ */
+ void addTest(AndroidTestReference testRef) {
+ mTests.add(testRef);
+ }
+
+ /** Returns the test suite of given name, null if no such test suite exists */
+ public TestSuiteReference getTestSuite(String name) {
+ for (AndroidTestReference ref: mTests) {
+ if (ref instanceof TestSuiteReference && ref.getName().equals(name)) {
+ return (TestSuiteReference) ref;
+ }
+ }
+
+ return null;
+ }
+}