1 // Copyright 2017 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.base.test.params;
6 
7 import org.junit.Test;
8 import org.junit.runner.Runner;
9 import org.junit.runners.BlockJUnit4ClassRunner;
10 import org.junit.runners.Suite;
11 import org.junit.runners.model.FrameworkField;
12 import org.junit.runners.model.TestClass;
13 
14 import org.chromium.base.test.params.ParameterAnnotations.ClassParameter;
15 import org.chromium.base.test.params.ParameterAnnotations.UseMethodParameter;
16 import org.chromium.base.test.params.ParameterAnnotations.UseRunnerDelegate;
17 import org.chromium.base.test.params.ParameterizedRunnerDelegateFactory.ParameterizedRunnerDelegateInstantiationException;
18 
19 import java.lang.reflect.Modifier;
20 import java.util.ArrayList;
21 import java.util.Arrays;
22 import java.util.Collections;
23 import java.util.List;
24 import java.util.Locale;
25 
26 /**
27  * ParameterizedRunner generates a list of runners for each of class parameter set in a test class.
28  *
29  * ParameterizedRunner looks for {@code @ClassParameter} annotation in test class and
30  * generates a list of ParameterizedRunnerDelegate runners for each ParameterSet.
31  */
32 public final class ParameterizedRunner extends Suite {
33     private final List<Runner> mRunners;
34 
35     /**
36      * Create a ParameterizedRunner to run test class
37      *
38      * @param klass the Class of the test class, test class should be atomic
39      *              (extends only Object)
40      */
ParameterizedRunner(Class<?> klass)41     public ParameterizedRunner(Class<?> klass) throws Throwable {
42         super(klass, Collections.emptyList()); // pass in empty list of runners
43         validate();
44         mRunners = createRunners(getTestClass());
45     }
46 
47     @Override
getChildren()48     protected List<Runner> getChildren() {
49         return mRunners;
50     }
51 
52     /**
53      * ParentRunner calls collectInitializationErrors() to check for errors in Test class.
54      * Parameterized tests are written in unconventional ways, therefore, this method is
55      * overridden and validation is done seperately.
56      */
57     @Override
collectInitializationErrors(List<Throwable> errors)58     protected void collectInitializationErrors(List<Throwable> errors) {
59         // Do not call super collectInitializationErrors
60     }
61 
validate()62     private void validate() throws Throwable {
63         validateNoNonStaticInnerClass();
64         validateOnlyOneConstructor();
65         validateInstanceMethods();
66         validateOnlyOneClassParameterField();
67         validateAtLeastOneParameterSetField();
68     }
69 
validateNoNonStaticInnerClass()70     private void validateNoNonStaticInnerClass() throws Exception {
71         if (getTestClass().isANonStaticInnerClass()) {
72             throw new Exception("The inner class " + getTestClass().getName() + " is not static.");
73         }
74     }
75 
validateOnlyOneConstructor()76     private void validateOnlyOneConstructor() throws Exception {
77         if (!hasOneConstructor()) {
78             throw new Exception("Test class should have exactly one public constructor");
79         }
80     }
81 
hasOneConstructor()82     private boolean hasOneConstructor() {
83         return getTestClass().getJavaClass().getConstructors().length == 1;
84     }
85 
validateOnlyOneClassParameterField()86     private void validateOnlyOneClassParameterField() {
87         if (getTestClass().getAnnotatedFields(ClassParameter.class).size() > 1) {
88             throw new IllegalParameterArgumentException(String.format(Locale.getDefault(),
89                     "%s class has more than one @ClassParameter, only one is allowed",
90                     getTestClass().getName()));
91         }
92     }
93 
validateAtLeastOneParameterSetField()94     private void validateAtLeastOneParameterSetField() {
95         if (getTestClass().getAnnotatedFields(ClassParameter.class).isEmpty()
96                 && getTestClass().getAnnotatedMethods(UseMethodParameter.class).isEmpty()) {
97             throw new IllegalArgumentException(String.format(Locale.getDefault(),
98                     "%s has no field annotated with @ClassParameter or method annotated with"
99                             + "@UseMethodParameter; it should not use ParameterizedRunner",
100                     getTestClass().getName()));
101         }
102     }
103 
validateInstanceMethods()104     private void validateInstanceMethods() throws Exception {
105         if (getTestClass().getAnnotatedMethods(Test.class).size() == 0) {
106             throw new Exception("No runnable methods");
107         }
108     }
109 
110     /**
111      * Return a list of runner delegates through ParameterizedRunnerDelegateFactory.
112      *
113      * For class parameter set: each class can only have one list of class parameter sets.
114      * Each parameter set will be used to create one runner.
115      *
116      * For method parameter set: a single list method parameter sets is associated with
117      * a string tag, an immutable map of string to parameter set list will be created and
118      * passed into factory for each runner delegate to create multiple tests. Only one
119      * Runner will be created for a method that uses @UseMethodParameter, regardless of the
120      * number of ParameterSets in the associated list.
121      *
122      * @return a list of runners
123      * @throws ParameterizedRunnerDelegateInstantiationException if runner delegate can not
124      *         be instantiated with constructor reflectively
125      * @throws IllegalAccessError if the field in tests are not accessible
126      */
createRunners(TestClass testClass)127     static List<Runner> createRunners(TestClass testClass)
128             throws IllegalAccessException, ParameterizedRunnerDelegateInstantiationException {
129         List<ParameterSet> classParameterSetList;
130         if (testClass.getAnnotatedFields(ClassParameter.class).isEmpty()) {
131             classParameterSetList = new ArrayList<>();
132             classParameterSetList.add(null);
133         } else {
134             classParameterSetList = getParameterSetList(
135                     testClass.getAnnotatedFields(ClassParameter.class).get(0), testClass);
136             validateWidth(classParameterSetList);
137         }
138 
139         Class<? extends ParameterizedRunnerDelegate> runnerDelegateClass =
140                 getRunnerDelegateClass(testClass);
141         ParameterizedRunnerDelegateFactory factory = new ParameterizedRunnerDelegateFactory();
142         List<Runner> runnersForTestClass = new ArrayList<>();
143         for (ParameterSet classParameterSet : classParameterSetList) {
144             BlockJUnit4ClassRunner runner = (BlockJUnit4ClassRunner) factory.createRunner(
145                     testClass, classParameterSet, runnerDelegateClass);
146             runnersForTestClass.add(runner);
147         }
148         return runnersForTestClass;
149     }
150 
151     /**
152      * Return an unmodifiable list of ParameterSet through a FrameworkField
153      */
getParameterSetList(FrameworkField field, TestClass testClass)154     private static List<ParameterSet> getParameterSetList(FrameworkField field, TestClass testClass)
155             throws IllegalAccessException {
156         field.getField().setAccessible(true);
157         if (!Modifier.isStatic(field.getField().getModifiers())) {
158             throw new IllegalParameterArgumentException(String.format(Locale.getDefault(),
159                     "ParameterSetList fields must be static, this field %s in %s is not",
160                     field.getName(), testClass.getName()));
161         }
162         if (!(field.get(testClass.getJavaClass()) instanceof List)) {
163             throw new IllegalArgumentException(String.format(Locale.getDefault(),
164                     "Fields with @ClassParameter annotations must be an instance of List, "
165                             + "this field %s in %s is not list",
166                     field.getName(), testClass.getName()));
167         }
168         @SuppressWarnings("unchecked") // checked above
169         List<ParameterSet> result = (List<ParameterSet>) field.get(testClass.getJavaClass());
170         return Collections.unmodifiableList(result);
171     }
172 
validateWidth(Iterable<ParameterSet> parameterSetList)173     static void validateWidth(Iterable<ParameterSet> parameterSetList) {
174         int lastSize = -1;
175         for (ParameterSet set : parameterSetList) {
176             if (set.size() == 0) {
177                 throw new IllegalParameterArgumentException(
178                         "No parameter is added to method ParameterSet");
179             }
180             if (lastSize == -1 || set.size() == lastSize) {
181                 lastSize = set.size();
182             } else {
183                 throw new IllegalParameterArgumentException(String.format(Locale.getDefault(),
184                         "All ParameterSets in a list of ParameterSet must have equal"
185                                 + " length. The current ParameterSet (%s) contains %d parameters,"
186                                 + " while previous ParameterSet contains %d parameters",
187                         Arrays.toString(set.getValues().toArray()), set.size(), lastSize));
188             }
189         }
190     }
191 
192     /**
193      * Get the runner delegate class for the test class if {@code @UseRunnerDelegate} is used.
194      * The default runner delegate is BaseJUnit4RunnerDelegate.class
195      */
getRunnerDelegateClass( TestClass testClass)196     private static Class<? extends ParameterizedRunnerDelegate> getRunnerDelegateClass(
197             TestClass testClass) {
198         if (testClass.getAnnotation(UseRunnerDelegate.class) != null) {
199             return testClass.getAnnotation(UseRunnerDelegate.class).value();
200         }
201         return BaseJUnit4RunnerDelegate.class;
202     }
203 
204     static class IllegalParameterArgumentException extends IllegalArgumentException {
IllegalParameterArgumentException(String msg)205         IllegalParameterArgumentException(String msg) {
206             super(msg);
207         }
208     }
209 
210     public static class ParameterizedTestInstantiationException extends Exception {
ParameterizedTestInstantiationException( TestClass testClass, String parameterSetString, Exception e)211         ParameterizedTestInstantiationException(
212                 TestClass testClass, String parameterSetString, Exception e) {
213             super(String.format(
214                           "Test class %s can not be initiated, the provided parameters are %s,"
215                                   + " the required parameter types are %s",
216                           testClass.getJavaClass().toString(), parameterSetString,
217                           Arrays.toString(testClass.getOnlyConstructor().getParameterTypes())),
218                     e);
219         }
220     }
221 }
222