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 public TestSuite createTestSuite() { 178 checkCanCreate(); 179 180 logger.fine(" Testing: " + name); 181 logger.fine("Features: " + formatFeatureSet(features)); 182 183 FeatureUtil.addImpliedFeatures(features); 184 185 logger.fine("Expanded: " + formatFeatureSet(features)); 186 187 @SuppressWarnings("rawtypes") // class literals 188 List<Class<? extends AbstractTester>> testers = getTesters(); 189 190 TestSuite suite = new TestSuite(name); 191 for (@SuppressWarnings("rawtypes") // class literals 192 Class<? extends AbstractTester> testerClass : testers) { 193 @SuppressWarnings("unchecked") // getting rid of the raw type, for better or for worse 194 TestSuite testerSuite = 195 makeSuiteForTesterClass((Class<? extends AbstractTester<?>>) testerClass); 196 if (testerSuite.countTestCases() > 0) { 197 suite.addTest(testerSuite); 198 } 199 } 200 return suite; 201 } 202 203 /** Throw {@link IllegalStateException} if {@link #createTestSuite()} can't be called yet. */ 204 protected void checkCanCreate() { 205 if (subjectGenerator == null) { 206 throw new IllegalStateException("Call using() before createTestSuite()."); 207 } 208 if (name == null) { 209 throw new IllegalStateException("Call named() before createTestSuite()."); 210 } 211 if (features == null) { 212 throw new IllegalStateException("Call withFeatures() before createTestSuite()."); 213 } 214 } 215 216 @SuppressWarnings("rawtypes") // class literals 217 protected abstract List<Class<? extends AbstractTester>> getTesters(); 218 219 private boolean matches(Test test) { 220 Method method; 221 try { 222 method = extractMethod(test); 223 } catch (IllegalArgumentException e) { 224 logger.finer(Platform.format("%s: including by default: %s", test, e.getMessage())); 225 return true; 226 } 227 if (suppressedTests.contains(method)) { 228 logger.finer(Platform.format("%s: excluding because it was explicitly suppressed.", test)); 229 return false; 230 } 231 TesterRequirements requirements; 232 try { 233 requirements = FeatureUtil.getTesterRequirements(method); 234 } catch (ConflictingRequirementsException e) { 235 throw new RuntimeException(e); 236 } 237 if (!features.containsAll(requirements.getPresentFeatures())) { 238 if (logger.isLoggable(FINER)) { 239 Set<Feature<?>> missingFeatures = Helpers.copyToSet(requirements.getPresentFeatures()); 240 missingFeatures.removeAll(features); 241 logger.finer( 242 Platform.format( 243 "%s: skipping because these features are absent: %s", method, missingFeatures)); 244 } 245 return false; 246 } 247 if (intersect(features, requirements.getAbsentFeatures())) { 248 if (logger.isLoggable(FINER)) { 249 Set<Feature<?>> unwantedFeatures = Helpers.copyToSet(requirements.getAbsentFeatures()); 250 unwantedFeatures.retainAll(features); 251 logger.finer( 252 Platform.format( 253 "%s: skipping because these features are present: %s", method, unwantedFeatures)); 254 } 255 return false; 256 } 257 return true; 258 } 259 260 private static boolean intersect(Set<?> a, Set<?> b) { 261 return !disjoint(a, b); 262 } 263 264 private static Method extractMethod(Test test) { 265 if (test instanceof AbstractTester) { 266 AbstractTester<?> tester = (AbstractTester<?>) test; 267 return Helpers.getMethod(tester.getClass(), tester.getTestMethodName()); 268 } else if (test instanceof TestCase) { 269 TestCase testCase = (TestCase) test; 270 return Helpers.getMethod(testCase.getClass(), testCase.getName()); 271 } else { 272 throw new IllegalArgumentException("unable to extract method from test: not a TestCase."); 273 } 274 } 275 276 protected TestSuite makeSuiteForTesterClass(Class<? extends AbstractTester<?>> testerClass) { 277 TestSuite candidateTests = new TestSuite(testerClass); 278 TestSuite suite = filterSuite(candidateTests); 279 280 Enumeration<?> allTests = suite.tests(); 281 while (allTests.hasMoreElements()) { 282 Object test = allTests.nextElement(); 283 if (test instanceof AbstractTester) { 284 @SuppressWarnings("unchecked") 285 AbstractTester<? super G> tester = (AbstractTester<? super G>) test; 286 tester.init(subjectGenerator, name, setUp, tearDown); 287 } 288 } 289 290 return suite; 291 } 292 293 private TestSuite filterSuite(TestSuite suite) { 294 TestSuite filtered = new TestSuite(suite.getName()); 295 Enumeration<?> tests = suite.tests(); 296 while (tests.hasMoreElements()) { 297 Test test = (Test) tests.nextElement(); 298 if (matches(test)) { 299 filtered.addTest(test); 300 } 301 } 302 return filtered; 303 } 304 305 protected static String formatFeatureSet(Set<? extends Feature<?>> features) { 306 List<String> temp = new ArrayList<>(); 307 for (Feature<?> feature : features) { 308 Object featureAsObject = feature; // to work around bogus JDK warning 309 if (featureAsObject instanceof Enum) { 310 Enum<?> f = (Enum<?>) featureAsObject; 311 temp.add(f.getDeclaringClass().getSimpleName() + "." + feature); 312 } else { 313 temp.add(feature.toString()); 314 } 315 } 316 return temp.toString(); 317 } 318}