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