diff --git a/src/test/java/org/apache/commons/lang3/compare/BulkTest.java b/src/test/java/org/apache/commons/lang3/compare/BulkTest.java new file mode 100644 index 000000000..bd7d61b03 --- /dev/null +++ b/src/test/java/org/apache/commons/lang3/compare/BulkTest.java @@ -0,0 +1,464 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.commons.collections; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * A {@link TestCase} that can define both simple and bulk test methods. + *

+ * A simple test method is the type of test traditionally + * supplied by by {@link TestCase}. To define a simple test, create a public + * no-argument method whose name starts with "test". You can specify the + * the name of simple test in the constructor of BulkTest; + * a subsequent call to {@link TestCase#run} will run that simple test. + *

+ * A bulk test method, on the other hand, returns a new instance + * of BulkTest, which can itself define new simple and bulk + * test methods. By using the {@link #makeSuite} method, you can + * automatically create a hierarchal suite of tests and child bulk tests. + *

+ * For instance, consider the following two classes: + * + *

+ *  public class TestSet extends BulkTest {
+ *
+ *      private Set set;
+ *
+ *      public TestSet(Set set) {
+ *          this.set = set;
+ *      }
+ *
+ *      public void testContains() {
+ *          boolean r = set.contains(set.iterator().next()));
+ *          assertTrue("Set should contain first element, r);
+ *      }
+ *
+ *      public void testClear() {
+ *          set.clear();
+ *          assertTrue("Set should be empty after clear", set.isEmpty());
+ *      }
+ *  }
+ *
+ *
+ *  public class TestHashMap extends BulkTest {
+ *
+ *      private Map makeFullMap() {
+ *          HashMap result = new HashMap();
+ *          result.put("1", "One");
+ *          result.put("2", "Two");
+ *          return result;
+ *      }
+ *
+ *      public void testClear() {
+ *          Map map = makeFullMap();
+ *          map.clear();
+ *          assertTrue("Map empty after clear", map.isEmpty());
+ *      }
+ *
+ *      public BulkTest bulkTestKeySet() {
+ *          return new TestSet(makeFullMap().keySet());
+ *      }
+ *
+ *      public BulkTest bulkTestEntrySet() {
+ *          return new TestSet(makeFullMap().entrySet());
+ *      }
+ *  }
+ *  
+ * + * In the above examples, TestSet defines two + * simple test methods and no bulk test methods; TestHashMap + * defines one simple test method and two bulk test methods. When + * makeSuite(TestHashMap.class).run is executed, + * five simple test methods will be run, in this order:

+ * + *

    + *
  1. TestHashMap.testClear() + *
  2. TestHashMap.bulkTestKeySet().testContains(); + *
  3. TestHashMap.bulkTestKeySet().testClear(); + *
  4. TestHashMap.bulkTestEntrySet().testContains(); + *
  5. TestHashMap.bulkTestEntrySet().testClear(); + *
+ * + * In the graphical junit test runners, the tests would be displayed in + * the following tree:

+ * + *

+ * + * A subclass can override a superclass's bulk test by + * returning null from the bulk test method. If you only + * want to override specific simple tests within a bulk test, use the + * {@link #ignoredTests} method.

+ * + * Note that if you want to use the bulk test methods, you must + * define your suite() method to use {@link #makeSuite}. + * The ordinary {@link TestSuite} constructor doesn't know how to + * interpret bulk test methods. + * + * @author Paul Jack + * @version $Id$ + */ +public class BulkTest extends TestCase implements Cloneable { + + + // Note: BulkTest is Cloneable to make it easier to construct + // BulkTest instances for simple test methods that are defined in + // anonymous inner classes. Basically we don't have to worry about + // finding weird constructors. (And even if we found them, technically + // it'd be illegal for anyone but the outer class to invoke them). + // Given one BulkTest instance, we can just clone it and reset the + // method name for every simple test it defines. + + + /** + * The full name of this bulk test instance. This is the full name + * that is compared to {@link #ignoredTests} to see if this + * test should be ignored. It's also displayed in the text runner + * to ease debugging. + */ + String verboseName; + + + /** + * Constructs a new BulkTest instance that will run the + * specified simple test. + * + * @param name the name of the simple test method to run + */ + public BulkTest(String name) { + super(name); + this.verboseName = getClass().getName(); + } + + + /** + * Creates a clone of this BulkTest.

+ * + * @return a clone of this BulkTest + */ + @Override + public Object clone() { + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + throw new Error(); // should never happen + } + } + + + /** + * Returns an array of test names to ignore.

+ * + * If a test that's defined by this BulkTest or + * by one of its bulk test methods has a name that's in the returned + * array, then that simple test will not be executed.

+ * + * A test's name is formed by taking the class name of the + * root BulkTest, eliminating the package name, then + * appending the names of any bulk test methods that were invoked + * to get to the simple test, and then appending the simple test + * method name. The method names are delimited by periods: + * + *

+     *  TestHashMap.bulkTestEntrySet.testClear
+     *  
+ * + * is the name of one of the simple tests defined in the sample classes + * described above. If the sample TestHashMap class + * included this method: + * + *
+     *  public String[] ignoredTests() {
+     *      return new String[] { "TestHashMap.bulkTestEntrySet.testClear" };
+     *  }
+     *  
+ * + * then the entry set's clear method wouldn't be tested, but the key + * set's clear method would. + * + * @return an array of the names of tests to ignore, or null if + * no tests should be ignored + */ + public String[] ignoredTests() { + return null; + } + + + /** + * Returns the display name of this BulkTest. + * + * @return the display name of this BulkTest + */ + @Override + public String toString() { + return getName() + "(" + verboseName + ") "; + } + + + /** + * Returns a {@link TestSuite} for testing all of the simple tests + * and all the bulk tests defined by the given class.

+ * + * The class is examined for simple and bulk test methods; any child + * bulk tests are also examined recursively; and the results are stored + * in a hierarchal {@link TestSuite}.

+ * + * The given class must be a subclass of BulkTest and must + * not be abstract.

+ * + * @param c the class to examine for simple and bulk tests + * @return a {@link TestSuite} containing all the simple and bulk tests + * defined by that class + */ + public static TestSuite makeSuite(Class c) { + if (Modifier.isAbstract(c.getModifiers())) { + throw new IllegalArgumentException("Class must not be abstract."); + } + if (!BulkTest.class.isAssignableFrom(c)) { + throw new IllegalArgumentException("Class must extend BulkTest."); + } + return new BulkTestSuiteMaker(c).make(); + } + +} + + +// It was easier to use a separate class to do all the reflection stuff +// for making the TestSuite instances. Having permanent state around makes +// it easier to handle the recursion. +class BulkTestSuiteMaker { + + /** The class that defines simple and bulk tests methods. */ + private Class startingClass; + + /** List of ignored simple test names. */ + private List ignored; + + /** The TestSuite we're currently populating. Can change over time. */ + private TestSuite result; + + /** + * The prefix for simple test methods. Used to check if a test is in + * the ignored list. + */ + private String prefix; + + /** + * Constructor. + * + * @param startingClass the starting class + */ + public BulkTestSuiteMaker(Class startingClass) { + this.startingClass = startingClass; + } + + /** + * Makes a hierarchal TestSuite based on the starting class. + * + * @return the hierarchal TestSuite for startingClass + */ + public TestSuite make() { + this.result = new TestSuite(); + this.prefix = getBaseName(startingClass); + result.setName(prefix); + + BulkTest bulk = makeFirstTestCase(startingClass); + ignored = new ArrayList(); + String[] s = bulk.ignoredTests(); + if (s != null) { + ignored.addAll(Arrays.asList(s)); + } + make(bulk); + return result; + } + + /** + * Appends all the simple tests and bulk tests defined by the given + * instance's class to the current TestSuite. + * + * @param bulk An instance of the class that defines simple and bulk + * tests for us to append + */ + void make(BulkTest bulk) { + Class c = bulk.getClass(); + Method[] all = c.getMethods(); + for (int i = 0; i < all.length; i++) { + if (isTest(all[i])) addTest(bulk, all[i]); + if (isBulk(all[i])) addBulk(bulk, all[i]); + } + } + + /** + * Adds the simple test defined by the given method to the TestSuite. + * + * @param bulk The instance of the class that defined the method + * (I know it's weird. But the point is, we can clone the instance + * and not have to worry about constructors.) + * @param m The simple test method + */ + void addTest(BulkTest bulk, Method m) { + BulkTest bulk2 = (BulkTest)bulk.clone(); + bulk2.setName(m.getName()); + bulk2.verboseName = prefix + "." + m.getName(); + if (ignored.contains(bulk2.verboseName)) return; + result.addTest(bulk2); + } + + /** + * Adds a whole new suite of tests that are defined by the result of + * the given bulk test method. In other words, the given bulk test + * method is invoked, and the resulting BulkTest instance is examined + * for yet more simple and bulk tests. + * + * @param bulk The instance of the class that defined the method + * @param m The bulk test method + */ + void addBulk(BulkTest bulk, Method m) { + String verboseName = prefix + "." + m.getName(); + if (ignored.contains(verboseName)) return; + + BulkTest bulk2; + try { + bulk2 = (BulkTest)m.invoke(bulk, (Object[]) null); + if (bulk2 == null) return; + } catch (InvocationTargetException ex) { + ex.getTargetException().printStackTrace(); + throw new Error(); // FIXME; + } catch (IllegalAccessException ex) { + ex.printStackTrace(); + throw new Error(); // FIXME; + } + + // Save current state on the stack. + String oldPrefix = prefix; + TestSuite oldResult = result; + + prefix = prefix + "." + m.getName(); + result = new TestSuite(); + result.setName(m.getName()); + + make(bulk2); + + oldResult.addTest(result); + + // Restore the old state + prefix = oldPrefix; + result = oldResult; + } + + /** + * Returns the base name of the given class. + * + * @param c the class + * @return the name of that class, minus any package names + */ + private static String getBaseName(Class c) { + String name = c.getName(); + int p = name.lastIndexOf('.'); + if (p > 0) { + name = name.substring(p + 1); + } + return name; + } + + + // These three methods are used to create a valid BulkTest instance + // from a class. + + private static Constructor getTestCaseConstructor(Class c) { + try { + return c.getConstructor(new Class[] { String.class }); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException(c + " must provide " + + "a (String) constructor"); + } + } + + private static BulkTest makeTestCase(Class c, Method m) { + Constructor con = getTestCaseConstructor(c); + try { + return con.newInstance(new Object[] { m.getName() }); + } catch (InvocationTargetException e) { + e.printStackTrace(); + throw new RuntimeException(); // FIXME; + } catch (IllegalAccessException e) { + throw new Error(); // should never occur + } catch (InstantiationException e) { + throw new RuntimeException(); // FIXME; + } + } + + private static BulkTest makeFirstTestCase(Class c) { + Method[] all = c.getMethods(); + for (int i = 0; i < all.length; i++) { + if (isTest(all[i])) return makeTestCase(c, all[i]); + } + throw new IllegalArgumentException(c.getName() + " must provide " + + " at least one test method."); + } + + /** + * Returns true if the given method is a simple test method. + */ + private static boolean isTest(Method m) { + if (!m.getName().startsWith("test")) return false; + if (m.getReturnType() != Void.TYPE) return false; + if (m.getParameterTypes().length != 0) return false; + int mods = m.getModifiers(); + if (Modifier.isStatic(mods)) return false; + if (Modifier.isAbstract(mods)) return false; + return true; + } + + /** + * Returns true if the given method is a bulk test method. + */ + private static boolean isBulk(Method m) { + if (!m.getName().startsWith("bulkTest")) return false; + if (m.getReturnType() != BulkTest.class) return false; + if (m.getParameterTypes().length != 0) return false; + int mods = m.getModifiers(); + if (Modifier.isStatic(mods)) return false; + if (Modifier.isAbstract(mods)) return false; + return true; + } + +}