001: /*BEGIN_COPYRIGHT_BLOCK
002: *
003: * Copyright (c) 2001-2007, JavaPLT group at Rice University (javaplt@rice.edu)
004: * All rights reserved.
005: *
006: * Redistribution and use in source and binary forms, with or without
007: * modification, are permitted provided that the following conditions are met:
008: * * Redistributions of source code must retain the above copyright
009: * notice, this list of conditions and the following disclaimer.
010: * * Redistributions in binary form must reproduce the above copyright
011: * notice, this list of conditions and the following disclaimer in the
012: * documentation and/or other materials provided with the distribution.
013: * * Neither the names of DrJava, the JavaPLT group, Rice University, nor the
014: * names of its contributors may be used to endorse or promote products
015: * derived from this software without specific prior written permission.
016: *
017: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
018: * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
019: * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
020: * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
021: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
022: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
023: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
024: * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
025: * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
026: * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
027: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028: *
029: * This software is Open Source Initiative approved Open Source Software.
030: * Open Source Initative Approved is a trademark of the Open Source Initiative.
031: *
032: * This file is part of DrJava. Download the current version of this project
033: * from http://www.drjava.org/ or http://sourceforge.net/projects/drjava/
034: *
035: * END_COPYRIGHT_BLOCK*/
036:
037: package edu.rice.cs.drjava.model.junit;
038:
039: import java.io.File;
040: import java.io.IOException;
041:
042: import java.util.List;
043: import java.util.LinkedList;
044: import java.util.ArrayList;
045: import java.util.Arrays;
046: import java.util.HashMap;
047: import java.util.HashSet;
048: import java.util.Set;
049:
050: import javax.swing.JOptionPane;
051: import javax.swing.SwingUtilities;
052:
053: import edu.rice.cs.drjava.DrJava;
054: import edu.rice.cs.drjava.model.GlobalModel;
055: import edu.rice.cs.drjava.model.FileMovedException;
056: import edu.rice.cs.drjava.model.OpenDefinitionsDocument;
057: import edu.rice.cs.drjava.model.repl.newjvm.MainJVM;
058: import edu.rice.cs.drjava.model.compiler.CompilerModel;
059: import edu.rice.cs.drjava.model.compiler.CompilerListener;
060: import edu.rice.cs.drjava.model.compiler.DummyCompilerListener;
061: import edu.rice.cs.drjava.model.definitions.InvalidPackageException;
062:
063: //import edu.rice.cs.util.ExitingNotAllowedException;
064: import edu.rice.cs.plt.io.IOUtil;
065: import edu.rice.cs.util.UnexpectedException;
066: import edu.rice.cs.util.classloader.ClassFileError;
067: import edu.rice.cs.util.text.SwingDocument;
068: import edu.rice.cs.util.swing.Utilities;
069:
070: import org.apache.bcel.classfile.*;
071:
072: import static edu.rice.cs.drjava.config.OptionConstants.*;
073: import static edu.rice.cs.plt.debug.DebugUtil.debug;
074:
075: /** Manages unit testing via JUnit.
076: * @version $Id: DefaultJUnitModel.java 4255 2007-08-28 19:17:37Z mgricken $
077: */
078: public class DefaultJUnitModel implements JUnitModel,
079: JUnitModelCallback {
080:
081: /** Manages listeners to this model. */
082: private final JUnitEventNotifier _notifier = new JUnitEventNotifier();
083:
084: /** RMI interface to a secondary JVM for running tests. Using a second JVM prevents interactions and tests from
085: * corrupting the state of DrJava.
086: */
087: private final MainJVM _jvm;
088:
089: /** The compiler model. It contains a lock used to prevent simultaneous test and compile. It also tracks the number
090: * errors in the last compilation, which is required information if junit forces compilation.
091: */
092: private final CompilerModel _compilerModel;
093:
094: /** The global model to which the JUnitModel belongs */
095: private final GlobalModel _model;
096:
097: /** The error model containing all current JUnit errors. */
098: private volatile JUnitErrorModel _junitErrorModel;
099:
100: /** State flag to prevent starting new tests on top of old ones */
101: private volatile boolean _testInProgress = false;
102:
103: /** State flag to record if test classes in projects must end in "Test" */
104: private boolean _forceTestSuffix = false;
105:
106: /** The document used to display JUnit test results. Used only for testing. */
107: private final SwingDocument _junitDoc = new SwingDocument();
108:
109: /** Main constructor.
110: * @param jvm RMI interface to a secondary JVM for running tests
111: * @param compilerModel the CompilerModel, used only as a lock to prevent simultaneous test and compile
112: * @param model used only for getSourceFile
113: */
114: public DefaultJUnitModel(MainJVM jvm, CompilerModel compilerModel,
115: GlobalModel model) {
116: _jvm = jvm;
117: _compilerModel = compilerModel;
118: _model = model;
119: _junitErrorModel = new JUnitErrorModel(new JUnitError[0],
120: _model, false);
121: }
122:
123: //-------------------------- Field Setters --------------------------------//
124:
125: public void setForceTestSuffix(boolean b) {
126: _forceTestSuffix = b;
127: }
128:
129: //------------------------ Simple Predicates ------------------------------//
130:
131: public boolean isTestInProgress() {
132: return _testInProgress;
133: }
134:
135: //------------------------Listener Management -----------------------------//
136:
137: /** Add a JUnitListener to the model.
138: * @param listener a listener that reacts to JUnit events
139: */
140: public void addListener(JUnitListener listener) {
141: _notifier.addListener(listener);
142: }
143:
144: /** Remove a JUnitListener from the model. If the listener is not currently listening to this model, this method
145: * has no effect.
146: * @param listener a listener that reacts to JUnit events
147: */
148: public void removeListener(JUnitListener listener) {
149: _notifier.removeListener(listener);
150: }
151:
152: /** Removes all JUnitListeners from this model. */
153: public void removeAllListeners() {
154: _notifier.removeAllListeners();
155: }
156:
157: //-------------------------------- Triggers --------------------------------//
158:
159: /** Used only for testing. */
160: public SwingDocument getJUnitDocument() {
161: return _junitDoc;
162: }
163:
164: /** Creates a JUnit test suite over all currently open documents and runs it. If the class file
165: * associated with a file is not a test case, it is ignored.
166: */
167: public void junitAll() {
168: junitDocs(_model.getOpenDefinitionsDocuments());
169: }
170:
171: /** Creates a JUnit test suite over all currently open documents and runs it. If a class file associated with a
172: * source file is not a test case, it will be ignored. Synchronized against the compiler model to prevent
173: * testing and compiling at the same time, which would create invalid results.
174: */
175: public void junitProject() {
176: LinkedList<OpenDefinitionsDocument> lod = new LinkedList<OpenDefinitionsDocument>();
177:
178: for (OpenDefinitionsDocument doc : _model
179: .getOpenDefinitionsDocuments()) {
180: if (doc.inProjectPath())
181: lod.add(doc);
182: }
183: junitOpenDefDocs(lod, true);
184: }
185:
186: /** Forwards the classnames and files to the test manager to test all of them; does not notify
187: * since we don't have ODD's to send out with the notification of junit start.
188: * @param qualifiedClassnames a list of all the qualified class names to test.
189: * @param files a list of their source files in the same order as qualified class names.
190: */
191: public void junitClasses(List<String> qualifiedClassnames,
192: List<File> files) {
193: Utilities.showDebug("junitClasses(" + qualifiedClassnames
194: + ", " + files);
195: synchronized (_compilerModel.getCompilerLock()) {
196:
197: // Check _testInProgress
198: if (_testInProgress)
199: return;
200:
201: List<String> testClasses;
202: try {
203: testClasses = _jvm.findTestClasses(qualifiedClassnames,
204: files);
205: } catch (IOException e) {
206: throw new UnexpectedException(e);
207: }
208:
209: // System.err.println("Found test classes: " + testClasses);
210:
211: if (testClasses.isEmpty()) {
212: nonTestCase(true);
213: return;
214: }
215: _notifier.junitClassesStarted();
216: try {
217: _jvm.runTestSuite();
218: } catch (Exception e) {
219: // System.err.println("Threw exception " + e);
220: _notifier.junitEnded();
221: _testInProgress = false;
222: throw new UnexpectedException(e);
223: }
224: }
225: }
226:
227: public void junitDocs(List<OpenDefinitionsDocument> lod) {
228: junitOpenDefDocs(lod, true);
229: }
230:
231: /** Runs JUnit on the current document. Forces the user to compile all open source documents before proceeding. */
232: public void junit(OpenDefinitionsDocument doc)
233: throws ClassNotFoundException, IOException {
234: debug.logStart("junit(doc)");
235: // new ScrollableDialog(null, "junit(" + doc + ") called in DefaultJunitModel", "", "").show();
236: File testFile;
237: try {
238: testFile = doc.getFile();
239: if (testFile == null) { // document is untitiled: abort unit testing and return
240: nonTestCase(false);
241: debug.logEnd("junit(doc): no corresponding file");
242: return;
243: }
244: } catch (FileMovedException fme) { /* do nothing */
245: }
246:
247: LinkedList<OpenDefinitionsDocument> lod = new LinkedList<OpenDefinitionsDocument>();
248: lod.add(doc);
249: junitOpenDefDocs(lod, false);
250: debug.logEnd("junit(doc)");
251: }
252:
253: /** Ensures that all documents have been compiled since their last modification and then delegates the actual testing
254: * to _rawJUnitOpenTestDocs. */
255: private void junitOpenDefDocs(
256: final List<OpenDefinitionsDocument> lod,
257: final boolean allTests) {
258: // If a test is running, don't start another one.
259:
260: // System.err.println("junitOpenDefDocs(" + lod + "," + allTests + ")");
261:
262: // Check_testInProgress flag
263: if (_testInProgress)
264: return;
265:
266: // Reset the JUnitErrorModel, fixes bug #907211 "Test Failures Not Cleared Properly".
267: _junitErrorModel = new JUnitErrorModel(new JUnitError[0], null,
268: false);
269:
270: if (_model.hasOutOfSyncDocuments(lod)
271: || _model.hasModifiedDocuments(lod)) {
272: /* hasOutOfSyncDocments(lod) can return false when some documents have not been successfully compiled; the
273: * granularity of time-stamping and the presence of multiple classes in a file (some of which compile
274: * successfully) can produce false reports. */
275: // System.err.println("Out of sync documents exist");
276: CompilerListener testAfterCompile = new DummyCompilerListener() {
277: @Override
278: public void compileEnded(File workDir,
279: List<? extends File> excludedFiles) {
280: final CompilerListener listenerThis = this ;
281: try {
282: if (_model.hasOutOfSyncDocuments(lod)
283: || _model.getNumCompErrors() > 0) {
284: if (!Utilities.TEST_MODE)
285: JOptionPane
286: .showMessageDialog(
287: null,
288: "All open files must be compiled before running a unit test",
289: "Must Compile All Before Testing",
290: JOptionPane.ERROR_MESSAGE);
291: nonTestCase(allTests);
292: return;
293: }
294: _rawJUnitOpenDefDocs(lod, allTests);
295: } finally { // always remove this listener after its first execution
296: SwingUtilities.invokeLater(new Runnable() {
297: public void run() {
298: _compilerModel
299: .removeListener(listenerThis);
300: }
301: });
302: }
303: }
304: };
305:
306: // Utilities.show("Notifying JUnitModelListener");
307: _notifier.compileBeforeJUnit(testAfterCompile);
308: }
309:
310: else
311: _rawJUnitOpenDefDocs(lod, allTests);
312: }
313:
314: /** Runs all TestCases in the document list lod; assumes all documents have been compiled. It finds the TestCase
315: * classes by searching the build directories for the documents. */
316: private void _rawJUnitOpenDefDocs(
317: List<OpenDefinitionsDocument> lod, boolean allTests) {
318: File buildDir = _model.getBuildDirectory();
319: // System.err.println("Build directory is " + buildDir);
320:
321: /** Open java source files */
322: HashSet<String> openDocFiles = new HashSet<String>();
323:
324: /** A map whose keys are directories containing class files corresponding to open java source files.
325: * Their values are the corresponding source roots.
326: */
327: HashMap<File, File> classDirsAndRoots = new HashMap<File, File>();
328:
329: // Initialize openDocFiles and classDirsAndRoots
330: // All packageNames should be valid because all source files are compiled
331:
332: for (OpenDefinitionsDocument doc : lod) /* for all nonEmpty documents in lod */{
333: if (doc.isSourceFile()) { // excludes Untitled documents and open non-source files
334: try {
335: File sourceRoot = doc.getSourceRoot(); // may throw an InvalidPackageException
336:
337: // doc has valid package name; add it to list of open java source doc files
338: openDocFiles.add(doc.getCanonicalPath());
339:
340: String packagePath = doc.getPackageName().replace(
341: '.', File.separatorChar);
342:
343: // Add (canonical path name for) build directory for doc to classDirs
344:
345: File buildRoot = (buildDir == null) ? sourceRoot
346: : buildDir;
347:
348: File classFileDir = new File(IOUtil
349: .attemptCanonicalFile(buildRoot),
350: packagePath);
351:
352: File sourceDir = (buildDir == null) ? classFileDir
353: : new File(IOUtil
354: .attemptCanonicalFile(sourceRoot),
355: packagePath);
356:
357: if (!classDirsAndRoots.containsKey(classFileDir)) {
358: classDirsAndRoots.put(classFileDir, sourceDir);
359: // System.err.println("Adding " + classFileDir + " with source root " + sourceRoot +
360: // " to list of class directories");
361: }
362: } catch (InvalidPackageException e) { /* Skip the file, since it doesn't have a valid package */
363: }
364: }
365: }
366:
367: // System.err.println("classDirs = " + classDirsAndRoots.keySet());
368:
369: /** set of dirs potentially containing test classes */
370: Set<File> classDirs = classDirsAndRoots.keySet();
371:
372: // System.err.println("openDocFiles = " + openDocFiles);
373:
374: /* Names of test classes. */
375: ArrayList<String> classNames = new ArrayList<String>();
376:
377: /* Source files corresonding to potential test class files */
378: ArrayList<File> files = new ArrayList<File>();
379:
380: /* Flag indicating if project is open */
381: boolean isProject = _model.isProjectActive();
382:
383: try {
384: for (File dir : classDirs) { // foreach class file directory
385: // System.err.println("Examining directory " + dir);
386:
387: File[] listing = dir.listFiles();
388:
389: // System.err.println("Directory contains the files: " + Arrays.asList(listing));
390:
391: if (listing != null) { // listFiles may return null if there's an IO error
392: for (File entry : listing) { /* for each class file in the build directory */
393:
394: // System.err.println("Examining file " + entry);
395:
396: /* ignore non-class files */
397: String name = entry.getName();
398: if (!name.endsWith(".class"))
399: continue;
400:
401: /* In projects, ignore class names that do not end in "Test" if FORCE_TEST_SUFFIX option is set */
402: if (_forceTestSuffix) {
403: String noExtName = name.substring(0, name
404: .length() - 6); // remove ".class" from name
405: int indexOfLastDot = noExtName
406: .lastIndexOf('.');
407: String simpleClassName = noExtName
408: .substring(indexOfLastDot + 1);
409: // System.err.println("Simple class name is " + simpleClassName);
410: if (isProject
411: && !simpleClassName
412: .endsWith("Test"))
413: continue;
414: }
415:
416: // System.err.println("Found test class: " + noExtName);
417:
418: /* ignore entries that do not correspond to files? Can this happen? */
419: if (!entry.isFile())
420: continue;
421:
422: // Add this class and the corrresponding source file to classNames and files, respectively.
423: // Finding the source file is non-trivial because it may be a language-levels file
424:
425: try {
426: JavaClass clazz = new ClassParser(entry
427: .getCanonicalPath()).parse();
428: String className = clazz.getClassName(); // get classfile name
429: // System.err.println("looking for source file for: " + className);
430: int indexOfDot = className.lastIndexOf('.');
431:
432: File rootDir = classDirsAndRoots.get(dir);
433:
434: /** The canonical pathname for the file (including the file name) */
435: String javaSourceFileName = rootDir
436: .getCanonicalPath()
437: + File.separator
438: + clazz.getSourceFileName();
439: // System.err.println("Full java source fileName = " + javaSourceFileName);
440:
441: /* The index in fileName of the dot preceding the extension ".java", ".dj0*, ".dj1", or ".dj2" */
442: int indexOfExtDot = javaSourceFileName
443: .lastIndexOf('.');
444: // System.err.println("indexOfExtDot = " + indexOfExtDot);
445: if (indexOfExtDot == -1)
446: continue; // RMI stub class files return source file names without extensions
447: // System.err.println("File found in openDocFiles = " + openDocFiles.contains(sourceFileName));
448:
449: /* Determine if this java source file was generated from a language levels file. */
450: String strippedName = javaSourceFileName
451: .substring(0, indexOfExtDot);
452: // System.err.println("Stripped name = " + strippedName);
453:
454: String sourceFileName;
455:
456: if (openDocFiles
457: .contains(javaSourceFileName))
458: sourceFileName = javaSourceFileName;
459: else if (openDocFiles.contains(strippedName
460: + ".dj0"))
461: sourceFileName = strippedName + ".dj0";
462: else if (openDocFiles.contains(strippedName
463: + ".dj1"))
464: sourceFileName = strippedName + ".dj1";
465: else if (openDocFiles.contains(strippedName
466: + ".dj2"))
467: sourceFileName = strippedName + ".dj2";
468: else
469: continue; // no matching source file is open
470:
471: File sourceFile = new File(sourceFileName);
472: classNames.add(className);
473: files.add(sourceFile);
474: // System.err.println("Class " + className + "added to classNames. File " + sourceFileName + " added to files.");
475: } catch (IOException e) { /* ignore it; can't read class file */
476: } catch (ClassFormatException e) { /* ignore it; class file is bad */
477: }
478: }
479: }
480: }
481: } catch (Exception e) {
482: // new ScrollableDialog(null, "UnexceptedExceptionThrown", e.toString(), "").show();
483: throw new UnexpectedException(e);
484: }
485: // finally {
486: // new ScrollableDialog(null, "junit setup loop terminated", classNames.toString(), "").show();
487: // }
488:
489: // synchronized over _compilerModel to ensure that compilation and junit testing are mutually exclusive.
490: // TODO: should we disable compile commands while testing? Should we use protected flag instead of lock?
491:
492: synchronized (_compilerModel.getCompilerLock()) {
493: /** Set up junit test suite on slave JVM; get TestCase classes forming that suite */
494: List<String> tests;
495: try {
496: tests = _jvm.findTestClasses(classNames, files);
497: } catch (IOException e) {
498: throw new UnexpectedException(e);
499: }
500:
501: if (tests == null || tests.isEmpty()) {
502: nonTestCase(allTests);
503: return;
504: }
505:
506: try {
507: /** Run the junit test suite that has already been set up on the slave JVM */
508: _notifier.junitStarted(); // notify listeners that JUnit testing has finally started!
509: // new ScrollableDialog(null, "junitStarted executed in DefaultJunitModel", "", "").show();
510: _jvm.runTestSuite();
511:
512: } catch (Exception e) {
513: // Probably a java.rmi.UnmarshalException caused by the interruption of unit testing.
514: // Swallow the exception and proceed.
515: _notifier.junitEnded(); // balances junitStarted()
516: _testInProgress = false;
517: throw new UnexpectedException(e);
518: }
519: }
520: }
521:
522: //-------------------------------- Helpers --------------------------------//
523:
524: //----------------------------- Error Results -----------------------------//
525:
526: /** Gets the JUnitErrorModel, which contains error info for the last test run. */
527: public JUnitErrorModel getJUnitErrorModel() {
528: return _junitErrorModel;
529: }
530:
531: /** Resets the junit error state to have no errors. */
532: public void resetJUnitErrors() {
533: _junitErrorModel = new JUnitErrorModel(new JUnitError[0],
534: _model, false);
535: }
536:
537: //---------------------------- Model Callbacks ----------------------------//
538:
539: /** Called from the JUnitTestManager if its given className is not a test case.
540: * @param isTestAll whether or not it was a use of the test all button
541: */
542: public void nonTestCase(final boolean isTestAll) {
543: // NOTE: junitStarted is called in a different thread from the testing thread. The _testInProgress flag
544: // is used to prevent a new test from being started and overrunning the existing one.
545: // Utilities.show("DefaultJUnitModel.nonTestCase(" + isTestAll + ") called");
546: _notifier.nonTestCase(isTestAll);
547: _testInProgress = false;
548: }
549:
550: /** Called to indicate that an illegal class file was encountered
551: * @param e the ClassFileObject describing the error.
552: */
553: public void classFileError(ClassFileError e) {
554: _notifier.classFileError(e);
555: }
556:
557: /** Called to indicate that a suite of tests has started running.
558: * @param numTests The number of tests in the suite to be run.
559: */
560: public void testSuiteStarted(final int numTests) {
561: _notifier.junitSuiteStarted(numTests);
562: }
563:
564: /** Called when a particular test is started.
565: * @param testName The name of the test being started.
566: */
567: public void testStarted(final String testName) {
568: _notifier.junitTestStarted(testName);
569: }
570:
571: /** Called when a particular test has ended.
572: * @param testName The name of the test that has ended.
573: * @param wasSuccessful Whether the test passed or not.
574: * @param causedError If not successful, whether the test caused an error
575: * or simply failed.
576: */
577: public void testEnded(final String testName,
578: final boolean wasSuccessful, final boolean causedError) {
579: _notifier.junitTestEnded(testName, wasSuccessful, causedError);
580: }
581:
582: /** Called when a full suite of tests has finished running.
583: * @param errors The array of errors from all failed tests in the suite.
584: */
585: public void testSuiteEnded(JUnitError[] errors) {
586: // new ScrollableDialog(null, "DefaultJUnitModel.testSuiteEnded(...) called", "", "").show();
587: _junitErrorModel = new JUnitErrorModel(errors, _model, true);
588: _notifier.junitEnded();
589: _testInProgress = false;
590: // new ScrollableDialog(null, "DefaultJUnitModel.testSuiteEnded(...) finished", "", "").show();
591: }
592:
593: /** Called when the JUnitTestManager wants to open a file that is not currently open.
594: * @param className the name of the class for which we want to find the file
595: * @return the file associated with the given class
596: */
597: public File getFileForClassName(String className) {
598: return _model.getSourceFile(className + ".java");
599: }
600:
601: /** Returns the current classpath in use by the JUnit JVM, in the form of a path-separator delimited string. */
602: public Iterable<File> getClassPath() {
603: return _jvm.getClassPath();
604: }
605:
606: /** Called when the JVM used for unit tests has registered. */
607: public void junitJVMReady() {
608:
609: if (!_testInProgress)
610: return;
611: JUnitError[] errors = new JUnitError[1];
612: errors[0] = new JUnitError(
613: "Previous test suite was interrupted", true, "");
614: _junitErrorModel = new JUnitErrorModel(errors, _model, true);
615: _notifier.junitEnded();
616: _testInProgress = false;
617: }
618: }
|