LCOV - code coverage report
Current view: top level - testing - ConfigSuite.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 89 102 87.3 %
Date: 2022-11-19 15:00:39 Functions: 16 16 100.0 %

          Line data    Source code
       1             : // Copyright (C) 2014 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.testing;
      16             : 
      17             : import static com.google.common.base.Preconditions.checkArgument;
      18             : import static java.lang.annotation.ElementType.FIELD;
      19             : import static java.lang.annotation.ElementType.METHOD;
      20             : import static java.lang.annotation.RetentionPolicy.RUNTIME;
      21             : import static java.util.stream.Collectors.toSet;
      22             : 
      23             : import com.google.common.base.MoreObjects;
      24             : import com.google.common.collect.ImmutableMap;
      25             : import com.google.common.collect.Iterables;
      26             : import com.google.common.collect.Lists;
      27             : import java.lang.annotation.Annotation;
      28             : import java.lang.annotation.ElementType;
      29             : import java.lang.annotation.Retention;
      30             : import java.lang.annotation.RetentionPolicy;
      31             : import java.lang.annotation.Target;
      32             : import java.lang.reflect.Field;
      33             : import java.lang.reflect.InvocationTargetException;
      34             : import java.lang.reflect.Method;
      35             : import java.lang.reflect.Modifier;
      36             : import java.lang.reflect.ParameterizedType;
      37             : import java.lang.reflect.Type;
      38             : import java.util.List;
      39             : import java.util.Map;
      40             : import org.junit.internal.runners.statements.RunAfters;
      41             : import org.junit.internal.runners.statements.RunBefores;
      42             : import org.junit.rules.TestRule;
      43             : import org.junit.runner.Runner;
      44             : import org.junit.runners.BlockJUnit4ClassRunner;
      45             : import org.junit.runners.Suite;
      46             : import org.junit.runners.model.FrameworkMethod;
      47             : import org.junit.runners.model.InitializationError;
      48             : import org.junit.runners.model.Statement;
      49             : 
      50             : /**
      51             :  * Suite to run tests with different {@code gerrit.config} values.
      52             :  *
      53             :  * <p>For each {@link Config} method in the class and base classes, a new group of tests is created
      54             :  * with the {@link Parameter} field set to the config.
      55             :  *
      56             :  * <p>Additional actions can be executed before or after each group of tests using
      57             :  * {@literal @}BeforeConfig, {@literal @}AfterConfig or {@literal @}ConfigRule annotations.
      58             :  *
      59             :  * <pre>
      60             :  * {@literal @}RunWith(ConfigSuite.class)
      61             :  * public abstract class MyAbstractTest {
      62             :  *   {@literal @}ConfigSuite.Parameter
      63             :  *   protected Config cfg;
      64             :  *
      65             :  *   {@literal @}ConfigSuite.Config
      66             :  *   public static Config firstConfig() {
      67             :  *     Config cfg = new Config();
      68             :  *     cfg.setString("gerrit", null, "testValue", "a");
      69             :  *     return cfg;
      70             :  *   }
      71             :  * }
      72             :  *
      73             :  * public class MyTest extends MyAbstractTest {
      74             :  *   {@literal @}ConfigSuite.Config
      75             :  *   public static Config secondConfig() {
      76             :  *     Config cfg = new Config();
      77             :  *     cfg.setString("gerrit", null, "testValue", "b");
      78             :  *     return cfg;
      79             :  *   }
      80             :  *
      81             :  *   {@literal @}Test
      82             :  *   public void myTest() {
      83             :  *     // Test using cfg.
      84             :  *   }
      85             :  * }
      86             :  * </pre>
      87             :  *
      88             :  * This creates a suite of tests with three groups:
      89             :  *
      90             :  * <ul>
      91             :  *   <li><strong>default</strong>: {@code MyTest.myTest}
      92             :  *   <li><strong>firstConfig</strong>: {@code MyTest.myTest[firstConfig]}
      93             :  *   <li><strong>secondConfig</strong>: {@code MyTest.myTest[secondConfig]}
      94             :  * </ul>
      95             :  *
      96             :  * Additionally, config values used by <strong>default</strong> can be set in a method annotated
      97             :  * with {@code @ConfigSuite.Default}.
      98             :  *
      99             :  * <p>In addition groups of tests for different configurations can be defined by annotating a method
     100             :  * that returns a Map&lt;String, Config&gt; with {@link Configs}. The map keys define the test suite
     101             :  * names, while the values define the configurations for the test suites.
     102             :  *
     103             :  * <pre>
     104             :  * {@literal @}ConfigSuite.Configs
     105             :  * public static Map&lt;String, Config&gt; configs() {
     106             :  *   Config cfgA = new Config();
     107             :  *   cfgA.setString("gerrit", null, "testValue", "a");
     108             :  *   Config cfgB = new Config();
     109             :  *   cfgB.setString("gerrit", null, "testValue", "b");
     110             :  *   return ImmutableMap.of("testWithValueA", cfgA, "testWithValueB", cfgB);
     111             :  * }
     112             :  * </pre>
     113             :  *
     114             :  * <p>The name of the config method corresponding to the currently-running test can be stored in a
     115             :  * field annotated with {@code @ConfigSuite.Name}.
     116             :  */
     117             : public class ConfigSuite extends Suite {
     118             :   public static final String DEFAULT = "default";
     119             : 
     120             :   @Target({METHOD})
     121             :   @Retention(RUNTIME)
     122             :   public static @interface Default {}
     123             : 
     124             :   @Target({METHOD})
     125             :   @Retention(RUNTIME)
     126             :   public static @interface Config {}
     127             : 
     128             :   @Target({METHOD})
     129             :   @Retention(RUNTIME)
     130             :   public static @interface Configs {}
     131             : 
     132             :   @Target({FIELD})
     133             :   @Retention(RUNTIME)
     134             :   public static @interface Parameter {}
     135             : 
     136             :   @Target({FIELD})
     137             :   @Retention(RUNTIME)
     138             :   public static @interface Name {}
     139             : 
     140             :   /**
     141             :    * Annotation for methods which should be run after executing group of tests with a new
     142             :    * configuration.
     143             :    *
     144             :    * <p>Works similar to {@link org.junit.AfterClass}, but a method can be executed multiple times
     145             :    * if a test class provides multiple configs.
     146             :    */
     147             :   @Retention(RetentionPolicy.RUNTIME)
     148             :   @Target(ElementType.METHOD)
     149             :   public @interface AfterConfig {}
     150             : 
     151             :   /**
     152             :    * Annotation for methods which should be run before executing group of tests with a new
     153             :    * configuration.
     154             :    *
     155             :    * <p>Works similar to {@link org.junit.BeforeClass}, but a method can be executed multiple times
     156             :    * if a test class provides multiple configs.
     157             :    */
     158             :   @Retention(RetentionPolicy.RUNTIME)
     159             :   @Target(ElementType.METHOD)
     160             :   public @interface BeforeConfig {}
     161             : 
     162             :   /**
     163             :    * Annotation for fields or methods which wraps all tests with the same config
     164             :    *
     165             :    * <p>Works similar to {@link org.junit.ClassRule}, but Statement evaluates multiple time - ones
     166             :    * for each config provided by a test class.
     167             :    */
     168             :   @Retention(RetentionPolicy.RUNTIME)
     169             :   @Target({ElementType.FIELD, ElementType.METHOD})
     170             :   public @interface ConfigRule {}
     171             : 
     172             :   private static class ConfigRunner extends BlockJUnit4ClassRunner {
     173             :     private final org.eclipse.jgit.lib.Config cfg;
     174             :     private final Field parameterField;
     175             :     private final Field nameField;
     176             :     private final String name;
     177             : 
     178             :     private ConfigRunner(
     179             :         Class<?> clazz,
     180             :         Field parameterField,
     181             :         Field nameField,
     182             :         String name,
     183             :         org.eclipse.jgit.lib.Config cfg)
     184             :         throws InitializationError {
     185         150 :       super(clazz);
     186         150 :       this.parameterField = parameterField;
     187         150 :       this.nameField = nameField;
     188         150 :       this.name = name;
     189         150 :       this.cfg = cfg;
     190         150 :     }
     191             : 
     192             :     @Override
     193             :     public Object createTest() throws Exception {
     194         150 :       Object test = getTestClass().getJavaClass().getDeclaredConstructor().newInstance();
     195         150 :       parameterField.set(test, new org.eclipse.jgit.lib.Config(cfg));
     196         150 :       if (nameField != null) {
     197           1 :         nameField.set(test, name);
     198             :       }
     199         150 :       return test;
     200             :     }
     201             : 
     202             :     @Override
     203             :     protected String getName() {
     204         150 :       return MoreObjects.firstNonNull(name, DEFAULT);
     205             :     }
     206             : 
     207             :     @Override
     208             :     protected String testName(FrameworkMethod method) {
     209         150 :       String n = method.getName();
     210         150 :       return name == null ? n : n + "[" + name + "]";
     211             :     }
     212             : 
     213             :     @Override
     214             :     protected Statement withBeforeClasses(Statement statement) {
     215         150 :       List<FrameworkMethod> befores = getTestClass().getAnnotatedMethods(BeforeConfig.class);
     216         150 :       return befores.isEmpty() ? statement : new RunBefores(statement, befores, null);
     217             :     }
     218             : 
     219             :     @Override
     220             :     protected Statement withAfterClasses(Statement statement) {
     221         150 :       List<FrameworkMethod> afters = getTestClass().getAnnotatedMethods(AfterConfig.class);
     222         150 :       return afters.isEmpty() ? statement : new RunAfters(statement, afters, null);
     223             :     }
     224             : 
     225             :     @Override
     226             :     protected List<TestRule> classRules() {
     227         150 :       List<TestRule> result =
     228         150 :           getTestClass().getAnnotatedMethodValues(null, ConfigRule.class, TestRule.class);
     229         150 :       result.addAll(getTestClass().getAnnotatedFieldValues(null, ConfigRule.class, TestRule.class));
     230         150 :       return result;
     231             :     }
     232             :   }
     233             : 
     234             :   private static List<Runner> runnersFor(Class<?> clazz) {
     235         150 :     Method defaultConfig = getDefaultConfig(clazz);
     236         150 :     List<Method> configs = getConfigs(clazz);
     237         150 :     Map<String, org.eclipse.jgit.lib.Config> configMap =
     238         150 :         callConfigMapMethod(getConfigMap(clazz), configs);
     239             : 
     240         150 :     Field parameterField = getOnlyField(clazz, Parameter.class);
     241         150 :     checkArgument(parameterField != null, "No @ConfigSuite.Parameter found");
     242         150 :     Field nameField = getOnlyField(clazz, Name.class);
     243         150 :     List<Runner> result = Lists.newArrayListWithCapacity(configs.size() + 1);
     244             :     try {
     245         150 :       result.add(
     246             :           new ConfigRunner(
     247         150 :               clazz, parameterField, nameField, null, callConfigMethod(defaultConfig)));
     248         150 :       for (Method m : configs) {
     249          26 :         result.add(
     250          26 :             new ConfigRunner(clazz, parameterField, nameField, m.getName(), callConfigMethod(m)));
     251          26 :       }
     252         150 :       for (Map.Entry<String, org.eclipse.jgit.lib.Config> e : configMap.entrySet()) {
     253           6 :         result.add(new ConfigRunner(clazz, parameterField, nameField, e.getKey(), e.getValue()));
     254           6 :       }
     255         150 :       return result;
     256           0 :     } catch (InitializationError e) {
     257           0 :       System.err.println("Errors initializing runners:");
     258           0 :       for (Throwable t : e.getCauses()) {
     259           0 :         t.printStackTrace();
     260           0 :       }
     261           0 :       throw new RuntimeException(e);
     262             :     }
     263             :   }
     264             : 
     265             :   private static Method getDefaultConfig(Class<?> clazz) {
     266         150 :     return getAnnotatedMethod(clazz, Default.class);
     267             :   }
     268             : 
     269             :   private static Method getConfigMap(Class<?> clazz) {
     270         150 :     return getAnnotatedMethod(clazz, Configs.class);
     271             :   }
     272             : 
     273             :   private static <T extends Annotation> Method getAnnotatedMethod(
     274             :       Class<?> clazz, Class<T> annotationClass) {
     275         150 :     Method result = null;
     276         150 :     for (Method m : clazz.getMethods()) {
     277         150 :       T ann = m.getAnnotation(annotationClass);
     278         150 :       if (ann != null) {
     279          27 :         checkArgument(result == null, "Multiple methods annotated with %s: %s, %s", ann, result, m);
     280          27 :         result = m;
     281             :       }
     282             :     }
     283         150 :     return result;
     284             :   }
     285             : 
     286             :   private static List<Method> getConfigs(Class<?> clazz) {
     287         150 :     List<Method> result = Lists.newArrayListWithExpectedSize(3);
     288         150 :     for (Method m : clazz.getMethods()) {
     289         150 :       Config ann = m.getAnnotation(Config.class);
     290         150 :       if (ann != null) {
     291          26 :         checkArgument(!m.getName().equals(DEFAULT), "%s cannot be named %s", ann, DEFAULT);
     292          26 :         result.add(m);
     293             :       }
     294             :     }
     295         150 :     return result;
     296             :   }
     297             : 
     298             :   private static org.eclipse.jgit.lib.Config callConfigMethod(Method m) {
     299         150 :     if (m == null) {
     300         131 :       return new org.eclipse.jgit.lib.Config();
     301             :     }
     302          42 :     checkArgument(
     303          42 :         org.eclipse.jgit.lib.Config.class.isAssignableFrom(m.getReturnType()),
     304             :         "%s must return Config",
     305             :         m);
     306          42 :     checkArgument((m.getModifiers() & Modifier.STATIC) != 0, "%s must be static", m);
     307          42 :     checkArgument(m.getParameterTypes().length == 0, "%s must take no parameters", m);
     308             :     try {
     309          42 :       return (org.eclipse.jgit.lib.Config) m.invoke(null);
     310           0 :     } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
     311           0 :       throw new IllegalArgumentException(e);
     312             :     }
     313             :   }
     314             : 
     315             :   private static Map<String, org.eclipse.jgit.lib.Config> callConfigMapMethod(
     316             :       Method m, List<Method> configs) {
     317         150 :     if (m == null) {
     318         144 :       return ImmutableMap.of();
     319             :     }
     320           6 :     checkArgument(Map.class.isAssignableFrom(m.getReturnType()), "%s must return Map", m);
     321           6 :     Type[] types = ((ParameterizedType) m.getGenericReturnType()).getActualTypeArguments();
     322           6 :     checkArgument(
     323           6 :         String.class.isAssignableFrom((Class<?>) types[0]),
     324             :         "The map returned by %s must have String as key",
     325             :         m);
     326           6 :     checkArgument(
     327           6 :         org.eclipse.jgit.lib.Config.class.isAssignableFrom((Class<?>) types[1]),
     328             :         "The map returned by %s must have Config as value",
     329             :         m);
     330           6 :     checkArgument((m.getModifiers() & Modifier.STATIC) != 0, "%s must be static", m);
     331           6 :     checkArgument(m.getParameterTypes().length == 0, "%s must take no parameters", m);
     332             :     try {
     333             :       @SuppressWarnings("unchecked")
     334           6 :       Map<String, org.eclipse.jgit.lib.Config> configMap =
     335           6 :           (Map<String, org.eclipse.jgit.lib.Config>) m.invoke(null);
     336           6 :       checkArgument(
     337           6 :           !configMap.containsKey(DEFAULT),
     338             :           "The map returned by %s cannot contain key %s (duplicate test suite name)",
     339             :           m,
     340             :           DEFAULT);
     341           6 :       for (String name : configs.stream().map(Method::getName).collect(toSet())) {
     342           0 :         checkArgument(
     343           0 :             !configMap.containsKey(name),
     344             :             "The map returned by %s cannot contain key %s (duplicate test suite name)",
     345             :             m,
     346             :             name);
     347           0 :       }
     348           6 :       return configMap;
     349           0 :     } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
     350           0 :       throw new IllegalArgumentException(e);
     351             :     }
     352             :   }
     353             : 
     354             :   private static Field getOnlyField(Class<?> clazz, Class<? extends Annotation> ann) {
     355         150 :     List<Field> fields = Lists.newArrayListWithExpectedSize(1);
     356         150 :     for (Field f : clazz.getFields()) {
     357         150 :       if (f.getAnnotation(ann) != null) {
     358         150 :         fields.add(f);
     359             :       }
     360             :     }
     361         150 :     checkArgument(
     362         150 :         fields.size() <= 1,
     363             :         "expected 1 @ConfigSuite.%s field, found: %s",
     364         150 :         ann.getSimpleName(),
     365             :         fields);
     366         150 :     return Iterables.getFirst(fields, null);
     367             :   }
     368             : 
     369             :   public ConfigSuite(Class<?> clazz) throws InitializationError {
     370         150 :     super(clazz, runnersFor(clazz));
     371         150 :   }
     372             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750