| /* |
| * 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. |
| */ |
| /* |
| * 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.persistence.common.utils; |
| |
| import java.beans.BeanInfo; |
| import java.beans.Introspector; |
| import java.beans.PropertyDescriptor; |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| 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.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.EnumSet; |
| 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.Map.Entry; |
| import java.util.NoSuchElementException; |
| import java.util.StringTokenizer; |
| |
| import javax.management.IntrospectionException; |
| import javax.persistence.EntityManager; |
| import javax.persistence.EntityManagerFactory; |
| import javax.persistence.Query; |
| |
| import org.apache.openjpa.conf.OpenJPAConfiguration; |
| import org.apache.openjpa.kernel.BrokerFactory; |
| import org.apache.openjpa.kernel.OpenJPAStateManager; |
| import org.apache.openjpa.lib.log.Log; |
| import org.apache.openjpa.meta.ClassMetaData; |
| import org.apache.openjpa.persistence.JPAFacadeHelper; |
| import org.apache.openjpa.persistence.OpenJPAEntityManager; |
| import org.apache.openjpa.persistence.OpenJPAEntityManagerFactory; |
| import org.apache.openjpa.persistence.OpenJPAPersistence; |
| import org.apache.openjpa.persistence.test.AbstractCachedEMFTestCase; |
| import org.apache.regexp.RE; |
| import org.apache.regexp.RESyntaxException; |
| import org.apache.regexp.REUtil; |
| |
| /** |
| * Extends AbstractCachedEMFTestCase, which extends AbstractPersistenceTestCase. |
| * Cleans up EMFs returned by getEmf() in tearDown(). |
| * |
| * @version $Rev$ $Date$ |
| */ |
| public abstract class AbstractTestCase extends AbstractCachedEMFTestCase { |
| |
| private String persistenceXmlResource; |
| private Map<Map,OpenJPAEntityManagerFactory> emfs = |
| new HashMap<>(); |
| private OpenJPAEntityManager currentEntityManager; |
| private Object[] props; |
| |
| protected enum Platform { |
| EMPRESS, |
| HYPERSONIC, |
| POSTGRESQL, |
| MARIADB, |
| MYSQL, |
| SQLSERVER, |
| DB2, |
| ORACLE, |
| DERBY, |
| INFORMIX, |
| POINTBASE, |
| SYBASE, |
| INGRES, |
| } |
| |
| protected String multiThreadExecuting = null; |
| protected boolean inTimeoutThread = false; |
| |
| |
| public AbstractTestCase(String name, String s) { |
| setName(name); |
| persistenceXmlResource = computePersistenceXmlResource(s); |
| } |
| |
| /** |
| * Use the given persistent types during the test. |
| * |
| * @param props |
| * list of persistent types used in testing and/or configuration values in the form |
| * key,value,key,value... |
| */ |
| protected void setUp(Object... props) throws Exception { |
| super.setUp(); |
| this.props = props; |
| } |
| |
| /** |
| * Closes any EMFs created by getEmf() |
| * |
| */ |
| @Override |
| public void tearDown() throws Exception { |
| try { |
| super.tearDown(); |
| } finally { |
| for (EntityManagerFactory emf : emfs.values()) { |
| try { |
| // closeEMF() will also close any open/active EMs |
| closeEMF(emf); |
| emf = null; |
| } catch (Exception e) { |
| e.printStackTrace(); |
| } |
| } |
| } |
| } |
| |
| public AbstractTestCase() { |
| } |
| |
| public AbstractTestCase(String name) { |
| setName(name); |
| } |
| |
| protected String computePersistenceXmlResource(String s) { |
| String resourceName = getClass().getPackage().getName() |
| .replaceAll("\\.", "/"); |
| resourceName += "/common/apps/META-INF/persistence.xml"; |
| URL resource = getClass().getClassLoader().getResource(resourceName); |
| if (resource != null) |
| return resourceName; |
| return defaultPersistenceXmlResource(); |
| } |
| |
| protected String defaultPersistenceXmlResource() { |
| return "org/apache/openjpa/persistence/" + |
| "common/apps/META-INF/persistence.xml"; |
| } |
| |
| protected OpenJPAStateManager getStateManager(Object obj, |
| EntityManager em) { |
| return JPAFacadeHelper.toBroker(em).getStateManager(obj); |
| } |
| |
| protected int deleteAll(Class type, EntityManager em) { |
| ClassMetaData meta = JPAFacadeHelper.getMetaData(em, type); |
| if (meta != null) |
| return em.createQuery("delete from " + meta.getTypeAlias()) |
| .executeUpdate(); |
| else |
| return -1; |
| } |
| |
| protected int deleteAll(Class... types) { |
| EntityManager em = getEmf().createEntityManager(); |
| em.getTransaction().begin(); |
| int ret = 0; |
| for (Class type : types) |
| ret += deleteAll(type, em); |
| em.getTransaction().commit(); |
| em.close(); |
| return ret; |
| } |
| |
| /** |
| * Creates a EMF and adds it to a Map for cleanup in tearDown() |
| * |
| * @param map |
| */ |
| protected OpenJPAEntityManagerFactory getEmf(Map map) { |
| if (map == null) |
| map = new HashMap(); |
| Collection keys = new ArrayList(); |
| for (Object key : map.keySet()) |
| if (key.toString().startsWith("kodo")) |
| keys.add(key); |
| if (keys.size() > 0) |
| throw new IllegalArgumentException( |
| "kodo-prefixed properties must be converted to openjpa. " + |
| "Properties: " + keys); |
| |
| addProperties(map); |
| |
| OpenJPAEntityManagerFactory emf = emfs.get(map); |
| if (emf != null) { |
| return emf; |
| } |
| |
| if (props != null) { |
| // Join properties passed in setUp (usually entities) with the given map and use them to create EMF. |
| Object[] propsAndMap = new Object[props.length + map.size() * 2]; |
| System.arraycopy(props, 0, propsAndMap, 0, props.length); |
| int i = props.length; |
| for (Object o : map.entrySet()) { |
| Entry mapEntry = (Entry) o; |
| propsAndMap[i++] = mapEntry.getKey(); |
| propsAndMap[i++] = mapEntry.getValue(); |
| } |
| emf = createEMF(propsAndMap); |
| } else { |
| emf = OpenJPAPersistence.createEntityManagerFactory("TestConv", persistenceXmlResource, map); |
| } |
| emfs.put(map, emf); |
| return emf; |
| } |
| |
| protected void addProperties(Map map) { |
| if (!map.containsKey("openjpa.jdbc.SynchronizeMappings")) |
| map.put("openjpa.jdbc.SynchronizeMappings", |
| "buildSchema(ForeignKeys=true," + |
| "SchemaAction='add,deleteTableContents')"); |
| } |
| |
| /** |
| * Creates a EMF and adds it to a Map for cleanup in tearDown() |
| * |
| */ |
| protected OpenJPAEntityManagerFactory getEmf() { |
| Map m = new HashMap(); |
| return getEmf(m); |
| } |
| |
| protected BrokerFactory getBrokerFactory() { |
| return JPAFacadeHelper.toBrokerFactory(getEmf()); |
| } |
| |
| protected BrokerFactory getBrokerFactory(String[] args) { |
| if (args.length % 2 != 0) |
| throw new IllegalArgumentException( |
| "odd number of elements in arg array"); |
| Map map = new HashMap(); |
| for (int i = 0; i < args.length; i = i + 2) |
| map.put(args[i], args[i+1]); |
| return JPAFacadeHelper.toBrokerFactory(getEmf(map)); |
| } |
| |
| protected OpenJPAEntityManager currentEntityManager() { |
| if (currentEntityManager == null || !currentEntityManager.isOpen()) |
| currentEntityManager = getEmf().createEntityManager(); |
| return currentEntityManager; |
| } |
| |
| protected void startTx(EntityManager em) { |
| em.getTransaction().begin(); |
| } |
| |
| protected boolean isActiveTx(EntityManager em) { |
| return em.getTransaction().isActive(); |
| } |
| |
| protected void endTx(EntityManager em) { |
| if (em.getTransaction().isActive()) { |
| if (em.getTransaction().getRollbackOnly()) |
| em.getTransaction().rollback(); |
| else |
| em.getTransaction().commit(); |
| } |
| } |
| |
| protected void rollbackTx(EntityManager em) { |
| em.getTransaction().rollback(); |
| } |
| |
| protected void endEm(EntityManager em) { |
| if (em != null && em.isOpen()) |
| em.close(); |
| if (em == currentEntityManager) |
| currentEntityManager = null; |
| } |
| |
| protected Object getStackTrace(Throwable t) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| protected OpenJPAConfiguration getConfiguration() { |
| return getEmf().getConfiguration(); |
| } |
| |
| protected Platform getCurrentPlatform() { |
| throw new UnsupportedOperationException(); |
| } |
| |
| protected void bug(int id, String s) { |
| bug(id, null, s); |
| } |
| |
| protected void bug(Platform platform, int id, Throwable t, String s) { |
| bug(EnumSet.of(platform), id, t, s); |
| } |
| |
| protected void bug(EnumSet<Platform> platforms, int id, Throwable t, |
| String s) { |
| if (platforms.contains(getCurrentPlatform())) |
| bug(id, t, s); |
| else |
| fail(String.format( |
| "bug %s is unexpectedly occurring on platform %s", |
| id, getCurrentPlatform())); |
| } |
| |
| protected void bug(int id, Throwable t, String s) { |
| if (t != null) { |
| if (t instanceof RuntimeException) |
| throw (RuntimeException) t; |
| else |
| throw new RuntimeException(t); |
| } else { |
| fail(s); |
| } |
| } |
| |
| /** |
| * Support method to get a random Integer for testing. |
| */ |
| public static Integer randomInt() { |
| return (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 TEST_CHAR_ARRAY[ |
| (int) (Math.random() * TEST_CHAR_ARRAY.length)]; |
| } |
| |
| /** |
| * Support method to get a random Long for testing. |
| */ |
| public static Long randomLong() { |
| return (long) (Math.random() * Long.MAX_VALUE); |
| } |
| |
| /** |
| * Support method to get a random Short for testing. |
| */ |
| public static Short randomShort() { |
| return (short) (Math.random() * Short.MAX_VALUE); |
| } |
| |
| /** |
| * Support method to get a random Double for testing. |
| */ |
| public static Double randomDouble() { |
| return (double) (Math.round(Math.random() * 5000d)) / 1000d; |
| } |
| |
| /** |
| * Support method to get a random Float for testing. |
| */ |
| public static Float randomFloat() { |
| return (float) (Math.round(Math.random() * 5000f)) / 1000f; |
| } |
| |
| /** |
| * Support method to get a random Byte for testing. |
| */ |
| public static Byte randomByte() { |
| return (byte) (Math.random() * Byte.MAX_VALUE); |
| } |
| |
| /** |
| * Support method to get a random Boolean for testing. |
| */ |
| public static Boolean randomBoolean() { |
| return 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) { |
| StringBuilder buf = new StringBuilder(); |
| 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() { |
| StringBuilder sbuf = new StringBuilder(); |
| 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(); |
| |
| return blob; |
| } |
| |
| /** |
| * Invoke setters for pimitives and primitive wrappers on the |
| * specified object. |
| */ |
| public static Object randomizeBean(Object bean) |
| throws IntrospectionException, IllegalAccessException, |
| InvocationTargetException, java.beans.IntrospectionException { |
| 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; |
| } |
| |
| protected void assertSize(int size, Collection c) { |
| assertEquals(size, c.size()); |
| } |
| |
| protected void assertSize(int size, Query q) { |
| assertEquals(size, q.getResultList().size()); |
| } |
| |
| /** |
| * 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(); |
| |
| try { |
| if (validateEquality) { |
| assertEquals(orig.hashCode(), result.hashCode()); |
| assertEquals(orig, result); |
| } |
| } finally { |
| out.close(); |
| in.close(); |
| } |
| |
| 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()); |
| } |
| } |
| |
| protected Log getLog() { |
| return getConfiguration().getLog("Tests"); |
| } |
| |
| /////////////////// |
| // 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() { |
| @Override |
| 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 + "]") { |
| @Override |
| 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) |
| Thread.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; |
| } |
| |
| /** |
| * A Runnable that can throw an Exception: used to test cases. |
| */ |
| public interface VolatileRunnable { |
| |
| void run() throws Exception; |
| } |
| |
| /** |
| * Exception for errors caught during threading tests. |
| */ |
| public class ThreadingException extends RuntimeException { |
| |
| |
| private static final long serialVersionUID = 1L; |
| 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; |
| } |
| |
| @Override |
| public void printStackTrace() { |
| printStackTrace(System.out); |
| } |
| |
| @Override |
| public void printStackTrace(PrintStream out) { |
| printStackTrace(new PrintWriter(out)); |
| } |
| |
| @Override |
| 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); |
| } |
| } |
| } |
| |
| /** |
| * 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 (shortMethodName.equals(exclude)) |
| continue; |
| |
| return shortMethodName; |
| } |
| |
| throw new IllegalStateException("Could not identify calling " |
| + "method in stack trace"); |
| } |
| |
| ////////// |
| // 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(); |
| Thread.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)") { |
| @Override |
| 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(); |
| Thread.sleep(500); |
| |
| // try to kill the thread |
| try { |
| tot.stop(); |
| } catch (Throwable e) { |
| } |
| Thread.currentThread(); |
| Thread.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; |
| } |
| } |
| |
| 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 static final long serialVersionUID = 1L; |
| private final Throwable _err; |
| |
| public OperationTimedOutException(String msg, Throwable throwable) { |
| super(msg); |
| _err = throwable; |
| } |
| |
| @Override |
| public void printStackTrace() { |
| printStackTrace(System.out); |
| } |
| |
| @Override |
| public void printStackTrace(PrintStream out) { |
| printStackTrace(new PrintWriter(out)); |
| } |
| |
| @Override |
| public void printStackTrace(PrintWriter out) { |
| super.printStackTrace(out); |
| if (_err != null) { |
| out.print("Nested Throwable: "); |
| _err.printStackTrace(out); |
| } |
| } |
| } |
| |
| /////////////// |
| // 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(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(i); |
| |
| for (int i = 0; i < list.size(); i++) { |
| int rand = (int) (Math.random() * indices.size()); |
| Integer randIndex = (Integer) indices.remove(rand); |
| values4[randIndex] = list.get(randIndex); |
| } |
| |
| 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); |
| } |
| } |