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

mockit.internal.expectations.invocation.ExpectedInvocation Maven / Gradle / Ivy

Go to download

JMockit is a Java toolkit for automated developer testing. It contains APIs for the creation of the objects to be tested, for mocking dependencies, and for faking external APIs; JUnit (4 & 5) and TestNG test runners are supported. It also contains an advanced code coverage tool.

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

import java.io.*;
import java.util.*;
import javax.annotation.*;

import mockit.external.asm.*;
import mockit.internal.expectations.argumentMatching.*;
import mockit.internal.expectations.state.*;
import mockit.internal.reflection.*;
import mockit.internal.state.*;
import mockit.internal.util.*;
import mockit.internal.reflection.GenericTypeReflection.*;
import static mockit.external.asm.JavaType.getType;
import static mockit.internal.util.TypeDescriptor.getClassForType;

public final class ExpectedInvocation
{
   @Nonnull private static final Object UNDEFINED_DEFAULT_RETURN = new Object();

   @Nullable public final Object instance;
   @Nullable public Object replacementInstance;
   public boolean matchInstance;
   @Nonnull public final InvocationArguments arguments;
   @Nullable private final ExpectationError invocationCause;
   @Nullable Object defaultReturnValue;

   public ExpectedInvocation(
      @Nullable Object mock, @Nonnull String mockedClassDesc, @Nonnull String mockNameAndDesc,
      @Nullable String genericSignature, @Nonnull Object[] args)
   {
      instance = mock;
      arguments = new InvocationArguments(0, mockedClassDesc, mockNameAndDesc, genericSignature, args);
      invocationCause = null;
      defaultReturnValue = determineDefaultReturnValueFromMethodSignature();
   }

   public ExpectedInvocation(
      @Nullable Object mock, int access, @Nonnull String mockedClassDesc, @Nonnull String mockNameAndDesc,
      boolean matchInstance, @Nullable String genericSignature, @Nonnull Object[] args)
   {
      instance = mock;
      this.matchInstance = matchInstance;
      arguments = new InvocationArguments(access, mockedClassDesc, mockNameAndDesc, genericSignature, args);
      invocationCause = new ExpectationError();
      defaultReturnValue = determineDefaultReturnValueFromMethodSignature();
   }

   @Nullable
   public AssertionError getInvocationCause() { return invocationCause; }

