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;
6   
7   import edu.umd.cs.findbugs.annotations.NonNull;
8   import edu.umd.cs.findbugs.annotations.Nullable;
9   
10  import java.util.ArrayList;
11  import java.util.List;
12  
13  import mockit.internal.expectations.invocation.ExpectedInvocation;
14  import mockit.internal.expectations.invocation.InvocationArguments;
15  import mockit.internal.state.TestRun;
16  import mockit.internal.util.GeneratedClasses;
17  
18  final class PhasedExecutionState {
19      @NonNull
20      final List<Expectation> expectations;
21      @NonNull
22      final List<VerifiedExpectation> verifiedExpectations;
23      @NonNull
24      final EquivalentInstances equivalentInstances;
25      @NonNull
26      final InstanceBasedMatching instanceBasedMatching;
27      @Nullable
28      PartiallyMockedInstances partiallyMockedInstances;
29  
30      PhasedExecutionState() {
31          expectations = new ArrayList<>();
32          verifiedExpectations = new ArrayList<>();
33          equivalentInstances = new EquivalentInstances();
34          instanceBasedMatching = new InstanceBasedMatching();
35      }
36  
37      void addExpectation(@NonNull Expectation expectation) {
38          ExpectedInvocation invocation = expectation.invocation;
39          forceMatchingOnMockInstanceIfRequired(invocation);
40          removeMatchingExpectationsCreatedBefore(invocation);
41          expectations.add(expectation);
42      }
43  
44      private void forceMatchingOnMockInstanceIfRequired(@NonNull ExpectedInvocation invocation) {
45          if (!invocation.matchInstance
46                  && isToBeMatchedOnInstance(invocation.instance, invocation.getMethodNameAndDescription())) {
47              invocation.matchInstance = true;
48          }
49      }
50  
51      boolean isToBeMatchedOnInstance(@Nullable Object mock, @NonNull String mockNameAndDesc) {
52          if (mock == null || mockNameAndDesc.charAt(0) == '<') {
53              return false;
54          }
55  
56          if (instanceBasedMatching.isToBeMatchedOnInstance(mock)
57                  || partiallyMockedInstances != null && partiallyMockedInstances.isToBeMatchedOnInstance(mock)) {
58              return true;
59          }
60  
61          return TestRun.getExecutingTest().isInjectableMock(mock);
62      }
63  
64      private void removeMatchingExpectationsCreatedBefore(@NonNull ExpectedInvocation invocation) {
65          Expectation previousExpectation = findPreviousExpectation(invocation);
66  
67          if (previousExpectation != null) {
68              expectations.remove(previousExpectation);
69              invocation.copyDefaultReturnValue(previousExpectation.invocation);
70          }
71      }
72  
73      @Nullable
74      private Expectation findPreviousExpectation(@NonNull ExpectedInvocation newInvocation) {
75          int n = expectations.size();
76  
77          if (n == 0) {
78              return null;
79          }
80  
81          Object mock = newInvocation.instance;
82          @NonNull
83          Boolean matchInstance = newInvocation.matchInstance;
84          String mockClassDesc = newInvocation.getClassDesc();
85          String mockNameAndDesc = newInvocation.getMethodNameAndDescription();
86          boolean isConstructor = newInvocation.isConstructor();
87  
88          for (Expectation previous : expectations) {
89              if (isMatchingInvocation(mock, matchInstance, mockClassDesc, mockNameAndDesc, isConstructor, previous)
90                      && isWithMatchingArguments(newInvocation, previous.invocation)) {
91                  return previous;
92              }
93          }
94  
95          return null;
96      }
97  
98      private boolean isMatchingInvocation(@Nullable Object mock, @Nullable Boolean matchInstance,
99              @NonNull String mockClassDesc, @NonNull String mockNameAndDesc, boolean constructorInvocation,
100             @NonNull Expectation expectation) {
101         ExpectedInvocation invocation = expectation.invocation;
102 
103         return invocation.isMatch(mock, mockClassDesc, mockNameAndDesc) && isSameMockedClass(mock, invocation.instance)
104                 && (constructorInvocation || mock == null || isMatchingInstance(mock, matchInstance, expectation));
105     }
106 
107     private static boolean isSameMockedClass(@Nullable Object mock1, @Nullable Object mock2) {
108         if (mock1 == mock2) {
109             return true;
110         }
111 
112         if (mock1 != null && mock2 != null) {
113             Class<?> mockedClass1 = mock1.getClass();
114             Class<?> mockedClass2 = GeneratedClasses.getMockedClass(mock2);
115             return mockedClass2.isAssignableFrom(mockedClass1)
116                     || TestRun.mockFixture().areCapturedClasses(mockedClass1, mockedClass2);
117         }
118 
119         return false;
120     }
121 
122     private boolean isWithMatchingArguments(@NonNull ExpectedInvocation newInvocation,
123             @NonNull ExpectedInvocation previousInvocation) {
124         InvocationArguments newArguments = newInvocation.arguments;
125         InvocationArguments previousArguments = previousInvocation.arguments;
126 
127         if (newArguments.getMatchers() == null) {
128             return previousArguments.isMatch(newArguments.getValues(), equivalentInstances.instanceMap);
129         }
130 
131         return newArguments.hasEquivalentMatchers(previousArguments);
132     }
133 
134     @Nullable
135     Expectation findExpectation(@Nullable Object mock, @NonNull String mockClassDesc, @NonNull String mockNameAndDesc,
136             @NonNull Object[] args) {
137         boolean isConstructor = mockNameAndDesc.charAt(0) == '<';
138         Expectation replayExpectationFound = null;
139 
140         // Note: new expectations might get added to the list, so a regular loop would cause a CME:
141         // noinspection ForLoopReplaceableByForEach
142         for (Expectation expectation : expectations) {
143             if (replayExpectationFound != null && expectation.recordPhase == null) {
144                 continue;
145             }
146 
147             if (isMatchingInvocation(mock, null, mockClassDesc, mockNameAndDesc, isConstructor, expectation)
148                     && expectation.invocation.arguments.isMatch(args, equivalentInstances.instanceMap)) {
149                 if (expectation.recordPhase == null) {
150                     replayExpectationFound = expectation;
151                     continue;
152                 }
153 
154                 if (isConstructor) {
155                     equivalentInstances.registerReplacementInstanceIfApplicable(mock, expectation.invocation);
156                 }
157 
158                 return expectation;
159             }
160         }
161 
162         return replayExpectationFound;
163     }
164 
165     private boolean isMatchingInstance(@NonNull Object invokedInstance, @Nullable Boolean matchInstance,
166             @NonNull Expectation expectation) {
167         ExpectedInvocation invocation = expectation.invocation;
168         Object invocationInstance = invocation.instance;
169         assert invocationInstance != null;
170 
171         if (equivalentInstances.isEquivalentInstance(invocationInstance, invokedInstance)) {
172             return true;
173         }
174 
175         if (TestRun.getExecutingTest().isInjectableMock(invokedInstance)
176                 || partiallyMockedInstances != null
177                         && partiallyMockedInstances.isDynamicMockInstanceOrClass(invokedInstance, invocationInstance)
178                 || equivalentInstances.areNonEquivalentInstances(invocationInstance, invokedInstance)) {
179             return false;
180         }
181 
182         return (matchInstance == null || !matchInstance) && !invocation.matchInstance && expectation.recordPhase != null
183                 && !equivalentInstances.replacementMap.containsValue(invocationInstance);
184     }
185 }