Bug 285637: [JUnit] Improve discoverability of the ability to run a single method under JUnit Tests
diff --git a/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/JUnitMessages.java b/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/JUnitMessages.java
index 049d52b..9e61ef4 100644
--- a/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/JUnitMessages.java
+++ b/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/JUnitMessages.java
@@ -8,6 +8,7 @@
  * Contributors:
  *     IBM Corporation - initial API and implementation
  *     David Saff (saff@mit.edu) - bug 102632: [JUnit] Support for JUnit 4.
+ *     Robert Konigsberg <konigsberg@google.com> - [JUnit] Improve discoverability of the ability to run a single method under JUnit Tests - https://bugs.eclipse.org/bugs/show_bug.cgi?id=285637
  *******************************************************************************/
 package org.eclipse.jdt.internal.junit.ui;
 
@@ -104,6 +105,8 @@
 
 	public static String JUnitLaunchConfigurationTab_error_test_class_not_found;
 
+	public static String JUnitLaunchConfigurationTab_error_test_method_not_found;
+
 	public static String JUnitLaunchConfigurationTab_error_testannotationnotonpath;
 
 	public static String JUnitLaunchConfigurationTab_error_testcasenotonpath;
@@ -128,6 +131,15 @@
 
 	public static String JUnitLaunchConfigurationTab_label_search;
 
+	public static String JUnitLaunchConfigurationTab_label_search_method;
+
+	public static String JUnitLaunchConfigurationTab_method_text_decoration;
+
+	public static String JUnitLaunchConfigurationTab_select_method_header;
+	public static String JUnitLaunchConfigurationTab_select_method_title;
+
+	public static String JUnitLaunchConfigurationTab_all_methods_text;
+
 	public static String JUnitLaunchConfigurationTab_label_test;
 
 	public static String JUnitLaunchConfigurationTab_projectdialog_message;
diff --git a/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/JUnitMessages.properties b/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/JUnitMessages.properties
index 81a82cb..caf2ac7 100644
--- a/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/JUnitMessages.properties
+++ b/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/ui/JUnitMessages.properties
@@ -7,6 +7,7 @@
 #
 # Contributors:
 #     IBM Corporation - initial API and implementation
+#     Robert Konigsberg <konigsberg@google.com> - [JUnit] Improve discoverability of the ability to run a single method under JUnit Tests - https://bugs.eclipse.org/bugs/show_bug.cgi?id=285637
 ###############################################################################
 CopyTrace_action_label=Copy Trace
 CopyTraceAction_problem=Problem Copying to Clipboard
@@ -117,6 +118,7 @@
 
 JUnitLaunchConfigurationDelegate_dialog_title=Problems Launching JUnit Tests
 JUnitLaunchConfigurationTab_error_test_class_not_found=Can not find test class ''{0}'' in project ''{1}''
+JUnitLaunchConfigurationTab_error_test_method_not_found=Can not find test method {0}.{1} in project {2}
 JUnitLaunchConfigurationTab_error_testannotationnotonpath=Cannot find class 'org.junit.Test' on project build path.
 JUnitLaunchConfigurationTab_label_oneTest=Run a s&ingle test
 JUnitLaunchConfigurationTab_label_project=&Project:
@@ -124,6 +126,11 @@
 JUnitLaunchConfigurationTab_label_browse=&Browse...
 JUnitLaunchConfigurationTab_label_test=T&est class:
 JUnitLaunchConfigurationTab_label_search=&Search...
+JUnitLaunchConfigurationTab_label_search_method=Sea&rch...
+JUnitLaunchConfigurationTab_method_text_decoration=Omit the method name to run all tests.
+JUnitLaunchConfigurationTab_select_method_header=Choose a test method for ''{0}'':
+JUnitLaunchConfigurationTab_select_method_title=Test Method Selection
+JUnitLaunchConfigurationTab_all_methods_text=(all methods)
 JUnitLaunchConfigurationTab_label_containerTest=Run &all tests in the selected project, package or source folder:
 JUnitLaunchConfigurationTab_label_keeprunning=&Keep JUnit running after a test run when debugging
 JUnitLaunchConfigurationTab_testdialog_title=Test Selection
@@ -132,7 +139,7 @@
 JUnitLaunchConfigurationTab_projectdialog_message=Choose a project to constrain the search for test classes:
 JUnitLaunchConfigurationTab_tab_label=Test
 JUnitMainTab_label_defaultpackage=(default package)
