/* * Copyright (c) 2006 Rogério Liesenfeld * This file is subject to the terms of the MIT license (see LICENSE.txt). */ package mockit.internal.expectations.mocking; import java.util.*; import javax.annotation.*; import static java.lang.reflect.Modifier.*; import mockit.external.asm.*; import mockit.internal.*; import mockit.internal.expectations.*; import mockit.internal.util.*; import static mockit.external.asm.Opcodes.*; import static mockit.internal.expectations.MockingFilters.*; import static mockit.internal.expectations.mocking.MockedTypeModifier.*; import static mockit.internal.util.ObjectMethods.isMethodFromObject; import static mockit.internal.util.Utilities.*; final class ExpectationsModifier extends BaseClassModifier { private static final boolean NATIVE_UNSUPPORTED = !HOTSPOT_VM; private static final int METHOD_ACCESS_MASK = ACC_PRIVATE + ACC_SYNTHETIC + ACC_ABSTRACT; private static final int PUBLIC_OR_PROTECTED = ACC_PUBLIC + ACC_PROTECTED; @Nullable private final MockedType mockedType; private final boolean classFromNonBootstrapClassLoader; private String className; @Nullable private String baseClassNameForCapturedInstanceMethods; private boolean ignoreConstructors; private ExecutionMode executionMode; private boolean isProxy; @Nullable private String defaultFilters; @Nullable List<String> enumSubclasses; ExpectationsModifier( @Nullable ClassLoader classLoader, @Nonnull ClassReader classReader, @Nullable MockedType typeMetadata) { super(classReader); mockedType = typeMetadata; classFromNonBootstrapClassLoader = classLoader != null; setUseMockingBridge(classLoader); executionMode = ExecutionMode.Regular; useInstanceBasedMockingIfApplicable(); } private void useInstanceBasedMockingIfApplicable() { if (mockedType != null && mockedType.injectable) { ignoreConstructors = mockedType.getMaxInstancesToCapture() <= 0; executionMode = ExecutionMode.PerInstance; } } public void setClassNameForCapturedInstanceMethods(@Nonnull String internalClassName) { baseClassNameForCapturedInstanceMethods = internalClassName; } public void useDynamicMocking(boolean methodsOnly) { ignoreConstructors = methodsOnly; executionMode = ExecutionMode.Partial; } @Override public void visit( int version, int access, @Nonnull String name, @Nullable String signature, @Nullable String superName, @Nullable String[] interfaces) { validateMockingOfJREClass(name); super.visit(version, access, name, signature, superName, interfaces); isProxy = "java/lang/reflect/Proxy".equals(superName); if (isProxy) { assert interfaces != null; className = interfaces[0]; defaultFilters = null; } else { className = name; defaultFilters = filtersForClass(name); if (defaultFilters != null && defaultFilters.isEmpty()) { throw VisitInterruptedException.INSTANCE; } } } private void validateMockingOfJREClass(@Nonnull String internalName) { if (internalName.startsWith("java/")) { if (isUnmockable(internalName)) { throw new IllegalArgumentException("Class " + internalName.replace('/', '.') + " is not mockable"); } if (executionMode == ExecutionMode.Regular && mockedType != null && isFullMockingDisallowed(internalName)) { String modifyingClassName = internalName.replace('/', '.'); if (modifyingClassName.equals(mockedType.getClassType().getName())) { throw new IllegalArgumentException( "Class " + internalName.replace('/', '.') + " cannot be @Mocked fully; " + "instead, use @Injectable or partial mocking"); } } } } @Override public void visitInnerClass(@Nonnull String name, @Nullable String outerName, @Nullable String innerName, int access) { cw.visitInnerClass(name, outerName, innerName, access); if (access == ACC_ENUM + ACC_STATIC) { if (enumSubclasses == null) { enumSubclasses = new ArrayList<String>(); } enumSubclasses.add(name); } } @Nullable @Override public MethodVisitor visitMethod( int access, @Nonnull String name, @Nonnull String desc, @Nullable String signature, @Nullable String[] exceptions) { if ((access & METHOD_ACCESS_MASK) != 0) { return unmodifiedBytecode(access, name, desc, signature, exceptions); } boolean visitingConstructor = "<init>".equals(name); String internalClassName = className; if (visitingConstructor) { if (isConstructorNotAllowedByMockingFilters(name)) { return unmodifiedBytecode(access, name, desc, signature, exceptions); } startModifiedMethodVersion(access, name, desc, signature, exceptions); generateCallToSuperConstructor(); } else { if (isMethodNotToBeMocked(access, name, desc)) { return unmodifiedBytecode(access, name, desc, signature, exceptions); } if ("<clinit>".equals(name)) { return stubOutClassInitializationIfApplicable(access); } if (stubOutFinalizeMethod(access, name, desc)) { return null; } if (isMethodNotAllowedByMockingFilters(access, name)) { return unmodifiedBytecode(access, name, desc, signature, exceptions); } startModifiedMethodVersion(access, name, desc, signature, exceptions); if (baseClassNameForCapturedInstanceMethods != null) { internalClassName = baseClassNameForCapturedInstanceMethods; } } ExecutionMode actualExecutionMode = determineAppropriateExecutionMode(visitingConstructor); if (useMockingBridge) { return generateCallToHandlerThroughMockingBridge( signature, internalClassName, visitingConstructor, actualExecutionMode); } generateDirectCallToHandler(mw, internalClassName, access, name, desc, signature, actualExecutionMode); generateDecisionBetweenReturningOrContinuingToRealImplementation(); // Constructors of non-JRE classes can't be modified (unless running with "-noverify") in a way that // "super(...)/this(...)" get called twice, so we disregard such calls when copying the original bytecode. return copyOriginalImplementationCode(visitingConstructor); } private boolean isConstructorNotAllowedByMockingFilters(@Nonnull String name) { return isProxy || ignoreConstructors || isUnmockableInvocation(defaultFilters, name); } private boolean isMethodNotToBeMocked(int access, @Nonnull String name, @Nonnull String desc) { return isNative(access) && (NATIVE_UNSUPPORTED || (access & PUBLIC_OR_PROTECTED) == 0) || (isProxy || executionMode == ExecutionMode.Partial) && ( isMethodFromObject(name, desc) || "annotationType".equals(name) && "()Ljava/lang/Class;".equals(desc) ); } @Nonnull private MethodVisitor unmodifiedBytecode( int access, @Nonnull String name, @Nonnull String desc, @Nullable String signature, @Nullable String[] exceptions) { return cw.visitMethod(access, name, desc, signature, exceptions); } @Nullable private MethodVisitor stubOutClassInitializationIfApplicable(int access) { startModifiedMethodVersion(access, "<clinit>", "()V", null, null); if (mockedType != null && mockedType.isClassInitializationToBeStubbedOut()) { generateEmptyImplementation(); return null; } return mw; } private boolean stubOutFinalizeMethod(int access, @Nonnull String name, @Nonnull String desc) { if ("finalize".equals(name) && "()V".equals(desc)) { startModifiedMethodVersion(access, name, desc, null, null); generateEmptyImplementation(); return true; } return false; } private boolean isMethodNotAllowedByMockingFilters(int access, @Nonnull String name) { return baseClassNameForCapturedInstanceMethods != null && (access & ACC_STATIC) != 0 || executionMode.isMethodToBeIgnored(access) || isUnmockableInvocation(defaultFilters, name); } @Nonnull private ExecutionMode determineAppropriateExecutionMode(boolean visitingConstructor) { if (executionMode == ExecutionMode.PerInstance) { if (visitingConstructor) { return ignoreConstructors ? ExecutionMode.Regular : ExecutionMode.Partial; } if (isStatic(methodAccess)) { return ExecutionMode.Partial; } } return executionMode; } @Nonnull private MethodVisitor generateCallToHandlerThroughMockingBridge( @Nullable String genericSignature, @Nonnull String internalClassName, boolean visitingConstructor, @Nonnull ExecutionMode actualExecutionMode) { generateCodeToObtainInstanceOfMockingBridge(MockedBridge.MB); // First and second "invoke" arguments: boolean isStatic = generateCodeToPassThisOrNullIfStaticMethod(); mw.visitInsn(ACONST_NULL); // Create array for call arguments (third "invoke" argument): Type[] argTypes = Type.getArgumentTypes(methodDesc); generateCodeToCreateArrayOfObject(mw, 6 + argTypes.length); int i = 0; generateCodeToFillArrayElement(i++, methodAccess); generateCodeToFillArrayElement(i++, internalClassName); generateCodeToFillArrayElement(i++, methodName); generateCodeToFillArrayElement(i++, methodDesc); generateCodeToFillArrayElement(i++, genericSignature); generateCodeToFillArrayElement(i++, actualExecutionMode.ordinal()); generateCodeToFillArrayWithParameterValues(mw, argTypes, i, isStatic ? 0 : 1); generateCallToInvocationHandler(); generateDecisionBetweenReturningOrContinuingToRealImplementation(); // Copies the entire original implementation even for a constructor, in which case the complete bytecode inside // the constructor fails the strict verification activated by "-Xfuture". However, this is necessary to allow the // full execution of a bootstrap class constructor when the call was not meant to be mocked. return copyOriginalImplementationCode(visitingConstructor && classFromNonBootstrapClassLoader); } private void generateDecisionBetweenReturningOrContinuingToRealImplementation() { Label startOfRealImplementation = new Label(); mw.visitInsn(DUP); mw.visitLdcInsn(VOID_TYPE); mw.visitJumpInsn(IF_ACMPEQ, startOfRealImplementation); generateReturnWithObjectAtTopOfTheStack(methodDesc); mw.visitLabel(startOfRealImplementation); mw.visitInsn(POP); } }