diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit')
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; + } +} |