/*
* Copyright (c) 2006-2011 Rogério Liesenfeld
* This file is subject to the terms of the MIT license (see LICENSE.txt).
*/
package mockit.internal.state;
import java.lang.reflect.*;
import java.util.*;
import java.util.Map.*;
import mockit.internal.*;
import mockit.internal.expectations.mocking.*;
/**
* Holds data about redefined real classes and their corresponding mock classes (if any), and
* provides methods to add/remove such state both from this instance and from other state holders
* with associated data.
*/
public final class MockFixture
{
/**
* Class names with their bytecode fixed definitions, that is, those to which the class should
* eventually be restored.
* <p/>
* This map is to be used by other bytecode instrumentation tools such as JMockit Coverage,
* which need to avoid having their bytecode redefinitions/transformations getting discarded when
* test tear down executes and restores mocked classes.
* <p/>
* The modified bytecode arrays in the map allow a new redefinition for a given class to be made,
* on top of the fixed definition.
*/
private final Map<String, byte[]> fixedClassDefinitions = new HashMap<String, byte[]>();
/**
* Similar to <code>redefinedClasses</code>, but for classes modified by a ClassFileTransformer
* such as the CaptureTransformer, and containing the pre-transform bytecode instead of the
* modified one.
*/
private final Map<String, byte[]> transformedClasses = new HashMap<String, byte[]>(2);
/**
* Real classes currently redefined in the running JVM and their current (modified) bytecodes.
* <p/>
* The keys in the map allow each redefined real class to be later restored to its original
* definition at any moment, by re-reading the class file from disk.
* <p/>
* The modified bytecode arrays in the map allow a new redefinition for a given real class to be
* made, on top of the current redefinition.
*/
private final Map<Class<?>, byte[]> redefinedClasses = new HashMap<Class<?>, byte[]>(8);
/**
* Subset of all currently redefined classes which contain one or more native methods.
* <p/>
* 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)
*/
private final Set<String> redefinedClassesWithNativeMethods = new HashSet<String>();
/**
* Maps redefined real classes to the internal name of the corresponding mock classes, when it's
* the case.
* <p/>
* This allows any global state associated to a mock class to be discarded when the corresponding
* real class is later restored to its original definition.
*/
private final Map<Class<?>, String> realClassesToMockClasses = new HashMap<Class<?>, String>(8);
private final Map<Class<?>, InstanceFactory> mockedTypesAndInstances = new HashMap<Class<?>, InstanceFactory>();
// Methods to add/remove redefined classes /////////////////////////////////////////////////////////////////////////
public void addFixedClass(String className, byte[] fixedClassfile)
{
fixedClassDefinitions.put(className, fixedClassfile);
}
public void addTransformedClass(String className, byte[] pretransformClassfile)
{
transformedClasses.put(className, pretransformClassfile);
}
public void addRedefinedClass(String mockClassInternalName, Class<?> redefinedClass, byte[] modifiedClassfile)
{
if (mockClassInternalName != null) {
String previousNames = realClassesToMockClasses.put(redefinedClass, mockClassInternalName);
if (previousNames != null) {
realClassesToMockClasses.put(redefinedClass, previousNames + ' ' + mockClassInternalName);
}
}
addRedefinedClass(redefinedClass, modifiedClassfile);
// TODO: implement support for multiple simultaneous redefinitions for each class?
// at least support the case where some class is stubbed out for the whole test class and
// then mocked in one or more tests, particularly when using @Mock(invocations = n)
}
public void addRedefinedClass(Class<?> redefinedClass, byte[] modifiedClassfile)
{
redefinedClasses.put(redefinedClass, modifiedClassfile);
}
public void addInstanceForMockedType(Class<?> mockedType, InstanceFactory mockInstanceFactory)
{
mockedTypesAndInstances.put(mockedType, mockInstanceFactory);
}
public Object getNewInstanceForMockedType(Class<?> mockedType)
{
InstanceFactory instanceFactory = mockedTypesAndInstances.get(mockedType);
if (instanceFactory == null) {
return null;
}
TestRun.getExecutingTest().setShouldIgnoreMockingCallbacks(true);
try {
return instanceFactory.create();
}
finally {
TestRun.getExecutingTest().setShouldIgnoreMockingCallbacks(false);
}
}
public void restoreAndRemoveTransformedClasses(Set<String> transformedClassesToRestore)
{
RedefinitionEngine redefinitionEngine = new RedefinitionEngine();
for (String transformedClassName : transformedClassesToRestore) {
byte[] definitionToRestore = transformedClasses.get(transformedClassName);
redefinitionEngine.restoreToDefinition(transformedClassName, definitionToRestore);
}
transformedClasses.keySet().removeAll(transformedClassesToRestore);
}
public void restoreAndRemoveRedefinedClasses(Set<Class<?>> redefinedClassesToRestore)
{
RedefinitionEngine redefinitionEngine = new RedefinitionEngine();
for (Class<?> redefinedClass : redefinedClassesToRestore) {
redefinitionEngine.restoreOriginalDefinition(redefinedClass);
if (redefinedClassesWithNativeMethods.contains(redefinedClass.getName())) {
reregisterNativeMethodsForRestoredClass(redefinedClass);
}
discardStateForCorrespondingMockClassIfAny(redefinedClass);
}
redefinedClasses.keySet().removeAll(redefinedClassesToRestore);
mockedTypesAndInstances.keySet().removeAll(redefinedClassesToRestore);
}
private void discardStateForCorrespondingMockClassIfAny(Class<?> redefinedClass)
{
String mockClassesInternalNames = realClassesToMockClasses.remove(redefinedClass);
TestRun.getMockClasses().getMockStates().removeClassState(redefinedClass, mockClassesInternalNames);
}
// Methods that deal with redefined native methods /////////////////////////////////////////////////////////////////
public void addRedefinedClassWithNativeMethods(String redefinedClassInternalName)
{
redefinedClassesWithNativeMethods.add(redefinedClassInternalName.replace('/', '.'));
}
private void reregisterNativeMethodsForRestoredClass(Class<?> realClass)
{
Method registerNatives = null;
try {
registerNatives = realClass.getDeclaredMethod("registerNatives");
}
catch (NoSuchMethodException ignore) {
try {
registerNatives = realClass.getDeclaredMethod("initIDs");
}
catch (NoSuchMethodException alsoIgnore) {
// 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).
}
// Getter methods for the maps of redefined classes ////////////////////////////////////////////////////////////////
public byte[] getFixedClassfile(String className)
{
return fixedClassDefinitions.get(className);
}
public int getRedefinedClassCount()
{
return redefinedClasses.size();
}
public byte[] getRedefinedClassfile(Class<?> redefinedClass)
{
return redefinedClasses.get(redefinedClass);
}
public Set<String> getTransformedClasses()
{
return transformedClasses.keySet();
}
public Set<Class<?>> getRedefinedClasses()
{
return redefinedClasses.keySet();
}
public boolean containsRedefinedClass(Class<?> redefinedClass)
{
return redefinedClasses.containsKey(redefinedClass);
}
public void turnRedefinedClassesIntoFixedOnes()
{
Iterator<Entry<Class<?>, byte[]>> itr = redefinedClasses.entrySet().iterator();
while (itr.hasNext()) {
Entry<Class<?>, byte[]> classAndBytecode = itr.next();
itr.remove();
addFixedClass(classAndBytecode.getKey().getName(), classAndBytecode.getValue());
}
}
}