/*==========================================================================*\ | $Id: PlistJUnitResultFormatter.java,v 1.8 2012/02/05 22:07:25 stedwar2 Exp $ |*-------------------------------------------------------------------------*| | Copyright (C) 2006-2010 Virginia Tech | | This file is part of Web-CAT. | | Web-CAT is free software; you can redistribute it and/or modify | it under the terms of the GNU General Public License as published by | the Free Software Foundation; either version 2 of the License, or | (at your option) any later version. | | Web-CAT is distributed in the hope that it will be useful, | but WITHOUT ANY WARRANTY; without even the implied warranty of | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | GNU General Public License for more details. | | You should have received a copy of the GNU General Public License | along with Web-CAT; if not, write to the Free Software | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA | | Project manager: Stephen Edwards <edwards@cs.vt.edu> | Virginia Tech CS Dept, 660 McBryde Hall (0106), Blacksburg, VA 24061 USA \*==========================================================================*/ package net.sf.webcat.plugins.javatddplugin; import junit.framework.AssertionFailedError; import junit.framework.Test; import org.apache.tools.ant.taskdefs.optional.junit.JUnitTest; import org.apache.tools.ant.util.StringUtils; //------------------------------------------------------------------------- /** * A custom formatter for the ANT junit task that prints results * in plist format (Apple-style property lists). * * @author Stephen Edwards * @author Last changed by $Author: stedwar2 $ * @version $Revision: 1.8 $, $Date: 2012/02/05 22:07:25 $ */ public class PlistJUnitResultFormatter extends PerlScoringJUnitResultFormatter { //~ Constructors .......................................................... // ---------------------------------------------------------- /** * Default constructor. */ public PlistJUnitResultFormatter() { // Nothing to construct } //~ Public Methods ........................................................ // ---------------------------------------------------------- /** * @see JUnitResultFormatter#startTestSuite(JUnitTest) */ /** {@inheritDoc}. */ public void startTestSuite( JUnitTest suite ) { super.startTestSuite( suite ); currentSuite = suite; } // ---------------------------------------------------------- /** * @see TestListener#startTest(Test) */ /** {@inheritDoc}. */ public void startTest( Test test ) { super.startTest( test ); testPassed = true; } // ---------------------------------------------------------- /** * @see TestListener#endTest(Test) */ /** {@inheritDoc}. */ public void endTest( Test test ) { super.endTest( test ); if ( testPassed ) { formatTestResultAsPlist( test, null ); } } // ---------------------------------------------------------- /** * @see TestListener#addFailure(Test,AssertionFailedError) */ /** {@inheritDoc}. */ public void addFailure( Test test, AssertionFailedError t ) { super.addFailure( test, t ); testPassed = false; formatTestResultAsPlist( test, t ); } // ---------------------------------------------------------- /** * @see TestListener#addError(Test,Throwable) */ /** {@inheritDoc}. */ public void addError( Test test, Throwable error ) { super.addError( test, error ); testPassed = false; formatTestResultAsPlist( test, error ); } // ---------------------------------------------------------- /** * Escapes perl string interpolation characters. * @param text the input string to escape * @return the escaped version of text */ public String perlEscape( String text ) { if ( text == null ) { return null; } return text.replaceAll( "([@$%#\"\\\\])", "\\\\$1" ); } // ---------------------------------------------------------- /** * Produces a perl string literal from the given string. * @param text the input string to convert * @return the corresponding string literal */ public String perlStringLiteral(String text) { if (text == null) { return "''"; } return "'" + text.replaceAll( "'", "\\\\'" ) + "'"; } //~ Protected Methods ..................................................... /** A simple record storing information about a test outcome. */ protected static class TestResultDescriptor { /** The suite for this test case. */ public JUnitTest suite; /** The test that has completed. */ public Test test; /** An associated exception object, or null. */ public Throwable error; /** An error code. */ public int code; /** The associated error level. */ public int level; /** A priorty level (greater number == higher priority). */ public int priority = 0; /** A message associated with the exception object, if any. */ public String message; /** * Create a new descriptor. * @param suite * @param test * @param error * @param code * @param level * @param message */ public TestResultDescriptor( JUnitTest suite, Test test, Throwable error, int code, int level, String message ) { this.suite = suite; this.test = test; this.error = error; this.code = code; this.level = level; this.message = message; } } // ---------------------------------------------------------- /** * Generate a descriptor summarizing a test result. * @param test the test to print * @param error the exception produced by the test, or null * @return the descriptor */ protected TestResultDescriptor describe( Test test, Throwable error ) { int code = codeOf( error ); int level = levelOf( code ); String msg = null; if ( error != null ) { if ( level == 4 && error instanceof junit.framework.AssertionFailedError && error.getCause() != null ) { error = error.getCause(); } msg = error.getMessage(); if ( msg != null ) { msg = msg.replaceAll( "\"", "\\\"" ); if ( level > 2 ) { msg = error.getClass().getName() + ": " + msg; } } else { msg = error.getClass().getName(); } String testName = "" + test; // avoiding null dereference if ( level == 2 && testName.startsWith( "warning(" ) && msg.startsWith( "No tests found in " ) ) { code = 10; // No tests in test class } } return new TestResultDescriptor( currentSuite, test, error, code, level, msg ); } // ---------------------------------------------------------- /** * Format and print out a plist dictionary summarizing a test result. * @param test the test to print * @param error the exception produced by the test, or null */ protected void formatTestResultAsPlist( Test test, Throwable error ) { formatTestResultAsPlist( describe( test, error ) ); } // ---------------------------------------------------------- private void appendResults(char c) { testResultsPlist.append(c); testResultsPerlList.append(c); } // ---------------------------------------------------------- private void appendResults(String str) { testResultsPlist.append(str); testResultsPerlList.append(str); } // ---------------------------------------------------------- private void appendResultsQuotedValue(String str) { testResultsPlist.append('"'); testResultsPlist.append(str.replace("\"", "\\\"")); testResultsPlist.append('"'); testResultsPerlList.append('\''); testResultsPerlList.append(str.replace("'", "\\'")); testResultsPerlList.append('\''); } // ---------------------------------------------------------- private void appendResultsValue(int value) { appendResults(Integer.toString(value)); } // ---------------------------------------------------------- private void appendResultsLabel(String str) { testResultsPlist.append(str); testResultsPlist.append('='); testResultsPerlList.append('\''); testResultsPerlList.append(str); testResultsPerlList.append("'=>"); } // ---------------------------------------------------------- private void appendResultsValueSeparator() { testResultsPlist.append(';'); testResultsPerlList.append(','); if (debugFormat) appendResults("\n\t"); } // ---------------------------------------------------------- /** * Format and print out a plist dictionary summarizing a test result. * @param result */ protected void formatTestResultAsPlist(TestResultDescriptor result) { appendResults('{'); if (debugFormat) appendResults("\n\t"); appendResultsLabel("suite"); appendResultsQuotedValue(result.suite.getName()); appendResultsValueSeparator(); appendResultsLabel("test"); String testName = ""; if ( result.test != null ) { testName = result.test.toString(); int pos = testName.indexOf( "(" ); if ( pos >= 0 ) { testName = testName.substring( 0, pos ); } } appendResultsQuotedValue(testName); appendResultsValueSeparator(); appendResultsLabel("level"); appendResultsValue(result.level); appendResultsValueSeparator(); appendResultsLabel("code"); appendResultsValue(result.code); appendResultsValueSeparator(); appendResultsLabel("priority"); appendResultsValue(result.priority); appendResultsValueSeparator(); if (result.message != null) { appendResultsLabel("message"); appendResultsQuotedValue(result.message); appendResultsValueSeparator(); } appendResults("},"); if (debugFormat) appendResults("\n"); } // ---------------------------------------------------------- /** * @see PerlScoringJUnitResultFormatter#outputForSuite(StringBuffer,JUnitTest) */ /** {@inheritDoc}. */ protected void outputForSuite( StringBuffer buffer, JUnitTest suite ) { super.outputForSuite( buffer, suite ); buffer.append( "# Suite: " ); buffer.append( currentSuite.getName() ); buffer.append( StringUtils.LINE_SEP ); buffer.append( "$results->addToPlist( <<PLIST );"); buffer.append( StringUtils.LINE_SEP ); buffer.append( perlEscape( testResultsPlist.toString() ) ); buffer.append( StringUtils.LINE_SEP ); buffer.append( "PLIST"); buffer.append( StringUtils.LINE_SEP ); buffer.append( "$results->addToPerlList( <<PERLLIST );"); buffer.append( StringUtils.LINE_SEP ); buffer.append( perlEscape( testResultsPerlList.toString() ) ); buffer.append( StringUtils.LINE_SEP ); buffer.append( "PERLLIST"); buffer.append( StringUtils.LINE_SEP ); testResultsPlist.setLength( 0 ); } // ---------------------------------------------------------- /** * Look up the error code associated with an exception. * @param error the exception to code, or null * @return the code */ protected int codeOf( Throwable error ) { if ( error == null ) return 1; // First-pass code assignment is made by the code table int code = 0; for ( int i = 0; i < codeTable.length; i++ ) { if ( codeTable[i] != null ) { if (codeTable[i].isAssignableFrom(error.getClass()) || codeTable[i].getName().equals(error.getClass().getName())) { // error instanceof codeTable[i] code = i; break; } } } // If it is a test case failure, we cannot use the exception type // alone, so we must break down the message to refine the code if (error instanceof AssertionFailedError) { // Older JUnit 3.x-style errors // Also works with newer 4.x-style errors, because of the // adapters used by the ANT JUnit task if (error instanceof junit.framework.ComparisonFailure || error instanceof org.junit.ComparisonFailure || (error.getCause() != null && error.getCause() instanceof org.junit.ComparisonFailure)) { code = 2; } else { code = 13; StackTraceElement[] trace = error.getStackTrace(); String methodName = null; int pos = 0; if ( error.getCause() != null ) { // Then it is a 4.x-style error, wrapped by the ANT 1.7 // JUnit test adapter trace = error.getCause().getStackTrace(); } pos = findLast( trace, 0, "junit.framework.Assert" ); if ( pos < trace.length && trace[pos].getClassName().equals( "junit.framework.Assert" ) ) { methodName = trace[pos].getMethodName(); } else { pos = findLast( trace, 0, "student.TestCase" ); if ( pos < trace.length && trace[pos].getClassName().equals( "student.TestCase" ) ) { methodName = trace[pos].getMethodName(); } else { pos = findLast( trace, 0, "org.junit.Assert" ); if ( pos < trace.length && trace[pos].getClassName().equals( "org.junit.Assert" ) ) { methodName = trace[pos].getMethodName(); } } } if ( methodName != null ) { code = assertFailCodeOf( methodName ); // Next, check for a fuzzy equals pos++; if ( pos < trace.length && trace[pos].getClassName().equals( "net.sf.webcat.junit.Assert" ) ) { code = 10; } // Last, check for a custom assert pos = findFirst( trace, pos, currentSuite.getName() ); if ( pos < trace.length && trace[pos].getMethodName().startsWith( "assert" ) ) { // custom assert code = 11; } } else if ( pos < trace.length ) { // Must be a wrapped AssertionError from some other // code code = 29; } } } return code; } // ---------------------------------------------------------- /** * Look up the error level for a given error code. * @param code the code to look up * @return the corresponding error level */ protected int levelOf( int code ) { if ( code <= 1 ) return 1; else if ( code <= 13 ) return 2; else if ( code <= 28 ) return 3; else if ( code <= 33 ) return 4; else return 5; } // ---------------------------------------------------------- /** * Search a stack trace for the next occurrence of any method from * a given class. The search begins at the specified position, * and advances as long as stack trace elements are from other classes. * @param stack the stack trace to look in * @param pos the position in the stack trace array to start looking * @param className the class to look for * @return the index of the first call to any method in the specified * class. Returns length + 1 if no such call exists. */ protected int findFirst( StackTraceElement[] stack, int pos, String className ) { while ( pos < stack.length && !stack[pos].getClassName().equals( className ) ) { pos++; } return pos; } // ---------------------------------------------------------- /** * Search a stack trace for the deepest contiguous occurrence of any * method from a given class. The search begins at the specified position, * and advances as long as stack trace elements are located in the * named class. * @param stack the stack trace to look in * @param pos the position in the stack trace array to start looking * @param className the class to look for * @return the index of the deepest call of the contiguous block of * calls to any methods in the specified class. Returns pos if the * given stack trace location doesn't belong to the specified class. */ protected int findLast( StackTraceElement[] stack, int pos, String className ) { if ( stack[pos].getClassName().equals( className ) ) { pos++; while ( pos < stack.length && stack[pos].getClassName().equals( className ) ) { pos++; } pos--; } return pos; } // ---------------------------------------------------------- /** * Look up the error code for a given JUnit assert method name. * @param name the method name to look up * @return the corresponding error level */ protected int assertFailCodeOf( String name ) { for ( int i = 0; i < assertMethodTable.length; i++ ) { if ( assertMethodTable[i].equals( name ) ) { return i + 3; } } return assertMethodTable.length + 2; } //~ Instance/static variables ............................................. /** The current suite name. */ protected JUnitTest currentSuite = null; /** Records the status of the current test. */ protected boolean testPassed = true; /** Records the status of the current test. */ protected StringBuffer testResultsPlist = new StringBuffer(); /** Records the status of the current test. */ protected StringBuffer testResultsPerlList = new StringBuffer(); /** * If true, extra newlines and tabs will be produced in the plist output. * */ private static final boolean debugFormat = false; /** A lookup table for determining error codes. */ private static final String[] assertMethodTable = { "assertEquals", "assertFalse", "assertNotNull", "assertNotSame", "assertNull", "assertSame", "assertTrue", "assertFuzzyEquals", "custom assert", "fail" }; /** A lookup table for determining error codes. */ private static final Class<?>[] codeTable = { // Nothing matches the zero case, EVER! null, // 0 // Nothing matches the pass case null, // 1 // Test case failures org.junit.ComparisonFailure.class, // 2 null, // 3 null, // 4 null, // 5 null, // 6 null, // 7 null, // 8 null, // 9 null, // 10 null, // 11 null, // 12 junit.framework.AssertionFailedError.class, // 13 // The class above even works for JUnit 4.x, in the current ANT // JUnit task. // unchecked exceptions ArithmeticException.class, // 14 ClassCastException.class, // 15 java.util.ConcurrentModificationException.class, // 16 java.util.EmptyStackException.class, // 17 IllegalArgumentException.class, // 18 IllegalStateException.class, // 19 IndexOutOfBoundsException.class, // 20 java.util.MissingResourceException.class, // 21 NegativeArraySizeException.class, // 22 java.util.NoSuchElementException.class, // 23 NullPointerException.class, // 24 SecurityException.class, // 25 TypeNotPresentException.class, // 26 UnsupportedOperationException.class, // 27 RuntimeException.class, // 28 // Errors student.testingsupport.ReflectionSupport.ReflectionError.class, // 29 AssertionError.class, // 30 OutOfMemoryError.class, // 31 StackOverflowError.class, // 32 Error.class, // 33 // checked exceptions ClassNotFoundException.class, // 34 CloneNotSupportedException.class, // 35 java.security.GeneralSecurityException.class, // 36 IllegalAccessException.class, // 37 InstantiationException.class, // 38 java.io.IOException.class, // 39 java.text.ParseException.class, // 40 java.net.URISyntaxException.class, // 41 Exception.class, // 42 Throwable.class // 43 }; }