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 17 package com.google.common.collect.testing; 18 19 import static java.util.Collections.disjoint; 20 import static java.util.logging.Level.FINER; 21 22 import com.google.common.annotations.GwtIncompatible; 23 import com.google.common.collect.testing.features.ConflictingRequirementsException; 24 import com.google.common.collect.testing.features.Feature; 25 import com.google.common.collect.testing.features.FeatureUtil; 26 import com.google.common.collect.testing.features.TesterRequirements; 27 import java.lang.reflect.Method; 28 import java.util.ArrayList; 29 import java.util.Arrays; 30 import java.util.Collection; 31 import java.util.Collections; 32 import java.util.Enumeration; 33 import java.util.HashSet; 34 import java.util.LinkedHashSet; 35 import java.util.List; 36 import java.util.Set; 37 import java.util.logging.Logger; 38 import junit.framework.Test; 39 import junit.framework.TestCase; 40 import junit.framework.TestSuite; 41 42 /** 43 * Creates, based on your criteria, a JUnit test suite that exhaustively tests the object generated 44 * by a G, selecting appropriate tests by matching them against specified features. 45 * 46 * @param <B> The concrete type of this builder (the 'self-type'). All the Builder methods of this 47 * class (such as {@link #named}) return this type, so that Builder methods of more derived 48 * classes can be chained onto them without casting. 49 * @param <G> The type of the generator to be passed to testers in the generated test suite. An 50 * instance of G should somehow provide an instance of the class under test, plus any other 51 * information required to parameterize the test. 52 * @author George van den Driessche 53 */ 54 @GwtIncompatible 55 public abstract class FeatureSpecificTestSuiteBuilder< 56 B extends FeatureSpecificTestSuiteBuilder<B, G>, G> { 57 @SuppressWarnings("unchecked") self()58 protected B self() { 59 return (B) this; 60 } 61 62 // Test Data 63 64 private G subjectGenerator; 65 // Gets run before every test. 66 private Runnable setUp; 67 // Gets run at the conclusion of every test. 68 private Runnable tearDown; 69 usingGenerator(G subjectGenerator)70 protected B usingGenerator(G subjectGenerator) { 71 this.subjectGenerator = subjectGenerator; 72 return self(); 73 } 74 getSubjectGenerator()75 public G getSubjectGenerator() { 76 return subjectGenerator; 77 } 78 withSetUp(Runnable setUp)79 public B withSetUp(Runnable setUp) { 80 this.setUp = setUp; 81 return self(); 82 } 83 getSetUp()84 public Runnable getSetUp() { 85 return setUp; 86 } 87 withTearDown(Runnable tearDown)88 public B withTearDown(Runnable tearDown) { 89 this.tearDown = tearDown; 90 return self(); 91 } 92 getTearDown()93 public Runnable getTearDown() { 94 return tearDown; 95 } 96 97 // Features 98 99 private final 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 */ withFeatures(Feature<?>.... features)105 public B withFeatures(Feature<?>... features) { 106 return withFeatures(Arrays.asList(features)); 107 } 108 withFeatures(Iterable<? extends Feature<?>> features)109 public B withFeatures(Iterable<? extends Feature<?>> features) { 110 for (Feature<?> feature : features) { 111 this.features.add(feature); 112 } 113 return self(); 114 } 115 getFeatures()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. */ named(String 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 getName()135 public String getName() { 136 return name; 137 } 138 139 // Test suppression 140 141 private final 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 */ suppressing(Method... methods)150 public B suppressing(Method... methods) { 151 return suppressing(Arrays.asList(methods)); 152 } 153 suppressing(Collection<Method> methods)154 public B suppressing(Collection<Method> methods) { 155 suppressedTests.addAll(methods); 156 return self(); 157 } 158 getSuppressedTests()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") createTestSuite()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. */ checkCanCreate()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. getTesters()211 protected abstract List<Class<? extends AbstractTester>> getTesters(); 212 matches(Test test)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 intersect(Set<?> a, Set<?> b)254 private static boolean intersect(Set<?> a, Set<?> b) { 255 return !disjoint(a, b); 256 } 257 extractMethod(Test test)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 makeSuiteForTesterClass(Class<? extends AbstractTester<?>> testerClass)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 filterSuite(TestSuite suite)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 formatFeatureSet(Set<? extends Feature<?>> features)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 } 313