mockit.internal.state.MockFixture Maven / Gradle / Ivy
Show all versions of jmockit Show documentation
/*
* Copyright (c) 2006 JMockit developers
* This file is subject to the terms of the MIT license (see LICENSE.txt).
*/
package mockit.internal.state;
import static java.lang.reflect.Modifier.isAbstract;
import static mockit.internal.util.GeneratedClasses.getMockedClass;
import static mockit.internal.util.GeneratedClasses.getMockedClassOrInterfaceType;
import static mockit.internal.util.GeneratedClasses.isGeneratedImplementationClass;
import static mockit.internal.util.Utilities.getClassType;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.lang.instrument.ClassDefinition;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import mockit.internal.ClassFile;
import mockit.internal.ClassIdentification;
import mockit.internal.capturing.CaptureTransformer;
import mockit.internal.expectations.mocking.CaptureOfNewInstances;
import mockit.internal.expectations.mocking.InstanceFactory;
import mockit.internal.startup.Startup;
import mockit.internal.util.ClassLoad;
/**
* Holds data about redefined/transformed classes, with methods to add/remove and query such data.
*/
public final class MockFixture {
/**
* Similar to {@link #redefinedClasses}, but for classes modified by a ClassFileTransformer
such as the
* CaptureTransformer
, and containing the pre-transform bytecode instead of the modified one.
*
* @see #addTransformedClass(ClassIdentification, byte[])
* @see #getTransformedClasses()
* @see #restoreTransformedClasses(Set)
*/
@NonNull
private final Map transformedClasses;
/**
* Real classes currently redefined in the running JVM and their current (modified) bytecodes.
*
* The keys in the map allow each redefined real class to be later restored to a previous definition.
*
* The modified bytecode arrays in the map allow a new redefinition to be made on top of the current redefinition
* (in the case of the Faking API), or to restore the class to a previous definition (provided the map is copied
* between redefinitions of the same class).
*
* @see #addRedefinedClass(ClassDefinition)
* @see #getRedefinedClasses()
* @see #getRedefinedClassfile(Class)
* @see #containsRedefinedClass(Class)
* @see #restoreRedefinedClasses(Map)
*/
@NonNull
private final Map, byte[]> redefinedClasses;
/**
* Subset of all currently redefined classes which contain one or more native methods.
*
* This is needed because in order to restore such methods it is necessary (for some classes) to re-register them
* with the JVM.
*
* @see #reregisterNativeMethodsForRestoredClass(Class)
*/
@NonNull
private final Set redefinedClassesWithNativeMethods;
/**
* Maps redefined real classes to the internal name of the corresponding fake classes, when it's the case.
*
* This allows any global state associated to a fake class to be discarded when the corresponding real class is
* later restored to its original definition.
*
* @see #addRedefinedClass(String, ClassDefinition)
*/
@NonNull
private final Map, String> realClassesToFakeClasses;
/**
* A list of classes that are currently mocked. Said classes are also added to {@link #mockedTypesAndInstances}.
*
* @see #registerMockedClass(Class)
* @see #getMockedClasses()
* @see #isStillMocked(Object, String)
* @see #isInstanceOfMockedClass(Object)
* @see #removeMockedClasses(List)
*/
@NonNull
private final List> mockedClasses;
/**
* A map of mocked types to their corresponding {@linkplain InstanceFactory mocked instance factories}.
*
* @see #registerInstanceFactoryForMockedType(Class, InstanceFactory)
* @see #findInstanceFactory(Type)
* @see #isStillMocked(Object, String)
* @see #removeMockedClasses(List)
*/
@NonNull
private final Map mockedTypesAndInstances;
/**
* A list of "capturing" class file transformers, used by both the mocking and faking APIs.
*
* @see #addCaptureTransformer(CaptureTransformer)
* @see #areCapturedClasses(Class, Class)
* @see #isCaptured(Object)
* @see #getCaptureTransformerCount()
* @see #removeCaptureTransformers(int)
*/
@NonNull
private final List> captureTransformers;
MockFixture() {
transformedClasses = new HashMap<>(2);
redefinedClasses = new ConcurrentHashMap<>(8);
redefinedClassesWithNativeMethods = new HashSet<>();
realClassesToFakeClasses = new IdentityHashMap<>(8);
mockedClasses = new ArrayList<>();
mockedTypesAndInstances = new IdentityHashMap<>();
captureTransformers = new ArrayList<>();
}
// Methods to add/remove transformed/redefined classes /////////////////////////////////////////////////////////////
public void addTransformedClass(@NonNull ClassIdentification classId, @NonNull byte[] pretransformClassfile) {
transformedClasses.put(classId, pretransformClassfile);
}
// Methods used by both the Mocking and Faking APIs.
public void addRedefinedClass(@NonNull ClassDefinition newClassDefinition) {
redefinedClasses.put(newClassDefinition.getDefinitionClass(), newClassDefinition.getDefinitionClassFile());
}
public void registerMockedClass(@NonNull Class> mockedType) {
if (!mockedClasses.contains(mockedType)) {
mockedType = getMockedClassOrInterfaceType(mockedType);
mockedClasses.add(mockedType);
}
}
// Methods used by the Mocking API.
public void redefineClasses(@NonNull ClassDefinition... definitions) {
Startup.redefineMethods(definitions);
for (ClassDefinition def : definitions) {
addRedefinedClass(def);
}
}
public void redefineMethods(@NonNull Map, byte[]> modifiedClassfiles) {
ClassDefinition[] classDefs = new ClassDefinition[modifiedClassfiles.size()];
int i = 0;
for (Entry, byte[]> classAndBytecode : modifiedClassfiles.entrySet()) {
Class> modifiedClass = classAndBytecode.getKey();
byte[] modifiedClassfile = classAndBytecode.getValue();
ClassDefinition classDef = new ClassDefinition(modifiedClass, modifiedClassfile);
classDefs[i] = classDef;
i++;
addRedefinedClass(classDef);
}
Startup.redefineMethods(classDefs);
}
public boolean isStillMocked(@Nullable Object instance, @NonNull String classDesc) {
Class> targetClass;
if (instance == null) {
targetClass = ClassLoad.loadByInternalName(classDesc);
return isClassAssignableTo(targetClass);
}
targetClass = instance.getClass();
return mockedTypesAndInstances.containsKey(targetClass) || isInstanceOfMockedClass(instance);
}
private boolean isClassAssignableTo(@NonNull Class> toClass) {
for (Class> mockedClass : mockedClasses) {
if (toClass == mockedClass || toClass.isAssignableFrom(mockedClass)) {
return true;
}
}
return false;
}
public boolean isInstanceOfMockedClass(@NonNull Object mockedInstance) {
Class> mockedClass = getMockedClassOrInterfaceType(mockedInstance.getClass());
Class> mockedSuperclass = mockedClass.getSuperclass();
if (mockedSuperclass != null && mockedSuperclass.isEnum()) {
return mockedClasses.contains(mockedSuperclass);
}
return mockedClasses.contains(mockedClass) || isCaptured(mockedClass);
}
public void registerInstanceFactoryForMockedType(@NonNull Class> mockedType,
@NonNull InstanceFactory mockedInstanceFactory) {
registerMockedClass(mockedType);
mockedTypesAndInstances.put(mockedType, mockedInstanceFactory);
}
@Nullable
public InstanceFactory findInstanceFactory(@NonNull Type mockedType) {
InstanceFactory instanceFactory = mockedTypesAndInstances.get(mockedType);
if (instanceFactory != null) {
return instanceFactory;
}
Class> mockedClass = getClassType(mockedType);
// noinspection ReuseOfLocalVariable
instanceFactory = mockedTypesAndInstances.get(mockedClass);
if (instanceFactory != null) {
return instanceFactory;
}
boolean abstractType = mockedClass.isInterface() || isAbstract(mockedClass.getModifiers());
for (Entry entry : mockedTypesAndInstances.entrySet()) {
Type registeredMockedType = entry.getKey();
Class> registeredMockedClass = getClassType(registeredMockedType);
if (abstractType) {
registeredMockedClass = getMockedClassOrInterfaceType(registeredMockedClass);
}
if (mockedClass.isAssignableFrom(registeredMockedClass)) {
instanceFactory = entry.getValue();
break;
}
}
return instanceFactory;
}
// Methods used by the Faking API.
public void addRedefinedClass(@NonNull String fakeClassInternalName, @NonNull ClassDefinition classDef) {
@NonNull
Class> redefinedClass = classDef.getDefinitionClass();
String previousNames = realClassesToFakeClasses.put(redefinedClass, fakeClassInternalName);
if (previousNames != null) {
realClassesToFakeClasses.put(redefinedClass, previousNames + ' ' + fakeClassInternalName);
}
addRedefinedClass(classDef);
}
// Methods used by test save-points ////////////////////////////////////////////////////////////////////////////////
void restoreTransformedClasses(@NonNull Set previousTransformedClasses) {
if (!transformedClasses.isEmpty()) {
Set classesToRestore;
if (previousTransformedClasses.isEmpty()) {
classesToRestore = transformedClasses.keySet();
} else {
classesToRestore = getTransformedClasses();
classesToRestore.removeAll(previousTransformedClasses);
}
if (!classesToRestore.isEmpty()) {
restoreAndRemoveTransformedClasses(classesToRestore);
}
}
}
@NonNull
Set getTransformedClasses() {
return transformedClasses.isEmpty() ? Collections.emptySet() : new HashSet<>(transformedClasses.keySet());
}
@NonNull
Map, byte[]> getRedefinedClasses() {
return redefinedClasses.isEmpty() ? Collections.emptyMap() : new HashMap<>(redefinedClasses);
}
private void restoreAndRemoveTransformedClasses(@NonNull Set classesToRestore) {
for (ClassIdentification transformedClassId : classesToRestore) {
byte[] definitionToRestore = transformedClasses.get(transformedClassId);
Startup.redefineMethods(transformedClassId, definitionToRestore);
}
transformedClasses.keySet().removeAll(classesToRestore);
}
void restoreRedefinedClasses(@NonNull Map, byte[]> previousDefinitions) {
if (redefinedClasses.isEmpty()) {
return;
}
Iterator, byte[]>> itr = redefinedClasses.entrySet().iterator();
while (itr.hasNext()) {
Entry, byte[]> entry = itr.next();
Class> redefinedClass = entry.getKey();
byte[] currentDefinition = entry.getValue();
byte[] previousDefinition = previousDefinitions.get(redefinedClass);
if (previousDefinition == null) {
restoreDefinition(redefinedClass);
itr.remove();
} else if (currentDefinition != previousDefinition) {
Startup.redefineMethods(redefinedClass, previousDefinition);
entry.setValue(previousDefinition);
}
}
}
private void restoreDefinition(@NonNull Class> redefinedClass) {
if (!isGeneratedImplementationClass(redefinedClass)) {
byte[] previousDefinition = ClassFile.getClassFile(redefinedClass);
Startup.redefineMethods(redefinedClass, previousDefinition);
}
if (redefinedClassesWithNativeMethods.contains(redefinedClass.getName())) {
reregisterNativeMethodsForRestoredClass(redefinedClass);
}
removeMockedClass(redefinedClass);
discardStateForCorrespondingFakeClassIfAny(redefinedClass);
}
private void removeMockedClass(@NonNull Class> mockedClass) {
mockedTypesAndInstances.remove(mockedClass);
mockedClasses.remove(mockedClass);
}
private void discardStateForCorrespondingFakeClassIfAny(@NonNull Class> redefinedClass) {
String mockClassesInternalNames = realClassesToFakeClasses.remove(redefinedClass);
TestRun.getFakeStates().removeClassState(redefinedClass, mockClassesInternalNames);
}
void removeMockedClasses(@NonNull List> previousMockedClasses) {
int currentMockedClassCount = mockedClasses.size();
if (currentMockedClassCount > 0) {
int previousMockedClassCount = previousMockedClasses.size();
if (previousMockedClassCount == 0) {
mockedClasses.clear();
mockedTypesAndInstances.clear();
} else if (previousMockedClassCount < currentMockedClassCount) {
mockedClasses.retainAll(previousMockedClasses);
mockedTypesAndInstances.keySet().retainAll(previousMockedClasses);
}
}
}
// Getter methods for the maps and collections of transformed/redefined/mocked classes /////////////////////////////
@Nullable
public byte[] getRedefinedClassfile(@NonNull Class> redefinedClass) {
return redefinedClasses.get(redefinedClass);
}
public boolean containsRedefinedClass(@NonNull Class> redefinedClass) {
return redefinedClasses.containsKey(redefinedClass);
}
@NonNull
public List> getMockedClasses() {
return mockedClasses.isEmpty() ? Collections.emptyList() : new ArrayList<>(mockedClasses);
}
// Methods dealing with capture transformers ///////////////////////////////////////////////////////////////////////
public void addCaptureTransformer(@NonNull CaptureTransformer> transformer) {
captureTransformers.add(transformer);
}
// The following methods are used by test save-points to discard currently active capture transformers.
int getCaptureTransformerCount() {
return captureTransformers.size();
}
void removeCaptureTransformers(int previousTransformerCount) {
int currentTransformerCount = captureTransformers.size();
for (int i = currentTransformerCount - 1; i >= previousTransformerCount; i--) {
CaptureTransformer> transformer = captureTransformers.get(i);
transformer.deactivate();
Startup.instrumentation().removeTransformer(transformer);
captureTransformers.remove(i);
}
}
// The following methods are only used by the Mocking API.
public boolean isCaptured(@NonNull Object mockedInstance) {
if (!captureTransformers.isEmpty()) {
Class> mockedClass = getMockedClass(mockedInstance);
return isCaptured(mockedClass);
}
return false;
}
private boolean isCaptured(@NonNull Class> mockedClass) {
for (CaptureTransformer> captureTransformer : captureTransformers) {
CaptureOfNewInstances capture = captureTransformer.getCaptureOfImplementationsIfApplicable(mockedClass);
if (capture != null) {
return true;
}
}
return false;
}
public boolean areCapturedClasses(@NonNull Class> mockedClass1, @NonNull Class> mockedClass2) {
for (CaptureTransformer> captureTransformer : captureTransformers) {
if (captureTransformer.areCapturedClasses(mockedClass1, mockedClass2)) {
return true;
}
}
return false;
}
private static void reregisterNativeMethodsForRestoredClass(@NonNull Class> realClass) {
Method registerNatives = null;
try {
registerNatives = realClass.getDeclaredMethod("registerNatives");
} catch (NoSuchMethodException ignore) {
try {
registerNatives = realClass.getDeclaredMethod("initIDs");
} catch (NoSuchMethodException ignored) {
} // OK
}
if (registerNatives != null) {
try {
registerNatives.setAccessible(true);
registerNatives.invoke(null);
} catch (IllegalAccessException ignore) {
} // won't happen
catch (InvocationTargetException ignore) {
} // shouldn't happen either
}
// OK, although another solution will be required for this particular class if it requires
// natives to be explicitly registered again (not all do, such as java.lang.Float).
}
public void addRedefinedClassWithNativeMethods(@NonNull String redefinedClassInternalName) {
redefinedClassesWithNativeMethods.add(redefinedClassInternalName.replace('/', '.'));
}
}