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