View Javadoc

1   /*
2    * SymmetricDS is an open source database synchronization solution.
3    *   
4    * Copyright (C) Chris Henson <chenson42@users.sourceforge.net>
5    *
6    * This library is free software; you can redistribute it and/or
7    * modify it under the terms of the GNU Lesser General Public
8    * License as published by the Free Software Foundation; either
9    * version 3 of the License, or (at your option) any later version.
10   *
11   * This library is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14   * Lesser General Public License for more details.
15   *
16   * You should have received a copy of the GNU Lesser General Public
17   * License along with this library; if not, see
18   * <http://www.gnu.org/licenses/>.
19   */
20  package org.jumpmind.symmetric.test;
21  
22  import java.lang.annotation.ElementType;
23  import java.lang.annotation.Retention;
24  import java.lang.annotation.RetentionPolicy;
25  import java.lang.annotation.Target;
26  import java.lang.reflect.Constructor;
27  import java.lang.reflect.InvocationTargetException;
28  import java.lang.reflect.Method;
29  import java.lang.reflect.Modifier;
30  import java.util.Collection;
31  import java.util.HashSet;
32  import java.util.Iterator;
33  import java.util.List;
34  import java.util.Set;
35  
36  import org.apache.commons.lang.ArrayUtils;
37  import org.junit.Assert;
38  import org.junit.internal.runners.ClassRoadie;
39  import org.junit.internal.runners.CompositeRunner;
40  import org.junit.internal.runners.InitializationError;
41  import org.junit.internal.runners.JUnit4ClassRunner;
42  import org.junit.internal.runners.MethodValidator;
43  import org.junit.internal.runners.TestClass;
44  import org.junit.runner.notification.RunNotifier;
45  import org.junit.runners.Parameterized.Parameters;
46  import org.junit.runners.Suite.SuiteClasses;
47  
48  /***
49   * Merge JUnits Parameterized runner and their Suite runner so we have a nice
50   * efficient way to run an entire suite of tests against parameterized 'sets' of
51   * data across the entire suite.
52   */
53  public class ParameterizedSuite extends CompositeRunner {
54  
55      @Retention(RetentionPolicy.RUNTIME)
56      @Target(ElementType.METHOD)
57      public static @interface ParameterMatcher {
58          String[] value();
59      }
60      
61      @Retention(RetentionPolicy.RUNTIME)
62      @Target(ElementType.METHOD)
63      public static @interface ParameterExcluder {
64          String[] value();
65      }
66  
67      static class TestClassRunnerForParameters extends JUnit4ClassRunner {
68          private final Object[] fParameters;
69  
70          private final Constructor<?> fConstructor;
71  
72          private List<Method> methods;
73  
74          TestClassRunnerForParameters(TestClass testClass, Object[] parameters) throws InitializationError {
75              super(testClass.getJavaClass());
76              fParameters = parameters;
77              fConstructor = getOnlyConstructor();
78              filterParameters();
79          }
80  
81          protected void filterParameters() {
82              for (Iterator<Method> iterator = methods.iterator(); iterator.hasNext();) {
83                  Method method = (Method) iterator.next();
84                  ParameterMatcher match = method.getAnnotation(ParameterMatcher.class);
85                  if (match != null) {
86                      boolean remove = true;
87                      for (Object p : fParameters) {
88                          String[] matchValues = match.value();
89                          for (String matchValue : matchValues) {
90                              if (p != null && p.toString().equals(matchValue)) {
91                                  remove = false;
92                              }                            
93                          }
94                      }
95                      if (remove) {
96                          iterator.remove();
97                      }
98                  }
99                  
100                 ParameterExcluder excluder = method.getAnnotation(ParameterExcluder.class);
101                 if (excluder != null) {
102                     boolean remove = false;
103                     for (Object p : fParameters) {
104                         String[] excludeValues = excluder.value();
105                         for (String excludeValue : excludeValues) {
106                             if (p != null && p.toString().equals(excludeValue)) {
107                                 remove = true;
108                             }                            
109                         }
110                     }
111                     if (remove) {
112                         iterator.remove();
113                     }
114                 }
115             }
116         }
117 
118         @Override
119         protected Object createTest() throws Exception {
120             return fConstructor.newInstance(fParameters);
121         }
122 
123         @Override
124         protected String getName() {
125             return String.format("%s with params %s", getTestClass().getName(), ArrayUtils.toString(fParameters));
126         }
127 
128         /***
129          * Get a sneaky handle on methods so we can filter them.
130          */
131         @Override
132         protected List<Method> getTestMethods() {
133             methods = super.getTestMethods();
134             return methods;
135         }
136 
137         @Override
138         protected String testName(final Method method) {
139             return String.format("%s with params %s", method.getName(), ArrayUtils.toString(fParameters));
140         }
141 
142         private Constructor<?> getOnlyConstructor() {
143             Constructor<?> c = null;
144             Constructor<?>[] constructors = getTestClass().getJavaClass().getConstructors();
145             for (Constructor<?> constructor : constructors) {
146                 if (constructor.getGenericParameterTypes().length == fParameters.length) {
147                     c = constructor;
148                     break;
149                 }
150             }
151             Assert.assertNotNull("Could not find an appropriate constructor for " + getName(), c);
152             return c;
153         }
154 
155         @Override
156         protected void validate() throws InitializationError {
157             // do nothing: validated before.
158         }
159 
160         @Override
161         public void run(RunNotifier notifier) {
162             runMethods(notifier);
163         }
164     }
165 
166     public ParameterizedSuite(Class<?> klass) throws Exception {
167         this(klass, getAnnotatedClasses(klass));
168     }
169 
170     // This won't work correctly in the face of concurrency. For that we need to
171     // add parameters to getRunner(), which would be much more complicated.
172     private static Set<Class<?>> parents = new HashSet<Class<?>>();
173     private TestClass fTestClass;
174 
175     protected ParameterizedSuite(Class<?> klass, Class<?>[] annotatedClasses) throws Exception {
176         super(klass.getName());
177 
178         fTestClass = new TestClass(klass);
179 
180         addParent(klass);
181         for (final Object each : getParametersList()) {
182             if (each instanceof Object[]) {
183                 for (Class<?> clazz : annotatedClasses) {
184                     add(new TestClassRunnerForParameters(new TestClass(clazz), (Object[]) each));
185                 }
186             } else {
187                 throw new Exception(String.format("%s.%s() must return a Collection of arrays.", fTestClass.getName(),
188                         getParametersMethod().getName()));
189             }
190         }
191         removeParent(klass);
192         MethodValidator methodValidator = new MethodValidator(fTestClass);
193         methodValidator.validateStaticMethods();
194         methodValidator.assertValid();
195     }
196 
197     private Class<?> addParent(Class<?> parent) throws InitializationError {
198         if (!parents.add(parent))
199             throw new InitializationError(String.format(
200                     "class '%s' (possibly indirectly) contains itself as a SuiteClass", parent.getName()));
201         return parent;
202     }
203 
204     private void removeParent(Class<?> klass) {
205         parents.remove(klass);
206     }
207 
208     private static Class<?>[] getAnnotatedClasses(Class<?> klass) throws InitializationError {
209         SuiteClasses annotation = klass.getAnnotation(SuiteClasses.class);
210         if (annotation == null)
211             throw new InitializationError(String.format("class '%s' must have a SuiteClasses annotation", klass
212                     .getName()));
213         Class<?>[] classes = new Class[annotation.value().length + 1];
214         classes[0] = klass;
215         for (int i = 1; i <= annotation.value().length; i++) {
216             classes[i] = annotation.value()[i - 1];
217         }
218         return classes;
219     }
220 
221     protected void validate(MethodValidator methodValidator) {
222         methodValidator.validateStaticMethods();
223         methodValidator.validateInstanceMethods();
224     }
225 
226     private Collection<?> getParametersList() throws IllegalAccessException, InvocationTargetException, Exception {
227         return (Collection<?>) getParametersMethod().invoke(null);
228     }
229 
230     private Method getParametersMethod() throws Exception {
231         List<Method> methods = fTestClass.getAnnotatedMethods(Parameters.class);
232         for (Method each : methods) {
233             int modifiers = each.getModifiers();
234             if (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers))
235                 return each;
236         }
237 
238         throw new Exception("No public static parameters method on class " + getName());
239     }
240 
241     @Override
242     public void run(final RunNotifier notifier) {
243         new ClassRoadie(notifier, fTestClass, getDescription(), new Runnable() {
244             public void run() {
245                 runChildren(notifier);
246             }
247         }).runProtected();
248     }
249 
250 }