/*******************************************************************************
* Copyright (c) 2006-2008, Cloudsmith Inc.
* The code, documentation and other materials contained herein have been
* licensed under the Eclipse Public License - v 1.0 by the copyright holder
* listed above, as the Initial Contributor under such license. The text of
* such license is available at www.eclipse.org.
******************************************************************************/
package org.eclipse.buckminster.test.junit;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import javax.mail.internet.MimeUtility;
import junit.framework.AssertionFailedError;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestListener;
import junit.framework.TestResult;
import junit.framework.TestSuite;
import org.eclipse.buckminster.runtime.Logger;
import org.eclipse.buckminster.sax.ISaxableElement;
import org.eclipse.buckminster.sax.Utils;
import org.eclipse.buckminster.test.junit.TestCommand.TestLocationResolver.ResolutionException;
import org.eclipse.buckminster.test.junit.TestCommand.TestLocationResolver.TestSuiteDescriptor;
import org.eclipse.core.runtime.IProgressMonitor;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
/**
* <p>
* JUnit testing support for Buckminster.
* </p>
* <p>
* The class implements the actual execution of the test.
* </p>
*
* @author Michal R��i�ka
*/
public class TestRunner
{
/**
* <p>
* A test result holder helper class. The class implements the {@link org.eclipse.buckminster.sax.ISaxableElement}
* interface and is thus capable of emitting the result in a form of SAX event stream.
* </p>
*/
public static class TestSuiteResult implements TestListener, ISaxableElement
{
protected static class ExceptionWrapper implements ISaxableElement
{
/** the error element */
public static final String ERROR = "error";
/** the failure element */
public static final String FAILURE = "failure";
/** type attribute for error and failure elements */
public static final String ATTR_TYPE = "type";
/** message attribute for error and failure elements */
public static final String ATTR_MESSAGE = "message";
protected static char[] getStackTrace(Throwable t)
{
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw, true);
t.printStackTrace(pw);
pw.close();
StringBuffer buf = sw.getBuffer();
char[] target = new char[buf.length()];
buf.getChars(0, buf.length(), target, 0);
return target;
}
private String m_type;
private Throwable m_exception;
public ExceptionWrapper(String type, Throwable exception)
{
m_type = type;
m_exception = exception;
}
public String getDefaultTag()
{
return m_type;
}
public void toSax(ContentHandler receiver, String namespace, String prefix, String localName)
throws SAXException
{
String qName = Utils.makeQualifiedName(prefix, localName);
AttributesImpl attrs = new AttributesImpl();
String message = m_exception.getMessage();
char[] text = getStackTrace(m_exception);
if(message != null && message.length() > 0)
{
Utils.addAttribute(attrs, ATTR_MESSAGE, message);
}
Utils.addAttribute(attrs, ATTR_TYPE, m_exception.getClass().getName());
receiver.startElement(namespace, localName, qName, attrs);
receiver.characters(text, 0, text.length);
receiver.endElement(namespace, localName, qName);
}
}
/**
* <p>
* A {@link junit.framework.TestResult} subclass that is aware of being monitored by a
* {@link org.eclipse.core.runtime.IProgressMonitor}.
* </p>
*/
protected class MonitoredTestResult extends TestResult
{
@Override
public synchronized boolean shouldStop()
{
return super.shouldStop() || m_monitor.isCanceled();
}
}
protected static class OutputWrapper implements ISaxableElement
{
private String m_type;
private byte[] m_data;
public OutputWrapper(String type, byte[] data)
{
m_type = type;
m_data = data;
}
public String getDefaultTag()
{
return m_type;
}
public void toSax(ContentHandler receiver, String namespace, String prefix, String localName)
throws SAXException
{
String qName = Utils.makeQualifiedName(prefix, localName);
char[] encoded;
try
{
encoded = MimeUtility.encodeText(new String(m_data), null, null).toCharArray();
}
catch(UnsupportedEncodingException e)
{
throw new RuntimeException("Default charset not supported ?!?", e);
}
receiver.startElement(namespace, localName, qName, EMPTY_ATTRIBUTES);
receiver.characters(encoded, 0, encoded.length);
receiver.endElement(namespace, localName, qName);
}
}
/**
* <p>
* A single test case result holder helper class. The class implements the
* {@link org.eclipse.buckminster.sax.ISaxableElement} interface and is thus capable of emitting the result in a
* form of SAX event stream.
* </p>
*/
protected static class TestCaseResult implements ISaxableElement
{
/** the testcase element */
public static final String TESTCASE = "testcase";
/** classname attribute for testcase elements */
public static final String ATTR_CLASSNAME = "classname";
/** name attribute for testcase elements */
@SuppressWarnings("hiding")
public static final String ATTR_NAME = TestSuiteResult.ATTR_NAME;
/** time attribute for testcase elements */
@SuppressWarnings("hiding")
public static final String ATTR_TIME = TestSuiteResult.ATTR_TIME;
/** timestamp attribute for testcase elements */
@SuppressWarnings("hiding")
public static final String ATTR_TIMESTAMP = TestSuiteResult.ATTR_TIMESTAMP;
// back reference to the Test Object
protected Test m_test;
protected long m_startTimestamp;
protected long m_endTimestamp;
protected List<ISaxableElement> m_additionalInfo = new LinkedList<ISaxableElement>();
public TestCaseResult(Test test)
{
m_endTimestamp = m_startTimestamp = System.currentTimeMillis();
m_test = test;
}
public void addInfo(ISaxableElement child)
{
m_additionalInfo.add(child);
}
public String getDefaultTag()
{
return TESTCASE;
}
public Test getTest()
{
return m_test;
}
public void setEndTimestamp(long stamp)
{
if(stamp > m_startTimestamp)
m_endTimestamp = stamp;
}
public void toSax(ContentHandler receiver, String namespace, String prefix, String localName)
throws SAXException
{
String qName = Utils.makeQualifiedName(prefix, localName);
AttributesImpl attrs = new AttributesImpl();
Utils.addAttribute(attrs, ATTR_NAME, getTestCaseName(m_test));
// a TestSuite can contain Tests from multiple classes,
// even tests with the same name - disambiguate them.
Utils.addAttribute(attrs, ATTR_CLASSNAME, m_test.getClass().getName());
Utils.addAttribute(attrs, ATTR_TIMESTAMP, "" + m_startTimestamp);
Utils.addAttribute(attrs, ATTR_TIME, "" + ((m_endTimestamp - m_startTimestamp) / 1000.0));
receiver.startElement(namespace, localName, qName, attrs);
for(ISaxableElement info : m_additionalInfo)
{
info.toSax(receiver, namespace, prefix, info.getDefaultTag());
}
receiver.endElement(namespace, localName, qName);
}
}
/** the testsuites element for the aggregate document */
public static final String TESTSUITES = "testsuites";
/** constant for unnamed testsuites/cases */
public static final String UNKNOWN = "unknown";
/** the testsuite element */
public static final String TESTSUITE = "testsuite";
/** name attribute for testsuite and testcase elements */
public static final String ATTR_NAME = "name";
/** time attribute for testsuite and testcase elements */
public static final String ATTR_TIME = "time";
/** timestamp attribute for testsuite and testcase elements */
public static final String ATTR_TIMESTAMP = "timestamp";
/** errors attribute for testsuite elements */
public static final String ATTR_ERRORS = "errors";
/** failures attribute for testsuite elements */
public static final String ATTR_FAILURES = "failures";
/** tests attribute for testsuite elements */
public static final String ATTR_TESTS = "tests";
/** the system-err element */
public static final String SYSTEM_ERR = "system-err";
/** the system-out element */
public static final String SYSTEM_OUT = "system-out";
/**
* <p>
* Emits SAX events closing the results report document.
* </p>
*
* @param report
* the SAX handler to emit the events to
*/
public static void endReport(ContentHandler report) throws SAXException
{
report.endElement(null, null, TestSuiteResult.TESTSUITES);
report.endDocument();
}
/**
* <p>
* Emits SAX events opening the results report document.
* </p>
*
* @param report
* the SAX handler to emit the events to
*/
public static void startReport(ContentHandler report) throws SAXException
{
report.startDocument();
report.startElement(null, null, TestSuiteResult.TESTSUITES, ISaxableElement.EMPTY_ATTRIBUTES);
}
protected static String getTestCaseName(Test test)
{
try
{
Class<?> clazz = test.getClass();
Method getNameMethod;
try
{
getNameMethod = clazz.getMethod("getName");
}
catch(NoSuchMethodException e)
{
getNameMethod = clazz.getMethod("name");
}
if(getNameMethod.getReturnType() == String.class)
{
return (String)getNameMethod.invoke(test);
}
}
catch(Throwable e)
{
// ignore
}
return UNKNOWN;
}
/** The name of the testsuite. */
protected String m_testSuiteName;
/** The JUnit result of the test suite. */
protected TestResult m_testResult;
/** The timestamp of the start of the testsuite execution. */
protected long m_startTimestamp;
/** The timestamp of the end of the testsuite execution. */
protected long m_endTimestamp;
/** Map of running tests. */
protected Map<Test, TestCaseResult> m_runningTests = new HashMap<Test, TestCaseResult>();
/** Map of failed tests. */
protected Map<Test, TestCaseResult> m_failedTests = new HashMap<Test, TestCaseResult>();
/** Map of finished tests. */
protected Map<Test, TestCaseResult> m_finishedTests = new HashMap<Test, TestCaseResult>();
/** Throwable thrown by the test suite execution code. */
protected Throwable m_error;
/** System.out captured during the execution of the test suite. */
protected byte[] m_out;
/** System.err captured during the execution of the test suite. */
protected byte[] m_err;
/** A progress monitor monitoring the test suite execution. */
protected IProgressMonitor m_monitor;
public TestSuiteResult(String testSuiteName, IProgressMonitor monitor)
{
m_testSuiteName = testSuiteName;
m_monitor = monitor;
}
public void addError(Test test, Throwable t)
{
addException(test, ExceptionWrapper.ERROR, t);
}
public void addFailure(Test test, AssertionFailedError e)
{
addException(test, ExceptionWrapper.FAILURE, e);
}
public void addOutput(byte[] out, byte[] err)
{
m_out = out;
m_err = err;
}
public void endTest(Test test)
{
long timestamp = System.currentTimeMillis();
TestCaseResult result;
try
{
result = getTestCaseResult(test);
// if the test is already finished then ignore this event
if(result == null)
return;
result.setEndTimestamp(timestamp);
}
catch(NoSuchElementException e)
{
// Fix for bug #5637 - if a junit.extensions.TestSetup is
// used and throws an exception during setUp then startTest
// would never have been called
result = new TestCaseResult(test);
}
m_finishedTests.put(test, result);
}
public String getDefaultTag()
{
return TESTSUITE;
}
/**
* <p>
* Run the whole testsuite.
* </p>
*/
public void run(Test suite)
{
try
{
m_testResult = new MonitoredTestResult();
m_testResult.addListener(this);
m_startTimestamp = System.currentTimeMillis();
try
{
try
{
if(suite != null)
suite.run(m_testResult);
}
finally
{
if(!m_runningTests.isEmpty())
{
for(TestCaseResult result : m_runningTests.values())
{
// Note that this moves the test from running to failed
addError(result.getTest(), new Exception("Test not completed"));
}
throw new RuntimeException("Tests not completed");
}
}
}
finally
{
m_endTimestamp = System.currentTimeMillis();
for(TestCaseResult result : m_failedTests.values())
{
Test test = result.getTest();
m_failedTests.remove(test);
result.setEndTimestamp(m_endTimestamp);
m_finishedTests.put(test, result);
}
}
}
catch(Throwable t)
{
addError(t);
}
}
public void startTest(Test test)
{
long timestamp = System.currentTimeMillis();
TestCaseResult result = m_runningTests.get(test);
// just return if the test is already running
if(result != null)
return;
result = m_failedTests.get(test);
// if the test is failed then finish it
if(result != null)
{
m_failedTests.remove(test);
result.setEndTimestamp(timestamp);
m_finishedTests.put(test, result);
}
m_runningTests.put(test, new TestCaseResult(test));
}
public void toSax(ContentHandler receiver, String namespace, String prefix, String localName)
throws SAXException
{
if(m_testResult == null)
{
throw new IllegalStateException("Test suite not run: " + m_testSuiteName);
}
String qName = Utils.makeQualifiedName(prefix, localName);
AttributesImpl attrs = new AttributesImpl();
Utils.addAttribute(attrs, ATTR_NAME, m_testSuiteName);
Utils.addAttribute(attrs, ATTR_TESTS, "" + m_testResult.runCount());
Utils.addAttribute(attrs, ATTR_FAILURES, "" + m_testResult.failureCount());
Utils.addAttribute(attrs, ATTR_ERRORS, "" + m_testResult.errorCount());
Utils.addAttribute(attrs, ATTR_TIMESTAMP, "" + m_startTimestamp);
Utils.addAttribute(attrs, ATTR_TIME, "" + ((m_endTimestamp - m_startTimestamp) / 1000.0));
receiver.startElement(namespace, localName, qName, attrs);
for(TestCaseResult result : m_finishedTests.values())
{
result.toSax(receiver, namespace, prefix, result.getDefaultTag());
}
if(m_error != null)
{
ISaxableElement element = new ExceptionWrapper(ExceptionWrapper.ERROR, m_error);
element.toSax(receiver, namespace, prefix, element.getDefaultTag());
}
if(m_out != null)
{
ISaxableElement element = new OutputWrapper(SYSTEM_OUT, m_out);
element.toSax(receiver, namespace, prefix, element.getDefaultTag());
}
if(m_err != null)
{
ISaxableElement element = new OutputWrapper(SYSTEM_ERR, m_err);
element.toSax(receiver, namespace, prefix, element.getDefaultTag());
}
receiver.endElement(namespace, localName, qName);
}
protected void addError(Throwable t)
{
if(m_error == null)
{
m_error = t;
}
}
protected void addException(Test test, String type, Throwable t)
{
TestCaseResult result = m_runningTests.get(test);
// ignore this event if the test is not running
if(result == null)
return;
m_runningTests.remove(test);
result.addInfo(new ExceptionWrapper(type, t));
m_failedTests.put(test, result);
}
protected TestCaseResult getTestCaseResult(Test test) throws NoSuchElementException
{
TestCaseResult result = m_runningTests.get(test);
if(result != null)
{
m_runningTests.remove(test);
return result;
}
result = m_failedTests.get(test);
if(result != null)
{
m_failedTests.remove(test);
return result;
}
if(m_finishedTests.get(test) != null)
return null;
// throw an exception if the test is unknown
throw new NoSuchElementException();
}
}
@SuppressWarnings("unchecked")
protected static Class<? extends TestCase> getTestCaseClass(Class clazz)
{
return clazz;
}
protected ByteArrayOutputStream m_outStreamBuffer;
protected ByteArrayOutputStream m_errStreamBuffer;
protected PrintStream m_bufferedOut;
protected PrintStream m_bufferedErr;
protected PrintStream m_originalSystemOut;
protected PrintStream m_originalSystemErr;
protected PrintStream m_originalLoggerOut;
protected PrintStream m_originalLoggerErr;
public TestRunner()
{
m_outStreamBuffer = new ByteArrayOutputStream();
m_errStreamBuffer = new ByteArrayOutputStream();
}
/**
* <p>
* Executes the specified <code>testSuite</code>.
* </p>
*
* @param testSuite
* the test suite to execute
* @param monitor
* a monitor that monitors the execution (used only for testing if the cancellation of the test execution
* was requested by means of calling the {@link org.eclipse.core.runtime.IProgressMonitor#isCanceled()
* monitor.isCanceled()})
* @return <code>TestSuiteResult</code> holding the result of the <code>testSuite</code> execution.
*/
public TestSuiteResult run(TestSuiteDescriptor testSuite, IProgressMonitor monitor)
{
TestSuiteResult result = new TestSuiteResult(testSuite.getSuiteName(), monitor);
monitor.isCanceled();
Test suite = null;
bufferStreams();
try
{
try
{
Class<?> suiteClass = testSuite.getSuiteClass();
if((suiteClass.getModifiers() & (Modifier.INTERFACE | Modifier.ABSTRACT)) != 0)
{
throw new InstantiationException();
}
{
Method suiteMethod = null;
try
{
// Check if there is a suite method.
suiteMethod = suiteClass.getMethod("suite", new Class[0]);
}
catch(NoSuchMethodException e)
{
// No appropriate suite method found. We don't report any
// error here since it might be perfectly normal.
}
if(suiteMethod != null)
{
// If there is a suite method available, then try
// to extract the suite from it. If there is an error
// here it will be caught below and reported.
suite = (Test)suiteMethod.invoke(null);
if(suite == null)
throw new Exception("suite method returned null");
}
else
{
// Try to extract a test suite automatically; this
// will generate warnings if the class is not
// a suitable Test.
suite = new TestSuite(getTestCaseClass(suiteClass));
}
}
}
finally
{
result.run(suite);
}
}
catch(Throwable t)
{
result.addError((t instanceof ResolutionException)
? t.getCause()
: new Exception("Failed to create test suite", t));
}
unbufferStreams();
result.addOutput(m_outStreamBuffer.toByteArray(), m_errStreamBuffer.toByteArray());
return result;
}
protected void bufferStreams()
{
if(m_bufferedOut != null)
return;
m_outStreamBuffer.reset();
m_errStreamBuffer.reset();
m_bufferedOut = new PrintStream(m_outStreamBuffer);
m_bufferedErr = new PrintStream(m_errStreamBuffer);
m_originalSystemOut = System.out;
m_originalSystemErr = System.err;
m_originalLoggerOut = Logger.getOutStream();
m_originalLoggerErr = Logger.getErrStream();
System.setOut(m_bufferedOut);
System.setErr(m_bufferedErr);
Logger.setOutStream(m_bufferedOut);
Logger.setErrStream(m_bufferedErr);
}
protected void unbufferStreams()
{
if(m_bufferedOut == null)
return;
System.setOut(m_originalSystemOut);
System.setErr(m_originalSystemErr);
Logger.setOutStream(m_originalLoggerOut);
Logger.setErrStream(m_originalLoggerErr);
m_bufferedOut.close();
m_bufferedErr.close();
m_bufferedOut = null;
m_bufferedErr = null;
}
}