diff options
Diffstat (limited to 'src/com/google/android/testing/mocking/AndroidMockGenerator.java')
-rw-r--r-- | src/com/google/android/testing/mocking/AndroidMockGenerator.java | 480 |
1 files changed, 480 insertions, 0 deletions
diff --git a/src/com/google/android/testing/mocking/AndroidMockGenerator.java b/src/com/google/android/testing/mocking/AndroidMockGenerator.java new file mode 100644 index 0000000..1a557a6 --- /dev/null +++ b/src/com/google/android/testing/mocking/AndroidMockGenerator.java @@ -0,0 +1,480 @@ +/* + * 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; + } +} |