All Downloads are FREE. Search and download functionalities are using the official Maven repository.

mockit.internal.expectations.RecordAndReplayExecution Maven / Gradle / Ivy

Go to download

JMockit is a Java toolkit for automated developer testing. It contains mocking/faking APIs and a code coverage tool, supporting both JUnit and TestNG. The mocking APIs allow all kinds of Java code, without testability restrictions, to be tested in isolation from selected dependencies.

There is a newer version: 1.49
Show newest version
/*
 * Copyright (c) 2006 JMockit developers
 * This file is subject to the terms of the MIT license (see LICENSE.txt).
 */
package mockit.internal.expectations;

import java.util.*;
import java.util.concurrent.locks.*;

import javax.annotation.*;

import static java.lang.reflect.Modifier.isAbstract;

import mockit.*;
import mockit.internal.expectations.invocation.*;
import mockit.internal.expectations.mocking.*;
import mockit.internal.expectations.state.*;
import mockit.internal.reflection.*;
import mockit.internal.state.*;
import mockit.internal.util.*;
import static mockit.internal.util.GeneratedClasses.*;
import static mockit.internal.util.Utilities.*;

@SuppressWarnings("OverlyCoupledClass")
public final class RecordAndReplayExecution
{
   public static final ReentrantLock RECORD_OR_REPLAY_LOCK = new ReentrantLock();
   public static final ReentrantLock TEST_ONLY_PHASE_LOCK = new ReentrantLock();

   @Nullable private final DynamicPartialMocking dynamicPartialMocking;
   @Nonnull final PhasedExecutionState executionState;
   @Nonnull private final FailureState failureState;
   @Nullable private RecordPhase recordPhase;
   @Nullable private ReplayPhase replayPhase;
   @Nullable private BaseVerificationPhase verificationPhase;

   public RecordAndReplayExecution() {
      executionState = new PhasedExecutionState();
      dynamicPartialMocking = null;
      discoverMockedTypesAndInstancesForMatchingOnInstance();
      failureState = new FailureState();
      replayPhase = new ReplayPhase(this);
   }

   public RecordAndReplayExecution(@Nonnull Expectations targetObject, @Nullable Object... classesOrInstancesToBePartiallyMocked) {
      TestRun.enterNoMockingZone();
      ExecutingTest executingTest = TestRun.getExecutingTest();
      executingTest.setShouldIgnoreMockingCallbacks(true);

      try {
         RecordAndReplayExecution previous = executingTest.getPreviousRecordAndReplay();

         executionState = previous == null ? new PhasedExecutionState() : previous.executionState;
         failureState = new FailureState();
         recordPhase = new RecordPhase(this);

         executingTest.setRecordAndReplay(this);
         dynamicPartialMocking = applyDynamicPartialMocking(classesOrInstancesToBePartiallyMocked);
         discoverMockedTypesAndInstancesForMatchingOnInstance();

         //noinspection LockAcquiredButNotSafelyReleased
         TEST_ONLY_PHASE_LOCK.lock();
      }
      catch (RuntimeException e) {
         executingTest.setRecordAndReplay(null);
         throw e;
      }
      finally {
         executingTest.setShouldIgnoreMockingCallbacks(false);
         TestRun.exitNoMockingZone();
      }
   }

   private void discoverMockedTypesAndInstancesForMatchingOnInstance() {
      TypeRedefinitions fieldTypeRedefinitions = TestRun.getFieldTypeRedefinitions();

      if (fieldTypeRedefinitions != null) {
         List> fields = fieldTypeRedefinitions.getTargetClasses();
         List> targetClasses = new ArrayList<>(fields);

         TypeRedefinitions paramTypeRedefinitions = TestRun.getExecutingTest().getParameterRedefinitions();

         if (paramTypeRedefinitions != null) {
            targetClasses.addAll(paramTypeRedefinitions.getTargetClasses());
         }

         executionState.discoverMockedTypesToMatchOnInstances(targetClasses);

         if (dynamicPartialMocking != null && !dynamicPartialMocking.targetInstances.isEmpty()) {
            executionState.setDynamicMockInstancesToMatch(dynamicPartialMocking.targetInstances);
         }
      }
   }

   @Nullable
   private static DynamicPartialMocking applyDynamicPartialMocking(@Nullable Object... classesOrInstances) {
      if (classesOrInstances == null || classesOrInstances.length == 0) {
         return null;
      }

      DynamicPartialMocking mocking = new DynamicPartialMocking();
      mocking.redefineTypes(classesOrInstances);
      return mocking;
   }

   @Nullable
   public RecordPhase getRecordPhase() { return recordPhase; }

   @Nullable Error getErrorThrown() { return failureState.getErrorThrown(); }
   void setErrorThrown(@Nullable Error error) { failureState.setErrorThrown(error); }

