From f7bd0e0bcfc4d2603cf735597cd205ba200dcd07 Mon Sep 17 00:00:00 2001 From: Aidan Skinner Date: Thu, 24 Apr 2008 00:06:43 +0000 Subject: QPID-832 copy from M2.x git-svn-id: https://svn.apache.org/repos/asf/incubator/qpid/trunk/qpid@651118 13f79535-47bb-0310-9956-ffa450edef68 --- java/junit-toolkit/pom.xml | 75 ++ .../junit/concurrency/DefaultThreadFactory.java | 48 ++ .../concurrency/PossibleDeadlockException.java | 46 ++ .../qpid/junit/concurrency/TestRunnable.java | 239 +++++++ .../junit/concurrency/ThreadTestCoordinator.java | 485 +++++++++++++ .../qpid/junit/concurrency/ThreadTestExample.java | 145 ++++ .../org/apache/qpid/junit/concurrency/package.html | 7 + .../qpid/junit/extensions/AsymptoticTestCase.java | 303 ++++++++ .../junit/extensions/AsymptoticTestDecorator.java | 170 +++++ .../apache/qpid/junit/extensions/BaseThrottle.java | 98 +++ .../qpid/junit/extensions/BatchedThrottle.java | 94 +++ .../junit/extensions/DurationTestDecorator.java | 199 ++++++ .../qpid/junit/extensions/InstrumentedTest.java | 66 ++ .../qpid/junit/extensions/NullResultPrinter.java | 92 +++ .../ParameterVariationTestDecorator.java | 172 +++++ .../qpid/junit/extensions/ScaledTestDecorator.java | 375 ++++++++++ .../qpid/junit/extensions/SetupTaskAware.java | 55 ++ .../qpid/junit/extensions/SetupTaskHandler.java | 92 +++ .../qpid/junit/extensions/ShutdownHookable.java | 42 ++ .../qpid/junit/extensions/SleepThrottle.java | 81 +++ .../apache/qpid/junit/extensions/TKTestResult.java | 625 ++++++++++++++++ .../apache/qpid/junit/extensions/TKTestRunner.java | 694 ++++++++++++++++++ .../TestRunnerImprovedErrorHandling.java | 131 ++++ .../qpid/junit/extensions/TestThreadAware.java | 49 ++ .../org/apache/qpid/junit/extensions/Throttle.java | 73 ++ .../qpid/junit/extensions/TimingController.java | 175 +++++ .../junit/extensions/TimingControllerAware.java | 43 ++ .../extensions/WrappedSuiteTestDecorator.java | 134 ++++ .../extensions/listeners/CSVTestListener.java | 532 ++++++++++++++ .../extensions/listeners/ConsoleTestListener.java | 264 +++++++ .../junit/extensions/listeners/TKTestListener.java | 132 ++++ .../extensions/listeners/XMLTestListener.java | 400 +++++++++++ .../qpid/junit/extensions/listeners/package.html | 6 + .../org/apache/qpid/junit/extensions/package.html | 12 + .../junit/extensions/util/CommandLineParser.java | 787 +++++++++++++++++++++ .../extensions/util/ContextualProperties.java | 494 +++++++++++++ .../qpid/junit/extensions/util/MathUtils.java | 428 +++++++++++ .../junit/extensions/util/ParsedProperties.java | 390 ++++++++++ .../apache/qpid/junit/extensions/util/SizeOf.java | 94 +++ .../qpid/junit/extensions/util/StackQueue.java | 131 ++++ .../extensions/util/TestContextProperties.java | 202 ++++++ .../qpid/junit/extensions/util/TestUtils.java | 54 ++ .../apache/qpid/junit/extensions/util/package.html | 6 + 43 files changed, 8740 insertions(+) create mode 100644 java/junit-toolkit/pom.xml create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/DefaultThreadFactory.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/PossibleDeadlockException.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/TestRunnable.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/ThreadTestCoordinator.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/ThreadTestExample.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/package.html create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/AsymptoticTestCase.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/AsymptoticTestDecorator.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/BaseThrottle.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/BatchedThrottle.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/DurationTestDecorator.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/InstrumentedTest.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/NullResultPrinter.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/ParameterVariationTestDecorator.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/ScaledTestDecorator.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/SetupTaskAware.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/SetupTaskHandler.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/ShutdownHookable.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/SleepThrottle.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TKTestResult.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TKTestRunner.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TestRunnerImprovedErrorHandling.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TestThreadAware.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/Throttle.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TimingController.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TimingControllerAware.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/WrappedSuiteTestDecorator.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/CSVTestListener.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/ConsoleTestListener.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/TKTestListener.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/XMLTestListener.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/package.html create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/package.html create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/CommandLineParser.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/ContextualProperties.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/MathUtils.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/ParsedProperties.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/SizeOf.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/StackQueue.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/TestContextProperties.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/TestUtils.java create mode 100644 java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/package.html (limited to 'java/junit-toolkit') diff --git a/java/junit-toolkit/pom.xml b/java/junit-toolkit/pom.xml new file mode 100644 index 0000000000..4bf7f8a954 --- /dev/null +++ b/java/junit-toolkit/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + + org.apache.qpid + junit-toolkit + junit-toolkit + 1.0-incubating-M2.1-SNAPSHOT + + jar + + + org.apache.qpid + qpid + 1.0-incubating-M2.1-SNAPSHOT + ../pom.xml + + + + .. + + + + + + + junit + junit + 3.8.1 + + + + log4j + log4j + 1.2.12 + + + + + + + + junit + junit + + + + log4j + log4j + + + + + src/main + src/unittests + + + \ No newline at end of file diff --git a/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/DefaultThreadFactory.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/DefaultThreadFactory.java new file mode 100644 index 0000000000..6c88d019c4 --- /dev/null +++ b/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. + * + *

