| // Copyright (C) 2014 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.gerrit.testing; |
| |
| import static com.google.common.base.Preconditions.checkArgument; |
| import static java.lang.annotation.ElementType.FIELD; |
| import static java.lang.annotation.ElementType.METHOD; |
| import static java.lang.annotation.RetentionPolicy.RUNTIME; |
| import static java.util.stream.Collectors.toSet; |
| |
| import com.google.common.base.MoreObjects; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.Lists; |
| import java.lang.annotation.Annotation; |
| import java.lang.annotation.ElementType; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.lang.annotation.Target; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.InvocationTargetException; |
| import java.lang.reflect.Method; |
| import java.lang.reflect.Modifier; |
| import java.lang.reflect.ParameterizedType; |
| import java.lang.reflect.Type; |
| import java.util.List; |
| import java.util.Map; |
| import org.junit.internal.runners.statements.RunAfters; |
| import org.junit.internal.runners.statements.RunBefores; |
| import org.junit.rules.TestRule; |
| import org.junit.runner.Runner; |
| import org.junit.runners.BlockJUnit4ClassRunner; |
| import org.junit.runners.Suite; |
| import org.junit.runners.model.FrameworkMethod; |
| import org.junit.runners.model.InitializationError; |
| import org.junit.runners.model.Statement; |
| |
| /** |
| * Suite to run tests with different {@code gerrit.config} values. |
| * |
| * <p>For each {@link Config} method in the class and base classes, a new group of tests is created |
| * with the {@link Parameter} field set to the config. |
| * |
| * <p>Additional actions can be executed before or after each group of tests using |
| * {@literal @}BeforeConfig, {@literal @}AfterConfig or {@literal @}ConfigRule annotations. |
| * |
| * <pre> |
| * {@literal @}RunWith(ConfigSuite.class) |
| * public abstract class MyAbstractTest { |
| * {@literal @}ConfigSuite.Parameter |
| * protected Config cfg; |
| * |
| * {@literal @}ConfigSuite.Config |
| * public static Config firstConfig() { |
| * Config cfg = new Config(); |
| * cfg.setString("gerrit", null, "testValue", "a"); |
| * return cfg; |
| * } |
| * } |
| * |
| * public class MyTest extends MyAbstractTest { |
| * {@literal @}ConfigSuite.Config |
| * public static Config secondConfig() { |
| * Config cfg = new Config(); |
| * cfg.setString("gerrit", null, "testValue", "b"); |
| * return cfg; |
| * } |
| * |
| * {@literal @}Test |
| * public void myTest() { |
| * // Test using cfg. |
| * } |
| * } |
| * </pre> |
| * |
| * This creates a suite of tests with three groups: |
| * |
| * <ul> |
| * <li><strong>default</strong>: {@code MyTest.myTest} |
| * <li><strong>firstConfig</strong>: {@code MyTest.myTest[firstConfig]} |
| * <li><strong>secondConfig</strong>: {@code MyTest.myTest[secondConfig]} |
| * </ul> |
| * |
| * Additionally, config values used by <strong>default</strong> can be set in a method annotated |
| * with {@code @ConfigSuite.Default}. |
| * |
| * <p>In addition groups of tests for different configurations can be defined by annotating a method |
| * that returns a Map<String, Config> with {@link Configs}. The map keys define the test suite |
| * names, while the values define the configurations for the test suites. |
| * |
| * <pre> |
| * {@literal @}ConfigSuite.Configs |
| * public static Map<String, Config> configs() { |
| * Config cfgA = new Config(); |
| * cfgA.setString("gerrit", null, "testValue", "a"); |
| * Config cfgB = new Config(); |
| * cfgB.setString("gerrit", null, "testValue", "b"); |
| * return ImmutableMap.of("testWithValueA", cfgA, "testWithValueB", cfgB); |
| * } |
| * </pre> |
| * |
| * <p>The name of the config method corresponding to the currently-running test can be stored in a |
| * field annotated with {@code @ConfigSuite.Name}. |
| */ |
| public class ConfigSuite extends Suite { |
| public static final String DEFAULT = "default"; |
| |
| @Target({METHOD}) |
| @Retention(RUNTIME) |
| public static @interface Default {} |
| |
| @Target({METHOD}) |
| @Retention(RUNTIME) |
| public static @interface Config {} |
| |
| @Target({METHOD}) |
| @Retention(RUNTIME) |
| public static @interface Configs {} |
| |
| @Target({FIELD}) |
| @Retention(RUNTIME) |
| public static @interface Parameter {} |
| |
| @Target({FIELD}) |
| @Retention(RUNTIME) |
| public static @interface Name {} |
| |
| /** |
| * Annotation for methods which should be run after executing group of tests with a new |
| * configuration. |
| * |
| * <p>Works similar to {@link org.junit.AfterClass}, but a method can be executed multiple times |
| * if a test class provides multiple configs. |
| */ |
| @Retention(RetentionPolicy.RUNTIME) |
| @Target(ElementType.METHOD) |
| public @interface AfterConfig {} |
| |
| /** |
| * Annotation for methods which should be run before executing group of tests with a new |
| * configuration. |
| * |
| * <p>Works similar to {@link org.junit.BeforeClass}, but a method can be executed multiple times |
| * if a test class provides multiple configs. |
| */ |
| @Retention(RetentionPolicy.RUNTIME) |
| @Target(ElementType.METHOD) |
| public @interface BeforeConfig {} |
| |
| /** |
| * Annotation for fields or methods which wraps all tests with the same config |
| * |
| * <p>Works similar to {@link org.junit.ClassRule}, but Statement evaluates multiple time - ones |
| * for each config provided by a test class. |
| */ |
| @Retention(RetentionPolicy.RUNTIME) |
| @Target({ElementType.FIELD, ElementType.METHOD}) |
| public @interface ConfigRule {} |
| |
| private static class ConfigRunner extends BlockJUnit4ClassRunner { |
| private final org.eclipse.jgit.lib.Config cfg; |
| private final Field parameterField; |
| private final Field nameField; |
| private final String name; |
| |
| private ConfigRunner( |
| Class<?> clazz, |
| Field parameterField, |
| Field nameField, |
| String name, |
| org.eclipse.jgit.lib.Config cfg) |
| throws InitializationError { |
| super(clazz); |
| this.parameterField = parameterField; |
| this.nameField = nameField; |
| this.name = name; |
| this.cfg = cfg; |
| } |
| |
| @Override |
| public Object createTest() throws Exception { |
| Object test = getTestClass().getJavaClass().getDeclaredConstructor().newInstance(); |
| parameterField.set(test, new org.eclipse.jgit.lib.Config(cfg)); |
| if (nameField != null) { |
| nameField.set(test, name); |
| } |
| return test; |
| } |
| |
| @Override |
| protected String getName() { |
| return MoreObjects.firstNonNull(name, DEFAULT); |
| } |
| |
| @Override |
| protected String testName(FrameworkMethod method) { |
| String n = method.getName(); |
| return name == null ? n : n + "[" + name + "]"; |
| } |
| |
| @Override |
| protected Statement withBeforeClasses(Statement statement) { |
| List<FrameworkMethod> befores = getTestClass().getAnnotatedMethods(BeforeConfig.class); |
| return befores.isEmpty() ? statement : new RunBefores(statement, befores, null); |
| } |
| |
| @Override |
| protected Statement withAfterClasses(Statement statement) { |
| List<FrameworkMethod> afters = getTestClass().getAnnotatedMethods(AfterConfig.class); |
| return afters.isEmpty() ? statement : new RunAfters(statement, afters, null); |
| } |
| |
| @Override |
| protected List<TestRule> classRules() { |
| List<TestRule> result = |
| getTestClass().getAnnotatedMethodValues(null, ConfigRule.class, TestRule.class); |
| result.addAll(getTestClass().getAnnotatedFieldValues(null, ConfigRule.class, TestRule.class)); |
| return result; |
| } |
| } |
| |
| private static List<Runner> runnersFor(Class<?> clazz) { |
| Method defaultConfig = getDefaultConfig(clazz); |
| List<Method> configs = getConfigs(clazz); |
| Map<String, org.eclipse.jgit.lib.Config> configMap = |
| callConfigMapMethod(getConfigMap(clazz), configs); |
| |
| Field parameterField = getOnlyField(clazz, Parameter.class); |
| checkArgument(parameterField != null, "No @ConfigSuite.Parameter found"); |
| Field nameField = getOnlyField(clazz, Name.class); |
| List<Runner> result = Lists.newArrayListWithCapacity(configs.size() + 1); |
| try { |
| result.add( |
| new ConfigRunner( |
| clazz, parameterField, nameField, null, callConfigMethod(defaultConfig))); |
| for (Method m : configs) { |
| result.add( |
| new ConfigRunner(clazz, parameterField, nameField, m.getName(), callConfigMethod(m))); |
| } |
| for (Map.Entry<String, org.eclipse.jgit.lib.Config> e : configMap.entrySet()) { |
| result.add(new ConfigRunner(clazz, parameterField, nameField, e.getKey(), e.getValue())); |
| } |
| return result; |
| } catch (InitializationError e) { |
| System.err.println("Errors initializing runners:"); |
| for (Throwable t : e.getCauses()) { |
| t.printStackTrace(); |
| } |
| throw new RuntimeException(e); |
| } |
| } |
| |
| private static Method getDefaultConfig(Class<?> clazz) { |
| return getAnnotatedMethod(clazz, Default.class); |
| } |
| |
| private static Method getConfigMap(Class<?> clazz) { |
| return getAnnotatedMethod(clazz, Configs.class); |
| } |
| |
| private static <T extends Annotation> Method getAnnotatedMethod( |
| Class<?> clazz, Class<T> annotationClass) { |
| Method result = null; |
| for (Method m : clazz.getMethods()) { |
| T ann = m.getAnnotation(annotationClass); |
| if (ann != null) { |
| checkArgument(result == null, "Multiple methods annotated with %s: %s, %s", ann, result, m); |
| result = m; |
| } |
| } |
| return result; |
| } |
| |
| private static List<Method> getConfigs(Class<?> clazz) { |
| List<Method> result = Lists.newArrayListWithExpectedSize(3); |
| for (Method m : clazz.getMethods()) { |
| Config ann = m.getAnnotation(Config.class); |
| if (ann != null) { |
| checkArgument(!m.getName().equals(DEFAULT), "%s cannot be named %s", ann, DEFAULT); |
| result.add(m); |
| } |
| } |
| return result; |
| } |
| |
| private static org.eclipse.jgit.lib.Config callConfigMethod(Method m) { |
| if (m == null) { |
| return new org.eclipse.jgit.lib.Config(); |
| } |
| checkArgument( |
| org.eclipse.jgit.lib.Config.class.isAssignableFrom(m.getReturnType()), |
| "%s must return Config", |
| m); |
| checkArgument((m.getModifiers() & Modifier.STATIC) != 0, "%s must be static", m); |
| checkArgument(m.getParameterTypes().length == 0, "%s must take no parameters", m); |
| try { |
| return (org.eclipse.jgit.lib.Config) m.invoke(null); |
| } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { |
| throw new IllegalArgumentException(e); |
| } |
| } |
| |
| private static Map<String, org.eclipse.jgit.lib.Config> callConfigMapMethod( |
| Method m, List<Method> configs) { |
| if (m == null) { |
| return ImmutableMap.of(); |
| } |
| checkArgument(Map.class.isAssignableFrom(m.getReturnType()), "%s must return Map", m); |
| Type[] types = ((ParameterizedType) m.getGenericReturnType()).getActualTypeArguments(); |
| checkArgument( |
| String.class.isAssignableFrom((Class<?>) types[0]), |
| "The map returned by %s must have String as key", |
| m); |
| checkArgument( |
| org.eclipse.jgit.lib.Config.class.isAssignableFrom((Class<?>) types[1]), |
| "The map returned by %s must have Config as value", |
| m); |
| checkArgument((m.getModifiers() & Modifier.STATIC) != 0, "%s must be static", m); |
| checkArgument(m.getParameterTypes().length == 0, "%s must take no parameters", m); |
| try { |
| @SuppressWarnings("unchecked") |
| Map<String, org.eclipse.jgit.lib.Config> configMap = |
| (Map<String, org.eclipse.jgit.lib.Config>) m.invoke(null); |
| checkArgument( |
| !configMap.containsKey(DEFAULT), |
| "The map returned by %s cannot contain key %s (duplicate test suite name)", |
| m, |
| DEFAULT); |
| for (String name : configs.stream().map(Method::getName).collect(toSet())) { |
| checkArgument( |
| !configMap.containsKey(name), |
| "The map returned by %s cannot contain key %s (duplicate test suite name)", |
| m, |
| name); |
| } |
| return configMap; |
| } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { |
| throw new IllegalArgumentException(e); |
| } |
| } |
| |
| private static Field getOnlyField(Class<?> clazz, Class<? extends Annotation> ann) { |
| List<Field> fields = Lists.newArrayListWithExpectedSize(1); |
| for (Field f : clazz.getFields()) { |
| if (f.getAnnotation(ann) != null) { |
| fields.add(f); |
| } |
| } |
| checkArgument( |
| fields.size() <= 1, |
| "expected 1 @ConfigSuite.%s field, found: %s", |
| ann.getSimpleName(), |
| fields); |
| return Iterables.getFirst(fields, null); |
| } |
| |
| public ConfigSuite(Class<?> clazz) throws InitializationError { |
| super(clazz, runnersFor(clazz)); |
| } |
| } |