KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > edu > rice > cs > drjava > model > junit > DefaultJUnitModel


1 /*BEGIN_COPYRIGHT_BLOCK
2  *
3  * This file is part of DrJava. Download the current version of this project from http://www.drjava.org/
4  * or http://sourceforge.net/projects/drjava/
5  *
6  * DrJava Open Source License
7  *
8  * Copyright (C) 2001-2005 JavaPLT group at Rice University (javaplt@rice.edu). All rights reserved.
9  *
10  * Developed by: Java Programming Languages Team, Rice University, http://www.cs.rice.edu/~javaplt/
11  *
12  * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
13  * documentation files (the "Software"), to deal with the Software without restriction, including without limitation
14  * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
15  * to permit persons to whom the Software is furnished to do so, subject to the following conditions:
16  *
17  * - Redistributions of source code must retain the above copyright notice, this list of conditions and the
18  * following disclaimers.
19  * - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
20  * following disclaimers in the documentation and/or other materials provided with the distribution.
21  * - Neither the names of DrJava, the JavaPLT, Rice University, nor the names of its contributors may be used to
22  * endorse or promote products derived from this Software without specific prior written permission.
23  * - Products derived from this software may not be called "DrJava" nor use the term "DrJava" as part of their
24  * names without prior written permission from the JavaPLT group. For permission, write to javaplt@rice.edu.
25  *
26  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
27  * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28  * CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
29  * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
30  * WITH THE SOFTWARE.
31  *
32 END_COPYRIGHT_BLOCK*/

33
34 package edu.rice.cs.drjava.model.junit;
35
36 import java.io.File JavaDoc;
37 import java.io.IOException JavaDoc;
38
39 import java.util.List JavaDoc;
40 import java.util.LinkedList JavaDoc;
41 import java.util.ArrayList JavaDoc;
42 import java.util.Arrays JavaDoc;
43 import java.util.HashMap JavaDoc;
44 import java.util.HashSet JavaDoc;
45 import java.util.Set JavaDoc;
46
47 import javax.swing.JOptionPane JavaDoc;
48 import javax.swing.SwingUtilities JavaDoc;
49
50 import edu.rice.cs.drjava.DrJava;
51 import edu.rice.cs.drjava.model.GlobalModel;
52 import edu.rice.cs.drjava.model.FileMovedException;
53 import edu.rice.cs.drjava.model.OpenDefinitionsDocument;
54 import edu.rice.cs.drjava.model.repl.newjvm.MainJVM;
55 import edu.rice.cs.drjava.model.compiler.CompilerModel;
56 import edu.rice.cs.drjava.model.compiler.CompilerListener;
57 import edu.rice.cs.drjava.model.compiler.DummyCompilerListener;
58 import edu.rice.cs.drjava.model.definitions.InvalidPackageException;
59
60 //import edu.rice.cs.util.ExitingNotAllowedException;
61
import edu.rice.cs.plt.io.IOUtil;
62 import edu.rice.cs.util.ClassPathVector;
63 import edu.rice.cs.util.UnexpectedException;
64 import edu.rice.cs.util.classloader.ClassFileError;
65 import edu.rice.cs.util.text.SwingDocument;
66 import edu.rice.cs.util.swing.Utilities;
67
68 import org.apache.bcel.classfile.*;
69
70 import static edu.rice.cs.drjava.config.OptionConstants.*;
71
72 /** Manages unit testing via JUnit.
73   * @version $Id: DefaultJUnitModel.java 4099 2007-01-30 22:37:16Z dlsmith $
74   */

