1 package org.robolectric; 2 3 import java.lang.annotation.Annotation; 4 import java.lang.annotation.ElementType; 5 import java.lang.annotation.Retention; 6 import java.lang.annotation.RetentionPolicy; 7 import java.lang.annotation.Target; 8 import java.lang.reflect.Constructor; 9 import java.lang.reflect.Field; 10 import java.lang.reflect.Method; 11 import java.lang.reflect.Modifier; 12 import java.text.MessageFormat; 13 import java.util.ArrayList; 14 import java.util.Collections; 15 import java.util.HashSet; 16 import java.util.List; 17 import java.util.Locale; 18 import org.junit.Assert; 19 import org.junit.runner.Runner; 20 import org.junit.runners.Parameterized; 21 import org.junit.runners.Suite; 22 import org.junit.runners.model.FrameworkField; 23 import org.junit.runners.model.FrameworkMethod; 24 import org.junit.runners.model.InitializationError; 25 import org.junit.runners.model.TestClass; 26 import org.robolectric.internal.SandboxTestRunner; 27 import org.robolectric.util.ReflectionHelpers; 28 29 /** 30 * A Parameterized test runner for Robolectric. Copied from the {@link Parameterized} class, then 31 * modified the custom test runner to extend the {@link RobolectricTestRunner}. The {@link 32 * org.robolectric.RobolectricTestRunner#getHelperTestRunner(Class)} is overridden in order to 33 * create instances of the test class with the appropriate parameters. Merged in the ability to name 34 * your tests through the {@link Parameters#name()} property. Merged in support for {@link 35 * Parameter} annotation alternative to providing a constructor. 36 * 37 * <p>This class takes care of the fact that the test runner and the test class are actually loaded 38 * from different class loaders and therefore parameter objects created by one cannot be assigned to 39 * instances of the other. 40 */ 41 public final class ParameterizedRobolectricTestRunner extends Suite { 42 43 /** 44 * Annotation for a method which provides parameters to be injected into the test class 45 * constructor by <code>Parameterized</code> 46 */ 47 @Retention(RetentionPolicy.RUNTIME) 48 @Target(ElementType.METHOD) 49 public @interface Parameters { 50 51 /** 52 * Optional pattern to derive the test's name from the parameters. Use numbers in braces to 53 * refer to the parameters or the additional data as follows: 54 * 55 * <pre> 56 * {index} - the current parameter index 57 * {0} - the first parameter value 58 * {1} - the second parameter value 59 * etc... 60 * </pre> 61 * 62 * <p>Default value is "{index}" for compatibility with previous JUnit versions. 63 * 64 * @return {@link MessageFormat} pattern string, except the index placeholder. 65 * @see MessageFormat 66 */ 67 String name() default "{index}"; 68 } 69 70 /** 71 * Annotation for fields of the test class which will be initialized by the method annotated by 72 * <code>Parameters</code><br> 73 * By using directly this annotation, the test class constructor isn't needed.<br> 74 * Index range must start at 0. Default value is 0. 75 */ 76 @Retention(RetentionPolicy.RUNTIME) 77 @Target(ElementType.FIELD) 78 public @interface Parameter { 79 /** 80 * Method that returns the index of the parameter in the array returned by the method annotated 81 * by <code>Parameters</code>.<br> 82 * Index range must start at 0. Default value is 0. 83 * 84 * @return the index of the parameter. 85 */ 86 int value() default 0; 87 } 88 89 private static class TestClassRunnerForParameters extends RobolectricTestRunner { 90 91 private final int parametersIndex; 92 private final String name; 93 94 TestClassRunnerForParameters(Class<?> type, int parametersIndex, String name) 95 throws InitializationError { 96 super(type); 97 this.parametersIndex = parametersIndex; 98 this.name = name; 99 } 100 101 private Object createTestInstance(Class bootstrappedClass) throws Exception { 102 Constructor<?>[] constructors = bootstrappedClass.getConstructors(); 103 Assert.assertEquals(1, constructors.length); 104 if (!fieldsAreAnnotated()) { 105 return constructors[0].newInstance(computeParams(bootstrappedClass.getClassLoader())); 106 } else { 107 Object instance = constructors[0].newInstance(); 108 injectParametersIntoFields(instance, bootstrappedClass.getClassLoader()); 109 return instance; 110 } 111 } 112 113 private Object[] computeParams(ClassLoader classLoader) throws Exception { 114 // Robolectric uses a different class loader when running the tests, so the parameters objects 115 // created by the test runner are not compatible with the parameters required by the test. 116 // Instead, we compute the parameters within the test's class loader. 117 try { 118 List<Object[]> parametersList = getParametersList(getTestClass(), classLoader); 119 if (parametersIndex >= parametersList.size()) { 120 throw new Exception( 121 "Re-computing the parameter list returned a different number of " 122 + "parameters values. Is the data() method of your test non-deterministic?"); 123 } 124 return parametersList.get(parametersIndex); 125 } catch (ClassCastException e) { 126 throw new Exception( 127 String.format( 128 "%s.%s() must return a Collection of arrays.", getTestClass().getName(), name)); 129 } catch (Exception exception) { 130 throw exception; 131 } catch (Throwable throwable) { 132 throw new Exception(throwable); 133 } 134 } 135 136 @SuppressWarnings("unchecked") 137 private void injectParametersIntoFields(Object testClassInstance, ClassLoader classLoader) 138 throws Exception { 139 // Robolectric uses a different class loader when running the tests, so referencing Parameter 140 // directly causes type mismatches. Instead, we find its class within the test's class loader. 141 Class<?> parameterClass = getClassInClassLoader(Parameter.class, classLoader); 142 Object[] parameters = computeParams(classLoader); 143 HashSet<Integer> parameterFieldsFound = new HashSet<>(); 144 for (Field field : testClassInstance.getClass().getFields()) { 145 Annotation parameter = field.getAnnotation((Class<Annotation>) parameterClass); 146 if (parameter != null) { 147 int index = ReflectionHelpers.callInstanceMethod(parameter, "value"); 148 parameterFieldsFound.add(index); 149 try { 150 field.set(testClassInstance, parameters[index]); 151 } catch (IllegalArgumentException iare) { 152 throw new Exception( 153 getTestClass().getName() 154 + ": Trying to set " 155 + field.getName() 156 + " with the value " 157 + parameters[index] 158 + " that is not the right type (" 159 + parameters[index].getClass().getSimpleName() 160 + " instead of " 161 + field.getType().getSimpleName() 162 + ").", 163 iare); 164 } 165 } 166 } 167 if (parameterFieldsFound.size() != parameters.length) { 168 throw new IllegalStateException( 169 String.format( 170 Locale.US, 171 "Provided %d parameters, but only found fields for parameters: %s", 172 parameters.length, 173 parameterFieldsFound.toString())); 174 } 175 } 176 177 @Override 178 protected String getName() { 179 return name; 180 } 181 182 @Override 183 protected String testName(final FrameworkMethod method) { 184 return method.getName() + getName(); 185 } 186 187 @Override 188 protected void validateConstructor(List<Throwable> errors) { 189 validateOnlyOneConstructor(errors); 190 if (fieldsAreAnnotated()) { 191 validateZeroArgConstructor(errors); 192 } 193 } 194 195 @Override 196 public String toString() { 197 return "TestClassRunnerForParameters " + name; 198 } 199 200 @Override 201 protected void validateFields(List<Throwable> errors) { 202 super.validateFields(errors); 203 // Ensure that indexes for parameters are correctly defined 204 if (fieldsAreAnnotated()) { 205 List<FrameworkField> annotatedFieldsByParameter = getAnnotatedFieldsByParameter(); 206 int[] usedIndices = new int[annotatedFieldsByParameter.size()]; 207 for (FrameworkField each : annotatedFieldsByParameter) { 208 int index = each.getField().getAnnotation(Parameter.class).value(); 209 if (index < 0 || index > annotatedFieldsByParameter.size() - 1) { 210 errors.add( 211 new Exception( 212 "Invalid @Parameter value: " 213 + index 214 + ". @Parameter fields counted: " 215 + annotatedFieldsByParameter.size() 216 + ". Please use an index between 0 and " 217 + (annotatedFieldsByParameter.size() - 1) 218 + ".")); 219 } else { 220 usedIndices[index]++; 221 } 222 } 223 for (int index = 0; index < usedIndices.length; index++) { 224 int numberOfUse = usedIndices[index]; 225 if (numberOfUse == 0) { 226 errors.add(new Exception("@Parameter(" + index + ") is never used.")); 227 } else if (numberOfUse > 1) { 228 errors.add( 229 new Exception( 230 "@Parameter(" + index + ") is used more than once (" + numberOfUse + ").")); 231 } 232 } 233 } 234 } 235 236 @Override 237 protected SandboxTestRunner.HelperTestRunner getHelperTestRunner(Class bootstrappedTestClass) { 238 try { 239 return new HelperTestRunner(bootstrappedTestClass) { 240 @Override 241 protected void validateConstructor(List<Throwable> errors) { 242 TestClassRunnerForParameters.this.validateOnlyOneConstructor(errors); 243 } 244 245 @Override 246 protected Object createTest() throws Exception { 247 return TestClassRunnerForParameters.this.createTestInstance( 248 getTestClass().getJavaClass()); 249 } 250 251 @Override 252 public String toString() { 253 return "HelperTestRunner for " + TestClassRunnerForParameters.this.toString(); 254 } 255 }; 256 } catch (InitializationError initializationError) { 257 throw new RuntimeException(initializationError); 258 } 259 } 260 261 private List<FrameworkField> getAnnotatedFieldsByParameter() { 262 return getTestClass().getAnnotatedFields(Parameter.class); 263 } 264 265 private boolean fieldsAreAnnotated() { 266 return !getAnnotatedFieldsByParameter().isEmpty(); 267 } 268 } 269 270 private final ArrayList<Runner> runners = new ArrayList<>(); 271 272 /* 273 * Only called reflectively. Do not use programmatically. 274 */ 275 public ParameterizedRobolectricTestRunner(Class<?> klass) throws Throwable { 276 super(klass, Collections.<Runner>emptyList()); 277 TestClass testClass = getTestClass(); 278 ClassLoader classLoader = getClass().getClassLoader(); 279 Parameters parameters = 280 getParametersMethod(testClass, classLoader).getAnnotation(Parameters.class); 281 List<Object[]> parametersList = getParametersList(testClass, classLoader); 282 for (int i = 0; i < parametersList.size(); i++) { 283 Object[] parameterArray = parametersList.get(i); 284 runners.add( 285 new TestClassRunnerForParameters( 286 testClass.getJavaClass(), i, nameFor(parameters.name(), i, parameterArray))); 287 } 288 } 289 290 @Override 291 protected List<Runner> getChildren() { 292 return runners; 293 } 294 295 @SuppressWarnings("unchecked") 296 private static List<Object[]> getParametersList(TestClass testClass, ClassLoader classLoader) 297 throws Throwable { 298 return (List<Object[]>) getParametersMethod(testClass, classLoader).invokeExplosively(null); 299 } 300 301 private static FrameworkMethod getParametersMethod(TestClass testClass, ClassLoader classLoader) 302 throws Exception { 303 List<FrameworkMethod> methods = testClass.getAnnotatedMethods(Parameters.class); 304 for (FrameworkMethod each : methods) { 305 int modifiers = each.getMethod().getModifiers(); 306 if (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers)) { 307 return getFrameworkMethodInClassLoader(each, classLoader); 308 } 309 } 310 311 throw new Exception("No public static parameters method on class " + testClass.getName()); 312 } 313 314 private static String nameFor(String namePattern, int index, Object[] parameters) { 315 String finalPattern = namePattern.replaceAll("\\{index\\}", Integer.toString(index)); 316 String name = MessageFormat.format(finalPattern, parameters); 317 return "[" + name + "]"; 318 } 319 320 /** 321 * Returns the {@link FrameworkMethod} object for the given method in the provided class loader. 322 */ 323 private static FrameworkMethod getFrameworkMethodInClassLoader( 324 FrameworkMethod method, ClassLoader classLoader) 325 throws ClassNotFoundException, NoSuchMethodException { 326 Method methodInClassLoader = getMethodInClassLoader(method.getMethod(), classLoader); 327 if (methodInClassLoader.equals(method.getMethod())) { 328 // The method was already loaded in the right class loader, return it as is. 329 return method; 330 } 331 return new FrameworkMethod(methodInClassLoader); 332 } 333 334 /** Returns the {@link Method} object for the given method in the provided class loader. */ 335 private static Method getMethodInClassLoader(Method method, ClassLoader classLoader) 336 throws ClassNotFoundException, NoSuchMethodException { 337 Class<?> declaringClass = method.getDeclaringClass(); 338 339 if (declaringClass.getClassLoader() == classLoader) { 340 // The method was already loaded in the right class loader, return it as is. 341 return method; 342 } 343 344 // Find the class in the class loader corresponding to the declaring class of the method. 345 Class<?> declaringClassInClassLoader = getClassInClassLoader(declaringClass, classLoader); 346 347 // Find the method with the same signature in the class loader. 348 return declaringClassInClassLoader.getMethod(method.getName(), method.getParameterTypes()); 349 } 350 351 /** Returns the {@link Class} object for the given class in the provided class loader. */ 352 private static Class<?> getClassInClassLoader(Class<?> klass, ClassLoader classLoader) 353 throws ClassNotFoundException { 354 if (klass.getClassLoader() == classLoader) { 355 // The method was already loaded in the right class loader, return it as is. 356 return klass; 357 } 358 359 // Find the class in the class loader corresponding to the declaring class of the method. 360 return classLoader.loadClass(klass.getName()); 361 } 362 } 363