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 static mockit.internal.util.Utilities.NO_ARGS;
9   
10  import edu.umd.cs.findbugs.annotations.NonNull;
11  import edu.umd.cs.findbugs.annotations.Nullable;
12  
13  import java.util.ArrayList;
14  import java.util.List;
15  import java.util.concurrent.locks.ReentrantLock;
16  
17  import mockit.Expectations;
18  import mockit.internal.expectations.invocation.ExpectedInvocation;
19  import mockit.internal.expectations.mocking.CaptureOfNewInstances;
20  import mockit.internal.expectations.mocking.FieldTypeRedefinitions;
21  import mockit.internal.expectations.mocking.PartialMocking;
22  import mockit.internal.expectations.mocking.TypeRedefinitions;
23  import mockit.internal.expectations.state.ExecutingTest;
24  import mockit.internal.state.TestRun;
25  import mockit.internal.util.ClassNaming;
26  import mockit.internal.util.DefaultValues;
27  import mockit.internal.util.ObjectMethods;
28  
29  public final class RecordAndReplayExecution {
30      public static final ReentrantLock RECORD_OR_REPLAY_LOCK = new ReentrantLock();
31      public static final ReentrantLock TEST_ONLY_PHASE_LOCK = new ReentrantLock();
32  
33      @Nullable
34      private final PartialMocking partialMocking;
35      @NonNull
36      private final PhasedExecutionState executionState;
37      @NonNull
38      private final FailureState failureState;
39      @Nullable
40      private RecordPhase recordPhase;
41      @Nullable
42      private ReplayPhase replayPhase;
43      @Nullable
44      private BaseVerificationPhase verificationPhase;
45  
46      public RecordAndReplayExecution() {
47          executionState = new PhasedExecutionState();
48          partialMocking = null;
49          discoverMockedTypesAndInstancesForMatchingOnInstance();
50          failureState = new FailureState();
51          replayPhase = new ReplayPhase(executionState, failureState);
52      }
53  
54      public RecordAndReplayExecution(@NonNull Expectations targetObject,
55              @Nullable Object... instancesToBePartiallyMocked) {
56          TestRun.enterNoMockingZone();
57          ExecutingTest executingTest = TestRun.getExecutingTest();
58          executingTest.setShouldIgnoreMockingCallbacks(true);
59  
60          try {
61              RecordAndReplayExecution previous = executingTest.getPreviousRecordAndReplay();
62  
63              executionState = previous == null ? new PhasedExecutionState() : previous.executionState;
64              failureState = new FailureState();
65              recordPhase = new RecordPhase(executionState);
66  
67              executingTest.setRecordAndReplay(this);
68              partialMocking = applyPartialMocking(instancesToBePartiallyMocked);
69              discoverMockedTypesAndInstancesForMatchingOnInstance();
70  
71              // noinspection LockAcquiredButNotSafelyReleased
72              TEST_ONLY_PHASE_LOCK.lock();
73          } catch (RuntimeException e) {
74              executingTest.setRecordAndReplay(null);
75              throw e;
76          } finally {
77              executingTest.setShouldIgnoreMockingCallbacks(false);
78              TestRun.exitNoMockingZone();
79          }
80      }
81  
82      private void discoverMockedTypesAndInstancesForMatchingOnInstance() {
83          TypeRedefinitions fieldTypeRedefinitions = TestRun.getFieldTypeRedefinitions();
84  
85          if (fieldTypeRedefinitions != null) {
86              List<Class<?>> fields = fieldTypeRedefinitions.getTargetClasses();
87              List<Class<?>> targetClasses = new ArrayList<>(fields);
88  
89              TypeRedefinitions paramTypeRedefinitions = TestRun.getExecutingTest().getParameterRedefinitions();
90  
91              if (paramTypeRedefinitions != null) {
92                  targetClasses.addAll(paramTypeRedefinitions.getTargetClasses());
93              }
94  
95              executionState.instanceBasedMatching.discoverMockedTypesToMatchOnInstances(targetClasses);
96  
97              if (partialMocking != null && !partialMocking.targetInstances.isEmpty()) {
98                  executionState.partiallyMockedInstances = new PartiallyMockedInstances(partialMocking.targetInstances);
99              }
100         }
101     }
102 
103     @Nullable
104     private static PartialMocking applyPartialMocking(@Nullable Object... instances) {
105         if (instances == null || instances.length == 0) {
106             return null;
107         }
108 
109         PartialMocking mocking = new PartialMocking();
110         mocking.redefineTypes(instances);
111         return mocking;
112     }
113 
114     @Nullable
115     public RecordPhase getRecordPhase() {
116         return recordPhase;
117     }
118 
119     /**
120      * Only to be called from generated bytecode or from the Mocking Bridge.
121      */
122     @Nullable
123     public static Object recordOrReplay(@Nullable Object mock, int mockAccess, @NonNull String classDesc,
124             @NonNull String mockDesc, @Nullable String genericSignature, int executionModeOrdinal,
125             @Nullable Object[] args) throws Throwable {
126         @NonNull
127         Object[] mockArgs = args == null ? NO_ARGS : args;
128         ExecutionMode executionMode = ExecutionMode.values()[executionModeOrdinal];
129 
130         if (notToBeMocked(mock, classDesc)) {
131             // This occurs if called from a custom argument matching method, in a call to an overridden Object method
132             // (equals, hashCode,
133             // toString), from a different thread during recording/verification, or during replay but between tests.
134             return defaultReturnValue(mock, classDesc, mockDesc, genericSignature, executionMode, mockArgs);
135         }
136 
137         ExecutingTest executingTest = TestRun.getExecutingTest();
138 
139         if (executingTest.isShouldIgnoreMockingCallbacks()) {
140             // This occurs when called from a reentrant delegate method, or during static initialization of a mocked
141             // class.
142             return defaultReturnValue(executingTest, mock, classDesc, mockDesc, genericSignature, executionMode,
143                     mockArgs);
144         }
145 
146         if (executingTest.shouldProceedIntoRealImplementation(mock, classDesc)
147                 || executionMode.isToExecuteRealImplementation(mock)) {
148             return Void.class;
149         }
150 
151         boolean isConstructor = mock != null && mockDesc.startsWith("<init>");
152         RECORD_OR_REPLAY_LOCK.lock();
153 
154         try {
155             RecordAndReplayExecution instance = executingTest.getOrCreateRecordAndReplay();
156 
157             if (isConstructor && instance.handleCallToConstructor(mock, classDesc)) {
158                 return instance.getResultForConstructor(mock, executionMode);
159             }
160 
161             return instance.getResult(mock, mockAccess, classDesc, mockDesc, genericSignature, executionMode, mockArgs);
162         } finally {
163             RECORD_OR_REPLAY_LOCK.unlock();
164         }
165     }
166 
167     private static boolean notToBeMocked(@Nullable Object mock, @NonNull String classDesc) {
168         return RECORD_OR_REPLAY_LOCK.isHeldByCurrentThread()
169                 || TEST_ONLY_PHASE_LOCK.isLocked() && !TEST_ONLY_PHASE_LOCK.isHeldByCurrentThread()
170                 || !TestRun.mockFixture().isStillMocked(mock, classDesc);
171     }
172 
173     @NonNull
174     private static Object defaultReturnValue(@Nullable Object mock, @NonNull String classDesc,
175             @NonNull String nameAndDesc, @Nullable String genericSignature, @NonNull ExecutionMode executionMode,
176             @NonNull Object[] args) {
177         if (executionMode.isToExecuteRealImplementation(mock)) {
178             return Void.class;
179         }
180 
181         if (mock != null) {
182             Object rv = ObjectMethods.evaluateOverride(mock, nameAndDesc, args);
183 
184             if (rv != null) {
185                 return executionMode.isToExecuteRealObjectOverride(mock) ? Void.class : rv;
186             }
187         }
188 
189         String returnTypeDesc = DefaultValues.getReturnTypeDesc(nameAndDesc);
190 
191         if (returnTypeDesc.charAt(0) == 'L') {
192             ExpectedInvocation invocation = new ExpectedInvocation(mock, classDesc, nameAndDesc, genericSignature,
193                     args);
194             Object cascadedInstance = invocation.getDefaultValueForReturnType();
195 
196             if (cascadedInstance != null) {
197                 return cascadedInstance;
198             }
199         }
200 
201         return Void.class;
202     }
203 
204     @Nullable
205     private static Object defaultReturnValue(@NonNull ExecutingTest executingTest, @Nullable Object mock,
206             @NonNull String classDesc, @NonNull String nameAndDesc, @Nullable String genericSignature,
207             @NonNull ExecutionMode executionMode, @NonNull Object[] args) throws Throwable {
208         RecordAndReplayExecution execution = executingTest.getCurrentRecordAndReplay();
209 
210         if (execution != null) {
211             Expectation recordedExpectation = execution.executionState.findExpectation(mock, classDesc, nameAndDesc,
212                     args);
213 
214             if (recordedExpectation != null) {
215                 return recordedExpectation.produceResult(mock, args);
216             }
217         }
218 
219         return defaultReturnValue(mock, classDesc, nameAndDesc, genericSignature, executionMode, args);
220     }
221 
222     private boolean handleCallToConstructor(@NonNull Object mock, @NonNull String classDesc) {
223         if (replayPhase != null) {
224             TypeRedefinitions paramTypeRedefinitions = TestRun.getExecutingTest().getParameterRedefinitions();
225 
226             if (paramTypeRedefinitions != null) {
227                 CaptureOfNewInstances paramTypeCaptures = paramTypeRedefinitions.getCaptureOfNewInstances();
228 
229                 if (paramTypeCaptures != null && paramTypeCaptures.captureNewInstance(null, mock)) {
230                     return true;
231                 }
232             }
233 
234             FieldTypeRedefinitions fieldTypeRedefinitions = TestRun.getFieldTypeRedefinitions();
235 
236             if (fieldTypeRedefinitions != null
237                     && fieldTypeRedefinitions.captureNewInstanceForApplicableMockField(mock)) {
238                 return true;
239             }
240         }
241 
242         return isCallToSuperClassConstructor(mock, classDesc);
243     }
244 
245     private static boolean isCallToSuperClassConstructor(@NonNull Object mock, @NonNull String calledClassDesc) {
246         Class<?> mockedClass = mock.getClass();
247 
248         if (ClassNaming.isAnonymousClass(mockedClass)) {
249             // An anonymous class instantiation always invokes the constructor on the super-class,
250             // so that is the class we need to consider, not the anonymous one.
251             mockedClass = mockedClass.getSuperclass();
252 
253             if (mockedClass == Object.class) {
254                 return false;
255             }
256         }
257 
258         String calledClassName = calledClassDesc.replace('/', '.');
259 
260         return !calledClassName.equals(mockedClass.getName());
261     }
262 
263     @Nullable
264     private Object getResultForConstructor(@NonNull Object mock, @NonNull ExecutionMode executionMode) {
265         return executionMode == ExecutionMode.Regular || executionMode == ExecutionMode.Partial && replayPhase == null
266                 || TestRun.getExecutingTest().isInjectableMock(mock) ? null : Void.class;
267     }
268 
269     @Nullable
270     private Object getResult(@Nullable Object mock, int mockAccess, @NonNull String classDesc, @NonNull String mockDesc,
271             @Nullable String genericSignature, @NonNull ExecutionMode executionMode, @NonNull Object[] args)
272             throws Throwable {
273         Phase currentPhase = getCurrentPhase();
274         failureState.clearErrorThrown();
275 
276         boolean withRealImpl = executionMode.isWithRealImplementation(mock);
277         Object result = currentPhase.handleInvocation(mock, mockAccess, classDesc, mockDesc, genericSignature,
278                 withRealImpl, args);
279 
280         failureState.reportErrorThrownIfAny();
281         return result;
282     }
283 
284     @NonNull
285     private Phase getCurrentPhase() {
286         ReplayPhase replay = replayPhase;
287 
288         if (replay == null) {
289             RecordPhase recordPhaseLocal = recordPhase;
290             assert recordPhaseLocal != null;
291             return recordPhaseLocal;
292         }
293 
294         BaseVerificationPhase verification = verificationPhase;
295 
296         if (verification != null) {
297             return verification;
298         }
299 
300         return replay;
301     }
302 
303     @NonNull
304     public BaseVerificationPhase startVerifications(boolean inOrder,
305             @Nullable Object[] mockedTypesAndInstancesToVerify) {
306         assert replayPhase != null;
307 
308         if (inOrder) {
309             verificationPhase = new OrderedVerificationPhase(replayPhase);
310         } else if (mockedTypesAndInstancesToVerify == null) {
311             verificationPhase = new UnorderedVerificationPhase(replayPhase);
312         } else {
313             verificationPhase = new FullVerificationPhase(replayPhase, mockedTypesAndInstancesToVerify);
314         }
315 
316         return verificationPhase;
317     }
318 
319     @Nullable
320     public static Error endCurrentReplayIfAny() {
321         RecordAndReplayExecution instance = TestRun.getRecordAndReplayForRunningTest();
322         return instance == null ? null : instance.endExecution();
323     }
324 
325     @Nullable
326     private Error endExecution() {
327         if (TEST_ONLY_PHASE_LOCK.isLocked()) {
328             TEST_ONLY_PHASE_LOCK.unlock();
329         }
330 
331         ReplayPhase replay = switchFromRecordToReplayIfNotYet();
332         Error error = replay.endExecution();
333 
334         if (error == null) {
335             error = failureState.getErrorThrownInAnotherThreadIfAny();
336         }
337 
338         if (error == null && verificationPhase != null) {
339             error = verificationPhase.endVerification();
340             verificationPhase = null;
341         }
342 
343         return error;
344     }
345 
346     @NonNull
347     private ReplayPhase switchFromRecordToReplayIfNotYet() {
348         if (replayPhase == null) {
349             recordPhase = null;
350             replayPhase = new ReplayPhase(executionState, failureState);
351         }
352 
353         return replayPhase;
354     }
355 
356     @Nullable
357     TestOnlyPhase getCurrentTestOnlyPhase() {
358         return recordPhase != null ? recordPhase : verificationPhase;
359     }
360 
361     void endInvocations() {
362         TEST_ONLY_PHASE_LOCK.unlock();
363 
364         if (verificationPhase == null) {
365             switchFromRecordToReplayIfNotYet();
366         } else {
367             Error error = verificationPhase.endVerification();
368             verificationPhase = null;
369 
370             if (error != null) {
371                 throw error;
372             }
373         }
374     }
375 }