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.collect.testing.features.ConflictingRequirementsException; 23 import com.google.common.collect.testing.features.Feature; 24 import com.google.common.collect.testing.features.FeatureUtil; 25 import com.google.common.collect.testing.features.TesterRequirements; 26 27 import junit.framework.Test; 28 import junit.framework.TestCase; 29 import junit.framework.TestSuite; 30 31 import java.lang.reflect.Method; 32 import java.util.ArrayList; 33 import java.util.Arrays; 34 import java.util.Collection; 35 import java.util.Collections; 36 import java.util.Enumeration; 37 import java.util.HashSet; 38 import java.util.LinkedHashSet; 39 import java.util.List; 40 import java.util.Set; 41 import java.util.logging.Logger; 42 43 /** 44 * Creates, based on your criteria, a JUnit test suite that exhaustively tests 45 * the object generated by a G, selecting appropriate tests by matching them 46 * against specified features. 47 * 48 * @param <B> The concrete type of this builder (the 'self-type'). All the 49 * Builder methods of this class (such as {@link #named}) return this type, so 50 * that Builder methods of more derived classes can be chained onto them without 51 * casting. 52 * @param <G> The type of the generator to be passed to testers in the 53 * generated test suite. An instance of G should somehow provide an 54 * instance of the class under test, plus any other information required 55 * to parameterize the test. 56 * 57 * @author George van den Driessche 58 */ 59 public abstract class FeatureSpecificTestSuiteBuilder< 60 B extends FeatureSpecificTestSuiteBuilder<B, G>, G> { 61 @SuppressWarnings("unchecked") self()62 protected B self() { 63 return (B) this; 64 } 65 66 // Test Data 67 68 private G subjectGenerator; 69 // Gets run before every test. 70 private Runnable setUp; 71 // Gets run at the conclusion of every test. 72 private Runnable tearDown; 73 usingGenerator(G subjectGenerator)74 protected B usingGenerator(G subjectGenerator) { 75 this.subjectGenerator = subjectGenerator; 76 return self(); 77 } 78 getSubjectGenerator()79 public G getSubjectGenerator() { 80 return subjectGenerator; 81 } 82 withSetUp(Runnable setUp)83 public B withSetUp(Runnable setUp) { 84 this.setUp = setUp; 85 return self(); 86 } 87 getSetUp()88 protected Runnable getSetUp() { 89 return setUp; 90 } 91 withTearDown(Runnable tearDown)92 public B withTearDown(Runnable tearDown) { 93 this.tearDown = tearDown; 94 return self(); 95 } 96 getTearDown()97 protected Runnable getTearDown() { 98 return tearDown; 99 } 100 101 // Features 102 103 private Set<Feature<?>> features = new LinkedHashSet<Feature<?>>(); 104 105 /** 106 * Configures this builder to produce tests appropriate for the given 107 * features. This method may be called more than once to add features 108 * in multiple groups. 109 */ withFeatures(Feature<?>.... features)110 public B withFeatures(Feature<?>... features) { 111 return withFeatures(Arrays.asList(features)); 112 } 113 withFeatures(Iterable<? extends Feature<?>> features)114 public B withFeatures(Iterable<? extends Feature<?>> features) { 115 for (Feature<?> feature : features) { 116 this.features.add(feature); 117 } 118 return self(); 119 } 120 getFeatures()121 public Set<Feature<?>> getFeatures() { 122 return Collections.unmodifiableSet(features); 123 } 124 125 // Name 126 127 private String name; 128 129 /** Configures this builder produce a TestSuite with the given name. */ named(String name)130 public B named(String name) { 131 if (name.contains("(")) { 132 throw new IllegalArgumentException("Eclipse hides all characters after " 133 + "'('; please use '[]' or other characters instead of parentheses"); 134 } 135 this.name = name; 136 return self(); 137 } 138 getName()139 public String getName() { 140 return name; 141 } 142 143 // Test suppression 144 145 private Set<Method> suppressedTests = new HashSet<Method>(); 146 147 /** 148 * Prevents the given methods from being run as part of the test suite. 149 * 150 * <em>Note:</em> in principle this should never need to be used, but it 151 * might be useful if the semantics of an implementation disagree in 152 * unforeseen ways with the semantics expected by a test, or to keep dependent 153 * builds clean in spite of an erroneous test. 154 */ suppressing(Method... methods)155 public B suppressing(Method... methods) { 156 return suppressing(Arrays.asList(methods)); 157 } 158 suppressing(Collection<Method> methods)159 public B suppressing(Collection<Method> methods) { 160 suppressedTests.addAll(methods); 161 return self(); 162 } 163 getSuppressedTests()164 public Set<Method> getSuppressedTests() { 165 return suppressedTests; 166 } 167 168 private static final Logger logger = Logger.getLogger( 169 FeatureSpecificTestSuiteBuilder.class.getName()); 170 171 /** 172 * Creates a runnable JUnit test suite based on the criteria already given. 173 */ 174 /* 175 * Class parameters must be raw. This annotation should go on testerClass in 176 * the for loop, but the 1.5 javac crashes on annotations in for loops: 177 * <http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6294589> 178 */ 179 @SuppressWarnings("unchecked") createTestSuite()180 public TestSuite createTestSuite() { 181 checkCanCreate(); 182 183 logger.fine(" Testing: " + name); 184 logger.fine("Features: " + formatFeatureSet(features)); 185 186 FeatureUtil.addImpliedFeatures(features); 187 188 logger.fine("Expanded: " + formatFeatureSet(features)); 189 190 // Class parameters must be raw. 191 List<Class<? extends AbstractTester>> testers = getTesters(); 192 193 TestSuite suite = new TestSuite(name); 194 for (Class<? extends AbstractTester> testerClass : testers) { 195 final TestSuite testerSuite = makeSuiteForTesterClass( 196 (Class<? extends AbstractTester<?>>) testerClass); 197 if (testerSuite.countTestCases() > 0) { 198 suite.addTest(testerSuite); 199 } 200 } 201 return suite; 202 } 203 204 /** 205 * Throw {@link IllegalStateException} if {@link #createTestSuite()} can't 206 * be called yet. 207 */ checkCanCreate()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( 217 "Call withFeatures() before createTestSuite()."); 218 } 219 } 220 221 // Class parameters must be raw. 222 protected abstract List<Class<? extends AbstractTester>> getTesters()223 getTesters(); 224 matches(Test test)225 private boolean matches(Test test) { 226 final Method method; 227 try { 228 method = extractMethod(test); 229 } catch (IllegalArgumentException e) { 230 logger.finer(Platform.format( 231 "%s: including by default: %s", test, e.getMessage())); 232 return true; 233 } 234 if (suppressedTests.contains(method)) { 235 logger.finer(Platform.format( 236 "%s: excluding because it was explicitly suppressed.", test)); 237 return false; 238 } 239 final TesterRequirements requirements; 240 try { 241 requirements = FeatureUtil.getTesterRequirements(method); 242 } catch (ConflictingRequirementsException e) { 243 throw new RuntimeException(e); 244 } 245 if (!features.containsAll(requirements.getPresentFeatures())) { 246 if (logger.isLoggable(FINER)) { 247 Set<Feature<?>> missingFeatures = 248 Helpers.copyToSet(requirements.getPresentFeatures()); 249 missingFeatures.removeAll(features); 250 logger.finer(Platform.format( 251 "%s: skipping because these features are absent: %s", 252 method, missingFeatures)); 253 } 254 return false; 255 } 256 if (intersect(features, requirements.getAbsentFeatures())) { 257 if (logger.isLoggable(FINER)) { 258 Set<Feature<?>> unwantedFeatures = 259 Helpers.copyToSet(requirements.getAbsentFeatures()); 260 unwantedFeatures.retainAll(features); 261 logger.finer(Platform.format( 262 "%s: skipping because these features are present: %s", 263 method, unwantedFeatures)); 264 } 265 return false; 266 } 267 return true; 268 } 269 intersect(Set<?> a, Set<?> b)270 private static boolean intersect(Set<?> a, Set<?> b) { 271 return !disjoint(a, b); 272 } 273 extractMethod(Test test)274 private static Method extractMethod(Test test) { 275 if (test instanceof AbstractTester) { 276 AbstractTester<?> tester = (AbstractTester<?>) test; 277 return Helpers.getMethod(tester.getClass(), tester.getTestMethodName()); 278 } else if (test instanceof TestCase) { 279 TestCase testCase = (TestCase) test; 280 return Helpers.getMethod(testCase.getClass(), testCase.getName()); 281 } else { 282 throw new IllegalArgumentException( 283 "unable to extract method from test: not a TestCase."); 284 } 285 } 286 makeSuiteForTesterClass( Class<? extends AbstractTester<?>> testerClass)287 protected TestSuite makeSuiteForTesterClass( 288 Class<? extends AbstractTester<?>> testerClass) { 289 final TestSuite candidateTests = new TestSuite(testerClass); 290 final TestSuite suite = filterSuite(candidateTests); 291 292 Enumeration<?> allTests = suite.tests(); 293 while (allTests.hasMoreElements()) { 294 Object test = allTests.nextElement(); 295 if (test instanceof AbstractTester) { 296 @SuppressWarnings("unchecked") 297 AbstractTester<? super G> tester = (AbstractTester<? super G>) test; 298 tester.init(subjectGenerator, name, setUp, tearDown); 299 } 300 } 301 302 return suite; 303 } 304 filterSuite(TestSuite suite)305 private TestSuite filterSuite(TestSuite suite) { 306 TestSuite filtered = new TestSuite(suite.getName()); 307 final Enumeration<?> tests = suite.tests(); 308 while (tests.hasMoreElements()) { 309 Test test = (Test) tests.nextElement(); 310 if (matches(test)) { 311 filtered.addTest(test); 312 } 313 } 314 return filtered; 315 } 316 formatFeatureSet(Set<? extends Feature<?>> features)317 protected static String formatFeatureSet(Set<? extends Feature<?>> features) { 318 List<String> temp = new ArrayList<String>(); 319 for (Feature<?> feature : features) { 320 Object featureAsObject = feature; // to work around bogus JDK warning 321 if (featureAsObject instanceof Enum) { 322 Enum<?> f = (Enum<?>) featureAsObject; 323 temp.add(f.getDeclaringClass().getSimpleName() + "." + feature); 324 } else { 325 temp.add(feature.toString()); 326 } 327 } 328 return temp.toString(); 329 } 330 } 331