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