/*
* soapUI, copyright (C) 2004-2011 eviware.com
*
* soapUI is free software; you can redistribute it and/or modify it under the
* terms of version 2.1 of the GNU Lesser General Public License as published by
* the Free Software Foundation.
*
* soapUI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details at gnu.org.
*/
package com.eviware.soapui.impl.wsdl.loadtest;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.apache.xmlbeans.XmlException;
import com.eviware.soapui.SoapUI;
import com.eviware.soapui.config.LoadTestConfig;
import com.eviware.soapui.config.LoadTestLimitTypesConfig;
import com.eviware.soapui.config.SecurityTestConfig;
import com.eviware.soapui.config.TestCaseConfig;
import com.eviware.soapui.impl.wsdl.loadtest.log.LoadTestLogMessageEntry;
import com.eviware.soapui.impl.wsdl.testcase.WsdlTestCase;
import com.eviware.soapui.impl.wsdl.testcase.WsdlTestCaseRunner;
import com.eviware.soapui.model.settings.Settings;
import com.eviware.soapui.model.support.TestRunListenerAdapter;
import com.eviware.soapui.model.testsuite.LoadTestRunListener;
import com.eviware.soapui.model.testsuite.LoadTestRunner;
import com.eviware.soapui.model.testsuite.TestCaseRunContext;
import com.eviware.soapui.model.testsuite.TestCaseRunner;
import com.eviware.soapui.model.testsuite.TestRunContext;
import com.eviware.soapui.model.testsuite.TestRunnable;
import com.eviware.soapui.model.testsuite.TestStep;
import com.eviware.soapui.model.testsuite.TestStepResult;
import com.eviware.soapui.settings.HttpSettings;
import com.eviware.soapui.settings.WsdlSettings;
import com.eviware.soapui.support.UISupport;
import com.eviware.soapui.support.types.StringToObjectMap;
import com.eviware.x.dialogs.Worker;
import com.eviware.x.dialogs.XProgressDialog;
import com.eviware.x.dialogs.XProgressMonitor;
/**
* TestRunner for load-tests.
*
* @author Ole.Matzura
* @todo statistics should be calculated first after all threads have been
* started..
*/
public class WsdlLoadTestRunner implements LoadTestRunner
{
private final WsdlLoadTest loadTest;
private Set<InternalTestCaseRunner> runners = new HashSet<InternalTestCaseRunner>();
private long startTime = 0;
private InternalPropertyChangeListener internalPropertyChangeListener = new InternalPropertyChangeListener();
private InternalTestRunListener testRunListener = new InternalTestRunListener();
private long runCount;
private Status status;
private WsdlLoadTestContext context;
private String reason;
private int threadCount;
private int threadsWaitingToStart;
private int startedCount;
private boolean hasTearedDown;
private TestCaseStarter testCaseStarter;
private boolean stopped;
private TestCaseConfig blueprintConfig;
public WsdlLoadTestRunner( WsdlLoadTest test )
{
this.loadTest = test;
status = Status.INITIALIZED;
}
public Status getStatus()
{
return status;
}
void start()
{
loadTest.getTestCase().beforeSave();
runners.clear();
runCount = 0;
threadCount = 0;
threadsWaitingToStart = 0;
startedCount = 0;
context = new WsdlLoadTestContext( this );
try
{
loadTest.runSetupScript( context, this );
}
catch( Exception e1 )
{
SoapUI.logError( e1 );
}
for( LoadTestRunListener listener : loadTest.getLoadTestRunListeners() )
{
try
{
listener.beforeLoadTest( this, context );
}
catch( Throwable e )
{
SoapUI.logError( e );
}
}
status = Status.RUNNING;
loadTest.addPropertyChangeListener( WsdlLoadTest.THREADCOUNT_PROPERTY, internalPropertyChangeListener );
XProgressDialog progressDialog = UISupport.getDialogs().createProgressDialog( "Starting threads",
( int )loadTest.getThreadCount(), "", true );
try
{
testCaseStarter = new TestCaseStarter();
progressDialog.run( testCaseStarter );
}
catch( Exception e )
{
SoapUI.logError( e );
}
if( status == Status.RUNNING )
{
for( LoadTestRunListener listener : loadTest.getLoadTestRunListeners() )
{
listener.loadTestStarted( this, context );
}
startStrategyThread();
}
else
{
stop();
}
}
/**
* Starts thread the calls the current strategy to recalculate
*/
private void startStrategyThread()
{
new Thread( new Runnable()
{
public void run()
{
while( getStatus() == Status.RUNNING )
{
try
{
loadTest.getLoadStrategy().recalculate( WsdlLoadTestRunner.this, context );
long strategyInterval = loadTest.getStrategyInterval();
if( strategyInterval < 1 )
strategyInterval = WsdlLoadTest.DEFAULT_STRATEGY_INTERVAL;
Thread.sleep( strategyInterval );
}
catch( InterruptedException e )
{
e.printStackTrace();
}
}
}
} ).start();
}
private InternalTestCaseRunner startTestCase( WsdlTestCase testCase )
{
InternalTestCaseRunner testCaseRunner = new InternalTestCaseRunner( testCase, threadCount++ );
SoapUI.getThreadPool().submit( testCaseRunner );
runners.add( testCaseRunner );
return testCaseRunner;
}
public synchronized void cancel( String reason )
{
if( status != Status.RUNNING )
return;
this.reason = reason;
status = Status.CANCELED;
if( testCaseStarter != null )
testCaseStarter.stop();
InternalTestCaseRunner[] r = runners.toArray( new InternalTestCaseRunner[runners.size()] );
for( InternalTestCaseRunner runner : r )
{
runner.cancel( reason, true );
}
String msg = "LoadTest [" + loadTest.getName() + "] canceled";
if( reason != null )
msg += "; " + reason;
loadTest.getLoadTestLog().addEntry( new LoadTestLogMessageEntry( msg ) );
for( LoadTestRunListener listener : loadTest.getLoadTestRunListeners() )
{
listener.loadTestStopped( this, context );
}
if( r.length == 0 )
stop();
}
public synchronized void fail( String reason )
{
boolean wasRunning = ( status == Status.RUNNING );
this.reason = reason;
status = Status.FAILED;
if( !wasRunning )
return;
if( testCaseStarter != null )
testCaseStarter.stop();
String msg = "LoadTest [" + loadTest.getName() + "] failed";
if( reason != null )
msg += "; " + reason;
loadTest.getLoadTestLog().addEntry( new LoadTestLogMessageEntry( msg ) );
for( LoadTestRunListener listener : loadTest.getLoadTestRunListeners() )
{
try
{
listener.loadTestStopped( this, context );
}
catch( Throwable e )
{
SoapUI.logError( e );
}
}
InternalTestCaseRunner[] r = runners.toArray( new InternalTestCaseRunner[runners.size()] );
for( InternalTestCaseRunner runner : r )
{
runner.cancel( reason, true );
}
if( r.length == 0 )
stop();
}
private synchronized void tearDown()
{
if( hasTearedDown )
return;
try
{
loadTest.runTearDownScript( context, this );
}
catch( Exception e1 )
{
SoapUI.logError( e1 );
}
hasTearedDown = true;
}
public Status waitUntilFinished()
{
while( runners.size() > 0 || threadsWaitingToStart > 0 || !hasStopped() )
{
try
{
Thread.sleep( 200 );
}
catch( InterruptedException e )
{
SoapUI.logError( e );
}
}
return getStatus();
}
public void finishTestCase( String reason, WsdlTestCase testCase )
{
for( InternalTestCaseRunner runner : runners )
{
if( runner.getTestCase() == testCase )
{
runner.cancel( reason, false );
break;
}
}
}
public synchronized void finishRunner( InternalTestCaseRunner runner )
{
if( !runners.contains( runner ) )
{
throw new RuntimeException( "Trying to finish unknown runner.. " );
}
runners.remove( runner );
if( getProgress() >= 1 || status != Status.RUNNING )
{
stop();
}
}
private synchronized void stop()
{
if( stopped )
return;
loadTest.removePropertyChangeListener( WsdlLoadTest.THREADCOUNT_PROPERTY, internalPropertyChangeListener );
if( testCaseStarter != null )
testCaseStarter.stop();
if( status == Status.RUNNING )
status = Status.FINISHED;
loadTest.getLoadTestLog().addEntry(
new LoadTestLogMessageEntry( "LoadTest ended at " + new Date( System.currentTimeMillis() ) ) );
try
{
tearDown();
}
catch( Throwable e )
{
SoapUI.logError( e );
}
for( LoadTestRunListener listener : loadTest.getLoadTestRunListeners() )
{
try
{
listener.afterLoadTest( this, context );
}
catch( Throwable e )
{
SoapUI.logError( e );
}
}
context.clear();
stopped = true;
blueprintConfig = null;
}
public boolean hasStopped()
{
return stopped;
}
public int getRunningThreadCount()
{
return runners.size();
}
public float getProgress()
{
long testLimit = loadTest.getTestLimit();
if( testLimit == 0 )
return -1;
if( loadTest.getLimitType() == LoadTestLimitTypesConfig.COUNT )
return ( float )runCount / ( float )testLimit;
if( loadTest.getLimitType() == LoadTestLimitTypesConfig.COUNT_PER_THREAD )
return ( float )runCount / ( float )( testLimit * loadTest.getThreadCount() );
if( loadTest.getLimitType() == LoadTestLimitTypesConfig.TIME )
return startTime == 0 ? 0 : ( float )getTimeTaken() / ( float )( testLimit * 1000 );
return -1;
}
private synchronized boolean afterRun( InternalTestCaseRunner runner )
{
if( status != Status.RUNNING )
return false;
runCount++ ;
if( loadTest.getTestLimit() < 1 )
return true;
if( loadTest.getLimitType() == LoadTestLimitTypesConfig.COUNT_PER_THREAD )
return runner.getRunCount() < loadTest.getTestLimit();
if( loadTest.getLimitType() == LoadTestLimitTypesConfig.COUNT )
return runCount + runners.size() + threadsWaitingToStart <= loadTest.getTestLimit();
if( loadTest.getLimitType() == LoadTestLimitTypesConfig.TIME )
return getTimeTaken() < loadTest.getTestLimit() * 1000;
return true;
}
private final class TestCaseStarter extends Worker.WorkerAdapter
{
private List<WsdlTestCase> testCases = new ArrayList<WsdlTestCase>();
private boolean canceled;
public Object construct( XProgressMonitor monitor )
{
int startDelay = loadTest.getStartDelay();
for( int c = 0; c < loadTest.getThreadCount() && !canceled; c++ )
{
monitor.setProgress( 1, "Creating Virtual User " + ( c + 1 ) );
testCases.add( createTestCase() );
}
startTime = System.currentTimeMillis();
if( canceled )
{
loadTest.getLoadTestLog().addEntry(
new LoadTestLogMessageEntry( "LoadTest canceled during startup at " + new Date( startTime ) ) );
return null;
}
loadTest.getLoadTestLog().addEntry(
new LoadTestLogMessageEntry( "LoadTest started at " + new Date( startTime ) ) );
threadsWaitingToStart = testCases.size();
int cnt = 0;
while( !testCases.isEmpty() && !canceled )
{
if( startDelay > 0 )
{
try
{
Thread.sleep( startDelay );
}
catch( InterruptedException e )
{
SoapUI.logError( e );
}
}
if( status != Status.RUNNING || getProgress() >= 1 )
{
while( !testCases.isEmpty() )
testCases.remove( 0 ).release();
threadsWaitingToStart = 0;
break;
}
// could have been canceled..
if( !testCases.isEmpty() )
{
startTestCase( testCases.remove( 0 ) );
monitor.setProgress( 1, "Started thread " + ( ++cnt ) );
threadsWaitingToStart-- ;
}
}
return null;
}
public boolean onCancel()
{
cancel( "Stopped from UI during start-up" );
stop();
return false;
}
public void stop()
{
if( !canceled )
{
canceled = true;
while( !testCases.isEmpty() )
testCases.remove( 0 ).release();
threadsWaitingToStart = 0;
}
}
}
public class InternalTestCaseRunner implements Runnable
{
private final WsdlTestCase testCase;
private boolean canceled;
private long runCount;
private WsdlTestCaseRunner runner;
private final int threadIndex;
public InternalTestCaseRunner( WsdlTestCase testCase, int threadIndex )
{
this.testCase = testCase;
this.threadIndex = threadIndex;
}
public void run()
{
try
{
if( System.getProperty( "soapui.enablenamedthreads" ) != null )
Thread.currentThread().setName(
testCase.getName() + " " + loadTest.getName() + " ThreadIndex = " + threadIndex );
runner = new WsdlTestCaseRunner( testCase, new StringToObjectMap() );
while( !canceled )
{
try
{
runner.getRunContext().reset();
runner.getRunContext().setProperty( TestCaseRunContext.THREAD_INDEX, threadIndex );
runner.getRunContext().setProperty( TestCaseRunContext.RUN_COUNT, runCount );
runner.getRunContext().setProperty( TestCaseRunContext.LOAD_TEST_RUNNER, WsdlLoadTestRunner.this );
runner.getRunContext().setProperty( TestCaseRunContext.LOAD_TEST_CONTEXT, context );
synchronized( this )
{
runner.getRunContext().setProperty( TestCaseRunContext.TOTAL_RUN_COUNT, startedCount++ );
}
runner.run();
}
catch( Throwable e )
{
System.err.println( "Error running testcase: " + e );
SoapUI.logError( e );
}
runCount++ ;
if( !afterRun( this ) )
break;
}
}
finally
{
finishRunner( this );
testCase.release();
testCase.removeTestRunListener( testRunListener );
}
}
public void cancel( String reason, boolean cancelRunner )
{
if( runner != null && cancelRunner )
runner.cancel( reason );
this.canceled = true;
}
public boolean isCanceled()
{
return canceled;
}
public long getRunCount()
{
return runCount;
}
public WsdlTestCase getTestCase()
{
return testCase;
}
}
public WsdlLoadTest getLoadTest()
{
return loadTest;
}
public class InternalPropertyChangeListener implements PropertyChangeListener
{
public void propertyChange( PropertyChangeEvent evt )
{
updateThreadCount();
}
}
public synchronized void updateThreadCount()
{
if( status != Status.RUNNING )
return;
long newCount = loadTest.getThreadCount();
// get list of active runners
Iterator<InternalTestCaseRunner> iterator = runners.iterator();
List<InternalTestCaseRunner> activeRunners = new ArrayList<InternalTestCaseRunner>();
while( iterator.hasNext() )
{
InternalTestCaseRunner runner = iterator.next();
if( !runner.isCanceled() )
activeRunners.add( runner );
}
long diff = newCount - activeRunners.size();
if( diff == 0 )
return;
// cancel runners if thread count has been decreased
if( diff < 0 && loadTest.getCancelExcessiveThreads() )
{
diff = Math.abs( diff );
for( int c = 0; c < diff && c < activeRunners.size(); c++ )
{
activeRunners.get( c ).cancel( "excessive thread", false );
}
}
// start new runners if thread count has been increased
else if( diff > 0 )
{
for( int c = 0; c < diff; c++ )
{
int startDelay = loadTest.getStartDelay();
if( startDelay > 0 )
{
try
{
Thread.sleep( startDelay );
}
catch( InterruptedException e )
{
SoapUI.logError( e );
}
}
if( status == Status.RUNNING )
startTestCase( createTestCase() );
}
}
}
/**
* Creates a copy of the underlying WsdlTestCase with all LoadTests removed
* and configured for LoadTesting
*/
private synchronized WsdlTestCase createTestCase()
{
WsdlTestCase testCase = loadTest.getTestCase();
TestCaseConfig config = null;
if( blueprintConfig == null )
{
try
{
blueprintConfig = TestCaseConfig.Factory.parse( testCase.getConfig().xmlText() );
blueprintConfig.setLoadTestArray( new LoadTestConfig[0] );
blueprintConfig.setSecurityTestArray( new SecurityTestConfig[0] );
}
catch( XmlException e )
{
e.printStackTrace();
}
}
config = ( TestCaseConfig )blueprintConfig.copy();
// clone entire testCase
WsdlTestCase tc = testCase.getTestSuite().buildTestCase( config, true );
tc.afterLoad();
tc.addTestRunListener( testRunListener );
Settings settings = tc.getSettings();
settings.setBoolean( HttpSettings.INCLUDE_REQUEST_IN_TIME_TAKEN,
loadTest.getSettings().getBoolean( HttpSettings.INCLUDE_REQUEST_IN_TIME_TAKEN ) );
settings.setBoolean( HttpSettings.INCLUDE_RESPONSE_IN_TIME_TAKEN,
loadTest.getSettings().getBoolean( HttpSettings.INCLUDE_RESPONSE_IN_TIME_TAKEN ) );
settings.setBoolean( HttpSettings.CLOSE_CONNECTIONS,
loadTest.getSettings().getBoolean( HttpSettings.CLOSE_CONNECTIONS ) );
// disable default pretty-printing since it takes time
settings.setBoolean( WsdlSettings.PRETTY_PRINT_RESPONSE_MESSAGES, false );
// don't discard.. the WsdlLoadTests internal listener will discard after
// asserting..
tc.setDiscardOkResults( false );
tc.setMaxResults( 0 );
return tc;
}
public String getReason()
{
return reason;
}
public long getTimeTaken()
{
return System.currentTimeMillis() - startTime;
}
private class InternalTestRunListener extends TestRunListenerAdapter
{
public void beforeRun( TestCaseRunner testRunner, TestCaseRunContext runContext )
{
if( getProgress() > 1 && loadTest.getCancelOnReachedLimit() )
{
testRunner.cancel( "LoadTest Limit reached" );
}
else
for( LoadTestRunListener listener : loadTest.getLoadTestRunListeners() )
{
listener.beforeTestCase( WsdlLoadTestRunner.this, context, testRunner, runContext );
}
}
public void beforeStep( TestCaseRunner testRunner, TestCaseRunContext runContext, TestStep testStep )
{
if( getProgress() > 1 && loadTest.getCancelOnReachedLimit() )
{
testRunner.cancel( "LoadTest Limit reached" );
}
else if( runContext.getCurrentStep() != null )
{
for( LoadTestRunListener listener : loadTest.getLoadTestRunListeners() )
{
listener.beforeTestStep( WsdlLoadTestRunner.this, context, testRunner, runContext, testStep );
}
}
}
public void afterStep( TestCaseRunner testRunner, TestCaseRunContext runContext, TestStepResult result )
{
for( LoadTestRunListener listener : loadTest.getLoadTestRunListeners() )
{
listener.afterTestStep( WsdlLoadTestRunner.this, context, testRunner, runContext, result );
}
}
public void afterRun( TestCaseRunner testRunner, TestCaseRunContext runContext )
{
for( LoadTestRunListener listener : loadTest.getLoadTestRunListeners() )
{
listener.afterTestCase( WsdlLoadTestRunner.this, context, testRunner, runContext );
}
}
}
public boolean isRunning()
{
return status == Status.RUNNING;
}
public TestRunContext getRunContext()
{
return context;
}
public long getStartTime()
{
return startTime;
}
public void start( boolean async )
{
start();
}
public TestRunnable getTestRunnable()
{
return loadTest;
}
public void release()
{
loadTest.removePropertyChangeListener( WsdlLoadTest.THREADCOUNT_PROPERTY, internalPropertyChangeListener );
}
}