-JUnitLaunchConfigurationTab_label_method=Test method: {0} 
+JUnitLaunchConfigurationTab_label_method=Test &method:
 JUnitLaunchConfigurationTab_Test_Loader=&Test runner:
 JUnitLaunchConfigurationTab_folderdialog_title=Folder Selection
 JUnitLaunchConfigurationTab_folderdialog_message=Choose a Project, Source Folder or Package:
diff --git a/org.eclipse.jdt.junit/src/org/eclipse/jdt/junit/launcher/JUnitLaunchConfigurationTab.java b/org.eclipse.jdt.junit/src/org/eclipse/jdt/junit/launcher/JUnitLaunchConfigurationTab.java
index d5272c2..328f3a4 100644
--- a/org.eclipse.jdt.junit/src/org/eclipse/jdt/junit/launcher/JUnitLaunchConfigurationTab.java
+++ b/org.eclipse.jdt.junit/src/org/eclipse/jdt/junit/launcher/JUnitLaunchConfigurationTab.java
@@ -9,12 +9,15 @@
  *     IBM Corporation - initial API and implementation
  *     Sebastian Davids: sdavids@gmx.de bug: 26293, 27889
  *     David Saff (saff@mit.edu) - bug 102632: [JUnit] Support for JUnit 4.
+ *     Robert Konigsberg <konigsberg@google.com> - [JUnit] Improve discoverability of the ability to run a single method under JUnit Tests - https://bugs.eclipse.org/bugs/show_bug.cgi?id=285637
  *******************************************************************************/
 package org.eclipse.jdt.junit.launcher;
 
 import java.lang.reflect.InvocationTargetException;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashSet;
+import java.util.Set;
 
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.events.ModifyEvent;
@@ -69,15 +72,19 @@
 
 import org.eclipse.debug.ui.AbstractLaunchConfigurationTab;
 
+import org.eclipse.jdt.core.IAnnotation;
 import org.eclipse.jdt.core.IJavaElement;
 import org.eclipse.jdt.core.IJavaModel;
 import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IMethod;
 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.core.Signature;
+import org.eclipse.jdt.core.dom.Modifier;
 import org.eclipse.jdt.core.search.IJavaSearchScope;
 import org.eclipse.jdt.core.search.SearchEngine;
 
@@ -136,6 +143,8 @@
 	private final Image fTestIcon= createImage("obj16/test.gif"); //$NON-NLS-1$
 	private String fOriginalTestMethodName;
 	private Label fTestMethodLabel;
+	private Text fTestMethodText;
+	private Button fTestMethodSearchButton;
 	private Text fContainerText;
 	private IJavaElement fContainerElement;
 	private final ILabelProvider fJavaElementLabelProvider= new JavaElementLabelProvider();
@@ -149,6 +158,11 @@
 
 	private ILaunchConfiguration fLaunchConfiguration;
 
+	private boolean fIsValid= true;
+	
+	private Set/*<String>*/ fMethodsCache;
+	private String fMethodsCacheKey;
+	
 	/**
 	 * Creates a JUnit launch configuration tab.
 	 */
@@ -168,12 +182,12 @@
 		comp.setLayout(topLayout);
 
 		createSingleTestSection(comp);
+		createSpacer(comp);
+		
 		createTestContainerSelectionGroup(comp);
-
 		createSpacer(comp);
 
 		createTestLoaderGroup(comp);
-
 		createSpacer(comp);
 
 		createKeepAliveGroup(comp);
@@ -264,6 +278,7 @@
 		fTestText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
 		fTestText.addModifyListener(new ModifyListener() {
 			public void modifyText(ModifyEvent evt) {
+				fTestMethodSearchButton.setEnabled(fTestText.getText().length() > 0);
 				validatePage();
 				updateLaunchConfigurationDialog();
 			}
@@ -279,14 +294,35 @@
 		});
 		setButtonGridData(fSearchButton);
 
-		new Label(comp, SWT.NONE);
-
-		fTestMethodLabel= new Label(comp, SWT.NONE);
-		fTestMethodLabel.setText("");  //$NON-NLS-1$
-		gd= new GridData();
-		gd.horizontalSpan = 2;
+		fTestMethodLabel = new Label(comp, SWT.NONE);
+		gd = new GridData();
+		gd.horizontalIndent = 25;
 		fTestMethodLabel.setLayoutData(gd);
