View Javadoc

1   /*
2    *  soapUI, copyright (C) 2004-2007 eviware.com 
3    *
4    *  soapUI is free software; you can redistribute it and/or modify it under the 
5    *  terms of version 2.1 of the GNU Lesser General Public License as published by 
6    *  the Free Software Foundation.
7    *
8    *  soapUI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 
9    *  even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
10   *  See the GNU Lesser General Public License for more details at gnu.org.
11   */
12  
13  package com.eviware.soapui.impl.wsdl.loadtest;
14  
15  import java.beans.PropertyChangeEvent;
16  import java.beans.PropertyChangeListener;
17  import java.util.ArrayList;
18  import java.util.Date;
19  import java.util.HashSet;
20  import java.util.Iterator;
21  import java.util.List;
22  import java.util.Set;
23  
24  import com.eviware.soapui.SoapUI;
25  import com.eviware.soapui.config.LoadTestConfig;
26  import com.eviware.soapui.config.LoadTestLimitTypesConfig;
27  import com.eviware.soapui.config.TestCaseConfig;
28  import com.eviware.soapui.impl.wsdl.loadtest.log.LoadTestLogMessageEntry;
29  import com.eviware.soapui.impl.wsdl.testcase.WsdlTestCase;
30  import com.eviware.soapui.impl.wsdl.testcase.WsdlTestCaseRunner;
31  import com.eviware.soapui.model.settings.Settings;
32  import com.eviware.soapui.model.testsuite.LoadTest;
33  import com.eviware.soapui.model.testsuite.LoadTestRunListener;
34  import com.eviware.soapui.model.testsuite.LoadTestRunner;
35  import com.eviware.soapui.model.testsuite.TestRunContext;
36  import com.eviware.soapui.model.testsuite.TestRunListener;
37  import com.eviware.soapui.model.testsuite.TestRunner;
38  import com.eviware.soapui.model.testsuite.TestStepResult;
39  import com.eviware.soapui.settings.HttpSettings;
40  import com.eviware.soapui.support.UISupport;
41  import com.eviware.soapui.support.types.StringToObjectMap;
42  import com.eviware.x.dialogs.Worker;
43  import com.eviware.x.dialogs.XProgressDialog;
44  import com.eviware.x.dialogs.XProgressMonitor;
45  
46  /***
47   * TestRunner for load-tests. 
48   *
49   * @todo statistics should be calculated first after all threads have been started..
50   * 
51   * @author Ole.Matzura
52   */
53  
54  public class WsdlLoadTestRunner implements LoadTestRunner 
55  {
56  	private final WsdlLoadTest loadTest;
57  	private ThreadGroup threadGroup;
58  	private Set<TestCaseRunner> runners = new HashSet<TestCaseRunner>();
59  	private long startTime = 0;
60  	private InternalPropertyChangeListener internalPropertyChangeListener = new InternalPropertyChangeListener();
61  	private InternalTestRunListener testRunListener = new InternalTestRunListener();
62  	private LoadTestRunListener[] listeners;
63  	private long runCount;
64  	private Status status;
65  	private WsdlLoadTestContext context;
66  	private String reason;
67  	private int threadCount;
68  	private int threadsWaitingToStart;
69  
70  	public WsdlLoadTestRunner(WsdlLoadTest test)
71  	{
72  		this.loadTest = test;
73  		threadGroup = new ThreadGroup( loadTest.getName() );
74  		
75  		status = Status.INITIALIZED;
76  	}
77  	
78  	public Status getStatus()
79  	{
80  		return status;
81  	}
82  
83  	void start()
84  	{
85  		loadTest.getTestCase().beforeSave();
86  		startTime = System.currentTimeMillis();
87  		
88  		runners.clear();
89  		loadTest.addPropertyChangeListener( WsdlLoadTest.THREADCOUNT_PROPERTY, internalPropertyChangeListener );
90  		runCount = 0;
91  		threadCount = 0;
92  		threadsWaitingToStart = 0;
93  		context = new WsdlLoadTestContext( this );
94  		
95  		status = Status.RUNNING;
96  		
97  		listeners = loadTest.getLoadTestRunListeners();
98  		for( LoadTestRunListener listener : listeners )
99  		{
100 			listener.beforeLoadTest( this, context );
101 		}
102 
103 		loadTest.getLoadTestLog().addEntry( 
104 				new LoadTestLogMessageEntry( "LoadTest started at " + new Date( startTime ) )); 
105 		
106 		int startDelay = loadTest.getStartDelay();
107 		if( startDelay >= 0 )
108 		{
109 			XProgressDialog progressDialog = UISupport.getDialogs().createProgressDialog( "Starting threads", 
110 						( int ) loadTest.getThreadCount(), 
111 						"", true );
112 			try
113 			{
114 				progressDialog.run( new Worker.WorkerAdapter() {
115 
116 					private List<WsdlTestCase> testCases = new ArrayList<WsdlTestCase>();
117 					private boolean canceled;
118 					
119 					public Object construct( XProgressMonitor monitor )
120 					{
121 						int startDelay = loadTest.getStartDelay();
122 						
123 						for( int c = 0; c < loadTest.getThreadCount() && !canceled; c++ )
124 						{
125 							monitor.setProgress( 1, "Creating Virtual User " + (c+1) );
126 							testCases.add( createTestCase() );
127 						}
128 						
129 						threadsWaitingToStart = testCases.size();
130 						int cnt = 0;
131 						while( !testCases.isEmpty() && !canceled )
132 						{
133 							if( startDelay > 0 )
134 							{
135 								try
136 								{
137 									Thread.sleep( startDelay );
138 								}
139 								catch( InterruptedException e )
140 								{
141 									SoapUI.logError( e );
142 								}
143 							}
144 								
145 							if( status != Status.RUNNING || getProgress() >= 1 ||
146 								(loadTest.getLimitType() == LoadTestLimitTypesConfig.COUNT && 
147 											runners.size() >= loadTest.getTestLimit() ))
148 								break;
149 							
150 							// could have been canceled..
151 							if( !testCases.isEmpty() )
152 							{
153 								startTestCase( testCases.remove( 0 ));
154 								monitor.setProgress( 1, "Started thread " + (++cnt) );
155 								threadsWaitingToStart--;
156 							}
157 						}
158 						
159 						return null;
160 					}
161 
162 					public boolean onCancel()
163 					{
164 						cancel( "Stopped from UI during start-up" );
165 						canceled = true;
166 						while( !testCases.isEmpty() )
167 							testCases.remove( 0 ).release();
168 						
169 						return false;
170 					}} );
171 				
172 			}
173 			catch( Exception e )
174 			{
175 				SoapUI.logError( e );
176 			}
177 		}
178 		else
179 		{
180 			List<WsdlTestCase> testCases = new ArrayList<WsdlTestCase>();
181 			for( int c = 0; c < loadTest.getThreadCount(); c++ )
182 				testCases.add( createTestCase() );
183 			
184 			for( int c = 0; c < loadTest.getThreadCount(); c++ )
185 			{
186 				if( loadTest.getLimitType() == LoadTestLimitTypesConfig.COUNT && 
187 							runners.size() >= loadTest.getTestLimit() )
188 					break;
189 			
190 				startTestCase( testCases.get( c ) );
191 			}
192 		}
193 		
194 		if( listeners != null )
195 		{
196 			if( status == Status.RUNNING )
197 			{
198 				for( LoadTestRunListener listener : listeners )
199 				{
200 					listener.loadTestStarted( this, context );
201 				}
202 			}
203 			else
204 			{
205 				for( LoadTestRunListener listener : listeners )
206 				{
207 					listener.afterLoadTest( this, context );
208 				}
209 			}
210 		}
211 	}
212 
213 	private TestCaseRunner startTestCase( WsdlTestCase testCase )
214 	{
215 		TestCaseRunner testCaseRunner = new TestCaseRunner( testCase, threadCount++ );
216 		Thread thread = new Thread( threadGroup, testCaseRunner, 
217 					testCase.getName() + " - " + loadTest.getName() + " - ThreadIndex " + testCaseRunner.threadIndex );
218 		thread.start();
219 		runners.add( testCaseRunner );
220 		return testCaseRunner;
221 	}
222 	
223 	public synchronized void cancel( String reason )
224 	{
225 		if( status != Status.RUNNING )
226 			return;
227 		
228 		this.reason = reason;
229 		status = Status.CANCELED;
230 		
231 		TestCaseRunner[] r = runners.toArray( new TestCaseRunner[ runners.size()] );
232 		
233 		for( TestCaseRunner runner : r )
234 		{
235 			runner.cancel( reason, true );
236 		}
237 		
238 		String msg = "LoadTest [" + loadTest.getName()+ "] canceled";
239 		if( reason != null )
240 			msg += "; " + reason;
241 		
242 		loadTest.getLoadTestLog().addEntry( new LoadTestLogMessageEntry( msg )); 
243 		
244 		for( LoadTestRunListener listener : listeners )
245 		{
246 			listener.loadTestStopped( this, context );
247 		}
248 	}
249 	
250 	public synchronized void fail( String reason )
251 	{
252 		if( status != Status.RUNNING )
253 			return;
254 		
255 		this.reason = reason;
256 		status = Status.FAILED;
257 		
258 		String msg = "LoadTest [" + loadTest.getName()+ "] failed";
259 		if( reason != null )
260 			msg += "; " + reason;
261 		
262 		loadTest.getLoadTestLog().addEntry( new LoadTestLogMessageEntry( msg )); 
263 		
264 		for( LoadTestRunListener listener : listeners )
265 		{
266 			listener.loadTestStopped( this, context );
267 		}
268 		
269 		TestCaseRunner[] r = runners.toArray( new TestCaseRunner[ runners.size()] );
270 		
271 		for( TestCaseRunner runner : r )
272 		{
273 			runner.cancel( reason, true );
274 		}
275 	}
276 
277 	public void waitUntilFinished()
278 	{
279 		while( runners.size() > 0 || threadsWaitingToStart > 0 )
280 		{
281 			try
282 			{
283 				Thread.sleep( 200 );
284 			}
285 			catch (InterruptedException e)
286 			{
287 				SoapUI.logError( e );
288 			}
289 		}
290 	}
291 	
292 	public void finishTestCase( String reason, WsdlTestCase testCase  )
293 	{
294 		for( TestCaseRunner runner : runners )
295 		{
296 			if( runner.getTestCase() == testCase )
297 			{
298 				runner.cancel( reason, false );
299 				break;
300 			}
301 		}
302 	}
303 	
304 	public synchronized void finishRunner( TestCaseRunner runner )
305 	{
306 		if( !runners.contains( runner ))
307 		{
308 			throw new RuntimeException( "Trying to finish unknown runner.. " );
309 		}
310 		
311 		runners.remove( runner );
312 		if( runners.size() == 0 && threadsWaitingToStart == 0 )
313 		{
314 			loadTest.removePropertyChangeListener( WsdlLoadTest.THREADCOUNT_PROPERTY, 
315 					internalPropertyChangeListener );
316 			
317 			if( status == Status.RUNNING )
318 				status = Status.FINISHED;
319 			
320 			loadTest.getLoadTestLog().addEntry( 
321 					new LoadTestLogMessageEntry( "LoadTest ended at " + new Date( System.currentTimeMillis() ) )); 
322 
323 			for( LoadTestRunListener listener : listeners )
324 			{
325 				listener.afterLoadTest( this, context );
326 			}
327 			
328 			listeners = null;
329 			context.clear();
330 		}
331 	}
332 
333 	public int getRunningThreadCount()
334 	{
335 		return runners.size();
336 	}
337 
338 	public float getProgress()
339 	{
340 		long testLimit = loadTest.getTestLimit();
341 		if( testLimit == 0 )
342 			return -1;
343 		
344 		if( loadTest.getLimitType() == LoadTestLimitTypesConfig.COUNT )
345 			return (float)runCount / (float)testLimit;
346 			
347 		if( loadTest.getLimitType() == LoadTestLimitTypesConfig.TIME )
348 			return (float)getTimeTaken() / (float)(testLimit*1000);
349 		
350 		return -1;
351 	}
352 	
353 	private synchronized boolean afterRun( TestCaseRunner runner )
354 	{
355 		if( status != Status.RUNNING )
356 			return false;
357 		
358 		runCount++;
359 		
360 		if( loadTest.getTestLimit() < 1 ) return true;
361 		
362 		if( loadTest.getLimitType() == LoadTestLimitTypesConfig.COUNT )
363 			return runCount + runners.size() + threadsWaitingToStart <= loadTest.getTestLimit();
364 			
365 		if( loadTest.getLimitType() == LoadTestLimitTypesConfig.TIME )
366 			return getTimeTaken() < loadTest.getTestLimit()*1000;
367 			
368 		return true;
369 	}
370 	
371 	public class TestCaseRunner implements Runnable
372 	{
373 		private final WsdlTestCase testCase;
374 		private boolean canceled;
375 		private long runCount;
376 		private WsdlTestCaseRunner runner;
377 		private final int threadIndex;
378 
379 		public TestCaseRunner(WsdlTestCase testCase, int threadIndex )
380 		{
381 			this.testCase = testCase;
382 			this.threadIndex = threadIndex;
383 		}
384 
385 		public void run()
386 		{
387 			try
388 			{
389 				runner = new WsdlTestCaseRunner( testCase, new StringToObjectMap() );
390 				
391 				while( !canceled )
392 				{
393 					try
394 					{
395 						runner.getRunContext().reset();
396 						runner.getRunContext().setProperty( TestRunContext.THREAD_INDEX, threadIndex );
397 						runner.getRunContext().setProperty( TestRunContext.RUN_COUNT, runCount );
398 						runner.getRunContext().setProperty( TestRunContext.LOAD_TEST_RUNNER, WsdlLoadTestRunner.this );
399 						runner.getRunContext().setProperty( TestRunContext.LOAD_TEST_CONTEXT, context );
400 	
401 						runner.run();
402 					}
403 					catch( Throwable e )
404 					{
405 						System.err.println( "Error running testcase: " + e );
406 						SoapUI.logError( e );
407 					}
408 					
409 					runCount++;
410 					
411 					if( !afterRun( this ) )
412 						break;
413 				}
414 			}
415 			finally
416 			{
417 				finishRunner( this );
418 				testCase.release();
419 				testCase.removeTestRunListener( testRunListener );
420 			}
421 		}
422 
423 		public void cancel( String reason, boolean cancelRunner )
424 		{
425 			if( runner != null && cancelRunner )
426 				runner.cancel( reason );
427 			
428 			this.canceled = true;
429 		}
430 
431 		public boolean isCanceled()
432 		{
433 			return canceled;
434 		}
435 		
436 		public long getRunCount()
437 		{
438 			return runCount;
439 		}
440 
441 		public WsdlTestCase getTestCase()
442 		{
443 			return testCase;
444 		}
445 	}
446 
447 	public LoadTest getLoadTest()
448 	{
449 		return loadTest;
450 	}
451 
452 	public class InternalPropertyChangeListener implements PropertyChangeListener
453 	{
454 		public void propertyChange(PropertyChangeEvent evt)
455 		{
456 			updateThreadCount();
457 		}
458 	}
459 
460 	public synchronized void updateThreadCount()
461 	{
462 		if( status != Status.RUNNING ) return;
463 		
464 		long newCount = loadTest.getThreadCount();
465 		
466 		// get list of active runners
467 		Iterator<TestCaseRunner> iterator = runners.iterator();
468 		List<TestCaseRunner> activeRunners = new ArrayList<TestCaseRunner>();
469 		while( iterator.hasNext() )
470 		{
471 			TestCaseRunner runner = iterator.next();
472 			if( !runner.isCanceled() )
473 				activeRunners.add( runner );
474 		}
475 		
476 		long diff = newCount-activeRunners.size();
477 		
478 		if( diff == 0 )
479 			return;	
480 		
481 		// cancel runners if thread count has been decreased
482 		if( diff < 0 )
483 		{
484 			diff = Math.abs( diff );
485 			for( int c = 0; c < diff && c < activeRunners.size(); c++ )
486 			{
487 				activeRunners.get( c ).cancel( "excessive thread", false );
488 			}
489 		}
490 		// start new runners if thread count has been increased
491 		else if( diff > 0 )
492 		{
493 			for( int c = 0; c < diff; c++ )
494 			{
495 				int startDelay = loadTest.getStartDelay();
496 				if( startDelay > 0 )
497 				{
498 					try
499 					{
500 						Thread.sleep( startDelay );
501 					}
502 					catch( InterruptedException e )
503 					{
504 						SoapUI.logError( e );
505 					}
506 				}
507 				
508 				if( status == Status.RUNNING ) 
509 					startTestCase( createTestCase() );
510 			}
511 		}
512 	}
513 	
514 	/***
515 	 * Creates a copy of the underlying WsdlTestCase with all LoadTests removed and configured for LoadTesting
516 	 */
517 	
518 	private WsdlTestCase createTestCase()
519 	{
520 		WsdlTestCase testCase = loadTest.getTestCase();
521 
522 		// clone config and remove and loadtests
523 		TestCaseConfig config = (TestCaseConfig) testCase.getConfig().copy();
524 		config.setLoadTestArray( new LoadTestConfig[0] );
525 		
526 		//	 clone entire testCase
527 		WsdlTestCase tc = new WsdlTestCase( testCase.getTestSuite(), config, true );
528 		tc.afterLoad();
529 		tc.addTestRunListener( testRunListener );
530 		Settings settings = tc.getSettings();
531 		settings.setBoolean( HttpSettings.INCLUDE_REQUEST_IN_TIME_TAKEN, loadTest.getSettings().getBoolean( HttpSettings.INCLUDE_REQUEST_IN_TIME_TAKEN ));
532 		settings.setBoolean( HttpSettings.INCLUDE_RESPONSE_IN_TIME_TAKEN, loadTest.getSettings().getBoolean( HttpSettings.INCLUDE_RESPONSE_IN_TIME_TAKEN ));
533 		settings.setBoolean( HttpSettings.CLOSE_CONNECTIONS, loadTest.getSettings().getBoolean( HttpSettings.CLOSE_CONNECTIONS ));
534 		
535 		// dont discard.. the WsdlLoadTests internal listener will discard after asserting..
536 		tc.setDiscardOkResults( false );
537 		return tc;
538 	}
539 
540 	public String getReason()
541 	{
542 		return reason;
543 	}
544 
545 	public long getTimeTaken()
546 	{
547 		return System.currentTimeMillis()-startTime;
548 	}
549 	
550 	private class InternalTestRunListener implements TestRunListener
551 	{
552 		public void beforeRun(TestRunner testRunner, TestRunContext runContext)
553 		{
554 			if( getProgress() > 1 && loadTest.getCancelOnReachedLimit() )
555 			{
556 				testRunner.cancel( "LoadTest Limit reached" );
557 			}
558 			else for( LoadTestRunListener listener : listeners )
559 			{
560 				listener.beforeTestCase( WsdlLoadTestRunner.this, context, testRunner, runContext );
561 			}
562 		}
563 
564 		public void beforeStep(TestRunner testRunner, TestRunContext runContext)
565 		{
566 			if( getProgress() > 1 && loadTest.getCancelOnReachedLimit() )
567 			{
568 				testRunner.cancel( "LoadTest Limit reached" );
569 			}
570 			else for( LoadTestRunListener listener : listeners )
571 			{
572 				listener.beforeTestStep( WsdlLoadTestRunner.this, context, testRunner, runContext, runContext.getCurrentStep() );
573 			}
574 		}
575 
576 		public void afterStep(TestRunner testRunner, TestRunContext runContext, TestStepResult result)
577 		{
578 			for( LoadTestRunListener listener : listeners )
579 			{
580 				listener.afterTestStep( WsdlLoadTestRunner.this, context, testRunner, runContext, result );
581 			}
582 		}
583 
584 		public void afterRun(TestRunner testRunner, TestRunContext runContext)
585 		{
586 			for( LoadTestRunListener listener : listeners )
587 			{
588 				listener.afterTestCase( WsdlLoadTestRunner.this, context, testRunner, runContext );
589 			}
590 		}
591 	}
592 }