1package org.robolectric;
2
3import java.lang.annotation.ElementType;
4import java.lang.annotation.Retention;
5import java.lang.annotation.RetentionPolicy;
6import java.lang.annotation.Target;
7import java.lang.reflect.Constructor;
8import java.lang.reflect.Modifier;
9import java.text.MessageFormat;
10import java.util.ArrayList;
11import java.util.Collections;
12import java.util.List;
13import javax.annotation.Nonnull;
14import org.junit.Assert;
15import org.junit.runner.Runner;
16import org.junit.runners.Parameterized;
17import org.junit.runners.Suite;
18import org.junit.runners.model.FrameworkMethod;
19import org.junit.runners.model.InitializationError;
20import org.junit.runners.model.TestClass;
21import org.robolectric.internal.DeepCloner;
22import org.robolectric.internal.SandboxTestRunner;
23import org.robolectric.internal.SdkEnvironment;
24
25/**
26 * A Parameterized test runner for Robolectric. Copied from the {@link Parameterized} class, then modified the custom
27 * test runner to extend the {@link RobolectricTestRunner}. The {@link RobolectricTestRunner#getHelperTestRunner(Class)}
28 * is overridden in order to create instances of the test class with the appropriate parameters. Merged in the ability
29 * to name your tests through the {@link Parameters#name()} property.
30 */
31public final class ParameterizedRobolectricTestRunner extends Suite {
32
33  /**
34   * Annotation for a method which provides parameters to be injected into the test class constructor by
35   * {@code Parameterized}
36   */
37  @Retention(RetentionPolicy.RUNTIME)
38  @Target(ElementType.METHOD)
39  public @interface Parameters {
40
41    /**
42     * Optional pattern to derive the test's name from the parameters. Use numbers in braces to refer to the
43     * parameters or the additional data as follows:
44     *
45     * <pre>
46     * {index} - the current parameter index
47     * {0} - the first parameter value
48     * {1} - the second parameter value
49     * etc...
50     * </pre>
51     *
52     * Default value is "{index}" for compatibility with previous JUnit versions.
53     *
54     * @return {@link MessageFormat} pattern string, except the index placeholder.
55     * @see MessageFormat
56     */
57    String name() default "{index}";
58  }
59
60  private static class TestClassRunnerForParameters extends RobolectricTestRunner {
61
62    private final String name;
63    private Object[] parameters;
64
65    TestClassRunnerForParameters(Class<?> type, Object[] parameters, String name) throws InitializationError {
66      super(type);
67      this.parameters = parameters;
68      this.name = name;
69    }
70
71    private Object createTestInstance(Class bootstrappedClass) throws Exception {
72      Constructor<?>[] constructors = bootstrappedClass.getConstructors();
73      Assert.assertEquals(1, constructors.length);
74      return constructors[0].newInstance(computeParams());
75    }
76
77    private Object[] computeParams() throws Exception {
78      try {
79        return parameters;
80      } catch (ClassCastException e) {
81        throw new Exception(String.format("%s.%s() must return a Collection of arrays.",
82                                          getTestClass().getName(),
83                                          name));
84      }
85    }
86
87    @Override
88    protected String getName() {
89      return name;
90    }
91
92    @Override
93    protected String testName(final FrameworkMethod method) {
94      return method.getName() + getName();
95    }
96
97    @Override
98    protected void validateConstructor(List<Throwable> errors) {
99      validateOnlyOneConstructor(errors);
100    }
101
102    @Nonnull
103    @Override
104    protected SdkEnvironment getSandbox(FrameworkMethod method) {
105      SdkEnvironment sandbox = super.getSandbox(method);
106
107      DeepCloner deepCloner = new DeepCloner(sandbox.getRobolectricClassLoader());
108      parameters = deepCloner.clone(parameters);
109
110      return sandbox;
111    }
112
113    @Override
114    public String toString() {
115      return "TestClassRunnerForParameters " + name;
116    }
117
118    @Override
119    protected SandboxTestRunner.HelperTestRunner getHelperTestRunner(Class bootstrappedTestClass) {
120      try {
121        return new HelperTestRunner(bootstrappedTestClass) {
122          @Override
123          protected void validateConstructor(List<Throwable> errors) {
124            TestClassRunnerForParameters.this.validateOnlyOneConstructor(errors);
125          }
126
127          @Override
128          protected Object createTest() throws Exception {
129            return TestClassRunnerForParameters.this.createTestInstance(getTestClass().getJavaClass());
130          }
131
132          @Override
133          public String toString() {
134            return "HelperTestRunner for " + TestClassRunnerForParameters.this.toString();
135          }
136        };
137      } catch (InitializationError initializationError) {
138        throw new RuntimeException(initializationError);
139      }
140    }
141  }
142
143  private final ArrayList<Runner> runners = new ArrayList<>();
144
145  /*
146   * Only called reflectively. Do not use programmatically.
147   */
148  public ParameterizedRobolectricTestRunner(Class<?> klass) throws Throwable {
149    super(klass, Collections.<Runner>emptyList());
150    Parameters parameters = getParametersMethod().getAnnotation(Parameters.class);
151    List<Object[]> parametersList = getParametersList();
152    for (int i = 0; i < parametersList.size(); i++) {
153      Object[] parameterArray = parametersList.get(i);
154      runners.add(new TestClassRunnerForParameters(getTestClass().getJavaClass(),
155                                                   parameterArray,
156                                                   nameFor(parameters.name(), i, parameterArray)));
157    }
158  }
159
160  @Override
161  protected List<Runner> getChildren() {
162    return runners;
163  }
164
165  @SuppressWarnings("unchecked")
166  private List<Object[]> getParametersList() throws Throwable {
167    return (List<Object[]>) getParametersMethod().invokeExplosively(null);
168  }
169
170  private FrameworkMethod getParametersMethod() throws Exception {
171    TestClass testClass = getTestClass();
172    List<FrameworkMethod> methods = testClass.getAnnotatedMethods(Parameters.class);
173    for (FrameworkMethod each : methods) {
174      int modifiers = each.getMethod().getModifiers();
175      if (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers)) {
176        return each;
177      }
178    }
179
180    throw new Exception("No public static parameters method on class " + testClass.getName());
181  }
182
183  private static String nameFor(String namePattern, int index, Object[] parameters) {
184    String finalPattern = namePattern.replaceAll("\\{index\\}", Integer.toString(index));
185    String name = MessageFormat.format(finalPattern, parameters);
186    return "[" + name + "]";
187  }
188
189}
190