• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 The Guava Authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.common.collect.testing;
18 
19 import static java.util.Collections.disjoint;
20 import static java.util.logging.Level.FINER;
21 
22 import com.google.common.collect.testing.features.ConflictingRequirementsException;
23 import com.google.common.collect.testing.features.Feature;
24 import com.google.common.collect.testing.features.FeatureUtil;
25 import com.google.common.collect.testing.features.TesterRequirements;
26 
27 import junit.framework.Test;
28 import junit.framework.TestCase;
29 import junit.framework.TestSuite;
30 
31 import java.lang.reflect.Method;
32 import java.util.ArrayList;
33 import java.util.Arrays;
34 import java.util.Collection;
35 import java.util.Collections;
36 import java.util.Enumeration;
37 import java.util.HashMap;
38 import java.util.HashSet;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.Set;
42 import java.util.logging.Logger;
43 
44 /**
45  * Creates, based on your criteria, a JUnit test suite that exhaustively tests
46  * the object generated by a G, selecting appropriate tests by matching them
47  * against specified features.
48  *
49  * @param <B> The concrete type of this builder (the 'self-type'). All the
50  * Builder methods of this class (such as {@link #named}) return this type, so
51  * that Builder methods of more derived classes can be chained onto them without
52  * casting.
53  * @param <G> The type of the generator to be passed to testers in the
54  * generated test suite. An instance of G should somehow provide an
55  * instance of the class under test, plus any other information required
56  * to parameterize the test.
57  *
58  * @author George van den Driessche
59  */
60 public abstract class FeatureSpecificTestSuiteBuilder<
61     B extends FeatureSpecificTestSuiteBuilder<B, G>, G> {
62   @SuppressWarnings("unchecked")
self()63   protected B self() {
64     return (B) this;
65   }
66 
67   // Test Data
68 
69   private G subjectGenerator;
70   // Gets run before every test.
71   private Runnable setUp;
72   // Gets run at the conclusion of every test.
73   private Runnable tearDown;
74 
usingGenerator(G subjectGenerator)75   protected B usingGenerator(G subjectGenerator) {
76     this.subjectGenerator = subjectGenerator;
77     return self();
78   }
79 
getSubjectGenerator()80   protected G getSubjectGenerator() {
81     return subjectGenerator;
82   }
83 
withSetUp(Runnable setUp)84   public B withSetUp(Runnable setUp) {
85     this.setUp = setUp;
86     return self();
87   }
88 
getSetUp()89   protected Runnable getSetUp() {
90     return setUp;
91   }
92 
withTearDown(Runnable tearDown)93   public B withTearDown(Runnable tearDown) {
94     this.tearDown = tearDown;
95     return self();
96   }
97 
getTearDown()98   protected Runnable getTearDown() {
99     return tearDown;
100   }
101 
102   // Features
103 
104   private Set<Feature<?>> features;
105 
106   /**
107    * Configures this builder to produce tests appropriate for the given
108    * features.
109    */
withFeatures(Feature<?>.... features)110   public B withFeatures(Feature<?>... features) {
111     return withFeatures(Arrays.asList(features));
112   }
113 
withFeatures(Iterable<? extends Feature<?>> features)114   public B withFeatures(Iterable<? extends Feature<?>> features) {
115     this.features = Helpers.copyToSet(features);
116     return self();
117   }
118 
getFeatures()119   protected Set<Feature<?>> getFeatures() {
120     return Collections.unmodifiableSet(features);
121   }
122 
123   // Name
124 
125   private String name;
126 
127   /** Configures this builder produce a TestSuite with the given name. */
named(String name)128   public B named(String name) {
129     if (name.contains("(")) {
130       throw new IllegalArgumentException("Eclipse hides all characters after "
131           + "'('; please use '[]' or other characters instead of parentheses");
132     }
133     this.name = name;
134     return self();
135   }
136 
getName()137   protected String getName() {
138     return name;
139   }
140 
141   // Test suppression
142 
143   private Set<Method> suppressedTests = new HashSet<Method>();
144 
145   /**
146    * Prevents the given methods from being run as part of the test suite.
147    *
148    * <em>Note:</em> in principle this should never need to be used, but it
149    * might be useful if the semantics of an implementation disagree in
150    * unforeseen ways with the semantics expected by a test, or to keep dependent
151    * builds clean in spite of an erroneous test.
152    */
suppressing(Method... methods)153   public B suppressing(Method... methods) {
154     return suppressing(Arrays.asList(methods));
155   }
156 
suppressing(Collection<Method> methods)157   public B suppressing(Collection<Method> methods) {
158     suppressedTests.addAll(methods);
159     return self();
160   }
161 
getSuppressedTests()162   protected Set<Method> getSuppressedTests() {
163     return suppressedTests;
164   }
165 
166   private static final Logger logger = Logger.getLogger(
167       FeatureSpecificTestSuiteBuilder.class.getName());
168 
169   /**
170    * Creates a runnable JUnit test suite based on the criteria already given.
171    */
172   /*
173    * Class parameters must be raw. This annotation should go on testerClass in
174    * the for loop, but the 1.5 javac crashes on annotations in for loops:
175    * <http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6294589>
176    */
177   @SuppressWarnings("unchecked")
createTestSuite()178   public TestSuite createTestSuite() {
179     checkCanCreate();
180 
181     logger.fine(" Testing: " + name);
182     logger.fine("Features: " + formatFeatureSet(features));
183 
184     FeatureUtil.addImpliedFeatures(features);
185 
186     logger.fine("Expanded: " + formatFeatureSet(features));
187 
188     // Class parameters must be raw.
189     List<Class<? extends AbstractTester>> testers = getTesters();
190 
191     TestSuite suite = new TestSuite(name);
192     for (Class<? extends AbstractTester> testerClass : testers) {
193       final TestSuite testerSuite = makeSuiteForTesterClass(
194           (Class<? extends AbstractTester<?>>) testerClass);
195       if (testerSuite.countTestCases() > 0) {
196         suite.addTest(testerSuite);
197       }
198     }
199     return suite;
200   }
201 
202   /**
203    * Throw {@link IllegalStateException} if {@link #createTestSuite()} can't
204    * be called yet.
205    */
checkCanCreate()206   protected void checkCanCreate() {
207     if (subjectGenerator == null) {
208       throw new IllegalStateException("Call using() before createTestSuite().");
209     }
210     if (name == null) {
211       throw new IllegalStateException("Call named() before createTestSuite().");
212     }
213     if (features == null) {
214       throw new IllegalStateException(
215           "Call withFeatures() before createTestSuite().");
216     }
217   }
218 
219   // Class parameters must be raw.
220   protected abstract List<Class<? extends AbstractTester>>
getTesters()221       getTesters();
222 
matches(Test test)223   private boolean matches(Test test) {
224     final Method method;
225     try {
226       method = extractMethod(test);
227     } catch (IllegalArgumentException e) {
228       logger.finer(Platform.format(
229           "%s: including by default: %s", test, e.getMessage()));
230       return true;
231     }
232     if (suppressedTests.contains(method)) {
233       logger.finer(Platform.format(
234           "%s: excluding because it was explicitly suppressed.", test));
235       return false;
236     }
237     final TesterRequirements requirements;
238     try {
239       requirements = FeatureUtil.getTesterRequirements(method);
240     } catch (ConflictingRequirementsException e) {
241       throw new RuntimeException(e);
242     }
243     if (!features.containsAll(requirements.getPresentFeatures())) {
244       if (logger.isLoggable(FINER)) {
245         Set<Feature<?>> missingFeatures =
246             Helpers.copyToSet(requirements.getPresentFeatures());
247         missingFeatures.removeAll(features);
248         logger.finer(Platform.format(
249             "%s: skipping because these features are absent: %s",
250            method, missingFeatures));
251       }
252       return false;
253     }
254     if (intersect(features, requirements.getAbsentFeatures())) {
255       if (logger.isLoggable(FINER)) {
256         Set<Feature<?>> unwantedFeatures =
257             Helpers.copyToSet(requirements.getAbsentFeatures());
258         unwantedFeatures.retainAll(features);
259         logger.finer(Platform.format(
260             "%s: skipping because these features are present: %s",
261             method, unwantedFeatures));
262       }
263       return false;
264     }
265     return true;
266   }
267 
intersect(Set<?> a, Set<?> b)268   private static boolean intersect(Set<?> a, Set<?> b) {
269     return !disjoint(a, b);
270   }
271 
extractMethod(Test test)272   private static Method extractMethod(Test test) {
273     if (test instanceof AbstractTester) {
274       AbstractTester<?> tester = (AbstractTester<?>) test;
275       return Platform.getMethod(tester.getClass(), tester.getTestMethodName());
276     } else if (test instanceof TestCase) {
277       TestCase testCase = (TestCase) test;
278       return Platform.getMethod(testCase.getClass(), testCase.getName());
279     } else {
280       throw new IllegalArgumentException(
281           "unable to extract method from test: not a TestCase.");
282     }
283   }
284 
makeSuiteForTesterClass( Class<? extends AbstractTester<?>> testerClass)285   protected TestSuite makeSuiteForTesterClass(
286       Class<? extends AbstractTester<?>> testerClass) {
287     final TestSuite candidateTests = getTemplateSuite(testerClass);
288     final TestSuite suite = filterSuite(candidateTests);
289 
290     Enumeration<?> allTests = suite.tests();
291     while (allTests.hasMoreElements()) {
292       Object test = allTests.nextElement();
293       if (test instanceof AbstractTester) {
294         @SuppressWarnings("unchecked")
295         AbstractTester<? super G> tester = (AbstractTester<? super G>) test;
296         tester.init(subjectGenerator, name, setUp, tearDown);
297       }
298     }
299 
300     return suite;
301   }
302 
303   private static final Map<Class<? extends AbstractTester<?>>, TestSuite>
304       templateSuiteForClass =
305           new HashMap<Class<? extends AbstractTester<?>>, TestSuite>();
306 
getTemplateSuite( Class<? extends AbstractTester<?>> testerClass)307   private static TestSuite getTemplateSuite(
308       Class<? extends AbstractTester<?>> testerClass) {
309     synchronized (templateSuiteForClass) {
310       TestSuite suite = templateSuiteForClass.get(testerClass);
311       if (suite == null) {
312         suite = new TestSuite(testerClass);
313         templateSuiteForClass.put(testerClass, suite);
314       }
315       return suite;
316     }
317   }
318 
filterSuite(TestSuite suite)319   private TestSuite filterSuite(TestSuite suite) {
320     TestSuite filtered = new TestSuite(suite.getName());
321     final Enumeration<?> tests = suite.tests();
322     while (tests.hasMoreElements()) {
323       Test test = (Test) tests.nextElement();
324       if (matches(test)) {
325         filtered.addTest(test);
326       }
327     }
328     return filtered;
329   }
330 
formatFeatureSet(Set<? extends Feature<?>> features)331   protected static String formatFeatureSet(Set<? extends Feature<?>> features) {
332     List<String> temp = new ArrayList<String>();
333     for (Feature<?> feature : features) {
334       Object featureAsObject = feature; // to work around bogus JDK warning
335       if (featureAsObject instanceof Enum) {
336         Enum<?> f = (Enum<?>) featureAsObject;
337         temp.add(Platform.classGetSimpleName(
338             f.getDeclaringClass()) + "." + feature);
339       } else {
340         temp.add(feature.toString());
341       }
342     }
343     return temp.toString();
344   }
345 }
346