View Javadoc

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