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 java.util.Collections.disjoint; 020import static java.util.logging.Level.FINER; 021 022import com.google.common.annotations.GwtIncompatible; 023import com.google.common.collect.testing.features.ConflictingRequirementsException; 024import com.google.common.collect.testing.features.Feature; 025import com.google.common.collect.testing.features.FeatureUtil; 026import com.google.common.collect.testing.features.TesterRequirements; 027import java.lang.reflect.Method; 028import java.util.ArrayList; 029import java.util.Arrays; 030import java.util.Collection; 031import java.util.Collections; 032import java.util.Enumeration; 033import java.util.HashSet; 034import java.util.LinkedHashSet; 035import java.util.List; 036import java.util.Set; 037import java.util.logging.Logger; 038import junit.framework.Test; 039import junit.framework.TestCase; 040import junit.framework.TestSuite; 041 042/** 043 * Creates, based on your criteria, a JUnit test suite that exhaustively tests the object generated 044 * by a G, selecting appropriate tests by matching them against specified features. 045 * 046 * @param <B> The concrete type of this builder (the 'self-type'). All the Builder methods of this 047 * class (such as {@link #named}) return this type, so that Builder methods of more derived 048 * classes can be chained onto them without casting. 049 * @param <G> The type of the generator to be passed to testers in the generated test suite. An 050 * instance of G should somehow provide an instance of the class under test, plus any other 051 * information required to parameterize the test. 052 * @author George van den Driessche 053 */ 054@GwtIncompatible 055public abstract class FeatureSpecificTestSuiteBuilder< 056 B extends FeatureSpecificTestSuiteBuilder<B, G>, G> { 057 @SuppressWarnings("unchecked") 058 protected B self() { 059 return (B) this; 060 } 061 062 // Test Data 063 064 private G subjectGenerator; 065 // Gets run before every test. 066 private Runnable setUp; 067 // Gets run at the conclusion of every test. 068 private Runnable tearDown; 069 070 protected B usingGenerator(G subjectGenerator) { 071 this.subjectGenerator = subjectGenerator; 072 return self(); 073 } 074 075 public G getSubjectGenerator() { 076 return subjectGenerator; 077 } 078 079 public B withSetUp(Runnable setUp) { 080 this.setUp = setUp; 081 return self(); 082 } 083 084 protected Runnable getSetUp() { 085 return setUp; 086 } 087 088 public B withTearDown(Runnable tearDown) { 089 this.tearDown = tearDown; 090 return self(); 091 } 092 093 protected Runnable getTearDown() { 094 return tearDown; 095 } 096 097 // Features 098 099 private 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 */ 105 public B withFeatures(Feature<?>... features) { 106 return withFeatures(Arrays.asList(features)); 107 } 108 109 public B withFeatures(Iterable<? extends Feature<?>> features) { 110 for (Feature<?> feature : features) { 111 this.features.add(feature); 112 } 113 return self(); 114 } 115 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. */ 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 135 public String getName() { 136 return name; 137 } 138 139 // Test suppression 140 141 private 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 */ 150 public B suppressing(Method... methods) { 151 return suppressing(Arrays.asList(methods)); 152 } 153 154 public B suppressing(Collection<Method> methods) { 155 suppressedTests.addAll(methods); 156 return self(); 157 } 158 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") 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. */ 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. 211 protected abstract List<Class<? extends AbstractTester>> getTesters(); 212 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 254 private static boolean intersect(Set<?> a, Set<?> b) { 255 return !disjoint(a, b); 256 } 257 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 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 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 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}