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