summaryrefslogtreecommitdiff
path: root/base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedRunner.java
blob: 834f26139fd115ffd3575733244347bef3df4321 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.base.test.params;

import org.junit.Test;
import org.junit.runner.Runner;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.Suite;
import org.junit.runners.model.FrameworkField;
import org.junit.runners.model.TestClass;

import org.chromium.base.test.params.ParameterAnnotations.ClassParameter;
import org.chromium.base.test.params.ParameterAnnotations.UseMethodParameter;
import org.chromium.base.test.params.ParameterAnnotations.UseRunnerDelegate;
import org.chromium.base.test.params.ParameterizedRunnerDelegateFactory.ParameterizedRunnerDelegateInstantiationException;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;

/**
 * ParameterizedRunner generates a list of runners for each of class parameter set in a test class.
 *
 * ParameterizedRunner looks for {@code @ClassParameter} annotation in test class and
 * generates a list of ParameterizedRunnerDelegate runners for each ParameterSet.
 */
public final class ParameterizedRunner extends Suite {
    private final List<Runner> mRunners;

    /**
     * Create a ParameterizedRunner to run test class
     *
     * @param klass the Class of the test class, test class should be atomic
     *              (extends only Object)
     */
    public ParameterizedRunner(Class<?> klass) throws Throwable {
        super(klass, Collections.emptyList()); // pass in empty list of runners
        validate();
        mRunners = createRunners(getTestClass());
    }

    @Override
    protected List<Runner> getChildren() {
        return mRunners;
    }

    /**
     * ParentRunner calls collectInitializationErrors() to check for errors in Test class.
     * Parameterized tests are written in unconventional ways, therefore, this method is
     * overridden and validation is done seperately.
     */
    @Override
    protected void collectInitializationErrors(List<Throwable> errors) {
        // Do not call super collectInitializationErrors
    }

    private void validate() throws Throwable {
        validateNoNonStaticInnerClass();
        validateOnlyOneConstructor();
        validateInstanceMethods();
        validateOnlyOneClassParameterField();
        validateAtLeastOneParameterSetField();
    }

    private void validateNoNonStaticInnerClass() throws Exception {
        if (getTestClass().isANonStaticInnerClass()) {
            throw new Exception("The inner class " + getTestClass().getName() + " is not static.");
        }
    }

    private void validateOnlyOneConstructor() throws Exception {
        if (!hasOneConstructor()) {
            throw new Exception("Test class should have exactly one public constructor");
        }
    }

    private boolean hasOneConstructor() {
        return getTestClass().getJavaClass().getConstructors().length == 1;
    }

    private void validateOnlyOneClassParameterField() {
        if (getTestClass().getAnnotatedFields(ClassParameter.class).size() > 1) {
            throw new IllegalParameterArgumentException(String.format(Locale.getDefault(),
                    "%s class has more than one @ClassParameter, only one is allowed",
                    getTestClass().getName()));
        }
    }

    private void validateAtLeastOneParameterSetField() {
        if (getTestClass().getAnnotatedFields(ClassParameter.class).isEmpty()
                && getTestClass().getAnnotatedMethods(UseMethodParameter.class).isEmpty()) {
            throw new IllegalArgumentException(String.format(Locale.getDefault(),
                    "%s has no field annotated with @ClassParameter or method annotated with"
                            + "@UseMethodParameter; it should not use ParameterizedRunner",
                    getTestClass().getName()));
        }
    }

    private void validateInstanceMethods() throws Exception {
        if (getTestClass().getAnnotatedMethods(Test.class).size() == 0) {
            throw new Exception("No runnable methods");
        }
    }

