package com.limegroup.gnutella.util; import java.io.File; import java.io.IOException; import java.io.InterruptedIOException; import java.net.InetAddress; import java.net.Socket; import java.net.UnknownHostException; import java.util.Arrays; import java.util.Enumeration; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.StringTokenizer; import java.util.Timer; import java.util.TimerTask; 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.Connection; import com.limegroup.gnutella.ErrorCallback; import com.limegroup.gnutella.ErrorService; import com.limegroup.gnutella.GUID; import com.limegroup.gnutella.UrnCache; import com.limegroup.gnutella.UrnCallback; import com.limegroup.gnutella.messages.BadPacketException; import com.limegroup.gnutella.messages.Message; import com.limegroup.gnutella.messages.PingReply; import com.limegroup.gnutella.messages.PingRequest; import com.limegroup.gnutella.messages.QueryReply; import com.limegroup.gnutella.messages.QueryRequest; import com.limegroup.gnutella.routing.RouteTableMessage; import com.limegroup.gnutella.settings.ConnectionSettings; import com.limegroup.gnutella.settings.ContentSettings; import com.limegroup.gnutella.settings.FilterSettings; import com.limegroup.gnutella.settings.SearchSettings; import com.limegroup.gnutella.settings.SettingsHandler; import com.limegroup.gnutella.settings.SharingSettings; import com.limegroup.gnutella.settings.UltrapeerSettings; public class BaseTestCase extends AssertComparisons implements ErrorCallback { protected static File _baseDir; protected static File _sharedDir; protected static File _savedDir; protected static File _incompleteDir; protected static File _settingsDir; protected static File _xmlDir; protected static File _xmlDataDir; protected static File _xmlSchemasDir; protected static Class _testClass; private static Timer _testKillerTimer = new Timer(true); protected Thread _testThread; protected TestResult _testResult; protected TimerTask _testKiller; protected long _startTimeForTest; /** * Unassigned port for tests to use. */ protected static final int TEST_PORT = 49000; /* Flag indicate that we have launched the backend process and should * shut it down when we are finished. * NOTE. this has to be declared static because a separate TestCase is * insantiated for each test. */ private static boolean[] shutdownBackend = new boolean[]{false, false}; /** * The base constructor. * Nothing should ever be initialized in the constructor. * This is because of the way JUnit sets up tests -- * It first builds a new instance of the class for every possible test, * then it runs through those instances, calling the appropriate test. * All pre & post initializations that are necessary for every test * should be in the new 'preSetUp' and 'postTearDown' methods. */ public BaseTestCase(String name) { super(name); _testClass = getClass(); } /** * Build a test suite containing all of the test methods in the given class * @param cls The test class (must be subclassed from TestCase) * @return <tt>TestSuite</tt> object that can be returned by suite method */ public static TestSuite buildSingleTestSuite(Class cls) { _testClass = cls; String method = System.getProperty("junit.test.method"); if(method != null) { method = method.trim(); if(!"".equals(method) && !"${method}".equals(method)) { StringTokenizer st = new StringTokenizer(method, ","); List l = new LinkedList(); while(st.hasMoreTokens()) l.add(st.nextToken()); String[] tests = (String[])l.toArray(new String[l.size()]); return buildTestSuite(cls, tests); } } return new LimeTestSuite(cls); } public static TestSuite buildTestSuite(Class cls) { TestSuite suite = buildSingleTestSuite(cls); String timesP = System.getProperty("junit.test.times","1"); int times = 1 ; try { times = Integer.parseInt(timesP); } catch (NumberFormatException ignored ){} List tests = new LinkedList(); for (Enumeration e = suite.tests();e.hasMoreElements();) tests.add(e.nextElement()); while (times-- > 1) { for (Iterator iter = tests.iterator(); iter.hasNext();) suite.addTest((Test) iter.next()); } // add a warning if we are running individual tests if (!System.getProperty("junit.test.method","${method}").equals("${method}")) suite.addTest(warning("Warning - Full test suite has not been run.")); return suite; } /** * Build a test suite containing a single test from a specificed test class * @param cls The test class (must be subclassed from TestCase) * @param test The name of the test method in cls to be run * @return <tt>TestSuite</tt> object that can be returned by suite method */ public static TestSuite buildTestSuite(Class cls, String test) { _testClass = cls; return buildTestSuite(cls, new String[]{test}); } /** * Build a test suite containing a set of tests from a specificed test class * @param cls The test class (must be subclassed from TestCase) * @param test Array containing the names of the test methods in cls to be * run * @return <tt>TestSuite</tt> object that can be returned by suite method */ public static TestSuite buildTestSuite(Class cls, String[] tests) { _testClass = cls; TestSuite suite = new LimeTestSuite(); LimeTestSuite.setTestClass(cls); for (int ii = 0; ii < tests.length; ii++) { if (!tests[ii].startsWith("test")) tests[ii]="test"+tests[ii]; suite.addTest(TestSuite.createTest(cls, tests[ii])); } return suite; } /** * Get test save directory */ public File getSaveDirectory() { return _savedDir; } /** * Get test shared directory */ public File getSharedDirectory() { return _sharedDir; } /** * Recursively delete a directory. */ protected static void cleanFiles(File dir, boolean deleteDirs) { if ( dir == null ) return; File[] files = dir.listFiles(); for(int i=0; i< files.length; i++) { if ( files[i].isDirectory() ) { cleanFiles(files[i], deleteDirs); } else { files[i].delete(); } } if ( deleteDirs ) dir.delete(); } /** * Launches all backend servers. * * @throws <tt>IOException</tt> if the launching of either * backend fails */ public static void launchAllBackends() throws IOException { launchBackend(Backend.BACKEND_PORT); launchBackend(Backend.REJECT_PORT); } /** * Launch backend server if it is not running already * @throws IOException if attempt to launch backend server fails */ public static void launchBackend() throws IOException { launchBackend(Backend.BACKEND_PORT); } /** * Launch backend server if it is not running already * @throws IOException if attempt to launch backend server fails */ private static void launchBackend(int port) throws IOException { /* If we've already launched the backend, don't try it again */ int index = (port == Backend.REJECT_PORT ? 1 : 0); if (shutdownBackend[index]) return; /* Otherwise launch one if needed */ shutdownBackend[index] = Backend.launch(port); } /** * Shutdown any backend servers that we started * This must be static so LimeTestSuite can call it. * (Which implicitly means that shutdownBackend[] must * stay static also.) */ private static void shutdownBackends() { for (int ii = 0; ii < 2; ii++) { if (shutdownBackend[ii]) Backend.shutdown(ii == 1); } // Wait a couople seconds for any shutdown error reports. try { Thread.sleep(2000); } catch (InterruptedException ex) {} Backend.setErrorCallback(null); } /* * This is modified to run 'preSetUp' and 'postTearDown' as methods * which all tests will run, regardless of their implementation * (or lack of) of setUp and tearDown. * * It is also modified so that if setUp throws something, tearDown * will still be run. * */ public void runBare() throws Throwable { String testName = getName(); System.out.println("Running test: " + testName); assertNotNull(testName); try { preSetUp(); setUp(); runTest(); } finally { try { tearDown(); } finally { postTearDown(); } } } /** * Intercepted to allow us to get a handle to the test result, so we can * add errors from the ErrorService callback (giving us errors that were * triggered from outside of the test thread). */ public void run(TestResult result) { _testResult = result; super.run(result); } /** * Called before each test's setUp. * Used to determine which thread the test is running in, * set up the testing directories, and possibly print * debugging information (such as the current test being run) * This must also set the ErrorService's callback, so it * associates with the correct test object. */ public void preSetUp() throws Exception { _testThread = Thread.currentThread(); ErrorService.setErrorCallback(this); setupUniqueDirectories(); setupSettings(); setupTestTimer(); // The backend must also have its error callback reset // for each test, otherwise it could send errors to a stale // TestResult object (one whose test hasn't started or already ended). // But, we don't want to let the actual Backend class set it, // because that could provide an infinite loop of writing // to the socket, then reading it and rewriting it, // then reading it and rewriting it, etc... if (!(this instanceof Backend) ) Backend.setErrorCallback(this); } /** * Called statically before any settings. */ public static void beforeAllTestsSetUp() throws Throwable { // SystemUtils must pretend to not be loaded, so the idle // time isn't counted. // For tests that are testing SystemUtils specifically, they can // set loaded to true. SystemUtils.getIdleTime(); // make it loaded. // then unload it. PrivilegedAccessor.setValue(SystemUtils.class, "isLoaded", Boolean.FALSE); setupUniqueDirectories(); setupSettings(); } /** * Called after each test's tearDown. * Used to remove directories and possibly other things. */ public void postTearDown() { cleanFiles(_baseDir, false); stopTestTimer(); } /** * Runs after all tests are completed. */ public static void afterAllTestsTearDown() throws Throwable { cleanFiles(_baseDir, true); shutdownBackends(); } /** * Sets up the TimerTask to kill the running test after a certian amount of time. */ private final void setupTestTimer() { _startTimeForTest = System.currentTimeMillis(); _testKiller = new TimerTask() { public void run() { long now = System.currentTimeMillis(); error(new RuntimeException("Stalled! Took " + (now - _startTimeForTest) + " ms."), "Test Took Too Long"); } }; // kill in a bit. _testKillerTimer.schedule(_testKiller, 7 * 60 * 1000); } /** * Stops the test timer since the test finished. */ private final void stopTestTimer() { _testKiller.cancel(); _testKiller = null; } /** * Sets up settings to a pristine environment for this test. * Ensures that no settings are saved. */ public static void setupSettings() throws Exception{ SettingsHandler.setShouldSave(false); SettingsHandler.revertToDefault(); ConnectionSettings.DISABLE_UPNP.setValue(true); ConnectionSettings.DO_NOT_MULTICAST_BOOTSTRAP.setValue(true); UltrapeerSettings.NEED_MIN_CONNECT_TIME.setValue(false); SearchSettings.ENABLE_SPAM_FILTER.setValue(false); SharingSettings.setSaveDirectory(_savedDir); ContentSettings.CONTENT_MANAGEMENT_ACTIVE.setValue(false); ContentSettings.USER_WANTS_MANAGEMENTS.setValue(false); _incompleteDir = SharingSettings.INCOMPLETE_DIRECTORY.getValue(); setSharedDirectories( new File[] { _sharedDir } ); } /** * Creates a new directory prepended by the given name. */ public static File createNewBaseDirectory(String name) throws Exception { File t = getTestDirectory(); File f = new File(t, name); int append = 1; while ( f.exists() ) { f = new File(t, name + "_" + append); append++; } return f.getCanonicalFile(); } /** * Sets this test up to have unique directories. */ public static void setupUniqueDirectories() throws Exception { if( _baseDir == null ) { _baseDir = createNewBaseDirectory( _testClass.getName() ); } _savedDir = new File(_baseDir, "saved"); _sharedDir = new File(_baseDir, "shared"); _settingsDir = new File(_baseDir, "settings"); _xmlDir = new File(_settingsDir, "xml"); _xmlDataDir = new File(_xmlDir, "data"); _xmlSchemasDir = new File(_xmlDir, "schemas"); _baseDir.mkdirs(); _savedDir.mkdirs(); _sharedDir.mkdirs(); _settingsDir.mkdirs(); _xmlDir.mkdirs(); _xmlDataDir.mkdirs(); _xmlSchemasDir.mkdirs(); PrivilegedAccessor.setValue(CommonUtils.class, "SETTINGS_DIRECTORY", _settingsDir); File f = getRootDir(); // Expand the xml.war file. File xmlWar = new File(f, "gui/xml.war"); assertTrue(xmlWar.exists()); Expand.expandFile(xmlWar, _settingsDir); //make sure it'll delete even if something odd happens. // Expand the update.ver file. File updateVer = new File(f, "gui/update.ver"); assertTrue(updateVer.exists()); Expand.expandFile(updateVer, _settingsDir); _baseDir.deleteOnExit(); } /** * Get tests directory from a marker resource file. */ public static File getTestDirectory() throws Exception { return new File(getRootDir(), "testData"); } public static File getGUIDir() throws Exception { return new File(getRootDir(), "gui"); } public static File getCoreDir() throws Exception { return new File(getRootDir(), "core"); } public static File getRootDir() throws Exception { // Get a marker file. File f = CommonUtils.getResourceFile("com/limegroup/gnutella/Backend.java"); f = f.getCanonicalFile(); //gnutella // limegroup // com // tests // . return f.getParentFile().getParentFile().getParentFile().getParentFile().getParentFile(); } /** * Sets standard settings for a pristine test environment. */ public static void setStandardSettings() { SettingsHandler.revertToDefault(); SharingSettings.EXTENSIONS_TO_SHARE.setValue("tmp"); ConnectionSettings.NUM_CONNECTIONS.setValue(4); SearchSettings.GUESS_ENABLED.setValue(true); UltrapeerSettings.DISABLE_ULTRAPEER_MODE.setValue(false); UltrapeerSettings.EVER_ULTRAPEER_CAPABLE.setValue(true); UltrapeerSettings.FORCE_ULTRAPEER_MODE.setValue(true); ConnectionSettings.EVER_ACCEPTED_INCOMING.setValue(true); ConnectionSettings.CONNECT_ON_STARTUP.setValue(false); ConnectionSettings.LOCAL_IS_PRIVATE.setValue(false); ConnectionSettings.USE_GWEBCACHE.setValue(false); FilterSettings.BLACK_LISTED_IP_ADDRESSES.setValue( new String[] {"*.*.*.*"}); try { FilterSettings.WHITE_LISTED_IP_ADDRESSES.setValue( new String[] {"127.*.*.*",InetAddress.getLocalHost().getHostAddress()}); }catch(UnknownHostException bad) { fail(bad); } } /** * Fails the test with an AssertionFailedError and another * error as the root cause. */ static public void fail(Throwable e) { fail(null, e); } /** * Fails the test with an AssertionFailedError and another * error as the root cause, with a message. */ static public void fail(String message, Throwable e) { throw new UnexpectedExceptionError(message, e); } /** * Stub for error(Throwable, String) */ public void error(Throwable ex) { error(ex, null); } /** * This is the callback from ErrorService, and why we implement * ErrorCallback. * * It is used to catch errors that may or may not be inside of the * test thread. If it is in the thread, we can just rethrow the * error, and the test will fail as normal. If it is outside of the * thread, we want the test results to remember the error, but we * must allow the test to continue as normal, possibly succeeding, * failing or erroring. * * Note that while the XML formatter can easily handle the case of * multiple failures/errors in a single test, the XML->HTML converter * doesn't do that good of a job. It correctly lists the amount of * errors/failures, but it will only write the last one as the status * of the test, and will also only write the last one as the * message/stacktrace. */ public void error(Throwable ex, String detail) { ex = new UnexpectedExceptionError(detail, ex); // remember the detail & stack trace of the ErrorService. if ( _testThread != Thread.currentThread() ) { _testResult.addError(this, ex); _testThread.interrupt(); } else { fail("ErrorService callback error", ex); } } /** * Returns a test which will fail and log a warning message. * Copied from JUnit's TestSuite.java * Note that it does not have to extend BaseTestCase, just TestCase. * BaseTestCase would add needless complexity to an otherwise * simple failure message. */ private static Test warning(final String message) { return new TestCase("warning") { protected void runTest() { fail(message); } }; } ///////////////////////// Useful Testing Methods ////////////////////////// public static void setSharedDirectories(File[] dirs) { Set set = new HashSet(Arrays.asList(dirs)); SharingSettings.DIRECTORIES_TO_SHARE.setValue(set); } public static Set calculateAndCacheURN(File f) throws Exception { final Set myUrns = new HashSet(1); UrnCallback blocker = new UrnCallback() { public void urnsCalculated(File file, Set urns) { synchronized(myUrns) { myUrns.addAll(urns); myUrns.notify(); } } public boolean isOwner(Object o) { return false; } }; synchronized(myUrns) { UrnCache.instance().calculateAndCacheUrns(f, blocker); if(myUrns.isEmpty()) // only wait if it didn't fill immediately. myUrns.wait(3000); } return myUrns; } private static final int TIMEOUT = 2000; /** * Sends a pong through the connection to keep it alive. */ public static void keepAlive(Connection c) throws IOException { PingReply pr = PingReply.create(GUID.makeGuid(), (byte)1); c.send(pr); c.flush(); } /** * Sends a pong through all connections to keep them alive. */ public static void keepAllAlive(Connection[] cs) throws IOException { for(int i = 0; i < cs.length; i++) { PingReply pr = PingReply.create(GUID.makeGuid(), (byte)1); cs[i].send(pr); cs[i].flush(); } } /** * Tries to receive any outstanding messages on c * * @return <tt>true</tt> if this got a message, otherwise <tt>false</tt> */ public static boolean drain(Connection c) throws IOException { return drain(c, TIMEOUT); } public static boolean drain(Connection c, int timeout) throws IOException { if(!c.isOpen()) return false; boolean ret=false; for(int i = 0; i < 100; i++) { try { c.receive(timeout); ret = true; i = 0; } catch (InterruptedIOException e) { // we read a null message or received another // InterruptedIOException, which means a messages was not // received return ret; } catch (BadPacketException e) { // ignore... } } return ret; } /** * Tries to drain all messages from the array of connections. */ public static void drainAll(Connection[] conns) throws Exception { drainAll(conns, TIMEOUT); } /** * drains all messages from the given connections simultaneously. */ public static void drainAllParallel(final Connection [] conns) { Thread []r = new Thread[conns.length]; for (int i = 0; i < conns.length; i++) { final int index = i; r[i] = new ManagedThread() { public void managedRun() { try { drain(conns[index],TIMEOUT); } catch (Exception bad) { ErrorService.error(bad); } } }; r[i].start(); } for (int i = 0; i < r.length; i++) { try { r[i].join(); } catch (InterruptedException ignored) {} } } public static void drainAll(Connection[] cs, int tout) throws IOException { for (int i = 0; i < cs.length; i++) { if (cs[i].isOpen()) drain(cs[i], tout); } } /** * Returns true if no messages beside expected ones (such as QRP, Pings) * were received. */ public static boolean noUnexpectedMessages(Connection c) { return noUnexpectedMessages(c, TIMEOUT); } public static boolean noUnexpectedMessages(Connection c, int timeout) { for(int i = 0; i < 100; i++) { if(!c.isOpen()) return true; try { Message m = c.receive(timeout); if (m instanceof RouteTableMessage) ; else if (m instanceof PingRequest) ; else // we should never get any other sort of message... return false; i = 0; } catch (InterruptedIOException ie) { return true; } catch (BadPacketException e) { // ignore.... } catch (IOException ioe) { // ignore.... } } throw new RuntimeException("No IIOE or Message after 100 iterations"); } /** * Returns the first message of the expected type, ignoring * RouteTableMessages and PingRequests. */ public static Message getFirstMessageOfType(Connection c, Class type) { return getFirstMessageOfType(c, type, TIMEOUT); } public static Message getFirstMessageOfType(Connection c, Class type, int timeout) { for(int i = 0; i < 100; i++) { if(!c.isOpen()){ //System.out.println(c + " is not open"); return null; } try { Message m = c.receive(timeout); //System.out.println("m: " + m + ", class: " + m.getClass()); if (m instanceof RouteTableMessage) ; else if (m instanceof PingRequest) ; else if (type.isInstance(m)) return m; else return null; // this is usually an error.... i = 0; } catch (InterruptedIOException ie) { //ie.printStackTrace(); return null; } catch (BadPacketException e) { // e.printStackTrace(); // ignore... } catch (IOException ioe) { //ioe.printStackTrace(); // ignore.... } } throw new RuntimeException("No IIOE or Message after 100 iterations"); } public static Message getFirstInstanceOfMessageType(Connection c, Class type) throws BadPacketException { return getFirstInstanceOfMessageType(c, type, TIMEOUT); } public static Message getFirstInstanceOfMessageType(Connection c, Class type, int timeout) throws BadPacketException { for(int i = 0; i < 200; i++) { if(!c.isOpen()){ //System.out.println(c + " is not open"); return null; } try { Message m = c.receive(timeout); //System.out.println("m: " + m + ", class: " + m.getClass()); if (type.isInstance(m)) return m; i = 0; } catch (InterruptedIOException ie) { // ie.printStackTrace(); return null; } catch (IOException iox) { //ignore iox } } throw new RuntimeException("No IIOE or Message after 100 iterations"); } /** * @return the first message of type <pre>type</pre>. Read messages within * the time out, so it's possible to wait upto almost 2 * timeout for this * method to return */ public static Message getFirstInstanceOfMessage(Socket socket, Class type, int timeout) throws IOException, BadPacketException { int oldTimeout = socket.getSoTimeout(); try { for(int i=0; i<200; i++) { if(socket.isClosed()) return null; try { socket.setSoTimeout(timeout); Message m=Message.read(socket.getInputStream(), Message.N_TCP); if(type.isInstance(m)) return m; else if(m == null) //interruptedIOException thrown return null; i=0; } catch(InterruptedIOException iiox) { return null; } } } finally { //before we return reset the so-timeout socket.setSoTimeout(oldTimeout); } return null; } public static QueryRequest getFirstQueryRequest(Connection c) { return getFirstQueryRequest(c, TIMEOUT); } public static QueryRequest getFirstQueryRequest(Connection c, int tout) { return (QueryRequest)getFirstMessageOfType(c, QueryRequest.class, tout); } public static QueryReply getFirstQueryReply(Connection c) { return getFirstQueryReply(c, TIMEOUT); } public static QueryReply getFirstQueryReply(Connection c, int tout) { return (QueryReply)getFirstMessageOfType(c, QueryReply.class, tout); } public static void failIfAnyArrive(final Connection []connections, final Class type) throws Exception { Thread [] drainers = new ManagedThread[connections.length]; for (int i = 0; i < connections.length; i++) { final int index = i; drainers[i] = new ManagedThread() { public void managedRun() { try { Message m = getFirstInstanceOfMessageType(connections[index],type); assertNull(m); } catch (BadPacketException bad) { fail(bad); } }}; drainers[i].start(); } for(int i = 0;i < drainers.length;i++) drainers[i].join(); } }