001: /*
002: *
003: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
004: *
005: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
006: *
007: * The contents of this file are subject to the terms of either the GNU
008: * General Public License Version 2 only ("GPL") or the Common
009: * Development and Distribution License("CDDL") (collectively, the
010: * "License"). You may not use this file except in compliance with the
011: * License. You can obtain a copy of the License at
012: * http://www.netbeans.org/cddl-gplv2.html
013: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
014: * specific language governing permissions and limitations under the
015: * License. When distributing the software, include this License Header
016: * Notice in each file and include the License file at
017: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
018: * particular file as subject to the "Classpath" exception as provided
019: * by Sun in the GPL Version 2 section of the License file that
020: * accompanied this code. If applicable, add the following below the
021: * License Header, with the fields enclosed by brackets [] replaced by
022: * your own identifying information:
023: * "Portions Copyrighted [year] [name of copyright owner]"
024: *
025: * Contributor(s):
026: *
027: * The Original Software is NetBeans. The Initial Developer of the Original
028: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
029: * Microsystems, Inc. All Rights Reserved.
030: *
031: * If you wish your version of this file to be governed by only the CDDL
032: * or only the GPL Version 2, indicate your decision by adding
033: * "[Contributor] elects to include this software in this distribution
034: * under the [CDDL or GPL Version 2] license." If you do not indicate a
035: * single choice of license, a recipient has the option to distribute
036: * your version of this file under either the CDDL, the GPL Version 2 or
037: * to extend the choice of license to its licensees as provided above.
038: * However, if you add GPL Version 2 code and therefore, elected the GPL
039: * Version 2 license, then the option applies only if the new code is
040: * made subject to such option by the copyright holder.
041: */
042:
043: package org.netbeans.xtest.testrunner;
044:
045: import java.io.File;
046: import java.io.IOException;
047: import java.io.PrintStream;
048: import java.io.PrintWriter;
049: import java.lang.reflect.Method;
050: import java.lang.reflect.Constructor;
051: import java.lang.reflect.InvocationTargetException;
052: import java.util.ArrayList;
053: import junit.framework.Test;
054: import junit.framework.TestCase;
055: import junit.framework.TestResult;
056: import junit.framework.TestSuite;
057: import org.netbeans.junit.Filter;
058: import org.netbeans.junit.NbTest;
059: import org.netbeans.junit.NbTestSuite;
060:
061: /**
062: * @author breh
063: */
064: public class JUnitTestRunner {
065:
066: // debug method for testing purposes - DEBUG should be set to false in production code
067: private static final boolean DEBUG = true;
068:
069: private static void debugInfo(final String message) {
070: if (DEBUG) {
071: System.out.println("JUnitTestRunner:" + message);
072: }
073: }
074:
075: // test suite method name - taken from junit.runner.BaseTestRunner (should
076: // we take it directly from this place ?
077: public static final String SUITE_METHODNAME = "suite";
078:
079: public static final String TESTRUNNER_PROPERTIES_FILENAME_KEY = "testlist";
080:
081: // exit states
082: public static final int OK = 0;
083: public static final int ERROR = -1;
084:
085: // our printwriter to which all messages are printed out
086: // currently this is just stdout;
087: private PrintWriter out;
088: //private PrintStream out = System.out;
089:
090: // test runner properties - it is better to use
091: // property file than to transfer parameters via command line,
092: // because on some OSes there is a very short command line length
093: private JUnitTestRunnerProperties runnerProperties;
094:
095: // result processor used for tracking this testrun
096: // currently set here, but we should allow user to
097: // set the processor by own selection
098: private JUnitTestListener[] resultProcessors;
099:
100: // custom classloader used for loading tests (if not defined,
101: // tests are loaded via Class.forName();
102: private ClassLoader testLoader;
103:
104: public JUnitTestRunner(ClassLoader testLoader, PrintStream out)
105: throws IllegalArgumentException, IOException {
106: this (loadJUnitTestRunnerProperties(), testLoader, out);
107: }
108:
109: /** Creates a new instance of JUnitTestRunner */
110: public JUnitTestRunner(JUnitTestRunnerProperties runnerProperties,
111: ClassLoader testLoader, PrintStream out)
112: throws IllegalArgumentException {
113:
114: //System.out.println("JUnitTestRunner: runnerProperties = "+runnerProperties);
115: this .runnerProperties = runnerProperties;
116: this .testLoader = testLoader;
117: if (out != null) {
118: this .out = new PrintWriter(out, true);
119: } else {
120: this .out = new PrintWriter(System.out, true);
121: }
122: // this should be placed elsewhere, just a hack for now
123: checkRunnerPropertiesValidity();
124: }
125:
126: /** Adds given listener to the beginning of an array of existing listeners.
127: */
128: protected void addResultProcessor(JUnitTestListener listener) {
129: if (resultProcessors == null) {
130: resultProcessors = new JUnitTestListener[] { listener };
131: } else {
132: JUnitTestListener[] newResultProcessors = new JUnitTestListener[resultProcessors.length + 1];
133: // add new listener at the beginning
134: newResultProcessors[0] = listener;
135: for (int i = 0; i < resultProcessors.length; i++) {
136: newResultProcessors[i + 1] = resultProcessors[i];
137: }
138: resultProcessors = newResultProcessors;
139: }
140: }
141:
142: // runtests - now always run setup/teardown methods (when available)
143: public void runTests() {
144: runTests(true);
145: }
146:
147: /** run the junit tests */
148: protected void runTests(boolean doSetup) {
149:
150: // add result processors
151: addResultProcessor(new ConsoleSummaryReporter(out));
152: if (getResultsDirectory() != null) {
153: addResultProcessor(new XMLReporter(getResultsDirectory()));
154: } else {
155: out
156: .println("! Results Directory is not available - not storing results to xml files");
157: }
158:
159: TestSuite[] suites = getTestSuites();
160:
161: if ((doSetup)
162: & (runnerProperties.getTestbagSetupClassName() != null)) {
163:
164: try {
165: callMethod(runnerProperties.getTestbagSetupClassName(),
166: runnerProperties.getTestbagSetupMethodName());
167: } catch (InvocationTargetException ite) {
168: out
169: .println("Testbag setup method call failed. Reason = "
170: + ite.getMessage());
171: ite.printStackTrace(out);
172: }
173: }
174:
175: // test whether we run testbag or just test suite
176: String testrunType = runnerProperties.getTestRunType();
177: /*
178: if (testrunType.equals(TESTRUN_TYPE_TESTBAG)) {
179: // possibly fire start testbag
180: // TBD !!!
181: }
182: */
183:
184: // start the each suite ....
185: for (int i = 0; i < suites.length; i++) {
186: TestSuite currentSuite = suites[i];
187: TestResult suiteResult = new TestResult();
188: // add listeners for the testsuite
189: addTestListeners(suiteResult);
190: // suite start
191: fireStartTestSuite(currentSuite);
192: // run the suite (NbTestSuites should have attached filter if applicable)
193: currentSuite.run(suiteResult);
194: // suite done
195: fireEndTestSuite(currentSuite, suiteResult);
196: }
197: /*
198: if (testrunType.equals(TESTRUN_TYPE_TESTBAG)) {
199: // possibly fire end testbag
200: // TBD !!!
201: }
202: */
203: if ((doSetup)
204: & (runnerProperties.getTestbagTeardownClassName() != null)) {
205: try {
206: callMethod(runnerProperties
207: .getTestbagTeardownClassName(),
208: runnerProperties.getTestbagTeardownMethodName());
209: } catch (InvocationTargetException ite) {
210: out
211: .println("Testbag teardown method call failed. Reason = "
212: + ite.getMessage());
213: ite.printStackTrace(out);
214: }
215: }
216: }
217:
218: public static JUnitTestRunnerProperties loadJUnitTestRunnerProperties()
219: throws IOException {
220: // get from the system properties
221: String propertiesFilename = System
222: .getProperty(TESTRUNNER_PROPERTIES_FILENAME_KEY);
223: if (propertiesFilename == null) {
224: throw new IOException(
225: "Cannot load testrunner properties file, because system property "
226: + TESTRUNNER_PROPERTIES_FILENAME_KEY
227: + " is not specified");
228: }
229: return JUnitTestRunnerProperties.load(propertiesFilename);
230: }
231:
232: //
233: // private methods
234: //
235:
236: private void checkRunnerPropertiesValidity()
237: throws IllegalArgumentException {
238: // TDB !!!!
239: }
240:
241: /** A fake test used to report error while loading a test class.
242: * It is used in getTestSuites() method.
243: */
244: public static class FakeTest extends TestCase {
245: private Throwable throwable;
246: private String message;
247:
248: public FakeTest(String name, String message, Throwable throwable) {
249: super (name);
250: this .throwable = throwable;
251: this .message = message;
252: }
253:
254: public void loadingSuites() throws Exception {
255: throw new Exception(message, throwable);
256: }
257: }
258:
259: // get all test suites to execute (in the case of nbtestsuites, also with filters)
260: TestSuite[] getTestSuites() {
261: // get all testnames from properties
262: String[] testNames = runnerProperties.getTestNames();
263: ArrayList testSuites = new ArrayList(testNames.length);
264: for (int i = 0; i < testNames.length; i++) {
265: final String testName = testNames[i];
266: try {
267: // get the testsuite and store it in hashmap
268: TestSuite testSuite = getTestSuiteForName(testName);
269: //out.println("XXX got test suite:"+testName+"/"+i);
270: if (testSuite instanceof NbTestSuite) {
271: //out.println("is NbTestSuite");
272: Filter.IncludeExclude[] includes = runnerProperties
273: .getTestFilterIncludes(i);
274: Filter.IncludeExclude[] excludes = runnerProperties
275: .getTestFilterExcludes(i);
276: Filter filter = getFilter(includes, excludes);
277: ((NbTestSuite) testSuite).setFilter(filter);
278: }
279: // now continue with filter
280: testSuites.add(testSuite);
281: } catch (final Throwable t) {
282: // It can be ClassNotFoundException, or ClassCastException when class
283: // doesn't implement Test interface or other Throwable.
284: t.printStackTrace();
285: // Create a fake suite which reports the Throwable.
286: TestSuite suite = new TestSuite("Critical Error");
287: suite.addTest(new FakeTest("loadingSuites",
288: "Error while loading " + testName + ": "
289: + t.getMessage(), t));
290: testSuites.add(suite);
291: break;
292: }
293: }
294: if (testSuites.isEmpty()) {
295: // Create a fake suite which reports error.
296: TestSuite suite = new TestSuite("Critical Error");
297: suite.addTest(new FakeTest("loadingSuites",
298: "No suites to be loaded. Check your config files.",
299: null));
300: testSuites.add(suite);
301: }
302: // return the array
303: return (TestSuite[]) (testSuites.toArray(new TestSuite[0]));
304: }
305:
306: // get testsuite for the supplied classname
307: TestSuite getTestSuiteForName(String className)
308: throws ClassNotFoundException {
309: // load test class via appropritate classloader
310: Class testClass = getClassForName(className);
311: // try to extract suite method
312: Method suiteMethod = null;
313: try {
314: suiteMethod = testClass.getMethod("suite", new Class[0]);
315: } catch (Exception e) {
316: // test suite() method not found - swallow the exception
317: // similarly as JUnit runners do
318: }
319: // now try to prepate the TestSuite
320: if (suiteMethod != null) {
321: // call suite method and see what happens
322: try {
323: Test aTest = (Test) (suiteMethod.invoke(null,
324: new Object[0]));
325: if (aTest instanceof TestSuite) {
326: // ok, we have the suite - everything is ok
327: TestSuite aTestSuite = (TestSuite) aTest;
328: if (aTestSuite.getName() == null) {
329: /*
330: String newSuiteName = testClass.getName();
331: */
332: String newSuiteName = className;
333: out.println("Suite name is null, installing "
334: + newSuiteName);
335: aTestSuite.setName(newSuiteName);
336: }
337: return aTestSuite;
338: } else {
339: // we need to construct or own suite
340: TestSuite suite;
341: /*
342: String suiteName = aTest.getClass().getName();
343: */
344: String suiteName = className;
345: if (aTest instanceof NbTest) {
346: // need to create NbTestSuite for NbTests -> we can use filter
347: suite = new NbTestSuite(suiteName);
348: } else {
349: // just plain Test - create TestSuite
350: suite = new TestSuite(suiteName);
351: }
352: // add the test we got and return it
353: suite.addTest(aTest);
354: return suite;
355: }
356: } catch (Exception e) {
357: // this is bad -> print out the exception
358: // and create suite from the class
359: // as the suite() method was not defined at all
360: out
361: .println("Caught Exception when tried to extract suite method from class "
362: + className);
363: e.printStackTrace(out);
364: out.println("Creating suite directly from the class.");
365: }
366: }
367: // suite method not found - let's create or own testsuite
368: // for the test class
369: if (NbTest.class.isAssignableFrom(testClass)) {
370: // need to create NbTestSuite for NbTests -> we can use filter
371: //out.println("XX: suite not found, but test implements NbTest : class name "+testClass.getName());
372: //TestSuite aSuite = new NbTestSuite(testClass.getName());
373: //aSuite.addTest(instintiateTestFromClass(testClass));
374: TestSuite aSuite = new NbTestSuite(testClass);
375: return aSuite;
376: } else {
377: //out.println("XX: suite not found, test does not implement NbTest : class name "+testClass.getName());
378: if (Test.class.isAssignableFrom(testClass)) {
379: //out.println("XX: suite not found, test at least implements Test");
380: // just plain Test - create TestSuite
381: //TestSuite aSuite = new TestSuite(testClass.getName());
382: //aSuite.addTest(instintiateTestFromClass(testClass));
383: TestSuite aSuite = new TestSuite(testClass);
384: return aSuite;
385: } else {
386: //
387: // there should be also added possibility to run tests which
388: // implements just Test or NbTest interfaces
389: // - this needs some more investigation .... !!!!!
390: //
391:
392: // this happend when a class which is not test is used as a test
393: throw new ClassCastException(
394: "Specified class: "
395: + testClass
396: + " is not assignable from junit.framework.Test "
397: + "interface - cannot run such test");
398: }
399: }
400: }
401:
402: // load test class via appropriate class loader
403: private Class getClassForName(String className)
404: throws ClassNotFoundException {
405: debugInfo("Loading class " + className);
406: if (testLoader == null) {
407: debugInfo("Using default classloader");
408: return Class.forName(className);
409: } else {
410: debugInfo("Using supplied classloader:"
411: + testLoader.toString());
412: return testLoader.loadClass(className);
413: }
414: }
415:
416: // call setup/teardown method in a setup object
417: private void callMethod(String className, String methodName)
418: throws InvocationTargetException {
419: try {
420: Class setupClass = getClassForName(className);
421: // try to find the method
422: Method setupMethod = setupClass.getMethod(methodName,
423: new Class[0]);
424: // instintiate the object
425: Object obj = setupClass.newInstance();
426: setupMethod.invoke(obj, new Object[0]);
427: } catch (ClassNotFoundException cnfe) {
428: throw new InvocationTargetException(cnfe,
429: "Cannot invoke setup/teardown method, because of ClassNotFoundException.");
430: } catch (InstantiationException ie) {
431: throw new InvocationTargetException(ie,
432: "Cannot invoke setup/teardown method, because of InstantiationException.");
433: } catch (NoSuchMethodException nsme) {
434: throw new InvocationTargetException(nsme,
435: "Cannot invoke setup/teardown method, because of NoSuchMethodException.");
436: } catch (IllegalAccessException iae) {
437: throw new InvocationTargetException(iae,
438: "Cannot invoke setup/teardown method, because of IllegalAccessException.");
439: }
440: }
441:
442: private String getClassnameFromFilename(String filename) {
443: String shortFilename;
444: if (filename.endsWith(".java")) {
445: shortFilename = filename
446: .substring(0, filename.length() - 5);
447: } else if (filename.endsWith(".class")) {
448: shortFilename = filename
449: .substring(0, filename.length() - 6);
450: } else {
451: shortFilename = filename;
452: }
453: // now convert separator chars to dots (fully qualified java name)
454: return shortFilename.replace(File.separatorChar, '.');
455: }
456:
457: private Test instintiateTestFromClass(Class testClass)
458: throws ClassCastException {
459: if (Test.class.isAssignableFrom(testClass)) {
460: // try the standard junit constructor with name parameter
461: try {
462: Constructor constructorWithString = testClass
463: .getConstructor(new Class[] { String.class });
464: // call the constructor
465: Test aTest = (Test) constructorWithString
466: .newInstance(new Object[] { testClass.getName() });
467: return aTest;
468: } catch (Exception e) {
469: // constructor not found -> search for a constructor without any parameter
470: // or instantiation was not succesfull
471: try {
472: Constructor simpleConstructor = testClass
473: .getConstructor(new Class[0]);
474: // call the constructor
475: Test aTest = (Test) simpleConstructor
476: .newInstance(new Object[0]);
477: return aTest;
478: } catch (Exception e2) {
479: // even this time we were not succesfull -> class is not suitable
480: // throw exception
481: throw new ClassCastException(
482: "Class "
483: + testClass
484: + " does not contain constructor with <String> parameter, or constructor without parameter at all. Cannot use this class for test.");
485: }
486:
487: }
488: // ok, constructor not found, try the normal constructor, without any parameter
489: } else {
490: throw new ClassCastException(
491: "Class "
492: + testClass
493: + " does not implement interface junit.framework.Test");
494: }
495: }
496:
497: // get Filter from include and exclude ArrayLists ...
498: private Filter getFilter(Filter.IncludeExclude[] includes,
499: Filter.IncludeExclude[] excludes) {
500: Filter filter = new Filter();
501: boolean includesSet = false;
502: if (includes != null) {
503: filter.setIncludes(includes);
504: includesSet = true;
505: }
506: if (excludes != null) {
507: filter.setExcludes(excludes);
508: includesSet = true;
509: }
510: if (includesSet) {
511: return filter;
512: } else {
513: return null;
514: }
515: }
516:
517: // get the results directoru
518: private File getResultsDirectory() {
519: String resultsDirname = runnerProperties.getResultsDirName();
520: if (resultsDirname != null) {
521: File resultsDir = new File(resultsDirname);
522: if (resultsDir.isDirectory()) {
523: // directory exists, just return it
524: return resultsDir;
525: } else {
526: // need to create one
527: if (resultsDir.mkdirs()) {
528: // directory created - return it
529: return resultsDir;
530: } else {
531: // directory cannot be created
532: // throw new IOException("Cannot create results directory "+resultsDirname);
533: return null;
534: }
535: }
536: } else {
537: return null;
538: }
539: }
540:
541: // add test listeners to testrun object
542: protected void addTestListeners(TestResult testResult) {
543: if (testResult == null)
544: throw new NullPointerException("testResult cannot be null");
545: for (int i = 0; i < resultProcessors.length; i++) {
546: testResult.addListener(resultProcessors[i]);
547: }
548: }
549:
550: // fire start test suite to all result processors
551: private void fireStartTestSuite(TestSuite testSuite) {
552: for (int i = 0; i < resultProcessors.length; i++) {
553: resultProcessors[i].startTestSuite(testSuite);
554: }
555: }
556:
557: // fire end test suite to all result processors
558: private void fireEndTestSuite(TestSuite testSuite,
559: TestResult suiteResult) {
560: for (int i = 0; i < resultProcessors.length; i++) {
561: resultProcessors[i].endTestSuite(testSuite, suiteResult);
562: }
563: }
564: }
|