/* | |
* 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.openjpa.lib.test; | |
import java.beans.BeanInfo; | |
import java.beans.IntrospectionException; | |
import java.beans.Introspector; | |
import java.beans.PropertyDescriptor; | |
import java.io.ByteArrayInputStream; | |
import java.io.ByteArrayOutputStream; | |
import java.io.File; | |
import java.io.FileInputStream; | |
import java.io.FileNotFoundException; | |
import java.io.FileOutputStream; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.io.ObjectInputStream; | |
import java.io.ObjectOutputStream; | |
import java.io.PrintStream; | |
import java.io.PrintWriter; | |
import java.io.StringWriter; | |
import java.lang.reflect.InvocationTargetException; | |
import java.lang.reflect.Method; | |
import java.math.BigDecimal; | |
import java.math.BigInteger; | |
import java.net.URL; | |
import java.text.NumberFormat; | |
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.Collection; | |
import java.util.Collections; | |
import java.util.Comparator; | |
import java.util.Date; | |
import java.util.HashMap; | |
import java.util.Iterator; | |
import java.util.LinkedList; | |
import java.util.List; | |
import java.util.ListIterator; | |
import java.util.Map; | |
import java.util.NoSuchElementException; | |
import java.util.StringTokenizer; | |
import junit.framework.TestCase; | |
import junit.framework.TestResult; | |
import junit.textui.TestRunner; | |
import org.apache.regexp.RE; | |
import org.apache.regexp.RESyntaxException; | |
import org.apache.regexp.REUtil; | |
import org.apache.tools.ant.AntClassLoader; | |
import org.apache.tools.ant.Project; | |
import org.apache.tools.ant.ProjectHelper; | |
import org.apache.openjpa.lib.log.Log; | |
import org.apache.openjpa.lib.log.LogFactoryImpl; | |
import org.apache.openjpa.lib.util.Localizer; | |
/** | |
* TestCase framework to run various tests against solarmetric code. | |
* This class contains various utility methods for the following functions: | |
* <ul> | |
* <li>Using multiple, isolated ClassLoaders</li> | |
* <li>Running a test in multiple concurrent threads</li> | |
* <li>Assertion helpers</li> | |
* <li>Creating random Strings, numbers, etc.</li> | |
* </ul> | |
* | |
* @author Marc Prud'hommeaux | |
* @author Patrick Linskey | |
*/ | |
public abstract class AbstractTestCase extends TestCase { | |
public static final String TEST_METHODS = | |
System.getProperty(AbstractTestCase.class.getName() + ".testMethods"); | |
public static final long PLATFORM_ALL = 2 << 1; | |
public static final long PLATFORM_UNKNOWN = 2 << 2; | |
public static final String SKIP_TOKEN = "SOLARSKIP"; | |
public static final String SKIP_DELIMITER = "|"; | |
private static final Localizer _loc = | |
Localizer.forPackage(AbstractTestCase.class); | |
protected String multiThreadExecuting = null; | |
protected boolean inTimeoutThread = false; | |
private Log log = null; | |
private static Map _times = new HashMap(); | |
private static AbstractTestCase _lastTest = null; | |
private static WatchdogThread _watchdog = new WatchdogThread(); | |
private long _timeout; | |
/** | |
* Constructor. Create a test case with the specified name. | |
*/ | |
public AbstractTestCase(String test) { | |
super(test); | |
} | |
public AbstractTestCase() { | |
} | |
protected final Log getLog() { | |
if (log == null) | |
log = newLog(); | |
return log; | |
} | |
protected Log newLog() { | |
// this implementation leaves much to be desired, as it just | |
// creates a new LogFactoryImpl each time, and does not apply | |
// any configurations. | |
return new LogFactoryImpl().getLog(getLogName()); | |
} | |
protected String getLogName() { | |
return "com.solarmetric.Runtime"; | |
} | |
/** | |
* Called before the watchdog thread is about to kill the entire | |
* JVM due to a test case's timeout. This method offers the | |
* ability to try to resolve whatever contention is taking place | |
* in the test. It will be given 10 seconds to try to end the | |
* test peacefully before the watchdog exits the JVM. | |
*/ | |
protected void preTimeout() { | |
} | |
public void run(TestResult result) { | |
if (skipTest()) { | |
// keep track of the tests we skip so that we can get an | |
// idea in the autobuild status | |
System.err.println(SKIP_TOKEN + SKIP_DELIMITER | |
+ ("" + getClass().getName()) | |
+ "." + getName() + SKIP_DELIMITER); | |
return; | |
} | |
if (_lastTest != null && _lastTest.getClass() != getClass()) { | |
try { | |
_lastTest.tearDownTestClass(); | |
} catch (Throwable t) { | |
getLog().error(null, t); | |
} | |
} | |
if (_lastTest == null || _lastTest.getClass() != getClass()) { | |
try { | |
setUpTestClass(); | |
} catch (Throwable t) { | |
getLog().error(null, t); | |
} | |
} | |
_lastTest = this; | |
// inform the watchdog thread that we are entering the test | |
_watchdog.enteringTest(this); | |
try { | |
super.run(result); | |
} finally { | |
_watchdog.leavingTest(this); | |
} | |
} | |
/** | |
* If this test should be skipped given the current | |
* environment, return <code>true</code>. This allows a unit test | |
* class to disable test cases on a per-method granularity, and | |
* prevents the test from showing up as a passed test just | |
* because it was skipped. | |
* For example, if a particular test case method should not be | |
* run against a certain database, this method could check the | |
* name of the test result and the current database configuration | |
* in order to make the decision: | |
* <p/> | |
* <code> protected boolean skipTest() { | |
* // don't run with pointbase: it uses a DataSource, which | |
* // can't be translated into a JBoss DataSource configuration. | |
* if ("testJBoss".equals(getName()) && | |
* getCurrentPlatform() == PLATFORM_POINTBASE) | |
* return true; | |
* } | |
* </code> | |
* If you want to disable execution of an entire test case | |
* class for a given database, you might want to add the class to | |
* the excluded test list in that database's properties file. | |
*/ | |
protected boolean skipTest() { | |
if (TEST_METHODS != null && TEST_METHODS.length() > 0) | |
return TEST_METHODS.indexOf(getName()) == -1; | |
return false; | |
} | |
/** | |
* This method is called before the first test in this test class | |
* is executed. | |
*/ | |
public void setUpTestClass() throws Exception { | |
} | |
/** | |
* This method is called after the last test in this test class | |
* is executed. It can be used to do things like clean up | |
* large, slow processes that may have been started. | |
*/ | |
public void tearDownTestClass() throws Exception { | |
} | |
public void tearDown() throws Exception { | |
if ("true".equals(System.getProperty("meminfo"))) | |
printMemoryInfo(); | |
super.tearDown(); | |
} | |
////////////////////////// | |
// Generating random data | |
////////////////////////// | |
/** | |
* Support method to get a random Integer for testing. | |
*/ | |
public static Integer randomInt() { | |
return new Integer((int) (Math.random() * Integer.MAX_VALUE)); | |
} | |
/** | |
* Support method to get a random Character for testing. | |
*/ | |
public static Character randomChar() { | |
char [] TEST_CHAR_ARRAY = new char []{ | |
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', | |
'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', | |
's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '1', | |
'2', '3', '4', '5', '6', '7', '8', '9' }; | |
return new Character(TEST_CHAR_ARRAY[ | |
(int) (Math.random() * TEST_CHAR_ARRAY.length)]); | |
} | |
/** | |
* Support method to get a random Long for testing. | |
*/ | |
public static Long randomLong() { | |
return new Long((long) (Math.random() * Long.MAX_VALUE)); | |
} | |
/** | |
* Support method to get a random Short for testing. | |
*/ | |
public static Short randomShort() { | |
return new Short((short) (Math.random() * Short.MAX_VALUE)); | |
} | |
/** | |
* Support method to get a random Double for testing. | |
*/ | |
public static Double randomDouble() { | |
return new Double((double) (Math.round(Math.random() * 5000d)) / 1000d); | |
} | |
/** | |
* Support method to get a random Float for testing. | |
*/ | |
public static Float randomFloat() { | |
return new Float((float) (Math.round(Math.random() * 5000f)) / 1000f); | |
} | |
/** | |
* Support method to get a random Byte for testing. | |
*/ | |
public static Byte randomByte() { | |
return new Byte((byte) (Math.random() * Byte.MAX_VALUE)); | |
} | |
/** | |
* Support method to get a random Boolean for testing. | |
*/ | |
public static Boolean randomBoolean() { | |
return new Boolean(Math.random() > 0.5 ? true : false); | |
} | |
/** | |
* Support method to get a random Date for testing. | |
*/ | |
public static Date randomDate() { | |
long millis = (long) (Math.random() * System.currentTimeMillis()); | |
// round millis to the nearest 1000: this is because some | |
// databases do not store the milliseconds correctly(e.g., MySQL). | |
// This is a really a bug we should fix. FC #27. | |
millis -= (millis % 1000); | |
return new Date(millis); | |
} | |
/** | |
* Support method to get a random String for testing. | |
*/ | |
public static String randomString() { | |
// default to a small string, in case column sizes are | |
// limited(such as with a string primary key) | |
return randomString(50); | |
} | |
/** | |
* Support method to get a random String for testing. | |
*/ | |
public static String randomString(int len) { | |
StringBuffer buf = new StringBuffer(); | |
for (int i = 0; i < (int) (Math.random() * len) + 1; i++) | |
buf.append(randomChar()); | |
return buf.toString(); | |
} | |
/** | |
* Support method to get a random clob for testing. | |
*/ | |
public static String randomClob() { | |
StringBuffer sbuf = new StringBuffer(); | |
while (sbuf.length() < (5 * 1024)) { // at least 5K | |
sbuf.append(randomString(1024)); | |
} | |
return sbuf.toString(); | |
} | |
/** | |
* Support method to get a random BigInteger for testing. | |
*/ | |
public static BigInteger randomBigInteger() { | |
// too many of our test databases don't support bigints > MAX_LONG: | |
// I don't like it, but for now, let's only test below MAX_LONG | |
BigInteger lng = new BigInteger( | |
((long) (Math.random() * Long.MAX_VALUE)) + ""); | |
BigInteger multiplier = new BigInteger("1"); | |
// (1 + (int)(Math.random() * 10000)) + ""); | |
if (Math.random() < 0.5) | |
multiplier = multiplier.multiply(new BigInteger("-1")); | |
return lng.multiply(multiplier); | |
} | |
/** | |
* Support method to get a random BigDecimal for testing. | |
*/ | |
public static BigDecimal randomBigDecimal() { | |
BigInteger start = randomBigInteger(); | |
String str = start.toString(); | |
// truncate off the last 8 digits: we still get some | |
// overflows with lame databases. | |
for (int i = 0; i < 8; i++) | |
if (str.length() > 2) | |
str = str.substring(0, str.length() - 1); | |
start = new BigInteger(str); | |
String val = start + "." + ((int) (Math.random() * 10)) | |
+ ((int) (Math.random() * 10)) | |
+ ((int) (Math.random() * 10)) | |
+ ((int) (Math.random() * 10)) | |
+ ((int) (Math.random() * 10)) | |
+ ((int) (Math.random() * 10)) | |
+ ((int) (Math.random() * 10)) | |
+ ((int) (Math.random() * 10)) | |
+ ((int) (Math.random() * 10)) | |
+ ((int) (Math.random() * 10)); | |
return new BigDecimal(val); | |
} | |
/** | |
* Support method to get a random blob for testing. | |
*/ | |
public static byte[] randomBlob() { | |
// up to 100K blob | |
byte [] blob = new byte [(int) (Math.random() * 1024 * 100)]; | |
for (int i = 0; i < blob.length; i++) | |
blob[i] = randomByte().byteValue(); | |
return blob; | |
} | |
/** | |
* Invoke setters for pimitives and primitive wrappers on the | |
* specified object. | |
*/ | |
public static Object randomizeBean(Object bean) | |
throws IntrospectionException, IllegalAccessException, | |
InvocationTargetException { | |
BeanInfo info = Introspector.getBeanInfo(bean.getClass()); | |
PropertyDescriptor [] props = info.getPropertyDescriptors(); | |
for (int i = 0; i < props.length; i++) { | |
Method write = props[i].getWriteMethod(); | |
if (write == null) | |
continue; | |
Class [] params = write.getParameterTypes(); | |
if (params == null || params.length != 1) | |
continue; | |
Class paramType = params[0]; | |
Object arg = null; | |
if (paramType == boolean.class || paramType == Boolean.class) | |
arg = randomBoolean(); | |
else if (paramType == byte.class || paramType == Byte.class) | |
arg = randomByte(); | |
else if (paramType == char.class || paramType == Character.class) | |
arg = randomChar(); | |
else if (paramType == short.class || paramType == Short.class) | |
arg = randomShort(); | |
else if (paramType == int.class || paramType == Integer.class) | |
arg = randomInt(); | |
else if (paramType == long.class || paramType == Long.class) | |
arg = randomLong(); | |
else if (paramType == double.class || paramType == Double.class) | |
arg = randomDouble(); | |
else if (paramType == float.class || paramType == Float.class) | |
arg = randomFloat(); | |
else if (paramType == String.class) | |
arg = randomString(); | |
else if (paramType == BigInteger.class) | |
arg = randomBigInteger(); | |
else if (paramType == BigDecimal.class) | |
arg = randomBigDecimal(); | |
else if (paramType == Date.class) | |
arg = randomDate(); | |
if (arg != null) | |
write.invoke(bean, new Object []{ arg }); | |
} | |
return bean; | |
} | |
/////////////////// | |
// Multi threading | |
/////////////////// | |
/** | |
* Re-execute the invoking method a random number of times | |
* in a random number of Threads. | |
*/ | |
public void mttest() throws ThreadingException { | |
// 6 iterations in 8 threads is a good trade-off between | |
// tests taking way too long and having a decent chance of | |
// identifying MT problems. | |
int iterations = 6; | |
int threads = 8; | |
mttest(threads, iterations); | |
} | |
/** | |
* Execute the calling method <code>iterations</code> | |
* times in <code>threads</code> Threads. | |
*/ | |
public void mttest(int threads, int iterations) { | |
mttest(0, threads, iterations); | |
} | |
public void mttest(int serialCount, int threads, int iterations) | |
throws ThreadingException { | |
String methodName = callingMethod("mttest"); | |
mttest(serialCount, threads, iterations, methodName, new Object [0]); | |
} | |
/** | |
* Execute a test method in multiple threads. | |
* | |
* @param threads the number of Threads to run in | |
* @param iterations the number of times the method should | |
* be execute in a single Thread | |
* @param method the name of the method to execute | |
* @param args the arguments to pass to the method | |
* @throws ThreadingException if an errors occur in | |
* any of the Threads. The actual exceptions | |
* will be embedded in the exception. Note that | |
* this means that assert() failures will be | |
* treated as errors rather than warnings. | |
* @author Marc Prud'hommeaux | |
*/ | |
public void mttest(int threads, int iterations, final String method, | |
final Object [] args) throws ThreadingException { | |
mttest(0, threads, iterations, method, args); | |
} | |
public void mttest(int serialCount, | |
int threads, int iterations, final String method, final Object [] args) | |
throws ThreadingException { | |
if (multiThreadExecuting != null && multiThreadExecuting.equals(method)) | |
{ | |
// we are currently executing in multi-threaded mode: | |
// don't deadlock! | |
return; | |
} | |
multiThreadExecuting = method; | |
try { | |
Class [] paramClasses = new Class [args.length]; | |
for (int i = 0; i < paramClasses.length; i++) | |
paramClasses[i] = args[i].getClass(); | |
final Method meth; | |
try { | |
meth = getClass().getMethod(method, paramClasses); | |
} catch (NoSuchMethodException nsme) { | |
throw new ThreadingException(nsme.toString(), nsme); | |
} | |
final Object thiz = this; | |
mttest("reflection invocation: (" + method + ")", | |
serialCount, threads, iterations, new VolatileRunnable() { | |
public void run() throws Exception { | |
meth.invoke(thiz, args); | |
} | |
}); | |
} finally { | |
multiThreadExecuting = null; | |
} | |
} | |
public void mttest(String title, final int threads, final int iterations, | |
final VolatileRunnable runner) throws ThreadingException { | |
mttest(title, 0, threads, iterations, runner); | |
} | |
/** | |
* Execute a test method in multiple threads. | |
* | |
* @param title a description of the test, for inclusion in the | |
* error message | |
* @param serialCount the number of times to run the method | |
* serially before spawning threads. | |
* @param threads the number of Threads to run in | |
* @param iterations the number of times the method should | |
* @param runner the VolatileRunnable that will execute | |
* the actual test from within the Thread. | |
* @throws ThreadingException if an errors occur in | |
* any of the Threads. The actual exceptions | |
* will be embedded in the exception. Note that | |
* this means that assert() failures will be | |
* treated as errors rather than warnings. | |
* @author Marc Prud'hommeaux be execute in a single Thread | |
* @author Marc Prud'hommeaux | |
*/ | |
public void mttest(String title, final int serialCount, | |
final int threads, final int iterations, final VolatileRunnable runner) | |
throws ThreadingException { | |
final List exceptions = Collections.synchronizedList(new LinkedList()); | |
Thread [] runners = new Thread [threads]; | |
final long startMillis = System.currentTimeMillis() + 1000; | |
for (int i = 1; i <= threads; i++) { | |
final int thisThread = i; | |
runners[i - 1] = | |
new Thread(title + " [" + i + " of " + threads + "]") { | |
public void run() { | |
// do our best to have all threads start at the exact | |
// same time. This is imperfect, but the closer we | |
// get to everyone starting at the same time, the | |
// better chance we have for identifying MT problems. | |
while (System.currentTimeMillis() < startMillis) | |
yield(); | |
int thisIteration = 1; | |
try { | |
for (; thisIteration <= iterations; thisIteration++) | |
{ | |
// go go go! | |
runner.run(); | |
} | |
} catch (Throwable error) { | |
synchronized (exceptions) { | |
// embed the exception into something that gives | |
// us some more information about the threading | |
// environment | |
exceptions.add(new ThreadingException("thread=" | |
+ this.toString() | |
+ ";threadNum=" + thisThread | |
+ ";maxThreads=" + threads | |
+ ";iteration=" + thisIteration | |
+ ";maxIterations=" + iterations, error)); | |
} | |
} | |
} | |
}; | |
} | |
// start the serial tests(does not spawn the threads) | |
for (int i = 0; i < serialCount; i++) { | |
runners[0].run(); | |
} | |
// start the multithreaded | |
for (int i = 0; i < threads; i++) { | |
runners[i].start(); | |
} | |
// wait for them all to complete | |
for (int i = 0; i < threads; i++) { | |
try { | |
runners[i].join(); | |
} catch (InterruptedException e) { | |
} | |
} | |
if (exceptions.size() == 0) | |
return; // sweeeeeeeet: no errors | |
// embed all the exceptions that were throws into a | |
// ThreadingException | |
Throwable [] errors = (Throwable []) exceptions.toArray( | |
new Throwable [0]); | |
throw new ThreadingException("The " | |
+ errors.length + " embedded errors " | |
+ "occured in the execution of " + iterations + " iterations " | |
+ "of " + threads + " threads: [" + title + "]", errors); | |
} | |
/** | |
* Check to see if we are in the top-level execution stack. | |
*/ | |
public boolean isRootThread() { | |
return multiThreadExecuting == null; | |
} | |
/** | |
* Return the last method name that called this one by | |
* parsing the current stack trace. | |
* | |
* @param exclude a method name to skip | |
* @throws IllegalStateException If the calling method could not be | |
* identified. | |
* @author Marc Prud'hommeaux | |
*/ | |
public String callingMethod(String exclude) { | |
// determine the currently executing method by | |
// looking at the stack track. Hackish, but convenient. | |
StringWriter sw = new StringWriter(); | |
new Exception().printStackTrace(new PrintWriter(sw)); | |
for (StringTokenizer stackTrace = new StringTokenizer(sw.toString(), | |
System.getProperty("line.separator")); | |
stackTrace.hasMoreTokens();) { | |
String line = stackTrace.nextToken().trim(); | |
// not a stack trace element | |
if (!(line.startsWith("at "))) | |
continue; | |
String fullMethodName = line.substring(0, line.indexOf("(")); | |
String shortMethodName = fullMethodName.substring( | |
fullMethodName.lastIndexOf(".") + 1); | |
// skip our own methods! | |
if (shortMethodName.equals("callingMethod")) | |
continue; | |
if (exclude != null && shortMethodName.equals(exclude)) | |
continue; | |
return shortMethodName; | |
} | |
throw new IllegalStateException("Could not identify calling " | |
+ "method in stack trace"); | |
} | |
/** | |
* A Runnable that can throw an Exception: used to test cases. | |
*/ | |
public static interface VolatileRunnable { | |
public void run() throws Exception; | |
} | |
/** | |
* Exception for errors caught during threading tests. | |
*/ | |
public class ThreadingException extends RuntimeException { | |
private final Throwable[] _nested; | |
public ThreadingException(String msg, Throwable nested) { | |
super(msg); | |
if (nested == null) | |
_nested = new Throwable[0]; | |
else | |
_nested = new Throwable[]{ nested }; | |
} | |
public ThreadingException(String msg, Throwable[] nested) { | |
super(msg); | |
if (nested == null) | |
_nested = new Throwable[0]; | |
else | |
_nested = nested; | |
} | |
public void printStackTrace() { | |
printStackTrace(System.out); | |
} | |
public void printStackTrace(PrintStream out) { | |
printStackTrace(new PrintWriter(out)); | |
} | |
public void printStackTrace(PrintWriter out) { | |
super.printStackTrace(out); | |
for (int i = 0; i < _nested.length; i++) { | |
out.print("Nested Throwable #" + (i + 1) + ": "); | |
_nested[i].printStackTrace(out); | |
} | |
} | |
} | |
////////// | |
// Timing | |
////////// | |
/** | |
* Sleep the current Thread for a random amount of time from 0-1000 ms. | |
*/ | |
public void sleepRandom() { | |
sleepRandom(1000); | |
} | |
/** | |
* Sleep the current Thread for a random amount of time from | |
* 0-<code>max</code> ms. | |
*/ | |
public void sleepRandom(int max) { | |
try { | |
Thread.currentThread().sleep((long) (Math.random() * max)); | |
} catch (InterruptedException ex) { | |
} | |
} | |
/** | |
* Re-run this method in the current thread, timing out | |
* after the specified number of seconds. | |
* Usage: | |
* <pre> public void timeOutOperation() { if (timeout(5 * 1000)) return; | |
* Thread.currentThread().sleep(10 * 1000); } | |
* </pre> | |
* <p/> | |
* <p/> | |
* <strong>Warning</strong> this method should be used sparingly, | |
* and only when you expect that a timeout will <strong>not</strong> | |
* occur. It utilized the deprecated {@link Thread#stop()} and | |
* {@link Thread#interrupt} methods, which can leave monitors in an | |
* invalid state. It is only used because it provides more | |
* meaningful information than just seeing that the entire autobuild | |
* timed out. | |
* | |
* @param millis the number of milliseconds we should wait. | |
* @return true if we are are in the thread that requested the | |
* timeout, false if we are in the timeout thread itself. | |
*/ | |
public boolean timeout(long millis) throws Throwable { | |
String methodName = callingMethod("timeout"); | |
return timeout(millis, methodName); | |
} | |
/** | |
* @see #timeout(long) | |
*/ | |
public boolean timeout(long millis, String methodName) throws Throwable { | |
// we are in the timing out-thread: do nothing so the | |
// actual test method can run | |
if (inTimeoutThread) | |
return false; | |
inTimeoutThread = true; | |
long endTime = System.currentTimeMillis() + millis; | |
try { | |
final Method method = getClass(). | |
getMethod(methodName, (Class[]) null); | |
final Object thz = this; | |
// spawn thread | |
TimeOutThread tot = new TimeOutThread("TimeOutThread [" | |
+ methodName + "] (" + millis + "ms)") { | |
public void run() { | |
try { | |
method.invoke(thz, (Object[]) null); | |
} catch (Throwable t) { | |
throwable = t; | |
} finally { | |
completed = true; | |
} | |
} | |
}; | |
tot.start(); | |
// wait for the completion or a timeout to occur | |
tot.join(millis); | |
// have we timed out? Kill the thread and throw an exception | |
if (System.currentTimeMillis() >= endTime) { | |
// if we are waiting on a monitor, this will give | |
// us a useful stack trace. | |
try { | |
tot.interrupt(); | |
} catch (Throwable e) { | |
} | |
Thread.currentThread().sleep(500); | |
// try to kill the thread | |
try { | |
tot.stop(); | |
} catch (Throwable e) { | |
} | |
Thread.currentThread().sleep(500); | |
throw new OperationTimedOutException("Execution of \"" | |
+ methodName + "\" timed out after " | |
+ millis + " milliseconds", tot.throwable); | |
} | |
// throw any exceptions that may have occured | |
if (tot.throwable != null) | |
throw tot.throwable; | |
// I guess everything was OK | |
return true; | |
} finally { | |
inTimeoutThread = false; | |
} | |
} | |
/** | |
* Utility method to start a profile. | |
* | |
* @see #endProfile(String) | |
*/ | |
public void startProfile(String name) { | |
_times.put(name, new Long(System.currentTimeMillis())); | |
} | |
/** | |
* Utility to end the profile and print out the time. Example usage: | |
* <p/> | |
* <pre><code> startProfile("Some long task"); doSomeLongTask(); | |
* endProfile("Some long task"); | |
* </code></pre> | |
* | |
* @param name | |
* @return the amount of time that this profile invocation took, or | |
* -1 if <code>name</code> was never started. | |
*/ | |
public long endProfile(String name) { | |
Long time = (Long) _times.remove(name); | |
long elapsed = -1; | |
if (time != null) | |
elapsed = System.currentTimeMillis() - time.longValue(); | |
getLog().info(_loc.get("profile-info", name, | |
(time == null ? "???" : "" + elapsed))); | |
return elapsed; | |
} | |
private static class TimeOutThread extends Thread { | |
public Throwable throwable = null; | |
public boolean completed = false; | |
public TimeOutThread(String name) { | |
super(name); | |
setDaemon(true); | |
} | |
} | |
/** | |
* Indicates that a timeout occured. | |
*/ | |
public static class OperationTimedOutException extends RuntimeException { | |
private final Throwable _err; | |
public OperationTimedOutException(String msg, Throwable throwable) { | |
super(msg); | |
_err = throwable; | |
} | |
public void printStackTrace() { | |
printStackTrace(System.out); | |
} | |
public void printStackTrace(PrintStream out) { | |
printStackTrace(new PrintWriter(out)); | |
} | |
public void printStackTrace(PrintWriter out) { | |
super.printStackTrace(out); | |
if (_err != null) { | |
out.print("Nested Throwable: "); | |
_err.printStackTrace(out); | |
} | |
} | |
} | |
///////////////////////// | |
// ClassLoader functions | |
///////////////////////// | |
/** | |
* Create a ClassLoader that will not use the parent | |
* ClassLoader to resolve classes. This is useful for | |
* testing interactions between Kodo in running | |
* in ClassLoaderA and instances in ClassLoaderB. | |
*/ | |
public ClassLoader createIsolatedClassLoader() { | |
return new IsolatedClassLoader(); | |
} | |
public NestedClassLoader createNestedClassLoader() { | |
return new NestedClassLoader(false); | |
} | |
public NestedClassLoader createNestedParentClassLoader() { | |
return new NestedClassLoader(true); | |
} | |
/** | |
* Reload the specified class in an isolated ClassLoader. | |
* | |
* @param target the target class to load | |
* @return the Class as reloaded in an new ClassLoader | |
*/ | |
public Class isolate(Class target) throws ClassNotFoundException { | |
Class result = isolate(target.getName()); | |
assertTrue(result != target); | |
assertNotEquals(result, target); | |
assertTrue(result.getClassLoader() != target.getClassLoader()); | |
return result; | |
} | |
public Class isolate(String target) throws ClassNotFoundException { | |
ClassLoader il = createIsolatedClassLoader(); | |
Class result = il.loadClass(target); | |
assertEquals(result.getName(), target); | |
return result; | |
} | |
public Class nest(Class target) throws ClassNotFoundException { | |
ClassLoader il = createNestedClassLoader(); | |
Class result = il.loadClass(target.getName()); | |
assertTrue(result != target); | |
assertNotEquals(result, target); | |
assertTrue(result.getClassLoader() != target.getClassLoader()); | |
assertEquals(result.getName(), target.getName()); | |
return result; | |
} | |
public Object isolateNew(Class target) | |
throws ClassNotFoundException, IllegalAccessException, | |
InstantiationException { | |
return isolate(target).newInstance(); | |
} | |
private static class NestedClassLoader extends AntClassLoader { | |
public NestedClassLoader(boolean useParent) { | |
super(ClassLoader.getSystemClassLoader(), useParent); | |
for (StringTokenizer cltok = new StringTokenizer( | |
System.getProperty("java.class.path"), File.pathSeparator); | |
cltok.hasMoreTokens();) { | |
String path = cltok.nextToken(); | |
// only load test paths, not jar files | |
if (path.indexOf(".jar") != -1) | |
continue; | |
if (path.indexOf(".zip") != -1) | |
continue; | |
addPathElement(path); | |
} | |
try { | |
if (!useParent) { | |
assertTrue(loadClass | |
(AbstractTestCase.class.getName()).getClassLoader() | |
!= AbstractTestCase.class.getClassLoader()); | |
} | |
} catch (ClassNotFoundException cnfe) { | |
fail(cnfe.toString()); | |
} | |
} | |
public Class findClass(String name) throws ClassNotFoundException { | |
// don't isolate PC and related classes in kodo.enhnace | |
if (name.indexOf(".enhance.") != -1) | |
throw new ClassNotFoundException(name); | |
if (name.indexOf("/enhance/") != -1) | |
throw new ClassNotFoundException(name); | |
return super.findClass(name); | |
} | |
} | |
/** | |
* A ClassLoader that is completely isolated with respect to | |
* any classes that are loaded in the System ClassLoader. | |
* | |
* @author Marc Prud'hommeaux | |
*/ | |
private static class IsolatedClassLoader extends NestedClassLoader { | |
public IsolatedClassLoader() { | |
super(false); | |
setIsolated(false); | |
} | |
} | |
/////////////// | |
// Collections | |
/////////////// | |
/** | |
* Validate that the specified {@link Collection} fulfills the | |
* Collection contract as specified by the Collections API. | |
* <p/> | |
* <strong>Note</strong>: does not validate mutable operations | |
*/ | |
public static void validateCollection(Collection collection) { | |
int size = collection.size(); | |
int iterated = 0; | |
// ensure we can walk along the iterator | |
for (Iterator i = collection.iterator(); i.hasNext();) { | |
iterated++; | |
i.next(); | |
} | |
// ensure the number of values iterated is the same as the list size | |
assertEquals(size, iterated); | |
// also validate the list | |
if (collection instanceof List) { | |
List ll = new ArrayList(); | |
for (int i = 0; i < 100; i++) | |
ll.add(new Integer(i)); | |
validateList((List) ll); | |
validateList((List) collection); | |
} | |
} | |
/** | |
* Validate that the specified {@link List} fulfills the | |
* List contract as specified by the Collections API. | |
* <p/> | |
* <strong>Note</strong>: does not validate mutable operations | |
*/ | |
public static void validateList(List list) { | |
Object [] coreValues = list.toArray(); | |
Object [] values1 = new Object [list.size()]; | |
Object [] values2 = new Object [list.size()]; | |
Object [] values3 = new Object [list.size()]; | |
Object [] values4 = new Object [list.size()]; | |
// fill sequential index access list | |
for (int i = 0; i < list.size(); i++) | |
values1[i] = list.get(i); | |
// fill sequential list | |
int index = 0; | |
ListIterator iter; | |
for (iter = list.listIterator(0); iter.hasNext();) { | |
assertEquals(index, iter.nextIndex()); | |
assertEquals(index, iter.previousIndex() + 1); | |
values2[index] = iter.next(); | |
assertTrue(list.contains(values2[index])); | |
index++; | |
} | |
// ensure NoSuchElementException is thrown as appropriate | |
try { | |
iter.next(); | |
fail("next() should have resulted in a NoSuchElementException"); | |
} catch (NoSuchElementException e) { | |
} // as expected | |
// fill reverse sequential list | |
int back = 0; | |
for (iter = list.listIterator(list.size()); iter.hasPrevious();) { | |
assertEquals(index, iter.previousIndex() + 1); | |
assertEquals(index, iter.nextIndex()); | |
values3[--index] = iter.previous(); | |
back++; | |
} | |
assertEquals(list.size(), back); | |
// ensure NoSuchElementException is thrown as appropriate | |
try { | |
iter.previous(); | |
fail("previous() should have resulted in a " | |
+ "NoSuchElementException"); | |
} catch (NoSuchElementException e) { | |
} // as expected | |
// fill random access list | |
List indices = new LinkedList(); | |
for (int i = 0; i < list.size(); i++) | |
indices.add(new Integer(i)); | |
for (int i = 0; i < list.size(); i++) { | |
int rand = (int) (Math.random() * indices.size()); | |
Integer randIndex = (Integer) indices.remove(rand); | |
values4[randIndex.intValue()] = list.get(randIndex.intValue()); | |
} | |
assertEquals(Arrays.asList(coreValues), Arrays.asList(values1)); | |
assertIdentical(Arrays.asList(coreValues), Arrays.asList(values1)); | |
assertEquals(Arrays.asList(coreValues), Arrays.asList(values2)); | |
assertIdentical(Arrays.asList(coreValues), Arrays.asList(values2)); | |
assertEquals(Arrays.asList(coreValues), Arrays.asList(values4)); | |
assertIdentical(Arrays.asList(coreValues), Arrays.asList(values4)); | |
assertEquals(Arrays.asList(coreValues), Arrays.asList(values3)); | |
assertIdentical(Arrays.asList(coreValues), Arrays.asList(values3)); | |
} | |
/** | |
* Assert that the given List contain the exact same | |
* elements. This is different than the normal List contract, which | |
* states that list1.equals(list2) if each element e1.equals(e2). | |
* This method asserts that e1 == n2. | |
*/ | |
public static void assertIdentical(List c1, List c2) { | |
assertEquals(c1.size(), c2.size()); | |
for (Iterator i1 = c1.iterator(), i2 = c2.iterator(); | |
i1.hasNext() && i2.hasNext();) | |
assertTrue(i1.next() == i2.next()); | |
} | |
/** | |
* Assert that the collection parameter is already ordered | |
* according to the specified comparator. | |
*/ | |
public void assertOrdered(Collection c, Comparator comp) { | |
List l1 = new LinkedList(c); | |
List l2 = new LinkedList(c); | |
assertEquals(l1, l2); | |
Collections.sort(l2, comp); | |
assertEquals(l1, l2); | |
Collections.sort(l1, comp); | |
assertEquals(l1, l2); | |
} | |
//////////////////// | |
// Assertion Helpers | |
//////////////////// | |
public void assertNotEquals(Object a, Object b) { | |
if (a == null && b != null) | |
return; | |
if (a != null && b == null) | |
return; | |
if (!(a.equals(b))) | |
return; | |
if (!(b.equals(a))) | |
return; | |
fail("expected !<" + a + ">.equals(<" + b + ">)"); | |
} | |
public void assertSize(int size, Object ob) { | |
if (ob == null) { | |
assertEquals(size, 0); | |
return; | |
} | |
if (ob instanceof Collection) | |
ob = ((Collection) ob).iterator(); | |
if (ob instanceof Iterator) { | |
Iterator i = (Iterator) ob; | |
int count = 0; | |
while (i.hasNext()) { | |
count++; | |
i.next(); | |
} | |
assertEquals(size, count); | |
} else | |
fail("assertSize: expected Collection, Iterator, " | |
+ "Query, or Extent, but got " + ob.getClass().getName()); | |
} | |
///////////////////// | |
// Generic utilities | |
///////////////////// | |
public void copy(File from, File to) throws IOException { | |
copy(new FileInputStream(from), to); | |
} | |
public void copy(InputStream in, File to) throws IOException { | |
FileOutputStream fout = new FileOutputStream(to); | |
byte[] b = new byte[1024]; | |
for (int n = 0; (n = in.read(b)) != -1;) | |
fout.write(b, 0, n); | |
} | |
/** | |
* Print out information on memory usage. | |
*/ | |
public void printMemoryInfo() { | |
Runtime rt = Runtime.getRuntime(); | |
long total = rt.totalMemory(); | |
long free = rt.freeMemory(); | |
long used = total - free; | |
NumberFormat nf = NumberFormat.getInstance(); | |
getLog().warn(_loc.get("mem-info", | |
nf.format(used), | |
nf.format(total), | |
nf.format(free))); | |
} | |
/** | |
* Return a list of all values iterated by the given iterator. | |
*/ | |
public static List iteratorToList(Iterator i) { | |
LinkedList list = new LinkedList(); | |
while (i.hasNext()) | |
list.add(i.next()); | |
return list; | |
} | |
/** | |
* Return an array of the objects iterated by the given iterator. | |
*/ | |
public static Object [] iteratorToArray(Iterator i, Class [] clazz) { | |
return iteratorToList(i).toArray(clazz); | |
} | |
/** | |
* Run ant on the specified build file. | |
* | |
* @param buildFile the build file to use | |
* @param target the name of the target to invoke | |
*/ | |
public void ant(File buildFile, String target) { | |
assertTrue(buildFile.isFile()); | |
Project project = new Project(); | |
project.init(); | |
project.setUserProperty("ant.file", buildFile.getAbsolutePath()); | |
ProjectHelper.configureProject(project, buildFile); | |
project.executeTarget(target); | |
} | |
/** | |
* Serialize and deserialize the object. | |
* | |
* @param validateEquality make sure the hashCode and equals | |
* methods hold true | |
*/ | |
public static Object roundtrip(Object orig, boolean validateEquality) | |
throws IOException, ClassNotFoundException { | |
assertNotNull(orig); | |
ByteArrayOutputStream bout = new ByteArrayOutputStream(); | |
ObjectOutputStream out = new ObjectOutputStream(bout); | |
out.writeObject(orig); | |
ByteArrayInputStream bin = new ByteArrayInputStream( | |
bout.toByteArray()); | |
ObjectInputStream in = new ObjectInputStream(bin); | |
Object result = in.readObject(); | |
if (validateEquality) { | |
assertEquals(orig.hashCode(), result.hashCode()); | |
assertEquals(orig, result); | |
} | |
return result; | |
} | |
/** | |
* @return true if the specified input matches the regular expression regex. | |
*/ | |
public static boolean matches(String regex, String input) | |
throws RESyntaxException { | |
RE re = REUtil.createRE(regex); | |
return re.match(input); | |
} | |
public static void assertMatches(String regex, String input) { | |
try { | |
if (!(matches(regex, input))) | |
fail("Expected regular expression: <" + regex + ">" | |
+ " did not match: <" + input + ">"); | |
} catch (RESyntaxException e) { | |
throw new IllegalArgumentException(e.toString()); | |
} | |
} | |
public static void assertNotMatches(String regex, String input) { | |
try { | |
if (matches(regex, input)) | |
fail("Regular expression: <" + regex + ">" | |
+ " should not match: <" + input + ">"); | |
} catch (RESyntaxException e) { | |
throw new IllegalArgumentException(e.toString()); | |
} | |
} | |
/** | |
* Check the list if strings and return the ones that match | |
* the specified match. | |
*/ | |
public static List matches(String regex, Collection input) | |
throws RESyntaxException { | |
List matches = new ArrayList(); | |
for (Iterator i = input.iterator(); i.hasNext();) { | |
String check = (String) i.next(); | |
if (matches(regex, check)) | |
matches.add(check); | |
} | |
return matches; | |
} | |
/** | |
* Assert that the specified collection of Strings contains at least | |
* one string that matches the specified regular expression. | |
*/ | |
public static void assertMatches(String regex, Collection input) { | |
try { | |
if (matches(regex, input).size() == 0) | |
fail("The specified list of size " + input.size() | |
+ " did not contain any strings that match the" | |
+ " specified regular expression(\"" + regex + "\")"); | |
} catch (RESyntaxException e) { | |
throw new IllegalArgumentException(e.toString()); | |
} | |
} | |
/** | |
* Assert that the specified collection of Strings does not match | |
* the specified regular expression. | |
*/ | |
public static void assertNotMatches(String regex, Collection input) { | |
try { | |
List matches; | |
if (((matches = matches(regex, input))).size() > 0) | |
fail("The specified list of size " + input.size() | |
+ " did contain one or more strings that matchs the" | |
+ " specified illegal regular expression" | |
+ " (\"" + regex + "\")." | |
+ " First example of a matching message is: " | |
+ matches.iterator().next()); | |
} catch (RESyntaxException e) { | |
throw new IllegalArgumentException(e.toString()); | |
} | |
} | |
/** | |
* To be called by the child. E.g.: | |
* <code> public static void main(String [] args) { main(TestBug375.class); | |
* } | |
* </code> | |
*/ | |
public static void main(Class c) { | |
TestRunner.run(c); | |
} | |
/** | |
* To be called by child. Figures out the class from the calling context. | |
*/ | |
public static void main() { | |
String caller = new SecurityManager() { | |
public String toString() { | |
return getClassContext()[2].getName(); | |
} | |
}.toString(); | |
try { | |
main(Class.forName(caller)); | |
} catch (ClassNotFoundException cnfe) { | |
throw new RuntimeException(cnfe.toString()); | |
} | |
} | |
/** | |
* Returns the jar file in which the class is contained. | |
* | |
* @return the jar file, or none if the class is not in a jar | |
* @throws FileNotFoundException if the jar file cannot located | |
*/ | |
public static File getJarFile(Class clazz) throws FileNotFoundException { | |
URL url = clazz.getResource(clazz.getName().substring( | |
clazz.getName().lastIndexOf(".") + 1) + ".class"); | |
if (url == null) | |
throw new FileNotFoundException(clazz.toString()); | |
String file = url.getFile(); | |
if (file == null) | |
throw new FileNotFoundException(url.toString()); | |
int index = file.indexOf("!"); | |
if (index == -1) | |
throw new FileNotFoundException(file); | |
file = file.substring(0, index); | |
file = file.substring("file:".length()); | |
File f = new File(file); | |
if (!(f.isFile())) | |
throw new FileNotFoundException(file); | |
return f.getAbsoluteFile(); | |
} | |
/** | |
* The number of milliseconds each test case will have for a timeout. | |
*/ | |
public void setTimeout(long timeout) { | |
_timeout = timeout; | |
} | |
/** | |
* The number of milliseconds each test case will have for a timeout. | |
*/ | |
public long getTimeout() { | |
return _timeout; | |
} | |
/** | |
* A watchdog that just exits the JVM if a test has not completed in | |
* a certain amount of time. This speeds up the mechanism of determining | |
* if a timeout has occurred, since we can exit the entire test run | |
* if a test hasn't completed in a shorted amount of time than | |
* the global test timeout. | |
* | |
* @author Marc Prud'hommeaux | |
*/ | |
private static class WatchdogThread extends Thread { | |
private final long _timeoutms; | |
private long _endtime = -1; | |
private AbstractTestCase _curtest = null; | |
public WatchdogThread() { | |
super("Kodo test case watchdog thread"); | |
setDaemon(true); | |
int timeoutMin = new Integer | |
(System.getProperty("autobuild.testcase.timeout", "20")) | |
.intValue(); | |
_timeoutms = timeoutMin * 60 * 1000; | |
} | |
public void run() { | |
while (true) { | |
try { | |
sleep(200); | |
} catch (InterruptedException ie) { | |
} | |
if (_endtime > 0 && System.currentTimeMillis() > _endtime) { | |
Thread preTimeout = new Thread | |
("Attempting pre-timeout for " + _curtest) { | |
public void run() { | |
_curtest.preTimeout(); | |
} | |
}; | |
preTimeout.start(); | |
// wait a little while for the pre-timeout | |
// thread to complete | |
try { | |
preTimeout.join(10 * 1000); | |
} catch (Exception e) { | |
} | |
// give it a few more seconds... | |
try { | |
sleep(5 * 1000); | |
} catch (Exception e) { | |
} | |
// new endtime? resume... | |
if (System.currentTimeMillis() < _endtime) | |
continue; | |
new Exception("test case " | |
+ (_curtest != null ? _curtest.getName() | |
: "UNKNOWN") + " timed out after " | |
+ _timeoutms + "ms").printStackTrace(); | |
// also run "killall -QUIT java" to try to grab | |
// a stack trace | |
try { | |
Runtime.getRuntime().exec | |
(new String[]{ "killall", "-QUIT", "java" }); | |
} catch (Exception e) { | |
} | |
try { | |
sleep(1000); | |
} catch (InterruptedException ie) { | |
} | |
// now actually exit | |
System.exit(111); | |
} | |
} | |
} | |
public synchronized void enteringTest(AbstractTestCase test) { | |
long timeout = test.getTimeout(); | |
if (timeout <= 0) | |
timeout = _timeoutms; | |
_endtime = System.currentTimeMillis() + timeout; | |
_curtest = test; | |
if (!isAlive()) | |
start(); | |
} | |
public synchronized void leavingTest(AbstractTestCase test) { | |
_endtime = -1; | |
_curtest = null; | |
} | |
} | |
} |