diff options
Diffstat (limited to 'qpid/java/junit-toolkit/src/main/org/apache/qpid')
42 files changed, 8905 insertions, 0 deletions
diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/DefaultThreadFactory.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/DefaultThreadFactory.java new file mode 100644 index 0000000000..8fb0a6a90e --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/DefaultThreadFactory.java @@ -0,0 +1,48 @@ +/* + * + * 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.qpid.junit.concurrency; + +import java.util.concurrent.ThreadFactory; + +/** + * Implements a default thread factory. + * + * <p/><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities <th> Collaborations + * <tr><td> Create default threads with no specialization. + * </table> + * + * @author Rupert Smith + */ +public class DefaultThreadFactory implements ThreadFactory +{ + /** + * Constructs a new <tt>Thread</tt>. + * + * @param r A runnable to be executed by new thread instance. + * + * @return The constructed thread. + */ + public Thread newThread(Runnable r) + { + return new Thread(r); + } +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/PossibleDeadlockException.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/PossibleDeadlockException.java new file mode 100644 index 0000000000..3bbfc2d502 --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/PossibleDeadlockException.java @@ -0,0 +1,46 @@ +/* + * + * 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.qpid.junit.concurrency; + +/** + * PossibleDeadlockException is used to signal that two test threads being executed by a {@link ThreadTestCoordinator} + * may be in a state of deadlock because they are mutually blocking each other or one is waiting on the other and the + * other has been blocked elsewhere for longer than a specified timeout. + * + * <p/><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities <th> Collaborations + * <tr><td> Signal a possible state of deadlock between coordinated test threads. + * </table> + * + * @author Rupert Smith + */ +public class PossibleDeadlockException extends RuntimeException +{ + /** + * Create a new possible deadlock execption. + * + * @param message The exception message. + */ + public PossibleDeadlockException(String message) + { + super(message); + } +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/TestRunnable.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/TestRunnable.java new file mode 100644 index 0000000000..02e776a4ea --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/TestRunnable.java @@ -0,0 +1,239 @@ +/* + * + * 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.qpid.junit.concurrency; + +/** + * TestRunnable is an extension of java.util.Runnable that adds some features to make it easier to coordinate the + * activities of threads in such a way as to expose bugs in multi threaded code. + * + * <p/>Sometimes several threads will run in a particular order so that a bug is not revealed. Other times the ordering + * of the threads will expose a bug. Such bugs can be hard to replicate as the exact execution ordering of threads is not + * usually controlled. This class adds some methods that allow threads to synchronize other threads, either allowing them + * to run, or waiting for them to allow this thread to run. It also provides convenience methods to gather error messages + * and exceptions from threads, which will often be reported in unit testing code. + * + * <p/>Coordination between threads is handled by the {@link ThreadTestCoordinator}. It is called through the convenience + * methods {@link #allow} and {@link #waitFor}. Threads to be coordinated must be set up with the coordinator and assigned + * integer ids. It is then possible to call the coordinator with an array of thread ids requesting that those threads + * be allowed to continue, or to wait until one of them allows this thread to continue. The otherwise non-deterministic + * execution order of threads can be controlled into a carefully determined sequence using these methods in order + * to reproduce race conditions, dead locks, live locks, dirty reads, phantom reads, non repeatable reads and so on. + * + * <p/>When waiting for another thread to give a signal to continue it is sometimes the case that the other thread has + * become blocked by the code under test. For example in testing for a dirty read (for example in database code), + * thread 1 lets thread 2 perform a write but not commit it, then thread 2 lets thread 1 run and attempt to perform a + * dirty read on its uncommitted write. Transaction synchronization code being tested against the possibility of a dirty + * write may make use of snapshots in which case both threads should be able to read and write without blocking. It may + * make use of explicit keys in which case thread 2 may become blocked on its write attempt because thread 1 holds a + * read lock and it must wait until thread 1 completes its transaction before it can acquire this lock. The + * {@link #waitFor} method accepts a boolean parameter to indicate that threads being blocked (other than on the + * coordinator) can be interpreted the same as if the thread explicitly allows the thread calling waitFor to continue. + * Using this technique a dirty read test could be written that works against either the snapshot or the locking + * implementation, allowing both approaches to pass the test yet arranging for multiple threads to run against the + * implementation in such a way that a potential dirty read bug is exposed. + * + * <p/><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities <th> Collaborations + * <tr><td> Wait for another thread to allow this one to continue. + * <tr><td> Allow another thread to continue. + * <tr><td> Accumulate error messages. + * <tr><td> Record exceptions from thread run. + * <tr><td> Maintain link to thread coordinator. + * <tr><td> Explicitly mark a thread with an integer id. + * <tr><td> Maintian a flag to indicate whether or not this thread is waiting on the coordinator. + * </table> + * + * @todo The allow then waitFor operations are very often used as a pair. So create a method allowAndWait that combines + * them into a single method call. + * + * @author Rupert Smith + */ +public abstract class TestRunnable implements Runnable +{ + /** Holds a reference to the thread coordinator. */ + private ThreadTestCoordinator coordinator; + + /** Holds the explicit integer id of this thread. */ + private int id; + + /** Used to indicate that this thread is waiting on the coordinator and not elsewhere. */ + private boolean waitingOnCoordinator = false; + + /** Used to accumulate error messsages. */ + private String errorMessage = ""; + + /** Holds the Java thread object that this is running under. */ + private Thread thisThread; + + /** Used to hold any exceptions resulting from the run method. */ + private Exception runException = null; + + /** + * Implementations override this to perform coordinated thread sequencing. + * + * @throws Exception Any exception raised by the implementation will be caught by the default {@link #run()} + * implementation for later querying by the {@link #getException()} method. + */ + public abstract void runWithExceptions() throws Exception; + + /** + * Provides a default implementation of the run method that allows exceptions to be thrown and keeps a record + * of those exceptions. Defers to the {@link #runWithExceptions()} method to provide the thread body implementation + * and catches any exceptions thrown by it. + */ + public void run() + { + try + { + runWithExceptions(); + } + catch (Exception e) + { + this.runException = e; + } + } + + /** + * Attempt to consume an allow event from one of the specified threads and blocks until such an event occurrs. + * + * @param threads The set of threads that can allow this one to continue. + * @param otherWaitIsAllow If set to <tt>true</tt> if the threads being waited on are blocked other than on + * the coordinator itself then this is to be interpreted as allowing this thread to + * continue. + * + * @return If the <tt>otherWaitIsAllow</tt> flag is set, then <tt>true</tt> is returned when the thread being waited on is found + * to be blocked outside of the thread test coordinator. <tt>false</tt> under all other conditions. + */ + protected boolean waitFor(int[] threads, boolean otherWaitIsAllow) + { + return coordinator.consumeAllowEvent(threads, otherWaitIsAllow, id, this); + } + + /** + * Produces allow events on each of the specified threads. + * + * @param threads The set of threads that are to be allowed to continue. + */ + protected void allow(int[] threads) + { + coordinator.produceAllowEvents(threads, id, this); + } + + /** + * Keeps the error message for later reporting by the coordinator. + * + * @param message The error message to keep. + */ + protected void addErrorMessage(String message) + { + errorMessage += message; + } + + /** + * Sets the coordinator for this thread. + * + * @param coordinator The coordinator for this thread. + */ + void setCoordinator(ThreadTestCoordinator coordinator) + { + this.coordinator = coordinator; + } + + /** + * Reports whether or not this thread is waiting on the coordinator. + * + * @return <tt>If this thread is waiting on the coordinator. + */ + boolean isWaitingOnCoordinator() + { + return waitingOnCoordinator; + } + + /** + * Sets the value of the waiting on coordinator flag. + * + * @param waiting The value of the waiting on coordinator flag. + */ + void setWaitingOnCoordinator(boolean waiting) + { + waitingOnCoordinator = waiting; + } + + /** + * Sets up the explicit int id for this thread. + * + * @param id The integer id. + */ + void setId(int id) + { + this.id = id; + } + + /** + * Reports any accumulated error messages. + * + * @return Any accumulated error messages. + */ + String getErrorMessage() + { + return errorMessage; + } + + /** + * Reports any exception thrown by the {@link #runWithExceptions} method. + * + * @return Any exception thrown by the {@link #runWithExceptions} method. + */ + Exception getException() + { + return runException; + } + + /** + * Sets the Java thread under which this runs. + * + * @param thread The Java thread under which this runs. + */ + void setThread(Thread thread) + { + thisThread = thread; + } + + /** + * Gets the Java thread under which this runs. + * + * @return The Java thread under which this runs. + */ + Thread getThread() + { + return thisThread; + } + + /** + * Provides a string summary of this test threads status. + * + * @return Summarizes this threads status. + */ + public String toString() + { + return "id = " + id + ", waitingOnCoordinator = " + waitingOnCoordinator; + } +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/ThreadTestCoordinator.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/ThreadTestCoordinator.java new file mode 100644 index 0000000000..3cf8543656 --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/ThreadTestCoordinator.java @@ -0,0 +1,486 @@ +/* + * + * 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.qpid.junit.concurrency; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.ThreadFactory; + +/** + * ThreadTestCoordinator provides an array of binary latches that allows threads to wait for other threads or to send + * them a signal that allows them to continue running or to wait for another thread to signal them. The binary latch + * array is always a square array, allowing one latch from and to every thread. Upon accepting an allow signal from one + * sender the latches for all senders for a are cleared. This class is always used in conjunction with + * {@link TestRunnable} for writing concurrent test code that coordinates multi-threaded activity in order to reproduce + * concurrency bugs. + * + * <p/><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities <th> Collaborations + * <tr><td> Accept test threads to coordinate. + * <tr><td> Allow test threads to send 'allow to continue' signals. + * <tr><td> Allow test threads to wait on this coordinator for 'allow to continue' signals. + * <tr><td> Report error messages from test threads. + * <tr><td> Report exceptions from test threads. + * <tr><td> Provide method to wait until all test threads have completed. + * </table> + * + * @todo This code was hacked together as a bit of an experiment, because I wasn't sure if this idea would work. It has + * proved extremely usefull. Some documentation for this needs to be written to explain it better. + * + * @todo Consider how deadlock detection will be handled. If all threads are blocking on the coordinator, waiting for + * each other, they are deadlocked and there is something wrong with the test code that put them in that + * situation. If they are all blocked elsewhere, they may be deadlocked, or could just be waiting on some + * external event. A timeout should be used. Timeout is already implemented, just need to sanity check how + * this is working and document it. + * + * @todo Consider how livelock detection could be implemented? LockFree data structures might cause live locks. I + * guess a longish timeout is the only thing that can be done for that. + * + * @todo Only course grained synchronous at the method class level can be obtained. This is because test code can + * only insert synchronization points between method calls it makes. So this code will not be usefull for + * checking sequences of events within methods, unless the code under test is explicitly instrumented for it. + * It might be possible to instrument code by using labels, and then use the debugger/profiler interface to + * put breakpoints on the labels and use them as synchronization points. Not perfect, but at the unused labels + * can be left in the code, without altering its behaviour. + * + * @author Rupert Smith + */ +public class ThreadTestCoordinator +{ + /** Used for logging. */ + private static final Logger log = LoggerFactory.getLogger(ThreadTestCoordinator.class); + + /** Keeps track of the test threads by their ids. */ + private TestRunnable[] testThreads; // = new TestRunnable[2]; + + /** An explicit thread monitor for the coordinator. Threads wait on the coordinator whilst waiting for events. */ + private final Object coordinatorLock = new Object(); + + /** A set of monitors for each test thread. */ + private Object[] locks; + + /** The binary latch array, this is always a square array allowing one event from and to every thread. */ + private boolean[][] allowEvents; + + /** Keeps track of the number of threads being coordinated. */ + private int threadCount = 0; + + /** Accumulates any exceptions resulting from the threads run methods. */ + private Collection<Exception> exceptions = new ArrayList<Exception>(); + + /** + * Holds the deadlock timeout after which threads are given a runtime exception to signal that a potential + * deadlock may be happening. + */ + private long deadlockTimeout = 1000 * 1000000; + + /** Holds the factory to create test thread with. */ + private ThreadFactory threadFactory; + + /** + * Creates a new test thread coordinator. The number of threads to run must be specified here. + * + * @param numThreads The number of threads to run. + */ + public ThreadTestCoordinator(int numThreads) + { + this.threadCount = numThreads; + + // Create an array big enough to hold all the test threads. + testThreads = new TestRunnable[threadCount]; + + // Use the default thread factory, as none specified. + threadFactory = new DefaultThreadFactory(); + } + + /** + * Creates a new test thread coordinator with a specific thread factory. The number of threads to run must be + * specified here. + * + * @param numThreads The number of threads to run. + * @param threadFactory The factory to use to create the test threads. + */ + public ThreadTestCoordinator(int numThreads, ThreadFactory threadFactory) + { + this.threadCount = numThreads; + + // Create an array big enough to hold all the test threads. + testThreads = new TestRunnable[threadCount]; + + // Use the specified thread factory. + this.threadFactory = threadFactory; + } + + /** + * Adds a thread to this coordinator and assigns an id to it. The ids must be numbered sequentially from 0 and + * it is up to the caller to do this. + * + * @param runnable The test thread. + * @param id The explicit id to assign to the test thread. + */ + public void addTestThread(TestRunnable runnable, int id) + { + testThreads[id] = runnable; + runnable.setCoordinator(this); + runnable.setId(id); + } + + /** + * Starts all the coordinated threads running. + */ + public void run() + { + // Create the monitors for each thread. + locks = new Object[threadCount]; + + // Create an appropriately sized event queue to allow one event from and to each thread. + allowEvents = new boolean[threadCount][threadCount]; + + // Initialize the monitors and clear the event queues. + for (int i = 0; i < locks.length; i++) + { + locks[i] = new Object(); + + for (int j = 0; j < locks.length; j++) + { + allowEvents[i][j] = false; + } + } + + // Start all the threads running. + for (TestRunnable nextRunnable : testThreads) + { + // Create a Java thread for the test thread. + Thread newThread = threadFactory.newThread(nextRunnable); + nextRunnable.setThread(newThread); + + // Start it running. + newThread.start(); + } + } + + /** + * Waits until all the test threads have completed and returns any accumulated error messages from them. Any + * exceptions thrown by their run methods are also kept at this point. + * + * @return The accumulated error messages from all the threads concatenated together. + */ + public String joinAndRetrieveMessages() + { + // Create an empty error message. + String errorMessage = ""; + + // Join all the test threads. + for (TestRunnable r : testThreads) + { + Thread t = r.getThread(); + + try + { + t.join(); + } + catch (InterruptedException e) + { } + + // Add any accumulated error messages to the return value. + errorMessage += r.getErrorMessage(); + + // Keep any exceptions resulting from the threads run method. + Exception e = r.getException(); + + if (e != null) + { + exceptions.add(e); + } + } + + return errorMessage; + } + + /** + * Reports any accumulated exceptions from the test threads run methods. This method must be called after + * {@link #joinAndRetrieveMessages}. + * + * @return Any accumulated exceptions from the test threads run methods. This method must be called after + */ + public Collection<Exception> getExceptions() + { + return exceptions; + } + + /** + * Sets a timeout to break out of potential deadlocks. If all threads are waiting for other threads to send + * them continue events for longer than this timeout then the threads are all terminated. + * + * @param millis The minimum time to allow to pass before breaking out of any potential deadlocks. + * + * @todo This has not been implemented yet. If a potential deadlock happens then the joinAndRetrieveMessages + * method should throw a PotentialDeadlockException. + */ + public void setDeadlockTimeout(long millis) + { + deadlockTimeout = millis * 1000000; + } + + /** + * Creates a set of 'allow to continue' events on the event queues of the specified threads. + * + * @param threads The set of threads to allow to continue. + * @param callerId The explicit id of the calling test thread. + * @param caller The calling test thread. + */ + void produceAllowEvents(int[] threads, int callerId, TestRunnable caller) + { + // Generate some debugging messages. Very usefull to know how thread synchronization is progressing. + String message = "Thread " + callerId + " is allowing threads [ "; + + for (int j = 0; j < threads.length; j++) + { + message += threads[j] + ((j < (threads.length - 1)) ? ", " : ""); + } + + message += " ] to continue."; + log.debug(message); + + // For each allow event, synchronize on the threads lock then set the event flag to true. + for (int id : threads) + { + // Set the waiting on coordinator flag to true in case the coordinator tries to test this thread for + // being blocked at this time. + caller.setWaitingOnCoordinator(true); + + synchronized (locks[id]) + { + // Release the wating on coordinator flag now that this thread is running again. + caller.setWaitingOnCoordinator(false); + + // Send the allow to continue event to the receiving thread. + allowEvents[id][callerId] = true; + } + } + + // Wake up any threads waiting on the coordinator lock to recheck their event queues. + // Set the waiting on coordinator flag to true in case the coordinator tries to test this thread for + // being blocked at this time. + caller.setWaitingOnCoordinator(true); + + synchronized (coordinatorLock) + { + // Release the wating on coordinator flag now that this thread is running again. + caller.setWaitingOnCoordinator(false); + coordinatorLock.notifyAll(); + } + } + + /** + * Consumes an 'allow to continue' from one of the specified threads or waits until one is available or in some + * cases if one of the specified threads is blocked elsewhere to accept that as an 'allow to continue' event. + * + * @param threads The set of threads to accept an allow to continue event from. + * @param otherWaitIsAllow Whether or not to accept threads being blocked elsewhere as permission to continue. + * @param callerId The explicit id of the calling test thread. + * @param caller The calling test thread. + * + * @return If the <tt>otherWaitIsAllow</tt> flag is set, then <tt>true</tt> is returned when the thread being waited on is found + * to be blocked outside of the thread test coordinator. <tt>false</tt> under all other conditions. + */ + boolean consumeAllowEvent(int[] threads, boolean otherWaitIsAllow, int callerId, TestRunnable caller) + { + // Generate some debugging messages. Very usefull to know how thread synchronization is progressing. + String message = "Thread " + callerId + " is requesting threads [ "; + + // Record the time at which this method was called. Will be used for breaking out of potential deadlocks. + long startTime = System.nanoTime(); + + for (int j = 0; j < threads.length; j++) + { + message += threads[j] + ((j < (threads.length - 1)) ? ", " : ""); + } + + message += " ] to allow it to continue."; + log.debug(message); + + // Loop until an allow to continue event is received. + while (true) + { + // Look at all the allowing thread to see if one has created an event for consumption. + for (int allowerId : threads) + { + // Get the threads lock for the event to consume. + // Set the waiting on coordinator flag to true in case the coordinator tries to test this thread for + // being blocked at this time. + caller.setWaitingOnCoordinator(true); + + synchronized (locks[callerId]) + { + // Release the wating on coordinator flag now that this thread is running again. + caller.setWaitingOnCoordinator(false); + + // Check if there is an event on the queue from the allowing thread to this one. + if (allowEvents[callerId][allowerId]) + { + log.debug("Found an allow event, thread " + allowerId + ", is allowing thread " + callerId + + ", to continue."); + + // Consume all the allow events for this thread. + /*for (int i = 0; i < allowEvents[callerId].length; i++) + { + allowEvents[callerId][i] = false; + }*/ + + // Consume just the event from the allower to the consumer, leaving other pending allow events alone. + allowEvents[callerId][allowerId] = false; + + return false; + } + } + } + + // If waiting elsewhere is to be interpreted as an 'allow to continue' event, then look at the thread status + // for the threads being waited on to see if any are blocked on other resources. + if (otherWaitIsAllow) + { + log.debug("Other wait is to be interpreted as an allow event."); + + // Look at all the potential allower threads. + for (int allowerId : threads) + { + // Get the Java thread state for the allowing thread. + Thread threadToTest = testThreads[allowerId].getThread(); + Thread.State state = threadToTest.getState(); + + // Check if the thread is blocked and so a potential candidate for releasing this one. + if ((state == Thread.State.BLOCKED) || (state == Thread.State.WAITING) + || (state == Thread.State.TIMED_WAITING)) + { + log.debug("Found an allower thread, id = " + allowerId + ", that is blocked or wating."); + + // Check that the allower thread is not waiting on the coordinator lock or any of the + // individual thread locks. It must be waiting or blocked on another monitor. + TestRunnable allowingRunnable = testThreads[allowerId]; + boolean isWaitingOnCoordinator = allowingRunnable.isWaitingOnCoordinator(); + + if (!isWaitingOnCoordinator) + { + log.debug("The allower thread, id = " + allowerId + + ", is blocked or waiting other than on the coordinator."); + + // Get the threads lock for the event to consume. + caller.setWaitingOnCoordinator(true); + + synchronized (locks[callerId]) + { + caller.setWaitingOnCoordinator(false); + + // Consume all the allow events for this thread. + for (int i = 0; i < allowEvents[callerId].length; i++) + { + allowEvents[callerId][i] = false; + } + + return true; + } + } + else + { + log.debug("The waiting allower thread, " + allowerId + + ", is waiting on the coordinator so does not allow thread " + callerId + " to continue."); + } + } + } + } + + // Keep waiting until an 'allow to continue' event can be consumed. + try + { + // Set the waiting on coordinator flag to true in case the coordinator tries to test this thread for + // being blocked at this time. + caller.setWaitingOnCoordinator(true); + + synchronized (coordinatorLock) + { + // Release the wating on coordinator flag now that this thread is running again. + caller.setWaitingOnCoordinator(false); + + log.debug("Thread " + callerId + " is waiting on coordinator lock for more allow events."); + + // Set the waiting on coordinator flag to true in case the coordinator tries to test this thread for + // being blocked at this time. + caller.setWaitingOnCoordinator(true); + coordinatorLock.wait(10); + } + } + catch (InterruptedException e) + { } + + // Release the waiting on coordinator flag now that this thread is running again. + caller.setWaitingOnCoordinator(false); + + // Check if this thread has been waiting for longer than the deadlock timeout and raise a possible + // deadlock exception if so. + long waitTime = System.nanoTime() - startTime; + log.debug("Thread " + callerId + " has been waiting for " + (waitTime / 1000000) + " milliseconds."); + + if (waitTime > deadlockTimeout) + { + // Throw a possible deadlock exception. + throw new PossibleDeadlockException("Possible deadlock due to timeout with state:\n" + this); + } + + log.debug("Thread " + callerId + " has woken up, was waiting for more allow events to become available."); + } + } + + /** + * Pretty prints the state of the thread test coordinator, for debugging purposes. + * + * @return Pretty printed state of the thread test coordinator. + */ + public String toString() + { + String result = "["; + + for (int i = 0; i < allowEvents.length; i++) + { + for (int j = 0; j < allowEvents[i].length; j++) + { + result += allowEvents[i][j]; + + result += (j < (allowEvents[i].length - 1)) ? ", " : ""; + } + + result += (i < (allowEvents.length - 1)) ? ",\n " : ""; + } + + result += "]"; + + for (int i = 0; i < testThreads.length; i++) + { + result += "thread[" + i + "] = " + testThreads[i].toString(); + } + + return result; + } + +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/ThreadTestExample.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/ThreadTestExample.java new file mode 100644 index 0000000000..b9865f2e22 --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/ThreadTestExample.java @@ -0,0 +1,145 @@ +/* + * + * 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.qpid.junit.concurrency; + +import org.apache.log4j.Logger; + +/** + * An example to illustrate the use of the {@link ThreadTestCoordinator} and {@link TestRunnable}s. + * + * <p/><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities <th> Collaborations + * <tr><td> Demo multi-threaded testing. + * </table> + * + * @author Rupert Smith + */ +public class ThreadTestExample +{ + /** Used for logging. */ + private static final Logger log = Logger.getLogger(ThreadTestExample.class); + + /** Test thread 1. */ + TestRunnable testThread1 = + new TestRunnable() + { + public void runWithExceptions() throws Exception + { + log.debug("public void run(): called"); + log.info("in testThread0, block 1"); + + // Wait for t2 to allow t1 to continue. + allow(new int[] { 1 }); + waitFor(new int[] { 1 }, false); + + log.info("in testThread0, block 2"); + + // Wait for t2 to allow t1 to continue. T2 is allowed to be blocked elsewhere than giving explicit + // permission to allow t1 to continue. + allow(new int[] { 1 }); + waitFor(new int[] { 1 }, true); + + log.info("in testThread0, block 3"); + + // Release thread 2 from waiting on the shared lock. + synchronized (sharedLock) + { + sharedLock.notifyAll(); + } + + allow(new int[] { 1 }); + } + }; + + /** A shared lock between the test threads. */ + final Object sharedLock = new Object(); + + /** Test thread 2. */ + TestRunnable testThread2 = + new TestRunnable() + { + public void runWithExceptions() throws Exception + { + log.debug("public void run(): called"); + log.info("in testThread1, block 1"); + + // Wait for t1 to allow t2 to continue. + allow(new int[] { 0 }); + waitFor(new int[] { 0 }, false); + + log.info("in testThread1, block 2"); + + // Wait on another resource. T1 should accept this as permission to continue. + try + { + synchronized (sharedLock) + { + log.debug("in testThread1, waiting on shared lock."); + sharedLock.wait(); + } + } + catch (InterruptedException e) + { + // Bail-out with a runtime if this happens. + throw new RuntimeException("Interrupted whilst waiting for shared lock.", e); + } + + log.info("in testThread1, finished waiting on shared lock."); + + // allow(new int[] { 0 }); + + // Wait for t1 to allow t2 to continue. + waitFor(new int[] { 0 }, false); + + log.info("in testThread1, block 3"); + + allow(new int[] { 0 }); + } + }; + + /** + * Executes the test threads with coordination. + * + * @param args Ignored. + */ + public void main(String[] args) + { + ThreadTestCoordinator tt = new ThreadTestCoordinator(2); + + tt.addTestThread(testThread1, 0); + tt.addTestThread(testThread2, 1); + tt.setDeadlockTimeout(500); + tt.run(); + + String errorMessage = tt.joinAndRetrieveMessages(); + + // Print any error messages or exceptions. + log.info(errorMessage); + + if (!tt.getExceptions().isEmpty()) + { + for (Exception e : tt.getExceptions()) + { + log.warn("Exception thrown during test thread: ", e); + } + } + } +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/package.html b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/package.html new file mode 100644 index 0000000000..904fd0fd05 --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/package.html @@ -0,0 +1,28 @@ +<!-- + + 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. + +--> + +<html> +<body> +Contains code to assist in testing concurrency issues using coordinated threads to present code under test with +oportunities to expose concurrency bugs. Some example concurrency bugs that may be tested using these techniques are +race conditions, dead locks, live locks, dirty reads, phantom reads, non repeatable reads and so on. +</body> +</html> diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/AsymptoticTestCase.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/AsymptoticTestCase.java new file mode 100644 index 0000000000..58a7f60f3c --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/AsymptoticTestCase.java @@ -0,0 +1,303 @@ +/* + * + * 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.qpid.junit.extensions; + +import junit.framework.TestCase; + +import org.apache.log4j.Logger; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +/** + * AsymptoticTestCase is an extension of TestCase for writing unit tests to analyze asymptotic time and space behaviour. + * + * <p>ParameterizedTestCases allow tests to be defined which have test methods that take a single int argument. Normal + * JUnit test methods do not take any arguments. This int argument can be interpreted in any way by the test but it is + * intended to denote the 'size' of the test to be run. For example, when testing the performance of a data structure + * for different numbers of data elements held in the data structure the int parameter should be interpreted as the + * number of elements. Test timings for different numbers of elements can then be captured and the asymptotic behaviour + * of the data structure with respect to time analyzed. Any non-parameterized tests defined in extensions of this class + * will also be run. + * + * <p>TestCases derived from this class may also define tear down methods to clean up their memory usage. This is + * intended to be used in conjunction with memory listeners that report the amount of memory a test uses. The idea is + * to write a test that allocates memory in the main test method in such a way that it leaves that memory still + * allocated at the end of the test. The amount of memory used can then be measured before calling the tear down method + * to clean it up. In the data structure example above, a test will allocate as many elements as are requested by the + * int parameter and deallocate them in the tear down method. In this way memory readings for different numbers of + * elements can be captured and the asymptotic behaviour of the data structure with respect to space analyzed. + * + * <p><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities <th> Collaborations + * <tr><td> Store the current int parameter value. <td> {@link TKTestResult} and see {@link AsymptoticTestDecorator} too. + * <tr><td> Invoke parameterized test methods. + * </table> + * + * @todo If possible try to move the code that invokes the test and setup/teardown methods into {@link TKTestResult} or + * {@link AsymptoticTestDecorator} rather than this class. This would mean that tests don't have to extend this + * class to do time and space performance analysis, these methods could be added to any JUnit TestCase class + * instead. This would be an improvement because existing unit tests wouldn't have to extend a different class to + * work with this extension, and also tests that extend other junit extension classes could have parameterized + * and tear down methods too. + * + * @author Rupert Smith + */ +public class AsymptoticTestCase extends TestCase implements InstrumentedTest +{ + /** Used for logging. */ + private static final Logger log = Logger.getLogger(AsymptoticTestCase.class); + + /** The name of the test case. */ + private String testCaseName; + + /** Thread local for holding measurements on a per thread basis. */ + ThreadLocal<TestMeasurements> threadLocalMeasurement = + new ThreadLocal<TestMeasurements>() + { + /** + * Sets up a default set test measurements (zeroed, apart from the size param which defaults to 1). + * + * @return A set of default test measurements. + */ + protected synchronized TestMeasurements initialValue() + { + return new TestMeasurements(); + } + }; + + /** + * Constructs a test case with the given name. + * + * @param name The name of the test. + */ + public AsymptoticTestCase(String name) + { + super(name); + + log.debug("public AsymptoticTestCase(String " + name + "): called"); + testCaseName = name; + } + + /** + * Gets the current value of the integer parameter to be passed to the parameterized test. + * + * @return The current value of the integer parameter. + */ + public int getN() + { + log.debug("public int getN(): called"); + int n = threadLocalMeasurement.get().n; + + log.debug("return: " + n); + + return n; + } + + /** + * Sets the current value of the integer parameter to be passed to the parameterized test. + * + * @param n The new current value of the integer parameter. + */ + public void setN(int n) + { + log.debug("public void setN(int " + n + "): called"); + threadLocalMeasurement.get().n = n; + } + + /** + * Reports how long the test took to run. + * + * @return The time in milliseconds that the test took to run. + */ + public long getTestTime() + { + log.debug("public long getTestTime(): called"); + long startTime = threadLocalMeasurement.get().startTime; + long endTime = threadLocalMeasurement.get().endTime; + long testTime = endTime - startTime; + + log.debug("return: " + testTime); + + return testTime; + } + + /** + * Reports the memory usage at the start of the test. + * + * @return The memory usage at the start of the test. + */ + public long getTestStartMemory() + { + // log.debug("public long getTestStartMemory(): called"); + long startMem = threadLocalMeasurement.get().startMem; + + // log.debug("return: " + startMem); + + return startMem; + } + + /** + * Reports the memory usage at the end of the test. + * + * @return The memory usage at the end of the test. + */ + public long getTestEndMemory() + { + // log.debug("public long getTestEndMemory(): called"); + long endMem = threadLocalMeasurement.get().endMem; + + // log.debug("return: " + endMem); + return endMem; + } + + /** + * Resets the instrumentation values to zero, and nulls any references to held measurements so that the memory + * can be reclaimed. + */ + public void reset() + { + log.debug("public void reset(): called"); + threadLocalMeasurement.remove(); + } + + /** + * Runs the test method for this test case. + * + * @throws Throwable Any Throwables from the test methods invoked are allowed to fall through. + */ + protected void runTest() throws Throwable + { + log.debug("protected void runTest(): called"); + + // Check that a test name has been set. This is used to define which method to run. + assertNotNull(testCaseName); + log.debug("testCaseName = " + testCaseName); + + // Try to get the method with matching name. + Method runMethod = null; + boolean isParameterized = false; + + // Check if a parameterized test method is available. + try + { + // Use getMethod to get all public inherited methods. getDeclaredMethods returns all + // methods of this class but excludes the inherited ones. + runMethod = getClass().getMethod(testCaseName, int.class); + isParameterized = true; + } + catch (NoSuchMethodException e) + { + // log.debug("Parameterized method \"" + testCaseName + "\" not found."); + // Set run method to null (it already will be but...) to indicate that no parameterized method + // version could be found. + runMethod = null; + } + + // If no parameterized method is available, try and get the unparameterized method. + if (runMethod == null) + { + try + { + runMethod = getClass().getMethod(testCaseName); + isParameterized = false; + + } + catch (NoSuchMethodException e) + { + fail("Method \"" + testCaseName + "\" not found."); + } + } + + // Check that the method is publicly accessable. + if (!Modifier.isPublic(runMethod.getModifiers())) + { + fail("Method \"" + testCaseName + "\" should be public."); + } + + // Try to execute the method, passing it the current int parameter value. Allow any invocation exceptions or + // resulting exceptions from the method to fall through. + try + { + Integer paramN = getN(); + log.debug("paramN = " + paramN); + + // Calculate parameters for parameterized tests so new does not get called during memory measurement. + Object[] params = new Object[] { paramN }; + + // Take the test start memory and start time. + threadLocalMeasurement.get().startMem = 0; // SizeOf.getUsedMemory(); + + threadLocalMeasurement.get().startTime = System.nanoTime(); + + if (isParameterized) + { + runMethod.invoke(this, params); + } + else + { + runMethod.invoke(this); + } + } + catch (InvocationTargetException e) + { + e.fillInStackTrace(); + throw e.getTargetException(); + } + catch (IllegalAccessException e) + { + e.fillInStackTrace(); + throw e; + } + finally + { + // Take the test end memory and end time and calculate how long it took to run. + long endTime = System.nanoTime(); + threadLocalMeasurement.get().endTime = endTime; + log.debug("startTime = " + threadLocalMeasurement.get().startTime + ", endTime = " + endTime + ", testTime = " + + getTestTime()); + + threadLocalMeasurement.get().endMem = 0; // SizeOf.getUsedMemory(); + } + } + + /** + * The test parameters, encapsulated as a unit for attaching on a per thread basis. + */ + private static class TestMeasurements + { + /** Holds the current value of the integer parameter to run tests on. */ + public int n = 1; + + /** Holds the test start memory. */ + public long startTime = 0; + + /** Holds the test end memory. */ + public long endTime = 0; + + /** Holds the test start memory. */ + public long startMem = 0; + + /** Holds the test end memory. */ + public long endMem = 0; + } +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/AsymptoticTestDecorator.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/AsymptoticTestDecorator.java new file mode 100644 index 0000000000..8869d25be3 --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/AsymptoticTestDecorator.java @@ -0,0 +1,173 @@ +/* + * + * 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.qpid.junit.extensions; + +import junit.framework.TestResult; + +import org.apache.log4j.Logger; + +import org.apache.qpid.junit.extensions.util.MathUtils; + +/** + * A Decorator that runs a test repeatedly on an increasing int parameter, or for a fixed number of repeats. If both + * a set of integer parameters and a repeat count are specified, then each test is run for the repeat count at each + * integer parameter. + * + * <p><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities <th> Collaborations + * <tr><td> Repeat a test for each of a set of integer parameters. <td> {@link TKTestResult} + * <tr><td> Repeat a test multiple times. + * <tr><td> + * </table> + * + * @author Rupert Smith + */ +public class AsymptoticTestDecorator extends WrappedSuiteTestDecorator +{ + /** Used for logging. */ + private static final Logger log = Logger.getLogger(AsymptoticTestDecorator.class); + + /** The int size parameters to run the test with. */ + private int[] params; + + /** The number of times the whole test should be repeated. */ + private int repeat; + + /** + * Creates an asymptotic test decorator that wraps a test with repeats and a set of integer 'size' paramters + * to call the test with. + * + * @param test The test to wrap. + * @param params The integer 'size' parameters. + * @param repeat The number of times to repeat the test. + */ + public AsymptoticTestDecorator(WrappedSuiteTestDecorator test, int[] params, int repeat) + { + super(test); + + log.debug("public AsymptoticTestDecorator(Test \"" + test + "\", int[] " + + ((params == null) ? null : MathUtils.printArray(params)) + ", int " + repeat + "): called"); + + this.params = params; + this.repeat = repeat; + } + + /** + * Creates a new AsymptoticTestDecorator object. + * + * @param test The test to decorate. + * @param start The starting asymptotic integer parameter value. + * @param end The ending asymptotic integer parameter value. + * @param step The increment size to move from the start to end values by. + * @param repeat The number of times to repeat the test at each step of the cycle. + */ + public AsymptoticTestDecorator(WrappedSuiteTestDecorator test, int start, int end, int step, int repeat) + { + super(test); + + if (start < 0) + { + throw new IllegalArgumentException("Start must be >= 0"); + } + + if (end < start) + { + throw new IllegalArgumentException("End must be >= start"); + } + + if (step < 1) + { + throw new IllegalArgumentException("Step must be >= 1"); + } + + if (repeat < 1) + { + throw new IllegalArgumentException("Repeat must be >= 1"); + } + + // Generate the sequence. + params = new int[((end - start) / step) + 1]; + int i = 0; + for (int n = start; n <= end; n += step) + { + params[i++] = n; + } + + this.repeat = repeat; + } + + /** + * Runs the test repeatedly for each value of the int parameter specified and for the correct number of test + * repeats. + * + * @param result The test result object that the tests will indicate their results to. This is also used + * to pass the int parameter from this class to the decorated test class. + */ + public void run(TestResult result) + { + log.debug("public void run(TestResult result): called"); + + if (!(result instanceof TKTestResult)) + { + throw new IllegalArgumentException("AsymptoticTestDecorator only works with TKTestResult"); + } + + // Cast the test result into a TKTestResult to place the current parameter into. + TKTestResult tkResult = (TKTestResult) result; + + log.debug("params = " + ((params == null) ? null : MathUtils.printArray(params))); + log.debug("repeat = " + repeat); + + if(params != null) + { + for (int n : params) + { + for (int j = 0; j < repeat; j++) + { + log.debug("n = " + n); + + // Set the integer parameter in the TKTestResult to be passed to the tests. + tkResult.setN(n); + + if (tkResult.shouldStop()) + { + log.debug("tkResult.shouldStop = " + true); + + break; + } + + log.debug("Calling super#run"); + super.run(tkResult); + } + } + } + } + + /** + * Prints out the name of this test with the string "(parameterized)" appended onto it for debugging purposes. + * + * @return The name of this test with the string "(parameterized)" appended onto it. + */ + public String toString() + { + return super.toString() + "(parameterized)"; + } +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/BaseThrottle.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/BaseThrottle.java new file mode 100644 index 0000000000..61d5746421 --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/BaseThrottle.java @@ -0,0 +1,98 @@ +/* + * + * 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.qpid.junit.extensions; + +/** + * Provides a base implementation of the non-waiting throttle checking method, using the system nano timer. + * + * <p/><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities <th> Collaborations + * <tr><td> Check against a throttle speed without waiting. + * </table> + * + * @author Rupert Smith + */ +public abstract class BaseThrottle implements Throttle +{ + /** Holds the length of a single cycle in nano seconds. */ + protected long cycleTimeNanos; + + /** Holds the time of the last succesfull call to the check method. */ + private long lastCheckTimeNanos; + + /** Flag used to detect the first call to the {@link #checkThrottle()} method. */ + boolean firstCheckCall = true; + + /** + * Flag used to detect the first call to the {@link #throttle()} method. Zero or negative start time cannot be + * relied on to detect this as System.nanoTime can return zero or negative values. + */ + boolean firstCall = true; + + /** + * Specifies the throttling rate in operations per second. This must be called with with a value, the inverse + * of which is a measurement in nano seconds, such that the number of nano seconds do not overflow a long integer. + * The value must also be larger than zero. + * + * @param hertz The throttling rate in cycles per second. + */ + public void setRate(float hertz) + { + // Check that the argument is above zero. + if (hertz <= 0.0f) + { + throw new IllegalArgumentException("The throttle rate must be above zero."); + } + + // Calculate the cycle time. + cycleTimeNanos = (long) (1000000000f / hertz); + + // Reset the first pass flag. + firstCall = false; + firstCheckCall = false; + } + + /** + * Checks but does not enforce the throttle rate. When this method is called, it checks if a length of time greater + * than that equal to the inverse of the throttling rate has passed since it was last called and returned <tt>true</tt> + * + * @return <tt>true</tt> if a length of time greater than that equal to the inverse of the throttling rate has + * passed since this method was last called and returned <tt>true</tt>, <tt>false</tt> otherwise. The very + * first time this method is called on a throttle, it returns <tt>true</tt> as the base case to the above + * self-referential definition. + */ + public boolean checkThrottle() + { + long now = System.nanoTime(); + + if ((now > (cycleTimeNanos + lastCheckTimeNanos)) || firstCheckCall) + { + firstCheckCall = false; + lastCheckTimeNanos = now; + + return true; + } + else + { + return false; + } + } +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/BatchedThrottle.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/BatchedThrottle.java new file mode 100644 index 0000000000..241e7aa2b7 --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/BatchedThrottle.java @@ -0,0 +1,94 @@ +/* + * + * 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.qpid.junit.extensions; + +/** + * BatchedThrottle is a {@link SleepThrottle} that uses batching to achieve much higher throttling rates than a sleep + * throttle can. Sleep throttle has difficulties once the rate gets above a few hundred hertz, because the JVM cannot + * generate timed pauses that are that short. BatchedThrottle gets around this by only inserting pauses once every so + * many calls to the {@link #throttle()} method, and using a sleep throttle run at a lower rate. The rate for the sleep + * throttle is chosen so that it remains under 100hz. The final throttling rate of this throttle is equal to the batch + * size times the rate of the underlying sleep throttle. + * + * <p/>The batching calculation involves taking the log to the base 100 of the desired rate and rounding this to + * an integer. The batch size is always an exact power of 100 because of the rounding. The rate for an underlying + * sleep throttle is then chosen appropriately. + * + * <p/>In practice, the accuracy of a BacthedThrottle skews off but can sometimes even be reasonable up to ten thousand + * hertz compared with 100 Hz for a {@link SleepThrottle}. + * + * <p><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities <th> Collaborations + * <tr><td> Accept throttling rate in operations per second. + * <tr><td> Inject short pauses, occasionaly, to fill out processing cycles to a specified rate. + * <tr><td> Check against a throttle speed without waiting. + * </table> + * + * @todo Should always round the log base 100 down to the nearest integer? + * + * @author Rupert Smith + */ +public class BatchedThrottle extends BaseThrottle +{ + /** Holds the batch size. */ + int batchSize; + + /** The call count within the current batch. */ + long callCount; + + /** Holds a sleep throttle configured to run at the batched rate. */ + private Throttle batchRateThrottle = new SleepThrottle(); + + /** + * Specifies the throttling rate in operations per second. + * + * @param hertz The throttling rate in cycles per second. + */ + public void setRate(float hertz) + { + // Pass the rate unaltered down to the base implementation, for the check method. + super.setRate(hertz); + + // Log base 10 over 2 is used here to get a feel for what power of 100 the total rate is. + // As the total rate goes up the powers of 100 the batch size goes up by powers of 100 to keep the + // throttle rate in the range 1 to 100. + int x = (int) (Math.log10(hertz) / 2); + batchSize = (int) Math.pow(100, x); + float throttleRate = hertz / batchSize; + + // Reset the call count. + callCount = 0; + + // Set the sleep throttle wrapped implementation at a rate within its abilities. + batchRateThrottle.setRate(throttleRate); + } + + /** + * Throttle calls to this method to the rate specified by the {@link #setRate(float)} method. + */ + public void throttle() + { + if ((callCount++ % batchSize) == 0) + { + batchRateThrottle.throttle(); + } + } +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/DurationTestDecorator.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/DurationTestDecorator.java new file mode 100644 index 0000000000..e99fcce752 --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/DurationTestDecorator.java @@ -0,0 +1,205 @@ +/* + * + * 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.qpid.junit.extensions; + +import junit.framework.Test; +import junit.framework.TestResult; + +import org.apache.log4j.Logger; + +import java.util.Timer; +import java.util.TimerTask; + +/** + * A test decorator that runs a test repeatedly until a specified length of time has passed. + * + * <p/><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities <th> Collaborations + * <tr><td> Repeatedly run a test for a fixed length of time. + * </table> + * + * @todo The count of the number of tests run is an important number to keep. Also num passed/error/failed is also + * important to record. What to do with these numbers? They are already logged to the test listeners. + * + * @todo The duration test runner wraps on top of size, repeat or thread wrappers, need a way for it to tell + * TKTestResult when the duration is up, so that it can terminate any repeats in progress. It should end + * as soon as possible once the test method exits. + * + * @author Rupert Smith + */ +public class DurationTestDecorator extends WrappedSuiteTestDecorator implements ShutdownHookable +{ + /** Used for logging. */ + private static final Logger log = Logger.getLogger(DurationTestDecorator.class); + + /** The test to run. */ + private Test test; + + /** The length of time to run the test for. */ + private long duration; + + /** Flag set by the shutdown hook. This decorator will not start any new tests when this is set. */ + private boolean shutdown = false; + + /** + * Creates an active test with default multiplier (1). + * + * @param test The target test. + */ + public DurationTestDecorator(WrappedSuiteTestDecorator test) + { + super(test); + this.test = test; + } + + /** + * Creates active test with default multiplier (1). + * + * @param test The target test. + * @param duration The duration in milliseconds. + */ + public DurationTestDecorator(WrappedSuiteTestDecorator test, long duration) + { + super(test); + + // log.debug("public DurationTestDecorator(Test \"" + test + "\", long " + duration + "): called"); + + this.test = test; + this.duration = duration; + } + + /** + * Runs the test repeatedly for the fixed duration. + * + * @param testResult The the results object to monitor the test results with. + */ + public void run(TestResult testResult) + { + log.debug("public void run(TestResult testResult): called"); + +// Removing the durationTimer as this addition prevents this TestDecorator being wrapped with a Scaled Test Decorator. + // This change will cause the tests to run for at least the specified duration + // If we need the test to stop much closer to the specified duration then we need to + // ensure that the solution doesn't prevent this Decorator being wrapped with other Decorators. + +// // Cast the test result to expose it as a TKTestResult if the test is running under the TKTestRunner. +// TKTestResult tkTestResult = null; +// +// if (testResult instanceof TKTestResult) +// { +// tkTestResult = (TKTestResult) testResult; +// } +// +// // If running under the TKTestRunner, set up a timer to notify the test framework when the test reaches its +// // completion time. +// Timer durationTimer = null; +// +// if (tkTestResult != null) +// { +// log.debug("Creating duration timer."); +// +// durationTimer = new Timer(); +// durationTimer.schedule(new DurationTimerTask((TKTestResult) testResult), duration); +// } + + + // Work out when the test should end. + long now = System.nanoTime(); + long end = (duration * 1000000) + now; + + // Run the test until the duration times out or the shutdown flag is set. The test method may not exit until + // interrupted in some cases, in which case the timer will do the interrupting. + while ((now < end) && !shutdown) + { + test.run(testResult); + + now = System.nanoTime(); + } + +// // Clean up any timer that was used. +// if (durationTimer != null) +// { +// log.debug("Cancelling duration timer."); +// +// durationTimer.cancel(); +// } + } + + /** + * Supplies the shutdown hook. This shutdown hook does not call {@link TKTestResult#shutdownNow()} because the + * {@link ScaledTestDecorator} already takes care of that. + * + * @return The shut down hook. + */ + public Thread getShutdownHook() + { + return new Thread(new Runnable() + { + public void run() + { + // log.debug("DurationTestDecorator::ShutdownHook: called"); + + // Set the shutdown flag so that no new tests are started. + shutdown = true; + } + }); + } + +// /** +// * DurationTimerTask is a timer task that is configured, upon expiry of its timer, to invoke +// * {@link TKTestResult#shutdownNow()}, for the test result object on which it is set. It also sets +// * the {@link DurationTestDecorator#shutdown} flag to indicate that no new tests should be run. +// * +// * <p/>The test loop implemented by DurationTestDecorator checks that the duration has not expired, on each +// * test case that it runs. However, it is possible to write test cases that never return until explicitly +// * interrupted by the test framework. This timer task exists to notify the test framework +// */ +// private class DurationTimerTask extends TimerTask +// { +// /** Used for debugging purposes. */ +// private final Logger log = Logger.getLogger(DurationTimerTask.class); +// +// /** Holds the test result for the test to which a duration limit is being applied. */ +// TKTestResult testResult; +// +// /** +// * Creates a duration limit timer which will notify the specified test result when the duration has +// * expired. +// * +// * @param testResult The test result to notify upon expiry of the test duration. +// */ +// public DurationTimerTask(TKTestResult testResult) +// { +// this.testResult = testResult; +// } +// +// /** +// * The action to be performed by this timer task. +// */ +// public void run() +// { +// log.debug("public void run(): called"); +// +// shutdown = true; +// testResult.shutdownNow(); +// } +// } +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/InstrumentedTest.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/InstrumentedTest.java new file mode 100644 index 0000000000..0804757dce --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/InstrumentedTest.java @@ -0,0 +1,66 @@ +/* + * + * 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.qpid.junit.extensions; + +import junit.framework.Test; + +/** + * An InstrumentedTest is one which can supply some additional instrumentation on top of the pass/fail/error behaviour + * of normal junit tests. Tests implementing this interface must additionally supply information about how long they + * took to run and how much memory they used. + * + * <p><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities + * <tr><td> Report test run time. + * <tr><td> Report test memory usage. + * </table> + * + * @author Rupert Smith + */ +public interface InstrumentedTest extends Test +{ + /** + * Reports how long the test took to run. + * + * @return The time in milliseconds that the test took to run. + */ + public long getTestTime(); + + /** + * Reports the memory usage at the start of the test. + * + * @return The memory usage at the start of the test. + */ + public long getTestStartMemory(); + + /** + * Reports the memory usage at the end of the test. + * + * @return The memory usage at the end of the test. + */ + public long getTestEndMemory(); + + /** + * Resets the instrumentation values to zero, and nulls any references to held measurements so that the memory + * can be reclaimed. + */ + public void reset(); +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/NullResultPrinter.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/NullResultPrinter.java new file mode 100644 index 0000000000..6727f6f152 --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/NullResultPrinter.java @@ -0,0 +1,92 @@ +/* + * + * 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.qpid.junit.extensions; + +import junit.framework.AssertionFailedError; +import junit.framework.Test; + +import junit.textui.ResultPrinter; + +import java.io.PrintStream; + +/** + * A ResultPrinter that prints nothing. This exists, in order to provide a replacement to JUnit's ResultPrinter, which + * is refered to directly by JUnit code, rather that as an abstracted TestListener. JUnit's text ui TestRunner must + * have a ResultPrinter. This provides an implementation of it that prints nothing, so that a better mechanism can + * be used for providing feedback to the console instead. + * + * <p/><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities <th> Collaborations + * <tr><td> + * </table> + * + * @todo See todo in TKTestRunner about completely replacing the test ui runner. Doing things like this in order to + * extend JUnit is not nice, and there needs to be a better way to do it. Delete this class and use a listener + * instead. + * + * @author Rupert Smith + */ +public class NullResultPrinter extends ResultPrinter +{ + /** + * Builds a fake ResultPrinter that prints nothing. + * + * @param writer The writer to send output to. + */ + public NullResultPrinter(PrintStream writer) + { + super(writer); + } + + /** + * Does nothing. + * + * @param test Ignored. + * @param t Ignored. + */ + public void addError(Test test, Throwable t) + { } + + /** + * Does nothing. + * + * @param test Ignored. + * @param t Ignored. + */ + public void addFailure(Test test, AssertionFailedError t) + { } + + /** + * Does nothing. + * + * @param test Ignored. + */ + public void endTest(Test test) + { } + + /** + * Does nothing. + * + * @param test Ignored. + */ + public void startTest(Test test) + { } +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/ParameterVariationTestDecorator.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/ParameterVariationTestDecorator.java new file mode 100644 index 0000000000..7da4667928 --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/ParameterVariationTestDecorator.java @@ -0,0 +1,175 @@ +/* + * + * 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.qpid.junit.extensions; + +import junit.framework.TestResult; + +import org.apache.log4j.Logger; + +import org.apache.qpid.junit.extensions.util.MathUtils; + +/** + * ParameterVariationTestDecorator is a test decorator that runs a test repeatedly under all permutations of its + * test parameters. + * + * a set of integer parameters and a repeat count are specified, then each test is run for the repeat count at each + * integer parameter. + * + * <p><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities <th> Collaborations + * <tr><td> Repeat a test for each of a set of integer parameters. <td> {@link org.apache.qpid.junit.extensions.TKTestResult} + * <tr><td> Repeat a test multiple times. + * <tr><td> + * </table> + * + * @author Rupert Smith + */ +public class ParameterVariationTestDecorator extends WrappedSuiteTestDecorator +{ + /** Used for logging. */ + private static final Logger log = Logger.getLogger(ParameterVariationTestDecorator.class); + + /** The int size parameters to run the test with. */ + private int[] params; + + /** The number of times the whole test should be repeated. */ + private int repeat; + + /** + * Creates an asymptotic test decorator that wraps a test with repeats and a set of integer 'size' paramters + * to call the test with. + * + * @param test The test to wrap. + * @param params The integer 'size' parameters. + * @param repeat The number of times to repeat the test. + */ + public ParameterVariationTestDecorator(WrappedSuiteTestDecorator test, int[] params, int repeat) + { + super(test); + + log.debug("public AsymptoticTestDecorator(Test \"" + test + "\", int[] " + + ((params == null) ? null : MathUtils.printArray(params)) + ", int " + repeat + "): called"); + + this.params = params; + this.repeat = repeat; + } + + /** + * Creates a new AsymptoticTestDecorator object. + * + * @param test The test to decorate. + * @param start The starting asymptotic integer parameter value. + * @param end The ending asymptotic integer parameter value. + * @param step The increment size to move from the start to end values by. + * @param repeat The number of times to repeat the test at each step of the cycle. + */ + public ParameterVariationTestDecorator(WrappedSuiteTestDecorator test, int start, int end, int step, int repeat) + { + super(test); + + if (start < 0) + { + throw new IllegalArgumentException("Start must be >= 0"); + } + + if (end < start) + { + throw new IllegalArgumentException("End must be >= start"); + } + + if (step < 1) + { + throw new IllegalArgumentException("Step must be >= 1"); + } + + if (repeat < 1) + { + throw new IllegalArgumentException("Repeat must be >= 1"); + } + + // Generate the sequence. + params = new int[((end - start) / step) + 1]; + int i = 0; + for (int n = start; n <= end; n += step) + { + params[i++] = n; + } + + this.repeat = repeat; + } + + /** + * Runs the test repeatedly for each value of the int parameter specified and for the correct number of test + * repeats. + * + * @param result The test result object that the tests will indicate their results to. This is also used + * to pass the int parameter from this class to the decorated test class. + */ + public void run(TestResult result) + { + log.debug("public void run(TestResult result): called"); + + if (!(result instanceof TKTestResult)) + { + throw new IllegalArgumentException("AsymptoticTestDecorator only works with TKTestResult"); + } + + // Cast the test result into a TKTestResult to place the current parameter into. + TKTestResult tkResult = (TKTestResult) result; + + log.debug("params = " + ((params == null) ? null : MathUtils.printArray(params))); + log.debug("repeat = " + repeat); + + if(params != null) + { + for (int n : params) + { + for (int j = 0; j < repeat; j++) + { + log.debug("n = " + n); + + // Set the integer parameter in the TKTestResult to be passed to the tests. + tkResult.setN(n); + + if (tkResult.shouldStop()) + { + log.debug("tkResult.shouldStop = " + true); + + break; + } + + log.debug("Calling super#run"); + super.run(tkResult); + } + } + } + } + + /** + * Prints out the name of this test with the string "(parameterized)" appended onto it for debugging purposes. + * + * @return The name of this test with the string "(parameterized)" appended onto it. + */ + public String toString() + { + return super.toString() + "(parameterized)"; + } +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/ScaledTestDecorator.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/ScaledTestDecorator.java new file mode 100644 index 0000000000..93e2a3c855 --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/ScaledTestDecorator.java @@ -0,0 +1,391 @@ +/* + * + * 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.qpid.junit.extensions; + +import junit.framework.Test; +import junit.framework.TestResult; + +import java.util.concurrent.BrokenBarrierException; +import java.util.concurrent.CyclicBarrier; + +/** + * A test decorator that runs a test many times simultaneously in many threads. + * + * <p><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities <th> Collaborations + * <tr><td> Clone a test run into many threads and run them simultaneously. + * <tr><td> Inform the test results of the start and end of each concurrent test batch. <td> {@link TKTestResult} + * <tr><td> Inform the test results of the concurrency level. <td> {@link TKTestResult} + * </table> + * + * @author Rupert Smith + */ +public class ScaledTestDecorator extends WrappedSuiteTestDecorator implements ShutdownHookable // TestDecorator +{ + /** Used for logging. */ + // private static final Logger log = Logger.getLogger(ScaledTestDecorator.class); + + /** Determines how long to wait for tests to cleanly exit on shutdown. */ + private static final long SHUTDOWN_PAUSE = 3000; + + /** + * The stress levels or numbers of simultaneous threads to run the test in. The test is repeated at each of + * the concurrency levels specified here. Defaults to 1 thread. + */ + private int[] threads = new int[] { 1 }; + + /** Used to hold the number of tests currently being run in parallel. */ + private int concurrencyLevel; + + /** The test to run. */ + private WrappedSuiteTestDecorator test; + + /** + * Used to hold the current {@link TKTestResult} for the tests currently being run. This is made available so that + * the shutdown hook can ask it to cleanly end the current tests in the event of a shutdown. + */ + private TKTestResult currentTestResult; + + /** Flag set by the shutdown hook. This decorator will not start any new tests when this is set. */ + private boolean shutdown = false; + + /** + * Creates an active test with default multiplier (1). + * + * @param test The target test. + */ + public ScaledTestDecorator(WrappedSuiteTestDecorator test) + { + super(test); + this.test = test; + } + + /** + * Creates a concurrently scaled test with the specified number of threads. + * + * @param test The target test. + * @param numThreads The stress level. + */ + public ScaledTestDecorator(WrappedSuiteTestDecorator test, int numThreads) + { + this(test, new int[] { numThreads }); + } + + /** + * Creates a concurrently scaled test with the specified thread levels, the test is repeated at each level. + * + * @param test The target test. + * @param threads The concurrency levels. + */ + public ScaledTestDecorator(WrappedSuiteTestDecorator test, int[] threads) + { + super(test); + + /*log.debug("public ScaledTestDecorator(WrappedSuiteTestDecorator test = \"" + test + "\", int[] threads = " + + MathUtils.printArray(threads) + "): called");*/ + + this.test = test; + this.threads = threads; + } + + /** + * Runs the test simultaneously in at the specified concurrency levels. + * + * @param testResult The results object to monitor the test results with. + */ + public void run(TestResult testResult) + { + // log.debug("public void run(TestResult testResult = " + testResult + "): called"); + + // Loop through all of the specified concurrent levels for the test, provided shutdown has not been called. + for (int i = 0; (i < threads.length) && !shutdown; i++) + { + // Get the number of threads for this run. + int numThreads = threads[i]; + + // Create test thread handlers for all the threads. + TestThreadHandler[] threadHandlers = new TestThreadHandler[numThreads]; + + // Create a cyclic barrier for the test threads to synch their setups and teardowns on. + CyclicBarrier barrier = new CyclicBarrier(numThreads); + + // Set up the test thread handlers to output results to the same test results object. + for (int j = 0; j < numThreads; j++) + { + threadHandlers[j] = new TestThreadHandler(testResult, test, barrier); + } + + // Ensure the concurrency level statistic is set up correctly. + concurrencyLevel = numThreads; + + // Begin batch. + if (testResult instanceof TKTestResult) + { + TKTestResult tkResult = (TKTestResult) testResult; + // tkResult.notifyStartBatch(); + tkResult.setConcurrencyLevel(numThreads); + + // Set the test result for the currently running tests, so that the shutdown hook can call it if necessary. + currentTestResult = tkResult; + } + + // Run all the tests and wait for them all to finish. + executeAndWaitForRunnables(threadHandlers); + + // Clear the test result for the currently running tests. + currentTestResult = null; + + // End batch. + if (testResult instanceof TKTestResult) + { + TKTestResult tkResult = (TKTestResult) testResult; + tkResult.notifyEndBatch(); + } + + // Clear up all the test threads, they hold references to their associated TestResult object and Test object, + // which may prevent them from being garbage collected as the TestResult and Test objects are long lived. + for (int j = 0; j < numThreads; j++) + { + threadHandlers[j].testResult = null; + threadHandlers[j].test = null; + threadHandlers[j] = null; + } + } + } + + /** + * Reports the number of tests that the scaled decorator is currently running concurrently. + * + * @return The number of tests that the scaled decorator is currently running concurrently. + */ + public int getConcurrencyLevel() + { + return concurrencyLevel; + } + + /** + * Executes all of the specifed runnable using the thread pool and waits for them all to complete. + * + * @param runnables The set of runnables to execute concurrently. + */ + private void executeAndWaitForRunnables(Runnable[] runnables) + { + int numThreads = runnables.length; + + // Used to keep track of the test threads in order to know when they have all completed. + Thread[] threads = new Thread[numThreads]; + + // Create all the test threads. + for (int j = 0; j < numThreads; j++) + { + threads[j] = new Thread(runnables[j]); + } + + // Start all the test threads. + for (int j = 0; j < numThreads; j++) + { + threads[j].start(); + } + + // Wait for all the test threads to complete. + for (int j = 0; j < numThreads; j++) + { + try + { + threads[j].join(); + } + catch (InterruptedException e) + { + // Restore the interrupted state of the thread. + Thread.currentThread().interrupt(); + } + } + } + + /** + * Supplies the shut-down hook. + * + * @return The shut-down hook. + */ + public Thread getShutdownHook() + { + return new Thread(new Runnable() + { + public void run() + { + // log.debug("ScaledTestDecorator::ShutdownHook: called"); + + // Set the shutdown flag so that no new tests are started. + shutdown = true; + + // Check if tests are currently running, and ask them to complete as soon as possible. Allow + // a short pause for this to happen. + TKTestResult testResult = currentTestResult; + + if (testResult != null) + { + // log.debug("There is a test result currently running tests, asking it to terminate ASAP."); + testResult.shutdownNow(); + + try + { + Thread.sleep(SHUTDOWN_PAUSE); + } + catch (InterruptedException e) + { + // Restore the interrupted state of the thread. + Thread.currentThread().interrupt(); + } + } + } + }); + } + + /** + * Prints a string summarizing this test decorator, mainly for debugging purposes. + * + * @return String representation for debugging purposes. + */ + public String toString() + { + return "ScaledTestDecorator: [ test = " + test + ", concurrencyLevel = " + concurrencyLevel + " ]"; + } + + /** + * TestThreadHandler is a runnable used to execute a test in. This is static to avoid implicit 'this' reference to + * the longer lived ScaledTestDecorator class. The scaled test decorator may execute many repeats but creates fresh + * handlers for each one. It re-uses the threads in a pool but does not re-use these handlers. + */ + private static class TestThreadHandler implements Runnable + { + /** The test result object for the test to be run with. */ + TestResult testResult; + + /** The test to run. */ + WrappedSuiteTestDecorator test; + + /** Holds the cyclic barrier to synchronize on the end of the setups and before the tear downs. */ + CyclicBarrier barrier; + + /** + * Creates a new TestThreadHandler object. + * + * @param testResult The test result object for the test to be run with. + * @param test The test to run in a sperate thread. + * @param barrier The barrier implementation to use to synchronize per-thread setup completion and test + * completion before moving on through the setup, test, teardown phases. The barrier should + * be configured for the number of test threads. + */ + TestThreadHandler(TestResult testResult, WrappedSuiteTestDecorator test, CyclicBarrier barrier) + { + this.testResult = testResult; + this.test = test; + this.barrier = barrier; + } + + /** + * Runs the test associated with this pool. + */ + public void run() + { + try + { + // Call setup on all underlying tests in the suite that are thread aware. + for (Test childTest : test.getAllUnderlyingTests()) + { + // Check that the test is concurrency aware, so provides a setup method to call. + if (childTest instanceof TestThreadAware) + { + // Call the tests per thread setup. + TestThreadAware setupTest = (TestThreadAware) childTest; + setupTest.threadSetUp(); + } + } + + // Wait until all test threads have completed their setups. + barrier.await(); + + + // Call setup on all underlying tests in the suite that are thread aware. + for (Test childTest : test.getAllUnderlyingTests()) + { + // Check that the test is concurrency aware, so provides a setup method to call. + if (childTest instanceof TestThreadAware) + { + // Call the tests post thread setup. + TestThreadAware setupTest = (TestThreadAware) childTest; + setupTest.postThreadSetUp(); + } + } + + // Wait until all test threads have completed their prefill. + barrier.await(); + + // Start timing the test batch, only after thread setups have completed. + if (testResult instanceof TKTestResult) + { + ((TKTestResult) testResult).notifyStartBatch(); + } + + // Run the tests. + test.run(testResult); + + // Wait unitl all test threads have completed their tests. + barrier.await(); + + // Call tear down on all underlying tests in the suite that are thread aware. + for (Test childTest : test.getAllUnderlyingTests()) + { + // Check that the test is concurrency aware, so provides a teardown method to call. + if (childTest instanceof TestThreadAware) + { + // Call the tests per thread tear down. + TestThreadAware setupTest = (TestThreadAware) childTest; + setupTest.threadTearDown(); + } + } + } + catch (InterruptedException e) + { + // Restore the interrupted state of the thread. + Thread.currentThread().interrupt(); + } + catch (BrokenBarrierException e) + { + // Set the interrupted state on the thread. The BrokenBarrierException may be caused where one thread + // waiting for the barrier is interrupted, causing the remaining threads correctly waiting on the + // barrier to fail. This condition is expected during test interruptions, and the response to it is to + // interrupt all the other threads running in the same scaled test. + Thread.currentThread().interrupt(); + } + } + + /** + * Prints the name of the test for debugging purposes. + * + * @return The name of the test. + */ + public String toString() + { + return "ScaledTestDecorator: [test = \"" + test + "\"]"; + } + } +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/SetupTaskAware.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/SetupTaskAware.java new file mode 100644 index 0000000000..e462145d7d --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/SetupTaskAware.java @@ -0,0 +1,55 @@ +/* + * + * 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.qpid.junit.extensions; + +/** + * SetupTaskAware is an interface that tests that can accept injectable setup tasks may implement. Typically this + * is used by configurable decorator stack to inject setup tasks into tests. It is then up to the test case to run + * the tasks in the setup or threadSetup methods as it chooses. + * + * <p/>Set up tasks should be chained so that they are executed in the order that they are applied. Tear down tasks + * should be chained so that they are executed in the reverse order to which they are applied. That way the set up and + * tear down tasks act as a 'task' stack, with nested setups and tear downs. + * + * <p/><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities. + * <tr><td> Handle injection of set up tasks. + * <tr><td> Handle injection of tear down tasks. + * </table> + * + * @author Rupert Smith + */ +public interface SetupTaskAware +{ + /** + * Adds the specified task to the tests setup. + * + * @param task The task to add to the tests setup. + */ + public void chainSetupTask(Runnable task); + + /** + * Adds the specified task to the tests tear down. + * + * @param task The task to add to the tests tear down. + */ + public void chainTearDownTask(Runnable task); +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/SetupTaskHandler.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/SetupTaskHandler.java new file mode 100644 index 0000000000..b91ce41ad3 --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/SetupTaskHandler.java @@ -0,0 +1,92 @@ +/* + * + * 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.qpid.junit.extensions; + +import org.apache.qpid.junit.extensions.util.StackQueue; + +import java.util.LinkedList; +import java.util.Queue; + +/** + * SetupTaskHandler implements a task stack. It can be used, by delegation, as a base implementation for tests that want + * to have configurable setup/teardown task stacks. Typically it is up to the test implementation to decide whether the + * stack is executed in the setup/teardown methods or in the threadSetup/threadTeaddown methods. + * + * <p/><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities <th> Collaborations + * <tr><td> Handle injection of set up tasks. + * <tr><td> Handle injection of tear down tasks. + * <tr><td> Run set up tasks in chain order. + * <tr><td> Run tear down tasks in reverse chain order. + * </table> + * + * @author Rupert Smith + */ +public class SetupTaskHandler implements SetupTaskAware +{ + /** Holds the set up tasks. */ + Queue<Runnable> setups = new LinkedList<Runnable>(); + + /** Holds the tear down tasks. */ + Queue<Runnable> teardowns = new StackQueue<Runnable>(); + + /** + * Adds the specified task to the tests setup. + * + * @param task The task to add to the tests setup. + */ + public void chainSetupTask(Runnable task) + { + setups.offer(task); + } + + /** + * Adds the specified task to the tests tear down. + * + * @param task The task to add to the tests tear down. + */ + public void chainTearDownTask(Runnable task) + { + teardowns.offer(task); + } + + /** + * Runs the set up tasks in the order that they way chained. + */ + public void runSetupTasks() + { + while (!setups.isEmpty()) + { + setups.remove().run(); + } + } + + /** + * Runs the tear down tasks in the reverse of the order in which they were chained. + */ + public void runTearDownTasks() + { + while (!teardowns.isEmpty()) + { + teardowns.remove().run(); + } + } +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/ShutdownHookable.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/ShutdownHookable.java new file mode 100644 index 0000000000..dc6aa3c291 --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/ShutdownHookable.java @@ -0,0 +1,42 @@ +/* + * + * 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.qpid.junit.extensions; + +/** + * Defines an interface that classes which supply shutdown hooks implement. Code that creates these classes can check + * if they supply a shutdown hook and register these hooks when the obejct are created. + * + * <p><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities + * <tr><td> Supply a shutdown hook. + * </table> + * + * @author Rupert Smith + */ +public interface ShutdownHookable +{ + /** + * Supplies the shutdown hook. + * + * @return The shut down hook. + */ + public Thread getShutdownHook(); +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/SleepThrottle.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/SleepThrottle.java new file mode 100644 index 0000000000..2dc4c0e272 --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/SleepThrottle.java @@ -0,0 +1,81 @@ +/* + * + * 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.qpid.junit.extensions; + +/** + * SleepThrottle is a Throttle implementation that generates short pauses using the thread sleep methods. As the pauses + * get shorter, this technique gets more innacurate. In practice, around 100 Hz is the cap rate for accuracy. + * + * <p/><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities <th> Collaborations + * <tr><td> Accept throttling rate in operations per second. + * <tr><td> Inject short pauses to fill out processing cycles to a specified rate. + * <tr><td> Check against a throttle speed without waiting. + * </table> + * + * @author Rupert Smith + */ +public class SleepThrottle extends BaseThrottle implements Throttle +{ + /** Holds the time of the last call to the throttle method in nano seconds. */ + private long lastTimeNanos; + + /** + * This method can only be called at the rate set by the {@link #setRate} method, if it is called faster than this + * it will inject short pauses to restrict the call rate to that rate. + */ + public void throttle() + { + // Get the current time in nanos. + long currentTimeNanos = System.nanoTime(); + + // Don't introduce any pause on the first call. + if (!firstCall) + { + // Check if there is any time left in the cycle since the last call to this method and introduce a short pause + // to fill that time if there is. + long remainingTimeNanos = cycleTimeNanos - (currentTimeNanos - lastTimeNanos); + + if (remainingTimeNanos > 0) + { + long milliPause = remainingTimeNanos / 1000000; + int nanoPause = (int) (remainingTimeNanos % 1000000); + + try + { + Thread.sleep(milliPause, nanoPause); + } + catch (InterruptedException e) + { + // Restore the interrupted thread, in-case the caller is checking for it. + Thread.currentThread().interrupt(); + } + } + } + else + { + firstCall = false; + } + + // Update the last time stamp. + lastTimeNanos = System.nanoTime(); + } +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TKTestResult.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TKTestResult.java new file mode 100644 index 0000000000..5ce56a690e --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TKTestResult.java @@ -0,0 +1,654 @@ +/* + * + * 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.qpid.junit.extensions; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestResult; + +import org.apache.log4j.Logger; + +import org.apache.qpid.junit.extensions.listeners.TKTestListener; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Properties; + +/** + * TKTestResult extends TestResult in order to calculate test timings, to pass the variable integer parameter for + * parameterized test cases to those test cases and to introduce an optional delay before test starts. Interested + * {@link TKTestListener}s may be attached to this and will be informed of all relevant test statistics. + * + * <p><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities <th> Collaborations + * <tr><td> Calculate test timings. + * <tr><td> Inform timing listeners of timings. + * <tr><td> Inform memory listeners of memory readings. + * <tr><td> Inform parameters listeners of parameters. + * <tr><td> Pass the integer parameter to parameterized test cases. + * <tr><td> Provide verbose test information on test start and end. + * </table> + * + * @todo Move the verbose test information on test start/end into a test listener instead. It confuses the intention + * of this class. Could also move the delay into a listener but that seems less appropriate as it would be a + * side-effecting listener. Delay and timing calculation are fundamental enough to this class. + * + * @todo The need for this class to act as a place-holder for the integer parameter for parameterized test cases is + * because this behaviour has been factored out into a test decorator class, see {@link AsymptoticTestDecorator}. + * The {@link AsymptoticTestDecorator#run} method takes a TestResult as an argument and cannot easily get to the + * {@link AsymptoticTestCase} class other than through this class. The option of using this class as a place hold + * for this value was chosen. Alternatively this class could provide a method for decorators to access the + * underlying test case through and then leave the setting of this parameter to the decorator which is a more + * natural home for this behaviour. It would also provide a more general framework for decorators. + * + * @todo The memory usage may need to be moved in closer to the test method invocation so that as little code as possible + * exists between it and the test or the results may be obscured. In fact it certainly does as the teardown method + * is getting called first. Wouldn't be a bad idea to move the timing code in closer too. + * + * @todo Get rid of the delay logic. Will be replaced by throttle control. + * + * @author Rupert Smith + */ +public class TKTestResult extends TestResult +{ + /** Used for logging. */ + private static final Logger log = Logger.getLogger(TKTestResult.class); + + /** The delay between two tests. */ + private int delay = 0; + + /** + * This flag indicates that the #completeTest method of the timing controller has been called. Once this has + * been called once, the end test event for the whole test method should be ignored because tests have taken + * charge of outputing their own timings. + */ + private boolean completeTestUsed = false; + + /** + * Thread locals to hold test start time for non-instrumented tests. (Instrumented tests hold their own + * measurement data). + */ + // private Hashtable threadStartTimeMap = new Hashtable(); + private ThreadLocal<ThreadLocalSettings> threadLocals = new ThreadLocal<ThreadLocalSettings>(); + + /** Used to hold the current integer parameter to pass to parameterized tests. This defaults to 1. */ + private int n = 1; + + /** The timing listeners. */ + private Collection<TKTestListener> tkListeners; + + /** The test case name. */ + private String testCaseName; + + /** Used to hold the current concurrency level, set by the {@link ScaledTestDecorator}. */ + private int concurrencyLevel = 1; + + /** Flag used to indicate that this test result should attempt to complete its current tests as soon as possible. */ + private boolean shutdownNow = false; + + /** Holds the parametes that the test is run with. */ + private Properties testParameters; + + /** + * Creates a new TKTestResult object. + * + * @param delay A delay in milliseconds to introduce before every test start. + * @param testCaseName The name of the test case that this is the TestResult object for. + */ + public TKTestResult(int delay, String testCaseName) + { + super(); + + /*log.debug("public TKTestResult(PrintStream writer, int " + delay + ", boolean " + verbose + ", String " + + testCaseName + "): called");*/ + + // Keep all the parameters that this is created with. + this.delay = delay; + this.testCaseName = testCaseName; + } + + /** + * Callback method use to inform this test result that a test will be started. Waits for the configured delay time + * if one has been set, starts the timer, then delegates to the super class implementation. + * + * @param test The test to be started. + */ + public void startTest(Test test) + { + // log.debug("public void startTest(Test test): called"); + + // If a delay time has been specified then wait for that length of time. + if (this.delay > 0) + { + try + { + Thread.sleep(delay); + } + catch (InterruptedException e) + { + // Ignore, but restore the interrupted flag. + Thread.currentThread().interrupt(); + } + } + + // Create the thread local settings for the test. + ThreadLocalSettings threadLocalSettings = new ThreadLocalSettings(); + threadLocals.set(threadLocalSettings); + + // Record the test start time against this thread for calculating the test timing. (Consider using ThreadLocal + // instead?) + Long startTime = System.nanoTime(); + threadLocalSettings.startTime = startTime; + // log.debug("startTime = " + startTime); + + // Check if the test is timing controller aware, in which case set up a new timing controller and hold it + // in the thread local settings. + if (test instanceof TimingControllerAware) + { + TimingControllerAware controllerAware = (TimingControllerAware) test; + TimingControllerImpl controller = + new TimingControllerImpl(this, test, startTime, Thread.currentThread().getId()); + controllerAware.setTimingController(controller); + + threadLocalSettings.timingController = controller; + } + + // Delegate to the super method to notify test event listeners. + super.startTest(test); + } + + /** + * Callback method use to inform this result that a test was completed. This calculates how long the test took + * to run, then delegates to the super class implementation. + * + * @param test The test that has ended. + */ + public void endTest(Test test) + { + // log.debug("public void endTest(Test test): called"); + + long runTime = 0; + + // Recover the thread local settings. + ThreadLocalSettings threadLocalSettings = threadLocals.get(); + + // Check if the test is an instrumented test and get the timing information from the instrumentation as this + // will be more accurate. + if (test instanceof InstrumentedTest) + { + InstrumentedTest iTest = (InstrumentedTest) test; + + // Calculate the test run time. + runTime = iTest.getTestTime(); + // log.debug("runTime = " + runTime); + + // Calculate the test memory usage. + long startMem = iTest.getTestStartMemory(); + long endMem = iTest.getTestEndMemory(); + + // log.debug("startMem = " + startMem); + // log.debug("endMem = " + endMem); + + // Inform any memory listeners of the test memory. + if (tkListeners != null) + { + for (TKTestListener memoryListener : tkListeners) + { + memoryListener.memoryUsed(test, startMem, endMem, null); + } + } + } + else + { + // Calculate the test run time. + long endTime = System.nanoTime(); + Long startTime = threadLocalSettings.startTime; + runTime = endTime - startTime; + // log.debug("runTime = " + runTime); + + threadLocals.remove(); + } + + // Output end test stats. This is only done when the tests have not used the timing controller to output + // mutiple timings. + if (!completeTestUsed) + { + // Check if the test is an asymptotic test case and get its int parameter if so. + if (test instanceof AsymptoticTestCase) + { + AsymptoticTestCase pTest = (AsymptoticTestCase) test; + + // Set the parameter. + int paramValue = pTest.getN(); + + // Inform any parameter listeners of the test parameter. + if (tkListeners != null) + { + for (TKTestListener parameterListener : tkListeners) + { + parameterListener.parameterValue(test, paramValue, null); + } + } + } + + // Inform any timing listeners of the test timing and concurrency level. + if (tkListeners != null) + { + for (TKTestListener tkListener : tkListeners) + { + TKTestListener next = tkListener; + + next.timing(test, runTime, null); + next.concurrencyLevel(test, concurrencyLevel, null); + } + } + + // Call the super method to notify test event listeners of the end event. + super.endTest(test); + } + } + + /** + * Gets the integer parameter to pass to parameterized test cases. + * + * @return The value of the integer parameter. + */ + public int getN() + { + return n; + } + + /** + * Sets the integer parameter to pass to parameterized test cases. + * + * @param n The new value of the integer parameter. + */ + public void setN(int n) + { + // log.debug("public void setN(int " + n + "): called"); + + this.n = n; + } + + /** + * Adds a timing listener to pass all timing events to. + * + * @param listener The timing listener to register. + */ + public void addTKTestListener(TKTestListener listener) + { + // Create the collection to hold the timing listeners if it does not already exist. + if (tkListeners == null) + { + tkListeners = new ArrayList<TKTestListener>(); + } + + // Keep the new timing listener. + tkListeners.add(listener); + } + + /** + * Called by the test runner to notify this that a new test batch is being begun. This method forwards this + * notification to all batch listeners. + */ + public void notifyStartBatch() + { + if (tkListeners != null) + { + for (TKTestListener batchListener : tkListeners) + { + batchListener.startBatch(); + } + } + } + + /** + * Called by the test runner to notify this that the current test batch has been ended. This method forwards this + * notification to all batch listener. + */ + public void notifyEndBatch() + { + // log.debug("public void notifyEndBatch(): called"); + + if (tkListeners != null) + { + for (TKTestListener batchListener : tkListeners) + { + batchListener.endBatch(testParameters); + } + } + } + + /** + * Called by the test runner to notify this of the properties that the test is using. + * + * @param properties The tests set/read properties. + */ + public void notifyTestProperties(Properties properties) + { + // log.debug("public void notifyTestProperties(Properties properties): called"); + + this.testParameters = properties; + + /* + if (tkListeners != null) + { + for (TKTestListener batchListener : tkListeners) + { + batchListener.properties(properties); + } + } + */ + } + + /** + * Intercepts the execution of a test case to pass the variable integer parameter to a test if it is a parameterized + * test case. + * + * @param test The test to run. + */ + protected void run(final TestCase test) + { + // log.debug("protected void run(final TestCase test): called"); + + // Check if the test case is a parameterized test and set its integer parameter if so. + if (test instanceof AsymptoticTestCase) + { + AsymptoticTestCase pTest = (AsymptoticTestCase) test; + + // Set up the integer parameter. + pTest.setN(n); + } + + // Delegate to the super method to run the test. + super.run(test); + } + + /** + * Helper method that generats a String of verbose information about a test. This includes the thread name, test + * class name and test method name. + * + * @param test The test to generate the info string for. + * + * @return Returns a string with the thread name, test class name and test method name. + */ + protected String getTestInfo(Test test) + { + // log.debug("protected String getTestInfo(Test test): called"); + + return "[" + Thread.currentThread().getName() + "@" + test.getClass().getName() + "." + + ((test instanceof TestCase) ? ((TestCase) test).getName() : "") + "]"; + } + + /** + * Sets the concurrency level to pass into the test result. + * + * @param concurrencyLevel The concurrency level the tests are running out. + */ + public void setConcurrencyLevel(int concurrencyLevel) + { + this.concurrencyLevel = concurrencyLevel; + } + + /** + * Tells this test result that it should stop running tests. Once this method has been called this test result + * will not start any new tests, and any tests that use the timing controller will be passed interrupted exceptions, + * to indicate that they should end immediately. Usually the caller of this method will introduce a short wait + * to allow an opporunity for running tests to complete, before forcing the shutdown of the JVM. + */ + public void shutdownNow() + { + log.debug("public void shutdownNow(): called on " + this); + + shutdownNow = true; + } + + /** + * Prints a string summary of this class, mainly for debugging purposes. + * + * @return A string summary of this class, mainly for debugging purposes. + */ + public String toString() + { + return "TKTestResult@" + Integer.toString(hashCode(), 16) + ": [ testCaseName = " + testCaseName + ", n = " + n + + ", tkListeners = " + tkListeners + " ]"; + } + + /** + * Holds things that need to be kept on a per thread basis for each test invocation, such as the test start + * time and its timing controller. + */ + private static class ThreadLocalSettings + { + /** Holds the test start time. */ + Long startTime; + + /** Holds the test threads timing controller. */ + TimingController timingController; + } + + /** + * Provides an implementation of the {@link TimingController} interface that timing aware tests can use to call + * back to reset timers, and register additional test timings. + */ + private static class TimingControllerImpl implements TimingController + { + /** Holds an explicit reference to the test TKTestResult that created this. */ + TKTestResult testResult; + + /** Holds a reference to the test that this is the timing controller for. */ + Test test; + + /** Holds the start time for this timing controller. This gets reset to now on each completed test. */ + long startTime; + + /** + * Holds the thread id of the thread that started the test, so that this controller may be called from other + * threads but still identify itself correctly to {@link TKTestListener}s as being associated with the + * thread that called the test method. + */ + long threadId; + + /** + * Creates a timing controller on a specified TKTestResult and a test. + * + * @param testResult The TKTestResult that this controller interacts with. + * @param test The test that this is the timing controller for. + * @param startTime The test start time in nanoseconds. + * @param threadId The thread id of the thread that is calling the test method. + */ + public TimingControllerImpl(TKTestResult testResult, Test test, long startTime, long threadId) + { + this.testResult = testResult; + this.test = test; + this.startTime = startTime; + this.threadId = threadId; + } + + /** + * Gets the timing controller associated with the current test thread. Tests that use timing controller should + * always get the timing controller from this method in the same thread that called the setUp, tearDown or test + * method. The controller returned by this method may be called from any thread because it remembers the thread + * id of the original test thread. + * + * @return The timing controller associated with the current test thread. + */ + public TimingController getControllerForCurrentThread() + { + // Recover the thread local settings and extract the timing controller from them. + ThreadLocalSettings threadLocalSettings = testResult.threadLocals.get(); + + return threadLocalSettings.timingController; + } + + /** + * Not implemented yet. + * + * @return Nothing. + */ + public long suspend() + { + throw new RuntimeException("Method not implemented."); + } + + /** + * Not implemented yet. + * + * @return Nothing. + */ + public long resume() + { + throw new RuntimeException("Method not implemented."); + } + + /** + * Resets the timer start time to now. + * + * @return The new value of the start time. + */ + public long restart() + { + startTime = System.nanoTime(); + + return startTime; + } + + /** + * Register an additional pass/fail for the current test. The test result is assumed to apply to a test of + * 'size' parmeter 1. Use the {@link #completeTest(boolean, int)} method to register timings with parameters. + * + * @param testPassed Whether or not this timing is for a test pass or fail. + * + * @throws InterruptedException If the test runner decides that testing should stop it throws this exception to + * indicate to the test method that it should stop immediately. + */ + public void completeTest(boolean testPassed) throws InterruptedException + { + completeTest(testPassed, 1); + } + + /** + * Register an additional pass/fail for the current test. The test result is applies to a test of the specified + * 'size' parmeter. + * + * @param testPassed Whether or not this timing is for a test pass or fail. + * @param param The test parameter size for parameterized tests. + * + * @throws InterruptedException If the test runner decides that testing should stop it throws this exception to + * indicate to the test method that it should stop immediately. + */ + public void completeTest(boolean testPassed, int param) throws InterruptedException + { + completeTest(testPassed, param, null, null); + } + + /** + * Register an additional pass/fail for the current test. The test result is applies to a test of the specified + * 'size' parmeter and allows the caller to sepecify the timing to log. + * + * @param testPassed Whether or not this timing is for a test pass or fail. + * @param param The test parameter size for parameterized tests. + * @param timeNanos The time in nano-seconds to log the test result with. + * + * A null value for timeNanos is a request to this method that it should + * calculate the time for the given test run. + * + * @throws InterruptedException If the test runner decides that testing should stop it throws this exception to + * indicate to the test method that it should stop immediately. + */ + public void completeTest(boolean testPassed, int param, Long timeNanos) throws InterruptedException + { + completeTest(testPassed, param, timeNanos, null); + } + + /** + * Register an additional pass/fail for the current test. The test result is applies to a test of the specified + * 'size' parmeter and allows the caller to sepecify the timing to log. + * + * @param testPassed Whether or not this timing is for a test pass or fail. + * @param param The test parameter size for parameterized tests. + * @param timeNanos The time in nano seconds to log the test result with. + * + * A null value for timeNanos is a request to this method that it should + * calculate the time for the given test run. + * A null value for timeNanos2 means this test does not provide a second + * timing value so a '-' is printed in the log. + * + * + * @throws InterruptedException If the test runner decides that testing should stop it throws this exception to + * indicate to the test method that it should stop immediately. + */ + public void completeTest(boolean testPassed, int param, Long timeNanos, Long time2Nanos) throws InterruptedException + { + log.debug("public void completeTest(boolean testPassed, int param, long timeNanos): called"); + log.debug("testResult = " + testResult); + + /*log.debug("public long completeTest(boolean testPassed = " + testPassed + ", int param = " + param + + "): called");*/ + + // Calculate the test run time. + long endTime = System.nanoTime(); + long runTime = endTime - startTime; + startTime = endTime; + + // + if (timeNanos != null) + { + runTime = timeNanos; + } + + // Tell the test result that completeTest has been used, so to not register end test events for the whole + // test method. + testResult.completeTestUsed = true; + + // Inform any timing listeners of the test timings and parameters and send an end test notification using + // the thread id of the thread that started the test. + if (testResult.tkListeners != null) + { + for (TKTestListener listener : testResult.tkListeners) + { + listener.reset(test, threadId); + listener.timing(test, runTime, threadId); + listener.timing2(test, time2Nanos, threadId); + listener.parameterValue(test, param, threadId); + listener.concurrencyLevel(test, testResult.concurrencyLevel, threadId); + + if (!testPassed) + { + listener.addFailure(test, null, threadId); + } + + listener.endTest(test, threadId); + } + } + + // log.debug("testResult.shutdownNow = " + testResult.shutdownNow); + + // Check if the test runner has been asked to shutdown and raise an interuppted exception if so. + if (testResult.shutdownNow) + { + // log.debug("The shutdown flag is set."); + + throw new InterruptedException("Attempting clean shutdown by suspending current test."); + } + } + } +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TKTestRunner.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TKTestRunner.java new file mode 100644 index 0000000000..671d33feed --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TKTestRunner.java @@ -0,0 +1,694 @@ +/* + * + * 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.qpid.junit.extensions; + +import junit.framework.Test; +import junit.framework.TestResult; +import junit.framework.TestSuite; + +import org.apache.log4j.Logger; + +import org.apache.qpid.junit.extensions.listeners.CSVTestListener; +import org.apache.qpid.junit.extensions.listeners.ConsoleTestListener; +import org.apache.qpid.junit.extensions.listeners.XMLTestListener; +import org.apache.qpid.junit.extensions.util.CommandLineParser; +import org.apache.qpid.junit.extensions.util.MathUtils; +import org.apache.qpid.junit.extensions.util.ParsedProperties; +import org.apache.qpid.junit.extensions.util.TestContextProperties; + +import java.io.*; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; + +/** + * TKTestRunner extends {@link junit.textui.TestRunner} with the ability to run tests multiple times, to execute a test + * simultaneously using many threads, to put a delay between test runs and adds support for tests that take integer + * parameters that can be 'stepped' through on multiple test runs. These features can be accessed by using this class + * as an entry point and passing command line arguments to specify which features to use: + * + * <pre> + * -w ms The number of milliseconds between invocations of test cases. + * -c pattern The number of tests to run concurrently. + * -r num The number of times to repeat each test. + * -d duration The length of time to run the tests for. + * -t name The name of the test case to execute. + * -s pattern The size parameter to run tests with. + * -o dir The name of the directory to output test timings to. + * --csv Output test results in CSV format. + * --xml Output test results in XML format. + * </pre> + * + * <p/>This command line may also have trailing 'name=value' parameters added to it. All of these values are added + * to the test context properties and passed to the test, which can access them by name. + * + * <p/>The pattern arguments are of the form [lowest(: ...)(: highest)](:sample=s)(:exp), where round brackets + * enclose optional values. Using this pattern form it is possible to specify a single value, a range of values divided + * into s samples, a range of values divided into s samples but distributed exponentially, or a fixed set of samples. + * + * <p/>The duration arguments are of the form (dD)(hH)(mM)(sS), where round brackets enclose optional values. At least + * one of the optional values must be present. + * + * <p/>When specifying optional test parameters on the command line, in 'name=value' format, it is also possible to use + * the format 'name=[value1:value2:value3:...]', to specify multiple values for a parameter. All permutations of all + * parameters with multiple values will be created and tested. If the values are numerical, it is also possible to use + * the sequence generation patterns instead of fully specifying all of the values. + * + * <p/>Here are some examples: + * + * <p/><table> + * <tr><td><pre> -c [10:20:30:40:50] </pre><td> Runs the test with 10,20,...,50 threads. + * <tr><td><pre> -s [1:100]:samples=10 </pre> + * <td> Runs the test with ten different size parameters evenly spaced between 1 and 100. + * <tr><td><pre> -s [1:1000000]:samples=10:exp </pre> + * <td> Runs the test with ten different size parameters exponentially spaced between 1 and 1000000. + * <tr><td><pre> -r 10 </pre><td> Runs each test ten times. + * <tr><td><pre> -d 10H </pre><td> Runs the test repeatedly for 10 hours. + * <tr><td><pre> -d 1M, -r 10 </pre> + * <td> Runs the test repeatedly for 1 minute but only takes a timing sample every 10 test runs. + * <tr><td><pre> -r 10, -c [1:5:10:50], -s [100:1000:10000] </pre> + * <td> Runs 12 test cycles (4 concurrency samples * 3 size sample), with 10 repeats each. In total the test + * will be run 199 times (3 + 15 + 30 + 150) + * <tr><td><pre> cache=true </pre><td> Passes the 'cache' parameter with value 'true' to the test. + * <tr><td><pre> cache=[true:false] </pre><td> Runs the test with the 'cache' parameter set to 'true' and 'false'. + * <tr><td><pre> cacheSize=[1000:1000000],samples=4,exp </pre> + * <td> Runs the test with the 'cache' parameter set to a series of exponentially increasing sizes. + * </table> + * + * <p><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities <th> Collaborations + * <tr><td> Create the test configuration specified by the command line parameters. + * </table> + * + * @todo Verify that the output directory exists or can be created. + * + * @todo Verify that the specific named test case to execute exists. + * + * @todo Drop the delay parameter as it is being replaced by throttling. + * + * @todo Completely replace the test ui test runner, instead of having TKTestRunner inherit from it, its just not + * good code to extend. + * + * @author Rupert Smith + */ +public class TKTestRunner extends TestRunnerImprovedErrorHandling +{ + /** Used for debugging. */ + private static final Logger log = Logger.getLogger(TKTestRunner.class); + + /** Used for displaying information on the console. */ + // private static final Logger console = Logger.getLogger("CONSOLE." + TKTestRunner.class.getName()); + + /** Used for generating the timestamp when naming output files. */ + protected static final DateFormat TIME_STAMP_FORMAT = new SimpleDateFormat("yyyy-MM-dd-HH.mm.ss"); + + /** Number of times to rerun the test. */ + protected Integer repetitions = 1; + + /** The length of time to run the tests for. */ + protected Long duration; + + /** Number of threads running the tests. */ + protected int[] threads; + + /** Delay in ms to wait between two test cases. */ + protected int delay = 0; + + /** The parameter values to pass to parameterized tests. */ + protected int[] params; + + /** Name of the single test case to execute. */ + protected String testCaseName = null; + + /** Name of the test class. */ + protected String testClassName = null; + + /** Name of the test run. */ + protected String testRunName = null; + + /** Directory to output XML reports into, if specified. */ + protected String reportDir = null; + + /** Flag that indicates the CSV results listener should be used to output results. */ + protected boolean csvResults; + + /** Flag that indiciates the XML results listener should be used to output results. */ + protected boolean xmlResults; + + /** + * Holds the name of the class of the test currently being run. Ideally passed into the {@link #createTestResult} + * method, but as the signature is already fixed for this, the current value gets pushed here as a member variable. + */ + protected String currentTestClassName; + + /** Holds the test results object, which is reponsible for instrumenting tests/threads to record results. */ + protected TKTestResult result; + + /** Holds a list of factories for instantiating optional user specified test decorators. */ + protected List<TestDecoratorFactory> decoratorFactories; + + /** + * Constructs a TKTestRunner using System.out for all the output. + * + * @param repetitions The number of times to repeat the test, or test batch size. + * @param duration The length of time to run the tests for. -1 means no duration has been set. + * @param threads The concurrency levels to ramp up to. + * @param delay A delay in milliseconds between test runs. + * @param params The sets of 'size' parameters to pass to test. + * @param testCaseName The name of the test case to run. + * @param reportDir The directory to output the test results to. + * @param runName The name of the test run; used to name the output file. + * @param csvResults <tt>true</tt> if the CSV results listener should be attached. + * @param xmlResults <tt>true</tt> if the XML results listener should be attached. + * @param decoratorFactories List of factories for user specified decorators. + */ + public TKTestRunner(Integer repetitions, Long duration, int[] threads, int delay, int[] params, String testCaseName, + String reportDir, String runName, boolean csvResults, boolean xmlResults, + List<TestDecoratorFactory> decoratorFactories) + { + super(new NullResultPrinter(System.out)); + + log.debug("public TKTestRunner(): called"); + + // Keep all the test parameters. + this.repetitions = repetitions; + this.duration = duration; + this.threads = threads; + this.delay = delay; + this.params = params; + this.testCaseName = testCaseName; + this.reportDir = reportDir; + this.testRunName = runName; + this.csvResults = csvResults; + this.xmlResults = xmlResults; + this.decoratorFactories = decoratorFactories; + } + + /** + * The entry point for the toolkit test runner. + * + * @param args The command line arguments. + */ + public static void main(String[] args) + { + // Use the command line parser to evaluate the command line. + CommandLineParser commandLine = + new CommandLineParser( + new String[][] + { + { "w", "The number of milliseconds between invocations of test cases.", "ms", "false" }, + { "c", "The number of tests to run concurrently.", "num", "false", MathUtils.SEQUENCE_REGEXP }, + { "r", "The number of times to repeat each test.", "num", "false" }, + { "d", "The length of time to run the tests for.", "duration", "false", MathUtils.DURATION_REGEXP }, + { "f", "The maximum rate to call the tests at.", "frequency", "false", "^([1-9][0-9]*)/([1-9][0-9]*)$" }, + { "s", "The size parameter to run tests with.", "size", "false", MathUtils.SEQUENCE_REGEXP }, + { "t", "The name of the test case to execute.", "name", "false" }, + { "o", "The name of the directory to output test timings to.", "dir", "false" }, + { "n", "A name for this test run, used to name the output file.", "name", "true" }, + { + "X:decorators", "A list of additional test decorators to wrap the tests in.", + "\"class.name[:class.name]*\"", "false" + }, + { "1", "Test class.", "class", "true" }, + { "-csv", "Output test results in CSV format.", null, "false" }, + { "-xml", "Output test results in XML format.", null, "false" } + }); + + // Capture the command line arguments or display errors and correct usage and then exit. + ParsedProperties options = null; + + try + { + options = new ParsedProperties(commandLine.parseCommandLine(args)); + } + catch (IllegalArgumentException e) + { + System.out.println(commandLine.getErrors()); + System.out.println(commandLine.getUsage()); + System.exit(FAILURE_EXIT); + } + + // Extract the command line options. + Integer delay = options.getPropertyAsInteger("w"); + String threadsString = options.getProperty("c"); + Integer repetitions = options.getPropertyAsInteger("r"); + String durationString = options.getProperty("d"); + String paramsString = options.getProperty("s"); + String testCaseName = options.getProperty("t"); + String reportDir = options.getProperty("o"); + String testRunName = options.getProperty("n"); + String decorators = options.getProperty("X:decorators"); + String testClassName = options.getProperty("1"); + boolean csvResults = options.getPropertyAsBoolean("-csv"); + boolean xmlResults = options.getPropertyAsBoolean("-xml"); + + int[] threads = (threadsString == null) ? null : MathUtils.parseSequence(threadsString); + int[] params = (paramsString == null) ? null : MathUtils.parseSequence(paramsString); + Long duration = (durationString == null) ? null : MathUtils.parseDuration(durationString); + + // The test run name defaults to the test class name unless a value was specified for it. + testRunName = (testRunName == null) ? testClassName : testRunName; + + // Add all the command line options and trailing settings to test context properties. Tests may pick up + // overridden values from there, and these values will be logged in the test results, for analysis and + // to make tests repeatable. + commandLine.addTrailingPairsToProperties(TestContextProperties.getInstance()); + commandLine.addOptionsToProperties(TestContextProperties.getInstance()); + + // Create and start the test runner. + try + { + // Create a list of test decorator factories for use specified decorators to be applied. + List<TestDecoratorFactory> decoratorFactories = parseDecorators(decorators); + + TKTestRunner testRunner = + new TKTestRunner(repetitions, duration, threads, (delay == null) ? 0 : delay, params, testCaseName, + reportDir, testRunName, csvResults, xmlResults, decoratorFactories); + + TestResult testResult = testRunner.start(testClassName); + + if (!testResult.wasSuccessful()) + { + System.exit(FAILURE_EXIT); + } + } + catch (Exception e) + { + System.err.println(e.getMessage()); + e.printStackTrace(new PrintStream(System.err)); + System.exit(EXCEPTION_EXIT); + } + } + + /** + * Parses a list of test decorators, in the form "class.name[:class.name]*", and creates factories for those + * TestDecorator classes , and returns a list of the factories. This list of factories will be in the same + * order as specified in the string. The factories can be used to succesively wrap tests in layers of + * decorators, as decorators themselves implement the 'Test' interface. + * + * <p/>If the string fails to parse, or if any of the decorators specified in it are cannot be loaded, or are not + * TestDecorators, a runtime exception with a suitable error message will be thrown. The factories themselves + * throw runtimes if the constructor method calls on the decorators fail. + * + * @param decorators The decorators list to be parsed. + * + * @return A list of instantiated decorators. + */ + protected static List<TestDecoratorFactory> parseDecorators(String decorators) + { + List<TestDecoratorFactory> result = new LinkedList<TestDecoratorFactory>(); + String toParse = decorators; + + // Check that the decorators string is not null or empty, returning an empty list of decorator factories it + // it is. + if ((decorators == null) || "".equals(decorators)) + { + return result; + } + + // Strip any leading and trailing quotes from the string. + if (toParse.charAt(0) == '\"') + { + toParse = toParse.substring(1, toParse.length() - 1); + } + + if (toParse.charAt(toParse.length() - 1) == '\"') + { + toParse = toParse.substring(0, toParse.length() - 2); + } + + // Instantiate all decorators. + for (String decoratorClassName : toParse.split(":")) + { + try + { + Class decoratorClass = Class.forName(decoratorClassName); + final Constructor decoratorConstructor = decoratorClass.getConstructor(WrappedSuiteTestDecorator.class); + + // Check that the decorator is an instance of WrappedSuiteTestDecorator. + if (!WrappedSuiteTestDecorator.class.isAssignableFrom(decoratorClass)) + { + throw new RuntimeException("The decorator class " + decoratorClassName + + " is not a sub-class of WrappedSuiteTestDecorator, which it needs to be."); + } + + result.add(new TestDecoratorFactory() + { + public WrappedSuiteTestDecorator decorateTest(Test test) + { + try + { + return (WrappedSuiteTestDecorator) decoratorConstructor.newInstance(test); + } + catch (InstantiationException e) + { + throw new RuntimeException( + "The decorator class " + decoratorConstructor.getDeclaringClass().getName() + + " cannot be instantiated.", e); + } + catch (IllegalAccessException e) + { + throw new RuntimeException( + "The decorator class " + decoratorConstructor.getDeclaringClass().getName() + + " does not have a publicly accessable constructor.", e); + } + catch (InvocationTargetException e) + { + throw new RuntimeException( + "The decorator class " + decoratorConstructor.getDeclaringClass().getName() + + " cannot be invoked.", e); + } + } + }); + } + catch (ClassNotFoundException e) + { + throw new RuntimeException("The decorator class " + decoratorClassName + " could not be found.", e); + } + catch (NoSuchMethodException e) + { + throw new RuntimeException("The decorator class " + decoratorClassName + + " does not have a constructor that accepts a single 'WrappedSuiteTestDecorator' argument.", e); + } + } + + return result; + } + + /** + * TestDecoratorFactory is a factory for creating test decorators from tests. + */ + protected interface TestDecoratorFactory + { + /** + * Decorates the specified test with a new decorator. + * + * @param test The test to decorate. + * + * @return The decorated test. + */ + public WrappedSuiteTestDecorator decorateTest(Test test); + } + + /** + * Runs a test or suite of tests, using the super class implemenation. This method wraps the test to be run + * in any test decorators needed to add in the configured toolkits enhanced junit functionality. + * + * @param test The test to run. + * @param wait Undocumented. Nothing in the JUnit javadocs to say what this is for. + * + * @return The results of the test run. + */ + public TestResult doRun(Test test, boolean wait) + { + log.debug("public TestResult doRun(Test \"" + test + "\", boolean " + wait + "): called"); + + // Wrap the tests in decorators for duration, scaling, repetition, parameterization etc. + WrappedSuiteTestDecorator targetTest = decorateTests(test); + + // Delegate to the super method to run the decorated tests. + log.debug("About to call super.doRun"); + + TestResult result = super.doRun(targetTest, wait); + log.debug("super.doRun returned."); + + /*if (result instanceof TKTestResult) + { + TKTestResult tkResult = (TKTestResult) result; + + tkResult.notifyEndBatch(); + }*/ + + return result; + } + + /** + * Applies test decorators to the tests for parameterization, duration, scaling and repetition. + * + * @param test The test to decorat. + * + * @return The decorated test. + */ + protected WrappedSuiteTestDecorator decorateTests(Test test) + { + log.debug("params = " + ((params == null) ? null : MathUtils.printArray(params))); + log.debug("repetitions = " + repetitions); + log.debug("threads = " + ((threads == null) ? null : MathUtils.printArray(threads))); + log.debug("duration = " + duration); + + // Wrap all tests in the test suite with WrappedSuiteTestDecorators. This is quite ugly and a bit baffling, + // but the reason it is done is because the JUnit implementation of TestDecorator has some bugs in it. + WrappedSuiteTestDecorator targetTest = null; + + if (test instanceof TestSuite) + { + log.debug("targetTest is a TestSuite"); + + TestSuite suite = (TestSuite) test; + + int numTests = suite.countTestCases(); + log.debug("There are " + numTests + " in the suite."); + + for (int i = 0; i < numTests; i++) + { + Test nextTest = suite.testAt(i); + log.debug("suite.testAt(" + i + ") = " + nextTest); + + if (nextTest instanceof TimingControllerAware) + { + log.debug("nextTest is TimingControllerAware"); + } + + if (nextTest instanceof TestThreadAware) + { + log.debug("nextTest is TestThreadAware"); + } + } + + targetTest = new WrappedSuiteTestDecorator(suite); + log.debug("Wrapped with a WrappedSuiteTestDecorator."); + } + // If the test has already been wrapped, no need to do it again. + else if (test instanceof WrappedSuiteTestDecorator) + { + targetTest = (WrappedSuiteTestDecorator) test; + } + + // If size parameter values have been set, then wrap the test in an asymptotic test decorator. + if (params != null) + { + targetTest = new AsymptoticTestDecorator(targetTest, params, (repetitions == null) ? 1 : repetitions); + log.debug("Wrapped with asymptotic test decorator."); + log.debug("targetTest = " + targetTest); + } + + // If no size parameters are set but the repitions parameter is, then wrap the test in an asymptotic test decorator. + else if ((repetitions != null) && (repetitions > 1)) + { + targetTest = new AsymptoticTestDecorator(targetTest, new int[] { 1 }, repetitions); + log.debug("Wrapped with asymptotic test decorator."); + log.debug("targetTest = " + targetTest); + } + + // Apply any optional user specified decorators. + targetTest = applyOptionalUserDecorators(targetTest); + + // If a test run duration has been set then wrap the test in a duration test decorator. This will wrap on + // top of size, repeat or concurrency wrappings already applied. + if (duration != null) + { + DurationTestDecorator durationTest = new DurationTestDecorator(targetTest, duration); + targetTest = durationTest; + + log.debug("Wrapped with duration test decorator."); + log.debug("targetTest = " + targetTest); + + registerShutdownHook(durationTest); + } + + // ParameterVariationTestDecorator... + + // If a test thread concurrency level is set then wrap the test in a scaled test decorator. This will wrap on + // top of size scaling or repetition wrappings. + ScaledTestDecorator scaledDecorator; + + if ((threads != null) && ((threads.length > 1) || (MathUtils.maxInArray(threads) > 1))) + { + scaledDecorator = new ScaledTestDecorator(targetTest, threads); + targetTest = scaledDecorator; + log.debug("Wrapped with scaled test decorator."); + log.debug("targetTest = " + targetTest); + } + else + { + scaledDecorator = new ScaledTestDecorator(targetTest, new int[] { 1 }); + targetTest = scaledDecorator; + log.debug("Wrapped with scaled test decorator with default of 1 thread."); + log.debug("targetTest = " + targetTest); + } + + // Register the scaled test decorators shutdown hook. + registerShutdownHook(scaledDecorator); + + return targetTest; + } + + /** + * If there were any user specified test decorators on the command line, this method instantiates them and wraps + * the test in them, from inner-most to outer-most in the order in which the decorators were supplied on the + * command line. + * + * @param targetTest The test to wrap. + * + * @return A wrapped test. + */ + protected WrappedSuiteTestDecorator applyOptionalUserDecorators(WrappedSuiteTestDecorator targetTest) + { + // If there are user defined test decorators apply them in order now. + for (TestDecoratorFactory factory : decoratorFactories) + { + targetTest = factory.decorateTest(targetTest); + } + + return targetTest; + } + + /** + * Creates the TestResult object to be used for test runs. See {@link TKTestResult} for more information and the + * enhanced test result class that this uses. + * + * @return An instance of the enhanced test result object, {@link TKTestResult}. + */ + protected TestResult createTestResult() + { + log.debug("protected TestResult createTestResult(): called"); + + TKTestResult result = new TKTestResult(delay, testCaseName); + + // Check if a directory to output reports to has been specified and attach test listeners if so. + if (reportDir != null) + { + // Create the report directory if it does not already exist. + File reportDirFile = new File(reportDir); + + if (!reportDirFile.exists()) + { + reportDirFile.mkdir(); + } + + // Create the results file (make the name of this configurable as a command line parameter). + Writer timingsWriter; + + // Always set up a console feedback listener. + ConsoleTestListener feedbackListener = new ConsoleTestListener(); + result.addListener(feedbackListener); + result.addTKTestListener(feedbackListener); + + // Set up an XML results listener to output the timings to the results file, if requested on the command line. + if (xmlResults) + { + try + { + File timingsFile = new File(reportDirFile, "TEST-" + currentTestClassName + ".xml"); + timingsWriter = new BufferedWriter(new FileWriter(timingsFile), 20000); + } + catch (IOException e) + { + throw new RuntimeException("Unable to create the log file to write test results to: " + e, e); + } + + XMLTestListener listener = new XMLTestListener(timingsWriter, currentTestClassName); + result.addListener(listener); + result.addTKTestListener(listener); + + registerShutdownHook(listener); + } + + // Set up an CSV results listener to output the timings to the results file, if requested on the command line. + if (csvResults) + { + try + { + File timingsFile = + new File(reportDirFile, testRunName + "-" + TIME_STAMP_FORMAT.format(new Date()) + "-timings.csv"); + timingsWriter = new BufferedWriter(new FileWriter(timingsFile), 20000); + } + catch (IOException e) + { + throw new RuntimeException("Unable to create the log file to write test results to: " + e, e); + } + + CSVTestListener listener = new CSVTestListener(timingsWriter); + result.addListener(listener); + result.addTKTestListener(listener); + + // Register the results listeners shutdown hook to flush its data if the test framework is shutdown + // prematurely. + registerShutdownHook(listener); + } + + // Register the results listeners shutdown hook to flush its data if the test framework is shutdown + // prematurely. + // registerShutdownHook(listener); + + // Record the start time of the batch. + // result.notifyStartBatch(); + + // At this point in time the test class has been instantiated, giving it an opportunity to read its parameters. + // Inform any test listers of the test properties. + result.notifyTestProperties(TestContextProperties.getAccessedProps()); + } + + return result; + } + + /** + * Registers the shutdown hook of a {@link ShutdownHookable}. + * + * @param hookable The hookable to register. + */ + protected void registerShutdownHook(ShutdownHookable hookable) + { + Runtime.getRuntime().addShutdownHook(hookable.getShutdownHook()); + } + + /** + * Initializes the test runner with the provided command line arguments and and starts the test run. + * + * @param testClassName The fully qualified name of the test class to run. + * + * @return The test results. + * + * @throws Exception Any exceptions from running the tests are allowed to fall through. + */ + protected TestResult start(String testClassName) throws Exception + { + // Record the current test class, so that the test results can be output to a file incorporating this name. + this.currentTestClassName = testClassName; + + // Delegate to the super method to run the tests. + return super.start(new String[] { testClassName }); + } +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TestRunnerImprovedErrorHandling.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TestRunnerImprovedErrorHandling.java new file mode 100644 index 0000000000..c68405bd06 --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TestRunnerImprovedErrorHandling.java @@ -0,0 +1,131 @@ +/* + * + * 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.qpid.junit.extensions; + +import junit.framework.Test; +import junit.framework.TestResult; + +import junit.runner.Version; + +import junit.textui.ResultPrinter; +import junit.textui.TestRunner; + +import org.apache.log4j.Logger; + +import java.io.PrintStream; + +/** + * The {@link junit.textui.TestRunner} does not provide very good error handling. It does not wrap exceptions and + * does not print out stack traces, losing valuable error tracing information. This class overrides methods in it + * in order to improve their error handling. The {@link TKTestRunner} is then built on top of this. + * + * <p/><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities <th> Collaborations + * </table> + * + * @author Rupert Smith + */ +public class TestRunnerImprovedErrorHandling extends TestRunner +{ + /** Used for logging. */ + Logger log = Logger.getLogger(TestRunnerImprovedErrorHandling.class); + + /** + * Delegates to the super constructor. + */ + public TestRunnerImprovedErrorHandling() + { + super(); + } + + /** + * Delegates to the super constructor. + * + * @param printStream The location to write test results to. + */ + public TestRunnerImprovedErrorHandling(PrintStream printStream) + { + super(printStream); + } + + /** + * Delegates to the super constructor. + * + * @param resultPrinter The location to write test results to. + */ + public TestRunnerImprovedErrorHandling(ResultPrinter resultPrinter) + { + super(resultPrinter); + } + + /** + * Starts a test run. Analyzes the command line arguments + * and runs the given test suite. + * + * @param args The command line arguments. + * + * @return The test results. + * + * @throws Exception Any exceptions falling through the tests are wrapped in Exception and rethrown. + */ + public TestResult start(String[] args) throws Exception + { + String testCase = ""; + boolean wait = false; + + for (int i = 0; i < args.length; i++) + { + if (args[i].equals("-wait")) + { + wait = true; + } + else if (args[i].equals("-c")) + { + testCase = extractClassName(args[++i]); + } + else if (args[i].equals("-v")) + { + System.err.println("JUnit " + Version.id() + " by Kent Beck and Erich Gamma"); + } + else + { + testCase = args[i]; + } + } + + if (testCase.equals("")) + { + throw new Exception("Usage: TestRunner [-wait] testCaseName, where name is the name of the TestCase class"); + } + + try + { + Test suite = getTest(testCase); + + return doRun(suite, wait); + } + catch (Exception e) + { + log.warn("Got exception whilst creating and running test suite.", e); + throw new Exception("Could not create and run the test suite.", e); + } + } +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TestThreadAware.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TestThreadAware.java new file mode 100644 index 0000000000..94dcf5499b --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TestThreadAware.java @@ -0,0 +1,54 @@ +/* + * + * 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.qpid.junit.extensions; + +/** + * This interface can be implemented by tests that want to know if they are being run concurrently. It provides + * lifecycle notification events to tell the test implementation when test threads are being created and destroyed. + * This can assist tests in creating and destroying resources that exist over the life of a test thread. A single + * test thread can excute the same test many times, and often it is convenient to keep resources, for example network + * connections, open over many test calls. + * + * <p/><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities + * <tr><td> Set up per thread test fixtures. + * <tr><td> Clean up per thread test fixtures. + * </table> + * + * @author Rupert Smith + */ +public interface TestThreadAware +{ + /** + * Called when a test thread is created. + */ + public void threadSetUp(); + + /** + * Called after all threads have completed their setup. + */ + public void postThreadSetUp(); + + /** + * Called when a test thread is destroyed. + */ + public void threadTearDown(); +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/Throttle.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/Throttle.java new file mode 100644 index 0000000000..1ea8e8e2be --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/Throttle.java @@ -0,0 +1,73 @@ +/* + * + * 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.qpid.junit.extensions; + +/** + * Throttle is an interface that supplies a {@link #throttle} method, that can only be called at the rate specified + * in a call to the {@link #setRate} method. This can be used to restict processing to run at a certain number + * of operations per second. + * + * <p/>Throttle also supplies a method to check the throttle rate, without waiting. This could be used to update a user + * interface every time an event occurs, but only up to a maximum rate. For example, as elements are added to a list, + * a count of elements is updated for the user to see, but only up to a maximum rate of ten updates a second, as updating + * faster than that slows the processing of element-by-element additions to the list unnecessarily. + * + * <p/><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities + * <tr><td> Accept throttling rate in operations per second. + * <tr><td> Inject short pauses to fill-out processing cycles to a specified rate. + * <tr><td> Check against a throttle speed without waiting. + * </table> + * + * @author Rupert Smith + */ +public interface Throttle +{ + /** + * Specifies the throttling rate in operations per second. This must be called with with a value, the inverse + * of which is a measurement in nano seconds, such that the number of nano seconds do not overflow a long integer. + * The value must also be larger than zero. + * + * @param hertz The throttling rate in cycles per second. + */ + public void setRate(float hertz); + + /** + * This method can only be called at the rate set by the {@link #setRate} method, if it is called faster than this + * it will inject short pauses to restrict the call rate to that rate. + * + * <p/>If the thread executing this method is interrupted, it must ensure that the threads interrupt thread + * remains set upon exit from the method. This method does not expose InterruptedException, to indicate interruption + * of the throttle during a timed wait. It may be changed so that it does. + */ + public void throttle(); + + /** + * Checks but does not enforce the throttle rate. When this method is called, it checks if a length of time greater + * than that equal to the inverse of the throttling rate has passed since it was last called and returned <tt>true</tt> + * + * @return <tt>true</tt> if a length of time greater than that equal to the inverse of the throttling rate has + * passed since this method was last called and returned <tt>true</tt>, <tt>false</tt> otherwise. The very + * first time this method is called on a throttle, it returns <tt>true</tt> as the base case to the above + * self-referential definition. + */ + public boolean checkThrottle(); +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TimingController.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TimingController.java new file mode 100644 index 0000000000..27e43a10a4 --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TimingController.java @@ -0,0 +1,198 @@ +/* + * + * 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.qpid.junit.extensions; + +/** + * A TimingController is a interface that a test that is aware of the fact that it is being timed can use to manage + * the timer. Using this interface tests can suspend and resume test timers. This is usefull if you want to exclude + * some expensive preparation from being timed as part of a test. In general when timing tests to measure the + * performance of code, you should try to set up data in the #setUp where possible, or as static members in the test + * class. This is not always convenient, this interface gives you a way to suspend and resume, or event completely + * restart test timers, to get accurate measurements. + * + * <p/>The interface can also be used to register multiple test pass/fails and timings from a single test method. + * In some cases it is easier to write tests in this way. For example a concurrent and asynchronous test may make + * many asynchronous requests and then wait for replies to all its requests. Writing such a test with one send/reply + * per test method and trying to scale up using many threads will quickly run into limitations if more than about + * 100 asynchronous calls need to be made at once. A better way to write such a test is as a single method that sends + * many (perhaps thousands or millions) and waits for replies in two threads, one for send, one for replies. It can + * then log pass/fails and timings on each individual reply as they come back in, even though the test has been written + * to send thousands of requests per test method in order to do volume testing. + * + * <p/>If when the {@link #completeTest(boolean)} is called, the test runner decides that testing should stop (perhaps + * because a duration test has expired), it throws an InterruptedException to indicate that the test method should stop + * immediately. The test method can do this by allowing this exception to fall through, if no other clean-up handling + * is necessary, or it can simply return as soon as it possibly can. The test runner will still call the tearDown + * method in the usual way when this happens. + * + * <p/>Below are some examples of how this can be used. Not how checking that the timing controller is really available + * rather than assuming it is, means that the test can run as an ordinary JUnit test under the default test runners. In + * general code should be written to take advantage of the extended capabilities of junit toolkit, without assuming they + * are going to be run under its test runner. + * + * <pre> + * public class MyTest extends TestCase implements TimingControllerAware { + * ... + * + * timingUtils = this.getTimingController(); + * + * // Do expensive data preparation here... + * + * if (timingUtils != null) + * timingUtils.restart(); + * </pre> + * + * <pre> + * public class MyTest extends TestCase implements TimingControllerAware { + * ... + * + * public void myVolumeTest(int size) { + * + * timingUtils = this.getTimingController(); + * + * boolean stopNow = false; + * + * // In Sender thread. + * for(int i = 0; !stopNow && i < size; i++) + * // Send request i. + * ... + * + * // In Receiver thread. + * onReceive(Object o) { + * try { + * // Check o is as expected. + * if (....) + * { + * if (timingUtils != null) + * timingUtils.completeTest(true); + * } + * else + * { + * if (timingUtils != null) + * timingUtils.completeTest(false); + * } + * } catch (InterruptedException e) { + * stopNow = true; + * return; + * } + * } + * </pre> + * + * <p><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities + * <tr><td> Allow test timers to be suspended, restarted or reset. + * <tr><td> Allow tests to register multiple pass/fails and timings. + * </table> + * + * @author Rupert Smith + */ +public interface TimingController +{ + /** + * Gets the timing controller associated with the current test thread. Tests that use timing controller should + * always get the timing controller from this method in the same thread that called the setUp, tearDown or test + * method. The controller returned by this method may be called from any thread because it remembers the thread + * id of the original test thread. + * + * @return The timing controller associated with the current test thread. + */ + public TimingController getControllerForCurrentThread(); + + /** + * Suspends the test timer. + * + * @return The current time in nanoseconds. + */ + public long suspend(); + + /** + * Allows the test timer to continue running after a suspend. + * + * @return The current time in nanoseconds. + */ + public long resume(); + + /** + * Completely restarts the test timer from zero. + * + * @return The current time in nanoseconds. + */ + public long restart(); + + /** + * Register an additional pass/fail for the current test. The test result is assumed to apply to a test of + * 'size' parmeter 1. Use the {@link #completeTest(boolean, int)} method to register timings with parameters. + * + * @param testPassed Whether or not this timing is for a test pass or fail. + * + * @throws InterruptedException If the test runner decides that testing should stop it throws this exception to + * indicate to the test method that it should stop immediately. + */ + public void completeTest(boolean testPassed) throws InterruptedException; + + /** + * Register an additional pass/fail for the current test. The test result is applies to a test of the specified + * 'size' parmeter. + * + * @param testPassed Whether or not this timing is for a test pass or fail. + * @param param The test parameter size for parameterized tests. + * + * @throws InterruptedException If the test runner decides that testing should stop it throws this exception to + * indicate to the test method that it should stop immediately. + */ + public void completeTest(boolean testPassed, int param) throws InterruptedException; + + /** + * Register an additional pass/fail for the current test. The test result is applies to a test of the specified + * 'size' parmeter and allows the caller to sepecify the timing to log. + * + * @param testPassed Whether or not this timing is for a test pass or fail. + * @param param The test parameter size for parameterized tests. + * @param timeNanos The time in nano seconds to log the test result with. + * + * A null value for timeNanos is a request to this method that it should + * calculate the time for the given test run. + * + * + * @throws InterruptedException If the test runner decides that testing should stop it throws this exception to + * indicate to the test method that it should stop immediately. + */ + public void completeTest(boolean testPassed, int param, Long timeNanos) throws InterruptedException; + + /** + * Register an additional pass/fail for the current test. The test result is applies to a test of the specified + * 'size' parmeter and allows the caller to sepecify the timing to log. + * + * @param testPassed Whether or not this timing is for a test pass or fail. + * @param param The test parameter size for parameterized tests. + * @param timeNanos The time in nano seconds to log the test result with. + * + * A null value for timeNanos is a request to this method that it should + * calculate the time for the given test run. + * A null value for timeNanos2 means this test does not provide a second + * timing value so a '-' is printed in the log. + * + * + * @throws InterruptedException If the test runner decides that testing should stop it throws this exception to + * indicate to the test method that it should stop immediately. + */ + public void completeTest(boolean testPassed, int param, Long timeNanos, Long time2Nanos) throws InterruptedException; +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TimingControllerAware.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TimingControllerAware.java new file mode 100644 index 0000000000..11db87e073 --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TimingControllerAware.java @@ -0,0 +1,43 @@ +/* + * + * 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.qpid.junit.extensions; + +/** + * TimingControllerAware is an interface that tests that manipulate the timing controller should implement. It enables + * the TK test runner to set the test up with a handle on the timing controller which the test can use to call back + * to the test runner to manage the timers. + * + * <p><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities + * <tr><td> Provide timing controller insertion point for tests. + * </table> + * + * @author Rupert Smith + */ +public interface TimingControllerAware +{ + /** + * Used by test runners that can supply a {@link TimingController} to set the controller on an aware test. + * + * @param controller The timing controller. + */ + public void setTimingController(TimingController controller); +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/WrappedSuiteTestDecorator.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/WrappedSuiteTestDecorator.java new file mode 100644 index 0000000000..d5690fc24a --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/WrappedSuiteTestDecorator.java @@ -0,0 +1,134 @@ +/* + * + * 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.qpid.junit.extensions; + +import junit.extensions.TestDecorator; + +import junit.framework.Test; +import junit.framework.TestSuite; + +import org.apache.log4j.Logger; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * WrappedSuiteTestDecorator is a test decorator that wraps a test suite, or another wrapped suite, but provides the + * same functionality for the {@link junit.extensions.TestDecorator#countTestCases()} and {@link TestSuite#testAt(int)} + * methods as the underlying suite. It returns the values that these methods provide, to enable classes using decorated + * tests to drill down to the underlying tests in the suite. That is to say that it indexes and reports the number of + * distinct tests in the suite, not the number of test runs that would result from, for example, wrapping the suite in a + * repeating decorator. + * + * <p><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities + * <tr><td> Provide access to the underlying tests in a suite. + * </table> + * + * @author Rupert Smith + */ +public class WrappedSuiteTestDecorator extends TestDecorator +{ + /** Used for logging. */ + private static Logger log = Logger.getLogger(WrappedSuiteTestDecorator.class); + + /** Holds the test suite that this supplies access to. */ + protected Test suite; + + /** + * Creates a wrappred suite test decorator from a test suite. + * + * @param suite The test suite. + */ + public WrappedSuiteTestDecorator(TestSuite suite) + { + super(suite); + this.suite = suite; + } + + /** + * Creates a wrapped suite test decorator from another one. + * + * @param suite The test suite. + */ + public WrappedSuiteTestDecorator(WrappedSuiteTestDecorator suite) + { + super(suite); + this.suite = suite; + } + + /** + * Returns the test count of the wrapped suite. + * + * @return The test count of the wrapped suite. + */ + public int countTestCases() + { + return suite.countTestCases(); + } + + /** + * Gets the ith test from the test suite. + * + * @param i The index of the test within the suite to get. + * + * @return The test with the specified index. + */ + public Test testAt(int i) + { + log.debug("public Test testAt(int i = " + i + "): called"); + + if (suite instanceof WrappedSuiteTestDecorator) + { + return ((WrappedSuiteTestDecorator) suite).testAt(i); + } + else if (suite instanceof TestSuite) + { + return ((TestSuite) suite).testAt(i); + } + + // This should never happen. + return null; + } + + /** + * Gets all the tests from the underlying test suite. + * + * @return All the tests from the underlying test suite. + */ + public Collection<Test> getAllUnderlyingTests() + { + log.debug("public Collection<Test> getAllUnderlyingTests(): called"); + + List<Test> tests = new ArrayList<Test>(); + + int numTests = countTestCases(); + log.debug("numTests = " + numTests); + + for (int i = 0; i < numTests; i++) + { + tests.add(testAt(i)); + } + + return tests; + } +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/CSVTestListener.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/CSVTestListener.java new file mode 100644 index 0000000000..40841189a3 --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/CSVTestListener.java @@ -0,0 +1,572 @@ +/* + * + * 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.qpid.junit.extensions.listeners; + +import junit.framework.AssertionFailedError; +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestListener; + +import org.apache.log4j.Logger; + +import org.apache.qpid.junit.extensions.ShutdownHookable; +import org.apache.qpid.junit.extensions.util.TestContextProperties; + +import java.io.IOException; +import java.io.Writer; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.TreeSet; + +/** + * CSVTestListener is both a test listener, a timings listener, a memory listener and a parameter listener. It listens for test completion events and + * then writes out all the data that it has listened to into a '.csv' (comma seperated values) file. + * + * <p><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities <th> Collaborations + * <tr><td> Listen to test events; start, end, fail, error. + * <tr><td> Listen to test timings. + * <tr><td> Listen to test memory usage. + * <tr><td> Listen to parameterized test parameters. + * <tr><td> Output all test data to a CSV file. + * </table> + * + * @author Rupert Smith + * + * @todo Write an XML output class. Write a transform to convert it into an HTML page with timings as graphs. + */ +public class CSVTestListener implements TestListener, TKTestListener, ShutdownHookable +{ + /** Used for logging. */ + private static final Logger log = Logger.getLogger(CSVTestListener.class); + + /** The timings file writer. */ + private Writer timingsWriter; + + /** + * Map for holding results on a per thread basis as they come in. A ThreadLocal is not used as sometimes an + * explicit thread id must be used, where notifications come from different threads than the ones that called + * the test method. + */ + Map<Long, TestResult> threadLocalResults = Collections.synchronizedMap(new HashMap<Long, TestResult>()); + + /** Used to record the start time of a complete test run, for outputing statistics at the end of the test run. */ + private long batchStartTime; + + /** Used to record the number of errors accross a complete test run. */ + private int numError; + + /** Used to record the number of failures accross a complete test run. */ + private int numFailed; + + /** Used to record the number of passes accross a complete test run. */ + private int numPassed; + + /** Used to record the total tests run accross a complete test run. Always equal to passes + errors + fails. */ + private int totalTests; + + /** Used to recrod the current concurrency level for the test batch. */ + private int concurrencyLevel; + + /** + * Used to record the total 'size' of the tests run, this is the number run times the average value of the test + * size parameters. + */ + private int totalSize; + + /** + * Used to record the summation of all of the individual test timings. Note that total time and summed time + * are unlikely to be in agreement, exception for a single threaded test (with no setup time). Total time is + * the time taken to run all the tests, summed time is the added up time that each individual test took. So if + * two tests run in parallel and take one second each, total time will be one seconds, summed time will be two + * seconds. + */ + private long summedTime; + + /** + * Used to record the summation of all of the second test timing information. + * One use of the second timing would be to provide latency as well as test timing. + */ + private long summedTime2; + + /** Flag to indicate when batch has been started but not ended to ensure end batch stats are output only once. */ + private boolean batchStarted = false; + + /** + * Creates a new CSVTestListener object. + * + * @param writer A writer where this CSV listener should write out its output to. + */ + public CSVTestListener(Writer writer) + { + // log.debug("public CSVTestListener(Writer writer): called"); + + // Keep the writer. + this.timingsWriter = writer; + } + + /** + * Resets the test results to the default state of time zero, memory usage zero, test passed. + * + * @param test The test to resest any results for. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void reset(Test test, Long threadId) + { + // log.debug("public void reset(Test test = \"" + test + "\", Long threadId = " + threadId + "): called"); + + TestResult r = + (threadId == null) ? threadLocalResults.get(Thread.currentThread().getId()) : threadLocalResults.get(threadId); + + r.testTime = 0L; + r.testTime2 = null; + r.testStartMem = 0L; + r.testEndMem = 0L; + r.testState = "Pass"; + r.testParam = 0; + } + + /** + * Called when a test results in an error. + * + * @param test The test which is in error. + * @param t Any Throwable raised by the test in error. + */ + public void addError(Test test, Throwable t) + { + // log.debug("public void addError(Test test, Throwable t): called"); + + TestResult r = threadLocalResults.get(Thread.currentThread().getId()); + r.testState = "Error"; + } + + /** + * Called when a test results in a failure. + * + * @param test The test which failed. + * @param t The AssertionFailedError that encapsulates the test failure. + */ + public void addFailure(Test test, AssertionFailedError t) + { + // log.debug("public void addFailure(Test \"" + test + "\", AssertionFailedError t): called"); + + TestResult r = threadLocalResults.get(Thread.currentThread().getId()); + r.testState = "Failure"; + } + + /** + * Called when a test completes to mark it as a test fail. This method should be used when registering a + * failure from a different thread than the one that started the test. + * + * @param test The test which failed. + * @param e The assertion that failed the test. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void addFailure(Test test, AssertionFailedError e, Long threadId) + { + // log.debug("public void addFailure(Test test = \"" + test + "\", AssertionFailedError e, Long threadId = " + threadId + // + "): called"); + + TestResult r = + (threadId == null) ? threadLocalResults.get(Thread.currentThread().getId()) : threadLocalResults.get(threadId); + + r.testState = "Failure"; + } + + /** + * Called when a test completes. Success, failure and errors. + * + * @param test The test which completed. + */ + public void endTest(Test test) + { + // log.debug("public void endTest(Test \"" + test + "\"): called"); + + TestResult r = threadLocalResults.get(Thread.currentThread().getId()); + + writeTestResults(r, test); + + // Clear all the test results for the thread. + threadLocalResults.remove(Thread.currentThread().getId()); + } + + /** + * Called when a test starts. + * + * @param test The test wich has started. + */ + public void startTest(Test test) + { + // log.debug("public void startTest(Test \"" + test + "\"): called"); + + // Initialize the thread local test results. + threadLocalResults.put(Thread.currentThread().getId(), new TestResult()); + } + + /** + * Should be called every time a test completes with the run time of that test. + * + * @param test The name of the test. + * @param nanos The run time of the test in nanoseconds. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void timing(Test test, long nanos, Long threadId) + { + // log.debug("public void timing(String \"" + test + "\", long " + nanos + "): called"); + + TestResult r = + (threadId == null) ? threadLocalResults.get(Thread.currentThread().getId()) : threadLocalResults.get(threadId); + + r.testTime = nanos; + summedTime += nanos; + } + + /** + * Optionally called every time a test completes with the second timing test. + * + * @param test The name of the test. + * @param nanos The second timing information of the test in nanoseconds. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void timing2(Test test, Long nanos, Long threadId) + { + TestResult r = + (threadId == null) ? threadLocalResults.get(Thread.currentThread().getId()) : threadLocalResults.get(threadId); + + + if (nanos != null) + { + r.testTime2 = nanos; + summedTime2 += nanos; + } + } + + /** + * Should be called every time a test completed with the amount of memory used before and after the test was run. + * + * @param test The test which memory was measured for. + * @param memStart The total JVM memory used before the test was run. + * @param memEnd The total JVM memory used after the test was run. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void memoryUsed(Test test, long memStart, long memEnd, Long threadId) + { + // log.debug("public void memoryUsed(Test \"" + test + "\", long " + memStart + ", long " + memEnd + ", Long " + // + threadId + "): called"); + + TestResult r = + (threadId == null) ? threadLocalResults.get(Thread.currentThread().getId()) : threadLocalResults.get(threadId); + + r.testStartMem = memStart; + r.testEndMem = memEnd; + } + + /** + * Should be called every time a parameterized test completed with the int value of its test parameter. + * + * @param test The test which memory was measured for. + * @param parameter The int parameter value. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void parameterValue(Test test, int parameter, Long threadId) + { + // log.debug("public void parameterValue(Test test = \"" + test + "\", int parameter = " + parameter + "): called"); + + TestResult r = + (threadId == null) ? threadLocalResults.get(Thread.currentThread().getId()) : threadLocalResults.get(threadId); + + r.testParam = parameter; + totalSize += parameter; + } + + /** + * Should be called every time a test completes with the current number of test threads running. This should not + * change within a test batch, therefore it is safe to take this as a batch level property value too. + * + * @param test The test for which the measurement is being generated. + * @param threads The number of tests being run concurrently. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void concurrencyLevel(Test test, int threads, Long threadId) + { + // log.debug("public void concurrencyLevel(Test test = \"" + test + "\", int threads = " + threads + "): called"); + + TestResult r = + (threadId == null) ? threadLocalResults.get(Thread.currentThread().getId()) : threadLocalResults.get(threadId); + + r.testConcurrency = threads; + concurrencyLevel = threads; + + } + + /** + * Called when a test completes. Success, failure and errors. This method should be used when registering an + * end test from a different thread than the one that started the test. + * + * @param test The test which completed. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void endTest(Test test, Long threadId) + { + // log.debug("public void endTest(Test test = \"" + test + "\", Long threadId " + threadId + "): called"); + + TestResult r = + (threadId == null) ? threadLocalResults.get(Thread.currentThread().getId()) : threadLocalResults.get(threadId); + + writeTestResults(r, test); + } + + /** + * Takes a time stamp for the beginning of the batch and resets stats counted for the batch. + */ + public synchronized void startBatch() + { + numError = 0; + numFailed = 0; + numPassed = 0; + totalTests = 0; + totalSize = 0; + batchStartTime = System.nanoTime(); + summedTime = 0; + summedTime2 = 0; + batchStarted = true; + + // Write out the column headers for the batch. + writeColumnHeaders(); + } + + /** + * Takes a time stamp for the end of the batch to calculate the total run time. + * Write this and other stats out to the tail of the csv file. + * + * @param parameters The optional test parameters, may be null. + */ + public synchronized void endBatch(Properties parameters) + { + boolean noParams = (parameters == null) || (parameters.size() == 0); + + // Check that a batch has been started but not ended. + if (batchStarted) + { + long batchEndTime = System.nanoTime(); + float totalTimeMillis = ((float) (batchEndTime - batchStartTime)) / 1000000f; + float summedTimeMillis = ((float) summedTime) / 1000000f; + float summedTime2Millis = ((float) summedTime2) / 1000000f; + + + // Write the stats for the batch out. + try + { + synchronized (this.getClass()) + { + timingsWriter.write("Total Tests:, " + totalTests + ", "); + timingsWriter.write("Total Passed:, " + numPassed + ", "); + timingsWriter.write("Total Failed:, " + numFailed + ", "); + timingsWriter.write("Total Error:, " + numError + ", "); + timingsWriter.write("Total Size:, " + totalSize + ", "); + timingsWriter.write("Summed Time:, " + summedTimeMillis + ", "); + timingsWriter.write("Summed Custom Time:, " + summedTime2Millis + ", "); + timingsWriter.write("Concurrency Level:, " + concurrencyLevel + ", "); + timingsWriter.write("Total Time:, " + totalTimeMillis + ", "); + timingsWriter.write("Average Custom Time:, " + ((summedTime2Millis/ (float) totalTests)) + ", "); + timingsWriter.write("Test Throughput:, " + (((float) totalTests) / totalTimeMillis) + ", "); + timingsWriter.write("Test * Size Throughput:, " + (((float) totalSize) / totalTimeMillis) + + (noParams ? "\n\n" : ", ")); + + // Write out the test parameters if there are any specified. + if (!noParams) + { + properties(parameters); + } + + timingsWriter.flush(); + } + } + catch (IOException e) + { + throw new RuntimeException("Unable to write out end batch statistics: " + e, e); + } + } + + // Reset the batch started flag to ensure stats are only output once. + batchStarted = false; + } + + /** + * Notifies listeners of the tests read/set properties. + * + * @param properties The tests read/set properties. + */ + public void properties(Properties properties) + { + // log.debug("public void properties(Properties properties): called"); + + // Write the properties out to the results file. + try + { + synchronized (this.getClass()) + { + Set keySet = new TreeSet(properties.keySet()); + + // timingsWriter.write("\n"); + + for (Object key : keySet) + { + timingsWriter.write(key + " = , " + properties.getProperty((String) key) + ", "); + } + + timingsWriter.write("\n\n"); + // timingsWriter.flush(); + } + } + catch (IOException e) + { + throw new RuntimeException("Unable to write out test parameters: " + e, e); + } + + // Write out the column headers after the properties. + // writeColumnHeaders(); + } + + /** + * Writes out and flushes the column headers for raw test data. + */ + private void writeColumnHeaders() + { + // Write the column headers for the CSV file. Any IO exceptions are ignored. + try + { + timingsWriter.write("Class, "); + timingsWriter.write("Method, "); + timingsWriter.write("Thread, "); + timingsWriter.write("Test Outcome, "); + timingsWriter.write("Time (milliseconds), "); + timingsWriter.write("Custom Time (milliseconds), "); + timingsWriter.write("Memory Used (bytes), "); + timingsWriter.write("Concurrency level, "); + timingsWriter.write("Test Size\n"); + + timingsWriter.flush(); + } + catch (IOException e) + { + throw new RuntimeException("Unable to write out column headers: " + e, e); + } + } + + /** + * Writes out the test results for the specified test. This outputs a single line of results to the csv file. + * + * @param r The test results to write out. + * @param test The test to write them out for. + */ + private void writeTestResults(TestResult r, Test test) + { + // Update the running stats for this batch. + if ("Error".equals(r.testState)) + { + numError++; + } + else if ("Failure".equals(r.testState)) + { + numFailed++; + } + else if ("Pass".equals(r.testState)) + { + numPassed++; + } + + totalTests++; + + // Write the test name and thread information plus all instrumenation a line of the CSV ouput. Any IO + // exceptions are ignored. + try + { + synchronized (this.getClass()) + { + timingsWriter.write(test.getClass().getName() + ", "); + timingsWriter.write(((test instanceof TestCase) ? ((TestCase) test).getName() : "") + ", "); + timingsWriter.write(Thread.currentThread().getName() + ", "); + timingsWriter.write(r.testState + ", "); + timingsWriter.write((((float) r.testTime) / 1000000f) + ", "); + + timingsWriter.write(r.testTime2 == null ? "- , " : + (((float) r.testTime2) / 1000000f) + ", "); + timingsWriter.write((r.testEndMem - r.testStartMem) + ", "); + timingsWriter.write(r.testConcurrency + ", "); + timingsWriter.write(r.testParam + "\n"); + } + } + catch (IOException e) + { + throw new RuntimeException("Unable to write out test results: " + e, e); + } + } + + /** + * Supplies the shutdown hook. This attempts to flush the results in the event of the test runner being prematurely + * suspended before the end of the current test batch. + * + * @return The shut down hook. + */ + public Thread getShutdownHook() + { + return new Thread(new Runnable() + { + public void run() + { + log.debug("CSVTestListener::ShutdownHook: called"); + + // Complete the current test batch stats. + endBatch(TestContextProperties.getInstance()); + } + }); + } + + /** Captures test results packaged into a single object, so that it can be set up as a thread local. */ + private static class TestResult + { + /** Used to hold the test timing. */ + public long testTime; + + /** Use to hold the second timing information. */ + public Long testTime2; + + /** Used to hold the test start memory usage. */ + public long testStartMem; + + /** Used to hold the test end memory usage. */ + public long testEndMem; + + /** Used to hold the test pass/fail/error state. */ + public String testState = "Pass"; + + /** Used to hold the test parameter value. */ + public int testParam; + + /** Used to hold the concurrency level under which the test was run. */ + public int testConcurrency; + + } +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/ConsoleTestListener.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/ConsoleTestListener.java new file mode 100644 index 0000000000..276fec328e --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/ConsoleTestListener.java @@ -0,0 +1,274 @@ +/* + * + * 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.qpid.junit.extensions.listeners; + +import junit.framework.AssertionFailedError; +import junit.framework.Test; +import junit.framework.TestListener; + +import org.apache.qpid.junit.extensions.SleepThrottle; +import org.apache.qpid.junit.extensions.Throttle; + +import java.util.Properties; + +/** + * ConsoleTestListener provides feedback to the console, as test timings are taken, by drawing a '.', or an 'E', or an + * 'F', for each test that passes, is in error or fails. It does this for every test result registered with the framework, + * not just on the completion of each test method as the JUnit one does. It also uses a throttle to cap the rate of + * dot drawing, as exessively high rates can degrade test performance without providing much usefull feedback to the user. + * Unlike the JUnit dot drawing feedback, this one will correctly wrap lines when tests are run concurrently (the + * rate capping ensures that this does not become a hot-spot for thread contention). + * + * <p/>Where rate capping causes the conflation of multiple requested dots into a single dot, the dot that is actually + * drawn will be the worst result within the conflation period, that is, error is worse than fail which is worse than pass. + * + * <p/><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities <th> Collaborations + * <tr><td> Draw dots as each test result completes, at a capped rate. + * </table> + * + * @author Rupert Smith + */ +public class ConsoleTestListener implements TestListener, TKTestListener +{ + /** Used to indicate a test pass. */ + private static final int PASS = 1; + + /** Used to indicate a test failure. */ + private static final int FAIL = 2; + + /** Used to indicate a test error. */ + private static final int ERROR = 3; + + /** Defines the maximum number of columns of dots to print. */ + private static final int MAX_COLUMNS = 80; + + /** Used to throttle the dot writing rate. */ + Throttle throttle; + + /** Tracks the worst test result so far, when the throttled print method must conflate results due to throttling. */ + private int conflatedResult = 0; + + /** Tracks the column count as dots are printed, so that newlines can be inserted at the right margin. */ + private int columnCount = 0; + + /** Used as a monitor on the print method criticial section, to ensure that line ends always happen in the right place. */ + private final Object printMonitor = new Object(); + + /** + * Creates a dot drawing feedback test listener, set by default to 80 columns at 80 dots per second capped rate. + */ + public ConsoleTestListener() + { + throttle = new SleepThrottle(); + throttle.setRate(80f); + } + + /** + * Prints dots at a capped rate, conflating the requested type of dot to draw if this method is called at a rate + * higher than the capped rate. The conflation works by always printing the worst result that occurs within the + * conflation period, that is, error is worse than fail which is worse than a pass. + * + * @param result The type of dot to draw, {@link #PASS}, {@link #FAIL} or {@link #ERROR}. + */ + private void throttledPrint(int result) + { + conflatedResult = (result > conflatedResult) ? result : conflatedResult; + + if (throttle.checkThrottle()) + { + synchronized (printMonitor) + { + switch (conflatedResult) + { + default: + case PASS: + System.out.print('.'); + break; + + case FAIL: + System.out.print('F'); + break; + + case ERROR: + System.out.print('E'); + break; + } + + columnCount = (columnCount >= MAX_COLUMNS) ? 0 : (columnCount + 1); + + if (columnCount == 0) + { + System.out.print('\n'); + } + + conflatedResult = 0; + } + } + } + + /** + * An error occurred. + * + * @param test The test in error. Ignored. + * @param t The error that the test threw. Ignored. + */ + public void addError(Test test, Throwable t) + { + throttledPrint(ERROR); + } + + /** + * A failure occurred. + * + * @param test The test that failed. Ignored. + * @param t The assertion failure that the test threw. Ignored. + */ + public void addFailure(Test test, AssertionFailedError t) + { + throttledPrint(FAIL); + } + + /** + * A test ended. + * + * @param test The test that ended. Ignored. + */ + public void endTest(Test test) + { + throttledPrint(PASS); + } + + /** + * A test started. + * + * @param test The test that started. Ignored. + */ + public void startTest(Test test) + { } + + /** + * Resets the test results to the default state of time zero, memory usage zero, parameter zero, test passed. + * + * @param test The test to resest any results for. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void reset(Test test, Long threadId) + { } + + /** + * Should be called every time a test completes with the run time of that test. + * + * @param test The name of the test. + * @param nanos The run time of the test in nanoseconds. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void timing(Test test, long nanos, Long threadId) + { } + + /** + * Optionally called every time a test completes with the second timing test. + * + * @param test The name of the test. + * @param nanos The second timing information of the test in nanoseconds. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void timing2(Test test, Long nanos, Long threadId) + { } + + /** + * Should be called every time a test completed with the amount of memory used before and after the test was run. + * + * @param test The test which memory was measured for. + * @param memStart The total JVM memory used before the test was run. + * @param memEnd The total JVM memory used after the test was run. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void memoryUsed(Test test, long memStart, long memEnd, Long threadId) + { } + + /** + * Should be called every time a parameterized test completed with the int value of its test parameter. + * + * @param test The test which memory was measured for. + * @param parameter The int parameter value. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void parameterValue(Test test, int parameter, Long threadId) + { } + + /** + * Should be called every time a test completes with the current number of test threads running. + * + * @param test The test for which the measurement is being generated. + * @param threads The number of tests being run concurrently. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void concurrencyLevel(Test test, int threads, Long threadId) + { } + + /** + * Called when a test completes. Success, failure and errors. This method should be used when registering an + * end test from a different thread than the one that started the test. + * + * @param test The test which completed. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void endTest(Test test, Long threadId) + { + throttledPrint(PASS); + } + + /** + * Called when a test completes to mark it as a test fail. This method should be used when registering a + * failure from a different thread than the one that started the test. + * + * @param test The test which failed. + * @param e The assertion that failed the test. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void addFailure(Test test, AssertionFailedError e, Long threadId) + { + throttledPrint(FAIL); + } + + /** + * Notifies listeners of the start of a complete run of tests. + */ + public void startBatch() + { } + + /** + * Notifies listeners of the end of a complete run of tests. + * + * @param parameters The optional test parameters to log out with the batch results. + */ + public void endBatch(Properties parameters) + { } + + /** + * Notifies listeners of the tests read/set properties. + * + * @param properties The tests read/set properties. + */ + public void properties(Properties properties) + { } +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/TKTestListener.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/TKTestListener.java new file mode 100644 index 0000000000..63b042db10 --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/TKTestListener.java @@ -0,0 +1,142 @@ +/* + * + * 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.qpid.junit.extensions.listeners; + +import junit.framework.AssertionFailedError; +import junit.framework.Test; +import junit.framework.TestListener; + +import java.util.Properties; + +/** + * TKTestListener is a listener interface for listeners that want to be informed of the run times of tests, the memory + * usage of tests, the 'size' parameters of parameterized tests and the begin and end events of complete test runs. + * {@link org.apache.qpid.junit.extensions.TKTestResult} is an example of a test result class that listeners + * interested in these events can be attached to. + * + * The {@link #timing(junit.framework.Test, long, Long)}, {@link #memoryUsed(junit.framework.Test, long, long, Long)}, + * {@link #parameterValue(junit.framework.Test, int, Long)} and {@link #endTest(junit.framework.Test, Long)} methods + * all accept on optional thread id parameter. + * + * <p><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities + * <tr><td> Listen to test timings. + * <tr><td> Listen to test memory usages. + * <tr><td> Listen to parameterized test parameters. + * </table> + * + * @author Rupert Smith + */ +public interface TKTestListener extends TestListener +{ + /** + * Resets the test results to the default state of time zero, memory usage zero, parameter zero, test passed. + * + * @param test The test to resest any results for. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void reset(Test test, Long threadId); + + /** + * Should be called every time a test completes with the run time of that test. + * + * @param test The name of the test. + * @param nanos The run time of the test in nanoseconds. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void timing(Test test, long nanos, Long threadId); + + + /** + * Optionally called every time a test completes with the second timing test. + * + * @param test The name of the test. + * @param nanos The second timing information of the test in nanoseconds. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void timing2(Test test, Long nanos, Long threadId); + + /** + * Should be called every time a test completed with the amount of memory used before and after the test was run. + * + * @param test The test which memory was measured for. + * @param memStart The total JVM memory used before the test was run. + * @param memEnd The total JVM memory used after the test was run. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void memoryUsed(Test test, long memStart, long memEnd, Long threadId); + + /** + * Should be called every time a parameterized test completed with the int value of its test parameter. + * + * @param test The test which memory was measured for. + * @param parameter The int parameter value. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void parameterValue(Test test, int parameter, Long threadId); + + /** + * Should be called every time a test completes with the current number of test threads running. + * + * @param test The test for which the measurement is being generated. + * @param threads The number of tests being run concurrently. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void concurrencyLevel(Test test, int threads, Long threadId); + + /** + * Called when a test completes. Success, failure and errors. This method should be used when registering an + * end test from a different thread than the one that started the test. + * + * @param test The test which completed. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void endTest(Test test, Long threadId); + + /** + * Called when a test completes to mark it as a test fail. This method should be used when registering a + * failure from a different thread than the one that started the test. + * + * @param test The test which failed. + * @param e The assertion that failed the test. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void addFailure(Test test, AssertionFailedError e, Long threadId); + + /** + * Notifies listeners of the start of a complete run of tests. + */ + public void startBatch(); + + /** + * Notifies listeners of the end of a complete run of tests. + * + * @param parameters The optional test parameters to log out with the batch results. + */ + public void endBatch(Properties parameters); + + /** + * Notifies listeners of the tests read/set properties. + * + * @param properties The tests read/set properties. + */ + public void properties(Properties properties); +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/XMLTestListener.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/XMLTestListener.java new file mode 100644 index 0000000000..ac875f89cf --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/XMLTestListener.java @@ -0,0 +1,410 @@ +/* + * + * 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.qpid.junit.extensions.listeners; + +import junit.framework.AssertionFailedError; +import junit.framework.Test; +import junit.framework.TestCase; + +import org.apache.log4j.Logger; + +import org.apache.qpid.junit.extensions.ShutdownHookable; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Writer; +import java.util.*; + +/** + * Listens for test results for a named test and outputs these in the standard JUnit XML format to the specified + * writer. + * + * <p/>The API for this listener accepts notifications about different aspects of a tests results through different + * methods, so some assumption needs to be made as to which test result a notification refers to. For example + * {@link #startTest} will be called, then possibly {@link #timing} will be called, even though the test instance is + * passed in both cases, it is not enough to distinguish a particular run of the test, as the test case instance may + * be being shared between multiple threads, or being run a repeated number of times, and can therfore be re-used + * between calls. The listeners make the assumption that, for every test, a unique thread will call {@link #startTest} + * and {@link #endTest} to delimit each test. All calls to set test parameters, timings, state and so on, will occur + * between the start and end and will be given with the same thread id as the start and end, so the thread id provides + * a unqiue value to identify a particular test run against. + * + * <p><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities <th> Collaborations + * <tr><td> Listen to test lifecycle notifications. + * <tr><td> Listen to test errors and failures. + * <tr><td> Listen to test timings. + * <tr><td> Listen to test memory usages. + * <tr><td> Listen to parameterized test parameters. + * <tr><th> Responsibilities + * </table> + * + * @todo Merge this class with CSV test listener, making the collection of results common to both, and only factoring + * out the results printing code into sub-classes. Provide a simple XML results formatter with the same format as + * the ant XML formatter, and a more structured one for outputing results with timings and summaries from + * performance tests. + * + * @author Rupert Smith + */ +public class XMLTestListener implements TKTestListener, ShutdownHookable +{ + /** Used for debugging. */ + private static final Logger log = Logger.getLogger(XMLTestListener.class); + + /** The results file writer. */ + protected Writer writer; + + /** Holds the results for individual tests. */ + // protected Map<Result, Result> results = new LinkedHashMap<Result, Result>(); + // protected List<Result> results = new ArrayList<Result>(); + + /** + * Map for holding results on a per thread basis as they come in. A ThreadLocal is not used as sometimes an + * explicit thread id must be used, where notifications come from different threads than the ones that called + * the test method. + */ + Map<Long, Result> threadLocalResults = Collections.synchronizedMap(new LinkedHashMap<Long, Result>()); + + /** + * Holds results for tests that have ended. Transferring these results here from the per-thread results map, means + * that the thread id is freed for the thread to generate more results. + */ + List<Result> results = new ArrayList<Result>(); + + /** Holds the overall error count. */ + protected int errors = 0; + + /** Holds the overall failure count. */ + protected int failures = 0; + + /** Holds the overall tests run count. */ + protected int runs = 0; + + /** Holds the name of the class that tests are being run for. */ + String testClassName; + + /** + * Creates a new XML results output listener that writes to the specified location. + * + * @param writer The location to write results to. + * @param testClassName The name of the test class to include in the test results. + */ + public XMLTestListener(Writer writer, String testClassName) + { + log.debug("public XMLTestListener(Writer writer, String testClassName = " + testClassName + "): called"); + + this.writer = writer; + this.testClassName = testClassName; + } + + /** + * Resets the test results to the default state of time zero, memory usage zero, parameter zero, test passed. + * + * @param test The test to resest any results for. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void reset(Test test, Long threadId) + { + log.debug("public void reset(Test test = " + test + ", Long threadId = " + threadId + "): called"); + + XMLTestListener.Result r = + (threadId == null) ? threadLocalResults.get(Thread.currentThread().getId()) : threadLocalResults.get(threadId); + + r.error = null; + r.failure = null; + + } + + /** + * Notification that a test started. + * + * @param test The test that started. + */ + public void startTest(Test test) + { + log.debug("public void startTest(Test test = " + test + "): called"); + + Result newResult = new Result(test.getClass().getName(), ((TestCase) test).getName()); + + // Initialize the thread local test results. + threadLocalResults.put(Thread.currentThread().getId(), newResult); + runs++; + } + + /** + * Should be called every time a test completes with the run time of that test. + * + * @param test The name of the test. + * @param nanos The run time of the test in nanoseconds. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void timing(Test test, long nanos, Long threadId) + { } + + /** + * Optionally called every time a test completes with the second timing test. + * + * @param test The name of the test. + * @param nanos The second timing information of the test in nanoseconds. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void timing2(Test test, Long nanos, Long threadId) + { } + + /** + * Should be called every time a test completed with the amount of memory used before and after the test was run. + * + * @param test The test which memory was measured for. + * @param memStart The total JVM memory used before the test was run. + * @param memEnd The total JVM memory used after the test was run. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void memoryUsed(Test test, long memStart, long memEnd, Long threadId) + { } + + /** + * Should be called every time a parameterized test completed with the int value of its test parameter. + * + * @param test The test which memory was measured for. + * @param parameter The int parameter value. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void parameterValue(Test test, int parameter, Long threadId) + { } + + /** + * Should be called every time a test completes with the current number of test threads running. + * + * @param test The test for which the measurement is being generated. + * @param threads The number of tests being run concurrently. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void concurrencyLevel(Test test, int threads, Long threadId) + { } + + /** + * Notifies listeners of the tests read/set properties. + * + * @param properties The tests read/set properties. + */ + public void properties(Properties properties) + { } + + /** + * Notification that a test ended. + * + * @param test The test that ended. + */ + public void endTest(Test test) + { + log.debug("public void endTest(Test test = " + test + "): called"); + + // Move complete test results into the completed tests list. + Result r = threadLocalResults.get(Thread.currentThread().getId()); + results.add(r); + + // Clear all the test results for the thread. + threadLocalResults.remove(Thread.currentThread().getId()); + } + + /** + * Called when a test completes. Success, failure and errors. This method should be used when registering an + * end test from a different thread than the one that started the test. + * + * @param test The test which completed. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void endTest(Test test, Long threadId) + { + log.debug("public void endTest(Test test = " + test + ", Long threadId = " + threadId + "): called"); + + // Move complete test results into the completed tests list. + Result r = + (threadId == null) ? threadLocalResults.get(Thread.currentThread().getId()) : threadLocalResults.get(threadId); + results.add(r); + + // Clear all the test results for the thread. + threadLocalResults.remove(Thread.currentThread().getId()); + } + + /** + * An error occurred. + * + * @param test The test in which the error occurred. + * @param t The throwable that resulted from the error. + */ + public void addError(Test test, Throwable t) + { + log.debug("public void addError(Test test = " + test + ", Throwable t = " + t + "): called"); + + Result r = threadLocalResults.get(Thread.currentThread().getId()); + r.error = t; + errors++; + } + + /** + * A failure occurred. + * + * @param test The test in which the failure occurred. + * @param t The JUnit assertions that led to the failure. + */ + public void addFailure(Test test, AssertionFailedError t) + { + log.debug("public void addFailure(Test test = " + test + ", AssertionFailedError t = " + t + "): called"); + + Result r = threadLocalResults.get(Thread.currentThread().getId()); + r.failure = t; + failures++; + } + + /** + * Called when a test completes to mark it as a test fail. This method should be used when registering a + * failure from a different thread than the one that started the test. + * + * @param test The test which failed. + * @param e The assertion that failed the test. + * @param threadId Optional thread id if not calling from thread that started the test method. May be null. + */ + public void addFailure(Test test, AssertionFailedError e, Long threadId) + { + log.debug("public void addFailure(Test test, AssertionFailedError e, Long threadId): called"); + + Result r = + (threadId == null) ? threadLocalResults.get(Thread.currentThread().getId()) : threadLocalResults.get(threadId); + r.failure = e; + failures++; + } + + /** + * Notifies listeners of the start of a complete run of tests. + */ + public void startBatch() + { + log.debug("public void startBatch(): called"); + + // Reset all results counts. + threadLocalResults = Collections.synchronizedMap(new HashMap<Long, Result>()); + errors = 0; + failures = 0; + runs = 0; + + // Write out the file header. + try + { + writer.write("<?xml version=\"1.0\" ?>\n"); + } + catch (IOException e) + { + throw new RuntimeException("Unable to write the test results.", e); + } + } + + /** + * Notifies listeners of the end of a complete run of tests. + * + * @param parameters The optional test parameters to log out with the batch results. + */ + public void endBatch(Properties parameters) + { + log.debug("public void endBatch(Properties parameters = " + parameters + "): called"); + + // Write out the results. + try + { + // writer.write("<?xml version=\"1.0\" ?>\n"); + writer.write("<testsuite errors=\"" + errors + "\" failures=\"" + failures + "\" tests=\"" + runs + "\" name=\"" + + testClassName + "\">\n"); + + for (Result result : results) + { + writer.write(" <testcase classname=\"" + result.testClass + "\" name=\"" + result.testName + "\">\n"); + + if (result.error != null) + { + writer.write(" <error type=\"" + result.error.getClass() + "\">"); + result.error.printStackTrace(new PrintWriter(writer)); + writer.write(" </error>"); + } + else if (result.failure != null) + { + writer.write(" <failure type=\"" + result.failure.getClass() + "\">"); + result.failure.printStackTrace(new PrintWriter(writer)); + writer.write(" </failure>"); + } + + writer.write(" </testcase>\n"); + } + + writer.write("</testsuite>\n"); + writer.flush(); + } + catch (IOException e) + { + throw new RuntimeException("Unable to write the test results.", e); + } + } + + /** + * Supplies the shutdown hook. + * + * @return The shut down hook. + */ + public Thread getShutdownHook() + { + return new Thread(new Runnable() + { + public void run() + { + log.debug("XMLTestListener::ShutdownHook: called"); + } + }); + } + + /** + * Used to capture the results of a particular test run. + */ + protected static class Result + { + /** Holds the name of the test class. */ + public String testClass; + + /** Holds the name of the test method. */ + public String testName; + + /** Holds the exception that caused error in this test. */ + public Throwable error; + + /** Holds the assertion exception that caused failure in this test. */ + public AssertionFailedError failure; + + /** + * Creates a placeholder for the results of a test. + * + * @param testClass The test class. + * @param testName The name of the test that was run. + */ + public Result(String testClass, String testName) + { + this.testClass = testClass; + this.testName = testName; + } + } +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/package.html b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/package.html new file mode 100644 index 0000000000..15acc02ab1 --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/package.html @@ -0,0 +1,27 @@ +<!-- + + 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. + +--> + +<html> +<body> +Listners for test statistics are defined in this package. At the moment there is only one listener which writes all test +statistics out to a CSV (comma seperated values) file which can be loaded by most spread sheets. +</body> +</html> diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/package.html b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/package.html new file mode 100644 index 0000000000..4cab8d936a --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/package.html @@ -0,0 +1,33 @@ +<!-- + + 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. + +--> + +<html> +<body> +Basic JUnit is enahanced with test runners to run tests repeatedly, simultaneously in many threads and with increasing +test sizes for asymptotic performance measurements. There are features to measure the time and amount of memory that +tests use as well as to record the asymptotic test size parameters. There are some utilities to write these test +statistics to various file formats too and these can be found in the listeners package. + +</p>The main test runner class is TKTestRunner which can be called with command line parameters to specify how tests +should be run. + +</body> +</html> diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/CommandLineParser.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/CommandLineParser.java new file mode 100644 index 0000000000..f158090e96 --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/CommandLineParser.java @@ -0,0 +1,787 @@ +/* + * + * 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.qpid.junit.extensions.util; + +import java.text.CharacterIterator; +import java.text.StringCharacterIterator; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * CommandLineParser provides a utility for specifying the format of a command line and parsing command lines to ensure + * that they fit their specified format. A command line is made up of flags and options, both may be refered to as + * options. A flag is an option that does not take an argument (specifying it means it has the value 'true' and not + * specifying it means it has the value 'false'). Options must take arguments but they can be set up with defaults so + * that they take a default value when not set. Options may be mandatory in wich case it is an error not to specify + * them on the command line. Flags are never mandatory because they are implicitly set to false when not specified. + * + * <p/>Some examples command line are: + * + * <ul> + * <li>This one has two options that expect arguments: + * <pre> + * cruisecontrol -configfile cruisecontrol.xml -port 9000 + * </pre> + * <li>This has one no-arg flag and two 'free' arguments: + * <pre> + * zip -r project.zip project/* + * </pre> + * <li>This one concatenates multiple flags into a single block with only one '-': + * <pre> + * jar -tvf mytar.tar + * </pre> + * + * <p/>The parsing rules are: + * + * <ol> + * <li>Flags may be combined after a single '-' because they never take arguments. Normally such flags are single letter + * flags but this is only a convention and not enforced. Flags of more than one letter are usually specified on their own. + * <li>Options expecting arguments must always be on their own. + * <li>The argument to an option may be seperated from it by whitespace or appended directly onto the option. + * <li>The argument to an option may never begin with a '-' character. + * <li>All other arguments not beginning with a '-' character are free arguments that do not belong to any option. + * <li>The second or later of a set of duplicate or repeated flags override earlier ones. + * <li>Options are matched up to the shortest matching option. This is because of the possibility of having no space + * between an option and its argument. This rules out the possibility of using two options where one is an opening + * substring of the other. For example, the options "foo" and "foobar" cannot be used on the same command line because + * it is not possible to distinguish the argument "-foobar" from being the "foobar" option or the "foo" option with + * the "bar" argument. + * </ol> + * + * <p/>By default, unknown options are simply ignored if specified on the command line. This behaviour may be changed + * so that the parser reports all unknowns as errors by using the {@link #setErrorsOnUnknowns} method. + * + * <p><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities <th> Collaborations + * <tr><td> Accept a command line specification. + * <tr><td> Parse a command line into properties, validating it against its specification. + * <tr><td> Report all errors between a command line and its specification. + * <tr><td> Provide a formatted usage string for a command line. + * <tr><td> Provide a formatted options in force string for a command line. + * <tr><td> Allow errors on unknowns behaviour to be turned on or off. + * </table> + * + * @author Rupert Smith + */ +public class CommandLineParser +{ + /** + * Holds a mapping from command line option names to detailed information about those options. + * Use of a tree map ensures that the options are easy to print in alphabetical order as a usage string. + * An alternative might be to use a LinkedHashMap to print them in the order they are specified. + */ + private Map<String, CommandLineOption> optionMap = new TreeMap<String, CommandLineOption>(); + + /** Holds a list of parsing errors. */ + private List<String> parsingErrors = new ArrayList<String>(); + + /** Holds the regular head matcher to match command line options with. */ + private Matcher optionMatcher = null; + + /** Holds the parsed command line properties after parsing. */ + private Properties parsedProperties = null; + + /** Holds any trailing name=value pairs specified in the free arguments. */ + private Properties trailingProperties = null; + + /** Flag used to indicate that errors should be created for unknown options. False by default. */ + private boolean errorsOnUnknowns = false; + + /** + * Creates a command line options parser from a command line specification. This is passed to this constructor + * as an array of arrays of strings. Each array of strings specifies the command line for a single option. A static + * array may therefore easily be used to configure the command line parser in a single method call with an easily + * readable format. + * + * <p/>Each array of strings must be 2, 3, 4 or 5 elements long. If any of the last three elements are missing they + * are assumed to be null. The elements specify the following parameters: + * <ol> + * <li>The name of the option without the leading '-'. For example, "file". To specify the format of the 'free' + * arguments use the option names "1", "2", ... and so on. + * <li>The option comment. A line of text describing the usage of the option. For example, "The file to be processed." + * <li>The options argument. This is a very short description of the argument to the option, often a single word + * or a reminder as to the arguments format. When this element is null the option is a flag and does not + * accept any arguments. For example, "filename" or "(unix | windows)" or null. The actual text specified + * is only used to print in the usage message to remind the user of the usage of the option. + * <li>The mandatory flag. When set to "true" an option must always be specified. Any other value, including null, + * means that the option is mandatory. Flags are always mandatory (see class javadoc for explanation of why) so + * this is ignored for flags. + * <li>A regular head describing the format that the argument must take. Ignored if null. + * </ol> + * <p/>An example call to this constructor is: + * + * <pre> + * CommandLineParser commandLine = new CommandLineParser( + * new String[][] {{"file", "The file to be processed. ", "filename", "true"}, + * {"dir", "Directory to store results in. Current dir used if not set.", "out dir"}, + * {"os", "Operating system EOL format to use.", "(windows | unix)", null, "windows\|unix"}, + * {"v", "Verbose mode. Prints information about the processing as it goes."}, + * {"1", "The processing command to run.", "command", "true", "add\|remove\|list"}}); + * </pre> + * + * @param config The configuration as an array of arrays of strings. + */ + public CommandLineParser(String[][] config) + { + // Loop through all the command line option specifications creating details for each in the options map. + for (String[] nextOptionSpec : config) + { + addOption(nextOptionSpec[0], nextOptionSpec[1], (nextOptionSpec.length > 2) ? nextOptionSpec[2] : null, + (nextOptionSpec.length > 3) && ("true".equals(nextOptionSpec[3])), + (nextOptionSpec.length > 4) ? nextOptionSpec[4] : null); + } + } + + /** + * Extracts all name=value pairs from the command line, sets them all as system properties and also returns + * a map of properties containing them. + * + * @param args The command line. + * @param commandLine The command line parser. + * @param properties The properties object to inject all parsed properties into (optional may be <tt>null</tt>). + * + * @return A set of properties containing all name=value pairs from the command line. + */ + public static Properties processCommandLine(String[] args, CommandLineParser commandLine, Properties properties) + { + // Capture the command line arguments or display errors and correct usage and then exit. + Properties options = null; + + try + { + options = commandLine.parseCommandLine(args); + + // Add all the command line options and trailing settings to properties if the optional properties object + // to copy them into has been set. + if (properties != null) + { + commandLine.addTrailingPairsToProperties(properties); + commandLine.addOptionsToProperties(properties); + } + } + catch (IllegalArgumentException e) + { + System.out.println(commandLine.getErrors()); + System.out.println(commandLine.getUsage()); + System.exit(1); + } + + return options; + } + + /** + * Lists all the parsing errors from the most recent parsing in a string. + * + * @return All the parsing errors from the most recent parsing. + */ + public String getErrors() + { + // Return the empty string if there are no errors. + if (parsingErrors.isEmpty()) + { + return ""; + } + + // Concatenate all the parsing errors together. + String result = ""; + + for (String s : parsingErrors) + { + result += s; + } + + return result; + } + + /** + * Lists the properties set from the most recent parsing or an empty string if no parsing has been done yet. + * + * @return The properties set from the most recent parsing or an empty string if no parsing has been done yet. + */ + public String getOptionsInForce() + { + // Check if there are no properties to report and return and empty string if so. + if (parsedProperties == null) + { + return ""; + } + + // List all the properties. + String result = "Options in force:\n"; + + for (Map.Entry<Object, Object> property : parsedProperties.entrySet()) + { + result += property.getKey() + " = " + property.getValue() + "\n"; + } + + return result; + } + + /** + * Generates a usage string consisting of the name of each option and each options argument description and + * comment. + * + * @return A usage string for all the options. + */ + public String getUsage() + { + String result = "Options:\n"; + + int optionWidth = 0; + int argumentWidth = 0; + + // Calculate the column widths required for aligned layout. + for (CommandLineOption optionInfo : optionMap.values()) + { + int oWidth = optionInfo.option.length(); + int aWidth = (optionInfo.argument != null) ? (optionInfo.argument.length()) : 0; + + optionWidth = (oWidth > optionWidth) ? oWidth : optionWidth; + argumentWidth = (aWidth > argumentWidth) ? aWidth : argumentWidth; + } + + // Print usage on each of the command line options. + for (CommandLineOption optionInfo : optionMap.values()) + { + String argString = ((optionInfo.argument != null) ? (optionInfo.argument) : ""); + String optionString = optionInfo.option; + + argString = rightPad(argString, " ", argumentWidth); + optionString = rightPad(optionString, " ", optionWidth); + + result += "-" + optionString + " " + argString + " " + optionInfo.comment + "\n"; + } + + return result; + } + + /** + * Right pads a string with a given string to a given size. This method will repeat the padder string as many + * times as is necessary until the exact specified size is reached. If the specified size is less than the size + * of the original string then the original string is returned unchanged. + * + * <pre> + * Example1 - original string "cat", padder string "white", size 8 gives "catwhite". + * Example2 - original string "cat", padder string "white", size 15 gives "catwhitewhitewh". + * Example3 - original string "cat", padder string "white", size 2 gives "cat". + * </pre> + * + * @param stringToPad The original string. + * @param padder The string to pad onto the original string. + * @param size The required size of the new string. + * + * @return The newly padded string. + */ + public static String rightPad(String stringToPad, String padder, int size) + { + if (padder.length() == 0) + { + return stringToPad; + } + + StringBuffer strb = new StringBuffer(stringToPad); + StringCharacterIterator sci = new StringCharacterIterator(padder); + + while (strb.length() < size) + { + for (char ch = sci.first(); ch != CharacterIterator.DONE; ch = sci.next()) + { + if (strb.length() < size) + { + strb.append(String.valueOf(ch)); + } + } + } + + return strb.toString(); + } + + /** + * Control the behaviour of the errors on unkowns reporting. When turned on this reports all unkowns options + * as errors. When turned off, all unknowns are simply ignored. + * + * @param errors The setting of the errors on unkown flag. True to turn it on. + */ + public void setErrorsOnUnknowns(boolean errors) + { + errorsOnUnknowns = errors; + } + + /** + * Parses a set of command line arguments into a set of properties, keyed by the argument flag. The free arguments + * are keyed by integers as strings starting at "1" and then "2", ... and so on. + * + * <p/>See the class level comment for a description of the parsing rules. + * + * @param args The command line arguments. + * + * @return The arguments as a set of properties. + * + * @throws IllegalArgumentException If the command line cannot be parsed against its specification. If this exception + * is thrown a call to {@link #getErrors} will provide a diagnostic of the command + * line errors. + */ + public Properties parseCommandLine(String[] args) throws IllegalArgumentException + { + Properties options = new Properties(); + + // Used to keep count of the current 'free' argument. + int free = 1; + + // Used to indicate that the most recently parsed option is expecting arguments. + boolean expectingArgs = false; + + // The option that is expecting arguments from the next element of the command line. + String optionExpectingArgs = null; + + // Used to indicate that the most recently parsed option is a duplicate and should be ignored. + // boolean ignore = false; + + // Create the regular head matcher for the command line options. + String regexp = "^("; + int optionsAdded = 0; + + for (Iterator<String> i = optionMap.keySet().iterator(); i.hasNext();) + { + String nextOption = i.next(); + + // Check that the option is not a free argument definition. + boolean notFree = false; + + try + { + Integer.parseInt(nextOption); + } + catch (NumberFormatException e) + { + notFree = true; + } + + // Add the option to the regular head matcher if it is not a free argument definition. + if (notFree) + { + regexp += nextOption + (i.hasNext() ? "|" : ""); + optionsAdded++; + } + } + + // There has to be more that one option in the regular head or else the compiler complains that the close + // cannot be nullable if the '?' token is used to make the matched option string optional. + regexp += ")" + ((optionsAdded > 0) ? "?" : "") + "(.*)"; + Pattern pattern = Pattern.compile(regexp); + + // Loop through all the command line arguments. + for (String arg1 : args) + { + // Check if the next command line argument begins with a '-' character and is therefore the start of + // an option. + if (arg1.startsWith("-")) + { + // Extract the value of the option without the leading '-'. + String arg = arg1.substring(1); + + // Match up to the longest matching option. + optionMatcher = pattern.matcher(arg); + optionMatcher.matches(); + + String matchedOption = optionMatcher.group(1); + + // Match any argument directly appended onto the longest matching option. + String matchedArg = optionMatcher.group(2); + + // Check that a known option was matched. + if ((matchedOption != null) && !"".equals(matchedOption)) + { + // Get the command line option information for the matched option. + CommandLineOption optionInfo = optionMap.get(matchedOption); + + // Check if this option is expecting arguments. + if (optionInfo.expectsArgs) + { + // The option is expecting arguments so swallow the next command line argument as an + // argument to this option. + expectingArgs = true; + optionExpectingArgs = matchedOption; + + // In the mean time set this options argument to the empty string in case no argument is ever + // supplied. + // options.put(matchedOption, ""); + } + + // Check if the option was matched on its own and is a flag in which case set that flag. + if ("".equals(matchedArg) && !optionInfo.expectsArgs) + { + options.put(matchedOption, "true"); + } + // The option was matched as a substring with its argument appended to it or is a flag that is + // condensed together with other flags. + else if (!"".equals(matchedArg)) + { + // Check if the option is a flag and therefore is allowed to be condensed together + // with other flags. + if (!optionInfo.expectsArgs) + { + // Set the first matched flag. + options.put(matchedOption, "true"); + + // Repeat the longest matching process on the remainder but ensure that the remainder + // consists only of flags as only flags may be condensed together in this fashion. + do + { + // Match the remainder against the options. + optionMatcher = pattern.matcher(matchedArg); + optionMatcher.matches(); + + matchedOption = optionMatcher.group(1); + matchedArg = optionMatcher.group(2); + + // Check that an option was matched. + if (matchedOption != null) + { + // Get the command line option information for the next matched option. + optionInfo = optionMap.get(matchedOption); + + // Ensure that the next option is a flag or raise an error if not. + if (optionInfo.expectsArgs) + { + parsingErrors.add("Option " + matchedOption + " cannot be combined with flags.\n"); + } + + options.put(matchedOption, "true"); + } + // The remainder could not be matched against a flag it is either an unknown flag + // or an illegal argument to a flag. + else + { + parsingErrors.add("Illegal argument to a flag in the option " + arg + "\n"); + + break; + } + } + // Continue until the remainder of the argument has all been matched with flags. + while (!"".equals(matchedArg)); + } + // The option is expecting an argument, so store the unmatched portion against it + // as its argument. + else + { + // Check the arguments format is correct against any specified format. + checkArgumentFormat(optionInfo, matchedArg); + + // Store the argument against its option (regardless of its format). + options.put(matchedOption, matchedArg); + + // The argument to this flag has already been supplied to it. Do not swallow the + // next command line argument as an argument to this flag. + expectingArgs = false; + } + } + } + else // No matching option was found. + { + // Add this to the list of parsing errors if errors on unkowns is being used. + if (errorsOnUnknowns) + { + parsingErrors.add("Option " + matchedOption + " is not a recognized option.\n"); + } + } + } + // The command line argument did not being with a '-' so it is an argument to the previous flag or it + // is a free argument. + else + { + // Check if a previous flag is expecting to swallow this next argument as its argument. + if (expectingArgs) + { + // Get the option info for the option waiting for arguments. + CommandLineOption optionInfo = optionMap.get(optionExpectingArgs); + + // Check the arguments format is correct against any specified format. + checkArgumentFormat(optionInfo, arg1); + + // Store the argument against its option (regardless of its format). + options.put(optionExpectingArgs, arg1); + + // Clear the expecting args flag now that the argument has been swallowed. + expectingArgs = false; + optionExpectingArgs = null; + } + // This command line option is not an argument to any option. Add it to the set of 'free' options. + else + { + // Get the option info for the free option, if there is any. + CommandLineOption optionInfo = optionMap.get(Integer.toString(free)); + + if (optionInfo != null) + { + // Check the arguments format is correct against any specified format. + checkArgumentFormat(optionInfo, arg1); + } + + // Add to the list of free options. + options.put(Integer.toString(free), arg1); + + // Move on to the next free argument. + free++; + } + } + } + + // Scan through all the specified options to check that all mandatory options have been set and that all flags + // that were not set are set to false in the set of properties. + for (CommandLineOption optionInfo : optionMap.values()) + { + // Check if this is a flag. + if (!optionInfo.expectsArgs) + { + // Check if the flag is not set in the properties and set it to false if so. + if (!options.containsKey(optionInfo.option)) + { + options.put(optionInfo.option, "false"); + } + } + // Check if this is a mandatory option and was not set. + else if (optionInfo.mandatory && !options.containsKey(optionInfo.option)) + { + // Create an error for the missing option. + parsingErrors.add("Option " + optionInfo.option + " is mandatory but not was not specified.\n"); + } + } + + // Check if there were any errors. + if (!parsingErrors.isEmpty()) + { + // Throw an illegal argument exception to signify that there were parsing errors. + throw new IllegalArgumentException(); + } + + // Convert any name/value pairs in the free arguments into properties in the parsed options. + trailingProperties = takeFreeArgsAsProperties(options, 1); + + parsedProperties = options; + + return options; + } + + /** + * If a command line has been parsed, calling this method sets all of its free arguments that were name=value pairs + * on the specified properties. + * + * @param properties The property set to add the name=value pairs to. + */ + public void addTrailingPairsToProperties(Properties properties) + { + if (trailingProperties != null) + { + for (Object propKey : trailingProperties.keySet()) + { + String name = (String) propKey; + String value = trailingProperties.getProperty(name); + + properties.setProperty(name, value); + } + } + } + + /** + * If a command line has been parsed, calling this method sets all of its options that were set to the specified + * properties. + * + * @param properties The property set to the options to. + */ + public void addOptionsToProperties(Properties properties) + { + if (parsedProperties != null) + { + for (Object propKey : parsedProperties.keySet()) + { + String name = (String) propKey; + String value = parsedProperties.getProperty(name); + + // This filters out all trailing items. + if (!name.matches("^[0-9]+$")) + { + properties.setProperty(name, value); + } + } + } + } + + /** + * Resets this command line parser after it has been used to parse a command line. This method will only need + * to be called to use this parser a second time which is not likely seeing as a command line is usually only + * specified once. However, it is exposed as a public method for the rare case where this may be done. + * + * <p/>Cleans the internal state of this parser, removing all stored errors and information about the options in + * force. + */ + public void reset() + { + parsingErrors = new ArrayList<String>(); + parsedProperties = null; + } + + /** + * Adds the option to list of available command line options. + * + * @param option The option to add as an available command line option. + * @param comment A comment for the option. + * @param argument The text that appears after the option in the usage string. + * @param mandatory When true, indicates that this option is mandatory. + * @param formatRegexp The format that the argument must take, defined as a regular head. + */ + protected void addOption(String option, String comment, String argument, boolean mandatory, String formatRegexp) + { + // Check if usage text has been set in which case this option is expecting arguments. + boolean expectsArgs = (!((argument == null) || argument.equals(""))); + + // Add the option to the map of command line options. + CommandLineOption opt = new CommandLineOption(option, expectsArgs, comment, argument, mandatory, formatRegexp); + optionMap.put(option, opt); + } + + /** + * Converts the free arguments into property declarations. After parsing the command line the free arguments + * are numbered from 1, such that the parsed properties contain values for the keys "1", "2", ... This method + * converts any free arguments declared using the 'name=value' syntax into properties with key 'name', value + * 'value'. + * + * <p/>For example the comand line: + * <pre> + * ... debug=true + * </pre> + * + * <p/>After parsing has properties: + * <pre>[[1, debug=true]]</pre> + * + * <p/>After applying this method the properties are: + * <pre>[[1, debug=true], [debug, true]]</pre> + * + * @param properties The parsed command line properties. + * @param from The free argument index to convert to properties from. + * + * @return The parsed command line properties, with free argument name value pairs too. + */ + private Properties takeFreeArgsAsProperties(Properties properties, int from) + { + Properties result = new Properties(); + + for (int i = from; true; i++) + { + String nextFreeArg = properties.getProperty(Integer.toString(i)); + + // Terminate the loop once all free arguments have been consumed. + if (nextFreeArg == null) + { + break; + } + + // Split it on the =, strip any whitespace and set it as a system property. + String[] nameValuePair = nextFreeArg.split("="); + + if (nameValuePair.length == 2) + { + result.setProperty(nameValuePair[0], nameValuePair[1]); + } + } + + return result; + } + + /** + * Checks the format of an argument to an option against its specified regular head format if one has + * been set. Any errors are added to the list of parsing errors. + * + * @param optionInfo The command line option information for the option which is havings its argument checked. + * @param matchedArg The string argument to the option. + */ + private void checkArgumentFormat(CommandLineOption optionInfo, String matchedArg) + { + // Check if this option enforces a format for its argument. + if (optionInfo.argumentFormatRegexp != null) + { + Pattern pattern = Pattern.compile(optionInfo.argumentFormatRegexp); + Matcher argumentMatcher = pattern.matcher(matchedArg); + + // Check if the argument does not meet its required format. + if (!argumentMatcher.matches()) + { + // Create an error for this badly formed argument. + parsingErrors.add("The argument to option " + optionInfo.option + " does not meet its required format.\n"); + } + } + } + + /** + * Holds information about a command line options. This includes what its name is, whether or not it is a flag, + * whether or not it is mandatory, what its user comment is, what its argument reminder text is and what its + * regular head format is. + * + * <p><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities <th> Collaborations + * <tr><td> Hold details of a command line option. + * </table> + * + * @author Rupert Smith + */ + protected class CommandLineOption + { + /** Holds the text for the flag to match this argument with. */ + public String option = null; + + /** Holds a string describing how to use this command line argument. */ + public String argument = null; + + /** Flag that determines whether or not this command line argument can take arguments. */ + public boolean expectsArgs = false; + + /** Holds a short comment describing what this command line argument is for. */ + public String comment = null; + + /** Flag that determines whether or not this is an mandatory command line argument. */ + public boolean mandatory = false; + + /** A regular head describing what format the argument to this option muist have. */ + public String argumentFormatRegexp = null; + + /** + * Create a command line option object that holds specific information about a command line option. + * + * @param option The text that matches the option. + * @param expectsArgs Whether or not the option expects arguments. It is a flag if this is false. + * @param comment A comment explaining how to use this option. + * @param argument A short reminder of the format of the argument to this option/ + * @param mandatory Set to true if this option is mandatory. + * @param formatRegexp The regular head that the argument to this option must meet to be valid. + */ + public CommandLineOption(String option, boolean expectsArgs, String comment, String argument, boolean mandatory, + String formatRegexp) + { + this.option = option; + this.expectsArgs = expectsArgs; + this.comment = comment; + this.argument = argument; + this.mandatory = mandatory; + this.argumentFormatRegexp = formatRegexp; + } + } +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/ContextualProperties.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/ContextualProperties.java new file mode 100644 index 0000000000..14de96d165 --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/ContextualProperties.java @@ -0,0 +1,494 @@ +/* + * + * 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.qpid.junit.extensions.util; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Properties; + +/** + * ContextualProperties is an extension of {@link java.util.Properties} that automatically selects properties based on an + * environment parameter (defined by the system property {@link #ENV_SYS_PROPERTY}), the name of a class, plus a modifier + * (which can be used to name a method of a class) and a property key. It also supports the definition of arrays of + * property values using indexes. The properties are searched in the following order until a match is found: + * + * <ol> + * <li>environment + class name with package name + modifier + key + * <li>environment + class name with package name + key + * <li>environment + key + * <li>class name with package name + modifier + key + * <li>class name with package name + key + * <li>key + * </ol> + * + * <p>To create arrays of property values add index numbers onto the end of the property keys. An array of string values + * will be created with the elements of the array set to the value of the property at the matching index. Ideally the + * index values will be contiguous, starting at 0. This does not need to be the case however. If an array definition + * begins at index n, and ends at index m, Then an array big enough to hold m + 1 elements will be created and populated + * with values from n to m. Values before n and any missing values between n and m will be null in the array. + * + * <p>To give an example, suppose you have two different environments 'DEVELOPMENT' and 'PRODUCTION' and they each need + * the same properties but set to different values for each environment and some properties the same in both, you could + * create a properties file like: + * + * <p><code> + * # Project configuration properties file.<br/> + * <br/> + * # These properties are environment specific.<br/> + * DEVELOPMENT.debug=true<br/> + * PRODUCTION.debug=false<br/> + * <br/> + * # Always debug MyClass in all environments but not the myMethod method.<br/> + * MyClass.debug=true<br/> + * MyClass.myMethod.debug=false<br/> + * <br/> + * # Set up an array of my ten favourite animals. Leave elements 3 to 8 as null as I haven't decided on them yet.<br/> + * animals.0=cat<br/> + * animals.1=dog<br/> + * animals.2=elephant<br/> + * animals.9=lion<br/> + * <br/> + * # This is a default value that will be used when the environment is not known.<br/> + * debug=false<br/> + * </code> + * + * <p>The most specific definition of a property is searched for first moving out to the most general. This allows + * general property defaults to be set and then overiden for specific uses by some classes and modifiers. + * + * <p>A ContextualProperties object can be loaded in the same way as a java.utils.Properties. A recommended way to do + * this that does not assume that the properties file is a file (it could be in a jar) is to load the properties from the + * url for the resource lookup up on the classpath: + * + * <p><code> + * Properties configProperties = new ContextualProperties();<br/> + * configProperties.load(this.getClass().getClassLoader().getResourceAsStream("config.properties"));<br/> + * </code> + * + * <p>EnvironmentProperties will load the 'DEVELOPMENT.debug' property or 'PROUCTION.debug' property based on the setting + * of the system environment property. If a matching property for the environment cannot be found then the simple property + * name without the environment pre-pended onto it will be used instead. This 'use of default environments' behaviour is + * turned on initially but it can be disabled by calling the {@link #useDefaultEnvironments} method. + * + * <p>When a property matching a key cannot be found then the property accessor methods will always return null. If a + * default value for a property exists but the 'use of default environments' behavious prevents it being used then the + * accessor methods will return null. + * + * <p><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities <th> Collaborations + * <tr><td> Automatically select properties dependant on environment, class name and modifier as well as property key. + * <tr><td> Convert indexed properties into arrays. + * </table> + * + * @author Rupert Smith + */ +public class ContextualProperties extends ParsedProperties +{ + /** The name of the system property that is used to define the environment. */ + public static final String ENV_SYS_PROPERTY = "environment"; + + /** + * <p>Holds the iteration count down order. + * + * <p>If e = 4, b = 2, m = 1 then the iteration order or i is 7,6,4 and then if using environment defaults 3,2,0 + * where the accessor key is: + * (i & e != 0 ? environment : "") + (i & b != 0 ? base : "") + (1 + m != 0 ? modifier : "") + key + * + * <p>In other words the presence or otherwise of the three least significant bits when counting down from 7 + * specifies which of the environment, base and modifier are to be included in the key where the environment, base + * and modifier stand for the bits in positions 2, 1 and 0. The numbers 5 and 1 are missed out of the count because + * they stand for the case where the modifier is used without the base which is not done. + */ + private static final int[] ORDER = new int[] { 7, 6, 4, 3, 2, 0 }; + + /** + * Defines the point in the iteration count order below which the 'use environment defaults' feature is being used. + */ + private static final int ENVIRONMENT_DEFAULTS_CUTOFF = 4; + + /** Defines the bit representation for the environment in the key ordering. See {@link #ORDER}. */ + private static final int E = 4; + + /** Defines the bit representation for the base in the key ordering. See {@link #ORDER}. */ + private static final int B = 2; + + /** Defines the bit representation for the modifier in the key ordering. See {@link #ORDER}. */ + private static final int M = 1; + + /** Used to hold the value of the environment system property. */ + private String environment; + + /** Used to indicate that the 'use of defaults' behaviour should be used. */ + private boolean useDefaults = true; + + /** Used to hold all the array properties. This is a mapping from property names to ArrayLists of Strings. */ + protected Map arrayProperties = new HashMap(); + + /** + * Default constructor that builds a ContextualProperties that uses environment defaults. + */ + public ContextualProperties() + { + super(); + + // Keep the value of the system environment property. + environment = System.getProperty(ENV_SYS_PROPERTY); + } + + /** + * Creates a ContextualProperties that uses environment defaults and is initialized with the specified properties. + * + * @param props The properties to initialize this with. + */ + public ContextualProperties(Properties props) + { + super(props); + + // Keep the value of the system environment property. + environment = System.getProperty(ENV_SYS_PROPERTY); + + // Extract any array properties as arrays. + createArrayProperties(); + } + + /** + * Parses an input stream as properties. + * + * @param inStream The input stream to read the properties from. + * + * @exception IOException If there is an IO error during reading from the input stream. + */ + public void load(InputStream inStream) throws IOException + { + super.load(inStream); + + // Extract any array properties as arrays. + createArrayProperties(); + } + + /** + * Tells this environment aware properties object whether it should use default environment properties without a + * pre-pended environment when a property for the current environment cannot be found. + * + * @param flag True to use defaults, false to not use defaults. + */ + public void useDefaultEnvironments(boolean flag) + { + useDefaults = flag; + } + + /** + * Looks up a property value relative to the environment, callers class and method. The default environment will be + * checked for a matching property if defaults are being used. In order to work out the callers class and method this + * method throws an exception and then searches one level up its stack frames. + * + * @param key The property key. + * + * @return The value of this property searching from the most specific definition (environment, class, method, key) + * to the most general (key only), unless use of default environments is turned off in which case the most general + * proeprty searched is (environment, key). + */ + public String getProperty(String key) + { + // Try to get the callers class name and method name by examing the stack. + String className = null; + String methodName = null; + + // Java 1.4 onwards only. + /*try + { + throw new Exception(); + } + catch (Exception e) + { + StackTraceElement[] stack = e.getStackTrace(); + + // Check that the stack trace contains at least two elements, one for this method and one for the caller. + if (stack.length >= 2) + { + className = stack[1].getClassName(); + methodName = stack[1].getMethodName(); + } + }*/ + + // Java 1.5 onwards only. + StackTraceElement[] stack = Thread.currentThread().getStackTrace(); + + // Check that the stack trace contains at least two elements, one for this method and one for the caller. + if (stack.length >= 2) + { + className = stack[1].getClassName(); + methodName = stack[1].getMethodName(); + } + + // Java 1.3 and before? Not sure, some horrible thing that parses the text spat out by printStackTrace? + + return getProperty(className, methodName, key); + } + + /** + * Looks up a property value relative to the environment, base class and modifier. The default environment will be + * checked for a matching property if defaults are being used. + * + * @param base An object of the class to retrieve properties relative to. + * @param modifier The modifier (which may stand for a method of the class). + * @param key The property key. + * + * @return The value of this property searching from the most specific definition (environment, class, modifier, key) + * to the most general (key only), unless use of default environments is turned off in which case the most general + * property searched is (environment, key). + */ + public String getProperty(Object base, String modifier, String key) + { + return getProperty(base.getClass().getName(), modifier, key); + } + + /** + * Looks up a property value relative to the environment, base class and modifier. The default environment will be + * checked for a matching property if defaults are being used. + * + * @param base The name of the class to retrieve properties relative to. + * @param modifier The modifier (which may stand for a method of the class). + * @param key The property key. + * + * @return The value of this property searching from the most specific definition (environment, class, modifier, key) + * to the most general (key only), unless use of default environments is turned off in which case the most general + * property searched is (environment, key). + */ + public String getProperty(String base, String modifier, String key) + { + String result = null; + + // Loop over the key orderings, from the most specific to the most general, until a matching value is found. + for (Iterator i = getKeyIterator(base, modifier, key); i.hasNext();) + { + String nextKey = (String) i.next(); + + result = super.getProperty(nextKey); + + if (result != null) + { + break; + } + } + + return result; + } + + /** + * Looks up an array property value relative to the environment, callers class and method. The default environment + * will be checked for a matching array property if defaults are being used. In order to work out the callers class + * and method this method throws an exception and then searches one level up its stack frames. + * + * @param key The property key. + * + * @return The array value of this indexed property searching from the most specific definition (environment, class, + * method, key) to the most general (key only), unless use of default environments is turned off in which + * case the most general proeprty searched is (environment, key). + */ + public String[] getProperties(String key) + { + // Try to get the callers class name and method name by throwing an exception an searching the stack frames. + String className = null; + String methodName = null; + + /* Java 1.4 onwards only. + try { + throw new Exception(); + } catch (Exception e) { + StackTraceElement[] stack = e.getStackTrace(); + // Check that the stack trace contains at least two elements, one for this method and one for the caller. + if (stack.length >= 2) { + className = stack[1].getClassName(); + methodName = stack[1].getMethodName(); + } + }*/ + return getProperties(className, methodName, key); + } + + /** + * Looks up an array property value relative to the environment, base class and modifier. The default environment will + * be checked for a matching array property if defaults are being used. + * + * @param base An object of the class to retrieve properties relative to. + * @param modifier The modifier (which may stand for a method of the class). + * @param key The property key. + * + * @return The array value of this indexed property searching from the most specific definition (environment, class, + * modifier, key) to the most general (key only), unless use of default environments is turned off in which + * case the most general proeprty searched is (environment, key). + */ + public String[] getProperties(Object base, String modifier, String key) + { + return getProperties(base.getClass().getName(), modifier, key); + } + + /** + * Looks up an array property value relative to the environment, base class and modifier. The default environment will + * be checked for a matching array property if defaults are being used. + * + * @param base The name of the class to retrieve properties relative to. + * @param modifier The modifier (which may stand for a method of the class). + * @param key The property key. + * + * @return The array value of this indexed property searching from the most specific definition (environment, class, + * modifier, key) to the most general (key only), unless use of default environments is turned off in which + * case the most general property searched is (environment, key). + */ + public String[] getProperties(String base, String modifier, String key) + { + String[] result = null; + + // Loop over the key orderings, from the most specific to the most general, until a matching value is found. + for (Iterator i = getKeyIterator(base, modifier, key); i.hasNext();) + { + String nextKey = (String) i.next(); + ArrayList arrayList = (ArrayList) arrayProperties.get(nextKey); + + if (arrayList != null) + { + result = (String[]) arrayList.toArray(new String[] {}); + + break; + } + } + + return result; + } + + /** + * For a given environment, base, modifier and key and setting of the use of default environments feature this + * generates an iterator that walks over the order in which to try and access properties. + * + * <p>See the {@link #ORDER} constant for an explanation of how the key ordering is generated. + * + * @param base The name of the class to retrieve properties relative to. + * @param modifier The modifier (which may stand for a method of the class). + * @param key The property key. + * + * @return An Iterator over String keys defining the order in which properties should be accessed. + */ + protected Iterator getKeyIterator(final String base, final String modifier, final String key) + { + return new Iterator() + { + // The key ordering count always begins at the start of the ORDER array. + private int i = 0; + + public boolean hasNext() + { + return (useDefaults ? ((i < ORDER.length) && (ORDER[i] > ENVIRONMENT_DEFAULTS_CUTOFF)) + : (i < ORDER.length)); + } + + public Object next() + { + // Check that there is a next element and return null if not. + if (!hasNext()) + { + return null; + } + + // Get the next ordering count. + int o = ORDER[i]; + + // Do bit matching on the count to choose which elements to include in the key. + String result = + (((o & E) != 0) ? (environment + ".") : "") + (((o & B) != 0) ? (base + ".") : "") + + (((o & M) != 0) ? (modifier + ".") : "") + key; + + // Increment the iterator to get the next key on the next call. + i++; + + return result; + } + + public void remove() + { + // This method is not supported. + throw new UnsupportedOperationException("remove() is not supported on this key order iterator as " + + "the ordering cannot be changed"); + } + }; + } + + /** + * Scans all the properties in the parent Properties object and creates arrays for any array property definitions. + * + * <p>Array properties are defined with indexes. For example: + * + * <p><code> + * property.1=one<br/> + * property.2=two<br/> + * property.3=three<br/> + * </code> + * + * <p>Note that these properties will be stored as the 'empty string' or "" property array. + * + * <p><code> + * .1=one<br/> + * 2=two<br/> + * </code> + */ + protected void createArrayProperties() + { + // Scan through all defined properties. + for (Object o : keySet()) + { + String key = (String) o; + String value = super.getProperty(key); + + // Split the property key into everything before the last '.' and after it. + int lastDotIndex = key.lastIndexOf('.'); + String keyEnding = key.substring(lastDotIndex + 1, key.length()); + String keyStart = key.substring(0, (lastDotIndex == -1) ? 0 : lastDotIndex); + + // Check if the property key ends in an integer, in which case it is an array property. + int index = 0; + + try + { + index = Integer.parseInt(keyEnding); + } + // The ending is not an integer so its not an array. + catch (NumberFormatException e) + { + // Scan the next property. + continue; + } + + // Check if an array property already exists for this base name and create one if not. + ArrayList propArray = (ArrayList) arrayProperties.get(keyStart); + + if (propArray == null) + { + propArray = new ArrayList(); + arrayProperties.put(keyStart, propArray); + } + + // Add the new property value to the array property for the index. + propArray.set(index, value); + } + } +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/MathUtils.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/MathUtils.java new file mode 100644 index 0000000000..7c803294f4 --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/MathUtils.java @@ -0,0 +1,428 @@ +/* + * + * 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.qpid.junit.extensions.util; + +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Mathematical support methods for the toolkit. Caculating averages, variances, min/max for test latencies and + * generating linear/exponential sequences for test size/concurrency ramping up. + * + * <p/>The sequence specifications are of the form [lowest(, ...)(, highest)](,sample=s)(,exp), where round brackets + * enclose optional values. Using this pattern form it is possible to specify a single value, a range of values divided + * into s samples, a range of values divided into s samples but distributed exponentially, or a fixed set of samples. + * + * <p/>The duration arguments are of the form (dD)(hH)(mM)(sS), where round brackets enclose optional values. At least + * one of the optional values must be present. + * + * <p/><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities <th> Collaborations + * <tr><td> Generate a sequene of integers from a sequence specification. + * <tr><td> Parse an encoded duration into milliseconds. + * </table> + * + * @author Rupert Smith + */ +public class MathUtils +{ + /** Used for debugging. */ + // private static final Logger log = Logger.getLogger(MathUtils.class); + + /** The sequence defintion matching regular expression. */ + public static final String SEQUENCE_REGEXP = "^(\\[[0-9:]+\\])(:samples=[0-9]+)?(:exp)?$"; + + /** The regular expression that matches sequence definitions. */ + private static final Pattern SEQUENCE_PATTERN = Pattern.compile(SEQUENCE_REGEXP); + + /** The duration definition matching regular expression. */ + public static final String DURATION_REGEXP = "^(\\d+D)?(\\d+H)?(\\d+M)?(\\d+S)?$"; + + /** The regular expression that matches the duration expression. */ + public static final Pattern DURATION_PATTERN = Pattern.compile(DURATION_REGEXP); + + /** For matching name=value pairs. */ + public static final String NAME_VALUE_REGEXP = "^\\w+=\\w+$"; + + /** For matching name=[value1: value2: ...] variations. */ + public static final String NAME_VALUE_VARIATION_REGEXP = "^\\w+=\\[[\\w:]+\\]$"; + + /** For matching name=[n: ... :m](:sample=s)(:exp) sequences. */ + public static final String NAME_VALUE_SEQUENCE_REGEXP = "^\\w+=(\\[[0-9:]+\\])(:samples=[0-9]+)?(:exp)?$"; + + /** The regular expression that matches name=value pairs and variations. */ + public static final Pattern NAME_VALUE_PATTERN = + Pattern.compile("(" + NAME_VALUE_REGEXP + ")|(" + NAME_VALUE_VARIATION_REGEXP + ")|(" + NAME_VALUE_SEQUENCE_REGEXP + + ")"); + + /** + * Runs a quick test of the sequence generation methods to confirm that they work as expected. + * + * @param args The command line parameters. + */ + public static void main(String[] args) + { + // Use the command line parser to evaluate the command line. + CommandLineParser commandLine = + new CommandLineParser( + new String[][] + { + { "s", "The sequence definition.", "[m:...:n](:sample=s)(:exp)", "true", MathUtils.SEQUENCE_REGEXP }, + { "d", "The duration definition.", "dDhHmMsS", "false", MathUtils.DURATION_REGEXP } + }); + + // Capture the command line arguments or display errors and correct usage and then exit. + ParsedProperties options = null; + + try + { + options = new ParsedProperties(commandLine.parseCommandLine(args)); + } + catch (IllegalArgumentException e) + { + System.out.println(commandLine.getErrors()); + System.out.println(commandLine.getUsage()); + System.exit(-1); + } + + // Extract the command line options. + String sequence = options.getProperty("s"); + String durationString = options.getProperty("d"); + + System.out.println("Sequence is: " + printArray(parseSequence(sequence))); + + if (durationString != null) + { + System.out.println("Duration is: " + parseDuration(durationString)); + } + } + + /** + * Given a start and end and a number of steps this method generates a sequence of evenly spaced integer + * values, starting at the start (inclusive) and finishing at the end (inclusive) with the specified number + * of values in the sequence. The sequence returned may contain less than the specified number where the integer + * range between start and end is too small to contain that many. + * + * <p/>As the results are integers, they will not be perfectly evenly spaced but a best-fit. + * + * @param start The sequence start. + * @param end The sequence end. + * @param steps The number of steps. + * + * @return The sequence. + */ + public static int[] generateSequence(int start, int end, int steps) + { + // Check that there are at least two steps. + if (steps < 2) + { + throw new IllegalArgumentException("There must be at least 2 steps."); + } + + ArrayList<Integer> result = new ArrayList<Integer>(); + + // Calculate the sequence using floating point, then round into the results. + double fStart = start; + double fEnd = end; + double fCurrent = start; + + for (int i = 0; i < steps; i++) + { + fCurrent = (((fEnd - fStart) / (steps - 1)) * i) + fStart; + + roundAndAdd(result, fCurrent); + } + + // Return the results after converting to a primitive array. + return intListToPrimitiveArray(result); + } + + /** + * Given a start and end and a number of steps this method generates a sequence of expontentially spaced integer + * values, starting at the start (inclusive) and finishing at the end (inclusive) with the specified number + * of values in the sequence. An exponentially spaced sequence is one where the ratio between any two consecutive + * numbers in the sequence remains constant. The sequence returned may contain less than the specified number where + * the difference between two consecutive values is too small (this is more likely at the start of the sequence, + * where the values are closer together). + * + * <p/>As the results are integers, they will not be perfectly exponentially spaced but a best-fit. + * + * @param start The sequence start. + * @param end The sequence end. + * @param steps The number of steps. + * + * @return The sequence. + */ + public static int[] generateExpSequence(int start, int end, int steps) + { + // Check that there are at least two steps. + if (steps < 2) + { + throw new IllegalArgumentException("There must be at least 2 steps."); + } + + ArrayList<Integer> result = new ArrayList<Integer>(); + + // Calculate the sequence using floating point, then round into the results. + double fStart = start; + double fEnd = end; + // float fCurrent = start; + double diff = fEnd - fStart; + double factor = java.lang.Math.pow(diff, (1.0f / (steps - 1))); + + for (int i = 0; i < steps; i++) + { + // This is a cheat to get the end exactly on and lose the accumulated rounding error. + if (i == (steps - 1)) + { + result.add(end); + } + else + { + roundAndAdd(result, fStart - 1.0f + java.lang.Math.pow(factor, i)); + } + } + + // Return the results after converting to a primitive array. + return intListToPrimitiveArray(result); + } + + /** + * Parses a string defintion of a sequence into an int array containing the sequence. The definition will conform + * to the regular expression: "^(\[[0-9,]+\])(,samples=[0-9]+)?(,exp)?$". This splits it into three parts, + * an array of integers, the optional sample count and the optional exponential flag. + * + * @param sequenceDef The sequence definition. + * + * @return The sequence as a fully expanded int array. + */ + public static int[] parseSequence(String sequenceDef) + { + // Match the sequence definition against the regular expression for sequences. + Matcher matcher = SEQUENCE_PATTERN.matcher(sequenceDef); + + // Check that the argument is of the right format accepted by this method. + if (!matcher.matches()) + { + throw new IllegalArgumentException("The sequence definition is not in the correct format."); + } + + // Get the total number of matching groups to see if either of the optional samples or exponential flag + // goups were set. + int numGroups = matcher.groupCount(); + + // Split the array of integers on commas. + String intArrayString = matcher.group(1); + + String[] intSplits = intArrayString.split("[:\\[\\]]"); + + int[] sequence = new int[intSplits.length - 1]; + + for (int i = 1; i < intSplits.length; i++) + { + sequence[i - 1] = Integer.parseInt(intSplits[i]); + } + + // Check for the optional samples count. + int samples = 0; + + if ((numGroups > 1) && (matcher.group(2) != null)) + { + String samplesGroup = matcher.group(2); + + String samplesString = samplesGroup.substring(",samples=".length()); + samples = Integer.parseInt(samplesString); + } + + // Check for the optional exponential flag. + boolean expFlag = false; + + if ((numGroups > 2) && (matcher.group(3) != null)) + { + expFlag = true; + } + + // If there is a sample count and 2 or more sequence values defined, then generate the sequence from the first + // and last sequence values. + if ((samples != 0) && (sequence.length >= 2)) + { + int start = sequence[0]; + int end = sequence[sequence.length - 1]; + + if (!expFlag) + { + sequence = generateSequence(start, end, samples); + } + else + { + sequence = generateExpSequence(start, end, samples); + } + } + + return sequence; + } + + /** + * Parses a duration defined as a string, giving a duration in days, hours, minutes and seconds into a number + * of milliseconds equal to that duration. + * + * @param duration The duration definition string. + * + * @return The duration in millliseconds. + */ + public static long parseDuration(String duration) + { + // Match the duration against the regular expression. + Matcher matcher = DURATION_PATTERN.matcher(duration); + + // Check that the argument is of the right format accepted by this method. + if (!matcher.matches()) + { + throw new IllegalArgumentException("The duration definition is not in the correct format."); + } + + // This accumulates the duration. + long result = 0; + + int numGroups = matcher.groupCount(); + + // Extract the days. + if (numGroups >= 1) + { + String daysString = matcher.group(1); + result += + (daysString == null) + ? 0 : (Long.parseLong(daysString.substring(0, daysString.length() - 1)) * 24 * 60 * 60 * 1000); + } + + // Extract the hours. + if (numGroups >= 2) + { + String hoursString = matcher.group(2); + result += + (hoursString == null) ? 0 + : (Long.parseLong(hoursString.substring(0, hoursString.length() - 1)) * 60 * 60 * 1000); + } + + // Extract the minutes. + if (numGroups >= 3) + { + String minutesString = matcher.group(3); + result += + (minutesString == null) + ? 0 : (Long.parseLong(minutesString.substring(0, minutesString.length() - 1)) * 60 * 1000); + } + + // Extract the seconds. + if (numGroups >= 4) + { + String secondsString = matcher.group(4); + result += + (secondsString == null) ? 0 : (Long.parseLong(secondsString.substring(0, secondsString.length() - 1)) * 1000); + } + + return result; + } + + /** + * Pretty prints an array of ints as a string. + * + * @param array The array to pretty print. + * + * @return The pretty printed string. + */ + public static String printArray(int[] array) + { + String result = "["; + for (int i = 0; i < array.length; i++) + { + result += array[i]; + result += (i < (array.length - 1)) ? ", " : ""; + } + + result += "]"; + + return result; + } + + /** + * Returns the maximum value in an array of integers. + * + * @param values The array to find the amx in. + * + * @return The max value. + */ + public static int maxInArray(int[] values) + { + if ((values == null) || (values.length == 0)) + { + throw new IllegalArgumentException("Cannot find the max of a null or empty array."); + } + + int max = values[0]; + + for (int value : values) + { + max = (max < value) ? value : max; + } + + return max; + } + + /** + * The #toArray methods of collections cannot be used with primitive arrays. This loops over and array list + * of Integers and outputs and array of int. + * + * @param result The array of Integers to convert. + * + * @return An array of int. + */ + private static int[] intListToPrimitiveArray(ArrayList<Integer> result) + { + int[] resultArray = new int[result.size()]; + int index = 0; + for (int r : result) + { + resultArray[index] = result.get(index); + index++; + } + + return resultArray; + } + + /** + * Rounds the specified floating point value to the nearest integer and adds it to the specified list of + * integers, provided it is not already in the list. + * + * @param result The list of integers to add to. + * @param value The new candidate to round and add to the list. + */ + private static void roundAndAdd(ArrayList<Integer> result, double value) + { + int roundedValue = (int) Math.round(value); + + if (!result.contains(roundedValue)) + { + result.add(roundedValue); + } + } +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/ParsedProperties.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/ParsedProperties.java new file mode 100644 index 0000000000..1cc6757675 --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/ParsedProperties.java @@ -0,0 +1,390 @@ +/* + * + * 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.qpid.junit.extensions.util; + +import java.util.Properties; + +/** + * ParsedProperties extends the basic Properties class with methods to extract properties, not as strings but as strings + * parsed into basic types. + * + * <p/><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities <th> Collaborations + * </table> + * + * @author Rupert Smith + */ +public class ParsedProperties extends Properties +{ + /** + * Creates an empty ParsedProperties. + */ + public ParsedProperties() + { + super(); + } + + /** + * Creates a ParsedProperties initialized with the specified properties. + * + * @param props The properties to initialize this with. + */ + public ParsedProperties(Properties props) + { + super(props); + } + + /** + * Helper method for setting system properties to defaults when they are not already set. + * + * @param propname The name of the system property to set. + * @param value The value to set it to. + * + * @return The value of the property after this method call. + */ + public static boolean setSysPropertyIfNull(String propname, boolean value) + { + return Boolean.parseBoolean(setSysPropertyIfNull(propname, Boolean.toString(value))); + } + + /** + * Helper method for setting system properties to defaults when they are not already set. + * + * @param propname The name of the system property to set. + * @param value The value to set it to. + * + * @return The value of the property after this method call. + */ + public static short setSysPropertyIfNull(String propname, short value) + { + return Short.parseShort(setSysPropertyIfNull(propname, Short.toString(value))); + } + + /** + * Helper method for setting system properties to defaults when they are not already set. + * + * @param propname The name of the system property to set. + * @param value The value to set it to. + * + * @return The value of the property after this method call. + */ + public static int setSysPropertyIfNull(String propname, int value) + { + return Integer.parseInt(setSysPropertyIfNull(propname, Integer.toString(value))); + } + + /** + * Helper method for setting system properties to defaults when they are not already set. + * + * @param propname The name of the system property to set. + * @param value The value to set it to. + * + * @return The value of the property after this method call. + */ + public static long setSysPropertyIfNull(String propname, long value) + { + return Long.parseLong(setSysPropertyIfNull(propname, Long.toString(value))); + } + + /** + * Helper method for setting system properties to defaults when they are not already set. + * + * @param propname The name of the system property to set. + * @param value The value to set it to. + * + * @return The value of the property after this method call. + */ + public static float setSysPropertyIfNull(String propname, float value) + { + return Float.parseFloat(setSysPropertyIfNull(propname, Float.toString(value))); + } + + /** + * Helper method for setting system properties to defaults when they are not already set. + * + * @param propname The name of the system property to set. + * @param value The value to set it to. + * + * @return The value of the property after this method call. + */ + public static double setSysPropertyIfNull(String propname, double value) + { + return Double.parseDouble(setSysPropertyIfNull(propname, Double.toString(value))); + } + + /** + * Helper method for setting system properties to defaults when they are not already set. + * + * @param propname The name of the system property to set. + * @param value The value to set it to. + * + * @return The value of the system property after this method call. + */ + public static String setSysPropertyIfNull(String propname, String value) + { + String property = System.getProperty(propname); + + if (property == null) + { + System.setProperty(propname, value); + + return value; + } + else + { + return property; + } + } + + /** + * Helper method for setting properties to defaults when they are not already set. + * + * @param propname The name of the system property to set. + * @param value The value to set it to. + * + * @return The value of the property after this method call. + */ + public boolean setPropertyIfNull(String propname, boolean value) + { + return Boolean.parseBoolean(setPropertyIfNull(propname, Boolean.toString(value))); + } + + /** + * Helper method for setting properties to defaults when they are not already set. + * + * @param propname The name of the system property to set. + * @param value The value to set it to. + * + * @return The value of the property after this method call. + */ + public short setPropertyIfNull(String propname, short value) + { + return Short.parseShort(setPropertyIfNull(propname, Short.toString(value))); + } + + /** + * Helper method for setting properties to defaults when they are not already set. + * + * @param propname The name of the system property to set. + * @param value The value to set it to. + * + * @return The value of the property after this method call. + */ + public int setPropertyIfNull(String propname, int value) + { + return Integer.parseInt(setPropertyIfNull(propname, Integer.toString(value))); + } + + /** + * Helper method for setting properties to defaults when they are not already set. + * + * @param propname The name of the system property to set. + * @param value The value to set it to. + * + * @return The value of the property after this method call. + */ + public long setPropertyIfNull(String propname, long value) + { + return Long.parseLong(setPropertyIfNull(propname, Long.toString(value))); + } + + /** + * Helper method for setting properties to defaults when they are not already set. + * + * @param propname The name of the system property to set. + * @param value The value to set it to. + * + * @return The value of the property after this method call. + */ + public float setPropertyIfNull(String propname, float value) + { + return Float.parseFloat(setPropertyIfNull(propname, Float.toString(value))); + } + + /** + * Helper method for setting properties to defaults when they are not already set. + * + * @param propname The name of the system property to set. + * @param value The value to set it to. + * + * @return The value of the property after this method call. + */ + public double setPropertyIfNull(String propname, double value) + { + return Double.parseDouble(setPropertyIfNull(propname, Double.toString(value))); + } + + /** + * Helper method for setting properties to defaults when they are not already set. + * + * @param propname The name of the system property to set. + * @param value The value to set it to. + * + * @return The value of the property after this method call. + */ + public String setPropertyIfNull(String propname, String value) + { + String property = super.getProperty(propname); + + if (property == null) + { + super.setProperty(propname, value); + + return value; + } + else + { + return property; + } + } + + /** + * Helper method for setting properties. + * + * @param propname The name of the system property to set. + * @param value The value to set it to. + * + * @return The value of the property after this method call. + */ + public boolean setProperty(String propname, boolean value) + { + setProperty(propname, Boolean.toString(value)); + + return value; + } + + /** + * Helper method for setting properties. + * + * @param propname The name of the system property to set. + * @param value The value to set it to. + * + * @return The value of the property after this method call. + */ + public short setProperty(String propname, short value) + { + setProperty(propname, Short.toString(value)); + + return value; + } + + /** + * Helper method for setting properties. + * + * @param propname The name of the system property to set. + * @param value The value to set it to. + * + * @return The value of the property after this method call. + */ + public int setProperty(String propname, int value) + { + setProperty(propname, Integer.toString(value)); + + return value; + } + + /** + * Helper method for setting properties. + * + * @param propname The name of the system property to set. + * @param value The value to set it to. + * + * @return The value of the property after this method call. + */ + public long setProperty(String propname, long value) + { + setProperty(propname, Long.toString(value)); + + return value; + } + + /** + * Helper method for setting properties. + * + * @param propname The name of the system property to set. + * @param value The value to set it to. + * + * @return The value of the property after this method call. + */ + public float setProperty(String propname, float value) + { + setProperty(propname, Float.toString(value)); + + return value; + } + + /** + * Helper method for setting properties. + * + * @param propname The name of the system property to set. + * @param value The value to set it to. + * + * @return The value of the property after this method call. + */ + public double setProperty(String propname, double value) + { + setProperty(propname, Double.toString(value)); + + return value; + } + + /** + * Parses a property as a boolean. + * + * @param propName The property. + * + * @return The property as a boolean, or false if it does not exist. + */ + public boolean getPropertyAsBoolean(String propName) + { + String prop = getProperty(propName); + + return (prop != null) && Boolean.parseBoolean(prop); + } + + /** + * Parses a property as an integer. + * + * @param propName The property. + * + * @return The property as a integer, or null if it does not exist. + */ + public Integer getPropertyAsInteger(String propName) + { + String prop = getProperty(propName); + + return (prop != null) ? new Integer(prop) : null; + } + + /** + * Parses a property as a long. + * + * @param propName The property. + * + * @return The property as a long, or null if it does not exist. + */ + public Long getPropertyAsLong(String propName) + { + String prop = getProperty(propName); + + return (prop != null) ? new Long(prop) : null; + } +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/SizeOf.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/SizeOf.java new file mode 100644 index 0000000000..ecc08770a9 --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/SizeOf.java @@ -0,0 +1,94 @@ +/* + * + * 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.qpid.junit.extensions.util; + +/** + * SizeOf provides a static method that does its best to return an accurate measure of the total amount of memory used by + * the virtual machine. This is calculated as the total memory available to the VM minus the actual amount used by it. + * Before this measurement is taken the garbage collector is run many times until the used memory calculation stabilizes. + * Generally, this trick works quite well to provide an accurate reading, however, it cannot be relied upon to be totally + * accurate. It is also quite slow. + * + * <p><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities <th> Collaborations + * <tr><td> Calculate total memory used. + * </table> + * + * @author Rupert Smith + */ +public class SizeOf +{ + /** Holds a reference to the runtime object. */ + private static final Runtime RUNTIME = Runtime.getRuntime(); + + /** + * Makes 4 calls the {@link #runGCTillStable} method. + */ + public static void runGCTillStableSeveralTimes() + { + // It helps to call Runtime.gc() using several method calls. + for (int r = 0; r < 4; ++r) + { + runGCTillStable(); + } + } + + /** + * Runs the garbage collector until the used memory reading stabilizes. It may run the garbage collector up + * to 500 times. + */ + public static void runGCTillStable() + { + long usedMem1 = usedMemory(), usedMem2 = Long.MAX_VALUE; + + for (int i = 0; (usedMem1 < usedMem2) && (i < 500); ++i) + { + RUNTIME.runFinalization(); + RUNTIME.gc(); + Thread.currentThread().yield(); + + usedMem2 = usedMem1; + usedMem1 = usedMemory(); + } + } + + /** + * Runs the garbage collector until the used memory stabilizes and then measures it. + * + * @return The amount of memory used by the virtual machine. + */ + public static long getUsedMemory() + { + runGCTillStableSeveralTimes(); + + return usedMemory(); + } + + /** + * Returns the amount of memory used by subtracting the free memory from the total available memory. + * + * @return The amount of memory used. + */ + private static long usedMemory() + { + return RUNTIME.totalMemory() - RUNTIME.freeMemory(); + } +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/StackQueue.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/StackQueue.java new file mode 100644 index 0000000000..acc1e2c218 --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/StackQueue.java @@ -0,0 +1,131 @@ +/* + * + * 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.qpid.junit.extensions.util; + +import java.util.EmptyStackException; +import java.util.NoSuchElementException; +import java.util.Queue; +import java.util.Stack; + +/** + * The Stack class in java.util (most unhelpfully) does not implement the Queue interface. This is an adaption of that + * class as a queue. + * + * <p><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities <th> Collaborations + * <tr><td> Turn a stack into a queue. + * </table> + * + * @todo Need to override the add method, and iterator and consider other methods too. They work like LIFO but + * really wany FIFO behaviour accross the whole data structure. + * + * @author Rupert Smith + */ +public class StackQueue<E> extends Stack<E> implements Queue<E> +{ + /** + * Retrieves, but does not remove, the head of this queue. + * + * @return The element at the top of the stack. + */ + public E element() + { + try + { + return super.peek(); + } + catch (EmptyStackException e) + { + NoSuchElementException t = new NoSuchElementException(); + t.initCause(e); + throw t; + } + } + + /** + * Inserts the specified element into this queue, if possible. + * + * @param o The data element to push onto the stack. + * + * @return True if it was added to the stack, false otherwise (this implementation always returns true). + */ + public boolean offer(E o) + { + push(o); + + return true; + } + + /** + * Retrieves, but does not remove, the head of this queue, returning null if this queue is empty. + * + * @return The top element from the stack, or null if the stack is empty. + */ + public E peek() + { + try + { + return super.peek(); + } + catch (EmptyStackException e) + { + return null; + } + } + + /** + * Retrieves and removes the head of this queue, or null if this queue is empty. + * + * @return The top element from the stack, or null if the stack is empty. + */ + public E poll() + { + try + { + return super.pop(); + } + catch (EmptyStackException e) + { + return null; + } + } + + /** + * Retrieves and removes the head of this queue. + * + * @return The top element from the stack, or null if the stack is empty. + * + * @throws NoSuchElementException If the stack is empty so no element can be removed from it. + */ + public E remove() + { + try + { + return super.pop(); + } + catch (EmptyStackException e) + { + NoSuchElementException t = new NoSuchElementException(); + t.initCause(e); + throw t; + } + } +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/TestContextProperties.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/TestContextProperties.java new file mode 100644 index 0000000000..d402077963 --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/TestContextProperties.java @@ -0,0 +1,202 @@ +/* + * + * 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.qpid.junit.extensions.util; + +import java.util.Properties; + +/** + * TestContextProperties is an extension of {@link ParsedProperties} that keeps track of property key/value pairs + * that are used by tests being run under the {@link org.apache.qpid.junit.extensions.TKTestRunner}. To keep the + * test runner notified of configurable test parameters, tests should establish their required property values by + * initiliazing fields or statics or in the constructor, through this class. The tk test runner automatically places + * any additional properties specified on the command line into the this class, and these are held statically. + * + * <p/>Here is an example: + * + * <pre> + * public class MyTestClass extends TestCase { + * ParsedProperties testProps = TestContextProperties.getInstance(); + * private int testParam = testProps.setPropertyIfNull("testParam", 1); + * ... + * </pre> + * + * <p/>This has the effect of setting up the field testParam with the default value of 1, unless it is overridden + * by values passed to the tk test runner. It also notifies the tk test runner of the name and value of the test + * parameter actually used for the test, so that this can be logged in the test output file. + * + * <p/><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities <th> Collaborations + * <tr><td> Log all name/value pairs read or written. + * </table> + * + * @author Rupert Smith + */ +public class TestContextProperties extends ParsedProperties +{ + /** Used for debugging. */ + // Logger log = Logger.getLogger(TestContextProperties.class); + + /** Holds all properties set or read through this property extension class. */ + private Properties accessedProps = new Properties(); + + /** The singleton instance of the test context properties. */ + private static TestContextProperties singleton = null; + + /** + * Default constructor that builds a ContextualProperties that uses environment defaults. + */ + private TestContextProperties() + { + super(); + } + + /** + * Gets the singleton instance of the test context properties. + * + * @return The singleton instance of the test context properties. + */ + public static synchronized ParsedProperties getInstance() + { + if (singleton == null) + { + singleton = new TestContextProperties(); + } + + return singleton; + } + + /** + * Gets the singleton instance of the test context properties, applying a specified set of default properties to + * it, if they are not already set. + * + * @param defaults The defaults to apply for properties not already set. + * + * @return The singleton instance of the test context properties. + */ + public static synchronized ParsedProperties getInstance(Properties defaults) + { + ParsedProperties props = getInstance(); + + for (Object key : defaults.keySet()) + { + String stringKey = (String) key; + String value = defaults.getProperty(stringKey); + + props.setPropertyIfNull(stringKey, value); + } + + return props; + } + + /* + * Creates a ContextualProperties that uses environment defaults and is initialized with the specified properties. + * + * @param props The properties to initialize this with. + */ + /*public TestContextProperties(Properties props) + { + super(); + }*/ + + /** + * Gets all of the properties (with their most recent values) that have been set or read through this class. + * + * @return All of the properties accessed through this class. + */ + public static Properties getAccessedProps() + { + return (singleton == null) ? new Properties() : singleton; + // return accessedProps; + } + + /** + * Looks up a property value relative to the environment, callers class and method. The default environment will be + * checked for a matching property if defaults are being used. The property key/value pair is remembered and made + * available to {@link org.apache.qpid.junit.extensions.TKTestRunner}. + * + * @param key The property key. + * + * @return The value of this property searching from the most specific definition (environment, class, method, key) + * to the most general (key only), unless use of default environments is turned off in which case the most general + * proeprty searched is (environment, key). + */ + public String getProperty(String key) + { + // log.debug("public String getProperty(String key = " + key + "): called"); + + String value = super.getProperty(key); + + if (value != null) + { + accessedProps.setProperty(key, value); + } + + // log.debug("value = " + value); + + return value; + } + + /** + * Calls the <tt>Hashtable</tt> method <code>put</code>. Provided for parallelism with the <tt>getProperty</tt> + * method. Enforces use of strings for property keys and values. The value returned is the result of the + * <tt>Hashtable</tt> call to <code>put</code>. The property key/value pair is remembered and made + * available to {@link org.apache.qpid.junit.extensions.TKTestRunner}. + * + * @param key The key to be placed into this property list. + * @param value The value corresponding to <tt>key</tt>. + * + * @return The previous value of the specified key in this property list, or <code>null</code> if it did not have one. + */ + public synchronized Object setProperty(String key, String value) + { + // log.debug("public synchronized Object setProperty(String key = " + key + ", String value = " + value + "): called"); + + Object result = super.setProperty(key, value); + accessedProps.setProperty(key, value); + + return result; + } + + /** + * Helper method for setting properties to defaults when they are not already set. The property key/value pair is + * remembered and made available to {@link org.apache.qpid.junit.extensions.TKTestRunner}. + * + * @param key The name of the system property to set. + * @param value The value to set it to. + * + * @return The value of the property, which will be the value passed in if it was null, or the existing value otherwise. + */ + public String setPropertyIfNull(String key, String value) + { + // log.debug("public String setPropertyIfNull(String key = " + key + ", String value = " + value + "): called"); + + String result = super.setPropertyIfNull(key, value); + + if (value != null) + { + accessedProps.setProperty(key, result); + } + + // log.debug("result = " + result); + + return result; + } +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/TestUtils.java b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/TestUtils.java new file mode 100644 index 0000000000..553a41ecae --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/TestUtils.java @@ -0,0 +1,54 @@ +/* + * + * 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.qpid.junit.extensions.util; + +/** + * Provides commonly used functions that aid testing. + * + * <p/><table id="crc"><caption>CRC Card</caption> + * <tr><th> Responsibilities <th> Collaborations + * <tr><td> Provide a short pause. + * </table> + * + * @author Rupert Smith + */ +public class TestUtils +{ + /** + * Injects a short pause. The pause may not complete its full length, if the thread is interrupted when waiting. + * In most cases, this will not happen and this method is a vry adequate pause implementation, without the + * need to handle interrupted exceptions. + * + * @param millis The length of the pause in milliseconds. + */ + public static void pause(long millis) + { + try + { + Thread.sleep(millis); + } + catch (InterruptedException e) + { + // Clear the flag and ignore. + Thread.interrupted(); + } + } +} diff --git a/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/package.html b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/package.html new file mode 100644 index 0000000000..f39b892e2d --- /dev/null +++ b/qpid/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/package.html @@ -0,0 +1,27 @@ +<!-- + + 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. + +--> + +<html> +<body> +Provides some helper classes. ContextualProperties allows a hierarchy of properties to be used in properties file with +default overrides. SizeOf takes memeory measurements by stabilizing the garbage collector. +</body> +</html> |