   @Nonnull
   private Object determineDefaultReturnValueFromMethodSignature()
   {
      if (instance != null) {
         Object rv = ObjectMethods.evaluateOverride(instance, getMethodNameAndDescription(), getArgumentValues());

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

      return UNDEFINED_DEFAULT_RETURN;
   }

   // Simple getters //////////////////////////////////////////////////////////////////////////////////////////////////

   @Nonnull public String getClassDesc() { return arguments.classDesc; }
   @Nonnull public String getClassName() { return arguments.getClassName(); }
   @Nonnull public String getMethodNameAndDescription() { return arguments.methodNameAndDesc; }
   @Nonnull public Object[] getArgumentValues() { return arguments.getValues(); }
   public boolean isConstructor() { return arguments.isForConstructor(); }

   @Nullable
   public Object getRecordedInstance() { return replacementInstance != null ? replacementInstance : instance; }

   @Nonnull
   public String getSignatureWithResolvedReturnType()
   {
      String signature = arguments.genericSignature;

      if (signature != null) {
         // TODO: cache it for use in return type conversion, cascading, etc.
         String classDesc = getClassDesc();
         Class mockedClass = instance != null ? instance.getClass() : ClassLoad.loadByInternalName(classDesc);
         GenericTypeReflection reflection = new GenericTypeReflection(mockedClass, null);
         signature = reflection.resolveSignature(classDesc, signature);

         char firstTypeChar = signature.charAt(signature.indexOf(')') + 1);

         if (firstTypeChar != 'T' && firstTypeChar != '[') {
            return signature;
         }
      }

      return arguments.methodNameAndDesc;
   }

   // Matching based on instance or mocked type ///////////////////////////////////////////////////////////////////////

   public boolean isMatch(@Nullable Object mock, @Nonnull String invokedClassDesc, @Nonnull String invokedMethod)
   {
      return
         (invokedClassDesc.equals(getClassDesc()) || mock != null && TestRun.mockFixture().isCaptured(mock)) &&
         (isMatchingGenericMethod(mock, invokedMethod) || isMatchingMethod(invokedMethod));
   }

   private boolean isMatchingGenericMethod(@Nullable Object mock, @Nonnull String invokedMethod)
   {
      if (mock != null && instance != null) {
         String genericSignature = arguments.genericSignature;

         if (genericSignature != null) {
            Class mockedClass = mock.getClass();

            if (mockedClass != instance.getClass()) {
               GenericTypeReflection typeReflection = new GenericTypeReflection(mockedClass, null);
               GenericSignature parsedSignature = typeReflection.parseSignature(genericSignature);
               return parsedSignature.satisfiesSignature(invokedMethod);
            }
         }
      }

      return false;
   }

   private boolean isMatchingMethod(@Nonnull String invokedMethod)
   {
      int returnTypeStartPos = getReturnTypePosition(invokedMethod);

      if (returnTypeStartPos < 0) {
         return false;
      }

      if (haveSameReturnTypes(invokedMethod, returnTypeStartPos)) {
         return true;
      }

      // At this point the methods are known to differ only in return type, so check if the return type of
      // the recorded one is assignable to the return type of the one invoked:
      return isReturnTypeOfRecordedMethodAssignableToReturnTypeOfInvokedMethod(invokedMethod, returnTypeStartPos);
   }

   // Returns -1 if the method names or parameters are different.
   private int getReturnTypePosition(@Nonnull String invokedMethod)
   {
      String recordedMethod = getMethodNameAndDescription();
      int i = 0;

      while (true) {
         char c = recordedMethod.charAt(i);

         if (c != invokedMethod.charAt(i)) {
            return -1;
         }

         i++;

         if (c == ')') {
            return i;
         }
      }
   }

   private boolean haveSameReturnTypes(@Nonnull String invokedMethod, @Nonnegative int returnTypeStartPos)
   {
      String recordedMethod = getMethodNameAndDescription();
      int n = invokedMethod.length();

      if (n != recordedMethod.length()) {
         return false;
      }

      int j = returnTypeStartPos;

      while (true) {
         char c = recordedMethod.charAt(j);

         if (c != invokedMethod.charAt(j)) {
            return false;
         }

         j++;

         if (j == n) {
            return true;
         }
      }
   }

   private boolean isReturnTypeOfRecordedMethodAssignableToReturnTypeOfInvokedMethod(
      @Nonnull String invokedMethod, @Nonnegative int returnTypeStartPos)
   {
      String recordedMethod = getMethodNameAndDescription();
      JavaType recordedRT = getType(recordedMethod.substring(returnTypeStartPos));
      JavaType invokedRT  = getType(invokedMethod.substring(returnTypeStartPos));

      return getClassForType(invokedRT).isAssignableFrom(getClassForType(recordedRT));
   }

   public boolean isMatch(@Nonnull ExpectedInvocation other)
   {
      return isMatch(other.instance, other.getClassDesc(), other.getMethodNameAndDescription(), null);
   }

   public boolean isMatch(
      @Nullable Object replayInstance, @Nonnull String invokedClassDesc, @Nonnull String invokedMethod,
      @Nullable Map replacementMap)
   {
      return
         isMatch(replayInstance, invokedClassDesc, invokedMethod) &&
         (arguments.isForConstructor() || !matchInstance || isEquivalentInstance(replayInstance, replacementMap));
   }

   private boolean isEquivalentInstance(@Nullable Object mockedInstance, @Nullable Map replacementMap)
   {
      return
         mockedInstance == instance ||
         mockedInstance != null && instance != null && (
            replacementMap != null && replacementMap.get(mockedInstance) == instance ||
            TestRun.getExecutingTest().isInvokedInstanceEquivalentToCapturedInstance(instance, mockedInstance)
         );
   }

   // Creation of Error instances for invocation mismatch reporting ///////////////////////////////////////////////////

   public ExpectedInvocation(
      @Nullable Object mockedInstance, @Nonnull String classDesc, @Nonnull String methodNameAndDesc,
      @Nonnull Object[] args)
   {
      instance = mockedInstance;
      matchInstance = false;
      arguments = new InvocationArguments(0, classDesc, methodNameAndDesc, null, args);
      invocationCause = null;
   }

   @Nonnull
   public UnexpectedInvocation errorForUnexpectedInvocation()
   {
      String initialMessage = "Unexpected invocation of " + this;
      return newUnexpectedInvocationWithCause("Unexpected invocation", initialMessage);
   }

   @Nonnull
   private UnexpectedInvocation newUnexpectedInvocationWithCause(
      @Nonnull String titleForCause, @Nonnull String initialMessage)
   {
      UnexpectedInvocation error = new UnexpectedInvocation(initialMessage);
      setErrorAsInvocationCause(titleForCause, error);
      return error;
   }

   private void setErrorAsInvocationCause(@Nonnull String titleForCause, @Nonnull Throwable error)
   {
      if (invocationCause != null) {
         invocationCause.defineCause(titleForCause, error);
      }
   }

   @Nonnull
   private MissingInvocation newMissingInvocationWithCause(
      @Nonnull String titleForCause, @Nonnull String initialMessage)
   {
      MissingInvocation error = new MissingInvocation(initialMessage);
      setErrorAsInvocationCause(titleForCause, error);
      return error;
   }

   @Nonnull
   public MissingInvocation errorForMissingInvocation(@Nonnull List nonMatchingInvocations)
   {
      StringBuilder errorMessage = new StringBuilder(200);
      errorMessage.append("Missing invocation to:\n").append(this);
      appendNonMatchingInvocations(errorMessage, nonMatchingInvocations);

      return newMissingInvocationWithCause("Missing invocation", errorMessage.toString());
   }

   @Nonnull
   public MissingInvocation errorForMissingInvocations(
      @Nonnegative int missingInvocations, @Nonnull List nonMatchingInvocations)
   {
      StringBuilder errorMessage = new StringBuilder(200);
      errorMessage.append("Missing ").append(missingInvocations).append(invocationsTo(missingInvocations)).append(this);
      appendNonMatchingInvocations(errorMessage, nonMatchingInvocations);

      return newMissingInvocationWithCause("Missing invocations", errorMessage.toString());
   }

   @Nonnull
   private static String invocationsTo(@Nonnegative int invocations)
   {
      return invocations == 1 ? " invocation to:\n" : " invocations to:\n";
   }

   private void appendNonMatchingInvocations(
      @Nonnull StringBuilder errorMessage, @Nonnull List nonMatchingInvocations)
   {
      if (!nonMatchingInvocations.isEmpty()) {
         errorMessage.append("\ninstead got:\n");
         String sep = "";

         for (ExpectedInvocation nonMatchingInvocation : nonMatchingInvocations) {
            String invocationDescription = nonMatchingInvocation.toString(instance);
            errorMessage.append(sep).append(invocationDescription);
            sep = "\n";
            nonMatchingInvocation.printCause(errorMessage);
         }
      }
   }

   @Nonnull
   public UnexpectedInvocation errorForUnexpectedInvocation(
      @Nullable Object mock, @Nonnull String invokedClassDesc, @Nonnull String invokedMethod,
      @Nonnull Object[] replayArgs)
   {
      StringBuilder message = new StringBuilder(200);
      message.append("Unexpected invocation of:\n");
      message.append(new MethodFormatter(invokedClassDesc, invokedMethod));

      if (replayArgs.length > 0) {
         ArgumentMismatch argumentMismatch = new ArgumentMismatch();
         argumentMismatch.appendFormatted(replayArgs);
         message.append("\n   with arguments: ").append(argumentMismatch);
      }

      if (mock != null) {
         message.append("\n   on instance: ").append(ObjectMethods.objectIdentity(mock));
      }

      message.append("\nwhen was expecting an invocation of:\n").append(this);

      return newUnexpectedInvocationWithCause("Unexpected invocation", message.toString());
   }

   @Nonnull
   public UnexpectedInvocation errorForUnexpectedInvocation(@Nonnull Object[] replayArgs)
   {
      String message = "Unexpected invocation to:\n" + toString(replayArgs);
      return newUnexpectedInvocationWithCause("Unexpected invocation", message);
   }

   @Nonnull
   public UnexpectedInvocation errorForUnexpectedInvocations(@Nonnull Object[] replayArgs, int numUnexpected)
   {
      String message = numUnexpected + " unexpected" + invocationsTo(numUnexpected) + toString(replayArgs);
      String titleForCause = numUnexpected == 1 ? "Unexpected invocation" : "Unexpected invocations";
      return newUnexpectedInvocationWithCause(titleForCause, message);
   }

   @Nonnull
   public UnexpectedInvocation errorForUnexpectedInvocationBeforeAnother(@Nonnull ExpectedInvocation another)
   {
      String titleForCause = "Unexpected invocation " + this;
      String initialMessage = "Unexpected invocation before " + another;
      return newUnexpectedInvocationWithCause(titleForCause, initialMessage);
   }

   @Nonnull
   public UnexpectedInvocation errorForUnexpectedInvocationFoundBeforeAnother()
   {
      String initialMessage = "Invocation occurred unexpectedly before another " + this;
      return newUnexpectedInvocationWithCause("Unexpected invocation", initialMessage);
   }

   @Nonnull
   public UnexpectedInvocation errorForUnexpectedInvocationFoundBeforeAnother(@Nonnull ExpectedInvocation another)
   {
      String titleForCause = "Unexpected invocation " + this;
      String initialMessage = "Another invocation unexpectedly occurred before" + another;
      return newUnexpectedInvocationWithCause(titleForCause, initialMessage);
   }

   @Nonnull
   public UnexpectedInvocation errorForUnexpectedInvocationAfterAnother(@Nonnull ExpectedInvocation another)
   {
      String titleForCause = "Unexpected invocation " + this;
      String initialMessage = "Unexpected invocation after " + another;
      return newUnexpectedInvocationWithCause(titleForCause, initialMessage);
   }

   @Nonnull @Override
   public String toString() { return toString((Object) null); }

   @Nonnull
   public String toString(@Nullable Object otherInstance)
   {
      String desc = arguments.toString();

      if (instance != otherInstance && instance != null) {
         desc += "\n   on mock instance: " + ObjectMethods.objectIdentity(instance);
      }

      return desc;
   }

   @Nonnull
   String toString(@Nonnull Object[] actualInvocationArguments)
   {
      Object[] invocationArgs = arguments.getValues();
      List> matchers = arguments.getMatchers();
      arguments.setValues(actualInvocationArguments);
      arguments.setMatchers(null);
      String description = toString();
      arguments.setMatchers(matchers);
      arguments.setValues(invocationArgs);
      return description;
   }

   public void printCause(@Nonnull Appendable errorMessage)
   {
      if (invocationCause != null) {
         try { errorMessage.append('\n'); } catch (IOException ignore) {}

         StackTrace st = new StackTrace(invocationCause);
         st.filter();
         st.print(errorMessage);
      }
   }

   @Nullable
   public Error assertThatArgumentsMatch(@Nonnull Object[] replayArgs, @Nonnull Map instanceMap)
   {
      return arguments.assertMatch(replayArgs, instanceMap);
   }

   // Default result //////////////////////////////////////////////////////////////////////////////////////////////////

   @Nullable
   public Object getDefaultValueForReturnType()
   {
      if (defaultReturnValue == UNDEFINED_DEFAULT_RETURN) {
         Class resolvedReturnType = getReturnTypeAsResolvedFromClassArgument();

         if (resolvedReturnType != null) {
            defaultReturnValue = DefaultValues.computeForType(resolvedReturnType);

            if (defaultReturnValue == null) {
               String returnTypeDesc = 'L' + resolvedReturnType.getName().replace('.', '/') + ';';
               String mockedTypeDesc = getClassDesc();
               defaultReturnValue = MockedTypeCascade.getMock(
                  mockedTypeDesc, arguments.methodNameAndDesc, instance, returnTypeDesc, resolvedReturnType);
            }

            return defaultReturnValue;
         }

         String returnTypeDesc = DefaultValues.getReturnTypeDesc(arguments.methodNameAndDesc);

         if ("V".equals(returnTypeDesc)) {
            return null;
         }

         defaultReturnValue = DefaultValues.computeForType(returnTypeDesc);

         if (defaultReturnValue == null) {
            String mockedTypeDesc = getClassDesc();
            defaultReturnValue = MockedTypeCascade.getMock(
               mockedTypeDesc, arguments.methodNameAndDesc, instance, returnTypeDesc, arguments.genericSignature);
         }
      }

      return defaultReturnValue;
   }

   @Nullable
   private Class getReturnTypeAsResolvedFromClassArgument()
   {
      String genericSignature = arguments.genericSignature;

      if (genericSignature != null) {
         int returnTypePos = genericSignature.lastIndexOf(')') + 1;
         char c = genericSignature.charAt(returnTypePos);

         if (c == 'T') {
            for (Object arg : arguments.getValues()) {
               if (arg instanceof Class) {
                  return (Class) arg;
               }
            }
         }
      }

      return null;
   }

   public void copyDefaultReturnValue(@Nonnull ExpectedInvocation other)
   {
      defaultReturnValue = other.defaultReturnValue;
   }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy