package com.limegroup.gnutella.util; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import junit.framework.AssertionFailedError; import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestResult; import junit.framework.TestSuite; import com.limegroup.gnutella.Backend; import com.limegroup.gnutella.ErrorCallback; import com.limegroup.gnutella.ErrorService; /** * A modified TestSuite that allows the backends * to always be shutdown after tests finished, * regardless of halt-on-failure or halt-on-error. */ public class LimeTestSuite extends TestSuite implements ErrorCallback { private TestResult _testResult = null; private boolean _beforeTests = false; private static Class _testClass; private static final String preTestName = "globalSetUp"; private static final String postTestName = "globalTearDown"; /** * Constructors... */ LimeTestSuite() { super(); } LimeTestSuite(Class a, String b) { super(a, b); _testClass = a; } LimeTestSuite(Class a) { super(a); _testClass = a; } /** * Allows the test class to be changed. * Required when forcing a single test to be run from a TestCase. */ public static void setTestClass(Class cls) { _testClass = cls; } /** * Modified run. * Before & after tests are run, it sets the error callback * to itself to catch any stray errors. */ public void run(TestResult result) { _beforeTests = true; _testResult = result; ErrorService.setErrorCallback(this); Backend.setErrorCallback(this); // First try doing the before-tests-setup try { BaseTestCase.beforeAllTestsSetUp(); } catch(Throwable t) { // If there is an error here, report it, // run the after all tests tear down, and exit. error(t); try { BaseTestCase.afterAllTestsTearDown(); } catch(Throwable t2) { error(t2); } return; } // Then try running the preTest method try { runStaticMethod(preTestName); } catch(TestFailedException tfe) { // If it fails, run the post test and exit. try { runStaticMethod(postTestName); } catch(TestFailedException tfe2) { // oh well. } return; } // Try running all the tests. try { super.run(result); } finally { // Regardless of if any fail or not, // always run the last post test & after all tests methods. _beforeTests = false; ErrorService.setErrorCallback(this); Backend.setErrorCallback(this); try { runStaticMethod(postTestName); } catch(TestFailedException tfe) { // oh well. } try { BaseTestCase.afterAllTestsTearDown(); } catch(Throwable t) { error(t); } } } public void runStaticMethod(String name) throws TestFailedException { List methods = null; try { methods = getAllStaticMethods(_testClass, name); if(methods.isEmpty()) return; } catch(NoSuchMethodException e) { return; } for(Iterator i = methods.iterator(); i.hasNext(); ) { Method m = (Method)i.next(); if ( !Modifier.isStatic(m.getModifiers()) ) { runTest(warning("Method "+name+" must be declared static."), _testResult); throw new TestFailedException(); } else if ( !Modifier.isPublic(m.getModifiers()) ) { runTest(warning("Method "+name+" must be declared public."), _testResult); throw new TestFailedException(); } else { try { // If this method takes the class parameter, send it if(m.getParameterTypes().length == 1) m.invoke(null, new Object[] { _testClass }); // Otherwise use the no-arg invocation else m.invoke(null, new Object[] {}); } catch (InvocationTargetException e) { runTest(reportError(e.getCause(), _testResult), _testResult); throw new TestFailedException(); } catch (IllegalAccessException e) { runTest(warning("Cannot access method: "+name, e), _testResult); throw new TestFailedException(); } } } } /** * Retrieves all static methods of the specified class (and all subclasses) * that are the specified name and either take no parameters or a Class * parameter. The method that takes a Class parameter takes priority * if both are found. * * Throws NoSuchMethodException if none are found. */ public List getAllStaticMethods(Class entryClass, String methodName) throws NoSuchMethodException { List methods = new LinkedList(); Class clazz = entryClass; Class[] classes = new Class[] { Class.class }; while(clazz != null) { Method add = null; try { add = clazz.getDeclaredMethod(methodName, classes); } catch(NoSuchMethodException tryAgain) { // If nothing with the class parameter, try with none try { add = clazz.getDeclaredMethod(methodName, null); } catch(NoSuchMethodException ignored) {} } // If we found a method, add it to the beginning of the list if(add != null) methods.add(0, add); // Try again with a superclass. clazz = clazz.getSuperclass(); } if(methods.isEmpty()) throw new NoSuchMethodException("Invalid method: " + methodName); return methods; } /** * Stub for error(Throwable, String) */ public void error(Throwable ex) { error(ex, null); } /** * The error service callback. * * This does a somewhat unusual thing to report the error. * Instead of just appending it to the _testResult, it creates * a new TestCase and has that fail. * This has the advantage of reporting the error in a different * 'test', ensuring that people aren't confused as to how the error * was caused. */ public void error(Throwable ex, String detail) { if ( _testResult != null ) runTest(reportError(ex, _testResult), _testResult); else ex.printStackTrace(); } /** * Returns a test which will fail and log a warning message. * Modified from JUnit's TestSuite.java's warning method. * Note that it does not have to extend BaseTestCase, just TestCase. * BaseTestCase would add needless complexity to an otherwise * simple error report. */ private Test reportError(final Throwable thrown, final TestResult result) { String testName = _beforeTests ? "LimeTestSuite - Before Test Errors" : "LimeTestSuite - After Test Errors"; return new TestCase(testName) { protected void runTest() { if ( thrown instanceof AssertionFailedError ) result.addFailure(this, (AssertionFailedError)thrown); else result.addError(this, thrown); } }; } /** * Returns a test which will fail and log a warning message. */ private static Test warning(final String message) { return new TestCase("warning") { protected void runTest() { fail(message); } }; } /** * Returns a test which will fail and log a warning message. */ private static Test warning(final String message, final Throwable thrown) { return new BaseTestCase("warning") { public void preSetup() {} public void postTearDown() {} protected void runTest() { fail(message, thrown); } }; } private class TestFailedException extends Exception { TestFailedException() { super(); } } }