From 4ade735c91ca5849a01e65e2e8e6ca0fc459316d Mon Sep 17 00:00:00 2001 From: fedejeanne Date: Thu, 20 Jul 2023 06:01:02 +0200 Subject: [PATCH] Show progress when searching test methods in run configuration #653 Move the logic to search for test methods from `JUnitLaunchConfigurationTab` to `TestSearchEngine::findTestMethods`. Extract the whole logic for caching the results into `TestMethodsCache` (new class) and do the whole searching and caching more efficiently i.e. only when necessary and showing the progress with a monitor by using `ModalContext::run` Contributes to https://github.com/eclipse-jdt/eclipse.jdt.ui/issues/653 --- .../jdt/internal/junit/ui/JUnitMessages.java | 2 + .../junit/ui/JUnitMessages.properties | 2 + .../internal/junit/util/TestSearchEngine.java | 133 +++++++ .../launcher/JUnitLaunchConfigurationTab.java | 332 +++++++++--------- .../jdt/junit/launcher/TestMethodsCache.java | 90 +++++ 5 files changed, 385 insertions(+), 174 deletions(-) create mode 100644 org.eclipse.jdt.junit/src/org/eclipse/jdt/junit/launcher/TestMethodsCache.java 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 87212316228..2b155f31f24 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 @@ -372,4 +372,6 @@ private JUnitMessages() { public static String TestRunnerViewPart_JUnitPasteAction_label; public static String TestRunnerViewPart_layout_menu; + + public static String TestSearchEngine_search_message_progress_monitor; } 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 f3d34405358..bba5e6d19e0 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 @@ -276,3 +276,5 @@ JUnitViewEditorLauncher_dialog_title=Import Test Run JUnitViewEditorLauncher_error_occurred=An error occurred while opening a test run file. ClasspathVariableMarkerResolutionGenerator_use_JUnit3=Use the JUnit 3 library ClasspathVariableMarkerResolutionGenerator_use_JUnit3_desc=Changes the classpath variable entry to use the JUnit 3 library + +TestSearchEngine_search_message_progress_monitor=Searching for test methods in ''{0}'' diff --git a/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/util/TestSearchEngine.java b/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/util/TestSearchEngine.java index c41c85adcfc..60ec8300d4a 100644 --- a/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/util/TestSearchEngine.java +++ b/org.eclipse.jdt.junit/src/org/eclipse/jdt/internal/junit/util/TestSearchEngine.java @@ -18,14 +18,27 @@ import java.util.Set; import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.SubMonitor; import org.eclipse.jface.operation.IRunnableContext; import org.eclipse.jface.operation.IRunnableWithProgress; +import org.eclipse.jdt.core.IAnnotation; import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IMethod; import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.Signature; +import org.eclipse.jdt.core.dom.Modifier; +import org.eclipse.jdt.internal.junit.JUnitCorePlugin; +import org.eclipse.jdt.internal.junit.Messages; import org.eclipse.jdt.internal.junit.launcher.ITestKind; +import org.eclipse.jdt.internal.junit.launcher.TestKind; +import org.eclipse.jdt.internal.junit.launcher.TestKindRegistry; +import org.eclipse.jdt.internal.junit.ui.JUnitMessages; /** @@ -48,4 +61,124 @@ public static Set findTests(IRunnableContext context, final IJavaElement return result; } + public static Set findTestMethods(IRunnableContext context, final IJavaProject javaProject, IType type, TestKind testKind) throws InvocationTargetException, InterruptedException { + final Set result= new HashSet<>(); + + IRunnableWithProgress runnable= progressMonitor -> { + try { + String message= Messages.format(JUnitMessages.TestSearchEngine_search_message_progress_monitor, type.getElementName()); + SubMonitor subMonitor= SubMonitor.convert(progressMonitor, message, 1); + + collectMethodNames(type, javaProject, testKind.getId(), result, subMonitor.split(1)); + } catch (CoreException e) { + throw new InvocationTargetException(e); + } + }; + context.run(true, true, runnable); + return result; + } + + private static void collectMethodNames(IType type, IJavaProject javaProject, String testKindId, Set methodNames, IProgressMonitor monitor) throws JavaModelException { + if (type == null) { + return; + } + + SubMonitor subMonitor= SubMonitor.convert(monitor, 3); + + collectDeclaredMethodNames(type, javaProject, testKindId, methodNames); + subMonitor.split(1); + + String superclassName= type.getSuperclassName(); + IType superType= getResolvedType(superclassName, type, javaProject); + collectMethodNames(superType, javaProject, testKindId, methodNames, subMonitor.split(1)); + + String[] superInterfaceNames= type.getSuperInterfaceNames(); + subMonitor.setWorkRemaining(superInterfaceNames.length); + for (String interfaceName : superInterfaceNames) { + superType= getResolvedType(interfaceName, type, javaProject); + collectMethodNames(superType, javaProject, testKindId, methodNames, subMonitor.split(1)); + } + } + + private static IType getResolvedType(String typeName, IType type, IJavaProject javaProject) throws JavaModelException { + IType resolvedType= null; + if (typeName != null) { + int pos= typeName.indexOf('<'); + if (pos != -1) { + typeName= typeName.substring(0, pos); + } + String[][] resolvedTypeNames= type.resolveType(typeName); + if (resolvedTypeNames != null && resolvedTypeNames.length > 0) { + String[] resolvedTypeName= resolvedTypeNames[0]; + resolvedType= javaProject.findType(resolvedTypeName[0], resolvedTypeName[1]); // secondary types not found by this API + } + } + return resolvedType; + } + + private static void collectDeclaredMethodNames(IType type, IJavaProject javaProject, String testKindId, Set methodNames) throws JavaModelException { + IMethod[] methods= type.getMethods(); + for (IMethod method : methods) { + String methodName= method.getElementName(); + 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()) && + methodName.startsWith("test")) { //$NON-NLS-1$ + methodNames.add(methodName); + } + boolean isJUnit3= TestKindRegistry.JUNIT3_TEST_KIND_ID.equals(testKindId); + boolean isJUnit5= TestKindRegistry.JUNIT5_TEST_KIND_ID.equals(testKindId); + if (!isJUnit3 && !Modifier.isPrivate(flags) && !Modifier.isStatic(flags)) { + IAnnotation annotation= method.getAnnotation("Test"); //$NON-NLS-1$ + if (annotation.exists()) { + methodNames.add(methodName + JUnitStubUtility.getParameterTypes(method, false)); + } else if (isJUnit5) { + boolean hasAnyTestAnnotation= method.getAnnotation("TestFactory").exists() //$NON-NLS-1$ + || method.getAnnotation("Testable").exists() //$NON-NLS-1$ + || method.getAnnotation("TestTemplate").exists() //$NON-NLS-1$ + || method.getAnnotation("ParameterizedTest").exists() //$NON-NLS-1$ + || method.getAnnotation("RepeatedTest").exists(); //$NON-NLS-1$ + if (hasAnyTestAnnotation || isAnnotatedWithTestable(method, type, javaProject)) { + methodNames.add(methodName + JUnitStubUtility.getParameterTypes(method, false)); + } + } + } + } + } + + // See JUnit5TestFinder.Annotation#annotates also. + private static boolean isAnnotatedWithTestable(IMethod method, IType declaringType, IJavaProject javaProject) throws JavaModelException { + for (IAnnotation annotation : method.getAnnotations()) { + IType annotationType= getResolvedType(annotation.getElementName(), declaringType, javaProject); + if (annotationType != null) { + if (matchesTestable(annotationType)) { + return true; + } + Set hierarchy= new HashSet<>(); + if (matchesTestableInAnnotationHierarchy(annotationType, javaProject, hierarchy)) { + return true; + } + } + } + return false; + } + + private static boolean matchesTestable(IType annotationType) { + return annotationType != null && JUnitCorePlugin.JUNIT5_TESTABLE_ANNOTATION_NAME.equals(annotationType.getFullyQualifiedName()); + } + + private static boolean matchesTestableInAnnotationHierarchy(IType annotationType, IJavaProject javaProject, Set hierarchy) throws JavaModelException { + if (annotationType != null) { + for (IAnnotation annotation : annotationType.getAnnotations()) { + IType annType= getResolvedType(annotation.getElementName(), annotationType, javaProject); + if (annType != null && hierarchy.add(annType)) { + if (matchesTestable(annType) || matchesTestableInAnnotationHierarchy(annType, javaProject, hierarchy)) { + return true; + } + } + } + } + return false; + } } 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 46498414930..1126379718d 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 @@ -22,32 +22,69 @@ import java.util.HashSet; import java.util.Set; -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.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +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.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + 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.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; + +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.viewers.ArrayContentProvider; +import org.eclipse.jface.viewers.ComboViewer; +import org.eclipse.jface.viewers.ILabelProvider; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.jface.viewers.ViewerFilter; +import org.eclipse.jface.window.Window; + +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.ElementListSelectionDialog; +import org.eclipse.ui.dialogs.ElementTreeSelectionDialog; +import org.eclipse.ui.dialogs.SelectionDialog; + import org.eclipse.debug.core.ILaunchConfiguration; import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; + 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; + import org.eclipse.jdt.internal.junit.BasicElementLabels; import org.eclipse.jdt.internal.junit.JUnitCorePlugin; import org.eclipse.jdt.internal.junit.Messages; @@ -64,14 +101,12 @@ import org.eclipse.jdt.internal.junit.util.JUnitStubUtility; import org.eclipse.jdt.internal.junit.util.LayoutUtil; import org.eclipse.jdt.internal.junit.util.TestSearchEngine; -import org.eclipse.jdt.internal.ui.JavaPlugin; -import org.eclipse.jdt.internal.ui.util.SWTUtil; -import org.eclipse.jdt.internal.ui.wizards.TypedElementSelectionValidator; -import org.eclipse.jdt.internal.ui.wizards.TypedViewerFilter; + import org.eclipse.jdt.launching.AbstractVMInstall; import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; import org.eclipse.jdt.launching.IVMInstall; import org.eclipse.jdt.launching.JavaRuntime; + import org.eclipse.jdt.ui.IJavaElementSearchConstants; import org.eclipse.jdt.ui.JavaElementComparator; import org.eclipse.jdt.ui.JavaElementLabelProvider; @@ -80,39 +115,11 @@ import org.eclipse.jdt.ui.StandardJavaElementContentProvider; import org.eclipse.jdt.ui.dialogs.ITypeInfoFilterExtension; import org.eclipse.jdt.ui.dialogs.TypeSelectionExtension; -import org.eclipse.jface.dialogs.Dialog; -import org.eclipse.jface.preference.IPreferenceStore; -import org.eclipse.jface.viewers.ArrayContentProvider; -import org.eclipse.jface.viewers.ComboViewer; -import org.eclipse.jface.viewers.ILabelProvider; -import org.eclipse.jface.viewers.ISelection; -import org.eclipse.jface.viewers.IStructuredSelection; -import org.eclipse.jface.viewers.LabelProvider; -import org.eclipse.jface.viewers.SelectionChangedEvent; -import org.eclipse.jface.viewers.StructuredSelection; -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.SelectionAdapter; -import org.eclipse.swt.events.SelectionEvent; -import org.eclipse.swt.events.SelectionListener; -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.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.ElementListSelectionDialog; -import org.eclipse.ui.dialogs.ElementTreeSelectionDialog; -import org.eclipse.ui.dialogs.SelectionDialog; + +import org.eclipse.jdt.internal.ui.JavaPlugin; +import org.eclipse.jdt.internal.ui.util.SWTUtil; +import org.eclipse.jdt.internal.ui.wizards.TypedElementSelectionValidator; +import org.eclipse.jdt.internal.ui.wizards.TypedViewerFilter; /** @@ -175,9 +182,7 @@ public class JUnitLaunchConfigurationTab extends AbstractLaunchConfigurationTab private boolean fIsValid= true; - private Set fMethodsCache; - - private String fMethodsCacheKey; + private TestMethodsCache fTestMethodsCache= new TestMethodsCache(); /** * Creates a JUnit launch configuration tab. @@ -260,7 +265,10 @@ public String getText(Object element) { fTestLoaderViewer.setInput(items); fTestLoaderViewer.addSelectionChangedListener(event -> { setEnableTagsGroup(event); - validatePage(); + try (var __= fTestMethodsCache.runNestedCancelable()) { + calculateMethodsCache(); + validatePage(); + } updateLaunchConfigurationDialog(); }); } @@ -309,7 +317,10 @@ public void widgetSelected(SelectionEvent e) { fProjText= new Text(comp, SWT.SINGLE | SWT.BORDER); fProjText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); fProjText.addModifyListener(evt -> { - validatePage(); + try (var __= fTestMethodsCache.runNestedCancelable()) { + calculateMethodsCache(); + validatePage(); + } updateLaunchConfigurationDialog(); fSearchButton.setEnabled(fTestRadioButton.getSelection() && fProjText.getText().length() > 0); }); @@ -334,8 +345,10 @@ public void widgetSelected(SelectionEvent evt) { fTestText= new Text(comp, SWT.SINGLE | SWT.BORDER); fTestText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); fTestText.addModifyListener(evt -> { - fTestMethodSearchButton.setEnabled(fTestText.getText().length() > 0); - validatePage(); + try (var __= fTestMethodsCache.runNestedCancelable()) { + calculateMethodsCache(); + validatePage(); + } updateLaunchConfigurationDialog(); }); @@ -457,23 +470,27 @@ private static Image createImage(String path) { @Override public void initializeFrom(ILaunchConfiguration config) { - fLaunchConfiguration= config; + try (var __= fTestMethodsCache.runNestedCancelable()) { + fLaunchConfiguration= config; - updateProjectFromConfig(config); - String containerHandle= ""; //$NON-NLS-1$ - try { - containerHandle= config.getAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER, ""); //$NON-NLS-1$ - } catch (CoreException ce) { - } + updateProjectFromConfig(config); + String containerHandle= ""; //$NON-NLS-1$ + try { + containerHandle= config.getAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER, ""); //$NON-NLS-1$ + } catch (CoreException ce) { + } - if (containerHandle.length() > 0) - updateTestContainerFromConfig(config); - else - updateTestTypeFromConfig(config); - updateKeepRunning(config); - updateTestLoaderFromConfig(config); + if (containerHandle.length() > 0) { + updateTestContainerFromConfig(config); + } else { + updateTestTypeFromConfig(config); + } + updateKeepRunning(config); + updateTestLoaderFromConfig(config); - validatePage(); + calculateMethodsCache(); + validatePage(); + } } @@ -487,7 +504,9 @@ private void updateTestLoaderFromConfig(ILaunchConfiguration config) { testKind= TestKindRegistry.getDefault().getKind(TestKindRegistry.JUNIT3_TEST_KIND_ID); } } - fTestLoaderViewer.setSelection(new StructuredSelection(testKind)); + try (var __= fTestMethodsCache.runNestedCancelable()) { + fTestLoaderViewer.setSelection(new StructuredSelection(testKind)); + } } private TestKind getSelectedTestKind() { @@ -510,7 +529,9 @@ private void updateProjectFromConfig(ILaunchConfiguration config) { projectName= config.getAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, ""); //$NON-NLS-1$ } catch (CoreException ce) { } - fProjText.setText(projectName); + try (var __= fTestMethodsCache.runNestedCancelable()) { + fProjText.setText(projectName); + } } private void updateTestTypeFromConfig(ILaunchConfiguration config) { @@ -525,9 +546,12 @@ private void updateTestTypeFromConfig(ILaunchConfiguration config) { setEnableSingleTestGroup(true); setEnableContainerTestGroup(false); fTestContainerRadioButton.setSelection(false); - fTestText.setText(testTypeName); - fContainerText.setText(""); //$NON-NLS-1$ - fTestMethodText.setText(fOriginalTestMethodName); + + try (var __= fTestMethodsCache.runNestedCancelable()) { + fTestText.setText(testTypeName); + fContainerText.setText(""); //$NON-NLS-1$ + fTestMethodText.setText(fOriginalTestMethodName); + } } private void updateTestContainerFromConfig(ILaunchConfiguration config) { @@ -548,7 +572,10 @@ private void updateTestContainerFromConfig(ILaunchConfiguration config) { fTestRadioButton.setSelection(false); if (fContainerElement != null) fContainerText.setText(getPresentationName(fContainerElement)); - fTestText.setText(""); //$NON-NLS-1$ + + try (var __= fTestMethodsCache.runNestedCancelable()) { + fTestText.setText(""); //$NON-NLS-1$ + } } @Override @@ -667,9 +694,11 @@ public ITypeInfoFilterExtension getFilterExtension() { IType type= (IType) results[0]; if (type != null) { - fTestText.setText(type.getFullyQualifiedName('.')); - javaProject= type.getJavaProject(); - fProjText.setText(javaProject.getElementName()); + try (var __= fTestMethodsCache.runNestedCancelable()) { + fTestText.setText(type.getFullyQualifiedName('.')); + javaProject= type.getJavaProject(); + fProjText.setText(javaProject.getElementName()); + } } } @@ -684,8 +713,10 @@ private void handleProjectButtonSelected() { return; } - String projectName= project.getElementName(); - fProjText.setText(projectName); + try (var __= fTestMethodsCache.runNestedCancelable()) { + String projectName= project.getElementName(); + fProjText.setText(projectName); + } } private void handleTestMethodSearchButtonSelected() { @@ -705,121 +736,76 @@ private void handleTestMethodSearchButtonSelected() { } } - private Set getMethodsForType(IJavaProject javaProject, IType type, TestKind testKind) throws JavaModelException { + private Set getMethodsForType(IJavaProject javaProject, IType type, TestKind testKind) { if (javaProject == null || type == null || testKind == null) return Collections.emptySet(); String testKindId= testKind.getId(); - String methodsCacheKey= javaProject.getElementName() + '\n' + type.getFullyQualifiedName() + '\n' + testKindId; - if (methodsCacheKey.equals(fMethodsCacheKey)) - return fMethodsCache; - - Set methodNames= new HashSet<>(); - fMethodsCache= methodNames; - fMethodsCacheKey= methodsCacheKey; - - collectMethodNames(type, javaProject, testKindId, methodNames); + String methodsCacheKey= getMethodsCacheKey(javaProject, type, testKindId); + if (fTestMethodsCache.containsKey(methodsCacheKey)) { + return fTestMethodsCache.get(methodsCacheKey); + } - return methodNames; + return Collections.emptySet(); } - private void collectMethodNames(IType type, IJavaProject javaProject, String testKindId, Set methodNames) throws JavaModelException { - if (type == null) { + private void calculateMethodsCache() { + fTestMethodText.setEnabled(false); + fTestMethodSearchButton.setEnabled(false); + + if (fTestMethodsCache.isCanceled()) { return; } - collectDeclaredMethodNames(type, javaProject, testKindId, methodNames); - - String superclassName= type.getSuperclassName(); - IType superType= getResolvedType(superclassName, type, javaProject); - collectMethodNames(superType, javaProject, testKindId, methodNames); - String[] superInterfaceNames= type.getSuperInterfaceNames(); - for (String interfaceName : superInterfaceNames) { - superType= getResolvedType(interfaceName, type, javaProject); - collectMethodNames(superType, javaProject, testKindId, methodNames); - } - } + try { + IJavaProject javaProject= getJavaProject(); - private IType getResolvedType(String typeName, IType type, IJavaProject javaProject) throws JavaModelException { - IType resolvedType= null; - if (typeName != null) { - int pos= typeName.indexOf('<'); - if (pos != -1) { - typeName= typeName.substring(0, pos); - } - String[][] resolvedTypeNames= type.resolveType(typeName); - if (resolvedTypeNames != null && resolvedTypeNames.length > 0) { - String[] resolvedTypeName= resolvedTypeNames[0]; - resolvedType= javaProject.findType(resolvedTypeName[0], resolvedTypeName[1]); // secondary types not found by this API + if (javaProject == null) { + // can't find methods if the project + return; } - } - return resolvedType; - } - private void collectDeclaredMethodNames(IType type, IJavaProject javaProject, String testKindId, Set methodNames) throws JavaModelException { - IMethod[] methods= type.getMethods(); - for (IMethod method : methods) { - String methodName= method.getElementName(); - 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()) && - methodName.startsWith("test")) { //$NON-NLS-1$ - methodNames.add(methodName); - } - boolean isJUnit3= TestKindRegistry.JUNIT3_TEST_KIND_ID.equals(testKindId); - boolean isJUnit5= TestKindRegistry.JUNIT5_TEST_KIND_ID.equals(testKindId); - if (!isJUnit3 && !Modifier.isPrivate(flags) && !Modifier.isStatic(flags)) { - IAnnotation annotation= method.getAnnotation("Test"); //$NON-NLS-1$ - if (annotation.exists()) { - methodNames.add(methodName + JUnitStubUtility.getParameterTypes(method, false)); - } else if (isJUnit5) { - boolean hasAnyTestAnnotation= method.getAnnotation("TestFactory").exists() //$NON-NLS-1$ - || method.getAnnotation("Testable").exists() //$NON-NLS-1$ - || method.getAnnotation("TestTemplate").exists() //$NON-NLS-1$ - || method.getAnnotation("ParameterizedTest").exists() //$NON-NLS-1$ - || method.getAnnotation("RepeatedTest").exists(); //$NON-NLS-1$ - if (hasAnyTestAnnotation || isAnnotatedWithTestable(method, type, javaProject)) { - methodNames.add(methodName + JUnitStubUtility.getParameterTypes(method, false)); - } - } + IType testClass= javaProject.findType(fTestText.getText()); + + if (testClass == null) { + // can't find methods if the class doesn't exist + return; } - } - } - // See JUnit5TestFinder.Annotation#annotates also. - private boolean isAnnotatedWithTestable(IMethod method, IType declaringType, IJavaProject javaProject) throws JavaModelException { - for (IAnnotation annotation : method.getAnnotations()) { - IType annotationType= getResolvedType(annotation.getElementName(), declaringType, javaProject); - if (annotationType != null) { - if (matchesTestable(annotationType)) { - return true; - } - Set hierarchy= new HashSet<>(); - if (matchesTestableInAnnotationHierarchy(annotationType, javaProject, hierarchy)) { - return true; - } + TestKind testKind= getSelectedTestKind(); + + if (testKind == null) { + // no need to search for methods if the type (JUnit3/4/5) is not set + return; } - } - return false; - } - private boolean matchesTestable(IType annotationType) { - return annotationType != null && JUnitCorePlugin.JUNIT5_TESTABLE_ANNOTATION_NAME.equals(annotationType.getFullyQualifiedName()); - } + String methodsCacheKey= getMethodsCacheKey(javaProject, testClass, testKind.getId()); - private boolean matchesTestableInAnnotationHierarchy(IType annotationType, IJavaProject javaProject, Set hierarchy) throws JavaModelException { - if (annotationType != null) { - for (IAnnotation annotation : annotationType.getAnnotations()) { - IType annType= getResolvedType(annotation.getElementName(), annotationType, javaProject); - if (annType != null && hierarchy.add(annType)) { - if (matchesTestable(annType) || matchesTestableInAnnotationHierarchy(annType, javaProject, hierarchy)) { - return true; - } - } + if (fTestMethodsCache.containsKey(methodsCacheKey)) { + // no need to recalculate since the source code can't change while the dialog is open. + fTestMethodText.setEnabled(true); + fTestMethodSearchButton.setEnabled(true); + return; } + + fTestMethodsCache.put(methodsCacheKey, // + TestSearchEngine.findTestMethods(getLaunchConfigurationDialog(), javaProject, testClass, testKind)); + + // calculation successful, reactivate the UI + fTestMethodText.setEnabled(true); + fTestMethodSearchButton.setEnabled(true); + } catch (InvocationTargetException | JavaModelException e) { + JUnitPlugin.log(e); + } catch (InterruptedException e) { + // the user probably canceled the operation. Sadly there is no way to know it for sure since ModalContext::run + // doesn't throw the original OperationCanceledException, it throws a new InterruptedException. + JUnitPlugin.log(e); + fTestMethodsCache.setCanceled(true); } - return false; + } + + private String getMethodsCacheKey(IJavaProject javaProject, IType type, String testKindId) { + return javaProject.getElementName() + '\n' + type.getFullyQualifiedName() + '\n' + testKindId; } private String chooseMethodName(Set methodNames) { @@ -1067,8 +1053,6 @@ private void setEnableSingleTestGroup(boolean enabled) { boolean projectTextHasContents= fProjText.getText().length() > 0; fSearchButton.setEnabled(enabled && projectTextHasContents); fTestMethodLabel.setEnabled(enabled); - fTestMethodText.setEnabled(enabled); - fTestMethodSearchButton.setEnabled(enabled && projectTextHasContents && fTestText.getText().length() > 0); } @Override diff --git a/org.eclipse.jdt.junit/src/org/eclipse/jdt/junit/launcher/TestMethodsCache.java b/org.eclipse.jdt.junit/src/org/eclipse/jdt/junit/launcher/TestMethodsCache.java new file mode 100644 index 00000000000..75aa1f5aa58 --- /dev/null +++ b/org.eclipse.jdt.junit/src/org/eclipse/jdt/junit/launcher/TestMethodsCache.java @@ -0,0 +1,90 @@ +/******************************************************************************* + * Copyright (c) 2023 Vector Informatik GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Vector Informatik GmbH - initial API and implementation + *******************************************************************************/ +package org.eclipse.jdt.junit.launcher; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * This class has the necessary logic to calculate the cache of all test methods that belong to a + * JUnit configuration. + */ +class TestMethodsCache { + /** + * An AutoCloseable that can contain nested instances and can run a + * Runnable upon closing the outer instance.
+ *
+ * Example: + * + *
+	 * 
+	 * try (var outer= new NestedAutoCloseable(() -> System.out.println("Bye bye outer"))) {
+	 * 	try (var inner= new NestedAutoCloseable(() -> System.out.println("Bye bye inner"))) {
+	 * 		// ...
+	 * 	} // doesn't print anything
+	 * } // prints "Bye bye outer"
+	 * 
+ */ + private static class NestedAutoCloseable implements AutoCloseable { + private static int fgDepth; + + private final Runnable fOnCloseOuterBlock; + + NestedAutoCloseable(Runnable onCloseOuterBlock) { + fOnCloseOuterBlock= onCloseOuterBlock; + fgDepth++; + } + + @Override + public final void close() { + fgDepth--; + if (fgDepth == 0) { + fOnCloseOuterBlock.run(); + } + } + } + + private boolean fCanceled; + + private final Map> fCacheMap= new HashMap<>(); + + void put(String key, Set value) { + fCacheMap.put(key, value); + } + + Set get(String key) { + return fCacheMap.get(key); + } + + boolean containsKey(String key) { + return fCacheMap.containsKey(key); + } + + boolean isCanceled() { + return fCanceled; + } + + void setCanceled(boolean canceled) { + fCanceled= canceled; + } + + /** + * @return an AutoCloseable that guarantees that searching for test methods needs + * to be canceled only once even in nested calls. + */ + NestedAutoCloseable runNestedCancelable() { + return new NestedAutoCloseable(() -> fCanceled= false); + } +}