001/*
002 * Copyright (C) 2008 The Guava Authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.google.common.collect.testing;
018
019import static java.util.Collections.disjoint;
020import static java.util.logging.Level.FINER;
021
022import com.google.common.annotations.GwtIncompatible;
023import com.google.common.collect.testing.features.ConflictingRequirementsException;
024import com.google.common.collect.testing.features.Feature;
025import com.google.common.collect.testing.features.FeatureUtil;
026import com.google.common.collect.testing.features.TesterRequirements;
027import java.lang.reflect.Method;
028import java.util.ArrayList;
029import java.util.Arrays;
030import java.util.Collection;
031import java.util.Collections;
032import java.util.Enumeration;
033import java.util.HashSet;
034import java.util.LinkedHashSet;
035import java.util.List;
036import java.util.Set;
037import java.util.logging.Logger;
038import junit.framework.Test;
039import junit.framework.TestCase;
040import junit.framework.TestSuite;
041
042/**
043 * Creates, based on your criteria, a JUnit test suite that exhaustively tests the object generated
044 * by a G, selecting appropriate tests by matching them against specified features.
045 *
046 * @param <B> The concrete type of this builder (the 'self-type'). All the Builder methods of this
047 *     class (such as {@link #named}) return this type, so that Builder methods of more derived
048 *     classes can be chained onto them without casting.
049 * @param <G> The type of the generator to be passed to testers in the generated test suite. An
050 *     instance of G should somehow provide an instance of the class under test, plus any other
051 *     information required to parameterize the test.
052 * @author George van den Driessche
053 */
054@GwtIncompatible
055public abstract class FeatureSpecificTestSuiteBuilder<
056    B extends FeatureSpecificTestSuiteBuilder<B, G>, G> {
057  @SuppressWarnings("unchecked")
058  protected B self() {
059    return (B) this;
060  }
061
062  // Test Data
063
064  private G subjectGenerator;
065  // Gets run before every test.
066  private Runnable setUp;
067  // Gets run at the conclusion of every test.
068  private Runnable tearDown;
069
070  protected B usingGenerator(G subjectGenerator) {
071    this.subjectGenerator = subjectGenerator;
072    return self();
073  }
074
075  public G getSubjectGenerator() {
076    return subjectGenerator;
077  }
078
079  public B withSetUp(Runnable setUp) {
080    this.setUp = setUp;
081    return self();
082  }
083
084  protected Runnable getSetUp() {
085    return setUp;
086  }
087
088  public B withTearDown(Runnable tearDown) {
089    this.tearDown = tearDown;
090    return self();
091  }
092
093  protected Runnable getTearDown() {
094    return tearDown;
095  }
096
097  // Features
098
099  private Set<Feature<?>> features = new LinkedHashSet<>();
100
101  /**
102   * Configures this builder to produce tests appropriate for the given features. This method may be
103   * called more than once to add features in multiple groups.
104   */
105  public B withFeatures(Feature<?>... features) {
106    return withFeatures(Arrays.asList(features));
107  }
108
109  public B withFeatures(Iterable<? extends Feature<?>> features) {
110    for (Feature<?> feature : features) {
111      this.features.add(feature);
112    }
113    return self();
114  }
115
116  public Set<Feature<?>> getFeatures() {
117    return Collections.unmodifiableSet(features);
118  }
119
120  // Name
121
122  private String name;
123
124  /** Configures this builder produce a TestSuite with the given name. */
125  public B named(String name) {
126    if (name.contains("(")) {
127      throw new IllegalArgumentException(
128          "Eclipse hides all characters after "
129              + "'('; please use '[]' or other characters instead of parentheses");
130    }
131    this.name = name;
132    return self();
133  }
134
135  public String getName() {
136    return name;
137  }
138
139  // Test suppression
140
141  private Set<Method> suppressedTests = new HashSet<>();
142
143  /**
144   * Prevents the given methods from being run as part of the test suite.
145   *
146   * <p><em>Note:</em> in principle this should never need to be used, but it might be useful if the
147   * semantics of an implementation disagree in unforeseen ways with the semantics expected by a
148   * test, or to keep dependent builds clean in spite of an erroneous test.
149   */
150  public B suppressing(Method... methods) {
151    return suppressing(Arrays.asList(methods));
152  }
153
154  public B suppressing(Collection<Method> methods) {
155    suppressedTests.addAll(methods);
156    return self();
157  }
158
159  public Set<Method> getSuppressedTests() {
160    return suppressedTests;
161  }
162
163  private static final Logger logger =
164      Logger.getLogger(FeatureSpecificTestSuiteBuilder.class.getName());
165
166  /** Creates a runnable JUnit test suite based on the criteria already given. */
167  /*
168   * Class parameters must be raw. This annotation should go on testerClass in
169   * the for loop, but the 1.5 javac crashes on annotations in for loops:
170   * <http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6294589>
171   */
172  @SuppressWarnings("unchecked")
173  public TestSuite createTestSuite() {
174    checkCanCreate();
175
176    logger.fine(" Testing: " + name);
177    logger.fine("Features: " + formatFeatureSet(features));
178
179    FeatureUtil.addImpliedFeatures(features);
180
181    logger.fine("Expanded: " + formatFeatureSet(features));
182
183    // Class parameters must be raw.
184    List<Class<? extends AbstractTester>> testers = getTesters();
185
186    TestSuite suite = new TestSuite(name);
187    for (Class<? extends AbstractTester> testerClass : testers) {
188      final TestSuite testerSuite =
189          makeSuiteForTesterClass((Class<? extends AbstractTester<?>>) testerClass);
190      if (testerSuite.countTestCases() > 0) {
191        suite.addTest(testerSuite);
192      }
193    }
194    return suite;
195  }
196
197  /** Throw {@link IllegalStateException} if {@link #createTestSuite()} can't be called yet. */
198  protected void checkCanCreate() {
199    if (subjectGenerator == null) {
200      throw new IllegalStateException("Call using() before createTestSuite().");
201    }
202    if (name == null) {
203      throw new IllegalStateException("Call named() before createTestSuite().");
204    }
205    if (features == null) {
206      throw new IllegalStateException("Call withFeatures() before createTestSuite().");
207    }
208  }
209
210  // Class parameters must be raw.
211  protected abstract List<Class<? extends AbstractTester>> getTesters();
212
213  private boolean matches(Test test) {
214    final Method method;
215    try {
216      method = extractMethod(test);
217    } catch (IllegalArgumentException e) {
218      logger.finer(Platform.format("%s: including by default: %s", test, e.getMessage()));
219      return true;
220    }
221    if (suppressedTests.contains(method)) {
222      logger.finer(Platform.format("%s: excluding because it was explicitly suppressed.", test));
223      return false;
224    }
225    final TesterRequirements requirements;
226    try {
227      requirements = FeatureUtil.getTesterRequirements(method);
228    } catch (ConflictingRequirementsException e) {
229      throw new RuntimeException(e);
230    }
231    if (!features.containsAll(requirements.getPresentFeatures())) {
232      if (logger.isLoggable(FINER)) {
233        Set<Feature<?>> missingFeatures = Helpers.copyToSet(requirements.getPresentFeatures());
234        missingFeatures.removeAll(features);
235        logger.finer(
236            Platform.format(
237                "%s: skipping because these features are absent: %s", method, missingFeatures));
238      }
239      return false;
240    }
241    if (intersect(features, requirements.getAbsentFeatures())) {
242      if (logger.isLoggable(FINER)) {
243        Set<Feature<?>> unwantedFeatures = Helpers.copyToSet(requirements.getAbsentFeatures());
244        unwantedFeatures.retainAll(features);
245        logger.finer(
246            Platform.format(
247                "%s: skipping because these features are present: %s", method, unwantedFeatures));
248      }
249      return false;
250    }
251    return true;
252  }
253
254  private static boolean intersect(Set<?> a, Set<?> b) {
255    return !disjoint(a, b);
256  }
257
258  private static Method extractMethod(Test test) {
259    if (test instanceof AbstractTester) {
260      AbstractTester<?> tester = (AbstractTester<?>) test;
261      return Helpers.getMethod(tester.getClass(), tester.getTestMethodName());
262    } else if (test instanceof TestCase) {
263      TestCase testCase = (TestCase) test;
264      return Helpers.getMethod(testCase.getClass(), testCase.getName());
265    } else {
266      throw new IllegalArgumentException("unable to extract method from test: not a TestCase.");
267    }
268  }
269
270  protected TestSuite makeSuiteForTesterClass(Class<? extends AbstractTester<?>> testerClass) {
271    final TestSuite candidateTests = new TestSuite(testerClass);
272    final TestSuite suite = filterSuite(candidateTests);
273
274    Enumeration<?> allTests = suite.tests();
275    while (allTests.hasMoreElements()) {
276      Object test = allTests.nextElement();
277      if (test instanceof AbstractTester) {
278        @SuppressWarnings("unchecked")
279        AbstractTester<? super G> tester = (AbstractTester<? super G>) test;
280        tester.init(subjectGenerator, name, setUp, tearDown);
281      }
282    }
283
284    return suite;
285  }
286
287  private TestSuite filterSuite(TestSuite suite) {
288    TestSuite filtered = new TestSuite(suite.getName());
289    final Enumeration<?> tests = suite.tests();
290    while (tests.hasMoreElements()) {
291      Test test = (Test) tests.nextElement();
292      if (matches(test)) {
293        filtered.addTest(test);
294      }
295    }
296    return filtered;
297  }
298
299  protected static String formatFeatureSet(Set<? extends Feature<?>> features) {
300    List<String> temp = new ArrayList<>();
301    for (Feature<?> feature : features) {
302      Object featureAsObject = feature; // to work around bogus JDK warning
303      if (featureAsObject instanceof Enum) {
304        Enum<?> f = (Enum<?>) featureAsObject;
305        temp.add(f.getDeclaringClass().getSimpleName() + "." + feature);
306      } else {
307        temp.add(feature.toString());
308      }
309    }
310    return temp.toString();
311  }
312}