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