| /* |
| * 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.LinkedHashSet; |
| 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 = new LinkedHashSet<Feature<?>>(); |
| |
| /** |
| * Configures this builder to produce tests appropriate for the given |
| * features. This method may be called more than once to add features |
| * in multiple groups. |
| */ |
| public B withFeatures(Feature<?>... features) { |
| return withFeatures(Arrays.asList(features)); |
| } |
| |
| public B withFeatures(Iterable<? extends Feature<?>> features) { |
| for (Feature<?> feature : features) { |
| this.features.add(feature); |
| } |
| 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(f.getDeclaringClass().getSimpleName() + "." + feature); |
| } else { |
| temp.add(feature.toString()); |
| } |
| } |
| return temp.toString(); |
| } |
| } |