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