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