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 
38   * and {@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. original argument: deprecated?
162 	    Actions.this.coreRunner.stop();//new StoppedByUserException()
163 	}
164     } // class BreakAction 
165 
166 
167     /**
168      * The action of exiting the tester application. 
169      */
170     static class ExitAction extends AbstractAction {
171 
172 	private static final long serialVersionUID = -589L;
173 
174 	ExitAction() {
175 	    super("Exit", GifResource.getIcon(Delete.class));
176 	    putValue(SHORT_DESCRIPTION, 
177 		     "Quits this application immediately. ");
178             putValue(MNEMONIC_KEY, KeyEvent.VK_E);
179             putValue(ACCELERATOR_KEY,
180 		     KeyStroke.getKeyStroke(KeyEvent.VK_E,
181 					    ActionEvent.CTRL_MASK));
182 	}
183 
184 	@edu.umd.cs.findbugs.annotations.SuppressWarnings
185 	(value = "DM_EXIT", 
186 	 justification = "To ensure safe exit " + 
187 	 "not reached by throwing exception. " + 
188 	 "Also Actions is not invoked by other code. ")
189 	public void actionPerformed(ActionEvent event) {
190 	    //throw new RuntimeException("Exit by user action. ");
191 	    System.exit(0);
192 	}
193     } // class ExitAction 
194 
195     /**
196      * A thread in which a testclass is executed 
197      * or at least a single testcase out of this testclass. 
198      * Essentially, this is defined by the class {@link #testClassName}. 
199      * The major task of this class is, to reload {@link #testClassName} 
200      * using a classloader which allows reloading 
201      * each test run without restarting the tester application. 
202      * The core of the code is copied from {@link JUnitCore}. 
203      */
204     class CoreRunner extends Thread {
205 
206 	/* ---------------------------------------------------------------- *
207 	 * fields.                                                          *
208 	 * ---------------------------------------------------------------- */
209 
210 	/**
211 	 * The notifier to run the tests as in JUnitCore. 
212 	 */
213 	private final RunNotifier notifier;
214 
215 	/**
216 	 * The name of the class to be tested. 
217 	 */
218 	private final String testClassName;
219 
220 	/* ---------------------------------------------------------------- *
221 	 * constructors.                                                    *
222 	 * ---------------------------------------------------------------- */
223 
224 	/**
225 	 * Creates a runner running all testcases in the given test class. 
226 	 */
227 	CoreRunner(String testClassName)  {
228 	    this.notifier = new RunNotifier();
229 	    this.notifier.addListener(Actions.this.listener);
230 
231 	    this.testClassName = testClassName;
232 	}
233 
234 	/**
235 	 * Copy constructor. 
236 	 */
237 	CoreRunner(CoreRunner other)  {
238 	    this(other.testClassName);
239 	}
240 
241 	/* ---------------------------------------------------------------- *
242 	 * methods.                                                         *
243 	 * ---------------------------------------------------------------- */
244 
245 
246 	// api-docs inherited from Runnable 
247 	// overwrites void implementation provided by class Thread 
248 	/**
249 	 * Loads the class with name {@link #testClassName} 
250 	 * with a {@link TestCaseClassLoader} to allow reloading. 
251 	 * Then creates a {@link Request} filtering it with {@link #filter} 
252 	 * defining the tests to be run and runs those tests 
253 	 * invoking {@link #run(Request)}. 
254 	 */
255 	public void run() {
256 	    Class<?> newTestClass = null;
257 	    try {
258 		newTestClass = new TestCaseClassLoader()
259 		    .loadClass(this.testClassName, true);
260 	    } catch (ClassNotFoundException e) {
261 		throw new IllegalStateException// NOPMD
262 		    ("Testclass '" + this.testClassName + "' disappeared. ");
263 		// **** This makes the problem 
264 		// that in the GUI the run button is shaded 
265 		// (and the stop button is not). 
266 	    }
267 	    assert newTestClass != null;
268 
269 	    Request request = Request.aClass(newTestClass);
270 	    //Request request = Request.classes(newTestClass);
271 	    if (Actions.this.filter != null && Actions.this.filter.isTest()) {
272 		request = request.filterWith(Actions.this.filter);
273 	    } else {
274 		Actions.this.listener.testClassStructureLoaded
275 		    (request.getRunner().getDescription());
276 	    }
277 
278 	    try {
279 		run(request);
280 	    } catch (StoppedByUserException ee) {
281 		// either Break or Stop 
282   		Actions.this.listener.testRunAborted();
283   	    }
284 
285 	    //System.out.println("...Core run()"+this.core);
286 	}
287 
288 	// almost copy from JUnitCore
289 	public void run(Request request) {
290 	    Runner runner = request.getRunner();
291 	    Result result = new Result();
292 	    RunListener listener = result.createListener();
293 	    this.notifier.addFirstListener(listener);
294 	    try {
295 		this.notifier.fireTestRunStarted(runner.getDescription());
296 		runner.run(this.notifier);
297 		this.notifier.fireTestRunFinished(result);
298 	    } finally {
299 		this.notifier.removeListener(listener);
300 	    }
301 	}
302 
303 	public void pleaseStop() {
304 	    this.notifier.pleaseStop();
305 	}
306 
307     } // class CoreRunner 
308 
309     /* -------------------------------------------------------------------- *
310      * fields.                                                              *
311      * -------------------------------------------------------------------- */
312 
313 
314     /**
315      * The action to open a new testclass. 
316      */
317     private final  OpenAction  openAction;
318 
319     /**
320      * The action to run a testcase. 
321      */
322     private final StartAction startAction;
323 
324     /**
325      * The action to stop after having finished the currently running testcase. 
326      */
327     private final  StopAction  stopAction;
328 
329     /**
330      * The action to break execution of testcases. 
331      * Tries to break the currently running testcase 
332      * and does not go on with further testcases. 
333      */
334     private final BreakAction breakAction;
335 
336     private final GUIRunner guiRunner;
337 
338     private CoreRunner coreRunner;
339 
340     private final ExtRunListener listener;
341 
342  
343     /**
344      * Defines whether a test is running. 
345      * This is used to activate/deactivate the actions 
346      * {@link #openAction}, {@link #startAction}, {@link #stopAction} 
347      * and {@link #breakAction}. 
348      */
349     private boolean isRunning;
350 
351     // to run a single testcase **** may be null 
352     // which signifies that all tests shall be executed. 
353     // shall be replaced by compounds. 
354     /**
355      * Defines the filter for tests to be run. 
356      * **** may be null 
357      *
358      * @see #setFilter(Description)
359      * @see Actions.CoreRunner#run()
360      */
361     private Description filter;
362 
363     /* -------------------------------------------------------------------- *
364      * constructor.                                                         *
365      * -------------------------------------------------------------------- */
366 
367     /**
368      * Creates a new <code>Actions</code> instance.
369      *
370      * @param testClassName
371      *    the name of the test class. 
372      */
373     @SuppressWarnings("checkstyle:nowhitespaceafter")
374     public Actions(String testClassName) {
375 
376 	this. openAction = new  OpenAction();
377 	this.startAction = new StartAction();
378 	this. stopAction = new  StopAction();
379 	this.breakAction = new BreakAction();
380 
381 	this.guiRunner  = new GUIRunner(this);
382 	this.listener   = new SeqRunListener(this.guiRunner);
383 
384 	this.coreRunner = new CoreRunner(testClassName);
385 	this.isRunning  = false;
386 	this.filter     = null; // **** to be reworked 
387     }
388 
389     /* -------------------------------------------------------------------- *
390      * methods.                                                             *
391      * -------------------------------------------------------------------- */
392 
393 
394     private static boolean descShouldRun(Description desc, 
395 					 Description desiredDesc) {
396 	assert desc.isTest();
397 	if (desiredDesc.isTest()) {
398 	    return desiredDesc.equals(desc);
399 	}
400 	for (Description each : desiredDesc.getChildren()) {
401 	    if (descShouldRun(desc, each)) {
402 		return true;
403 	    }
404 	}
405 	return false;
406     }
407 
408     // required only because of a bug in junit. 
409     /**
410      * Returns a {@code Filter} that only runs methods in 
411      * desiredDesc. 
412      *
413      * @param desiredDesc
414      *    a description of the tests to be run. 
415      * @return
416      *    a {@code Filter} that only runs methods in <code>desiredDesc</code>. 
417      */
418     public static Filter desc2filter(final Description desiredDesc) {
419         return new Filter() {
420             @Override
421             public boolean shouldRun(Description desc) {
422 		return descShouldRun(desc, desiredDesc);
423                }
424 
425             @Override
426             public String describe() {
427                 return String.format("Methods %s", 
428 				     desiredDesc.getDisplayName());
429             }
430         };
431     }
432 
433 
434     /**
435      * The fundamental method to start tests with the underlying JUnit-GUI. 
436      * The test class is supposed to define a method <code>main</code> 
437      * with body <code>Actions.run(&lt;testclass&gt;.class);</code>. 
438      *
439      * @param testClassName
440      *    the name of the test class to represent and run. 
441      * @see JUnitSingleTester
442      * @see #runFromMain()
443      */
444     static void runTestClass(final String testClassName) {
445 	Runnable guiCreator = new Runnable() {
446 		public void run() {
447 		    // parameter null is nowhere used. 
448 		    new Actions(testClassName)
449 			.startAction.actionPerformed(null);
450 		}
451         };
452  
453         // execute in Event-Dispatch-Thread 
454         SwingUtilities.invokeLater(guiCreator);
455     }
456 
457     /**
458      * The fundamental method to start tests with the underlying JUnit-GUI. 
459      * The test class is supposed to define a method <code>main</code> 
460      * with body <code>Actions.runFromMain();</code>. 
461      * Essentially invokes {@link #runTestClass(String)} 
462      * with the proper test class name. 
463      */
464     public static void runFromMain() {
465 	runTestClass(new Throwable().getStackTrace()[1].getClassName());
466 	// TBD: use instead
467 	// Thread.currentThread().getStackTrace()[1].getClassName()
468 	// avoiding creating a throwable which is nowhere used. 
469     }
470 
471     GUIRunner getRunner() {
472 	return this.guiRunner;
473     }
474 
475     /**
476      * Sets {@link #filter} according to <code>filter</code>. 
477      *
478      * @param filter
479      *    the filter for the tests to be run 
480      */
481     void setFilter(Description filter) {
482 	assert filter != null;
483 	this.filter = filter;
484     }
485 
486     /**
487      * Updates the action-enablements 
488      * depending on whether a test is running or not: 
489      * Open and Start action are enabled iff no test is running, 
490      * whereas stop and break actions are enabled iff some test is running. 
491      *
492      * @param isRunning
493      *    whether a test is running. 
494      */
495     @SuppressWarnings("checkstyle:nowhitespaceafter")
496     void setEnableForRun(boolean isRunning) {	
497 	assert this.isRunning ^ isRunning;
498 	this.isRunning = isRunning;
499 	this. openAction.setEnabled(!this.isRunning);
500 	this.startAction.setEnabled(!this.isRunning);
501 	this. stopAction.setEnabled( this.isRunning);
502 	this.breakAction.setEnabled( this.isRunning); //(true);
503     }
504 
505     AbstractAction getOpenAction() {
506 	return this.openAction;
507     }
508 
509     AbstractAction getStartAction() {
510 	return this.startAction;
511     }
512 
513     AbstractAction getStopAction() {
514 	return this.stopAction;
515     }
516 
517     AbstractAction getBreakAction() {
518 	return this.breakAction;
519     }
520 
521     AbstractAction getExitAction() {
522 	return new ExitAction();
523     }
524 
525     public static void main(String[] args) {
526 	runFromMain();
527     }
528 }