• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
name()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      */
value()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 
TestClassRunnerForParameters(Class<?> type, int parametersIndex, String name)94     TestClassRunnerForParameters(Class<?> type, int parametersIndex, String name)
95         throws InitializationError {
96       super(type);
97       this.parametersIndex = parametersIndex;
98       this.name = name;
99     }
100 
createTestInstance(Class bootstrappedClass)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 
computeParams(ClassLoader classLoader)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")
injectParametersIntoFields(Object testClassInstance, ClassLoader classLoader)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
getName()178     protected String getName() {
179       return name;
180     }
181 
182     @Override
testName(final FrameworkMethod method)183     protected String testName(final FrameworkMethod method) {
184       return method.getName() + getName();
185     }
186 
187     @Override
validateConstructor(List<Throwable> errors)188     protected void validateConstructor(List<Throwable> errors) {
189       validateOnlyOneConstructor(errors);
190       if (fieldsAreAnnotated()) {
191         validateZeroArgConstructor(errors);
192       }
193     }
194 
195     @Override
toString()196     public String toString() {
197       return "TestClassRunnerForParameters " + name;
198     }
199 
200     @Override
validateFields(List<Throwable> errors)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
getHelperTestRunner(Class bootstrappedTestClass)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 
getAnnotatedFieldsByParameter()261     private List<FrameworkField> getAnnotatedFieldsByParameter() {
262       return getTestClass().getAnnotatedFields(Parameter.class);
263     }
264 
fieldsAreAnnotated()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