1 /* 2 * Copyright 2010 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.android.testing.mocking; 17 18 import javassist.CannotCompileException; 19 import javassist.ClassClassPath; 20 import javassist.ClassPool; 21 import javassist.CtClass; 22 import javassist.CtConstructor; 23 import javassist.CtField; 24 import javassist.CtMethod; 25 import javassist.CtNewConstructor; 26 import javassist.NotFoundException; 27 28 import java.io.IOException; 29 import java.lang.reflect.Constructor; 30 import java.lang.reflect.Method; 31 import java.lang.reflect.Modifier; 32 import java.util.ArrayList; 33 import java.util.Arrays; 34 import java.util.HashMap; 35 import java.util.List; 36 import java.util.Map; 37 38 39 /** 40 * AndroidMockGenerator creates the subclass and interface required for mocking 41 * a given Class. 42 * 43 * The only public method of AndroidMockGenerator is createMocksForClass. See 44 * the javadocs for this method for more information about AndroidMockGenerator. 45 * 46 * @author swoodward@google.com (Stephen Woodward) 47 */ 48 class AndroidMockGenerator { AndroidMockGenerator()49 public AndroidMockGenerator() { 50 ClassPool.doPruning = false; 51 ClassPool.getDefault().insertClassPath(new ClassClassPath(MockObject.class)); 52 } 53 54 /** 55 * Creates a List of javassist.CtClass objects representing all of the 56 * interfaces and subclasses required to meet the Mocking requests of the 57 * Class specified by {@code clazz}. 58 * 59 * A test class can request that a Class be prepared for mocking by using the 60 * {@link UsesMocks} annotation at either the Class or Method level. All 61 * classes specified by these annotations will have exactly two CtClass 62 * objects created, one for a generated interface, and one for a generated 63 * subclass. The interface and subclass both define the same methods which 64 * comprise all of the mockable methods of the provided class. At present, for 65 * a method to be mockable, it must be non-final and non-static, although this 66 * may expand in the future. 67 * 68 * The class itself must be mockable, otherwise this method will ignore the 69 * requested mock and print a warning. At present, a class is mockable if it 70 * is a non-final publicly-instantiable Java class that is assignable from the 71 * java.lang.Object class. See the javadocs for 72 * {@link java.lang.Class#isAssignableFrom(Class)} for more information about 73 * what "is assignable from the Object class" means. As a non-exhaustive 74 * example, if a given Class represents an Enum, Annotation, Primitive or 75 * Array, then it is not assignable from Object. Interfaces are also ignored 76 * since these need no modifications in order to be mocked. 77 * 78 * @param clazz the Class object to have all of its UsesMocks annotations 79 * processed and the corresponding Mock Classes created. 80 * @return a List of CtClass objects representing the Classes and Interfaces 81 * required for mocking the classes requested by {@code clazz} 82 * @throws ClassNotFoundException 83 * @throws CannotCompileException 84 * @throws IOException 85 */ createMocksForClass(Class<?> clazz)86 public List<GeneratedClassFile> createMocksForClass(Class<?> clazz) 87 throws ClassNotFoundException, IOException, CannotCompileException { 88 return this.createMocksForClass(clazz, SdkVersion.UNKNOWN); 89 } 90 createMocksForClass(Class<?> clazz, SdkVersion sdkVersion)91 public List<GeneratedClassFile> createMocksForClass(Class<?> clazz, SdkVersion sdkVersion) 92 throws ClassNotFoundException, IOException, CannotCompileException { 93 if (!classIsSupportedType(clazz)) { 94 reportReasonForUnsupportedType(clazz); 95 return Arrays.asList(new GeneratedClassFile[0]); 96 } 97 CtClass newInterfaceCtClass = generateInterface(clazz, sdkVersion); 98 GeneratedClassFile newInterface = new GeneratedClassFile(newInterfaceCtClass.getName(), 99 newInterfaceCtClass.toBytecode()); 100 CtClass mockDelegateCtClass = generateSubClass(clazz, newInterfaceCtClass, sdkVersion); 101 GeneratedClassFile mockDelegate = new GeneratedClassFile(mockDelegateCtClass.getName(), 102 mockDelegateCtClass.toBytecode()); 103 return Arrays.asList(new GeneratedClassFile[] {newInterface, mockDelegate}); 104 } 105 reportReasonForUnsupportedType(Class<?> clazz)106 private void reportReasonForUnsupportedType(Class<?> clazz) { 107 String reason = null; 108 if (clazz.isInterface()) { 109 // do nothing to make sure none of the other conditions apply. 110 } else if (clazz.isEnum()) { 111 reason = "Cannot mock an Enum"; 112 } else if (clazz.isAnnotation()) { 113 reason = "Cannot mock an Annotation"; 114 } else if (clazz.isArray()) { 115 reason = "Cannot mock an Array"; 116 } else if (Modifier.isFinal(clazz.getModifiers())) { 117 reason = "Cannot mock a Final class"; 118 } else if (clazz.isPrimitive()) { 119 reason = "Cannot mock primitives"; 120 } else if (!Object.class.isAssignableFrom(clazz)) { 121 reason = "Cannot mock non-classes"; 122 } else if (!containsUsableConstructor(clazz)) { 123 reason = "Cannot mock a class with no public constructors"; 124 } else { 125 // Whatever the reason is, it's not one that we care about. 126 } 127 if (reason != null) { 128 // Sometimes we want to be silent, so check 'reason' against null. 129 System.err.println(reason + ": " + clazz.getName()); 130 } 131 } 132 containsUsableConstructor(Class<?> clazz)133 private boolean containsUsableConstructor(Class<?> clazz) { 134 Constructor<?>[] constructors = clazz.getDeclaredConstructors(); 135 for (Constructor<?> constructor : constructors) { 136 if (Modifier.isPublic(constructor.getModifiers()) || 137 Modifier.isProtected(constructor.getModifiers())) { 138 return true; 139 } 140 } 141 return false; 142 } 143 classIsSupportedType(Class<?> clazz)144 boolean classIsSupportedType(Class<?> clazz) { 145 return (containsUsableConstructor(clazz)) && Object.class.isAssignableFrom(clazz) 146 && !clazz.isInterface() && !clazz.isEnum() && !clazz.isAnnotation() && !clazz.isArray() 147 && !Modifier.isFinal(clazz.getModifiers()); 148 } 149 saveCtClass(CtClass clazz)150 void saveCtClass(CtClass clazz) throws ClassNotFoundException, IOException { 151 try { 152 clazz.writeFile(); 153 } catch (NotFoundException e) { 154 throw new ClassNotFoundException("Error while saving modified class " + clazz.getName(), e); 155 } catch (CannotCompileException e) { 156 throw new RuntimeException("Internal Error: Attempt to save syntactically incorrect code " 157 + "for class " + clazz.getName(), e); 158 } 159 } 160 generateInterface(Class<?> originalClass, SdkVersion sdkVersion)161 CtClass generateInterface(Class<?> originalClass, SdkVersion sdkVersion) { 162 ClassPool classPool = getClassPool(); 163 try { 164 return classPool.getCtClass(FileUtils.getInterfaceNameFor(originalClass, sdkVersion)); 165 } catch (NotFoundException e) { 166 CtClass newInterface = 167 classPool.makeInterface(FileUtils.getInterfaceNameFor(originalClass, sdkVersion)); 168 addInterfaceMethods(originalClass, newInterface); 169 return newInterface; 170 } 171 } 172 getInterfaceMethodSource(Method method)173 String getInterfaceMethodSource(Method method) throws UnsupportedOperationException { 174 StringBuilder methodBody = getMethodSignature(method); 175 methodBody.append(";"); 176 return methodBody.toString(); 177 } 178 getMethodSignature(Method method)179 private StringBuilder getMethodSignature(Method method) { 180 int modifiers = method.getModifiers(); 181 if (Modifier.isFinal(modifiers) || Modifier.isStatic(modifiers)) { 182 throw new UnsupportedOperationException( 183 "Cannot specify final or static methods in an interface"); 184 } 185 StringBuilder methodSignature = new StringBuilder("public "); 186 methodSignature.append(getClassName(method.getReturnType())); 187 methodSignature.append(" "); 188 methodSignature.append(method.getName()); 189 methodSignature.append("("); 190 int i = 0; 191 for (Class<?> arg : method.getParameterTypes()) { 192 methodSignature.append(getClassName(arg)); 193 methodSignature.append(" arg"); 194 methodSignature.append(i); 195 if (i < method.getParameterTypes().length - 1) { 196 methodSignature.append(","); 197 } 198 i++; 199 } 200 methodSignature.append(")"); 201 if (method.getExceptionTypes().length > 0) { 202 methodSignature.append(" throws "); 203 } 204 i = 0; 205 for (Class<?> exception : method.getExceptionTypes()) { 206 methodSignature.append(getClassName(exception)); 207 if (i < method.getExceptionTypes().length - 1) { 208 methodSignature.append(","); 209 } 210 i++; 211 } 212 return methodSignature; 213 } 214 getClassName(Class<?> clazz)215 private String getClassName(Class<?> clazz) { 216 return clazz.getCanonicalName(); 217 } 218 getClassPool()219 static ClassPool getClassPool() { 220 return ClassPool.getDefault(); 221 } 222 classExists(String name)223 private boolean classExists(String name) { 224 // The following line is the ideal, but doesn't work (bug in library). 225 // return getClassPool().find(name) != null; 226 try { 227 getClassPool().get(name); 228 return true; 229 } catch (NotFoundException e) { 230 return false; 231 } 232 } 233 generateSubClass(Class<?> superClass, CtClass newInterface, SdkVersion sdkVersion)234 CtClass generateSubClass(Class<?> superClass, CtClass newInterface, SdkVersion sdkVersion) 235 throws ClassNotFoundException { 236 if (classExists(FileUtils.getSubclassNameFor(superClass, sdkVersion))) { 237 try { 238 return getClassPool().get(FileUtils.getSubclassNameFor(superClass, sdkVersion)); 239 } catch (NotFoundException e) { 240 throw new ClassNotFoundException("This should be impossible, since we just checked for " 241 + "the existence of the class being created", e); 242 } 243 } 244 CtClass newClass = generateSkeletalClass(superClass, newInterface, sdkVersion); 245 if (!newClass.isFrozen()) { 246 newClass.addInterface(newInterface); 247 try { 248 newClass.addInterface(getClassPool().get(MockObject.class.getName())); 249 } catch (NotFoundException e) { 250 throw new ClassNotFoundException("Could not find " + MockObject.class.getName(), e); 251 } 252 addMethods(superClass, newClass); 253 addGetDelegateMethod(newClass); 254 addSetDelegateMethod(newClass, newInterface); 255 addConstructors(newClass, superClass); 256 } 257 return newClass; 258 } 259 addConstructors(CtClass clazz, Class<?> superClass)260 private void addConstructors(CtClass clazz, Class<?> superClass) throws ClassNotFoundException { 261 CtClass superCtClass = getCtClassForClass(superClass); 262 263 CtConstructor[] constructors = superCtClass.getDeclaredConstructors(); 264 for (CtConstructor constructor : constructors) { 265 int modifiers = constructor.getModifiers(); 266 if (Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers)) { 267 CtConstructor ctConstructor; 268 try { 269 ctConstructor = CtNewConstructor.make(constructor.getParameterTypes(), 270 constructor.getExceptionTypes(), clazz); 271 clazz.addConstructor(ctConstructor); 272 } catch (CannotCompileException e) { 273 throw new RuntimeException("Internal Error - Could not add constructors.", e); 274 } catch (NotFoundException e) { 275 throw new RuntimeException("Internal Error - Constructor suddenly could not be found", e); 276 } 277 } 278 } 279 } 280 getCtClassForClass(Class<?> clazz)281 CtClass getCtClassForClass(Class<?> clazz) throws ClassNotFoundException { 282 ClassPool classPool = getClassPool(); 283 try { 284 return classPool.get(clazz.getName()); 285 } catch (NotFoundException e) { 286 throw new ClassNotFoundException("Class not found when finding the class to be mocked: " 287 + clazz.getName(), e); 288 } 289 } 290 addSetDelegateMethod(CtClass clazz, CtClass newInterface)291 private void addSetDelegateMethod(CtClass clazz, CtClass newInterface) { 292 try { 293 clazz.addMethod(CtMethod.make(getSetDelegateMethodSource(newInterface), clazz)); 294 } catch (CannotCompileException e) { 295 throw new RuntimeException("Internal error while creating the setDelegate() method", e); 296 } 297 } 298 getSetDelegateMethodSource(CtClass newInterface)299 String getSetDelegateMethodSource(CtClass newInterface) { 300 return "public void setDelegate___AndroidMock(" + newInterface.getName() + " obj) { this." 301 + getDelegateFieldName() + " = obj;}"; 302 } 303 addGetDelegateMethod(CtClass clazz)304 private void addGetDelegateMethod(CtClass clazz) { 305 try { 306 CtMethod newMethod = CtMethod.make(getGetDelegateMethodSource(), clazz); 307 try { 308 CtMethod existingMethod = clazz.getMethod(newMethod.getName(), newMethod.getSignature()); 309 clazz.removeMethod(existingMethod); 310 } catch (NotFoundException e) { 311 // expected path... sigh. 312 } 313 clazz.addMethod(newMethod); 314 } catch (CannotCompileException e) { 315 throw new RuntimeException("Internal error while creating the getDelegate() method", e); 316 } 317 } 318 getGetDelegateMethodSource()319 private String getGetDelegateMethodSource() { 320 return "public Object getDelegate___AndroidMock() { return this." + getDelegateFieldName() 321 + "; }"; 322 } 323 getDelegateFieldName()324 String getDelegateFieldName() { 325 return "delegateMockObject"; 326 } 327 addInterfaceMethods(Class<?> originalClass, CtClass newInterface)328 void addInterfaceMethods(Class<?> originalClass, CtClass newInterface) { 329 Method[] methods = getAllMethods(originalClass); 330 for (Method method : methods) { 331 try { 332 if (isMockable(method)) { 333 CtMethod newMethod = CtMethod.make(getInterfaceMethodSource(method), newInterface); 334 newInterface.addMethod(newMethod); 335 } 336 } catch (UnsupportedOperationException e) { 337 // Can't handle finals and statics. 338 } catch (CannotCompileException e) { 339 throw new RuntimeException( 340 "Internal error while creating a new Interface method for class " 341 + originalClass.getName() + ". Method name: " + method.getName(), e); 342 } 343 } 344 } 345 addMethods(Class<?> superClass, CtClass newClass)346 void addMethods(Class<?> superClass, CtClass newClass) { 347 Method[] methods = getAllMethods(superClass); 348 if (newClass.isFrozen()) { 349 newClass.defrost(); 350 } 351 List<CtMethod> existingMethods = Arrays.asList(newClass.getDeclaredMethods()); 352 for (Method method : methods) { 353 try { 354 if (isMockable(method)) { 355 CtMethod newMethod = CtMethod.make(getDelegateMethodSource(method), newClass); 356 if (!existingMethods.contains(newMethod)) { 357 newClass.addMethod(newMethod); 358 } 359 } 360 } catch (UnsupportedOperationException e) { 361 // Can't handle finals and statics. 362 } catch (CannotCompileException e) { 363 throw new RuntimeException("Internal Error while creating subclass methods for " 364 + newClass.getName() + " method: " + method.getName(), e); 365 } 366 } 367 } 368 getAllMethods(Class<?> clazz)369 Method[] getAllMethods(Class<?> clazz) { 370 Map<String, Method> methodMap = getAllMethodsMap(clazz); 371 return methodMap.values().toArray(new Method[0]); 372 } 373 getAllMethodsMap(Class<?> clazz)374 private Map<String, Method> getAllMethodsMap(Class<?> clazz) { 375 Map<String, Method> methodMap = new HashMap<String, Method>(); 376 Class<?> superClass = clazz.getSuperclass(); 377 if (superClass != null) { 378 methodMap.putAll(getAllMethodsMap(superClass)); 379 } 380 List<Method> methods = new ArrayList<Method>(Arrays.asList(clazz.getDeclaredMethods())); 381 for (Method method : methods) { 382 String key = method.getName(); 383 for (Class<?> param : method.getParameterTypes()) { 384 key += param.getCanonicalName(); 385 } 386 methodMap.put(key, method); 387 } 388 return methodMap; 389 } 390 isMockable(Method method)391 boolean isMockable(Method method) { 392 if (isForbiddenMethod(method)) { 393 return false; 394 } 395 int modifiers = method.getModifiers(); 396 return !Modifier.isFinal(modifiers) && !Modifier.isStatic(modifiers) && !method.isBridge() 397 && (Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers)); 398 } 399 isForbiddenMethod(Method method)400 boolean isForbiddenMethod(Method method) { 401 if (method.getName().equals("equals")) { 402 return method.getParameterTypes().length == 1 403 && method.getParameterTypes()[0].equals(Object.class); 404 } else if (method.getName().equals("toString")) { 405 return method.getParameterTypes().length == 0; 406 } else if (method.getName().equals("hashCode")) { 407 return method.getParameterTypes().length == 0; 408 } 409 return false; 410 } 411 getReturnDefault(Method method)412 private String getReturnDefault(Method method) { 413 Class<?> returnType = method.getReturnType(); 414 if (!returnType.isPrimitive()) { 415 return "null"; 416 } else if (returnType == Boolean.TYPE) { 417 return "false"; 418 } else if (returnType == Void.TYPE) { 419 return ""; 420 } else { 421 return "(" + returnType.getName() + ")0"; 422 } 423 } 424 getDelegateMethodSource(Method method)425 String getDelegateMethodSource(Method method) { 426 StringBuilder methodBody = getMethodSignature(method); 427 methodBody.append("{"); 428 methodBody.append("if(this."); 429 methodBody.append(getDelegateFieldName()); 430 methodBody.append("==null){return "); 431 methodBody.append(getReturnDefault(method)); 432 methodBody.append(";}"); 433 if (!method.getReturnType().equals(Void.TYPE)) { 434 methodBody.append("return "); 435 } 436 methodBody.append("this."); 437 methodBody.append(getDelegateFieldName()); 438 methodBody.append("."); 439 methodBody.append(method.getName()); 440 methodBody.append("("); 441 for (int i = 0; i < method.getParameterTypes().length; ++i) { 442 methodBody.append("arg"); 443 methodBody.append(i); 444 if (i < method.getParameterTypes().length - 1) { 445 methodBody.append(","); 446 } 447 } 448 methodBody.append(");}"); 449 return methodBody.toString(); 450 } 451 generateSkeletalClass(Class<?> superClass, CtClass newInterface, SdkVersion sdkVersion)452 CtClass generateSkeletalClass(Class<?> superClass, CtClass newInterface, SdkVersion sdkVersion) 453 throws ClassNotFoundException { 454 ClassPool classPool = getClassPool(); 455 CtClass superCtClass = getCtClassForClass(superClass); 456 String subclassName = FileUtils.getSubclassNameFor(superClass, sdkVersion); 457 458 CtClass newClass; 459 try { 460 newClass = classPool.makeClass(subclassName, superCtClass); 461 } catch (RuntimeException e) { 462 if (e.getMessage().contains("frozen class")) { 463 try { 464 return classPool.get(subclassName); 465 } catch (NotFoundException ex) { 466 throw new ClassNotFoundException("Internal Error: could not find class", ex); 467 } 468 } 469 throw e; 470 } 471 472 try { 473 newClass.addField(new CtField(newInterface, getDelegateFieldName(), newClass)); 474 } catch (CannotCompileException e) { 475 throw new RuntimeException("Internal error adding the delegate field to " 476 + newClass.getName(), e); 477 } 478 return newClass; 479 } 480 } 481