/* * 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 createMocksForClass(Class clazz) throws ClassNotFoundException, IOException, CannotCompileException { return this.createMocksForClass(clazz, SdkVersion.UNKNOWN); } public List 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 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 methodMap = getAllMethodsMap(clazz); return methodMap.values().toArray(new Method[0]); } private Map getAllMethodsMap(Class clazz) { Map methodMap = new HashMap(); Class superClass = clazz.getSuperclass(); if (superClass != null) { methodMap.putAll(getAllMethodsMap(superClass)); } List methods = new ArrayList(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; } }