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.internal.expectations.invocation;
7   
8   import static mockit.internal.util.TypeDescriptor.getClassForType;
9   
10  import edu.umd.cs.findbugs.annotations.NonNull;
11  import edu.umd.cs.findbugs.annotations.Nullable;
12  
13  import java.io.IOException;
14  import java.util.List;
15  import java.util.Map;
16  
17  import mockit.asm.types.JavaType;
18  import mockit.internal.expectations.argumentMatching.ArgumentMatcher;
19  import mockit.internal.expectations.state.MockedTypeCascade;
20  import mockit.internal.reflection.GenericTypeReflection;
21  import mockit.internal.reflection.GenericTypeReflection.GenericSignature;
22  import mockit.internal.state.TestRun;
23  import mockit.internal.util.ClassLoad;
24  import mockit.internal.util.DefaultValues;
25  import mockit.internal.util.ObjectMethods;
26  import mockit.internal.util.StackTrace;
27  
28  import org.checkerframework.checker.index.qual.NonNegative;
29  
30  @SuppressWarnings("OverlyComplexClass")
31  public final class ExpectedInvocation {
32      @NonNull
33      private static final Object UNDEFINED_DEFAULT_RETURN = new Object();
34  
35      @Nullable
36      public final Object instance;
37      @Nullable
38      public Object replacementInstance;
39      public boolean matchInstance;
40      @NonNull
41      public final InvocationArguments arguments;
42      @Nullable
43      private final ExpectationError invocationCause;
44      @Nullable
45      Object defaultReturnValue;
46  
47      public ExpectedInvocation(@Nullable Object mock, @NonNull String mockedClassDesc, @NonNull String mockNameAndDesc,
48              @Nullable String genericSignature, @NonNull Object[] args) {
49          instance = mock;
50          arguments = new InvocationArguments(0, mockedClassDesc, mockNameAndDesc, genericSignature, args);
51          invocationCause = null;
52          defaultReturnValue = determineDefaultReturnValueFromMethodSignature();
53      }
54  
55      public ExpectedInvocation(@Nullable Object mock, int access, @NonNull String mockedClassDesc,
56              @NonNull String mockNameAndDesc, boolean matchInstance, @Nullable String genericSignature,
57              @NonNull Object[] args) {
58          instance = mock;
59          this.matchInstance = matchInstance;
60          arguments = new InvocationArguments(access, mockedClassDesc, mockNameAndDesc, genericSignature, args);
61          invocationCause = new ExpectationError();
62          defaultReturnValue = determineDefaultReturnValueFromMethodSignature();
63      }
64  
65      @Nullable
66      public AssertionError getInvocationCause() {
67          return invocationCause;
68      }
69  
70      @NonNull
71      private Object determineDefaultReturnValueFromMethodSignature() {
72          if (instance != null) {
73              Object rv = ObjectMethods.evaluateOverride(instance, getMethodNameAndDescription(), getArgumentValues());
74  
75              if (rv != null) {
76                  return rv;
77              }
78          }
79  
80          return UNDEFINED_DEFAULT_RETURN;
81      }
82  
83      // Simple getters
84      // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
85  
86      @NonNull
87      public String getClassDesc() {
88          return arguments.classDesc;
89      }
90  
91      @NonNull
92      public String getClassName() {
93          return arguments.getClassName();
94      }
95  
96      @NonNull
97      public String getMethodNameAndDescription() {
98          return arguments.methodNameAndDesc;
99      }
100 
101     @NonNull
102     public Object[] getArgumentValues() {
103         return arguments.getValues();
104     }
105 
106     public boolean isConstructor() {
107         return arguments.isForConstructor();
108     }
109 
110     @NonNull
111     public String getSignatureWithResolvedReturnType() {
112         String signature = arguments.genericSignature;
113 
114         if (signature != null) {
115             // TODO: cache it for use in return type conversion, cascading, etc.
116             String classDesc = getClassDesc();
117             Class<?> mockedClass = instance != null ? instance.getClass() : ClassLoad.loadByInternalName(classDesc);
118             GenericTypeReflection reflection = new GenericTypeReflection(mockedClass, null);
119             signature = reflection.resolveSignature(classDesc, signature);
120 
121             char firstTypeChar = signature.charAt(signature.indexOf(')') + 1);
122 
123             if (firstTypeChar != 'T' && firstTypeChar != '[') {
124                 return signature;
125             }
126         }
127 
128         return arguments.methodNameAndDesc;
129     }
130 
131     // Matching based on instance or mocked type
132     // ///////////////////////////////////////////////////////////////////////////////////////////
133 
134     public boolean isMatch(@Nullable Object mock, @NonNull String invokedClassDesc, @NonNull String invokedMethod) {
135         return (invokedClassDesc.equals(getClassDesc()) || mock != null && TestRun.mockFixture().isCaptured(mock))
136                 && (isMatchingGenericMethod(mock, invokedMethod) || isMatchingMethod(invokedMethod));
137     }
138 
139     private boolean isMatchingGenericMethod(@Nullable Object mock, @NonNull String invokedMethod) {
140         if (mock != null && instance != null) {
141             String genericSignature = arguments.genericSignature;
142 
143             if (genericSignature != null) {
144                 Class<?> mockedClass = mock.getClass();
145 
146                 if (mockedClass != instance.getClass()) {
147                     GenericTypeReflection typeReflection = new GenericTypeReflection(mockedClass, null);
148                     GenericSignature parsedSignature = typeReflection.parseSignature(genericSignature);
149                     return parsedSignature.satisfiesSignature(invokedMethod) && isMatchingMethodName(invokedMethod);
150                 }
151             }
152         }
153 
154         return false;
155     }
156 
157     private boolean isMatchingMethod(@NonNull String invokedMethod) {
158         int returnTypeStartPos = getReturnTypePosition(invokedMethod);
159 
160         if (returnTypeStartPos < 0) {
161             return false;
162         }
163 
164         if (haveSameReturnTypes(invokedMethod, returnTypeStartPos)) {
165             return true;
166         }
167 
168         // At this point the methods are known to differ only in return type, so check if the return type of
169         // the recorded one is assignable to the return type of the one invoked:
170         return isReturnTypeOfRecordedMethodAssignableToReturnTypeOfInvokedMethod(invokedMethod, returnTypeStartPos);
171     }
172 
173     private boolean isMatchingMethodName(@NonNull String invokedMethod) {
174         int methodNameEndPos = invokedMethod.indexOf('(');
175         String methodName = invokedMethod.substring(0, methodNameEndPos + 1);
176         return getMethodNameAndDescription().startsWith(methodName);
177     }
178 
179     // Returns -1 if the method names or parameters are different.
180     private int getReturnTypePosition(@NonNull String invokedMethod) {
181         String recordedMethod = getMethodNameAndDescription();
182         int i = 0;
183 
184         while (true) {
185             char c = recordedMethod.charAt(i);
186 
187             if (c != invokedMethod.charAt(i)) {
188                 return -1;
189             }
190 
191             i++;
192 
193             if (c == ')') {
194                 return i;
195             }
196         }
197     }
198 
199     private boolean haveSameReturnTypes(@NonNull String invokedMethod, @NonNegative int returnTypeStartPos) {
200         String recordedMethod = getMethodNameAndDescription();
201         int n = invokedMethod.length();
202 
203         if (n != recordedMethod.length()) {
204             return false;
205         }
206 
207         int j = returnTypeStartPos;
208 
209         while (true) {
210             char c = recordedMethod.charAt(j);
211 
212             if (c != invokedMethod.charAt(j)) {
213                 return false;
214             }
215 
216             j++;
217 
218             if (j == n) {
219                 return true;
220             }
221         }
222     }
223 
224     private boolean isReturnTypeOfRecordedMethodAssignableToReturnTypeOfInvokedMethod(@NonNull String invokedMethod,
225             @NonNegative int returnTypeStartPos) {
226         String recordedMethod = getMethodNameAndDescription();
227         JavaType recordedRT = JavaType.getType(recordedMethod.substring(returnTypeStartPos));
228         JavaType invokedRT = JavaType.getType(invokedMethod.substring(returnTypeStartPos));
229 
230         return getClassForType(invokedRT).isAssignableFrom(getClassForType(recordedRT));
231     }
232 
233     public boolean isMatch(@NonNull ExpectedInvocation other) {
234         return isMatch(other.instance, other.getClassDesc(), other.getMethodNameAndDescription(), null);
235     }
236 
237     public boolean isMatch(@Nullable Object replayInstance, @NonNull String invokedClassDesc,
238             @NonNull String invokedMethod, @Nullable Map<Object, Object> replacementMap) {
239         return isMatch(replayInstance, invokedClassDesc, invokedMethod) && (arguments.isForConstructor()
240                 || !matchInstance || isEquivalentInstance(replayInstance, replacementMap));
241     }
242 
243     private boolean isEquivalentInstance(@Nullable Object mockedInstance,
244             @Nullable Map<Object, Object> replacementMap) {
245         return mockedInstance == instance || mockedInstance != null && instance != null && replacementMap != null
246                 && replacementMap.get(mockedInstance) == instance;
247     }
248 
249     // Creation of Error instances for invocation mismatch reporting
250     // ///////////////////////////////////////////////////////////////////////
251 
252     @NonNull
253     public UnexpectedInvocation errorForUnexpectedInvocation() {
254         String initialMessage = "Unexpected invocation of " + this;
255         return newUnexpectedInvocationWithCause("Unexpected invocation", initialMessage);
256     }
257 
258     @NonNull
259     private UnexpectedInvocation newUnexpectedInvocationWithCause(@NonNull String titleForCause,
260             @NonNull String initialMessage) {
261         UnexpectedInvocation error = new UnexpectedInvocation(initialMessage);
262         setErrorAsInvocationCause(titleForCause, error);
263         return error;
264     }
265 
266     private void setErrorAsInvocationCause(@NonNull String titleForCause, @NonNull Throwable error) {
267         if (invocationCause != null) {
268             invocationCause.defineCause(titleForCause, error);
269         }
270     }
271 
272     @NonNull
273     public MissingInvocation errorForMissingInvocation(@NonNull List<ExpectedInvocation> nonMatchingInvocations) {
274         StringBuilder errorMessage = new StringBuilder(200);
275         errorMessage.append("Missing invocation to:\n").append(this);
276         appendNonMatchingInvocations(errorMessage, nonMatchingInvocations);
277 
278         return newMissingInvocationWithCause("Missing invocation", errorMessage.toString());
279     }
280 
281     @NonNull
282     public MissingInvocation errorForMissingInvocations(@NonNegative int missingInvocations,
283             @NonNull List<ExpectedInvocation> nonMatchingInvocations) {
284         StringBuilder errorMessage = new StringBuilder(200);
285         errorMessage.append("Missing ").append(missingInvocations).append(invocationsTo(missingInvocations))
286                 .append(this);
287         appendNonMatchingInvocations(errorMessage, nonMatchingInvocations);
288 
289         return newMissingInvocationWithCause("Missing invocations", errorMessage.toString());
290     }
291 
292     private void appendNonMatchingInvocations(@NonNull StringBuilder errorMessage,
293             @NonNull List<ExpectedInvocation> nonMatchingInvocations) {
294         if (!nonMatchingInvocations.isEmpty()) {
295             errorMessage.append("\ninstead got:\n");
296             String sep = "";
297 
298             for (ExpectedInvocation nonMatchingInvocation : nonMatchingInvocations) {
299                 String invocationDescription = nonMatchingInvocation.toString(instance);
300                 errorMessage.append(sep).append(invocationDescription);
301                 sep = "\n";
302                 nonMatchingInvocation.printCause(errorMessage);
303             }
304         }
305     }
306 
307     @NonNull
308     private MissingInvocation newMissingInvocationWithCause(@NonNull String titleForCause,
309             @NonNull String initialMessage) {
310         MissingInvocation error = new MissingInvocation(initialMessage);
311         setErrorAsInvocationCause(titleForCause, error);
312         return error;
313     }
314 
315     @NonNull
316     private static String invocationsTo(@NonNegative int invocations) {
317         return invocations == 1 ? " invocation to:\n" : " invocations to:\n";
318     }
319 
320     @NonNull
321     public UnexpectedInvocation errorForUnexpectedInvocation(@NonNull Object[] replayArgs) {
322         String message = "Unexpected invocation to:\n" + toString(replayArgs);
323         return newUnexpectedInvocationWithCause("Unexpected invocation", message);
324     }
325 
326     @NonNull
327     public UnexpectedInvocation errorForUnexpectedInvocations(@NonNull Object[] replayArgs, int numUnexpected) {
328         String message = numUnexpected + " unexpected" + invocationsTo(numUnexpected) + toString(replayArgs);
329         String titleForCause = numUnexpected == 1 ? "Unexpected invocation" : "Unexpected invocations";
330         return newUnexpectedInvocationWithCause(titleForCause, message);
331     }
332 
333     @NonNull
334     @Override
335     public String toString() {
336         return toString((Object) null);
337     }
338 
339     @NonNull
340     public String toString(@Nullable Object otherInstance) {
341         StringBuilder desc = new StringBuilder().append(arguments.toString());
342 
343         if (instance != otherInstance && instance != null) {
344             desc.append("\n   on mock instance: ").append(ObjectMethods.objectIdentity(instance));
345         }
346 
347         return desc.toString();
348     }
349 
350     @NonNull
351     String toString(@NonNull Object[] actualInvocationArguments) {
352         Object[] invocationArgs = arguments.getValues();
353         List<ArgumentMatcher<?>> matchers = arguments.getMatchers();
354         arguments.setValues(actualInvocationArguments);
355         arguments.setMatchers(null);
356         String description = toString();
357         arguments.setMatchers(matchers);
358         arguments.setValues(invocationArgs);
359         return description;
360     }
361 
362     private void printCause(@NonNull Appendable errorMessage) {
363         if (invocationCause != null) {
364             try {
365                 errorMessage.append('\n');
366             } catch (IOException ignore) {
367             }
368 
369             StackTrace st = new StackTrace(invocationCause);
370             st.filter();
371             st.print(errorMessage);
372         }
373     }
374 
375     // Default result
376     // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
377 
378     @Nullable
379     public Object getDefaultValueForReturnType() {
380         if (defaultReturnValue == UNDEFINED_DEFAULT_RETURN) {
381             Class<?> resolvedReturnType = getReturnTypeAsResolvedFromClassArgument();
382 
383             if (resolvedReturnType != null) {
384                 defaultReturnValue = DefaultValues.computeForType(resolvedReturnType);
385 
386                 if (defaultReturnValue == null) {
387                     String returnTypeDesc = 'L' + resolvedReturnType.getName().replace('.', '/') + ';';
388                     String mockedTypeDesc = getClassDesc();
389                     defaultReturnValue = MockedTypeCascade.getMock(mockedTypeDesc, arguments.methodNameAndDesc,
390                             instance, returnTypeDesc, resolvedReturnType);
391                 }
392 
393                 return defaultReturnValue;
394             }
395 
396             String returnTypeDesc = DefaultValues.getReturnTypeDesc(arguments.methodNameAndDesc);
397 
398             if ("V".equals(returnTypeDesc)) {
399                 return null;
400             }
401 
402             defaultReturnValue = DefaultValues.computeForType(returnTypeDesc);
403 
404             if (defaultReturnValue == null) {
405                 String mockedTypeDesc = getClassDesc();
406                 defaultReturnValue = MockedTypeCascade.getMock(mockedTypeDesc, arguments.methodNameAndDesc, instance,
407                         returnTypeDesc, arguments.genericSignature);
408             }
409         }
410 
411         return defaultReturnValue;
412     }
413 
414     @Nullable
415     private Class<?> getReturnTypeAsResolvedFromClassArgument() {
416         String genericSignature = arguments.genericSignature;
417 
418         if (genericSignature != null) {
419             int returnTypePos = genericSignature.lastIndexOf(')') + 1;
420             char c = genericSignature.charAt(returnTypePos);
421 
422             if (c == 'T') {
423                 for (Object arg : arguments.getValues()) {
424                     if (arg instanceof Class<?>) {
425                         return (Class<?>) arg;
426                     }
427                 }
428             }
429         }
430 
431         return null;
432     }
433 
434     public void copyDefaultReturnValue(@NonNull ExpectedInvocation other) {
435         defaultReturnValue = other.defaultReturnValue;
436     }
437 }