blob: 834f26139fd115ffd3575733244347bef3df4321 [file] [log] [blame]
// 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);
}
}
}