Skip to content

Commit 217db45

Browse files
authored
feat: Support Junit 5 meta-annotations and composed annotations (#756)
1 parent 08d7f68 commit 217db45

File tree

6 files changed

+85
-43
lines changed

6 files changed

+85
-43
lines changed

java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/searcher/BaseFrameworkSearcher.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public String[] getTestClassAnnotations() {
4444
@Override
4545
public boolean isTestClass(IType type) {
4646
for (final String annotation : this.getTestClassAnnotations()) {
47-
if (TestFrameworkUtils.hasAnnotation(type, annotation)) {
47+
if (TestFrameworkUtils.hasAnnotation(type, annotation, false /*checkHierarchy*/)) {
4848
return true;
4949
}
5050
}

java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/searcher/JUnit4TestSearcher.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public boolean isTestMethod(IMethod method) {
4545
return false;
4646
}
4747
for (final String annotation : this.testMethodAnnotations) {
48-
if (TestFrameworkUtils.hasAnnotation(method, annotation)) {
48+
if (TestFrameworkUtils.hasAnnotation(method, annotation, false /*checkHierarchy*/)) {
4949
return true;
5050
}
5151
}

java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/searcher/JUnit5TestSearcher.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public boolean isTestMethod(IMethod method) {
6161
return false;
6262
}
6363
for (final String annotation : this.testMethodAnnotations) {
64-
if (TestFrameworkUtils.hasAnnotation(method, annotation)) {
64+
if (TestFrameworkUtils.hasAnnotation(method, annotation, true /*checkHierarchy*/)) {
6565
if ("org.junit.jupiter.api.TestFactory".equals(annotation)) {
6666
return true;
6767
} else if ("V".equals(method.getReturnType())) {
@@ -83,7 +83,7 @@ public TestItem parseTestItem(IMethod method) throws JavaModelException {
8383
final TestItem item = super.parseTestItem(method);
8484
// Check if the method has annotated with @DisplayName
8585
final Optional<IAnnotation> annotation = TestFrameworkUtils.getAnnotation(method,
86-
DISPLAY_NAME_ANNOTATION_JUNIT5);
86+
DISPLAY_NAME_ANNOTATION_JUNIT5, false /*checkHierarchy*/);
8787
if (annotation.isPresent()) {
8888
item.setDisplayName((String) annotation.get().getMemberValuePairs()[0].getValue());
8989
}

java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/searcher/TestNGTestSearcher.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public boolean isTestMethod(IMethod method) {
4343
return false;
4444
}
4545
for (final String annotation : this.testMethodAnnotations) {
46-
if (TestFrameworkUtils.hasAnnotation(method, annotation)) {
46+
if (TestFrameworkUtils.hasAnnotation(method, annotation, false /*checkHierarchy*/)) {
4747
return true;
4848
}
4949
}

java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/util/TestFrameworkUtils.java

Lines changed: 78 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@
1919

2020
import org.eclipse.jdt.core.IAnnotatable;
2121
import org.eclipse.jdt.core.IAnnotation;
22-
import org.eclipse.jdt.core.ICompilationUnit;
22+
import org.eclipse.jdt.core.IJavaProject;
2323
import org.eclipse.jdt.core.IMember;
2424
import org.eclipse.jdt.core.IMethod;
2525
import org.eclipse.jdt.core.IType;
2626
import org.eclipse.jdt.core.JavaModelException;
2727

28-
import java.util.Arrays;
28+
import java.util.HashSet;
2929
import java.util.Optional;
30-
import java.util.stream.Collectors;
30+
import java.util.Set;
3131

3232
public class TestFrameworkUtils {
3333

@@ -52,51 +52,93 @@ public static TestItem resolveTestItemForClass(IType type) throws JavaModelExcep
5252
return null;
5353
}
5454

55-
public static Optional<IAnnotation> getAnnotation(IMember member, String memberAnnotation) {
56-
try {
57-
final Optional<IAnnotation> matched = getMatchedAnnotation(member, memberAnnotation);
58-
if (!matched.isPresent()) {
59-
return Optional.empty();
60-
}
61-
final IAnnotation annotation = matched.get();
62-
if (!annotation.exists()) {
63-
return Optional.empty();
64-
}
55+
/**
56+
* Find the {@link IAnnotation} if the {@link IMember} is annotated with the given annotation string.
57+
*
58+
* @param member the {@link IMember} to search.
59+
* @param annotationToSearch The annotation string.
60+
* @param checkHierarchy Specify whether to search the whole annotation hierarchy.
61+
*/
62+
public static Optional<IAnnotation> getAnnotation(IMember member, String annotationToSearch,
63+
boolean checkHierarchy) {
64+
if (!IAnnotatable.class.isInstance(member)) {
65+
return Optional.empty();
66+
}
6567

66-
final String name = annotation.getElementName();
68+
final IJavaProject javaProject = member.getJavaProject();
69+
if (javaProject == null) {
70+
return Optional.empty();
71+
}
6772

68-
String[][] fullNameArr = null;
69-
if (IType.class.isInstance(member) && member.getDeclaringType() == null) {
70-
fullNameArr = ((IType) member).resolveType(name);
71-
} else {
72-
fullNameArr = member.getDeclaringType().resolveType(name);
73-
}
74-
if (fullNameArr == null) {
75-
final ICompilationUnit cu = member.getCompilationUnit();
76-
if (cu != null && cu.getImport(memberAnnotation).exists()) {
77-
return Optional.of(annotation);
78-
} else {
79-
return Optional.empty();
73+
final IType declaringType = member.getDeclaringType();
74+
if (declaringType == null) {
75+
return Optional.empty();
76+
}
77+
78+
final IAnnotatable annotatable = (IAnnotatable) member;
79+
try {
80+
for (final IAnnotation annotation : annotatable.getAnnotations()) {
81+
final IType annotationType = getResolvedType(annotation.getElementName(), declaringType, javaProject);
82+
if (annotationType != null) {
83+
if (annotationToSearch.equals(annotationType.getFullyQualifiedName())) {
84+
return Optional.of(annotation);
85+
}
86+
}
87+
if (checkHierarchy) {
88+
final Set<IType> hierarchy = new HashSet<>();
89+
if (matchesInAnnotationHierarchy(annotationToSearch, annotationType, javaProject, hierarchy)) {
90+
return Optional.of(annotation);
91+
}
8092
}
8193
}
82-
final String fullName = Arrays.stream(fullNameArr[0]).collect(Collectors.joining("."));
83-
return fullName.equals(memberAnnotation) ?
84-
Optional.of(annotation) : Optional.empty();
8594
} catch (final JavaModelException e) {
95+
// Swallow the exception
8696
return Optional.empty();
8797
}
98+
return Optional.empty();
8899
}
89100

90-
public static boolean hasAnnotation(IMember member, String annotation) {
91-
return getAnnotation(member, annotation).isPresent();
101+
/**
102+
* Check the given {@link IMember} has annotated with the given annotation string.
103+
*
104+
* @param member the {@link IMember} to search.
105+
* @param annotationToSearch The annotation string.
106+
* @param checkHierarchy Specify whether to search the whole annotation hierarchy.
107+
*/
108+
public static boolean hasAnnotation(IMember member, String annotationToSearch, boolean checkHierarchy) {
109+
return getAnnotation(member, annotationToSearch, checkHierarchy).isPresent();
92110
}
93111

94-
protected static Optional<IAnnotation> getMatchedAnnotation(IMember member, String annotationToSearch)
112+
private static IType getResolvedType(String typeName, IType type, IJavaProject javaProject)
95113
throws JavaModelException {
96-
if (!IAnnotatable.class.isInstance(member)) {
97-
return Optional.empty();
114+
IType resolvedType = null;
115+
if (typeName != null) {
116+
final int pos = typeName.indexOf('<');
117+
if (pos != -1) {
118+
typeName = typeName.substring(0, pos);
119+
}
120+
final String[][] resolvedTypeNames = type.resolveType(typeName);
121+
if (resolvedTypeNames != null && resolvedTypeNames.length > 0) {
122+
final String[] resolvedTypeName = resolvedTypeNames[0];
123+
resolvedType = javaProject.findType(resolvedTypeName[0], resolvedTypeName[1]);
124+
}
125+
}
126+
return resolvedType;
127+
}
128+
129+
private static boolean matchesInAnnotationHierarchy(String annotationFullName, IType annotationType,
130+
IJavaProject javaProject, Set<IType> hierarchy) throws JavaModelException {
131+
if (annotationType != null) {
132+
for (final IAnnotation annotation : annotationType.getAnnotations()) {
133+
final IType annType = getResolvedType(annotation.getElementName(), annotationType, javaProject);
134+
if (annType != null && hierarchy.add(annType)) {
135+
if (annotationFullName.equals(annotationType.getFullyQualifiedName()) ||
136+
matchesInAnnotationHierarchy(annotationFullName, annType, javaProject, hierarchy)) {
137+
return true;
138+
}
139+
}
140+
}
98141
}
99-
return Arrays.stream(((IAnnotatable) member).getAnnotations())
100-
.filter(annotation -> annotationToSearch.endsWith(annotation.getElementName())).findAny();
142+
return false;
101143
}
102144
}

java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/util/TestSearchUtils.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ public static List<TestItem> searchCodeLens(List<Object> arguments, IProgressMon
111111
continue;
112112
}
113113
// Class annotated by @RunWith should be considered as a Suite even it has no test method children
114-
if (TestFrameworkUtils.hasAnnotation(type, JUnit4TestSearcher.RUN_WITH)) {
114+
if (TestFrameworkUtils.hasAnnotation(type, JUnit4TestSearcher.RUN_WITH, false /*checkHierarchy*/)) {
115115
resultList.add(TestItemUtils.constructTestItem(type, TestLevel.CLASS, TestKind.JUnit));
116116
}
117117
}
@@ -425,7 +425,7 @@ private static boolean isTestableClass(IType type) throws JavaModelException {
425425
return false;
426426
}
427427

428-
if (TestFrameworkUtils.hasAnnotation(type, JUnit5TestSearcher.NESTED) ||
428+
if (TestFrameworkUtils.hasAnnotation(type, JUnit5TestSearcher.NESTED, false /*checkHierarchy*/) ||
429429
(Flags.isStatic(flags) && Flags.isPublic(flags))) {
430430
return true;
431431
}

0 commit comments

Comments
 (0)