75 public class DefaultJUnitModel implements JUnitModel, JUnitModelCallback {
76   
77   /** Manages listeners to this model. */
78   private final JUnitEventNotifier _notifier = new JUnitEventNotifier();
79   
80   /** RMI interface to a secondary JVM for running tests. Using a second JVM prevents interactions and tests from
81     * corrupting the state of DrJava.
82     */

83   private final MainJVM _jvm;
84   
85   /** The compiler model. It contains a lock used to prevent simultaneous test and compile. It also tracks the number
86     * errors in the last compilation, which is required information if junit forces compilation.
87     */

88   private final CompilerModel _compilerModel;
89   
90   /** The global model to which the JUnitModel belongs */
91   private final GlobalModel _model;
92   
93   /** The error model containing all current JUnit errors. */
94   private volatile JUnitErrorModel _junitErrorModel;
95   
96   /** State flag to prevent starting new tests on top of old ones */
97   private volatile boolean _testInProgress = false;
98   
99   /** State flag to record if test classes in projects must end in "Test" */
100   private boolean _forceTestSuffix = false;
101   
102   /** The document used to display JUnit test results. Used only for testing. */
103   private final SwingDocument _junitDoc = new SwingDocument();
104   
105   /** Main constructor.
106     * @param jvm RMI interface to a secondary JVM for running tests
107     * @param compilerModel the CompilerModel, used only as a lock to prevent simultaneous test and compile
108     * @param model used only for getSourceFile
109     */

110   public DefaultJUnitModel(MainJVM jvm, CompilerModel compilerModel, GlobalModel model) {
111     _jvm = jvm;
112     _compilerModel = compilerModel;
113     _model = model;
114     _junitErrorModel = new JUnitErrorModel(new JUnitError[0], _model, false);
115   }
116   
117   //-------------------------- Field Setters --------------------------------//
118

119   public void setForceTestSuffix(boolean b) { _forceTestSuffix = b; }
120   
121   //------------------------ Simple Predicates ------------------------------//
122

123   public boolean isTestInProgress() { return _testInProgress; }
124
125   //------------------------Listener Management -----------------------------//
126

127   /** Add a JUnitListener to the model.
128    * @param listener a listener that reacts to JUnit events
129    */

130   public void addListener(JUnitListener listener) { _notifier.addListener(listener); }
131   
132   /** Remove a JUnitListener from the model. If the listener is not currently listening to this model, this method
133    * has no effect.
134    * @param listener a listener that reacts to JUnit events
135    */

136   public void removeListener(JUnitListener listener) { _notifier.removeListener(listener); }
137   
138   /** Removes all JUnitListeners from this model. */
139   public void removeAllListeners() { _notifier.removeAllListeners(); }
140   
141
142   
143   //-------------------------------- Triggers --------------------------------//
144

145   /** Used only for testing. */
146   public SwingDocument getJUnitDocument() { return _junitDoc; }
147   
148   /** Creates a JUnit test suite over all currently open documents and runs it. If the class file
149    * associated with a file is not a test case, it is ignored.
150    */

151   public void junitAll() { junitDocs(_model.getOpenDefinitionsDocuments()); }
152   
153   /** Creates a JUnit test suite over all currently open documents and runs it. If a class file associated with a
154    * source file is not a test case, it will be ignored. Synchronized against the compiler model to prevent
155    * testing and compiling at the same time, which would create invalid results.
156    */

157   public void junitProject() {
158     LinkedList JavaDoc<OpenDefinitionsDocument> lod = new LinkedList JavaDoc<OpenDefinitionsDocument>();
159     
160     for (OpenDefinitionsDocument doc : _model.getOpenDefinitionsDocuments()) {
161       if (doc.inProjectPath()) lod.add(doc);
162     }
163     junitDocs(lod);
164   }
165   
166   /** Forwards the classnames and files to the test manager to test all of them; does not notify
167    * since we don't have ODD's to send out with the notification of junit start.
168    * @param qualifiedClassnames a list of all the qualified class names to test.
169    * @param files a list of their source files in the same order as qualified class names.
170    */

171   public void junitClasses(List JavaDoc<String JavaDoc> qualifiedClassnames, List JavaDoc<File JavaDoc> files) {
172     Utilities.showDebug("junitClasses(" + qualifiedClassnames + ", " + files);
173     synchronized(_compilerModel.getCompilerLock()) {
174       
175       // Check _testInProgress
176
if (_testInProgress) return;
177       
178       List JavaDoc<String JavaDoc> testClasses;
179       try { testClasses = _jvm.findTestClasses(qualifiedClassnames, files); }
180       catch(IOException JavaDoc e) { throw new UnexpectedException(e); }
181       
182 // System.err.println("Found test classes: " + testClasses);
183

184       if (testClasses.isEmpty()) {
185         nonTestCase(true);
186         return;
187       }
188       _notifier.junitClassesStarted();
189       try { _jvm.runTestSuite(); }
190       catch(Throwable JavaDoc t) {
191 // System.err.println("Threw exception " + t);
192
_notifier.junitEnded();
193         _testInProgress = false;
194         throw new UnexpectedException(t);
195       }
196     }
197   }
198   
199   public void junitDocs(List JavaDoc<OpenDefinitionsDocument> lod) { junitOpenDefDocs(lod, true); }
200   
201   /** Runs JUnit on the current document. Forces the user to compile all open source documents before proceeding. */
202   public void junit(OpenDefinitionsDocument doc) throws ClassNotFoundException JavaDoc, IOException JavaDoc {
203 // new ScrollableDialog(null, "junit(" + doc + ") called in DefaultJunitModel", "", "").show();
204
File JavaDoc testFile;
205     try {
206       testFile = doc.getFile();
207       if (testFile == null) { // document is untitiled: abort unit testing and return
208
nonTestCase(false);
209         return;
210       }
211     }
212     catch(FileMovedException fme) { /* do nothing */ }
213     
214     LinkedList JavaDoc<OpenDefinitionsDocument> lod = new LinkedList JavaDoc<OpenDefinitionsDocument>();
215     lod.add(doc);
216     junitOpenDefDocs(lod, false);
217   }
218   
219   /** Ensures that all documents have been compiled since their last modification and then delegates the actual testing
220    * to _rawJUnitOpenTestDocs. */

221   private void junitOpenDefDocs(final List JavaDoc<OpenDefinitionsDocument> lod, final boolean allTests) {
222     // If a test is running, don't start another one.
223

224 // System.err.println("junitOpenDefDocs(" + lod + "," + allTests + ")");
225

226     // Check_testInProgress flag
227
if (_testInProgress) return;
228
229     //reset the JUnitErrorModel, fixes bug #907211 "Test Failures Not Cleared Properly".
230
_junitErrorModel = new JUnitErrorModel(new JUnitError[0], null, false);
231
232     // Gets system classpaths from the main JVM so that junit tests can find every class file.
233
// Given as one long String, this separates the paths into a list of strings. 3/12/05
234

235 // LinkedList<String> classpaths = separateClasspath(getClasspath().toString());
236

237     // new ScrollableDialog(null, "classpaths assembled in junitOpenDefDocs: " + classpaths, "", "").show();
238

239     if (_model.hasOutOfSyncDocuments() || _model.hasModifiedDocuments()) {
240       /* hasOutOfSyncDocments() can return false when some documents have not been successfully compiled; the granularity
241          of time-stamping and the presence of multiple classes in a file (some of which compile successfully) can produce
242          \false reports. */

243 // System.err.println("Out of sync documents exist");
244

245         CompilerListener testAfterCompile = new DummyCompilerListener() {
246           @Override JavaDoc public void compileEnded(File JavaDoc workDir, List JavaDoc<? extends File JavaDoc> excludedFiles) {
247             final CompilerListener listenerThis = this;
248             try {
249               if (_model.hasOutOfSyncDocuments() || _model.getNumCompErrors() > 0) {
250                 if (! Utilities.TEST_MODE)
251                   JOptionPane.showMessageDialog(null, "All open files must be compiled before running a unit test",
252                                               "Must Compile All Before Testing", JOptionPane.ERROR_MESSAGE);
253                 nonTestCase(allTests);
254                 return;
255               }
256               _rawJUnitOpenDefDocs(lod, allTests);
257             }
258             finally { // always remove this listener after its first execution
259
SwingUtilities.invokeLater(new Runnable JavaDoc() {
260                 public void run() { _compilerModel.removeListener(listenerThis); }
261               });
262             }
263           }
264         };
265         
266 // Utilities.show("Notifying JUnitModelListener");
267
_notifier.compileBeforeJUnit(testAfterCompile);
268       }
269       
270       else _rawJUnitOpenDefDocs(lod, allTests);
271   }
272   
273   /** Runs all TestCases in the document list lod; assumes all documents have been compiled. It finds the TestCase
274    * classes by searching the build directories for the documents. */

275   private void _rawJUnitOpenDefDocs(List JavaDoc<OpenDefinitionsDocument> lod, boolean allTests) {
276
277     File JavaDoc buildDir = _model.getBuildDirectory();
278 // System.err.println("Build directory is " + buildDir);
279

280     /** Open java source files */
281     HashSet JavaDoc<String JavaDoc> openDocFiles = new HashSet JavaDoc<String JavaDoc>();
282     
283     /** A map whose keys are directories containing class files corresponding to open java source files.
284      * Their values are the corresponding source roots.
285      */

286     HashMap JavaDoc<File JavaDoc, File JavaDoc> classDirsAndRoots = new HashMap JavaDoc<File JavaDoc, File JavaDoc>();
287     
288     // Initialize openDocFiles and classDirsAndRoots
289
// All packageNames should be valid because all source files are compiled
290

291     for (OpenDefinitionsDocument doc: lod) /* for all nonEmpty documents in lod */ {
292       if (doc.isSourceFile()) { // excludes Untitled documents and open non-source files
293
try {
294           File JavaDoc sourceRoot = doc.getSourceRoot(); // may throw an InvalidPackageException
295

296           // doc has valid package name; add it to list of open java source doc files
297
openDocFiles.add(doc.getCanonicalPath());
298         
299           String JavaDoc packagePath = doc.getPackageName().replace('.', File.separatorChar);
300           
301           // Add (canonical path name for) build directory for doc to classDirs
302

303           File JavaDoc buildRoot = (buildDir == null) ? sourceRoot: buildDir;
304           
305           File JavaDoc classFileDir = new File JavaDoc(IOUtil.attemptCanonicalFile(buildRoot), packagePath);
306           
307           File JavaDoc sourceDir =
308             (buildDir == null) ? classFileDir : new File JavaDoc(IOUtil.attemptCanonicalFile(sourceRoot), packagePath);
309           
310           if (! classDirsAndRoots.containsKey(classFileDir)) {
311             classDirsAndRoots.put(classFileDir, sourceDir);
312 // System.err.println("Adding " + classFileDir + " with source root " + sourceRoot +
313
// " to list of class directories");
314
}
315         }
316         catch (InvalidPackageException e) { /* Skip the file, since it doesn't have a valid package */ }
317       }
318     }
319     
320 // System.err.println("classDirs = " + classDirsAndRoots.keySet());
321

322     /** set of dirs potentially containing test classes */
323     Set JavaDoc<File JavaDoc> classDirs = classDirsAndRoots.keySet();
324     
325 // System.err.println("openDocFiles = " + openDocFiles);
326

327     /* Names of test classes. */
328     ArrayList JavaDoc<String JavaDoc> classNames = new ArrayList JavaDoc<String JavaDoc>();
329     
330     /* Source files corresonding to potential test class files */
331     ArrayList JavaDoc<File JavaDoc> files = new ArrayList JavaDoc<File JavaDoc>();
332     
333     /* Flag indicating if project is open */
334     boolean isProject = _model.isProjectActive();
335
336     try {
337       for (File JavaDoc dir: classDirs) { // foreach class file directory
338
// System.err.println("Examining directory " + dir);
339

340         File JavaDoc[] listing = dir.listFiles();
341         
342 // System.err.println("Directory contains the files: " + Arrays.asList(listing));
343

344         if (listing != null) { // listFiles may return null if there's an IO error
345
for (File JavaDoc entry : listing) { /* for each class file in the build directory */
346             
347 // System.err.println("Examining file " + entry);
348

349             /* ignore non-class files */
350             String JavaDoc name = entry.getName();
351             if (! name.endsWith(".class")) continue;
352             
353             /* In projects, ignore class names that do not end in "Test" if FORCE_TEST_SUFFIX option is set */
354             if (_forceTestSuffix) {
355               String JavaDoc noExtName = name.substring(0, name.length() - 6); // remove ".class" from name
356
int indexOfLastDot = noExtName.lastIndexOf('.');
357               String JavaDoc simpleClassName = noExtName.substring(indexOfLastDot + 1);
358 // System.err.println("Simple class name is " + simpleClassName);
359
if (isProject && ! simpleClassName.endsWith("Test")) continue;
360             }
361             
362 // System.err.println("Found test class: " + noExtName);
363

364             /* ignore entries that do not correspond to files? Can this happen? */
365             if (! entry.isFile()) continue;
366             
367             // Add this class and the corrresponding source file to classNames and files, respectively.
368
// Finding the source file is non-trivial because it may be a language-levels file
369

370             try {
371               JavaClass clazz = new ClassParser(entry.getCanonicalPath()).parse();
372               String JavaDoc className = clazz.getClassName(); // get classfile name
373
// System.err.println("looking for source file for: " + className);
374
int indexOfDot = className.lastIndexOf('.');
375               
376               File JavaDoc rootDir = classDirsAndRoots.get(dir);
377               
378               /** The canonical pathname for the file (including the file name) */
379               String JavaDoc javaSourceFileName = rootDir.getCanonicalPath() + File.separator + clazz.getSourceFileName();
380 // System.err.println("Full java source fileName = " + javaSourceFileName);
381

382               /* The index in fileName of the dot preceding the extension ".java", ".dj0*, ".dj1", or ".dj2" */
383               int indexOfExtDot = javaSourceFileName.lastIndexOf('.');
384 // System.err.println("indexOfExtDot = " + indexOfExtDot);
385
if (indexOfExtDot == -1) continue; // RMI stub class files return source file names without extensions
386
// System.err.println("File found in openDocFiles = " + openDocFiles.contains(sourceFileName));
387

388               /* Determine if this java source file was generated from a language levels file. */
389               String JavaDoc strippedName = javaSourceFileName.substring(0, indexOfExtDot);
390 // System.err.println("Stripped name = " + strippedName);
391

392               String JavaDoc sourceFileName;
393               
394               if (openDocFiles.contains(javaSourceFileName)) sourceFileName = javaSourceFileName;
395               else if (openDocFiles.contains(strippedName + ".dj0")) sourceFileName = strippedName + ".dj0";
396               else if (openDocFiles.contains(strippedName + ".dj1")) sourceFileName = strippedName + ".dj1";
397               else if (openDocFiles.contains(strippedName + ".dj2")) sourceFileName = strippedName + ".dj2";
398               else continue; // no matching source file is open
399

400               File JavaDoc sourceFile = new File JavaDoc(sourceFileName);
401               classNames.add(className);
402               files.add(sourceFile);
403 // System.err.println("Class " + className + "added to classNames. File " + sourceFileName + " added to files.");
404
}
405             catch(IOException JavaDoc e) { /* ignore it; can't read class file */ }
406             catch(ClassFormatException e) { /* ignore it; class file is bad */ }
407           }
408         }
409       }
410     }
411     catch(Throwable JavaDoc t) {
412 // new ScrollableDialog(null, "UnexceptedExceptionThrown", t.toString(), "").show();
413
throw new UnexpectedException(t);
414     }
415 // finally {
416
// new ScrollableDialog(null, "junit setup loop terminated", classNames.toString(), "").show();
417
// }
418

419     // synchronized over _compilerModel to ensure that compilation and junit testing are mutually exclusive.
420
// TODO: should we disable compile commands while testing? Should we use protected flag instead of lock?
421

422     synchronized(_compilerModel.getCompilerLock()) {
423       /** Set up junit test suite on slave JVM; get TestCase classes forming that suite */
424       List JavaDoc<String JavaDoc> tests;
425       try { tests = _jvm.findTestClasses(classNames, files); }
426       catch(IOException JavaDoc e) { throw new UnexpectedException(e); }
427       
428       if (tests == null || tests.isEmpty()) {
429 // Utilities.show("Set of test classes is empty!");
430
nonTestCase(allTests);
431         return;
432       }
433       
434       try {
435         /** Run the junit test suite that has already been set up on the slave JVM */
436         _notifier.junitStarted(); // notify listeners that JUnit testing has finally started!
437
// new ScrollableDialog(null, "junitStarted executed in DefaultJunitModel", "", "").show();
438
_jvm.runTestSuite();
439         
440       }
441       catch(Throwable JavaDoc t) {
442         // Probably a java.rmi.UnmarshalException caused by the interruption of unit testing.
443
// Swallow the exception and proceed.
444
_notifier.junitEnded(); // balances junitStarted()
445
_testInProgress = false;
446         throw new UnexpectedException(t);
447       }
448     }
449   }
450   
451   //-------------------------------- Helpers --------------------------------//
452

453   //----------------------------- Error Results -----------------------------//
454

455   /** Gets the JUnitErrorModel, which contains error info for the last test run. */
456   public JUnitErrorModel getJUnitErrorModel() { return _junitErrorModel; }
457   
458   /** Resets the junit error state to have no errors. */
459   public void resetJUnitErrors() {
460     _junitErrorModel = new JUnitErrorModel(new JUnitError[0], _model, false);
461   }
462   
463   //---------------------------- Model Callbacks ----------------------------//
464

465   /** Called from the JUnitTestManager if its given className is not a test case.
466    * @param isTestAll whether or not it was a use of the test all button
467    */

468   public void nonTestCase(final boolean isTestAll) {
469     // NOTE: junitStarted is called in a different thread from the testing thread. The _testInProgress flag
470
// is used to prevent a new test from being started and overrunning the existing one.
471
// Utilities.show("DefaultJUnitModel.nonTestCase(" + isTestAll + ") called");
472
_notifier.nonTestCase(isTestAll);
473       _testInProgress = false;
474   }
475   
476   /** Called to indicate that an illegal class file was encountered
477    * @param e the ClassFileObject describing the error.
478    */

479   public void classFileError(ClassFileError e) { _notifier.classFileError(e); }
480   
481   /** Called to indicate that a suite of tests has started running.
482    * @param numTests The number of tests in the suite to be run.
483    */

484   public void testSuiteStarted(final int numTests) { _notifier.junitSuiteStarted(numTests); }
485   
486   /** Called when a particular test is started.
487    * @param testName The name of the test being started.
488    */

489   public void testStarted(final String JavaDoc testName) { _notifier.junitTestStarted(testName); }
490   
491   /** Called when a particular test has ended.
492    * @param testName The name of the test that has ended.
493    * @param wasSuccessful Whether the test passed or not.
494    * @param causedError If not successful, whether the test caused an error
495    * or simply failed.
496    */

497   public void testEnded(final String JavaDoc testName, final boolean wasSuccessful, final boolean causedError) {
498      _notifier.junitTestEnded(testName, wasSuccessful, causedError);
499   }
500   
501   /** Called when a full suite of tests has finished running.
502    * @param errors The array of errors from all failed tests in the suite.
503    */

504   public void testSuiteEnded(JUnitError[] errors) {
505 // new ScrollableDialog(null, "DefaultJUnitModel.testSuiteEnded(...) called", "", "").show();
506
_junitErrorModel = new JUnitErrorModel(errors, _model, true);
507     _notifier.junitEnded();
508     _testInProgress = false;
509 // new ScrollableDialog(null, "DefaultJUnitModel.testSuiteEnded(...) finished", "", "").show();
510
}
511   
512   /** Called when the JUnitTestManager wants to open a file that is not currently open.
513    * @param className the name of the class for which we want to find the file
514    * @return the file associated with the given class
515    */

516   public File JavaDoc getFileForClassName(String JavaDoc className) { return _model.getSourceFile(className + ".java"); }
517   
518   /** Returns the current classpath in use by the JUnit JVM, in the form of a path-separator delimited string. */
519   public ClassPathVector getClassPath() { return _jvm.getClassPath(); }
520   
521   /** Called when the JVM used for unit tests has registered. */
522   public void junitJVMReady() {
523    
524     if (! _testInProgress) return;
525     JUnitError[] errors = new JUnitError[1];
526     errors[0] = new JUnitError("Previous test suite was interrupted", true, "");
527     _junitErrorModel = new JUnitErrorModel(errors, _model, true);
528     _notifier.junitEnded();
529     _testInProgress = false;
530   }
531 }
532
Popular Tags