   /**
    * Only to be called from generated bytecode or from the Mocking Bridge.
    */
   @Nullable @SuppressWarnings("OverlyComplexMethod")
   public static Object recordOrReplay(
      @Nullable Object mock, int mockAccess, @Nonnull String classDesc, @Nonnull String mockDesc, @Nullable String genericSignature,
      int executionModeOrdinal, @Nullable Object[] args
   ) throws Throwable {
      if (calledFromSpecialThread()) {
         return proceedIntoRealImplementationOrGetDefaultReturnType(mock, mockAccess, mockDesc, genericSignature);
      }

      if (args == null) {
         //noinspection AssignmentToMethodParameter
         args = NO_ARGS;
      }

      ExecutionMode executionMode = ExecutionMode.values()[executionModeOrdinal];

      if (
         RECORD_OR_REPLAY_LOCK.isHeldByCurrentThread() ||
         TEST_ONLY_PHASE_LOCK.isLocked() && !TEST_ONLY_PHASE_LOCK.isHeldByCurrentThread() ||
         !TestRun.mockFixture().isStillMocked(mock, classDesc)
      ) {
         // This occurs if called from a custom argument matching method, in a call to an overridden Object method (equals, hashCode,
         // toString), from a different thread during recording/verification, or during replay but between tests.
         return defaultReturnValue(mock, classDesc, mockDesc, genericSignature, executionMode, args);
      }

      ExecutingTest executingTest = TestRun.getExecutingTest();

      if (executingTest.isShouldIgnoreMockingCallbacks()) {
         // This occurs when called from a reentrant delegate method, or during static initialization of a mocked class.
         return defaultReturnValue(executingTest, mock, classDesc, mockDesc, genericSignature, executionMode, args);
      }

      if (executingTest.shouldProceedIntoRealImplementation(mock, classDesc) || executionMode.isToExecuteRealImplementation(mock)) {
         return Void.class;
      }

      RECORD_OR_REPLAY_LOCK.lock();

      try {
         boolean isConstructor = mock != null && mockDesc.startsWith("");
         RecordAndReplayExecution instance = executingTest.getOrCreateRecordAndReplay();

         if (isConstructor && handleCallToConstructor(instance, mock, classDesc)) {
            return
               executionMode == ExecutionMode.Regular ||
               executionMode == ExecutionMode.Partial && instance.replayPhase == null ||
               executingTest.isInjectableMock(mock) ?
                  null : Void.class;
         }

         Phase currentPhase = instance.getCurrentPhase();
         instance.failureState.clearErrorThrown();

         boolean withRealImpl = executionMode.isWithRealImplementation(mock);
         Object result = currentPhase.handleInvocation(mock, mockAccess, classDesc, mockDesc, genericSignature, withRealImpl, args);

         instance.failureState.reportErrorThrownIfAny();

         return result;
      }
      finally {
         RECORD_OR_REPLAY_LOCK.unlock();
      }
   }

   @Nullable
   private static Object proceedIntoRealImplementationOrGetDefaultReturnType(
      @Nullable Object mock, int mockAccess, @Nonnull String mockDesc, @Nullable String genericSignature
   ) {
      if (mock != null) {
         Class mockedClass = mock.getClass();
         String mockedClassName = mockedClass.getName();

         if (isGeneratedImplementationClass(mockedClassName) || isAbstract(mockAccess) && isGeneratedSubclass(mockedClassName)) {
            if (genericSignature != null) {
               GenericTypeReflection typeReflection = new GenericTypeReflection(mockedClass, null);
               String typeDesc = typeReflection.resolveReturnType(genericSignature);
               return DefaultValues.computeForType(typeDesc);
            }

            return DefaultValues.computeForReturnType(mockDesc);
         }
      }

      return Void.class;
   }

   @Nonnull
   private static Object defaultReturnValue(
      @Nullable Object mock, @Nonnull String classDesc, @Nonnull String nameAndDesc, @Nullable String genericSignature,
      @Nonnull ExecutionMode executionMode, @Nonnull Object[] args
   ) {
      if (executionMode.isToExecuteRealImplementation(mock)) {
         return Void.class;
      }

      if (mock != null) {
         Object rv = ObjectMethods.evaluateOverride(mock, nameAndDesc, args);

         if (rv != null) {
            return executionMode.isToExecuteRealObjectOverride(mock) ? Void.class : rv;
         }
      }

      String returnTypeDesc = DefaultValues.getReturnTypeDesc(nameAndDesc);

      if (returnTypeDesc.charAt(0) == 'L') {
         ExpectedInvocation invocation = new ExpectedInvocation(mock, classDesc, nameAndDesc, genericSignature, args);
         Object cascadedInstance = invocation.getDefaultValueForReturnType();

         if (cascadedInstance != null) {
            return cascadedInstance;
         }
      }

      return Void.class;
   }

