/**
* Generic framework code included with
* <a href="http://www.amazon.com/exec/obidos/tg/detail/-/1861007841/">Expert One-On-One J2EE Design and Development</a>
* by Rod Johnson (Wrox, 2002).
* This code is free to use and modify.
* Please contact <a href="mailto:rod.johnson@interface21.com">rod.johnson@interface21.com</a>
* for commercial support.
*/
package org.springframework.load;
import java.io.PrintWriter;
import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
/**
* Superclass for test suites.
* Test suites manage 1 or more worker threads (instances of
* Test).
* @author Rod Johnson
* @since February 9, 2001
*/
public abstract class AbstractTestSuite implements ConfigurableTest, TestStatus {
//---------------------------------------------------------------------
// Instance data
//---------------------------------------------------------------------
/** Time this test suite started execution */
private long startTime;
/** Maximum pause between passes. Inherited by threads,
* unless they override it */
private long maxPause;
/** Interval between reports to the console */
private long reportInterval;
/** The Test thread objects managed by this class.
* The execution of this object runs all these Tests
*/
private Test[] tests;
/**
*/
private int numTests;
/**
* The number of worker threads
*/
private int numWorkerThreads;
/** The descriptive name for this test suite */
private String name;
private boolean longReports;
private DecimalFormat df = (DecimalFormat) DecimalFormat.getInstance();
/**
* Test fixture shared by all test threads.
* May not always be necessary: ignored if it's null.
*/
private Object fixture;
//---------------------------------------------------------------------
// Constructors
//---------------------------------------------------------------------
/**
* Creates a new AbstractThreadedTest */
public AbstractTestSuite() {
df.applyPattern("###.##");
}
//---------------------------------------------------------------------
// JavaBean properties
//---------------------------------------------------------------------
/**
* Gets the name.
* @return Returns a String
*/
public String getName() {
return name;
}
public final DecimalFormat getDecimalFormat() {
return df;
}
public final void setDoubleFormat(String pattern) {
df.applyPattern(pattern);
}
/**
* Sets the name.
* @param name The name to set
*/
public void setName(String name) {
this.name = name;
}
/**
* Set whether toString methods in test threads should use
* more verbose format
*/
public final void setLongReports(boolean longReports) {
this.longReports = longReports;
}
/**
* @return get whether toString methods in test threads should
* use more verbose format (unless they override this via
* their own bean property)
*/
public final boolean getLongReports() {
return longReports;
}
/**
* Set the fixture that should be available to all tests.
* This is an object that will be set on all tests.
*/
public void setFixture(Object context) {
this.fixture = context;
}
/**
* @return the test fixture, which will be made available to all test
* threads. This enables all test threads to bash the same object.
* This object may be null, as not all tests require a fixture
* (for example, they may obtain a singleton).
*/
public Object getFixture() {
return this.fixture;
}
/**
* @return the number of test passes executed so far
*/
public int getTotalPassCount() {
int passCount = 0;
Test[] tests = getTests();
if (tests != null) {
for (int i = 0; i < tests.length; i++) {
passCount += tests[i].getPasses();
}
}
return passCount;
}
//---------------------------------------------------------------------
// Public methods
//---------------------------------------------------------------------
/**
* Run all the tests
* @param blockTillComplete should we block until the tests are complete?
*/
public void runAllTests(boolean blockTillComplete) {
Test[] tests = getTests();
Thread[] runners = new Thread[tests.length];
int nbTests = tests.length;
// Instantiate the Threads the run them - we do not want our timing to
// be augmented by the instantantion overhead
for (int i = 0; i < nbTests; i++) {
tests[i].reset();
runners[i] = new Thread(tests[i]);
}
for (int i = 0; i < nbTests; i++) {
//System.out.println("Starting thread " + i);
runners[i].start();
}
// Use a Java 1.3 Timer to do periodic reporting
if (this.reportInterval > 0L) {
Timer timer = new Timer();
timer.schedule(new ReportTimerTask(), getReportInterval(), getReportInterval());
System.out.println("Reporting every " + getReportInterval() + "ms");
}
if (blockTillComplete) {
for (int i = 0; i < nbTests; i++) {
try {
runners[i].join();
}
catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
// Always do a final report
report();
} // runAllTests
/**
* Return the tests managed by this suite.
* Asks concrete subclass to create tests if this object
* has not yet fully initialized.
*/
public Test[] getTests() {
if (this.tests == null) {
this.tests = createTests();
if (tests == null)
throw new TestConfigurationException("Must define some tests in " + getClass().getName() + ".createTests");
numWorkerThreads = tests.length;
for (int i = 0; i < tests.length; i++) {
String classname = tests[i].getClass().getName();
classname = classname.substring(classname.lastIndexOf(".") + 1);
if (tests[i].getName() == null)
tests[i].setName(classname + "-" + i);
tests[i].setTestSuite(this);
if (this.fixture != null) {
// If there's a fixture, all test threads must understand fixtures
tests[i].setFixture(this.fixture);
}
}
}
return tests;
} // getTests
/**
* @return the number of threads running tests
*/
public int getThreads() {
if (tests == null)
return numWorkerThreads;
return tests.length;
}
/**
* Clear tests results
*/
public void clearResults() {
Test[] allTests = getTests();
for (int i = 0; i < allTests.length; i++)
allTests[i].reset();
}
//---------------------------------------------------------------------
// Additional bean properties subclasses may want to use
//---------------------------------------------------------------------
public long getMaxPause() {
return maxPause;
}
public void setMaxPause(long maxPause) {
this.maxPause = maxPause;
}
public long getReportInterval() {
return reportInterval;
}
public void setReportInterval(long reportInterval) {
this.reportInterval = reportInterval;
}
public void setReportIntervalSeconds(int reportIntervalSecs) {
this.reportInterval = reportIntervalSecs * 1000L;
}
/** Only use if desired */
public final void setThreads(int numWorkerThreads) {
this.numWorkerThreads = numWorkerThreads;
}
/** Subclasses can use this as a convenience if all their test threads
* have the same number of tests */
public final void setPasses(int numTests) {
this.numTests = numTests;
}
public final int getPasses() {
return numTests;
}
public String toString() {
return "Test suite name='"
+ name
+ "': numTests="
+ numTests
+ "; numberOfWorkerThreads="
+ numWorkerThreads
+ "; maxPause="
+ maxPause;
}
//---------------------------------------------------------------------
// Abstract methods to be implemented by subclasses
//---------------------------------------------------------------------
/**
* Subclasses must implement this method to return
* all the tests they manage.
* Must not return null.
*/
protected abstract Test[] createTests();
//---------------------------------------------------------------------
// Implementation methods
//---------------------------------------------------------------------
/**
* Generate default report to console
*/
public void report() {
report(new PrintWriter(System.out));
}
/**
* Generate a report to this PrintWriter
*/
public void report(PrintWriter pw) {
StringBuffer sb = new StringBuffer();
report(sb);
pw.println(sb.toString());
pw.flush();
}
/**
* Write a descriptive report to this StringBuffer
*/
public void report(StringBuffer sb) {
sb.append("-----------------------------------\n");
Test[] theirTests = getTests();
// Take our own copy so we can sort it
Test[] myTests = new Test[theirTests.length];
System.arraycopy(theirTests, 0, myTests, 0, theirTests.length);
Arrays.sort(myTests, new TestPerformanceComparator());
for (int i = 0; i < myTests.length; i++) {
sb.append(myTests[i]).append("\n");
}
// Now do by group, ignoring default group
// Key is group name, key is a list
HashMap groupsToTests = new HashMap();
for (int i = 0; i < myTests.length; i++) {
if (myTests[i].getGroup() != null) {
List l = (List) groupsToTests.get(myTests[i].getGroup());
if (l == null) {
l = new LinkedList();
groupsToTests.put(myTests[i].getGroup(), l);
}
l.add(myTests[i]);
}
}
for (Iterator itr = groupsToTests.keySet().iterator(); itr.hasNext(); ) {
String name = (String) itr.next();
List l = (List) groupsToTests.get(name);
if (l.size() > 1) {
sb.append(new Stats("Group [" + name + "]", (Test[]) l.toArray(new Test[l.size()])));
}
}
sb.append(new Stats());
sb.append("Free memory=" + Runtime.getRuntime().freeMemory() /8L / 1024L + " Kb");
}
//---------------------------------------------------------------------
// Implementation of TestStatus
//---------------------------------------------------------------------
/**
* @see org.springframework.load.TestStatus#getElapsedTime()
*/
public long getElapsedTime() {
return new Stats().elapsedTime;
}
/**
* @see org.springframework.load.TestStatus#getErrorCount()
*/
public int getErrorCount() {
return new Stats().errors;
}
/**
* @see org.springframework.load.TestStatus#getTestsCompletedCount()
*/
public int getTestsCompletedCount() {
return new Stats().totalHits;
}
/**
* @see org.springframework.load.TestStatus#getTestsPerSecondCount()
*/
public double getTestsPerSecondCount() {
return new Stats().hitsPerSecond;
}
/**
* @see org.springframework.load.TestStatus#getTotalPauseTime()
*/
public long getTotalPauseTime() {
return new Stats().totalPauseTime;
}
/**
* @see org.springframework.load.TestStatus#getTotalWorkingTime()
*/
public long getTotalWorkingTime() {
return new Stats().workingTime;
}
/**
* @see org.springframework.load.TestStatus#getAverageResponseTime()
*/
public int getAverageResponseTime() {
return new Stats().avgResponseTime;
}
/**
* @return whether this test suite is complete (have all tests
* executed?)
*/
public final boolean isComplete() {
for (int i = 0; i < getTests().length; i++) {
if (!getTests()[i].isComplete())
return false;
}
return true;
}
//---------------------------------------------------------------------
// Inner classes
//---------------------------------------------------------------------
/**
* Inner class used in reporting.
* Used to run reports regularly
*/
private class ReportTimerTask extends TimerTask {
/**
* @see Runnable#run()
*/
public void run() {
AbstractTestSuite.this.report();
if (AbstractTestSuite.this.isComplete()) {
cancel();
System.exit(0);
}
}
}
/**
* Collects information about this class.
* Could make this implement TestStatus. Or composite test status?
*/
private class Stats {
public final int totalHits;
public final long totalPauseTime;
public final double hitsPerSecond;
public final int avgResponseTime;
public final int errors;
public final long elapsedTime;
public final long workingTime;
public String description;
public Stats() {
this("All tests", getTests());
}
public Stats(String description, Test[] allTests) {
int totalResponseTimeAvg = 0;
int totalHits = 0;
int errors = 0;
long elapsedTime = 0L;
long workingTime = 0L;
long totalPause = 0L;
double hps = 0.0;
for (int i = 0; i < allTests.length; i++) {
totalResponseTimeAvg += allTests[i].getTargetResponse().getAverageResponseTimeMillis();
hps += allTests[i].getTestsPerSecondCount();
totalHits += allTests[i].getTestsCompletedCount();
totalPause += allTests[i].getTotalPauseTime();
errors += allTests[i].getErrorCount();
elapsedTime += allTests[i].getElapsedTime();
workingTime += allTests[i].getTotalWorkingTime();
}
this.totalHits = totalHits;
this.avgResponseTime = totalResponseTimeAvg / allTests.length;
this.hitsPerSecond = hps;
this.totalPauseTime = totalPause;
this.errors = errors;
this.elapsedTime = elapsedTime;
this.workingTime = workingTime;
this.description = description;
}
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append("[" + description + "]");
sb.append("Total hits=" + totalHits);
sb.append("; HPS=" + df.format(hitsPerSecond));
sb.append("; Average response=" + avgResponseTime + "\n");
return sb.toString();
}
} // class Stats
/**
* Sort in ascending order by hits per second
*/
private class TestPerformanceComparator implements Comparator {
/**
* @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
*/
public int compare(Object o1, Object o2) {
if (! ((o1 instanceof Test) && (o2 instanceof Test)))
return 0;
Test t1 = (Test) o1;
Test t2 = (Test) o2;
int result = (int) (100.0 * (t2.getTestsPerSecondCount() - t1.getTestsPerSecondCount()));
//System.out.println("t1.hps=" + t1.getTestsPerSecondCount() + "t2.hps=" + t2.getTestsPerSecondCount() + "; result is " + result);
return result;
}
}
} // class AbstractTestSuite