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.collect.testing.Helpers.copyToSet; 020import static com.google.common.collect.testing.Helpers.getMethod; 021import static com.google.common.collect.testing.features.FeatureUtil.addImpliedFeatures; 022import static java.util.Arrays.asList; 023import static java.util.Collections.disjoint; 024import static java.util.Collections.unmodifiableSet; 025import static java.util.logging.Level.FINER; 026 027import com.google.common.annotations.GwtIncompatible; 028import com.google.common.collect.testing.features.ConflictingRequirementsException; 029import com.google.common.collect.testing.features.Feature; 030import com.google.common.collect.testing.features.FeatureUtil; 031import com.google.common.collect.testing.features.TesterRequirements; 032import com.google.errorprone.annotations.CanIgnoreReturnValue; 033import java.lang.reflect.Method; 034import java.util.ArrayList; 035import java.util.Collection; 036import java.util.Enumeration; 037import java.util.HashSet; 038import java.util.LinkedHashSet; 039import java.util.List; 040import java.util.Set; 041import java.util.logging.Logger; 042import junit.framework.Test; 043import junit.framework.TestCase; 044import junit.framework.TestSuite; 045import org.checkerframework.checker.nullness.qual.Nullable; 046 047/** 048 * Creates, based on your criteria, a JUnit test suite that exhaustively tests the object generated 049 * by a G, selecting appropriate tests by matching them against specified features. 050 * 051 * @param <B> The concrete type of this builder (the 'self-type'). All the Builder methods of this 052 * class (such as {@link #named}) return this type, so that Builder methods of more derived 053 * classes can be chained onto them without casting. 054 * @param <G> The type of the generator to be passed to testers in the generated test suite. An 055 * instance of G should somehow provide an instance of the class under test, plus any other 056 * information required to parameterize the test. 057 * @author George van den Driessche 058 */ 059@GwtIncompatible 060public abstract class FeatureSpecificTestSuiteBuilder< 061 B extends FeatureSpecificTestSuiteBuilder<B, G>, G> { 062 @SuppressWarnings("unchecked") 063 protected B self() { 064 return (B) this; 065 } 066 067 // Test Data 068 069 private @Nullable G subjectGenerator; 070 // Gets run before every test. 071 private Runnable setUp; 072 // Gets run at the conclusion of every test. 073 private Runnable tearDown; 074 075 @CanIgnoreReturnValue 076 protected B usingGenerator(G subjectGenerator) { 077 this.subjectGenerator = subjectGenerator; 078 return self(); 079 } 080 081 public G getSubjectGenerator() { 082 return subjectGenerator; 083 } 084 085 @CanIgnoreReturnValue 086 public B withSetUp(Runnable setUp) { 087 this.setUp = setUp; 088 return self(); 089 } 090 091 public Runnable getSetUp() { 092 return setUp; 093 } 094 095 @CanIgnoreReturnValue 096 public B withTearDown(Runnable tearDown) { 097 this.tearDown = tearDown; 098 return self(); 099 } 100 101 public Runnable getTearDown() { 102 return tearDown; 103 } 104 105 // Features 106 107 private final Set<Feature<?>> features = new LinkedHashSet<>(); 108 109 /** 110 * Configures this builder to produce tests appropriate for the given features. This method may be 111 * called more than once to add features in multiple groups. 112 */ 113 @CanIgnoreReturnValue 114 public B withFeatures(Feature<?>... features) { 115 return withFeatures(asList(features)); 116 } 117 118 @CanIgnoreReturnValue 119 public B withFeatures(Iterable<? extends Feature<?>> features) { 120 for (Feature<?> feature : features) { 121 this.features.add(feature); 122 } 123 return self(); 124 } 125 126 public Set<Feature<?>> getFeatures() { 127 return unmodifiableSet(features); 128 } 129 130 // Name 131 132 private @Nullable String name; 133 134 /** Configures this builder produce a TestSuite with the given name. */ 135 @CanIgnoreReturnValue 136 public B named(String name) { 137 if (name.contains("(")) { 138 throw new IllegalArgumentException( 139 "Eclipse hides all characters after " 140 + "'('; please use '[]' or other characters instead of parentheses"); 141 } 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}