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 com.google.errorprone.annotations.CanIgnoreReturnValue; 028import java.lang.reflect.Method; 029import java.util.ArrayList; 030import java.util.Arrays; 031import java.util.Collection; 032import java.util.Collections; 033import java.util.Enumeration; 034import java.util.HashSet; 035import java.util.LinkedHashSet; 036import java.util.List; 037import java.util.Set; 038import java.util.logging.Logger; 039import junit.framework.Test; 040import junit.framework.TestCase; 041import junit.framework.TestSuite; 042import org.checkerframework.checker.nullness.qual.Nullable; 043 044/** 045 * Creates, based on your criteria, a JUnit test suite that exhaustively tests the object generated 046 * by a G, selecting appropriate tests by matching them against specified features. 047 * 048 * @param <B> The concrete type of this builder (the 'self-type'). All the Builder methods of this 049 * class (such as {@link #named}) return this type, so that Builder methods of more derived 050 * classes can be chained onto them without casting. 051 * @param <G> The type of the generator to be passed to testers in the generated test suite. An 052 * instance of G should somehow provide an instance of the class under test, plus any other 053 * information required to parameterize the test. 054 * @author George van den Driessche 055 */ 056@GwtIncompatible 057public abstract class FeatureSpecificTestSuiteBuilder< 058 B extends FeatureSpecificTestSuiteBuilder<B, G>, G> { 059 @SuppressWarnings("unchecked") 060 protected B self() { 061 return (B) this; 062 } 063 064 // Test Data 065 066 private @Nullable G subjectGenerator; 067 // Gets run before every test. 068 private Runnable setUp; 069 // Gets run at the conclusion of every test. 070 private Runnable tearDown; 071 072 @CanIgnoreReturnValue 073 protected B usingGenerator(G subjectGenerator) { 074 this.subjectGenerator = subjectGenerator; 075 return self(); 076 } 077 078 public G getSubjectGenerator() { 079 return subjectGenerator; 080 } 081 082 @CanIgnoreReturnValue 083 public B withSetUp(Runnable setUp) { 084 this.setUp = setUp; 085 return self(); 086 } 087 088 public Runnable getSetUp() { 089 return setUp; 090 } 091 092 @CanIgnoreReturnValue 093 public B withTearDown(Runnable tearDown) { 094 this.tearDown = tearDown; 095 return self(); 096 } 097 098 public Runnable getTearDown() { 099 return tearDown; 100 } 101 102 // Features 103 104 private final Set<Feature<?>> features = new LinkedHashSet<>(); 105 106 /** 107 * Configures this builder to produce tests appropriate for the given features. This method may be 108 * called more than once to add features in multiple groups. 109 */ 110 @CanIgnoreReturnValue 111 public B withFeatures(Feature<?>... features) { 112 return withFeatures(Arrays.asList(features)); 113 } 114 115 @CanIgnoreReturnValue 116 public B withFeatures(Iterable<? extends Feature<?>> features) { 117 for (Feature<?> feature : features) { 118 this.features.add(feature); 119 } 120 return self(); 121 } 122 123 public Set<Feature<?>> getFeatures() { 124 return Collections.unmodifiableSet(features); 125 } 126 127 // Name 128 129 private @Nullable String name; 130 131 /** Configures this builder produce a TestSuite with the given name. */ 132 @CanIgnoreReturnValue 133 public B named(String name) { 134 if (name.contains("(")) { 135 throw new IllegalArgumentException( 136 "Eclipse hides all characters after " 137 + "'('; please use '[]' or other characters instead of parentheses"); 138 } 139 this.name = name; 140 return self(); 141 } 142 143 public String getName() { 144 return name; 145 } 146 147 // Test suppression 148 149 private final Set<Method> suppressedTests = new HashSet<>(); 150 151 /** 152 * Prevents the given methods from being run as part of the test suite. 153 * 154 * <p><em>Note:</em> in principle this should never need to be used, but it might be useful if the 155 * semantics of an implementation disagree in unforeseen ways with the semantics expected by a 156 * test, or to keep dependent builds clean in spite of an erroneous test. 157 */ 158 @CanIgnoreReturnValue 159 public B suppressing(Method... methods) { 160 return suppressing(Arrays.asList(methods)); 161 } 162 163 @CanIgnoreReturnValue 164 public B suppressing(Collection<Method> methods) { 165 suppressedTests.addAll(methods); 166 return self(); 167 } 168 169 public Set<Method> getSuppressedTests() { 170 return suppressedTests; 171 } 172 173 private static final Logger logger = 174 Logger.getLogger(FeatureSpecificTestSuiteBuilder.class.getName()); 175 176 /** Creates a runnable JUnit test suite based on the criteria already given. */ 177 /* 178 * Class parameters must be raw. This annotation should go on testerClass in 179 * the for loop, but the 1.5 javac crashes on annotations in for loops: 180 * <http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6294589> 181 */ 182 @SuppressWarnings("unchecked") 183 public TestSuite createTestSuite() { 184 checkCanCreate(); 185 186 logger.fine(" Testing: " + name); 187 logger.fine("Features: " + formatFeatureSet(features)); 188 189 FeatureUtil.addImpliedFeatures(features); 190 191 logger.fine("Expanded: " + formatFeatureSet(features)); 192 193 // Class parameters must be raw. 194 List<Class<? extends AbstractTester>> testers = getTesters(); 195 196 TestSuite suite = new TestSuite(name); 197 for (Class<? extends AbstractTester> testerClass : testers) { 198 TestSuite testerSuite = 199 makeSuiteForTesterClass((Class<? extends AbstractTester<?>>) testerClass); 200 if (testerSuite.countTestCases() > 0) { 201 suite.addTest(testerSuite); 202 } 203 } 204 return suite; 205 } 206 207 /** Throw {@link IllegalStateException} if {@link #createTestSuite()} can't be called yet. */ 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("Call withFeatures() before createTestSuite()."); 217 } 218 } 219 220 // Class parameters must be raw. 221 protected abstract List<Class<? extends AbstractTester>> getTesters(); 222 223 private boolean matches(Test test) { 224 Method method; 225 try { 226 method = extractMethod(test); 227 } catch (IllegalArgumentException e) { 228 logger.finer(Platform.format("%s: including by default: %s", test, e.getMessage())); 229 return true; 230 } 231 if (suppressedTests.contains(method)) { 232 logger.finer(Platform.format("%s: excluding because it was explicitly suppressed.", test)); 233 return false; 234 } 235 TesterRequirements requirements; 236 try { 237 requirements = FeatureUtil.getTesterRequirements(method); 238 } catch (ConflictingRequirementsException e) { 239 throw new RuntimeException(e); 240 } 241 if (!features.containsAll(requirements.getPresentFeatures())) { 242 if (logger.isLoggable(FINER)) { 243 Set<Feature<?>> missingFeatures = Helpers.copyToSet(requirements.getPresentFeatures()); 244 missingFeatures.removeAll(features); 245 logger.finer( 246 Platform.format( 247 "%s: skipping because these features are absent: %s", method, missingFeatures)); 248 } 249 return false; 250 } 251 if (intersect(features, requirements.getAbsentFeatures())) { 252 if (logger.isLoggable(FINER)) { 253 Set<Feature<?>> unwantedFeatures = Helpers.copyToSet(requirements.getAbsentFeatures()); 254 unwantedFeatures.retainAll(features); 255 logger.finer( 256 Platform.format( 257 "%s: skipping because these features are present: %s", method, unwantedFeatures)); 258 } 259 return false; 260 } 261 return true; 262 } 263 264 private static boolean intersect(Set<?> a, Set<?> b) { 265 return !disjoint(a, b); 266 } 267 268 private static Method extractMethod(Test test) { 269 if (test instanceof AbstractTester) { 270 AbstractTester<?> tester = (AbstractTester<?>) test; 271 return Helpers.getMethod(tester.getClass(), tester.getTestMethodName()); 272 } else if (test instanceof TestCase) { 273 TestCase testCase = (TestCase) test; 274 return Helpers.getMethod(testCase.getClass(), testCase.getName()); 275 } else { 276 throw new IllegalArgumentException("unable to extract method from test: not a TestCase."); 277 } 278 } 279 280 protected TestSuite makeSuiteForTesterClass(Class<? extends AbstractTester<?>> testerClass) { 281 TestSuite candidateTests = new TestSuite(testerClass); 282 TestSuite suite = filterSuite(candidateTests); 283 284 Enumeration<?> allTests = suite.tests(); 285 while (allTests.hasMoreElements()) { 286 Object test = allTests.nextElement(); 287 if (test instanceof AbstractTester) { 288 @SuppressWarnings("unchecked") 289 AbstractTester<? super G> tester = (AbstractTester<? super G>) test; 290 tester.init(subjectGenerator, name, setUp, tearDown); 291 } 292 } 293 294 return suite; 295 } 296 297 private TestSuite filterSuite(TestSuite suite) { 298 TestSuite filtered = new TestSuite(suite.getName()); 299 Enumeration<?> tests = suite.tests(); 300 while (tests.hasMoreElements()) { 301 Test test = (Test) tests.nextElement(); 302 if (matches(test)) { 303 filtered.addTest(test); 304 } 305 } 306 return filtered; 307 } 308 309 protected static String formatFeatureSet(Set<? extends Feature<?>> features) { 310 List<String> temp = new ArrayList<>(); 311 for (Feature<?> feature : features) { 312 Object featureAsObject = feature; // to work around bogus JDK warning 313 if (featureAsObject instanceof Enum) { 314 Enum<?> f = (Enum<?>) featureAsObject; 315 temp.add(f.getDeclaringClass().getSimpleName() + "." + feature); 316 } else { 317 temp.add(feature.toString()); 318 } 319 } 320 return temp.toString(); 321 } 322}