    /**
     * Return a list of runner delegates through ParameterizedRunnerDelegateFactory.
     *
     * For class parameter set: each class can only have one list of class parameter sets.
     * Each parameter set will be used to create one runner.
     *
     * For method parameter set: a single list method parameter sets is associated with
     * a string tag, an immutable map of string to parameter set list will be created and
     * passed into factory for each runner delegate to create multiple tests. Only one
     * Runner will be created for a method that uses @UseMethodParameter, regardless of the
     * number of ParameterSets in the associated list.
     *
     * @return a list of runners
     * @throws ParameterizedRunnerDelegateInstantiationException if runner delegate can not
     *         be instantiated with constructor reflectively
     * @throws IllegalAccessError if the field in tests are not accessible
     */
    static List<Runner> createRunners(TestClass testClass)
            throws IllegalAccessException, ParameterizedRunnerDelegateInstantiationException {
        List<ParameterSet> classParameterSetList;
        if (testClass.getAnnotatedFields(ClassParameter.class).isEmpty()) {
            classParameterSetList = new ArrayList<>();
            classParameterSetList.add(null);
        } else {
            classParameterSetList = getParameterSetList(
                    testClass.getAnnotatedFields(ClassParameter.class).get(0), testClass);
            validateWidth(classParameterSetList);
        }

        Class<? extends ParameterizedRunnerDelegate> runnerDelegateClass =
                getRunnerDelegateClass(testClass);
        ParameterizedRunnerDelegateFactory factory = new ParameterizedRunnerDelegateFactory();
        List<Runner> runnersForTestClass = new ArrayList<>();
        for (ParameterSet classParameterSet : classParameterSetList) {
            BlockJUnit4ClassRunner runner = (BlockJUnit4ClassRunner) factory.createRunner(
                    testClass, classParameterSet, runnerDelegateClass);
            runnersForTestClass.add(runner);
        }
        return runnersForTestClass;
    }

    /**
     * Return an unmodifiable list of ParameterSet through a FrameworkField
     */
    private static List<ParameterSet> getParameterSetList(FrameworkField field, TestClass testClass)
            throws IllegalAccessException {
        field.getField().setAccessible(true);
        if (!Modifier.isStatic(field.getField().getModifiers())) {
            throw new IllegalParameterArgumentException(String.format(Locale.getDefault(),
                    "ParameterSetList fields must be static, this field %s in %s is not",
                    field.getName(), testClass.getName()));
        }
        if (!(field.get(testClass.getJavaClass()) instanceof List)) {
            throw new IllegalArgumentException(String.format(Locale.getDefault(),
                    "Fields with @ClassParameter annotations must be an instance of List, "
                            + "this field %s in %s is not list",
                    field.getName(), testClass.getName()));
        }
        @SuppressWarnings("unchecked") // checked above
        List<ParameterSet> result = (List<ParameterSet>) field.get(testClass.getJavaClass());
        return Collections.unmodifiableList(result);
    }

    static void validateWidth(Iterable<ParameterSet> parameterSetList) {
        int lastSize = -1;
        for (ParameterSet set : parameterSetList) {
            if (set.size() == 0) {
                throw new IllegalParameterArgumentException(
                        "No parameter is added to method ParameterSet");
            }
            if (lastSize == -1 || set.size() == lastSize) {
                lastSize = set.size();
            } else {
                throw new IllegalParameterArgumentException(String.format(Locale.getDefault(),
                        "All ParameterSets in a list of ParameterSet must have equal"
                                + " length. The current ParameterSet (%s) contains %d parameters,"
                                + " while previous ParameterSet contains %d parameters",
                        Arrays.toString(set.getValues().toArray()), set.size(), lastSize));
            }
        }
    }

    /**
     * Get the runner delegate class for the test class if {@code @UseRunnerDelegate} is used.
     * The default runner delegate is BaseJUnit4RunnerDelegate.class
     */
    private static Class<? extends ParameterizedRunnerDelegate> getRunnerDelegateClass(
            TestClass testClass) {
        if (testClass.getAnnotation(UseRunnerDelegate.class) != null) {
            return testClass.getAnnotation(UseRunnerDelegate.class).value();
        }
        return BaseJUnit4RunnerDelegate.class;
    }

    static class IllegalParameterArgumentException extends IllegalArgumentException {
        IllegalParameterArgumentException(String msg) {
            super(msg);
        }
    }

    public static class ParameterizedTestInstantiationException extends Exception {
        ParameterizedTestInstantiationException(
                TestClass testClass, String parameterSetString, Exception e) {
            super(String.format(
                          "Test class %s can not be initiated, the provided parameters are %s,"
                                  + " the required parameter types are %s",
                          testClass.getJavaClass().toString(), parameterSetString,
                          Arrays.toString(testClass.getOnlyConstructor().getParameterTypes())),
                    e);
        }
    }
}