/* * Copyright (C) 2008 The Guava Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.common.collect.testing; import static java.util.Collections.disjoint; import static java.util.logging.Level.FINER; import com.google.common.collect.testing.features.ConflictingRequirementsException; import com.google.common.collect.testing.features.Feature; import com.google.common.collect.testing.features.FeatureUtil; import com.google.common.collect.testing.features.TesterRequirements; import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestSuite; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.logging.Logger; /** * Creates, based on your criteria, a JUnit test suite that exhaustively tests * the object generated by a G, selecting appropriate tests by matching them * against specified features. * * @param <B> The concrete type of this builder (the 'self-type'). All the * Builder methods of this class (such as {@link #named}) return this type, so * that Builder methods of more derived classes can be chained onto them without * casting. * @param <G> The type of the generator to be passed to testers in the * generated test suite. An instance of G should somehow provide an * instance of the class under test, plus any other information required * to parameterize the test. * * @author George van den Driessche */ public abstract class FeatureSpecificTestSuiteBuilder< B extends FeatureSpecificTestSuiteBuilder<B, G>, G> { @SuppressWarnings("unchecked") protected B self() { return (B) this; } // Test Data private G subjectGenerator; // Gets run before every test. private Runnable setUp; // Gets run at the conclusion of every test. private Runnable tearDown; protected B usingGenerator(G subjectGenerator) { this.subjectGenerator = subjectGenerator; return self(); } public G getSubjectGenerator() { return subjectGenerator; } public B withSetUp(Runnable setUp) { this.setUp = setUp; return self(); } protected Runnable getSetUp() { return setUp; } public B withTearDown(Runnable tearDown) { this.tearDown = tearDown; return self(); } protected Runnable getTearDown() { return tearDown; } // Features private Set<Feature<?>> features; /** * Configures this builder to produce tests appropriate for the given * features. */ public B withFeatures(Feature<?>... features) { return withFeatures(Arrays.asList(features)); } public B withFeatures(Iterable<? extends Feature<?>> features) { this.features = Helpers.copyToSet(features); return self(); } public Set<Feature<?>> getFeatures() { return Collections.unmodifiableSet(features); } // Name private String name; /** Configures this builder produce a TestSuite with the given name. */ public B named(String name) { if (name.contains("(")) { throw new IllegalArgumentException("Eclipse hides all characters after " + "'('; please use '[]' or other characters instead of parentheses"); } this.name = name; return self(); } public String getName() { return name; } // Test suppression private Set<Method> suppressedTests = new HashSet<Method>(); /** * Prevents the given methods from being run as part of the test suite. * * <em>Note:</em> in principle this should never need to be used, but it * might be useful if the semantics of an implementation disagree in * unforeseen ways with the semantics expected by a test, or to keep dependent * builds clean in spite of an erroneous test. */ public B suppressing(Method... methods) { return suppressing(Arrays.asList(methods)); } public B suppressing(Collection<Method> methods) { suppressedTests.addAll(methods); return self(); } public Set<Method> getSuppressedTests() { return suppressedTests; } private static final Logger logger = Logger.getLogger( FeatureSpecificTestSuiteBuilder.class.getName()); /** * Creates a runnable JUnit test suite based on the criteria already given. */ /* * Class parameters must be raw. This annotation should go on testerClass in * the for loop, but the 1.5 javac crashes on annotations in for loops: * <http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6294589> */ @SuppressWarnings("unchecked") public TestSuite createTestSuite() { checkCanCreate(); logger.fine(" Testing: " + name); logger.fine("Features: " + formatFeatureSet(features)); FeatureUtil.addImpliedFeatures(features); logger.fine("Expanded: " + formatFeatureSet(features)); // Class parameters must be raw. List<Class<? extends AbstractTester>> testers = getTesters(); TestSuite suite = new TestSuite(name); for (Class<? extends AbstractTester> testerClass : testers) { final TestSuite testerSuite = makeSuiteForTesterClass( (Class<? extends AbstractTester<?>>) testerClass); if (testerSuite.countTestCases() > 0) { suite.addTest(testerSuite); } } return suite; } /** * Throw {@link IllegalStateException} if {@link #createTestSuite()} can't * be called yet. */ protected void checkCanCreate() { if (subjectGenerator == null) { throw new IllegalStateException("Call using() before createTestSuite()."); } if (name == null) { throw new IllegalStateException("Call named() before createTestSuite()."); } if (features == null) { throw new IllegalStateException( "Call withFeatures() before createTestSuite()."); } } // Class parameters must be raw. protected abstract List<Class<? extends AbstractTester>> getTesters(); private boolean matches(Test test) { final Method method; try { method = extractMethod(test); } catch (IllegalArgumentException e) { logger.finer(Platform.format( "%s: including by default: %s", test, e.getMessage())); return true; } if (suppressedTests.contains(method)) { logger.finer(Platform.format( "%s: excluding because it was explicitly suppressed.", test)); return false; } final TesterRequirements requirements; try { requirements = FeatureUtil.getTesterRequirements(method); } catch (ConflictingRequirementsException e) { throw new RuntimeException(e); } if (!features.containsAll(requirements.getPresentFeatures())) { if (logger.isLoggable(FINER)) { Set<Feature<?>> missingFeatures = Helpers.copyToSet(requirements.getPresentFeatures()); missingFeatures.removeAll(features); logger.finer(Platform.format( "%s: skipping because these features are absent: %s", method, missingFeatures)); } return false; } if (intersect(features, requirements.getAbsentFeatures())) { if (logger.isLoggable(FINER)) { Set<Feature<?>> unwantedFeatures = Helpers.copyToSet(requirements.getAbsentFeatures()); unwantedFeatures.retainAll(features); logger.finer(Platform.format( "%s: skipping because these features are present: %s", method, unwantedFeatures)); } return false; } return true; } private static boolean intersect(Set<?> a, Set<?> b) { return !disjoint(a, b); } private static Method extractMethod(Test test) { if (test instanceof AbstractTester) { AbstractTester<?> tester = (AbstractTester<?>) test; return Helpers.getMethod(tester.getClass(), tester.getTestMethodName()); } else if (test instanceof TestCase) { TestCase testCase = (TestCase) test; return Helpers.getMethod(testCase.getClass(), testCase.getName()); } else { throw new IllegalArgumentException( "unable to extract method from test: not a TestCase."); } } protected TestSuite makeSuiteForTesterClass( Class<? extends AbstractTester<?>> testerClass) { final TestSuite candidateTests = new TestSuite(testerClass); final TestSuite suite = filterSuite(candidateTests); Enumeration<?> allTests = suite.tests(); while (allTests.hasMoreElements()) { Object test = allTests.nextElement(); if (test instanceof AbstractTester) { @SuppressWarnings("unchecked") AbstractTester<? super G> tester = (AbstractTester<? super G>) test; tester.init(subjectGenerator, name, setUp, tearDown); } } return suite; } private TestSuite filterSuite(TestSuite suite) { TestSuite filtered = new TestSuite(suite.getName()); final Enumeration<?> tests = suite.tests(); while (tests.hasMoreElements()) { Test test = (Test) tests.nextElement(); if (matches(test)) { filtered.addTest(test); } } return filtered; } protected static String formatFeatureSet(Set<? extends Feature<?>> features) { List<String> temp = new ArrayList<String>(); for (Feature<?> feature : features) { Object featureAsObject = feature; // to work around bogus JDK warning if (featureAsObject instanceof Enum) { Enum<?> f = (Enum<?>) featureAsObject; temp.add(Platform.classGetSimpleName( f.getDeclaringClass()) + "." + feature); } else { temp.add(feature.toString()); } } return temp.toString(); } }