Index: base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedRunner.java |
diff --git a/base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedRunner.java b/base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedRunner.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..a72c6962e7af4df78b89580ee522c10afac32c99 |
--- /dev/null |
+++ b/base/test/android/javatests/src/org/chromium/base/test/params/ParameterizedRunner.java |
@@ -0,0 +1,239 @@ |
+// 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.MethodParameter; |
+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.HashMap; |
+import java.util.List; |
+import java.util.Locale; |
+import java.util.Map; |
+ |
+/** |
+ * 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. The class |
+ * runner also looks for {@code @MethodParameter} annotation, and creates a map that maps Strings |
+ * value(tag) to ParameterSet List. |
+ */ |
+public final class ParameterizedRunner extends Suite { |
+ private static final String TAG = "cr_ParameterizedRunner"; |
+ 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.<Runner>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().getAnnotatedFields(MethodParameter.class).isEmpty()) { |
+ throw new IllegalArgumentException(String.format(Locale.getDefault(), |
+ "%s has no field annotated with @ClassParameter or @MethodParameter field, " |
+ + "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 @MethodParameter, regardless of the |
+ * number of ParameterSets in the associated list. |
+ * |
+ * @return a list of runners |
+ * @throws ParameterizedTestInstantiationException if the test class is malformed. |
+ * @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, ParameterizedTestInstantiationException, |
+ 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(); |
+ Map<String, List<ParameterSet>> tagToMethodParameterSetList = |
+ Collections.unmodifiableMap(generateMethodParameterMap(testClass)); |
+ List<Runner> runnersForTestClass = new ArrayList<>(); |
+ for (ParameterSet classParameterSet : classParameterSetList) { |
+ BlockJUnit4ClassRunner runner = (BlockJUnit4ClassRunner) factory.createRunner( |
+ testClass, classParameterSet, tagToMethodParameterSetList, runnerDelegateClass); |
+ runnersForTestClass.add(runner); |
+ } |
+ return runnersForTestClass; |
+ } |
+ |
+ /** |
+ * Returns a map between MethodParameter tags and corresponding ParameterSetLists. |
+ */ |
+ static Map<String, List<ParameterSet>> generateMethodParameterMap(TestClass testClass) |
+ throws IllegalAccessException { |
+ Map<String, List<ParameterSet>> result = new HashMap<>(); |
+ for (FrameworkField field : testClass.getAnnotatedFields(MethodParameter.class)) { |
+ List<ParameterSet> parameterSetList = getParameterSetList(field, testClass); |
+ validateWidth(parameterSetList); |
+ result.put(field.getAnnotation(MethodParameter.class).value(), parameterSetList); |
+ } |
+ return result; |
+ } |
+ |
+ /** |
+ * Return an unmodifiable list of ParameterSet through a FrameworkField |
+ */ |
+ 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 or @MethodParameter 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(List<ParameterSet> parameterSetList) { |
+ int lastSize = -1; |
+ for (ParameterSet set : parameterSetList) { |
+ 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 { |
+ public IllegalParameterArgumentException(String msg) { |
+ super(msg); |
+ } |
+ } |
+ |
+ 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); |
+ } |
+ } |
+} |