+		fTestMethodLabel.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_method);
 
+		
+		fTestMethodText= new Text(comp, SWT.SINGLE | SWT.BORDER);
+		gd= new GridData(GridData.FILL_HORIZONTAL);
+		fTestMethodText.setLayoutData(gd);
+
+		fTestMethodText.addModifyListener(new ModifyListener() {
+			public void modifyText(ModifyEvent evt) {
+				validatePage();
+				updateLaunchConfigurationDialog();
+			}
+		});
+
+
+		fTestMethodSearchButton = new Button(comp, SWT.PUSH);
+		fTestMethodSearchButton.setEnabled(fTestText.getText().length() > 0);
+		fTestMethodSearchButton.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_search_method);
+		fTestMethodSearchButton.addSelectionListener(new SelectionAdapter() {
+			public void widgetSelected(SelectionEvent evt) {
+				handleTestMethodSearchButtonSelected();
+			}
+		});
+
+		setButtonGridData(fTestMethodSearchButton);
 	}
 
 	private void createTestContainerSelectionGroup(Composite comp) {
@@ -428,9 +464,7 @@
 		fTestContainerRadioButton.setSelection(false);
 		fTestText.setText(testTypeName);
 		fContainerText.setText(""); //$NON-NLS-1$
-		String methodLabelText= "".equals(fOriginalTestMethodName) ? "" : //$NON-NLS-1$//$NON-NLS-2$
-				Messages.format(JUnitMessages.JUnitLaunchConfigurationTab_label_method, fOriginalTestMethodName);
-		fTestMethodLabel.setText(methodLabelText);
+		fTestMethodText.setText(fOriginalTestMethodName);
 	}
 
 	private void updateTestContainerFromConfig(ILaunchConfiguration config) {
@@ -469,7 +503,7 @@
 			config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, fProjText.getText());
 			config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, fTestText.getText());
 			config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER, ""); //$NON-NLS-1$
-			config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_METHOD_NAME, fOriginalTestMethodName);
+			config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_METHOD_NAME, fTestMethodText.getText());
 		}
 		config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_KEEPRUNNING, fKeepRunning.getSelection());
 		try {
@@ -600,6 +634,104 @@
 		fProjText.setText(projectName);
 	}
 
