blob: 1a557a62d25310ac98ac4cc3c104b3eec9cfad0d [file] [log] [blame]
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.testing.mocking;
import javassist.CannotCompileException;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtField;
import javassist.CtMethod;
import javassist.CtNewConstructor;
import javassist.NotFoundException;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* AndroidMockGenerator creates the subclass and interface required for mocking
* a given Class.
*
* The only public method of AndroidMockGenerator is createMocksForClass. See
* the javadocs for this method for more information about AndroidMockGenerator.
*
* @author swoodward@google.com (Stephen Woodward)
*/
class AndroidMockGenerator {
public AndroidMockGenerator() {
ClassPool.doPruning = false;
ClassPool.getDefault().insertClassPath(new ClassClassPath(MockObject.class));
}
/**
* Creates a List of javassist.CtClass objects representing all of the
* interfaces and subclasses required to meet the Mocking requests of the
* Class specified by {@code clazz}.
*
* A test class can request that a Class be prepared for mocking by using the
* {@link UsesMocks} annotation at either the Class or Method level. All
* classes specified by these annotations will have exactly two CtClass
* objects created, one for a generated interface, and one for a generated
* subclass. The interface and subclass both define the same methods which
* comprise all of the mockable methods of the provided class. At present, for
* a method to be mockable, it must be non-final and non-static, although this
* may expand in the future.
*
* The class itself must be mockable, otherwise this method will ignore the
* requested mock and print a warning. At present, a class is mockable if it
* is a non-final publicly-instantiable Java class that is assignable from the
* java.lang.Object class. See the javadocs for
* {@link java.lang.Class#isAssignableFrom(Class)} for more information about
* what "is assignable from the Object class" means. As a non-exhaustive
* example, if a given Class represents an Enum, Annotation, Primitive or
* Array, then it is not assignable from Object. Interfaces are also ignored
* since these need no modifications in order to be mocked.
*
* @param clazz the Class object to have all of its UsesMocks annotations
* processed and the corresponding Mock Classes created.
* @return a List of CtClass objects representing the Classes and Interfaces
* required for mocking the classes requested by {@code clazz}
* @throws ClassNotFoundException
* @throws CannotCompileException
* @throws IOException
*/
public List<GeneratedClassFile> createMocksForClass(Class<?> clazz)
throws ClassNotFoundException, IOException, CannotCompileException {
return this.createMocksForClass(clazz, SdkVersion.UNKNOWN);
}
public List<GeneratedClassFile> createMocksForClass(Class<?> clazz, SdkVersion sdkVersion)
throws ClassNotFoundException, IOException, CannotCompileException {
if (!classIsSupportedType(clazz)) {
reportReasonForUnsupportedType(clazz);
return Arrays.asList(new GeneratedClassFile[0]);
}
CtClass newInterfaceCtClass = generateInterface(clazz, sdkVersion);
GeneratedClassFile newInterface = new GeneratedClassFile(newInterfaceCtClass.getName(),
newInterfaceCtClass.toBytecode());
CtClass mockDelegateCtClass = generateSubClass(clazz, newInterfaceCtClass, sdkVersion);
GeneratedClassFile mockDelegate = new GeneratedClassFile(mockDelegateCtClass.getName(),
mockDelegateCtClass.toBytecode());
return Arrays.asList(new GeneratedClassFile[] {newInterface, mockDelegate});
}
private void reportReasonForUnsupportedType(Class<?> clazz) {
String reason = null;
if (clazz.isInterface()) {
// do nothing to make sure none of the other conditions apply.
} else if (clazz.isEnum()) {
reason = "Cannot mock an Enum";
} else if (clazz.isAnnotation()) {
reason = "Cannot mock an Annotation";
} else if (clazz.isArray()) {
reason = "Cannot mock an Array";
} else if (Modifier.isFinal(clazz.getModifiers())) {
reason = "Cannot mock a Final class";
} else if (clazz.isPrimitive()) {
reason = "Cannot mock primitives";
} else if (!Object.class.isAssignableFrom(clazz)) {
reason = "Cannot mock non-classes";
} else if (!containsUsableConstructor(clazz)) {
reason = "Cannot mock a class with no public constructors";
} else {
// Whatever the reason is, it's not one that we care about.
}
if (reason != null) {
// Sometimes we want to be silent, so check 'reason' against null.
System.err.println(reason + ": " + clazz.getName());
}
}
private boolean containsUsableConstructor(Class<?> clazz) {
Constructor<?>[] constructors = clazz.getDeclaredConstructors();
for (Constructor<?> constructor : constructors) {
if (Modifier.isPublic(constructor.getModifiers()) ||
Modifier.isProtected(constructor.getModifiers())) {
return true;
}
}
return false;
}
boolean classIsSupportedType(Class<?> clazz) {
return (containsUsableConstructor(clazz)) && Object.class.isAssignableFrom(clazz)
&& !clazz.isInterface() && !clazz.isEnum() && !clazz.isAnnotation() && !clazz.isArray()
&& !Modifier.isFinal(clazz.getModifiers());
}
void saveCtClass(CtClass clazz) throws ClassNotFoundException, IOException {
try {
clazz.writeFile();
} catch (NotFoundException e) {
throw new ClassNotFoundException("Error while saving modified class " + clazz.getName(), e);
} catch (CannotCompileException e) {
throw new RuntimeException("Internal Error: Attempt to save syntactically incorrect code "
+ "for class " + clazz.getName(), e);
}
}
CtClass generateInterface(Class<?> originalClass, SdkVersion sdkVersion) {
ClassPool classPool = getClassPool();
try {
return classPool.getCtClass(FileUtils.getInterfaceNameFor(originalClass, sdkVersion));
} catch (NotFoundException e) {
CtClass newInterface =
classPool.makeInterface(FileUtils.getInterfaceNameFor(originalClass, sdkVersion));
addInterfaceMethods(originalClass, newInterface);
return newInterface;
}
}
String getInterfaceMethodSource(Method method) throws UnsupportedOperationException {
StringBuilder methodBody = getMethodSignature(method);
methodBody.append(";");
return methodBody.toString();
}
private StringBuilder getMethodSignature(Method method) {
int modifiers = method.getModifiers();
if (Modifier.isFinal(modifiers) || Modifier.isStatic(modifiers)) {
throw new UnsupportedOperationException(
"Cannot specify final or static methods in an interface");
}
StringBuilder methodSignature = new StringBuilder("public ");
methodSignature.append(getClassName(method.getReturnType()));
methodSignature.append(" ");
methodSignature.append(method.getName());
methodSignature.append("(");
int i = 0;
for (Class<?> arg : method.getParameterTypes()) {
methodSignature.append(getClassName(arg));
methodSignature.append(" arg");
methodSignature.append(i);
if (i < method.getParameterTypes().length - 1) {
methodSignature.append(",");
}
i++;
}
methodSignature.append(")");
if (method.getExceptionTypes().length > 0) {
methodSignature.append(" throws ");
}
i = 0;
for (Class<?> exception : method.getExceptionTypes()) {
methodSignature.append(getClassName(exception));
if (i < method.getExceptionTypes().length - 1) {
methodSignature.append(",");
}
i++;
}
return methodSignature;
}
private String getClassName(Class<?> clazz) {
return clazz.getCanonicalName();
}
static ClassPool getClassPool() {
return ClassPool.getDefault();
}
private boolean classExists(String name) {
// The following line is the ideal, but doesn't work (bug in library).
// return getClassPool().find(name) != null;
try {
getClassPool().get(name);
return true;
} catch (NotFoundException e) {
return false;
}
}
CtClass generateSubClass(Class<?> superClass, CtClass newInterface, SdkVersion sdkVersion)
throws ClassNotFoundException {
if (classExists(FileUtils.getSubclassNameFor(superClass, sdkVersion))) {
try {
return getClassPool().get(FileUtils.getSubclassNameFor(superClass, sdkVersion));
} catch (NotFoundException e) {
throw new ClassNotFoundException("This should be impossible, since we just checked for "
+ "the existence of the class being created", e);
}
}
CtClass newClass = generateSkeletalClass(superClass, newInterface, sdkVersion);
if (!newClass.isFrozen()) {
newClass.addInterface(newInterface);
try {
newClass.addInterface(getClassPool().get(MockObject.class.getName()));
} catch (NotFoundException e) {
throw new ClassNotFoundException("Could not find " + MockObject.class.getName(), e);
}
addMethods(superClass, newClass);
addGetDelegateMethod(newClass);
addSetDelegateMethod(newClass, newInterface);
addConstructors(newClass, superClass);
}
return newClass;
}
private void addConstructors(CtClass clazz, Class<?> superClass) throws ClassNotFoundException {
CtClass superCtClass = getCtClassForClass(superClass);
CtConstructor[] constructors = superCtClass.getDeclaredConstructors();
for (CtConstructor constructor : constructors) {
int modifiers = constructor.getModifiers();
if (Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers)) {
CtConstructor ctConstructor;
try {
ctConstructor = CtNewConstructor.make(constructor.getParameterTypes(),
constructor.getExceptionTypes(), clazz);
clazz.addConstructor(ctConstructor);
} catch (CannotCompileException e) {
throw new RuntimeException("Internal Error - Could not add constructors.", e);
} catch (NotFoundException e) {
throw new RuntimeException("Internal Error - Constructor suddenly could not be found", e);
}
}
}
}
CtClass getCtClassForClass(Class<?> clazz) throws ClassNotFoundException {
ClassPool classPool = getClassPool();
try {
return classPool.get(clazz.getName());
} catch (NotFoundException e) {
throw new ClassNotFoundException("Class not found when finding the class to be mocked: "
+ clazz.getName(), e);
}
}
private void addSetDelegateMethod(CtClass clazz, CtClass newInterface) {
try {
clazz.addMethod(CtMethod.make(getSetDelegateMethodSource(newInterface), clazz));
} catch (CannotCompileException e) {
throw new RuntimeException("Internal error while creating the setDelegate() method", e);
}
}
String getSetDelegateMethodSource(CtClass newInterface) {
return "public void setDelegate___AndroidMock(" + newInterface.getName() + " obj) { this."
+ getDelegateFieldName() + " = obj;}";
}
private void addGetDelegateMethod(CtClass clazz) {
try {
CtMethod newMethod = CtMethod.make(getGetDelegateMethodSource(), clazz);
try {
CtMethod existingMethod = clazz.getMethod(newMethod.getName(), newMethod.getSignature());
clazz.removeMethod(existingMethod);
} catch (NotFoundException e) {
// expected path... sigh.
}
clazz.addMethod(newMethod);
} catch (CannotCompileException e) {
throw new RuntimeException("Internal error while creating the getDelegate() method", e);
}
}
private String getGetDelegateMethodSource() {
return "public Object getDelegate___AndroidMock() { return this." + getDelegateFieldName()
+ "; }";
}
String getDelegateFieldName() {
return "delegateMockObject";
}
void addInterfaceMethods(Class<?> originalClass, CtClass newInterface) {
Method[] methods = getAllMethods(originalClass);
for (Method method : methods) {
try {
if (isMockable(method)) {
CtMethod newMethod = CtMethod.make(getInterfaceMethodSource(method), newInterface);
newInterface.addMethod(newMethod);
}
} catch (UnsupportedOperationException e) {
// Can't handle finals and statics.
} catch (CannotCompileException e) {
throw new RuntimeException(
"Internal error while creating a new Interface method for class "
+ originalClass.getName() + ". Method name: " + method.getName(), e);
}
}
}
void addMethods(Class<?> superClass, CtClass newClass) {
Method[] methods = getAllMethods(superClass);
if (newClass.isFrozen()) {
newClass.defrost();
}
List<CtMethod> existingMethods = Arrays.asList(newClass.getDeclaredMethods());
for (Method method : methods) {
try {
if (isMockable(method)) {
CtMethod newMethod = CtMethod.make(getDelegateMethodSource(method), newClass);
if (!existingMethods.contains(newMethod)) {
newClass.addMethod(newMethod);
}
}
} catch (UnsupportedOperationException e) {
// Can't handle finals and statics.
} catch (CannotCompileException e) {
throw new RuntimeException("Internal Error while creating subclass methods for "
+ newClass.getName() + " method: " + method.getName(), e);
}
}
}
Method[] getAllMethods(Class<?> clazz) {
Map<String, Method> methodMap = getAllMethodsMap(clazz);
return methodMap.values().toArray(new Method[0]);
}
private Map<String, Method> getAllMethodsMap(Class<?> clazz) {
Map<String, Method> methodMap = new HashMap<String, Method>();
Class<?> superClass = clazz.getSuperclass();
if (superClass != null) {
methodMap.putAll(getAllMethodsMap(superClass));
}
List<Method> methods = new ArrayList<Method>(Arrays.asList(clazz.getDeclaredMethods()));
for (Method method : methods) {
String key = method.getName();
for (Class<?> param : method.getParameterTypes()) {
key += param.getCanonicalName();
}
methodMap.put(key, method);
}
return methodMap;
}
boolean isMockable(Method method) {
if (isForbiddenMethod(method)) {
return false;
}
int modifiers = method.getModifiers();
return !Modifier.isFinal(modifiers) && !Modifier.isStatic(modifiers) && !method.isBridge()
&& (Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers));
}
boolean isForbiddenMethod(Method method) {
if (method.getName().equals("equals")) {
return method.getParameterTypes().length == 1
&& method.getParameterTypes()[0].equals(Object.class);
} else if (method.getName().equals("toString")) {
return method.getParameterTypes().length == 0;
} else if (method.getName().equals("hashCode")) {
return method.getParameterTypes().length == 0;
}
return false;
}
private String getReturnDefault(Method method) {
Class<?> returnType = method.getReturnType();
if (!returnType.isPrimitive()) {
return "null";
} else if (returnType == Boolean.TYPE) {
return "false";
} else if (returnType == Void.TYPE) {
return "";
} else {
return "(" + returnType.getName() + ")0";
}
}
String getDelegateMethodSource(Method method) {
StringBuilder methodBody = getMethodSignature(method);
methodBody.append("{");
methodBody.append("if(this.");
methodBody.append(getDelegateFieldName());
methodBody.append("==null){return ");
methodBody.append(getReturnDefault(method));
methodBody.append(";}");
if (!method.getReturnType().equals(Void.TYPE)) {
methodBody.append("return ");
}
methodBody.append("this.");
methodBody.append(getDelegateFieldName());
methodBody.append(".");
methodBody.append(method.getName());
methodBody.append("(");
for (int i = 0; i < method.getParameterTypes().length; ++i) {
methodBody.append("arg");
methodBody.append(i);
if (i < method.getParameterTypes().length - 1) {
methodBody.append(",");
}
}
methodBody.append(");}");
return methodBody.toString();
}
CtClass generateSkeletalClass(Class<?> superClass, CtClass newInterface, SdkVersion sdkVersion)
throws ClassNotFoundException {
ClassPool classPool = getClassPool();
CtClass superCtClass = getCtClassForClass(superClass);
String subclassName = FileUtils.getSubclassNameFor(superClass, sdkVersion);
CtClass newClass;
try {
newClass = classPool.makeClass(subclassName, superCtClass);
} catch (RuntimeException e) {
if (e.getMessage().contains("frozen class")) {
try {
return classPool.get(subclassName);
} catch (NotFoundException ex) {
throw new ClassNotFoundException("Internal Error: could not find class", ex);
}
}
throw e;
}
try {
newClass.addField(new CtField(newInterface, getDelegateFieldName(), newClass));
} catch (CannotCompileException e) {
throw new RuntimeException("Internal error adding the delegate field to "
+ newClass.getName(), e);
}
return newClass;
}
}