View Javadoc
1   package eu.simuline.testhelpers;
2   
3   import eu.simuline.util.images.GifResource;
4   
5   import java.awt.event.ActionEvent;
6   import java.awt.event.KeyEvent;
7   
8   import javax.swing.AbstractAction;
9   import javax.swing.KeyStroke;
10  import javax.swing.SwingUtilities;
11  
12  import org.junit.runner.JUnitCore;
13  import org.junit.runner.Request;
14  import org.junit.runner.Runner;
15  import org.junit.runner.Result;
16  import org.junit.runner.Description;
17  
18  import org.junit.runner.manipulation.Filter;
19  
20  import org.junit.runner.notification.RunListener;
21  import org.junit.runner.notification.RunNotifier;
22  import org.junit.runner.notification.StoppedByUserException;
23  
24  import org.javalobby.icons20x20.Open;
25  import org.javalobby.icons20x20.ExecuteProject;
26  import org.javalobby.icons20x20.Stop;
27  import org.javalobby.icons20x20.Hammer;
28  import org.javalobby.icons20x20.Delete;
29  
30  
31  /**
32   * Represents the actions of the test GUI inspired by old junit GUI. 
33   * **** Moreover provides a wrapper to access junit 
34   * and the access point to run the gui started from the class to be tested. 
35   * ****
36   * The fundamental methods are {@link #runFromMain()} 
37   * which runs the test class from its main method and 
38   * {@link #runTestClass(String)} which runs a testclass with the given name. 
39   *
40   * @see GUIRunner
41   * @see GUIRunListener
42   * 
43   * Created: Tue Jun 13 02:53:06 2006
44   *
45   * @author <a href="mailto:ernst.reissner@simuline.eu">Ernst Reissner</a>
46   * @version 1.0
47   */
48  public final class Actions {
49  
50      /* -------------------------------------------------------------------- *
51       * inner classes and enums.                                             *
52       * -------------------------------------------------------------------- */
53  
54      /**
55       * The action of opening a <code>java</code>-file defining a test class 
56       * and starting the tests defined by it. 
57       */
58      class OpenAction extends AbstractAction {
59  
60  	private static final long serialVersionUID = -589L;
61  
62  	OpenAction() {
63  	    super("Open", GifResource.getIcon(Open.class));
64  	    putValue(SHORT_DESCRIPTION, "Opens a class and executes run. ");
65              putValue(MNEMONIC_KEY, KeyEvent.VK_O);
66              putValue(ACCELERATOR_KEY,
67  		     KeyStroke.getKeyStroke(KeyEvent.VK_O,
68  					    ActionEvent.CTRL_MASK));
69  	}
70  
71  	public void actionPerformed(ActionEvent event) {
72  	    String clsName = Actions.this.guiRunner.openClassChooser();
73  
74  	    if (clsName == null) {
75  		// open action was not successful
76  		System.out.println("Warning: no class choosen");
77  		return;
78  	    }
79  	    assert clsName != null;
80  	    System.out.println("Info: class `" + clsName + "' choosen");
81  	    Actions.this.coreRunner = new CoreRunner(clsName);
82  
83  	    // clears list of failed testcases
84  	    Actions.this.filter = null;
85  	    Actions.this.getStartAction().actionPerformed(null);
86  	}
87      } // class OpenAction 
88  
89  
90      /**
91       * The action of starting the testcases in the loaded testclass. 
92       */
93      class StartAction extends AbstractAction {
94  
95  	private static final long serialVersionUID = -589L;
96  
97  	StartAction() {
98  	    super("Run", GifResource.getIcon(ExecuteProject.class));
99  	    putValue(SHORT_DESCRIPTION, "Runs the testcases. ");
100             putValue(MNEMONIC_KEY, KeyEvent.VK_R);
101             putValue(ACCELERATOR_KEY,
102 		     KeyStroke.getKeyStroke(KeyEvent.VK_R,
103 					    ActionEvent.CTRL_MASK));
104 	}
105 
106 	public void actionPerformed(ActionEvent event) {
107  	    Actions.this.coreRunner = new CoreRunner(Actions.this.coreRunner);
108 	    Actions.this.coreRunner.start();
109 	}
110     } // class StartAction 
111 
112     /**
113      * The action of stopping the test run//  after having finished 
114      * the currently running testcase. 
115      */
116     class StopAction extends AbstractAction {
117 
118     	private static final long serialVersionUID = -589L;
119 
120     	StopAction() {
121     	    super("Stop", GifResource.getIcon(Stop.class));
122     	    putValue(SHORT_DESCRIPTION, 
123     		     "Stops after having executed the current testcase. ");
124             putValue(MNEMONIC_KEY, KeyEvent.VK_S);
125             putValue(ACCELERATOR_KEY,
126     		     KeyStroke.getKeyStroke(KeyEvent.VK_S,
127     					    ActionEvent.CTRL_MASK));
128     	}
129 
130      	public void actionPerformed(ActionEvent event) {
131 	    Actions.this.coreRunner.pleaseStop();
132 	}
133     } // class StopAction 
134 
135     /**
136      * The action of breaking the sequence of testcases currently running. 
137      * Tries to stop the currently running testcase 
138      * but guarantees only that the break is effected 
139      * after the current testcase has finished. 
140      */
141     class BreakAction extends AbstractAction {
142 
143 	private static final long serialVersionUID = -589L;
144 
145 	BreakAction() {
146 	    super("Break", GifResource.getIcon(Hammer.class));
147 	    putValue(SHORT_DESCRIPTION, 
148 		     "Tries to break execution of current testcases. ");
149             putValue(MNEMONIC_KEY, KeyEvent.VK_B);
150             putValue(ACCELERATOR_KEY,
151 		     KeyStroke.getKeyStroke(KeyEvent.VK_B,
152 					    ActionEvent.CTRL_MASK));
153 	}
154 
155 	@SuppressWarnings("deprecation")// because no alternative to stop. 
156 	public void actionPerformed(ActionEvent event) {
157 	    //setEnableForRun(false); // !isRunning
158 	    System.out.println("Break...");
159 	    Actions.this.coreRunner.pleaseStop(); // avoids going on after stop 
160 	    // method stop is deprecated but there is no alternative
161 	    // TBD: rework: shall throw exception. 
162 			// original argument: deprecated?
163 	    Actions.this.coreRunner.stop(); //new StoppedByUserException()
164 	}
165     } // class BreakAction 
166 
167 
168     /**
169      * The action of exiting the tester application. 
170      */
171     static class ExitAction extends AbstractAction {
172 
173 	private static final long serialVersionUID = -589L;
174 
175 	ExitAction() {
176 	    super("Exit", GifResource.getIcon(Delete.class));
177 	    putValue(SHORT_DESCRIPTION, 
178 		     "Quits this application immediately. ");
179             putValue(MNEMONIC_KEY, KeyEvent.VK_E);
180             putValue(ACCELERATOR_KEY,
181 		     KeyStroke.getKeyStroke(KeyEvent.VK_E,
182 					    ActionEvent.CTRL_MASK));
183 	}
184 
185 	@edu.umd.cs.findbugs.annotations.SuppressWarnings
186 	(value = "DM_EXIT", 
187 	 justification = "To ensure safe exit " + 
188 	 "not reached by throwing exception. " + 
189 	 "Also Actions is not invoked by other code. ")
190 	public void actionPerformed(ActionEvent event) {
191 	    //throw new RuntimeException("Exit by user action. ");
192 	    System.exit(0);
193 	}
194     } // class ExitAction 
195 
196     /**
197      * A thread in which a testclass is executed 
198      * or at least a single testcase out of this testclass. 
199      * Essentially, this is defined by the class {@link #testClassName}. 
200      * The major task of this class is, to reload {@link #testClassName} 
201      * using a classloader which allows reloading 
202      * each test run without restarting the tester application. 
203      * The core of the code is copied from {@link JUnitCore}. 
204      */
205     class CoreRunner extends Thread {
206 
207 	/* ---------------------------------------------------------------- *
208 	 * fields.                                                          *
209 	 * ---------------------------------------------------------------- */
210 
211 	/**
212 	 * The notifier to run the tests as in JUnitCore. 
213 	 */
214 	private final RunNotifier notifier;
215 
216 	/**
217 	 * The name of the class to be tested. 
218 	 */
219 	private final String testClassName;
220 
221 	/* ---------------------------------------------------------------- *
222 	 * constructors.                                                    *
223 	 * ---------------------------------------------------------------- */
224 
225 	/**
226 	 * Creates a runner running all testcases in the given test class. 
227 	 */
228 	CoreRunner(String testClassName)  {
229 	    this.notifier = new RunNotifier();
230 	    this.notifier.addListener(Actions.this.listener);
231 
232 	    this.testClassName = testClassName;
233 	}
234 
235 	/**
236 	 * Copy constructor. 
237 	 */
238 	CoreRunner(CoreRunner other)  {
239 	    this(other.testClassName);
240 	}
241 
242 	/* ---------------------------------------------------------------- *
243 	 * methods.                                                         *
244 	 * ---------------------------------------------------------------- */
245 
246 
247 	// api-docs inherited from Runnable 
248 	// overwrites void implementation provided by class Thread 
249 	/**
250 	 * Loads the class with name {@link #testClassName} 
251 	 * with a {@link TestCaseClassLoader} to allow reloading. 
252 	 * Then creates a {@link Request} filtering it with {@link #filter} 
253 	 * defining the tests to be run and runs those tests 
254 	 * invoking {@link #run(Request)}. 
255 	 */
256 	public void run() {
257 	    Class<?> newTestClass = null;
258 	    try {
259 		newTestClass = new TestCaseClassLoader()
260 		    .loadClass(this.testClassName, true);
261 	    } catch (ClassNotFoundException e) {
262 		throw new IllegalStateException// NOPMD
263 		    ("Testclass '" + this.testClassName + "' disappeared. ");
264 		// **** This makes the problem 
265 		// that in the GUI the run button is shaded 
266 		// (and the stop button is not). 
267 	    }
268 	    assert newTestClass != null;
269 
270 	    Request request = Request.aClass(newTestClass);
271 	    //Request request = Request.classes(newTestClass);
272 	    if (Actions.this.filter != null && Actions.this.filter.isTest()) {
273 		request = request.filterWith(Actions.this.filter);
274 	    } else {
275 		Actions.this.listener.testClassStructureLoaded
276 		    (request.getRunner().getDescription());
277 	    }
278 
279 	    try {
280 		run(request);
281 	    } catch (StoppedByUserException ee) {
282 		// either Break or Stop 
283   		Actions.this.listener.testRunAborted();
284   	    }
285 
286 	    //System.out.println("...Core run()"+this.core);
287 	}
288 
289 	// almost copy from JUnitCore
290 	public void run(Request request) {
291 	    Runner runner = request.getRunner();
292 	    Result result = new Result();
293 	    RunListener listener = result.createListener();
294 	    this.notifier.addFirstListener(listener);
295 	    try {
296 		this.notifier.fireTestRunStarted(runner.getDescription());
297 		runner.run(this.notifier);
298 		this.notifier.fireTestRunFinished(result);
299 	    } finally {
300 		this.notifier.removeListener(listener);
301 	    }
302 	}
303 
304 	public void pleaseStop() {
305 	    this.notifier.pleaseStop();
306 	}
307 
308     } // class CoreRunner 
309 
310     /* -------------------------------------------------------------------- *
311      * fields.                                                              *
312      * -------------------------------------------------------------------- */
313 
314 
315     /**
316      * The action to open a new testclass. 
317      */
318     private final  OpenAction  openAction;
319 
320     /**
321      * The action to run a testcase. 
322      */
323     private final StartAction startAction;
324 
325     /**
326      * The action to stop after having finished the currently running testcase. 
327      */
328     private final  StopAction  stopAction;
329 
330     /**
331      * The action to break execution of testcases. 
332      * Tries to break the currently running testcase 
333      * and does not go on with further testcases. 
334      */
335     private final BreakAction breakAction;
336 
337     private final GUIRunner guiRunner;
338 
339     private CoreRunner coreRunner;
340 
341     private final ExtRunListener listener;
342 
343  
344     /**
345      * Defines whether a test is running. 
346      * This is used to activate/deactivate the actions 
347      * {@link #openAction}, {@link #startAction}, {@link #stopAction} 
348      * and {@link #breakAction}. 
349      */
350     private boolean isRunning;
351 
352     // to run a single testcase **** may be null 
353     // which signifies that all tests shall be executed. 
354     // shall be replaced by compounds. 
355     /**
356      * Defines the filter for tests to be run. 
357      * **** may be null 
358      *
359      * @see #setFilter(Description)
360      * @see Actions.CoreRunner#run()
361      */
362     private Description filter;
363 
364     /* -------------------------------------------------------------------- *
365      * constructor.                                                         *
366      * -------------------------------------------------------------------- */
367 
368     /**
369      * Creates a new <code>Actions</code> instance.
370      *
371      * @param testClassName
372      *    the name of the test class. 
373      */
374     @SuppressWarnings("checkstyle:nowhitespaceafter")
375     public Actions(String testClassName) {
376 
377 	this. openAction = new  OpenAction();
378 	this.startAction = new StartAction();
379 	this. stopAction = new  StopAction();
380 	this.breakAction = new BreakAction();
381 
382 	this.guiRunner  = new GUIRunner(this);
383 	this.listener   = new SeqRunListener(this.guiRunner);
384 
385 	this.coreRunner = new CoreRunner(testClassName);
386 	this.isRunning  = false;
387 	this.filter     = null; // **** to be reworked 
388     }
389 
390     /* -------------------------------------------------------------------- *
391      * methods.                                                             *
392      * -------------------------------------------------------------------- */
393 
394 
395     private static boolean descShouldRun(Description desc, 
396 					 Description desiredDesc) {
397 	assert desc.isTest();
398 	if (desiredDesc.isTest()) {
399 	    return desiredDesc.equals(desc);
400 	}
401 	for (Description each : desiredDesc.getChildren()) {
402 	    if (descShouldRun(desc, each)) {
403 		return true;
404 	    }
405 	}
406 	return false;
407     }
408 
409     // required only because of a bug in junit. 
410     /**
411      * Returns a {@code Filter} that only runs methods in 
412      * desiredDesc. 
413      *
414      * @param desiredDesc
415      *    a description of the tests to be run. 
416      * @return
417      *    a {@code Filter} that only runs methods in <code>desiredDesc</code>. 
418      */
419     public static Filter desc2filter(final Description desiredDesc) {
420         return new Filter() {
421             @Override
422             public boolean shouldRun(Description desc) {
423 		return descShouldRun(desc, desiredDesc);
424                }
425 
426             @Override
427             public String describe() {
428                 return String.format("Methods %s", 
429 				     desiredDesc.getDisplayName());
430             }
431         };
432     }
433 
434 
435     /**
436      * The fundamental method to start tests with the underlying JUnit-GUI. 
437      * The test class is supposed to define a method <code>main</code> 
438      * with body <code>Actions.run(&lt;testclass&gt;.class);</code>. 
439      *
440      * @param testClassName
441      *    the name of the test class to represent and run. 
442      * @see JUnitSingleTester
443      * @see #runFromMain()
444      */
445     static void runTestClass(final String testClassName) {
446 	Runnable guiCreator = new Runnable() {
447 		public void run() {
448 		    // parameter null is nowhere used. 
449 		    new Actions(testClassName)
450 			.startAction.actionPerformed(null);
451 		}
452         };
453  
454         // execute in Event-Dispatch-Thread 
455         SwingUtilities.invokeLater(guiCreator);
456     }
457 
458     /**
459      * The fundamental method to start tests with the underlying JUnit-GUI. 
460      * The test class is supposed to define a method <code>main</code> 
461      * with body <code>Actions.runFromMain();</code>. 
462      * Essentially invokes {@link #runTestClass(String)} 
463      * with the proper test class name. 
464      */
465     public static void runFromMain() {
466 	runTestClass(new Throwable().getStackTrace()[1].getClassName());
467 	// TBD: use instead
468 	// Thread.currentThread().getStackTrace()[1].getClassName()
469 	// avoiding creating a throwable which is nowhere used. 
470     }
471 
472     GUIRunner getRunner() {
473 	return this.guiRunner;
474     }
475 
476     /**
477      * Sets {@link #filter} according to <code>filter</code>. 
478      *
479      * @param filter
480      *    the filter for the tests to be run 
481      */
482     void setFilter(Description filter) {
483 	assert filter != null;
484 	this.filter = filter;
485     }
486 
487     /**
488      * Updates the action-enablements 
489      * depending on whether a test is running or not: 
490      * Open and Start action are enabled iff no test is running, 
491      * whereas stop and break actions are enabled iff some test is running. 
492      *
493      * @param isRunning
494      *    whether a test is running. 
495      */
496     @SuppressWarnings("checkstyle:nowhitespaceafter")
497     void setEnableForRun(boolean isRunning) {	
498 	assert this.isRunning ^ isRunning;
499 	this.isRunning = isRunning;
500 	this. openAction.setEnabled(!this.isRunning);
501 	this.startAction.setEnabled(!this.isRunning);
502 	this. stopAction.setEnabled( this.isRunning);
503 	this.breakAction.setEnabled( this.isRunning); //(true);
504     }
505 
506     AbstractAction getOpenAction() {
507 	return this.openAction;
508     }
509 
510     AbstractAction getStartAction() {
511 	return this.startAction;
512     }
513 
514     AbstractAction getStopAction() {
515 	return this.stopAction;
516     }
517 
518     AbstractAction getBreakAction() {
519 	return this.breakAction;
520     }
521 
522     AbstractAction getExitAction() {
523 	return new ExitAction();
524     }
525 
526     public static void main(String[] args) {
527 	runFromMain();
528     }
529 }