FeatureSpecificTestSuiteBuilder.java revision dbd967a6e5c96cc1a97c5521f88dc1564ba2f81b
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
17package com.google.common.collect.testing;
18
19import static java.util.Collections.disjoint;
20import static java.util.logging.Level.FINER;
21
22import com.google.common.collect.testing.features.ConflictingRequirementsException;
23import com.google.common.collect.testing.features.Feature;
24import com.google.common.collect.testing.features.FeatureUtil;
25import com.google.common.collect.testing.features.TesterRequirements;
26
27import junit.framework.Test;
28import junit.framework.TestCase;
29import junit.framework.TestSuite;
30
31import java.lang.reflect.Method;
32import java.util.ArrayList;
33import java.util.Arrays;
34import java.util.Collection;
35import java.util.Collections;
36import java.util.Enumeration;
37import java.util.HashMap;
38import java.util.HashSet;
39import java.util.List;
40import java.util.Map;
41import java.util.Set;
42import 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 */
60public abstract class FeatureSpecificTestSuiteBuilder<
61    B extends FeatureSpecificTestSuiteBuilder<B, G>, G> {
62  @SuppressWarnings("unchecked")
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
75  protected B usingGenerator(G subjectGenerator) {
76    this.subjectGenerator = subjectGenerator;
77    return self();
78  }
79
80  protected G getSubjectGenerator() {
81    return subjectGenerator;
82  }
83
84  public B withSetUp(Runnable setUp) {
85    this.setUp = setUp;
86    return self();
87  }
88
89  protected Runnable getSetUp() {
90    return setUp;
91  }
92
93  public B withTearDown(Runnable tearDown) {
94    this.tearDown = tearDown;
95    return self();
96  }
97
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   */
110  public B withFeatures(Feature<?>... features) {
111    return withFeatures(Arrays.asList(features));
112  }
113
114  public B withFeatures(Iterable<? extends Feature<?>> features) {
115    this.features = Helpers.copyToSet(features);
116    return self();
117  }
118
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. */
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
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   */
153  public B suppressing(Method... methods) {
154    return suppressing(Arrays.asList(methods));
155  }
156
157  public B suppressing(Collection<Method> methods) {
158    suppressedTests.addAll(methods);
159    return self();
160  }
161
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")
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   */
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>>
221      getTesters();
222
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
268  private static boolean intersect(Set<?> a, Set<?> b) {
269    return !disjoint(a, b);
270  }
271
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
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
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
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
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