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