+	private void handleTestMethodSearchButtonSelected() {
+		try {
+			IJavaProject javaProject = getJavaProject();
+			IType testType= javaProject.findType(fTestText.getText());
+			Set methodNames= getMethodsForType(javaProject, testType, getSelectedTestKind());
+			String methodName= chooseMethodName(methodNames);
+			
+			if (methodName != null) {
+				fTestMethodText.setText(methodName);
+				validatePage();
+				updateLaunchConfigurationDialog();
+			}
+		} catch (JavaModelException e) {
+			JUnitPlugin.log(e.getStatus());
+		}
+	}
+
+	private Set getMethodsForType(IJavaProject javaProject, IType type, TestKind testKind) throws JavaModelException {
+		if (javaProject == null || type == null || testKind == null)
+			return Collections.EMPTY_SET;
+		
+		String methodsCacheKey= javaProject.getElementName() + '\n' + type.getFullyQualifiedName() + '\n' + testKind.getId();
+		if (methodsCacheKey.equals(fMethodsCacheKey))
+			return fMethodsCache;
+		
+		Set methodNames= new HashSet();
+		fMethodsCache= methodNames;
+		fMethodsCacheKey= methodsCacheKey;
+
+		boolean isJUnit4= TestKindRegistry.JUNIT4_TEST_KIND_ID.equals(testKind.getId());
+
+		while (type != null) {
+			IMethod[] methods= type.getMethods();
+			for (int i= 0; i < methods.length; i++) {
+				IMethod method= methods[i];
+				int flags= method.getFlags();
+				// Only include public, non-static, no-arg methods that return void and start with "test":
+				if (Modifier.isPublic(flags) && !Modifier.isStatic(flags) &&
+						method.getNumberOfParameters() == 0 && Signature.SIG_VOID.equals(method.getReturnType()) &&
+						method.getElementName().startsWith("test")) { //$NON-NLS-1$
+					methodNames.add(method.getElementName());
+				}
+				if (isJUnit4) {
+					IAnnotation annotation= method.getAnnotation("Test"); //$NON-NLS-1$
+					if (annotation.exists()) {
+						methodNames.add(method.getElementName());
+					}
+				}
+			}
+			String superclassName= type.getSuperclassName();
+			if (superclassName != null) {
+				int pos= superclassName.indexOf('<');
+				if (pos != -1) 
+					superclassName= superclassName.substring(0, pos);
+				String[][] resolvedSupertype= type.resolveType(superclassName);
+				if (resolvedSupertype != null && resolvedSupertype.length > 0) {
+					String[] superclass= resolvedSupertype[0];
+					type= javaProject.findType(superclass[0], superclass[1]);
+				} else {
+					type= null;
+				}
+			} else {
+				type= null;
+			}
+		}
+		return methodNames;
+	}
+
+	private String chooseMethodName(Set methodNames) {
+		Shell shell= getShell();
+
+		ElementListSelectionDialog dialog= new ElementListSelectionDialog(shell, new LabelProvider());
+		dialog.setMessage(Messages.format(JUnitMessages.JUnitLaunchConfigurationTab_select_method_header, fTestText.getText()));
+		dialog.setTitle(JUnitMessages.JUnitLaunchConfigurationTab_select_method_title);
+
+		int methodCount= methodNames.size();
+		String[] elements= new String[methodCount + 1];
+		methodNames.toArray(elements);
+		elements[methodCount]= JUnitMessages.JUnitLaunchConfigurationTab_all_methods_text;
+
+		dialog.setElements(elements);
+
+		String methodName= fTestMethodText.getText();
+
+		if (methodNames.contains(methodName)) {
+			dialog.setInitialSelections(new String[] { methodName });
+		}
+
+		dialog.setAllowDuplicates(false);
+		dialog.setMultipleSelection(false);
+		if (dialog.open() == Window.OK) {
+			String result= (String)dialog.getFirstResult();
+			return (result == null || result.equals(JUnitMessages.JUnitLaunchConfigurationTab_all_methods_text))
+					? "" : result; //$NON-NLS-1$
+		}
+		return null;
+	}
+	
 	/*
 	 * Realize a Java Project selection dialog and return the first selected project,
 	 * or null if there was none.
@@ -661,7 +793,7 @@
 	 */
 	public boolean isValid(ILaunchConfiguration config) {
 		validatePage();
-		return getErrorMessage() == null;
+		return fIsValid;
 	}
 
 	private void testModeChanged() {
@@ -679,6 +811,15 @@
 		validatePage();
 		updateLaunchConfigurationDialog();
 	}
+	
+	/*
+	 * @see org.eclipse.debug.ui.AbstractLaunchConfigurationTab#setErrorMessage(java.lang.String)
+	 * @since 3.6
+	 */
+	protected void setErrorMessage(String errorMessage) {
+		fIsValid= errorMessage == null;
+		super.setErrorMessage(errorMessage);
+	}
 
 	private void validatePage() {
 
@@ -724,10 +865,19 @@
 				setErrorMessage(JUnitMessages.JUnitLaunchConfigurationTab_error_testnotdefined);
 				return;
 			}
-			if (javaProject.findType(className) == null) {
+			IType type= javaProject.findType(className);
+			if (type == null) {
 				setErrorMessage(Messages.format(JUnitMessages.JUnitLaunchConfigurationTab_error_test_class_not_found, new String[] { className, projectName }));
 				return;
 			}
+			String methodName = fTestMethodText.getText();
+			if (methodName.length() > 0) {
+				Set methodsForType= getMethodsForType(javaProject, type, getSelectedTestKind());
+				if (!methodsForType.contains(methodName)) {
+					super.setErrorMessage(Messages.format(JUnitMessages.JUnitLaunchConfigurationTab_error_test_method_not_found, new String[] { className, methodName, projectName }));
+					return;
+				}
+			}
 
 
 		} catch (CoreException e) {
@@ -785,8 +935,11 @@
 		fProjButton.setEnabled(enabled);
 		fTestLabel.setEnabled(enabled);
 		fTestText.setEnabled(enabled);
-		fSearchButton.setEnabled(enabled && fProjText.getText().length() > 0);
+		boolean projectTextHasContents= fProjText.getText().length() > 0;
+		fSearchButton.setEnabled(enabled && projectTextHasContents);
 		fTestMethodLabel.setEnabled(enabled);
+		fTestMethodText.setEnabled(enabled);
+		fTestMethodSearchButton.setEnabled(enabled && projectTextHasContents &&  fTestText.getText().length() > 0);
 	}
 
 	/* (non-Javadoc)