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<String, Config> 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<String, Config> 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 : }
|