View Javadoc
1   /*
2    * MIT License
3    * Copyright (c) 2006-2025 JMockit developers
4    * See LICENSE file for full license text.
5    */
6   package mockit.coverage;
7   
8   import static java.lang.reflect.Modifier.isPublic;
9   
10  import edu.umd.cs.findbugs.annotations.NonNull;
11  import edu.umd.cs.findbugs.annotations.Nullable;
12  
13  import java.io.Serializable;
14  import java.lang.annotation.Annotation;
15  import java.lang.reflect.Method;
16  import java.util.HashMap;
17  import java.util.Map;
18  
19  import mockit.internal.util.StackTrace;
20  
21  import org.checkerframework.checker.index.qual.NonNegative;
22  
23  public final class CallPoint implements Serializable {
24      private static final long serialVersionUID = 362727169057343840L;
25      private static final Map<StackTraceElement, Boolean> steCache = new HashMap<>();
26      private static final Class<? extends Annotation> testAnnotation;
27      private static final boolean checkTestAnnotationOnClass;
28      private static final boolean checkIfTestCaseSubclass;
29  
30      static {
31          Class<?> annotation = getJUnitAnnotationIfAvailable();
32          boolean checkOnClassAlso = false;
33  
34          if (annotation == null) {
35              annotation = getTestNGAnnotationIfAvailable();
36              checkOnClassAlso = true;
37          }
38  
39          // noinspection unchecked
40          testAnnotation = (Class<? extends Annotation>) annotation;
41          checkTestAnnotationOnClass = checkOnClassAlso;
42          checkIfTestCaseSubclass = checkForJUnit3Availability();
43      }
44  
45      @Nullable
46      private static Class<?> getJUnitAnnotationIfAvailable() {
47          try {
48              // JUnit 5:
49              return Class.forName("org.junit.jupiter.api.Test");
50          } catch (ClassNotFoundException ignore) {
51              // JUnit 4:
52              try {
53                  return Class.forName("org.junit.Test");
54              } catch (ClassNotFoundException ignored) {
55                  return null;
56              }
57          }
58      }
59  
60      @Nullable
61      private static Class<?> getTestNGAnnotationIfAvailable() {
62          try {
63              return Class.forName("org.testng.annotations.Test");
64          } catch (ClassNotFoundException ignore) {
65              // For older versions of TestNG:
66              try {
67                  return Class.forName("org.testng.Test");
68              } catch (ClassNotFoundException ignored) {
69                  return null;
70              }
71          }
72      }
73  
74      private static boolean checkForJUnit3Availability() {
75          try {
76              Class.forName("junit.framework.TestCase");
77              return true;
78          } catch (ClassNotFoundException ignore) {
79              return false;
80          }
81      }
82  
83      @NonNull
84      private final StackTraceElement ste;
85      @NonNegative
86      private int repetitionCount;
87  
88      private CallPoint(@NonNull StackTraceElement ste) {
89          this.ste = ste;
90      }
91  
92      @NonNull
93      public StackTraceElement getStackTraceElement() {
94          return ste;
95      }
96  
97      @NonNegative
98      public int getRepetitionCount() {
99          return repetitionCount;
100     }
101 
102     public void incrementRepetitionCount() {
103         repetitionCount++;
104     }
105 
106     public boolean isSameTestMethod(@NonNull CallPoint other) {
107         StackTraceElement thisSTE = ste;
108         StackTraceElement otherSTE = other.ste;
109         return thisSTE == otherSTE || thisSTE.getClassName().equals(otherSTE.getClassName())
110                 && thisSTE.getMethodName().equals(otherSTE.getMethodName());
111     }
112 
113     public boolean isSameLineInTestCode(@NonNull CallPoint other) {
114         return isSameTestMethod(other) && ste.getLineNumber() == other.ste.getLineNumber();
115     }
116 
117     @Nullable
118     static CallPoint create(@NonNull Throwable newThrowable) {
119         StackTrace st = new StackTrace(newThrowable);
120         int n = st.getDepth();
121 
122         for (int i = 2; i < n; i++) {
123             StackTraceElement ste = st.getElement(i);
124 
125             if (isTestMethod(ste)) {
126                 return new CallPoint(ste);
127             }
128         }
129 
130         return null;
131     }
132 
133     private static boolean isTestMethod(@NonNull StackTraceElement ste) {
134         if (steCache.containsKey(ste)) {
135             return steCache.get(ste);
136         }
137 
138         boolean isTestMethod = false;
139 
140         if (ste.getFileName() != null && ste.getLineNumber() >= 0) {
141             String className = ste.getClassName();
142 
143             if (!isClassInExcludedPackage(className)) {
144                 Class<?> aClass = loadClass(className);
145 
146                 if (aClass != null) {
147                     isTestMethod = isTestMethod(aClass, ste.getMethodName());
148                 }
149             }
150         }
151 
152         steCache.put(ste, isTestMethod);
153         return isTestMethod;
154     }
155 
156     private static boolean isClassInExcludedPackage(@NonNull String className) {
157         return className.startsWith("jakarta.") || className.startsWith("java.") || className.startsWith("javax.")
158                 || className.startsWith("sun.") || className.startsWith("org.junit.")
159                 || className.startsWith("org.testng.") || className.startsWith("mockit.");
160     }
161 
162     @Nullable
163     private static Class<?> loadClass(@NonNull String className) {
164         try {
165             return Class.forName(className);
166         } catch (ClassNotFoundException | LinkageError ignore) {
167             return null;
168         }
169     }
170 
171     private static boolean isTestMethod(@NonNull Class<?> testClass, @NonNull String methodName) {
172         if (checkTestAnnotationOnClass && testClass.isAnnotationPresent(testAnnotation)) {
173             return true;
174         }
175 
176         Method method = findMethod(testClass, methodName);
177 
178         return method != null && (containsATestFrameworkAnnotation(method.getDeclaredAnnotations())
179                 || checkIfTestCaseSubclass && isJUnit3xTestMethod(testClass, method));
180     }
181 
182     @Nullable
183     private static Method findMethod(@NonNull Class<?> aClass, @NonNull String name) {
184         try {
185             for (Method method : aClass.getDeclaredMethods()) {
186                 if (method.getReturnType() == void.class && name.equals(method.getName())) {
187                     return method;
188                 }
189             }
190         } catch (NoClassDefFoundError ignore) {
191         }
192 
193         return null;
194     }
195 
196     private static boolean containsATestFrameworkAnnotation(@NonNull Annotation[] methodAnnotations) {
197         for (Annotation annotation : methodAnnotations) {
198             String annotationName = annotation.annotationType().getName();
199 
200             if (annotationName.startsWith("org.junit.") || annotationName.startsWith("org.testng.")) {
201                 return true;
202             }
203         }
204 
205         return false;
206     }
207 
208     private static boolean isJUnit3xTestMethod(@NonNull Class<?> aClass, @NonNull Method method) {
209         if (!isPublic(method.getModifiers()) || !method.getName().startsWith("test")) {
210             return false;
211         }
212 
213         Class<?> superClass = aClass.getSuperclass();
214 
215         while (superClass != Object.class) {
216             if ("junit.framework.TestCase".equals(superClass.getName())) {
217                 return true;
218             }
219 
220             superClass = superClass.getSuperclass();
221         }
222 
223         return false;
224     }
225 }