| // 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( |
| "%s class has more than one @ClassParameter, only one is allowed"); |
| } |
| } |
| |
| 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); |
| } |
| } |
| } |