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