+ *
CRC Card
Responsibilities Collaborations + *
Create default threads with no specialization. + *
+ * + * @author Rupert Smith + */ +public class DefaultThreadFactory implements ThreadFactory +{ + /** + * Constructs a new Thread. + * + * @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/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/PossibleDeadlockException.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/PossibleDeadlockException.java new file mode 100644 index 0000000000..0bb07a4557 --- /dev/null +++ b/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. + * + *

+ *
CRC Card
Responsibilities Collaborations + *
Signal a possible state of deadlock between coordinated test threads. + *
+ * + * @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/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/TestRunnable.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/TestRunnable.java new file mode 100644 index 0000000000..5bf0c430cd --- /dev/null +++ b/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. + * + *

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. + * + *

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. + * + *

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. + * + *

+ *
CRC Card
Responsibilities Collaborations + *
Wait for another thread to allow this one to continue. + *
Allow another thread to continue. + *
Accumulate error messages. + *
Record exceptions from thread run. + *
Maintain link to thread coordinator. + *
Explicitly mark a thread with an integer id. + *
Maintian a flag to indicate whether or not this thread is waiting on the coordinator. + *
+ * + * @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 true 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 otherWaitIsAllow flag is set, then true is returned when the thread being waited on is found + * to be blocked outside of the thread test coordinator. false 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 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/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/ThreadTestCoordinator.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/ThreadTestCoordinator.java new file mode 100644 index 0000000000..0be0fe37dc --- /dev/null +++ b/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/ThreadTestCoordinator.java @@ -0,0 +1,485 @@ +/* + * + * 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; + +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. + * + *

+ *
CRC Card
Responsibilities Collaborations + *
Accept test threads to coordinate. + *
Allow test threads to send 'allow to continue' signals. + *
Allow test threads to wait on this coordinator for 'allow to continue' signals. + *
Report error messages from test threads. + *
Report exceptions from test threads. + *
Provide method to wait until all test threads have completed. + *
+ * + * @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 = Logger.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 exceptions = new ArrayList(); + + /** + * 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 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 otherWaitIsAllow flag is set, then true is returned when the thread being waited on is found + * to be blocked outside of the thread test coordinator. false 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/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/ThreadTestExample.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/ThreadTestExample.java new file mode 100644 index 0000000000..ef177a4255 --- /dev/null +++ b/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. + * + *

+ *
CRC Card
Responsibilities Collaborations + *
Demo multi-threaded testing. + *
+ * + * @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/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/package.html b/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/package.html new file mode 100644 index 0000000000..4264367690 --- /dev/null +++ b/java/junit-toolkit/src/main/org/apache/qpid/junit/concurrency/package.html @@ -0,0 +1,7 @@ + + +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. + + \ No newline at end of file diff --git a/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/AsymptoticTestCase.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/AsymptoticTestCase.java new file mode 100644 index 0000000000..03e465695e --- /dev/null +++ b/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. + * + *

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. + * + *

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. + * + *

+ *
CRC Card
Responsibilities Collaborations + *
Store the current int parameter value. {@link TKTestResult} and see {@link AsymptoticTestDecorator} too. + *
Invoke parameterized test methods. + *
+ * + * @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 threadLocalMeasurement = + new ThreadLocal() + { + /** + * 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/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/AsymptoticTestDecorator.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/AsymptoticTestDecorator.java new file mode 100644 index 0000000000..e99f904331 --- /dev/null +++ b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/AsymptoticTestDecorator.java @@ -0,0 +1,170 @@ +/* + * + * 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. + * + *

+ *
CRC Card
Responsibilities Collaborations + *
Repeat a test for each of a set of integer parameters. {@link TKTestResult} + *
Repeat a test multiple times. + *
+ *
+ * + * @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); + + 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/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/BaseThrottle.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/BaseThrottle.java new file mode 100644 index 0000000000..e8e203f0a3 --- /dev/null +++ b/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. + * + *

+ *
CRC Card
Responsibilities Collaborations + *
Check against a throttle speed without waiting. + *
+ * + * @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 true + * + * @return true 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 true, false otherwise. The very + * first time this method is called on a throttle, it returns true 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/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/BatchedThrottle.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/BatchedThrottle.java new file mode 100644 index 0000000000..1d00fcf3b6 --- /dev/null +++ b/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. + * + *

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. + * + *

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}. + * + *

+ *
CRC Card
Responsibilities Collaborations + *
Accept throttling rate in operations per second. + *
Inject short pauses, occasionaly, to fill out processing cycles to a specified rate. + *
Check against a throttle speed without waiting. + *
+ * + * @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/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/DurationTestDecorator.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/DurationTestDecorator.java new file mode 100644 index 0000000000..fe1e044e67 --- /dev/null +++ b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/DurationTestDecorator.java @@ -0,0 +1,199 @@ +/* + * + * 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. + * + *

+ *
CRC Card
Responsibilities Collaborations + *
Repeatedly run a test for a fixed length of time. + *
+ * + * @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"); + + // 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; + } + + // Work out when the test should end. + long now = System.nanoTime(); + long end = (duration * 1000000) + now; + + // 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); + } + + // 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. + * + *

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/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/InstrumentedTest.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/InstrumentedTest.java new file mode 100644 index 0000000000..ed792fcd5a --- /dev/null +++ b/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. + * + *

+ *
CRC Card
Responsibilities + *
Report test run time. + *
Report test memory usage. + *
+ * + * @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/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/NullResultPrinter.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/NullResultPrinter.java new file mode 100644 index 0000000000..2ffbcb5bb8 --- /dev/null +++ b/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. + * + *

+ *
CRC Card
Responsibilities Collaborations + *
+ *
+ * + * @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/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/ParameterVariationTestDecorator.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/ParameterVariationTestDecorator.java new file mode 100644 index 0000000000..60ec156354 --- /dev/null +++ b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/ParameterVariationTestDecorator.java @@ -0,0 +1,172 @@ +/* + * + * 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. + * + *

+ *
CRC Card
Responsibilities Collaborations + *
Repeat a test for each of a set of integer parameters. {@link org.apache.qpid.junit.extensions.TKTestResult} + *
Repeat a test multiple times. + *
+ *
+ * + * @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); + + 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/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/ScaledTestDecorator.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/ScaledTestDecorator.java new file mode 100644 index 0000000000..f5e3e1a758 --- /dev/null +++ b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/ScaledTestDecorator.java @@ -0,0 +1,375 @@ +/* + * + * 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. + * + *

+ *
CRC Card
Responsibilities Collaborations + *
Clone a test run into many threads and run them simultaneously. + *
Inform the test results of the start and end of each concurrent test batch. {@link TKTestResult} + *
Inform the test results of the concurrency level. {@link TKTestResult} + *
+ * + * @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(); + + // 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/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/SetupTaskAware.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/SetupTaskAware.java new file mode 100644 index 0000000000..0e8e1879b6 --- /dev/null +++ b/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. + * + *

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. + * + *

+ *
CRC Card
Responsibilities. + *
Handle injection of set up tasks. + *
Handle injection of tear down tasks. + *
+ * + * @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/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/SetupTaskHandler.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/SetupTaskHandler.java new file mode 100644 index 0000000000..00736c59e5 --- /dev/null +++ b/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. + * + *

+ *
CRC Card
Responsibilities Collaborations + *
Handle injection of set up tasks. + *
Handle injection of tear down tasks. + *
Run set up tasks in chain order. + *
Run tear down tasks in reverse chain order. + *
+ * + * @author Rupert Smith + */ +public class SetupTaskHandler implements SetupTaskAware +{ + /** Holds the set up tasks. */ + Queue setups = new LinkedList(); + + /** Holds the tear down tasks. */ + Queue teardowns = new StackQueue(); + + /** + * 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/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/ShutdownHookable.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/ShutdownHookable.java new file mode 100644 index 0000000000..344d7abf82 --- /dev/null +++ b/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. + * + *

+ *
CRC Card
Responsibilities + *
Supply a shutdown hook. + *
+ * + * @author Rupert Smith + */ +public interface ShutdownHookable +{ + /** + * Supplies the shutdown hook. + * + * @return The shut down hook. + */ + public Thread getShutdownHook(); +} diff --git a/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/SleepThrottle.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/SleepThrottle.java new file mode 100644 index 0000000000..f7e350b1c7 --- /dev/null +++ b/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. + * + *

+ *
CRC Card
Responsibilities Collaborations + *
Accept throttling rate in operations per second. + *
Inject short pauses to fill out processing cycles to a specified rate. + *
Check against a throttle speed without waiting. + *
+ * + * @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/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TKTestResult.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TKTestResult.java new file mode 100644 index 0000000000..c9bcf3eb66 --- /dev/null +++ b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TKTestResult.java @@ -0,0 +1,625 @@ +/* + * + * 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. + * + *

+ *
CRC Card
Responsibilities Collaborations + *
Calculate test timings. + *
Inform timing listeners of timings. + *
Inform memory listeners of memory readings. + *
Inform parameters listeners of parameters. + *
Pass the integer parameter to parameterized test cases. + *
Provide verbose test information on test start and end. + *
+ * + * @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 threadLocals = new ThreadLocal(); + + /** 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 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(); + } + + // 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 + { + /*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; + // log.debug("runTime = " + runTime); + + // Reset the test start time to now, to reset the timer for the next result. + startTime = endTime; + + completeTest(testPassed, param, runTime); + } + + /** + * 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. + * + * @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 + { + log.debug("public void completeTest(boolean testPassed, int param, long timeNanos): called"); + log.debug("testResult = " + testResult); + + // 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, timeNanos, 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/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TKTestRunner.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TKTestRunner.java new file mode 100644 index 0000000000..7955a2e2e9 --- /dev/null +++ b/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: + * + *

+ * -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.
+ * 
+ * + *

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. + * + *

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. + * + *

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. + * + *

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. + * + *

Here are some examples: + * + *

+ *
 -c [10:20:30:40:50] 
Runs the test with 10,20,...,50 threads. + *
 -s [1:100]:samples=10 
+ *
Runs the test with ten different size parameters evenly spaced between 1 and 100. + *
 -s [1:1000000]:samples=10:exp 
+ *
Runs the test with ten different size parameters exponentially spaced between 1 and 1000000. + *
 -r 10 
Runs each test ten times. + *
 -d 10H 
Runs the test repeatedly for 10 hours. + *
 -d 1M, -r 10 
+ *
Runs the test repeatedly for 1 minute but only takes a timing sample every 10 test runs. + *
 -r 10, -c [1:5:10:50], -s [100:1000:10000] 
+ *
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) + *
 cache=true 
Passes the 'cache' parameter with value 'true' to the test. + *
 cache=[true:false] 
Runs the test with the 'cache' parameter set to 'true' and 'false'. + *
 cacheSize=[1000:1000000],samples=4,exp 
+ *
Runs the test with the 'cache' parameter set to a series of exponentially increasing sizes. + *
+ * + *

+ *
CRC Card
Responsibilities Collaborations + *
Create the test configuration specified by the command line parameters. + *
+ * + * @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 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 true if the CSV results listener should be attached. + * @param xmlResults true 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 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 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. + * + *

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 parseDecorators(String decorators) + { + List result = new LinkedList(); + 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/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TestRunnerImprovedErrorHandling.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TestRunnerImprovedErrorHandling.java new file mode 100644 index 0000000000..9b4a8707db --- /dev/null +++ b/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. + * + *

+ *
CRC Card
Responsibilities Collaborations + *
+ * + * @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. + */ + protected 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/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TestThreadAware.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TestThreadAware.java new file mode 100644 index 0000000000..aaa773260d --- /dev/null +++ b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TestThreadAware.java @@ -0,0 +1,49 @@ +/* + * + * 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. + * + *

+ *
CRC Card
Responsibilities + *
Set up per thread test fixtures. + *
Clean up per thread test fixtures. + *
+ * + * @author Rupert Smith + */ +public interface TestThreadAware +{ + /** + * Called when a test thread is created. + */ + public void threadSetUp(); + + /** + * Called when a test thread is destroyed. + */ + public void threadTearDown(); +} diff --git a/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/Throttle.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/Throttle.java new file mode 100644 index 0000000000..955e47c25b --- /dev/null +++ b/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. + * + *

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. + * + *

+ *
CRC Card
Responsibilities + *
Accept throttling rate in operations per second. + *
Inject short pauses to fill-out processing cycles to a specified rate. + *
Check against a throttle speed without waiting. + *
+ * + * @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. + * + *

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 true + * + * @return true 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 true, false otherwise. The very + * first time this method is called on a throttle, it returns true as the base case to the above + * self-referential definition. + */ + public boolean checkThrottle(); +} diff --git a/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TimingController.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TimingController.java new file mode 100644 index 0000000000..7b5763f1de --- /dev/null +++ b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TimingController.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; + +/** + * 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. + * + *

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. + * + *

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. + * + *

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. + * + *

+ * public class MyTest extends TestCase implements TimingControllerAware {
+ * ...
+ *
+ *    timingUtils = this.getTimingController();
+ *
+ *    // Do expensive data preparation here...
+ *
+ *    if (timingUtils != null)
+ *        timingUtils.restart();
+ * 
+ * + *
+ * 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;
+ *      }
+ *    }
+ * 
+ * + *

+ *
CRC Card
Responsibilities + *
Allow test timers to be suspended, restarted or reset. + *
Allow tests to register multiple pass/fails and timings. + *
+ * + * @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. + * + * @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; +} diff --git a/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TimingControllerAware.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/TimingControllerAware.java new file mode 100644 index 0000000000..1ccdc7dbad --- /dev/null +++ b/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. + * + *

+ *
CRC Card
Responsibilities + *
Provide timing controller insertion point for tests. + *
+ * + * @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/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/WrappedSuiteTestDecorator.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/WrappedSuiteTestDecorator.java new file mode 100644 index 0000000000..7a1e537b1c --- /dev/null +++ b/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. + * + *

+ *
CRC Card
Responsibilities + *
Provide access to the underlying tests in a suite. + *
+ * + * @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 getAllUnderlyingTests() + { + log.debug("public Collection getAllUnderlyingTests(): called"); + + List tests = new ArrayList(); + + int numTests = countTestCases(); + log.debug("numTests = " + numTests); + + for (int i = 0; i < numTests; i++) + { + tests.add(testAt(i)); + } + + return tests; + } +} diff --git a/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/CSVTestListener.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/CSVTestListener.java new file mode 100644 index 0000000000..a771e08cf7 --- /dev/null +++ b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/CSVTestListener.java @@ -0,0 +1,532 @@ +/* + * + * 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. + * + *

+ *
CRC Card
Responsibilities Collaborations + *
Listen to test events; start, end, fail, error. + *
Listen to test timings. + *
Listen to test memory usage. + *
Listen to parameterized test parameters. + *
Output all test data to a CSV file. + *
+ * + * @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 threadLocalResults = Collections.synchronizedMap(new HashMap()); + + /** 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 timgings. 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; + + /** 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.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; + } + + /** + * 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; + 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; + + // 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("Concurrency Level:, " + concurrencyLevel + ", "); + timingsWriter.write("Total Time:, " + totalTimeMillis + ", "); + 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("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.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; + + /** 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/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/ConsoleTestListener.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/ConsoleTestListener.java new file mode 100644 index 0000000000..5c328a8814 --- /dev/null +++ b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/ConsoleTestListener.java @@ -0,0 +1,264 @@ +/* + * + * 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). + * + *

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. + * + *

+ *
CRC Card
Responsibilities Collaborations + *
Draw dots as each test result completes, at a capped rate. + *
+ * + * @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) + { } + + /** + * 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/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/TKTestListener.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/TKTestListener.java new file mode 100644 index 0000000000..4f08e8bf2d --- /dev/null +++ b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/TKTestListener.java @@ -0,0 +1,132 @@ +/* + * + * 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. + * + *

+ *
CRC Card
Responsibilities + *
Listen to test timings. + *
Listen to test memory usages. + *
Listen to parameterized test parameters. + *
+ * + * @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); + + /** + * 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/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/XMLTestListener.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/XMLTestListener.java new file mode 100644 index 0000000000..a88837e323 --- /dev/null +++ b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/XMLTestListener.java @@ -0,0 +1,400 @@ +/* + * + * 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. + * + *

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. + * + *

+ *
CRC Card
Responsibilities Collaborations + *
Listen to test lifecycle notifications. + *
Listen to test errors and failures. + *
Listen to test timings. + *
Listen to test memory usages. + *
Listen to parameterized test parameters. + *
Responsibilities + *
+ * + * @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 results = new LinkedHashMap(); + // protected List results = new ArrayList(); + + /** + * 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 threadLocalResults = Collections.synchronizedMap(new LinkedHashMap()); + + /** + * 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 results = new ArrayList(); + + /** 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) + { } + + /** + * 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()); + errors = 0; + failures = 0; + runs = 0; + + // Write out the file header. + try + { + writer.write("\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("\n"); + writer.write("\n"); + + for (Result result : results) + { + writer.write(" \n"); + + if (result.error != null) + { + writer.write(" "); + result.error.printStackTrace(new PrintWriter(writer)); + writer.write(" "); + } + else if (result.failure != null) + { + writer.write(" "); + result.failure.printStackTrace(new PrintWriter(writer)); + writer.write(" "); + } + + writer.write(" \n"); + } + + writer.write("\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/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/package.html b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/package.html new file mode 100644 index 0000000000..326d6e176e --- /dev/null +++ b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/listeners/package.html @@ -0,0 +1,6 @@ + + +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. + + \ No newline at end of file diff --git a/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/package.html b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/package.html new file mode 100644 index 0000000000..091dcce08e --- /dev/null +++ b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/package.html @@ -0,0 +1,12 @@ + + +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. + +

The main test runner class is TKTestRunner which can be called with command line parameters to specify how tests +should be run. + + + \ No newline at end of file diff --git a/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/CommandLineParser.java b/java/junit-toolkit/src/main/org/apache/qpid/junit/extensions/util/CommandLineParser.java new file mode 100644 index 0000000000..61c58bf3ba --- /dev/null +++ b/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. + * + *

Some examples command line are: + * + *