   @Nullable
   private static Object defaultReturnValue(
      @Nonnull ExecutingTest executingTest, @Nullable Object mock, @Nonnull String classDesc, @Nonnull String nameAndDesc,
      @Nullable String genericSignature, @Nonnull ExecutionMode executionMode, @Nonnull Object[] args
   ) throws Throwable {
      RecordAndReplayExecution execution = executingTest.getCurrentRecordAndReplay();

      if (execution != null) {
         Expectation recordedExpectation = execution.executionState.findExpectation(mock, classDesc, nameAndDesc, args);

         if (recordedExpectation != null) {
            return recordedExpectation.produceResult(mock, args);
         }
      }

      return defaultReturnValue(mock, classDesc, nameAndDesc, genericSignature, executionMode, args);
   }

   private static boolean handleCallToConstructor(
      @Nonnull RecordAndReplayExecution instance, @Nonnull Object mock, @Nonnull String classDesc
   ) {
      if (instance.replayPhase != null) {
         TypeRedefinitions paramTypeRedefinitions = TestRun.getExecutingTest().getParameterRedefinitions();

         if (paramTypeRedefinitions != null) {
            CaptureOfNewInstances paramTypeCaptures = paramTypeRedefinitions.getCaptureOfNewInstances();

            if (paramTypeCaptures != null && paramTypeCaptures.captureNewInstance(mock)) {
               return true;
            }
         }

         FieldTypeRedefinitions fieldTypeRedefinitions = TestRun.getFieldTypeRedefinitions();

         if (fieldTypeRedefinitions != null && fieldTypeRedefinitions.captureNewInstanceForApplicableMockField(mock)) {
            return true;
         }
      }

      return isCallToSuperClassConstructor(mock, classDesc);
   }

   private static boolean isCallToSuperClassConstructor(@Nonnull Object mock, @Nonnull String calledClassDesc) {
      Class mockedClass = mock.getClass();

      if (ClassNaming.isAnonymousClass(mockedClass)) {
         // An anonymous class instantiation always invokes the constructor on the super-class,
         // so that is the class we need to consider, not the anonymous one.
         mockedClass = mockedClass.getSuperclass();

         if (mockedClass == Object.class) {
            return false;
         }
      }

      String calledClassName = calledClassDesc.replace('/', '.');

      return !calledClassName.equals(mockedClass.getName());
   }

   @Nonnull
   private Phase getCurrentPhase() {
      ReplayPhase replay = replayPhase;

      if (replay == null) {
         RecordPhase record = recordPhase;
         assert record != null;
         return record;
      }

      BaseVerificationPhase verification = verificationPhase;

      if (verification != null) {
         return verification;
      }

      return replay;
   }

   @Nonnull
   public BaseVerificationPhase startVerifications(boolean inOrder) {
      assert replayPhase != null;
      List expectations = replayPhase.invocations;
      List invocationInstances = replayPhase.invocationInstances;
      List invocationArguments = replayPhase.invocationArguments;

      verificationPhase = inOrder ?
         new OrderedVerificationPhase(this, expectations, invocationInstances, invocationArguments) :
         new UnorderedVerificationPhase(this, expectations, invocationInstances, invocationArguments);

      return verificationPhase;
   }

   @Nullable
   public static Error endCurrentReplayIfAny() {
      RecordAndReplayExecution instance = TestRun.getRecordAndReplayForRunningTest();
      return instance == null ? null : instance.endExecution();
   }

   @Nullable
   private Error endExecution() {
      if (TEST_ONLY_PHASE_LOCK.isLocked()) {
         TEST_ONLY_PHASE_LOCK.unlock();
      }

      ReplayPhase replay = switchFromRecordToReplayIfNotYet();
      Error error = replay.endExecution();

      if (error == null) {
         error = failureState.getErrorThrownInAnotherThreadIfAny();
      }

      if (error == null && verificationPhase != null) {
         error = verificationPhase.endVerification();
         verificationPhase = null;
      }

      return error;
   }

   @Nonnull
   private ReplayPhase switchFromRecordToReplayIfNotYet() {
      if (replayPhase == null) {
         recordPhase = null;
         replayPhase = new ReplayPhase(this);
      }

      return replayPhase;
   }

   @Nullable
   public TestOnlyPhase getCurrentTestOnlyPhase() {
      if (recordPhase != null) {
         return recordPhase;
      }

      return verificationPhase;
   }

   void endInvocations() {
      TEST_ONLY_PHASE_LOCK.unlock();

      if (verificationPhase == null) {
         switchFromRecordToReplayIfNotYet();
      }
      else {
         Error error = verificationPhase.endVerification();
         verificationPhase = null;

         if (error != null) {
            throw error;
         }
      }
   }
}