1 /*
2 * Copyright 1999-2004 The Apache Software Foundation
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package org.apache.commons.collections;
17
18 import junit.framework.TestCase;
19 import junit.framework.TestSuite;
20 import java.lang.reflect.Constructor;
21 import java.lang.reflect.InvocationTargetException;
22 import java.lang.reflect.Method;
23 import java.lang.reflect.Modifier;
24 import java.util.Arrays;
25 import java.util.ArrayList;
26 import java.util.List;
27
28
29 /**
30 * A {@link TestCase} that can define both simple and bulk test methods.<P>
31 *
32 * A <I>simple test method</I> is the type of test traditionally
33 * supplied by by {@link TestCase}. To define a simple test, create a public
34 * no-argument method whose name starts with "test". You can specify the
35 * the name of simple test in the constructor of <Code>BulkTest</Code>;
36 * a subsequent call to {@link TestCase#run} will run that simple test.<P>
37 *
38 * A <I>bulk test method</I>, on the other hand, returns a new instance
39 * of <Code>BulkTest</Code>, which can itself define new simple and bulk
40 * test methods. By using the {@link #makeSuite} method, you can
41 * automatically create a hierarchal suite of tests and child bulk tests.<P>
42 *
43 * For instance, consider the following two classes:
44 *
45 * <Pre>
46 * public class TestSet extends BulkTest {
47 *
48 * private Set set;
49 *
50 * public TestSet(Set set) {
51 * this.set = set;
52 * }
53 *
54 * public void testContains() {
55 * boolean r = set.contains(set.iterator().next()));
56 * assertTrue("Set should contain first element, r);
57 * }
58 *
59 * public void testClear() {
60 * set.clear();
61 * assertTrue("Set should be empty after clear", set.isEmpty());
62 * }
63 * }
64 *
65 *
66 * public class TestHashMap extends BulkTest {
67 *
68 * private Map makeFullMap() {
69 * HashMap result = new HashMap();
70 * result.put("1", "One");
71 * result.put("2", "Two");
72 * return result;
73 * }
74 *
75 * public void testClear() {
76 * Map map = makeFullMap();
77 * map.clear();
78 * assertTrue("Map empty after clear", map.isEmpty());
79 * }
80 *
81 * public BulkTest bulkTestKeySet() {
82 * return new TestSet(makeFullMap().keySet());
83 * }
84 *
85 * public BulkTest bulkTestEntrySet() {
86 * return new TestSet(makeFullMap().entrySet());
87 * }
88 * }
89 * </Pre>
90 *
91 * In the above examples, <Code>TestSet</Code> defines two
92 * simple test methods and no bulk test methods; <Code>TestHashMap</Code>
93 * defines one simple test method and two bulk test methods. When
94 * <Code>makeSuite(TestHashMap.class).run</Code> is executed,
95 * <I>five</I> simple test methods will be run, in this order:<P>
96 *
97 * <Ol>
98 * <Li>TestHashMap.testClear()
99 * <Li>TestHashMap.bulkTestKeySet().testContains();
100 * <Li>TestHashMap.bulkTestKeySet().testClear();
101 * <Li>TestHashMap.bulkTestEntrySet().testContains();
102 * <Li>TestHashMap.bulkTestEntrySet().testClear();
103 * </Ol>
104 *
105 * In the graphical junit test runners, the tests would be displayed in
106 * the following tree:<P>
107 *
108 * <UL>
109 * <LI>TestHashMap</LI>
110 * <UL>
111 * <LI>testClear
112 * <LI>bulkTestKeySet
113 * <UL>
114 * <LI>testContains
115 * <LI>testClear
116 * </UL>
117 * <LI>bulkTestEntrySet
118 * <UL>
119 * <LI>testContains
120 * <LI>testClear
121 * </UL>
122 * </UL>
123 * </UL>
124 *
125 * A subclass can override a superclass's bulk test by
126 * returning <Code>null</Code> from the bulk test method. If you only
127 * want to override specific simple tests within a bulk test, use the
128 * {@link #ignoredSimpleTests} method.<P>
129 *
130 * Note that if you want to use the bulk test methods, you <I>must</I>
131 * define your <Code>suite()</Code> method to use {@link #makeSuite}.
132 * The ordinary {@link TestSuite} constructor doesn't know how to
133 * interpret bulk test methods.
134 *
135 * @author Paul Jack
136 * @version $Id: BulkTest.java,v 1.2.2.1 2004/05/22 12:14:05 scolebourne Exp $
137 */
138 public class BulkTest extends TestCase implements Cloneable {
139
140
141 // Note: BulkTest is Cloneable to make it easier to construct
142 // BulkTest instances for simple test methods that are defined in
143 // anonymous inner classes. Basically we don't have to worry about
144 // finding wierd constructors. (And even if we found them, techinically
145 // it'd be illegal for anyone but the outer class to invoke them).
146 // Given one BulkTest instance, we can just clone it and reset the
147 // method name for every simple test it defines.
148
149
150 /**
151 * The full name of this bulk test instance. This is the full name
152 * that is compared to {@link #ignoredSimpleTests} to see if this
153 * test should be ignored. It's also displayed in the text runner
154 * to ease debugging.
155 */
156 String verboseName;
157
158
159 /**
160 * Constructs a new <Code>BulkTest</Code> instance that will run the
161 * specified simple test.
162 *
163 * @param name the name of the simple test method to run
164 */
165 public BulkTest(String name) {
166 super(name);
167 this.verboseName = getClass().getName();
168 }
169
170
171 /**
172 * Creates a clone of this <Code>BulkTest</Code>.<P>
173 *
174 * @return a clone of this <Code>BulkTest</Code>
175 */
176 public Object clone() {
177 try {
178 return super.clone();
179 } catch (CloneNotSupportedException e) {
180 throw new Error(); // should never happen
181 }
182 }
183
184
185 /**
186 * Returns an array of simple test names to ignore.<P>
187 *
188 * If a simple test that's defined by this <Code>BulkTest</Code> or
189 * by one of its bulk test methods has a name that's in the returned
190 * array, then that simple test will not be executed.<P>
191 *
192 * A simple test's name is formed by taking the class name of the
193 * root <Code>BulkTest</Code>, eliminating the package name, then
194 * appending the names of any bulk test methods that were invoked
195 * to get to the simple test, and then appending the simple test
196 * method name. The method names are delimited by periods:
197 *
198 * <Pre>
199 * TestHashMap.bulkTestEntrySet.testClear
200 * </Pre>
201 *
202 * is the name of one of the simple tests defined in the sample classes
203 * described above. If the sample <Code>TestHashMap</Code> class
204 * included this method:
205 *
206 * <Pre>
207 * public String[] ignoredSimpleTests() {
208 * return new String[] { "TestHashMap.bulkTestEntrySet.testClear" };
209 * }
210 * </Pre>
211 *
212 * then the entry set's clear method wouldn't be tested, but the key
213 * set's clear method would.
214 *
215 * @return an array of the names of simple tests to ignore, or null if
216 * no tests should be ignored
217 */
218 public String[] ignoredSimpleTests() {
219 return null;
220 }
221
222
223 /**
224 * Returns the display name of this <Code>BulkTest</Code>.
225 *
226 * @return the display name of this <Code>BulkTest</Code>
227 */
228 public String toString() {
229 return getName() + "(" + verboseName + ") ";
230 }
231
232
233 /**
234 * Returns a {@link TestSuite} for testing all of the simple tests
235 * <I>and</I> all the bulk tests defined by the given class.<P>
236 *
237 * The class is examined for simple and bulk test methods; any child
238 * bulk tests are also examined recursively; and the results are stored
239 * in a hierarchal {@link TestSuite}.<P>
240 *
241 * The given class must be a subclass of <Code>BulkTest</Code> and must
242 * not be abstract.<P>
243 *
244 * @param c the class to examine for simple and bulk tests
245 * @return a {@link TestSuite} containing all the simple and bulk tests
246 * defined by that class
247 */
248 public static TestSuite makeSuite(Class c) {
249 if (Modifier.isAbstract(c.getModifiers())) {
250 throw new IllegalArgumentException("Class must not be abstract.");
251 }
252 if (!BulkTest.class.isAssignableFrom(c)) {
253 throw new IllegalArgumentException("Class must extend BulkTest.");
254 }
255 return new BulkTestSuiteMaker(c).make();
256 }
257
258 }
259
260
261 // It was easier to use a separate class to do all the reflection stuff
262 // for making the TestSuite instances. Having permanent state around makes
263 // it easier to handle the recursion.
264 class BulkTestSuiteMaker {
265
266
267 /** The class that defines simple and bulk tests methods. */
268 private Class startingClass;
269
270
271 /** List of ignored simple test names. */
272 private List ignored;
273
274
275 /** The TestSuite we're currently populating. Can change over time. */
276 private TestSuite result;
277
278
279 /**
280 * The prefix for simple test methods. Used to check if a test is in
281 * the ignored list.
282 */
283 private String prefix;
284
285
286 /**
287 * Constructor.
288 *
289 * @param startingClass the starting class
290 */
291 public BulkTestSuiteMaker(Class startingClass) {
292 this.startingClass = startingClass;
293 }
294
295
296 /**
297 * Makes a hierarchal TestSuite based on the starting class.
298 *
299 * @return the hierarchal TestSuite for startingClass
300 */
301 public TestSuite make() {
302 this.result = new TestSuite();
303 this.prefix = getBaseName(startingClass);
304 result.setName(prefix);
305
306 BulkTest bulk = makeFirstTestCase(startingClass);
307 ignored = new ArrayList();
308 String[] s = bulk.ignoredSimpleTests();
309 if (s != null) {
310 ignored.addAll(Arrays.asList(s));
311 }
312 make(bulk);
313 return result;
314 }
315
316
317 /**
318 * Appends all the simple tests and bulk tests defined by the given
319 * instance's class to the current TestSuite.
320 *
321 * @param bulk An instance of the class that defines simple and bulk
322 * tests for us to append
323 */
324 void make(BulkTest bulk) {
325 Class c = bulk.getClass();
326 Method[] all = c.getMethods();
327 for (int i = 0; i < all.length; i++) {
328 if (isTest(all[i])) addTest(bulk, all[i]);
329 if (isBulk(all[i])) addBulk(bulk, all[i]);
330 }
331 }
332
333
334 /**
335 * Adds the simple test defined by the given method to the TestSuite.
336 *
337 * @param bulk The instance of the class that defined the method
338 * (I know it's wierd. But the point is, we can clone the instance
339 * and not have to worry about constructors.)
340 * @param m The simple test method
341 */
342 void addTest(BulkTest bulk, Method m) {
343 BulkTest bulk2 = (BulkTest)bulk.clone();
344 bulk2.setName(m.getName());
345 bulk2.verboseName = prefix + "." + m.getName();
346 if (ignored.contains(bulk2.verboseName)) return;
347 result.addTest(bulk2);
348 }
349
350
351 /**
352 * Adds a whole new suite of tests that are defined by the result of
353 * the given bulk test method. In other words, the given bulk test
354 * method is invoked, and the resulting BulkTest instance is examined
355 * for yet more simple and bulk tests.
356 *
357 * @param bulk The instance of the class that defined the method
358 * @param m The bulk test method
359 */
360 void addBulk(BulkTest bulk, Method m) {
361 BulkTest bulk2;
362 try {
363 bulk2 = (BulkTest)m.invoke(bulk, null);
364 if (bulk2 == null) return;
365 } catch (InvocationTargetException e) {
366 throw new Error(); // FIXME;
367 } catch (IllegalAccessException e) {
368 throw new Error(); // FIXME;
369 }
370
371 // Save current state on the stack.
372 String oldPrefix = prefix;
373 TestSuite oldResult = result;
374
375 prefix = prefix + "." + m.getName();
376 result = new TestSuite();
377 result.setName(m.getName());
378
379 make(bulk2);
380
381 oldResult.addTest(result);
382
383 // Restore the old state
384 prefix = oldPrefix;
385 result = oldResult;
386 }
387
388
389 /**
390 * Returns the base name of the given class.
391 *
392 * @param c the class
393 * @return the name of that class, minus any package names
394 */
395 private static String getBaseName(Class c) {
396 String name = c.getName();
397 int p = name.lastIndexOf('.');
398 if (p > 0) {
399 name = name.substring(p + 1);
400 }
401 return name;
402 }
403
404
405 // These three methods are used to create a valid BulkTest instance
406 // from a class.
407
408 private static Constructor getTestCaseConstructor(Class c) {
409 try {
410 return c.getConstructor(new Class[] { String.class });
411 } catch (NoSuchMethodException e) {
412 throw new IllegalArgumentException(c + " must provide " +
413 "a (String) constructor");
414 }
415 }
416
417
418 private static BulkTest makeTestCase(Class c, Method m) {
419 Constructor con = getTestCaseConstructor(c);
420 try {
421 return (BulkTest)con.newInstance(new String[] { m.getName() });
422 } catch (InvocationTargetException e) {
423 e.printStackTrace();
424 throw new RuntimeException(); // FIXME;
425 } catch (IllegalAccessException e) {
426 throw new Error(); // should never occur
427 } catch (InstantiationException e) {
428 throw new RuntimeException(); // FIXME;
429 }
430 }
431
432
433 private static BulkTest makeFirstTestCase(Class c) {
434 Method[] all = c.getMethods();
435 for (int i = 0; i < all.length; i++) {
436 if (isTest(all[i])) return makeTestCase(c, all[i]);
437 }
438 throw new IllegalArgumentException(c.getName() + " must provide "
439 + " at least one test method.");
440 }
441
442
443 /**
444 * Returns true if the given method is a simple test method.
445 */
446 private static boolean isTest(Method m) {
447 if (!m.getName().startsWith("test")) return false;
448 if (m.getReturnType() != Void.TYPE) return false;
449 if (m.getParameterTypes().length != 0) return false;
450 int mods = m.getModifiers();
451 if (Modifier.isStatic(mods)) return false;
452 if (Modifier.isAbstract(mods)) return false;
453 return true;
454 }
455
456
457 /**
458 * Returns true if the given method is a bulk test method.
459 */
460 private static boolean isBulk(Method m) {
461 if (!m.getName().startsWith("bulkTest")) return false;
462 if (m.getReturnType() != BulkTest.class) return false;
463 if (m.getParameterTypes().length != 0) return false;
464 int mods = m.getModifiers();
465 if (Modifier.isStatic(mods)) return false;
466 if (Modifier.isAbstract(mods)) return false;
467 return true;
468 }
469
470
471 }